Valkey 作为业界最快的 KV 数据库之一,在业界受到广泛使用和好评。在 1KB KV 场景下,单核性能超过 150K QPS,这也树立了良好的口碑。用户对它的预期同样很高 ——“快”,进一步来说是 “稳定的快”,不希望在任何场景下出现延迟抖动。然而,实际使用中往往不如预期:
实际的问题场景还要更加复杂,可能存在上述的场景的组合,甚至更多的问题场景还在这些枚举之外。自 Redis 时代开始,就逐步增强它的可观测性,然而它的覆盖、分析的场景终究比较有限,对于实际的问题解决只能提供有限的帮助。Vakley 的可观测性难在:
基于这几点,Valkey 需要一个高性能的(overhead 很低的)、不会对 valkey 自身带来性能大幅抖动、可以动态开关的 trace 机制,增强它的可观测性。于是,我(皮振伟 pizhenwei@github)和同事(李智强 artikell@github)一起开发了 Valkey 的 LTTng trace event 机制,并把它贡献到 Valkey 上游。
LTTng 是一个开源的系统级跟踪框架,专为 Linux 内核和用户空间应用程序设计,支持低开销、高精度的全系统事件追踪。它通过采用共享内存+RCU(Read-Copy-Update)同步机制,实现低锁竞争的数据写入,同时记录程序执行过程中的上下文信息(如函数调用、系统事件、线程调度等),进程异步回收已满的缓冲区数据,避免阻塞主程序,帮助开发者分析性能瓶颈、调试复杂问题。
例如,我们对所有的命令声明如下的格式(LTTng UST 八股文):
LTTNG_UST_TRACEPOINT_EVENT(
/* Tracepoint provider name */
valkey_commands,
/* Tracepoint name */
command_call,
/* Input arguments */
LTTNG_UST_TP_ARGS(
int, prot,
const char *, saddr,
const char *, daddr,
const char *, name,
uint64_t, duration
),
/* Output event fields */
LTTNG_UST_TP_FIELDS(
lttng_ust_field_enum(valkey_commands, valkey_conn_type_enum, int, enum_field, prot)
lttng_ust_field_string(saddr, saddr)
lttng_ust_field_string(daddr, daddr)
lttng_ust_field_string(name, name)
lttng_ust_field_integer(uint64_t, duration, duration)
)
)
同时,在 valkey/src/server.c 中嵌入如何的代码:
valkey_commands_trace(valkey_commands, command_call, connGetTypeId(c->conn), getClientPeerId(c), getClientPeerId(c), real_cmd->declared_name, duration);
其中,valkey_commands 是 LTTng trace event 的 provider,command_call 是 tracepoint。通过 lttng 命令行工具执行
lttng enable-event -u 'valkey_commands:command_call'
即可使能这个事件的追踪。经过实际测试,如果编译打开 LTTng 的支持,运行时每次都会都会执行这个函数,但是 overhead 很低,几乎可以忽略不计,是因为在函数入口的地方需要判断是否真的需要输出,在没有动态打开这个 tracepoint 的情况下,这里仅仅是 test 一个变量就返回。如果通过 lttng 命令动态打开这个 tracepoint,那么将会带来低于~7%的性能损失,然而在实际运行中,valkey 几乎不会跑满整个 CPU,那么这个损耗的比例将会更低,那么这个 overhead 是可以接受的。
同理,还支持了更多的 tracepoint,详情见
valkey/src/trace/README.md
| event | provider |
| -------------------------- | ----------------- |
| command_call | valkey_commands |
| rdb_unlink_temp_file | valkey_bgsave |
| fork | valkey_bgsave |
| while_blocked_cron | valkey_server |
| module_acquire_gil | valkey_server |
| command_unblocking | valkey_server |
| expire_del | valkey_db |
| active_defrag_cycle | valkey_db |
| eviction_del | valkey_db |
| eviction_lazyfree | valkey_db |
| eviction_cycle | valkey_db |
| expire_cycle | valkey_db |
| cluster_config_open | valkey_cluster |
| cluster_config_write | valkey_cluster |
| cluster_config_fsync | valkey_cluster |
| cluster_config_rename | valkey_cluster |
| cluster_config_dir_fsync | valkey_cluster |
| cluster_config_close | valkey_cluster |
| cluster_config_unlink | valkey_cluster |
| aof_write_pending_fsync | valkey_aof |
| aof_write_active_child | valkey_aof |
| aof_write_alone | valkey_aof |
| aof_write | valkey_aof |
| aof_fsync_always | valkey_aof |
| aof_fstat | valkey_aof |
| aof_rename | valkey_aof |
在开发的过程中,我们构造场景尝试效果:valkey-benchmark 多线程测试 valkey-server 的性能,同时另外的一个终端执行 keys 命令,valkey-benchmark 输出:
GET: rps=227428.0 (overall: 222756.2) avg_msec=0.114 (overall: 0.117)
GET: rps=225248.0 (overall: 223005.2) avg_msec=0.115 (overall: 0.117)
GET: rps=167474.1 (overall: 217942.2) avg_msec=0.193 (overall: 0.122) --> performance drop
GET: rps=220192.0 (overall: 218129.5) avg_msec=0.118 (overall: 0.122)
GET: rps=222868.0 (overall: 218493.7) avg_msec=0.117 (overall: 0.121)
其中的一秒(第三行)的输出中 RPS 大幅降低。同时我们分析对应的 trace event 可以看到:
[21:16:30.420997167] (+0.000004064) zhenwei valkey:command_call: { cpu_id = 6 }, { prot = "tcp", conn = "127.0.0.1:6379-127.0.0.
1:54668", name = "get", duration = 1 }
[21:16:30.421001262] (+0.000004095) zhenwei valkey:command_call: { cpu_id = 6 }, { prot = "tcp", conn = "127.0.0.1:6379-127.0.0.
1:54782", name = "get", duration = 1 }
[21:16:30.485562459] (+0.064561197) zhenwei valkey:command_call: { cpu_id = 6 }, { prot = "tcp", conn = "127.0.0.1:6379-127.0.0.1:54386", name = "keys", duration = 64551 } --> root cause
[21:16:30.485583101] (+0.000020642) zhenwei valkey:command_call: { cpu_id = 6 }, { prot = "tcp", conn = "127.0.0.1:6379-127.0.0.1:54522", name = "get", duration = 1 }
[21:16:30.485763891] (+0.000180790) zhenwei valkey:command_call: { cpu_id = 6 }, { prot = "tcp", conn = "127.0.0.1:6379-127.0.0.1:54542", name = "get", duration = 1 }
[21:16:30.485766451] (+0.000002560) zhenwei valkey:command_call: { cpu_id = 6 }, { prot = "tcp", conn = "127.0.0.1:6379-127.0.0.1:54438", name = "get", duration = 1 }
可以看到 keys 命令执行了 64551 us,考虑到 valkey-server 使用单线程运行,那么在执行 keys 命令期间,valkey-server 的多线程多连接上都会 阻塞,因此性能受到了大幅影响。详情见 valkey 的 commit
1941d28acd53c08335f85700ae8f8d1cf5cdc40c
Valkey 在 LTTng 的帮助下,预期可观测性将迎来史诗级升级,那种低频的、短暂的延迟抖动有可能会更加容易分析出来。未来更多的 tracepoint 加入,valkey 的可观测性能力将大幅帮助开发者和用户分析、解决问题!