☰
Memcached stats命令详解:从指标解读到故障排查实战
2026/9/29 16:39:19 网站建设 项目流程

先说说我为什么会写这个题。Memcached 用到现在的人不少,但真正把 stats 命令用明白的,说实话不多。很多人查缓存问题,第一反应是看命中率,盯着 get_hits 和 get_misses 两个数字看半天,然后不知道该干什么。其实 stats 这一条命令后面藏着的是一整套诊断体系,从内存 slab 分布到 LRU 驱逐情况,从连接状态到 item 过期回收,全都能在它的输出里找到线索。这篇就把我这几年来在生产环境里用 stats 排查问题的经验整理出来,从最基本的连接方式讲起,一直到每个字段项的含义、常用子命令的用法,以及我自己踩过的一些坑,尽量做到看完全篇就能直接上手。

1. stats 命令的基本玩法与连接方式

1.1 先解决怎么连上 Memcached

Memcached 本身没有像 MySQL 那样的客户端命令行,日常运维基本都是通过 telnet 建立 TCP 连接然后手动敲命令。当然,还有 nc(netcat)这类工具,本质上是一样的套路。

最常见的连接方式就一条命令:

telnet 127.0.0.1 11211

连接成功后会进入一个交互式会话,没有任何欢迎横幅,光标直接停在空行等你输入。这时候敲一个stats再回车,就能看到一大串STAT开头的输出,最后以END结束。

如果你不想进入交互模式,想直接一条命令拿结果,可以用 nc:

printf 'stats\r\n' | nc 127.0.0.1 11211

这里有个小细节:Memcached 的协议是文本行协议,命令必须以\r\n结尾。用 printf 的时候如果把\r漏了,有的版本能正常响应,有的版本会直接不鸟你。我为了这个折腾过好几次,后来统一用上面这种写法。

如果连接失败,常见原因无非这么几个:Memcached 没启动、监听端口不对、防火墙拦了、或者绑定的地址不是你想连的那个。启动 Memcached 时如果只写了-l 127.0.0.1,那远端机器肯定连不上,这个问题太常见了。查网络连通性的时候可以先用系统自带的 ping 确认主机通不通,再用 telnet 测端口通不通,这个思路跟排查其他 TCP 服务是一样的。注意我这里说的是测试端口连通性,跟你系统里用不用 telnet 客户端是两回事,很多服务器上没装 telnet 客户端,那就用 nc 来测。

1.2 基础 stats 命令与响应格式解读

执行stats后,输出大体是这个样子的:

STAT pid 2371 STAT uptime 812343 STAT time 1718000000 STAT version 1.6.18 STAT libevent 2.1.12-stable STAT pointer_size 64 STAT rusage_user 287.562500 STAT rusage_system 299.312500 STAT max_connections 1024 STAT curr_connections 512 STAT total_connections 300234 STAT rejected_connections 0 STAT connection_structures 520 STAT response_obj_oom 0 STAT response_obj_count 0 STAT curr_items 152345 STAT total_items 3456789 STAT evicted_active 23 STAT evicted_unfetched 10567 STAT evicted 19687 STAT evicted_time 154 STAT outofmemory 0 STAT tailrepairs 0 STAT reclaimed 876543 STAT expired_unfetched 65432 STAT crawler_reclaimed 4321 STAT lrutail_reflocked 12 STAT moves_to_cold 789123 STAT moves_to_warm 456789 STAT moves_within_lru 123456 STAT reclaimed_hashtable 3456 STAT direct_reclaims 234 STAT expired 56789 STAT get_hits 987654321 STAT get_misses 9080706 STAT get_expired 12345 STAT get_flushed 6789 STAT delete_misses 1234 STAT delete_hits 4567 STAT incr_misses 0 STAT incr_hits 0 STAT decr_misses 0 STAT decr_hits 0 STAT cas_misses 0 STAT cas_hits 0 STAT cas_badval 0 STAT touch_hits 0 STAT touch_misses 0 STAT auth_cmds 0 STAT auth_errors 0 STAT bytes_read 1234567890123 STAT bytes_written 456789012345 STAT limit_maxbytes 1073741824 STAT accepting_conns 1 STAT listen_disabled_num 0 STAT time_in_listen_disabled_us 0 STAT threads 4 STAT conn_yields 0 STAT hash_power_level 22 STAT hash_bytes 8388608 STAT hash_is_expanding 0 STAT slab_reassign_running 0 STAT slabs_moved 12 STAT slab_automove 1 STAT lru_crawler_running 0 STAT lru_crawler_starts 456 STAT lru_maintainer_thread 0 STAT malloc_fails 0 STAT log_worker_writes 0 STAT log_worker_written 456789 STAT log_worker_dropped 0 STAT log_watcher_skipped 0 STAT log_watcher_sent 345678 STAT bytes 734003200 STAT touch_requests 0 STAT thread_local_hits 0 STAT conn_yields 0 END

不同版本字段有差异,1.5 以后加入了expired、get_expired、moves_*这些与 LRU 维护线程相关的统计,1.6 又增加了log_*。不用背全部字段,但是要理解每一类字段的用途。我习惯把它分成四块来看:进程运行状态、数据与内存、命中与淘汰、连接与网络。下一节逐个拆。

2. 核心统计指标逐项拆解

2.1 服务运行与进程相关的指标

pid、uptime、version、time这几个属于最基础的运行态信息。uptime的单位是秒,用当前time减去uptime就能算出 Memcached 实例的启动时刻。这个在很多场景下非常有用,比如你怀疑缓存是不是半夜被重启过,不用去翻系统日志,跑一下 stats 自己算一遍就知道了。

threads表示启动时的 worker 线程数量,默认 4,这是-t参数决定的。如果线上请求量很大,你可能会看到 CPU 在多个核上都有消耗,但 Memcached 单实例的吞吐瓶颈其实很少在线程数上,更多是网络中断和内存分配。pointer_size是 64 表示这是 64 位编译版本,这个字段主要影响你对内存上限的预期,32 位版本下单个进程能管理的内存非常有限,生产环境尽量用 64 位。

rusage_user和rusage_system是进程累计消耗的用户态和内核态 CPU 时间,单位是秒。怎么用?比如你重启一个实例后过了一个小时再看这两个值,如果rusage_system涨得异常快,说明系统调用开销偏高,通常跟网络包处理有关,这时候去检查网卡中断或者连接数是否过高。

2.2 数据量与内存使用指标

curr_items是当前所有 slab class 里的 item 总数,total_items是实例启动以来累计写入的 item 数量,包括被淘汰的和过期的。这两个一对比就有意思了,如果total_items涨得飞快而curr_items纹丝不动,说明写入大量新 key 的同时也在大量淘汰旧 key,你的内存可能不够了。

bytes表示当前存储数据实际占用的内存字节数,不含管理结构开销。limit_maxbytes是你给 Memcached 配置的最大内存字节数,也就是启动参数-m的值乘以 1024 再乘以 1024。如果你配置的是 1G,那limit_maxbytes是 1073741824。那么实际使用率就是bytes / limit_maxbytes,这个比例才是你需要盯的核心指标,而不是光看curr_items。

这里有个容易踩的坑:bytes只算 item 的 key、value 以及部分元数据的存储占用,系统为每个 item 分配的内存块还有额外的 chunk 浪费,特别是 value 大小跟 chunk size 不匹配的时候,浪费可能超过 10%。所以实际 RSS 内存占用会明显高于bytes,你在操作系统里用ps看到的RES才是真实值,两者对不上是正常的,不用慌。

hash_power_level和hash_bytes是哈希表的信息。hash_bytes是哈希表本身占用的内存,hash_power_level是哈希表的大小等级,Memcached 会随着 key 数量增长自动扩容哈希表。如果看到hash_is_expanding为 1,说明正在扩容过程中,这时候 stats 的输出可能有一点抖动,属正常现象。哈希表占用的内存不在bytes里,所以也别拿bytes去核算内存上限。

2.3 命中率与淘汰策略指标

命中率是大家最关心的,公式很简单:命中率 = get_hits / (get_hits + get_misses)。这个结果的百分比就是你缓存的实际价值。如果命中率低于 80%,你就要想想是不是 key 设计有问题,或者过期时间设得太短,再或者数据访问局部性不强。

很多文章只讲 get_hits 和 get_misses,但现代 Memcached 的淘汰相关指标远比这两个重要。evicted是从 LRU 尾部逐出的 item 数量,evicted_time是最近一次驱逐的 item 距离现在多少秒,这个字段很有诊断价值。如果evicted_time很小,说明 LRU 正在高频驱逐,内存压力极大,缓存雪崩可能正在发生。evicted_active表示驱逐的 item 里有不少是最近被访问过的热数据,这比驱逐冷数据严重得多,说明内存已经连热数据都保不住了。evicted_unfetched表示被驱逐的 item 中有多少从未被 get 过,这个数很大倒是好事,说明驱逐的主要是垃圾数据,空间利用率不高。

reclaimed表示新写入的 item 复用了已过期 item 的内存空间,不用走 LRU 驱逐。这个数字大说明过期 key 处理得很有效率,对性能有利。expired_unfetched表示过期的 item 里有多少从未被读取过,这个数字越高,说明你写入了大量一次性数据,从设计角度可以考虑缩短过期时间或者换用更轻量的存储方案。

还有一个关键概念:Memcached 的懒惰过期机制。默认情况下,过期 item 不会立刻被后台线程清理,只有被 get 访问到时才会发现它已过期。所以你会发现get_misses里有一部分其实是get_expired,也就是 key 确实存在过,只是已经过期了。如果get_expired占比很高,说明大量请求在读取已经过期的 key,缓存命中率低是预期内的事,而不是故障。

2.4 网络与连接指标

curr_connections是当前打开的连接数,total_connections是实例启动以来累计建立的连接总数。rejected_connections是超出max_connections被拒绝的连接数,如果这个值持续增长,说明连接池配置得太大了,或者服务端连接数上限跟不上。

connection_structures是 Memcached 为每个连接分配的管理结构体数量,通常略大于curr_connections。如果这个值远大于curr_connections,说明有大量连接正处于关闭过程中,可能存在连接泄漏。listen_disabled_num是监听端口被暂时关闭的次数,Memcached 在达到max_connections后会自动关掉监听,等连接数降下来再重新开启。如果这个数一直在涨,说明连接数频繁触顶,你该动手调大-c参数或者优化客户端连接复用了。

bytes_read和bytes_written是累计的读写字节数。这两个值在排查询吞吐问题时有用,比如你怀疑带宽被打满,算一下(bytes_written - bytes_before) / 时间间隔就能得到实时写流量。很遗憾 stats 不直接提供速率字段,只能自己算差值。

3. stats 家族子命令:定位问题的利器

3.1 stats items 与 stats slabs:内存分配的真相

stats items输出每个 slab class 的 item 统计:

stats items STAT items:1:number 152 STAT items:1:age 1089 STAT items:1:mem_requested 14592 STAT items:1:evicted 0 STAT items:1:evicted_nonzero 0 STAT items:1:evicted_time 0 STAT items:1:outofmemory 0 STAT items:1:tailrepairs 0 STAT items:1:reclaimed 1324 STAT items:1:expired_unfetched 221 STAT items:1:evicted_unfetched 33 STAT items:1:crawler_reclaimed 456 STAT items:1:crawler_items_checked 10240 STAT items:1:crawler_items_wasted 12.5 STAT items:1:crawler_reclaimed_expired 432 ... END

items:1:number表示该 slab class 的 item 数,items:1:age表示这个 class 里最老 item 在缓存中存活的秒数。如果某个 class 的age特别大但evicted_time很小,说明这个 class 空间紧张,内部 LRU 一直在驱逐,而最老的 item 很久没有被访问也没被淘汰,这种状态往往意味着 key 分布不均匀。

stats slabs输出每个 slab class 的存储结构信息:

stats slabs STAT 1:chunk_size 96 STAT 1:chunks_per_page 10922 STAT 1:total_pages 1 STAT 1:total_chunks 10922 STAT 1:used_chunks 152 STAT 1:free_chunks 0 STAT 1:free_chunks_end 10770 STAT 1:mem_requested 14592 STAT 1:get_hits 404715 STAT 1:cmd_set 3459 STAT 1:delete_hits 0 STAT 1:incr_hits 0 STAT 1:decr_hits 0 STAT 1:cas_hits 0 STAT 1:cas_badval 0 STAT 1:touch_hits 0 STAT active_slabs 3 STAT total_malloced 327660 END

chunk_size是这个 class 每个内存块的大小,chunks_per_page是一页(默认 1MB)能切分多少个 chunk,total_pages是分配给该 class 的页数。used_chunks是当前使用的 chunk 数,free_chunks是已经被该 class 申请但没有被占用的 chunk 数,free_chunks_end是当前页末尾还没被切分使用的空间。如果free_chunks长期为 0 而free_chunks_end很大,说明这个 class 有可用空间但不够分配一整块,内存碎片化明显。

根因是 Memcached 的 slab 分配机制:内存按 1MB 的页划分,每页进一步切成固定大小的 chunk,不同 slab class 的 chunk 大小按增长因子递增。比如默认 1.25 倍增长,96、120、150、187……以此类推。一个 value 大小落在哪个区间,就会被放到对应 class。如果 class 太小装不下数据,Memcached 不会破块存储,而是寻找更大的 class,所以你会看到某些 class 的 chunk 大小远大于实际 value 体积,这就是内存浪费的主要来源。

3.2 stats cachedump 与 stats settings:查 key 和看配置

stats cachedump按 slab class 导出该 class 的 item 列表,格式是:

stats cachedump 1 100 ITEM some_key [96 bytes; 1717999999 s] ITEM another_key [64 bytes; 1717999950 s] END

括号里前面是 item 占用的字节数,后面是最近访问时间的时间戳。但注意两点:第一,cachedump输出不保证完整,它走的是 LRU 链遍历,生产环境大实例下可能只输出部分;第二,在新版本中这个命令已经被标记为调试用途,官方并不推荐在线上依赖它。想确认某个 key 是否存在,直接用get key更靠谱。

stats settings输出当前实例运行参数,相当于把启动参数都回显一遍:

stats settings STAT maxbytes 1073741824 STAT maxconns 1024 STAT tcpport 11211 STAT udpport 11211 STAT verbosity 0 STAT oldest 0 STAT evictions on STAT domain_socket NULL STAT umask 700 STAT growth_factor 1.25 STAT chunk_size 96 STAT num_threads 4 STAT num_threads_per_udp 4 STAT stat_key_prefix : STAT detail_enabled no STAT reclaim 1 STAT hashpower_init 0 STAT item_size_max 1048576 STAT slab_chunk_size_max 524288 STAT max_item_size 1048576 STAT requests_per_event 50 STAT cas_enabled 1 STAT tcp_backlog 1024 STAT auth_enabled_sasl no STAT maxconns_fast 1 STAT slab_reassign 1 STAT slab_automove 1 STAT lru_crawler 1 STAT lru_crawler_sleep 100 STAT lru_crawler_tocrawl 0 STAT lru_maintainer 1 STAT lru_maintainer_sleep 0 STAT hot_lru_pct 20 STAT warm_lru_pct 40 STAT lru_segmented 1 STAT temporary_ttl 0 STAT idle_timeout 0 STAT watcher_logbuf_size 262144 STAT worker_logbuf_size 65536 STAT track_sizes 0 STAT wait_failover_time 0 END

排障第一步先看stats settings,确认当前实例的启动参数跟你预期一致。我曾经遇到过一台服务器上起了两个 Memcached 进程,一个旧配置一个小内存,负载均衡把请求分发到了小内存那个,命中率怎么调都上不去,最后就是通过stats settings里的maxbytes发现的。这里也推荐你记一下启动命令,或者部署时统一用 systemd unit 文件,避免两套 Memcached 配置混淆。

3.3 各子命令实战场景对比

这几个子命令的侧重点完全不同,用哪个取决于你想回答什么问题。为了不让你到用的时候现翻,我列一个对照表:

命令核心信息典型使用场景
stats全局统计指标日常巡检、命中率计算、内存水位监控
stats items每个 slab class 的 item 数量与驱逐情况定位热 key 导致的 LRU 驱逐、检查过期回收效率
stats slabschunk 大小、内存页分配、碎片情况分析内存浪费、调整增长因子或 item 大小上限
stats cachedump指定 class 的 key 列表查看某个 class 里有哪些 key、检查 key 分布
stats settings实例运行参数确认配置、排查配置变更问题
stats reset清空部分计数统计长时间运行后重置计数器,便于观察新周期
stats sizesitem 大小分布直方图需要确认 value 大小分布时使用

这里单独说一下stats sizes,这个子命令输出一个直方图,格式类似:

STAT 96 15234 STAT 120 4500 STAT 150 233 ... END

但它有个前提:启动时要加-o track_sizes,否则输出是空的,或者提示不支持。我在生产环境一直开着这个选项,因为它对分析缓存适合度很有帮助。如果某个区间堆积了大量 item,而你的应用实际上只需要很小的 value,那说明序列化之后的数据比预期大,可能是把整个对象硬塞进去而没有做精简。

4. 基于 stats 的典型故障排查实录

4.1 缓存命中率持续走低

命中率低最常见的表象是get_misses的速率明显高于get_hits。但具体原因要看场景。有一次我遇到的情况是命中率从 95% 掉到 60%,跑 stats 发现curr_items没怎么变化,但expired_unfetched涨得厉害。一问开发才知道,他们把缓存过期时间统一设成了 5 分钟,而业务的突发流量每 4 分钟一波。由于过期时间设计不合理,大量 key 在刚过期后马上被重新请求,全部触发 miss,然后回源数据库。

排查思路可以这样走:先看get_misses和get_expired的占比。如果get_expired占比高,就是过期策略问题,调整 TTL 或者加一点随机抖动。如果get_misses高但get_expired不高,再去看evicted_time。如果evicted_time很新,说明是内存不足导致的驱逐 miss。还有一种情况是客户端发送的 key 集合变化太快,比如每次请求都拼接带时间戳的 key,这种不是调参能解决的,要从业务侧下手。

4.2 内存逐出激增导致缓存雪崩

内存逐出是所有 Memcached 运维必须警惕的问题。evicted不断上涨说明新数据一直在抢占旧数据的位置。比较危险的是evicted_active持续走高,这表示被逐出的 item 里有很大比例是最近还在被访问的数据,相当于热数据被垃圾数据挤掉了,后续请求又会把这些热数据重新写回来,形成反复驱逐。

我遇到过一次典型的雪崩场景:某大促活动上线后,大量一次性优惠券 key 打入缓存,把原本的商品缓存全部挤出了。当时 stats 里evicted十分钟内增长了十几万,evicted_active占比超过 30%,同时业务方反馈响应时间暴涨,数据库压力直接打满。解决思路有几层:第一层是业务侧优化,一次性 key 不要放进全局缓存,或者使用更短的 TTL;第二层是 Memcached 层面调大内存、优化 slab 分配;第三层是在代理或网关层对非核心 key 做限流,避免瞬时写入冲垮缓存。stats items可以帮你精确到是哪个 slab class 在被驱逐,从而反推那条链路的数据。

4.3 连接数异常与并发瓶颈

连接问题通常先从curr_connections和rejected_connections看起。有一次线上服务报警,Memcached 的rejected_connections开始增长,listen_disabled_num也在涨。进一步看是因为客户端连接池设置的连接数总和超过了服务端max_connections。单个客户端看起来没多少连接,但几十个服务实例乘起来就破千了。

处理办法不是简单粗暴调大-c,这只能缓解一时。更合理的做法是让客户端连接池保持长连接并限制空闲连接回收,同时检查服务实例数量是否被过度水平扩展。connection_structures如果明显高于curr_connections,说明 TIME_WAIT 状态的连接没有被及时回收,可以看下系统层的 TCP 参数,比如tcp_tw_reuse相关的设置,但这属于系统调优范畴了,不要随便在线上乱开。

还有一个很容易忽略的点:conn_yields表示单个 worker 线程在处理一个连接时主动让出 CPU 的次数,这个值很高说明有大 value 的请求阻塞了事件循环。当某个 value 超大(接近item_size_max默认 1MB)时,读写该 value 会长时间占用 worker 线程,其它连接上的请求只能排队等待。这种问题在 stats 中直接看不到,但conn_yields持续增长是重要信号,一旦发现就要去排查应用里有没有存大对象。

5. 实战脚本与监控集成经验

5.1 一行命令快速观测核心指标

命令行手工敲 stats 适合临时排查,日常巡检最好还是写个小脚本。我自己的习惯是把下面这个命令存成 shell 函数,用到的时候直接跑:

mem_stats() { local host=${1:-127.0.0.1} local port=${2:-11211} printf 'stats\r\n' | nc "$host" "$port" | awk \ '/STAT (curr_items|total_items|bytes|limit_maxbytes|get_hits|get_misses|evicted|evicted_time|curr_connections|reclaimed|expired_unfetched)/ {print}' }

输出大概是这样:

STAT curr_items 152345 STAT total_items 3456789 STAT bytes 734003200 STAT limit_maxbytes 1073741824 STAT get_hits 987654321 STAT get_misses 9080706 STAT evicted 19687 STAT evicted_time 154 STAT curr_connections 512 STAT reclaimed 876543 STAT expired_unfetched 65432

如果想计算命中率和内存使用率,可以写一段更完整的脚本。要注意的是 stats 输出里有小数和整数混在一起,awk 计算时直接用浮点除法就行,最后再用printf格式化两位小数。

mem_summary() { local host=${1:-127.0.0.1} local port=${2:-11211} local data data=$(printf 'stats\r\n' | nc "$host" "$port") local hits misses bytes limit hits=$(echo "$data" | awk '/^STAT get_hits /{print $3}') misses=$(echo "$data" | awk '/^STAT get_misses /{print $3}') bytes=$(echo "$data" | awk '/^STAT bytes /{print $3}') limit=$(echo "$data" | awk '/^STAT limit_maxbytes /{print $3}') awk -v h="$hits" -v m="$misses" -v b="$bytes" -v l="$limit" 'BEGIN { rate = (h + m) > 0 ? h / (h + m) * 100 : 0 mem = l > 0 ? b / l * 100 : 0 printf "命中率: %.2f%%\n", rate printf "内存使用率: %.2f%%\n", mem }' }

5.2 命中率计算的完整公式与临界值

很多人只算get_hits / (get_hits + get_misses),但在 Memcached 的统计里,get_misses包含get_expired和get_flushed。如果你只想看“有效 miss”,可以进一步细分。不过实际排障我一般先看总命中率,再单独看get_expired的绝对数值。假设get_hits是 900,get_misses是 100,其中get_expired占 60,那么纯正的 key 不存在 miss 只有 40。这种情况下,你该优化的是过期时间策略,而不是数据加载逻辑。

什么算健康?我的经验值:读多写少的缓存场景,命中率长期维持在 95% 以上才算正常;数据频繁过期或写入量很大的场景,80% 到 90% 也可以接受;低于 70% 基本能断定缓存设计有问题,需要重点排查 key 的复用度和 TTL 设置。注意这只是一般经验,具体业务模型不同会有波动,最好以基线数据为准。

监控上还有一个容易被忽略的指标:curr_items与limit_maxbytes的比值。如果每个 item 平均占用很小但数量巨大,可能导致哈希表膨胀、遍历 LRU 变慢。反过来如果 item 数量不多但单个价值很大,内存浪费就成了主要矛盾。这时用stats sizes看分布最直观。

5.3 监控告警的合理阈值设置

接入监控系统的时候,不建议直接对原始值设阈值,因为get_hits这类累计计数器跟运行时长强相关,涨上天也不代表有问题。更好的做法是把它们转成速率或者比率再告警。我在 Prometheus 体系里通常采集这么几个指标:

  • 命中率低于阈值持续 5 分钟
  • 内存使用率超过 90% 并且继续上升
  • evicted_active速率突增,比如过去 5 分钟的驱逐量超过基线 3 倍
  • rejected_connections大于 0
  • listen_disabled_num大于 0
  • curr_connections超过max_connections的 80%

导入 Prometheus 最省事的办法是直接用memcached_exporter,它本质上就是在后台不断调用 stats 命令把指标拉出来,然后转换成监控指标。如果你已经有现成的二进制监控方案,也可以自己在客户端里定时执行stats解析上报,效果一样。

告警阈值怎么定?我建议先观察一周的正常业务水位,把日均峰值和低谷记下来,阈值设在峰值的 1.5 倍到 2 倍之间。比如连接数正常峰值为 400,告警线可以设在 600 到 800,低于这个值容易误报,高于这个值又可能错过隐患。另外,驱逐量的告警要结合业务大促等场景动态调整,固定阈值在活动期间基本天天误报。

注意:stats reset会把大部分累计计数归零,生产环境慎用。如果你确实需要一段干净的数据来观察问题,我建议挑业务低峰期执行,并事先告知相关同事,避免影响监控系统的历史曲线。

6. 进阶使用技巧与版本变化提醒

6.1 通过文本协议手工模拟 stats 调用

有时候线上环境没有 nc、没有 telnet 客户端,甚至没有完整的 Python 环境。这时候可以用 bash 的/dev/tcp特性,部分发行版默认支持:

exec 3<>/dev/tcp/127.0.0.1/11211 printf 'stats\r\n' >&3 cat <&3 exec 3<&- exec 3>&-

这段脚本做的事和 nc 完全一样,只是走的是 bash 内置的伪设备。注意某些安全加固过的系统会禁用/dev/tcp,那就换其他方式。C 语言或者 Go 里实现起来也不难,关键是记得\r\n结尾和读到END才算响应完成。

6.2 版本差异导致的字段变化

Memcached 演进过程中,stats 输出一直在变。1.4 时代还没有expired_unfetched、evicted_active这些细分字段,1.5 引入 LRU 维护线程后增加了一大批lru_*和moves_*相关统计,1.6 又加了log_*字段。如果你在网上搜到一篇老文章,对不上字段,先确认一下版本,不要照搬。

stats items里比较新的字段是crawler_reclaimed_expired和crawler_items_checked,它们来自后台 LRU crawler 线程对过期 item 的清理。crawler_reclaimed表示 crawler 线程实际回收的过期 item 数量。这个数字如果一直在涨,说明系统有好多过期 item 在等待被清理,配合lru_crawler_running为 1 表示当前 crawler 正在工作,这是正常现象。

6.3 用 stats 分析真实业务场景

举一个实际发生过的例子:某个服务的 value 是用户信息 JSON,大小在 200 字节到 2KB 之间。上线一段时间后发现内存使用率很高,但命中率正常。我用stats slabs看了一下,发现大量 item 集中在 chunk size 为 1024 的 class 里,而实际 value 平均只有 300 字节,这意味着每个 item 浪费了约 70% 的内存。原因在于增长因子 1.25 的阶梯太宽,200 到 300 字节的 value 会直接跳到 384 甚至 512 的 chunk,而如果调整增长因子到 1.15,这个区间会更平滑,但也不是越小越好,因为 slab class 数量有限,增长因子过小会导致每个 class 的 chunk 大小差异不明显,反而让大 value 需要跨多个 class 找位置。最终我们选择保留默认增长因子,但业务侧做了 value 压缩和字段精简,把平均体积压了下来,内存使用率立刻降了 20%。

再举一个:应用报错说缓存读取超时,我用 stats 看curr_connections正常,conn_yields却高得离谱。进一步排查发现有个接口会把一张几 MB 的图片 base64 后塞进缓存,远超默认的 1MBitem_size_max,写入请求一直被拒绝,而且每次尝试写入都引发了 worker 线程的长时间阻塞。最后改成了图片对象存储,缓存只存 URL,问题彻底解决。

7. 一些个人使用经验与避坑建议

写到这里基本上把 stats 命令的每个角度都过了一遍,最后再分享几个我个人的使用习惯。

第一,stats 输出里的time和uptime看似无用,其实非常关键。我遇到过两次诡异问题:一次是缓存命中率在某个时间点突然暴跌,查遍代码没找到原因,后来发现是 devops 平台自动重启了 Memcached;另一次是两台机器上的 Memcached 版本不一致,导致行为差异,用version字段一下就定位了。养成看到 stats 先扫一眼这几个基础字段的习惯,能省很多事。

第二,不要依赖stats cachedump去做 key 遍历。这个命令在大数据量下效率差,而且输出不完整,还会给实例增加额外负担。真要遍历 key,用lru_crawler相关的调试手段或者直接在设计层面记录 key 前缀列表,都比在线扫描稳妥。

第三,每次变更完配置后都要重新跑一遍 stats 确认。比如调大-m后,limit_maxbytes的数值是否更新;调整连接数后,stats settings里的maxconns是否生效。Memcached 有些参数是启动时一次性加载的,热改接口有限,别想当然以为改了启动参数重启就完事,重启前做好数据失效预案。

最后,把 stats 的关键指标沉淀成基线。第一次部署完 Memcached 后,记录正常运行时的命中率、连接数、内存使用率、驱逐情况,之后每次排障先跟基线对比。没有基线的监控只能说是在碰运气。有了基线,很多问题一眼就能看出偏离,比临时查文档高效得多。

Memcached 不复杂,但 stats 命令给了你一个非常完整的内部视野。多花点时间把这几十个字段的含义吃透,再结合命令行的灵活组合,很多看起来玄乎的缓存问题,几分钟内就能理出头绪。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询