Memcached
一、是什么
1. Memcached 的准确定位
Memcached 是面向分布式系统的高性能内存 Key-Value 缓存。它采用事件驱动网络模型和多工作线程处理请求,主要保存可以从数据库、搜索系统、对象存储或其他事实源重新构造的数据。
“分布式”描述的是多个 Memcached 节点可以组成缓存池,不表示服务端会自动组成 Redis Cluster 式集群。常规 Memcached 服务端之间不复制数据、不选主、不交换槽位信息,也不维护客户端分片拓扑。请求发往哪个节点,通常由客户端一致性哈希、独立代理或云平台决定。
Memcached 不是关系型数据库,也不是持久化 Key-Value 数据库。进程退出、节点重启、内存回收或节点从缓存池移除,都可能让一部分甚至全部缓存消失。应用必须把 miss 当成正常分支,并能在受控并发下回源和重建缓存。
它的优势不是功能多,而是数据模型简单、协议开销低、多线程并发明确、内存分配可预测。它的约束也来自这些选择:没有服务端复杂数据结构,没有主从复制和自动选主,没有跨 Key 事务,没有 Redis/Valkey 式 ACL,也不能把普通缓存实例当作事实数据源。
2. 官方资源、版本与发布
| 项目 | 地址或结论 |
|---|---|
| 官网 | memcached.org |
| 下载 | memcached.org/downloads |
| 源码 | github.com/memcached/memcached |
| 发布说明 | Memcached ReleaseNotes |
| 协议文档 | Memcached protocols |
| 本文命令基线版本 | 1.6.45(截至本文核对时;当前版本必须以官方发布页为准) |
| Docker Official Image | memcached:1.6.45 |
| 许可证 | BSD-3-Clause |
生产环境应固定补丁版本,不使用 latest。本文命令以 1.6.45 为基线,版本信息以本文核对时的官方发布状态为边界;实施时必须重新检查官方下载页与发布说明。升级前先阅读从当前版本到目标版本之间的发布说明,并在相同启动参数、客户端协议和典型 Value 尺寸下回归。当前 memcached:1.6.45 Docker Official Image 构建时已启用 extstore、built-in proxy、SASL 和 TLS;其他镜像标签、发行版软件包和自行构建产物不能据此推断,仍须核对 Dockerfile、构建参数、memcached -h 和实际连接结果。
memcached -V
memcached -h
docker run --rm memcached:1.6.45 memcached -V
docker run --rm memcached:1.6.45 memcached -h关键预期输出类似:
memcached 1.6.45帮助中没有出现某个高级参数,就不能照搬后文高级示例。固定镜像版本只能证明该标签的构建结果,不能证明其他来源的二进制拥有相同能力。
后文在 Bash 中发送原始协议统一使用下面的函数。它从标准输入读取请求,补发 quit,并用三秒硬超时防止不同 nc 实现因 EOF 行为不同而无限等待。
mc_raw() {
local port="${1:?port required}"
{ cat; printf 'quit\r\n'; } |
timeout 3s nc 127.0.0.1 "$port"
}
printf 'version\r\n' | mc_raw 11211预期返回 VERSION 1.6.45 后退出。GNU/Linux 没有 timeout 时先安装 coreutils;macOS Homebrew 的命令通常是 gtimeout。若必须直接使用 OpenBSD nc,可用 nc -N 在标准输入结束后关闭网络写端;传统 netcat 常用 nc -q 1,两者参数不能混写。Windows 原生 PowerShell 不自带兼容的 nc 与 timeout,应在 WSL、Git Bash、运维容器中执行,或用语言客户端设置连接与读取超时。后文的 mc_raw 仅用于实验和排障,不代替生产客户端连接池。
3. 总体架构
一次请求会经过应用缓存封装、Key 标准化、节点路由、TCP 连接、协议解析、工作线程、Hash Table、item 和 slab。读取命中后更新统计与 LRU 状态,写入则根据对象尺寸选择 slab class。
这张图中没有服务端复制链路。节点 A 写入的 item 不会自动出现在节点 B;客户端把同一个 Key 路由到不同节点时,只会得到 miss,不会得到拓扑错误。
4. 单实例、客户端分片与 proxy
4.1 单实例
单实例适合本地开发、边车缓存、低价值可回源数据和小规模服务。进程或主机故障会让全部缓存失效,容量和网络吞吐受单机限制。
4.2 客户端分片
客户端分片是常见生产形态。所有应用实例和所有语言必须固定节点标识、配置顺序、权重、哈希算法、Key 编码和故障节点处理方式,再用真实客户端测试向量证明路由一致。不能绝对断言所有算法都会受节点顺序影响,但配置顺序仍应固定,因为客户端实现、重复节点处理和 continuum 构造可能不同。节点变化会让部分 Key 改变归属,原节点上的缓存不会迁移到新节点。
4.3 built-in proxy
built-in proxy 从 1.6.23 起提供,自行编译时需要显式启用,并通过代理配置定义后端池和路由;本文固定的 memcached:1.6.45 官方镜像已经启用该能力,其他制品仍须检查。代理可以统一路由和连接入口,但也引入代理容量、配置发布和故障域,不能把它误解为数据复制。
5. 一次 get 请求的执行链
假设业务读取 product:1001:v3,完整链路如下。
| 阶段 | 发生的事情 | 失败表现 |
|---|---|---|
| Key 构造 | 应用拼接环境、业务、对象和版本 | 前缀或编码不一致导致静默 miss |
| 节点选择 | 单节点直连,或哈希算法选出缓存节点 | server list 不一致导致读写落在不同节点 |
| 连接获取 | 从客户端连接池复用 TCP 连接 | 建连超时、连接耗尽或 fd 不足 |
| 协议编码 | 生成 get key\r\n 或 Meta mg 请求 | Key 超过限制或协议格式错误 |
| 网络事件 | libevent 发现 socket 可读 | 网络拥塞导致操作超时 |
| 工作线程 | worker 解析请求并执行命令 | 热 Key 或大 Value 让单节点 CPU、网络升高 |
| Hash 查找 | 对 Key 求哈希并在 Hash Table 查 item | 不存在时返回 miss |
| 有效性判断 | 判断 item 是否过期、是否被逻辑删除 | 过期 item 返回 miss,随后回收 |
| LRU 维护 | 命中影响 item 热度和队列位置 | 高频访问会让热点长期驻留 |
| 返回响应 | Basic Text 返回 VALUE...END | 客户端超时可能发生在服务端已成功之后 |
命中响应:
VALUE product:1001:v3 0 17
{"name":"phone"}
END未命中只返回:
ENDmiss 不是服务错误。应用应进入受并发保护的回源分支,而不是无限重试 Memcached。
6. 一次 set 请求的执行链
set product:1001:v3 0 300 17 的处理不仅是放进内存。
| 阶段 | 核心动作 |
|---|---|
| 客户端序列化 | 把对象编码为约定字节,计算准确字节数 |
| 路由 | 按 Key 选择唯一目标节点 |
| 协议解析 | 解析 flags、TTL、Value 长度和 noreply |
| 尺寸检查 | 检查 Key、item 与 -I 最大 item 限制 |
| slab 选择 | 根据 item 总尺寸选择可容纳它的 slab class |
| chunk 分配 | 从该 class 的空闲 chunk 获取空间,必要时淘汰 |
| item 建立 | 保存 Key、flags、过期时间、CAS、Value 等信息 |
| Hash 关联 | 将 Key 指向新 item,替换旧 item 时更新引用 |
| LRU 关联 | 把 item 放入合适的 LRU 队列 |
| 响应 | 正常返回 STORED,noreply 则不返回写入结果 |
客户端操作超时不代表写入一定失败。服务端可能已经完成存储,但响应未在超时前到达。缓存写入通常可以接受这种不确定性;若业务依赖精确写入结果,说明数据可能不适合只放在 Memcached。
7. libevent、监听线程、worker 与后台线程
Memcached 使用 libevent 管理网络事件。主监听线程接受连接,并把连接分配给 worker thread;worker 维护各自的事件循环,读取请求、解析协议、执行命令并写回响应。多线程提升多连接并发能力,但不意味着线程数越多越快。
-t 指定 worker 数量。线程过少可能让 CPU 核心闲置,线程过多会增加调度、锁竞争、线程栈和缓存抖动。调整前应同时观察 CPU、上下文切换、网络吞吐和 P99,而不是只看 QPS。
| 线程或任务 | 主要职责 | 需要关注的代价 |
|---|---|---|
| listen thread | 接收并分发新连接 | 连接突发、listen backlog |
| worker thread | socket 事件、协议解析、命令执行 | CPU、锁竞争、网络拷贝 |
| assoc maintenance | Hash Table 扩容维护 | 扩容期间额外工作 |
| LRU maintainer | 维护分段 LRU 和队列迁移 | 后台 CPU |
| LRU crawler | 扫描并回收过期 item、生成统计 | 扫描开销 |
| slab maintainer | slab reassignment 与 automove | class 间页迁移成本 |
| extstore thread | 可选的 flash 读写 | I/O 和尾延迟 |
单个命令的执行是服务端内部操作,CAS 可以检测同一 Key 的并发覆盖;Memcached 不提供跨请求、跨 Key 的事务。两个请求先 get 再 set 之间仍可被其他客户端修改,必须使用 gets/cas 或把事实写入数据库事务。
8. item、Hash Table 与内存占用
Memcached 的 Hash Table 保存 Key 到 item 的索引。item 不只包含 Value,还包含链表指针、时间、flags、Key 长度、Value 长度、引用计数、CAS 等元数据。实际内存消耗因此大于 Key 字节 + Value 字节。
item 原始需求 ≈ item header + Key 字节 + Value 字节 + 对齐开销
item 实际占用 = 所属 slab class 的 chunk_size
内部碎片 = chunk_size - item 原始需求同样是 100 万个对象,100 字节 Value 和 1KB Value 的元数据占比完全不同。小对象场景需要关注 item header 与 Hash Table,大对象场景更容易受 chunk 浪费、网络带宽和 -I 限制影响。
9. slab page、class、chunk 与 growth factor
Memcached 把 item 内存组织成 slab class。每个 class 有固定 chunk_size,slab page 再切分为多个同尺寸 chunk。新 item 会进入第一个足以容纳它的 class。
| 概念 | 含义 | 常见误解 |
|---|---|---|
| slab page | 从内存预算中分配的一大块页 | 不是操作系统 swap page |
| slab class | 一组相同 chunk 尺寸的 page | 不是业务数据类型 |
| chunk | 保存一个 item 的固定大小单元 | 剩余空间不能再放另一个 item |
| growth factor | 相邻 class chunk 尺寸增长倍率 | 越小不一定越省内存 |
-I | 单 item 最大尺寸上限 | 调大不会自动提高命中率 |
-m | 主要 item/slab 内存预算 | 不等于进程 RSS 上限 |
默认增长因子通常为 1.25,但应通过 stats settings 查看实际配置。增长因子大,class 数少但内部碎片可能更高;增长因子小,尺寸贴合更细,但 class、统计与管理开销增加。
printf "stats settings\r\n" | mc_raw 11211 |
grep -E "maxbytes|growth_factor|chunk_size|slab_page_size|item_size_max"关键输出示例:
STAT maxbytes 134217728
STAT growth_factor 1.25
STAT chunk_size 48
STAT slab_page_size 1048576
STAT item_size_max 1048576实际字段和值以当前二进制为准。-m 128 对应的主要 item 预算约为 128MiB,但进程还需要 Hash Table、连接、线程栈、libevent、统计和可选 TLS/extstore 缓冲区。
10. 内部碎片与 slab 不均衡
假设一个 item 原始需要 700 字节,而它进入的 class chunk_size 是 888 字节,则单 item 约浪费 188 字节。大量尺寸恰好落在 class 边界之后的对象会放大浪费。
另一类问题是 class 不均衡。某个尺寸段大量写入,所属 class 已无空闲 chunk 并持续淘汰;另一个 class 仍有空闲 page。此时进程总内存看起来没有异常,热点 class 却已经出现 eviction。
stats slabs 用于查看 page、chunk 和分配状态,stats items 用于查看各 class 的 item 年龄、淘汰和回收。不能只用 bytes / limit_maxbytes 判断是否需要扩容。
11. HOT、WARM、COLD 与 TEMP 分段 LRU
现代 Memcached 不是一条简单 LRU 链,而是按 slab class 维护分段 LRU。
| 队列 | 作用 | item 的典型变化 |
|---|---|---|
| HOT | 接纳新写入并观察是否真正变热 | 超出预算或变旧后移向 COLD |
| WARM | 保存被再次访问、有持续价值的 item | 长时间不活跃后移向 COLD |
| COLD | 主要淘汰候选与再评估区域 | 被访问可回到 WARM,压力下被淘汰 |
| TEMP | 保存短 TTL 临时 item | 由 temporary_ttl 等高级参数影响 |
访问不会简单地在每次请求中把 item 移到全局链首,否则高并发下锁竞争会很重。worker 记录活跃状态,后台 LRU maintainer 再完成队列迁移。LRU crawler 扫描过期 item,让尚未被访问的过期对象也能被回收,并可生成部分统计。
slab automove 尝试把 page 从低压力 class 迁移给高压力 class。它缓解的是 class 间内存错配,不会修复超大 Value、热点 Key、不合理 TTL 或整体容量不足。
12. 协议体系
官方协议页明确列出 Basic Text 与 Meta Text。Binary Protocol 已弃用且不再继续更新;新客户端协议能力应优先考虑 Meta Text。历史客户端和 SASL 场景仍可能依赖 Binary Protocol,使用前必须承认其维护状态和客户端约束。
| 协议 | 特点 | 适用判断 |
|---|---|---|
| Basic Text | 简单、可用 nc 调试、生态广 | 常规 get/set、排障和兼容性最好 |
| Meta Text | flag/token 可组合,支持精细 TTL、CAS、anti-dogpiling、stale-while-revalidate | 新客户端和高级缓存语义优先评估 |
| Binary Protocol | 二进制帧,历史 SASL 客户端常用 | 已弃用,不作为新客户端默认方向 |
Basic Text 存储命令格式:
set <key> <flags> <exptime> <bytes> [noreply]\r\n
<data>\r\n| 字段 | 含义 |
|---|---|
key | 最长 250 字节且不能包含空白和控制字符 |
flags | 服务端不解释的值,客户端可用于标识序列化 |
exptime | 0 不过期;不超过 30 天按相对秒数;更大值按 Unix 时间戳 |
bytes | Value 的准确字节数,不是字符数 |
noreply | 不等待正常响应,也会隐藏部分写入失败 |
| 响应 | 含义 |
|---|---|
STORED | 写入成功 |
NOT_STORED | 条件不满足 |
EXISTS | CAS token 已变化 |
NOT_FOUND | CAS 或删除目标不存在 |
CLIENT_ERROR ... | 请求格式、长度或参数错误 |
SERVER_ERROR ... | 服务端资源或内部错误 |
Meta Text Protocol以短命令和可组合 flag 工作。
| 命令 | 作用 |
|---|---|
mg | meta get |
ms | meta set |
md | meta delete |
ma | meta arithmetic |
Meta 请求把需要的能力写成 token。例如 v 请求 Value,t 请求剩余 TTL,c 请求 CAS,s 请求尺寸,T 设置 TTL,C 携带 CAS 条件。N<ttl> 是 mg 请求 token:miss 时创建短 TTL 占位;R<ttl> 是 mg 请求 token:剩余 TTL 低于阈值时竞争提前重建。I 用于 md 时把现有 item 标记为 stale 并推进 CAS;用于 ms 时必须结合 C,可接受较旧 CAS 的写入,但写入结果继续保持 stale。W、Z、X 是响应 token 而不是客户端随意发送的控制开关:W 表示当前请求赢得填充或重建资格,Z 表示该 item 已经向其他请求发放过 winner,X 表示返回的是 stale item。请求带 v 且 item 存在时使用 VA 并跟随 Value 数据块,没有请求 v 的命中才使用 HD;EN、NF、EX 分别用于结束或未找到、条件冲突等结果。响应 token 的排列顺序不应写死,客户端按 token 名解析。
Meta 协议可以实现 anti-dogpiling 和 stale-while-revalidate:多个请求都执行同一条 mg ... N30 或 mg ... R30,只有看到 W 的请求回源;看到 Z 的请求不得再次回源,应短暂等待、降级或读取允许的 stale 值。管理员或重建流程用 md key I T30 标记 stale 后,读取方可能同时看到 X 与 winner/loser 状态;不同请求并发到达,响应先后没有固定顺序,业务只能依据各自响应 token 决策。它提供缓存并发协调原语,不会替代数据库事务,也不能把过期旧值变成事实数据。
13. TTL、CAS、noreply 与计数器边界
TTL 的 30 天边界是协议语义,不是版本 Bug。
30 天 = 2592000 秒
3888000 表面上是 45 天,但会被当作 Unix epoch 附近的绝对时间戳
长期 TTL 应传入当前时间 + TTL 秒数的未来时间戳CAS 是乐观并发控制。gets 返回当前 CAS token,cas 只有在 token 未变化时才写入。它适合保护单 Key 的读、计算、条件写,不保证多个 Key 一起成功。
noreply 减少响应流量,但客户端不知道该条写入是否因对象过大、参数错误或资源问题失败。只有缓存写失败完全可忽略且另有指标观察的流量才应考虑使用。
incr/decr 只处理已存在的无符号十进制数字字符串,不会自动创建 Key。decr 不会减到负数。计数器仍是缓存数据,不能承担余额、库存最终值或审计序列。
14. 产品故障边界
| 事件 | Memcached 的行为 | 应用必须承担的责任 |
|---|---|---|
| 进程重启 | 常规模式 item 消失 | 限流回源、预热、逐步恢复命中率 |
| 单节点失联 | 路由到该节点的请求失败或 miss | 快速失败、移除节点、控制重映射 |
| 新增或删除节点 | 部分 Key 重新映射 | 灰度变更、预热、观察数据库负载 |
| 内存用满 | 在相关 slab class 淘汰,或关闭淘汰时写失败 | 容量规划、Value 治理、扩容 |
| Key 热点 | 一个 Key 固定落在一个节点 | 本地缓存、单飞、拆桶 |
| 客户端配置不同 | 同 Key 访问不同节点或不同字节 | 统一算法、顺序、权重和序列化 |
flush_all | 大量 item 同时失效 | 默认禁用,按业务 Key 主动失效 |
| extstore 故障 | flash Value 读取受影响 | 回源,不把 extstore 当持久化 |
warm restart 可以在版本、启动参数、memory file 和干净退出条件都满足时,从 memory file 恢复缓存 item;extstore 可以把部分 Value 放到 flash。两者都不构成持久化、复制或故障恢复承诺。
二、为什么
1. 适合与不适合
| 场景 | 适合的前提 | 主要收益 |
|---|---|---|
| 数据库查询结果 | 丢失后能重新查询,允许短暂 miss | 降低数据库读压力 |
| 页面片段或 API 响应 | 可重算且对象尺寸受控 | 减少重复计算和下游调用 |
| 简单对象缓存 | 只需要整对象 get/set | 协议和模型简单 |
| 大规模客户端分片池 | 客户端算法可统一,节点故障可回源 | 横向扩展容量和吞吐 |
| Session | 允许重新登录或有可靠降级 | 多应用实例共享临时状态 |
| 高频近似计数 | 最终值在可靠系统中 | 低成本原子增减 |
| 需求 | 不适合的原因 | 更合理的方向 |
|---|---|---|
| 不可丢数据 | 常规实例重启即丢缓存 | 关系型数据库、持久化 KV |
| 服务端复制和自动选主 | 节点互不复制 | Redis/Valkey 高可用 |
| 复杂数据结构 | 服务端只保存不透明 Value | Redis/Valkey |
| 跨 Key 事务 | 只提供单 Key CAS 等原语 | 数据库事务 |
| 可靠消息 | 没有确认、重放和持久化 | 专用消息系统 |
| 命令级权限 | 没有 Redis/Valkey 式 ACL | 网络隔离、独立实例 |
| 无法承受冷启动 | 重启会集中 miss | 预热、限流、两级缓存或重新选型 |
2. Memcached、Redis 与 Valkey
| 维度 | Memcached | Redis | Valkey |
|---|---|---|---|
| 定位 | 简单内存对象缓存 | 内存数据结构数据库 | 开源内存数据结构存储 |
| 数据结构 | Key 与不透明 Value | 丰富数据结构 | 丰富数据结构 |
| 服务端分片 | 常规没有 | Cluster | Cluster |
| 复制与选主 | 常规没有 | 主从、Sentinel、Cluster | 主从、Sentinel、Cluster |
| 持久化 | 常规没有 | RDB、AOF | RDB、AOF |
| 并发更新 | 单 Key CAS、add | 事务、Lua、原子命令 | 事务、Lua、原子命令 |
| 安全 | 网络隔离为主,可选 SASL/TLS | ACL、TLS | ACL、TLS |
选型不能只比较单机 QPS。还要比较故障时谁负责路由、谁负责恢复、冷启动会打到哪里、客户端升级是否统一,以及数据错误的业务成本。
3. 数据角色、RPO 与 RTO
Memcached 中的数据应是事实数据的可丢副本。对缓存本身而言,常规部署的 RPO 可以是全部缓存;业务 RPO 应由数据库或其他事实源保证。
业务 RTO 不是 Memcached 进程重新启动所需的几秒,而是从缓存变冷到应用延迟、错误率和下游负载恢复正常所需的时间。若 1 分钟内重新启动但数据库被回源流量压垮 30 分钟,实际恢复时间就是后者。
| 问题 | 必须得到的答案 |
|---|---|
| 数据能否全部丢失 | 不能则不得把 Memcached 当唯一存储 |
| miss 是否有可靠回源 | 回源失败时应用如何降级 |
| 下游能承受多少并发回源 | 决定单飞、限流和预热规模 |
| 可接受多长冷缓存期 | 决定扩缩容和发布窗口 |
| 节点失败是否自动移除 | 由客户端、代理还是平台负责 |
| 失败节点恢复后如何加入 | 原位恢复、重新预热还是新地址 |
4. 取模、一致性哈希、Ketama 与 Rendezvous
普通取模使用 hash(key) % node_count。节点数从 3 变为 2 时,大量 Key 的余数变化,通常会发生大面积重映射。
一致性哈希把节点及其虚拟节点放在哈希环上,Key 顺时针找到第一个节点。删除一个节点时,理论上主要影响该节点负责的区间。虚拟节点用于改善物理节点较少时的分布均匀性。
Ketama 是 Memcached 客户端中常见的一致性哈希约定,通常使用 MD5 构造 continuum。不同库对地址格式、权重和点数的实现仍可能不同。
Rendezvous hashing 对每个 Key 与每个节点计算分数,选择得分最高的节点。它不需要维护有序哈希环,节点变化时也能减少重映射。
| 算法 | 节点变化后的典型影响 | 兼容性重点 |
|---|---|---|
| 取模 | 节点数变化时大面积重映射 | 哈希函数与节点顺序 |
| 一致性哈希 | 主要影响相邻区间 | 环实现、虚拟节点、权重 |
| Ketama | 常用于跨客户端约定 | 地址格式、MD5 点生成规则 |
| Rendezvous | 主要影响变化节点相关 Key | 分数函数、权重、节点标识 |
这些算法只决定去哪个节点,不复制 Key,也不保证节点故障后仍能命中。
5. 容量、命中率与冷启动成本
单 item 实际占用 ≈ 对应 class 的 chunk_size
缓存 item 预算 ≈ Key 数量 × 平均实际 chunk_size
进程内存 ≈ slab 内存 + Hash Table + 连接 + 线程栈 + 协议缓冲 + 可选功能
安全容量 ≈ 正常峰值 item 预算 + 尺寸分布波动 + 节点故障余量一个命中率 95% 的缓存,每秒 100 万次读取仍有 5 万次 miss;若 miss 都查询数据库,冷启动时 100% miss 可能让数据库瞬间承受原缓存读流量。
正常回源 QPS = 读取 QPS × (1 - 命中率)
单节点故障附加回源 QPS ≈ 读取 QPS × 故障节点访问比例
冷启动峰值回源 QPS ≈ 读取 QPS - 本地缓存命中 - 被单飞合并的请求容量评审应同时考虑平均 Value、P95/P99 Value、Key 数、TTL 分布、热点比例、slab 内部碎片、节点失效后的剩余容量、网络吞吐、下游回源上限和恢复目标。增加内存后命中率没有提升,问题可能是 TTL、Key 版本、高基数低复用数据或路由不一致,而不是容量不足。
三、怎么做
1. 系统准备与能力确认
Linux 主机先确认 CPU、内存、端口和文件描述符。
uname -a
nproc
free -h
ulimit -n
ss -lntp | grep 11211 || true
getent passwd memcache || trueDocker 环境确认 Engine、Compose 和端口。
docker version
docker compose version
docker ps --format "table {{.Names}}\t{{.Ports}}"Windows 查看端口:
netstat -ano | findstr 11211正常状态是目标端口未被未知进程占用,生产进程的 fd 上限明显高于 -c 连接上限,并为日志、管理连接和其他文件留有余量。
任何安装方式完成后都执行:
memcached -V
memcached -h | sed -n '1,220p'
memcached -h | grep -Ei 'sasl|ssl|tls|proxy|extstore|restart|track_sizes'没有匹配结果不一定表示源码完全不支持,但足以说明当前二进制不能直接按高级示例启动。下一步应检查构建参数和官方功能文档,而不是猜参数。
2. 包安装与源码编译
Debian/Ubuntu:
sudo apt-get update
sudo apt-get install -y memcached libmemcached-tools netcat-openbsd
memcached -V
systemctl cat memcachedRHEL 系:
sudo dnf install -y memcached libmemcached
memcached -V
systemctl cat memcachedmacOS:
brew install memcached
memcached -V
brew services info memcached发行版仓库版本可能落后于 1.6.45,启动参数和编译能力也可能不同。需要固定稳定版或 TLS/proxy 等能力时,应使用源码构建或经过验证的内部镜像。
源码构建:
sudo apt-get update
sudo apt-get install -y build-essential libevent-dev pkg-config curl
curl -fLO https://memcached.org/files/memcached-1.6.45.tar.gz
tar -xzf memcached-1.6.45.tar.gz
cd memcached-1.6.45
./configure --help | grep -Ei 'tls|sasl|proxy'
./configure
make -j"$(nproc)"
make test
sudo make install
/usr/local/bin/memcached -V
/usr/local/bin/memcached -h预期版本:
memcached 1.6.45需要 TLS 时安装 OpenSSL 开发包并显式启用:
sudo apt-get install -y libssl-dev
./configure --enable-tls
make -j"$(nproc)"
./memcached -h | grep -Ei 'ssl|tls'需要 built-in proxy 时根据 1.6.45 的 ./configure --help 和官方文档确认开关:
./configure --help | grep -i proxy
./configure --enable-proxy
make -j"$(nproc)"
./memcached -h | grep -i proxy需要多种能力时应一次组合构建选项,并保存实际 ./configure 命令、编译器版本和依赖版本。
3. Docker 与 Compose
检查固定镜像:
docker pull memcached:1.6.45
docker run --rm memcached:1.6.45 memcached -V
docker run --rm memcached:1.6.45 memcached -h临时实例:
docker run --rm \
--name memcached-smoke \
-p 127.0.0.1:11211:11211 \
memcached:1.6.45 \
memcached -m 128 -c 1024 -t 4 -I 1m -U 0 -FCompose:
services:
memcached:
image: memcached:1.6.45
container_name: demo-memcached
command:
- memcached
- -m
- "256"
- -c
- "2048"
- -t
- "4"
- -I
- "1m"
- -U
- "0"
- -F
ports:
- "127.0.0.1:11211:11211"
restart: unless-stopped
mem_limit: 384m
cpus: 2启动和检查:
docker compose up -d memcached
docker compose ps memcached
docker compose logs --tail=100 memcached
printf "version\r\n" | mc_raw 11211
printf "stats settings\r\n" | mc_raw 11211 |
grep -E "maxbytes|maxconns|num_threads|item_size_max|udpport|flush_enabled"预期容器运行,协议返回 VERSION 1.6.45,UDP 关闭,flush_enabled 为 no,其余参数与 Compose 一致。容器内存上限必须高于 -m,否则进程可能在 item 预算未用满前被 OOM Kill。
Docker Official Image 不应被假设包含 nc、curl 或 shell 健康探测依赖。健康检查可以从宿主机、sidecar 或应用探针发送 version。
4. systemd 部署
sudo useradd --system --no-create-home --shell /usr/sbin/nologin memcache 2>/dev/null || true
sudo install -d -o memcache -g memcache /var/lib/memcached/etc/systemd/system/memcached.service:
[Unit]
Description=Memcached 1.6.45
After=network.target
[Service]
Type=simple
User=memcache
Group=memcache
RuntimeDirectory=memcached
ExecStart=/usr/local/bin/memcached \
-l 127.0.0.1 \
-p 11211 \
-U 0 \
-m 1024 \
-c 4096 \
-t 4 \
-I 1m \
-F
Restart=on-failure
RestartSec=2
LimitNOFILE=8192
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now memcached
systemctl status memcached --no-pager
journalctl -u memcached -n 100 --no-pager
ss -lntp | grep 11211
printf "version\r\n" | mc_raw 11211恢复标准是进程持续运行、端口只监听预期地址、版本正确、读写实验成功。仅有 active (running) 不代表参数和协议正确。
5. 启动参数
| 参数 | 作用 | 调整风险 |
|---|---|---|
-l | 监听地址 | 0.0.0.0 可能扩大暴露面 |
-p | TCP 端口 | 需与客户端和防火墙一致 |
-U 0 | 关闭 UDP | 不应为旧客户端随意开启 |
-m | item/slab 内存预算 MiB | 不是进程 RSS 上限 |
-c | 最大并发连接数 | 必须与 fd 和客户端池配套 |
-t | worker thread 数 | 过大增加调度和锁竞争 |
-I | 最大 item 大小 | 大对象放大网络和 slab 风险 |
-f | slab growth factor | 改变 class 和碎片分布 |
-n | 最小 chunk 空间 | 影响小对象效率 |
-b | listen backlog | 需结合内核队列 |
-R | 单事件最大连续请求数 | 影响连接公平性 |
-F | 禁用 flush_all | 生产共享池推荐 |
-M | 内存用尽时禁止淘汰 | 会转为写入失败 |
-o | 扩展选项 | 必须按当前帮助验证 |
-M 不是数据保护,只是内存用尽后返回写入错误,旧 item 仍不是持久化数据。
6. Basic Text 全命令实验
启动实验实例:
docker run -d --rm \
--name memcached-lab \
-p 127.0.0.1:11211:11211 \
memcached:1.6.45 \
memcached -m 64 -c 256 -t 2 -I 1m -U 06.1 set、get 与 delete
{
printf "set demo:user:1001 7 60 5\r\n"
printf "Alice\r\n"
printf "get demo:user:1001\r\n"
printf "delete demo:user:1001\r\n"
printf "get demo:user:1001\r\n"
} | mc_raw 11211预期:
STORED
VALUE demo:user:1001 7 5
Alice
END
DELETED
END6.2 add 与 replace
{
printf "delete demo:condition\r\n"
printf "add demo:condition 0 60 1\r\nA\r\n"
printf "add demo:condition 0 60 1\r\nB\r\n"
printf "replace demo:condition 0 60 1\r\nC\r\n"
printf "get demo:condition\r\n"
} | mc_raw 11211关键输出:
NOT_FOUND
STORED
NOT_STORED
STORED
VALUE demo:condition 0 1
C
ENDadd 只在 Key 不存在时写入,replace 只在 Key 已存在时写入。
6.3 append 与 prepend
{
printf "set demo:text 0 60 5\r\nworld\r\n"
printf "prepend demo:text 0 60 6\r\nhello \r\n"
printf "append demo:text 0 60 1\r\n!\r\n"
printf "get demo:text\r\n"
} | mc_raw 11211预期 Value:
hello world!它们操作原始字节,不理解 JSON、字符集或对象格式。
6.4 gets 与 cas
printf "set demo:cas 0 60 1\r\nA\r\n" | mc_raw 11211
printf "gets demo:cas\r\n" | mc_raw 11211响应最后一个数字是 CAS token:
VALUE demo:cas 0 1 42
A
END用实际 token 替换 42:
printf "cas demo:cas 0 60 1 42\r\nB\r\n" | mc_raw 11211
printf "cas demo:cas 0 60 1 42\r\nC\r\n" | mc_raw 11211第一次预期 STORED,再次使用旧 token 预期 EXISTS。收到冲突后应重新 gets、重新计算,并限制重试次数和总时限。
6.5 touch 与 gat
{
printf "set demo:ttl 0 5 2\r\nok\r\n"
printf "touch demo:ttl 120\r\n"
printf "gat 180 demo:ttl\r\n"
} | mc_raw 11211关键输出:
STORED
TOUCHED
VALUE demo:ttl 0 2
ok
END滑动过期场景仍需设置绝对生命周期上限。
6.6 incr 与 decr
{
printf "set demo:counter 0 60 2\r\n10\r\n"
printf "incr demo:counter 5\r\n"
printf "decr demo:counter 20\r\n"
printf "incr demo:missing 1\r\n"
} | mc_raw 11211预期:
STORED
15
0
NOT_FOUND计数器必须预先创建,decr 下限为 0。
6.7 version、stats 与 quit
{
printf "version\r\n"
printf "stats\r\n"
printf "stats settings\r\n"
printf "stats items\r\n"
printf "stats slabs\r\n"
printf "quit\r\n"
} | mc_raw 11211version 应返回 VERSION 1.6.45。stats 响应由多行 STAT name value 和最后的 END 组成。quit 关闭当前协议连接。
6.8 flush_all
只在独立实验实例测试:
printf "set demo:flush 0 300 2\r\nok\r\n" | mc_raw 11211
printf "flush_all\r\n" | mc_raw 11211
printf "get demo:flush\r\n" | mc_raw 11211允许执行时返回 OK,随后读取为 END。生产共享实例使用 -F 后,预期:
CLIENT_ERROR flush_all not allowed7. TTL 30 天边界
printf "set demo:ttl30 0 2592000 2\r\nok\r\nget demo:ttl30\r\n" |
mc_raw 11211预期命中。错误地把 45 天写成相对秒数:
printf "set demo:ttl45-wrong 0 3888000 2\r\nok\r\nget demo:ttl45-wrong\r\n" |
mc_raw 11211写入可能返回 STORED,读取却立即为 END,因为该值被解释为过去的 Unix 时间戳。
正确写法:
expires_at=$(( $(date +%s) + 45*24*3600 ))
printf "set demo:ttl45 0 %s 2\r\nok\r\nget demo:ttl45\r\n" "$expires_at" |
mc_raw 11211预期命中。跨语言系统应在统一客户端封装中处理这个边界。
8. noreply 风险
启动小上限实例:
docker run -d --rm \
--name memcached-noreply \
-p 127.0.0.1:11212:11211 \
memcached:1.6.45 \
memcached -m 16 -I 512k -U 0head -c 600000 /dev/zero | tr '\0' x > /tmp/large-value
bytes=$(wc -c < /tmp/large-value)
{
printf "set demo:large 0 60 %s noreply\r\n" "$bytes"
cat /tmp/large-value
printf "\r\n"
printf "get demo:large\r\n"
} | mc_raw 11212客户端看不到该写入本应返回的错误,后续读取只得到 END。noreply 不应在需要确认结果的路径使用。
9. Meta Text
写入:
printf "ms demo:meta 5 T60\r\nhello\r\n" | mc_raw 11211成功响应:
HD读取 Value、TTL、CAS 和尺寸:
printf "mg demo:meta v t c s\r\n" | mc_raw 11211响应结构类似:
VA 5 t59 c43 s5
hellotoken 的顺序和值以服务端实际响应为准。删除:
printf "md demo:meta\r\nmg demo:meta v\r\n" | mc_raw 11211关键响应是删除成功的 HD 和 miss 的 EN。
Meta arithmetic:
printf "ms demo:meta-counter 1 T60\r\n0\r\n" | mc_raw 11211
printf "ma demo:meta-counter D5 v\r\n" | mc_raw 11211预期数值变为 5。不同 flag 的准确组合以官方 Meta 文档为准。
N token 可在 miss 时创建短 TTL 占位并让一个请求获得重建资格;获得 W token 的请求负责回源,其他请求不应同时回源。
printf "md demo:dogpile\r\n" | mc_raw 11211
printf "mg demo:dogpile v N30\r\n" | mc_raw 11211stale-while-revalidate:
printf "ms demo:stale 3 T300\r\nold\r\n" | mc_raw 11211
printf "md demo:stale I T30\r\n" | mc_raw 11211
printf "mg demo:stale v t c R30\r\n" | mc_raw 11211并发启动多个完全相同的 mg demo:dogpile v N30 或 mg demo:stale v t c R30 请求:只有响应含 W 的 winner 执行回源并用 ms 写回;含 Z 的 loser 不回源;含 X 说明读到 stale 值。这里的请求都带 v,因此 item 存在时使用 VA:零字节占位项可能是 VA 0 W 或 VA 0 Z,stale Value 可能是 VA 3 t29 c777 W X 后跟数据行;只有未请求 v 的命中才使用 HD。token 顺序由服务端决定,不能把固定行序当协议。应用必须限定 winner 的回源时限与 stale 最长服务窗口。
10. slab 与 LRU 实验
docker run -d --rm \
--name memcached-slab \
-p 127.0.0.1:11213:11211 \
memcached:1.6.45 \
memcached -m 16 -I 2m -f 1.25 -U 0 -o track_sizesprintf "stats settings\r\n" | mc_raw 11213 |
grep -E "maxbytes|growth_factor|item_size_max|track_sizes"
for size in 64 256 1024 8192 65536; do
head -c "$size" /dev/zero | tr '\0' x > "/tmp/value-$size"
bytes=$(wc -c < "/tmp/value-$size")
{
printf "set demo:size:%s 0 300 %s\r\n" "$size" "$bytes"
cat "/tmp/value-$size"
printf "\r\n"
} | mc_raw 11213
doneprintf "stats slabs\r\n" | mc_raw 11213
printf "stats items\r\n" | mc_raw 11213
printf "stats sizes\r\n" | mc_raw 11213stats slabs 重点看 chunk_size、chunks_per_page、total_pages、used_chunks 和 free_chunks。stats items 重点看每个 class 的 number、age、evicted 和 reclaimed。
制造压力:
value=$(printf '%08000d' 0)
for i in $(seq 1 5000); do
printf "set demo:pressure:%s 0 600 8000\r\n%s\r\n" "$i" "$value"
done | mc_raw 11213 >/tmp/memcached-pressure.out
printf "stats\r\n" | mc_raw 11213 |
grep -E "bytes |limit_maxbytes|evictions|reclaimed"
printf "stats items\r\n" | mc_raw 11213 |
grep -E "number|age|evicted|reclaimed"要确认哪个 class 淘汰、该 class 的 Value 尺寸、其他 class 是否空闲、automove 是否工作,以及命中率是否受影响。
11. 三节点缓存池
services:
mc-a:
image: memcached:1.6.45
command: ["memcached", "-m", "64", "-c", "512", "-t", "2", "-I", "1m", "-U", "0", "-F"]
ports: ["127.0.0.1:11211:11211"]
mc-b:
image: memcached:1.6.45
command: ["memcached", "-m", "64", "-c", "512", "-t", "2", "-I", "1m", "-U", "0", "-F"]
ports: ["127.0.0.1:11212:11211"]
mc-c:
image: memcached:1.6.45
command: ["memcached", "-m", "64", "-c", "512", "-t", "2", "-I", "1m", "-U", "0", "-F"]
ports: ["127.0.0.1:11213:11211"]docker compose up -d mc-a mc-b mc-c
for port in 11211 11212 11213; do
printf "version\r\n" | mc_raw "$port"
done预期三次返回 VERSION 1.6.45。此时只是三个互不感知的服务端,客户端配置路由后才构成缓存池。
12. 可运行的哈希对比
保存为 hash_lab.py:
import bisect
import hashlib
import zlib
NODES = ["127.0.0.1:11211", "127.0.0.1:11212", "127.0.0.1:11213"]
KEYS = [f"product:{i}" for i in range(100_000)]
def h32(value):
return zlib.crc32(value.encode("utf-8")) & 0xFFFFFFFF
def modulo(key, nodes):
return nodes[h32(key) % len(nodes)]
def ring(nodes, replicas=160):
values = []
for node in nodes:
for replica in range(replicas):
digest = hashlib.md5(f"{node}-{replica}".encode()).digest()
values.append((int.from_bytes(digest[:4], "little"), node))
values.sort()
return [x[0] for x in values], [x[1] for x in values]
def ketama(key, continuum):
points, owners = continuum
index = bisect.bisect_left(points, h32(key))
return owners[index % len(owners)]
def rendezvous(key, nodes):
def score(node):
raw = hashlib.sha256(f"{key}|{node}".encode()).digest()
return int.from_bytes(raw[:8], "big")
return max(nodes, key=score)
def assign(name, nodes):
if name == "modulo":
return {key: modulo(key, nodes) for key in KEYS}
if name == "ketama":
continuum = ring(nodes)
return {key: ketama(key, continuum) for key in KEYS}
return {key: rendezvous(key, nodes) for key in KEYS}
for name in ("modulo", "ketama", "rendezvous"):
before = assign(name, NODES)
after = assign(name, NODES[:2])
moved = sum(before[key] != after[key] for key in KEYS)
distribution = {
node: sum(owner == node for owner in before.values())
for node in NODES
}
print(name, distribution, f"remap={moved / len(KEYS):.2%}")python3 hash_lab.py预期趋势是普通取模约有三分之二 Key 重映射;分布均匀的一致性哈希和 Rendezvous 主要重映射原第三节点负责的约三分之一 Key。具体比例受实现影响。正式客户端仍须用真实库生成映射并以固定测试向量跨语言比对。
13. Java 客户端
下面使用 spymemcached 展示历史兼容 API。该项目长期不活跃,2.12.3 仅是已有系统的兼容示例,不代表面向新项目的推荐或当前活跃版本。升级前应检查 Maven Central、项目仓库、未修复问题和 JDK 兼容性;新项目若需要 Meta Text、持续安全维护或现代 TLS,应选择明确支持这些能力的活跃客户端。
<dependency>
<groupId>net.spy</groupId>
<artifactId>spymemcached</artifactId>
<version>2.12.3</version>
</dependency>import java.net.InetSocketAddress;
import java.util.List;
import java.util.concurrent.TimeUnit;
import net.spy.memcached.CASResponse;
import net.spy.memcached.CASValue;
import net.spy.memcached.ConnectionFactoryBuilder;
import net.spy.memcached.MemcachedClient;
public final class MemcachedDemo {
public static void main(String[] args) throws Exception {
var factory = new ConnectionFactoryBuilder()
.setOpTimeout(800)
.setDaemon(true)
.build();
var client = new MemcachedClient(
factory,
List.of(new InetSocketAddress("127.0.0.1", 11211))
);
String key = "dev:demo:user:1001:v1";
String value = "{\"name\":\"Alice\"}";
try {
boolean stored = client.set(key, 60, value)
.get(1, TimeUnit.SECONDS);
System.out.println("stored=" + stored);
System.out.println("value=" + client.get(key));
CASValue<Object> current = client.gets(key);
CASResponse result = client.cas(
key, current.getCas(), 60, "{\"name\":\"Bob\"}"
);
System.out.println("cas=" + result);
} finally {
client.shutdown(2, TimeUnit.SECONDS);
}
}
}关键输出类似:
stored=true
value={"name":"Alice"}
cas=OK客户端应作为单例复用,不为每个 HTTP 请求新建连接。跨语言缓存使用 UTF-8 JSON、明确 flags 和版本化 Key,不使用 Java 原生对象序列化。多节点还要确认 locator、节点字符串、顺序和权重。
14. Node.js 客户端
npm view memcached version time repository
MEMCACHED_JS_VERSION="${MEMCACHED_JS_VERSION:?set a reviewed version}"
npm install --save-exact "memcached@${MEMCACHED_JS_VERSION}"3rd-Eden/memcached 长期不活跃,下面只用于解释已有 Node.js 系统的兼容 API,不应据此认定它适合新项目。依赖版本必须由项目在审查仓库活跃度、Node.js 版本、漏洞和故障恢复行为后固定;需要 Meta、TLS 或活跃维护时改选满足条件的客户端。
const Memcached = require("memcached");
const client = new Memcached(
["127.0.0.1:11211", "127.0.0.1:11212", "127.0.0.1:11213"],
{ timeout: 800, retries: 0, retry: 1000, remove: true, failures: 2 }
);
const call = (method, ...args) =>
new Promise((resolve, reject) => {
client[method](...args, (error, result) =>
error ? reject(error) : resolve(result)
);
});
(async () => {
const key = "dev:demo:user:1001:v1";
try {
await call("set", key, JSON.stringify({ name: "Alice" }), 60);
console.log(await call("get", key));
const current = await call("gets", key);
console.log("cas-token", current.cas);
await call("cas", key, JSON.stringify({ name: "Bob" }), current.cas, 60);
console.log(await call("get", key));
} finally {
client.end();
}
})().catch((error) => {
console.error(error);
process.exitCode = 1;
});node memcached-demo.js预期先输出 Alice JSON,再输出 CAS token 和 Bob JSON。不同版本的 gets 返回结构必须用锁定依赖实际确认;client.end() 关闭连接。
15. Python 客户端
python3 -m pip index versions pymemcache
PYMEMCACHE_VERSION="${PYMEMCACHE_VERSION:?set a reviewed version}"
python3 -m pip install "pymemcache==${PYMEMCACHE_VERSION}"
python3 -m pip freeze | grep '^pymemcache=='import json
from pymemcache.client.base import Client
client = Client(
("127.0.0.1", 11211),
connect_timeout=0.5,
timeout=0.8,
no_delay=True,
)
key = b"dev:demo:user:1001:v1"
try:
value = json.dumps({"name": "Alice"}, separators=(",", ":")).encode()
assert client.set(key, value, expire=60)
print(client.get(key).decode())
current, token = client.gets(key)
updated = json.dumps({"name": "Bob"}, separators=(",", ":")).encode()
print("cas=", client.cas(key, updated, token, expire=60))
print(client.get(key).decode())
finally:
client.close()多节点:
from pymemcache.client.hash import HashClient
client = HashClient(
[
("127.0.0.1", 11211),
("127.0.0.1", 11212),
("127.0.0.1", 11213),
],
connect_timeout=0.5,
timeout=0.8,
dead_timeout=10,
retry_attempts=1,
)
try:
client.set(b"dev:demo:smoke", b"ok", expire=60)
print(client.get(b"dev:demo:smoke"))
finally:
client.close()Python pickle 与其他语言不互通,也可能带来不安全反序列化风险。共享缓存池优先使用明确 schema 的 JSON 或 MessagePack。
16. Go 客户端
go mod init memcached-demo
go list -m -versions github.com/bradfitz/gomemcache
GOMEMCACHE_VERSION="${GOMEMCACHE_VERSION:?set an audited tag or pseudo-version}"
go get "github.com/bradfitz/gomemcache@${GOMEMCACHE_VERSION}"
go list -m github.com/bradfitz/gomemcachepackage main
import (
"encoding/json"
"fmt"
"log"
"time"
"github.com/bradfitz/gomemcache/memcache"
)
type User struct {
Name string `json:"name"`
}
func main() {
client := memcache.New(
"127.0.0.1:11211",
"127.0.0.1:11212",
"127.0.0.1:11213",
)
client.Timeout = 800 * time.Millisecond
client.MaxIdleConns = 4
key := "dev:demo:user:1001:v1"
value, _ := json.Marshal(User{Name: "Alice"})
if err := client.Set(&memcache.Item{
Key: key, Value: value, Expiration: 60,
}); err != nil {
log.Fatal(err)
}
item, err := client.Get(key)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(item.Value))
item.Value, _ = json.Marshal(User{Name: "Bob"})
item.Expiration = 60
if err := client.CompareAndSwap(item); err != nil {
log.Fatal(err)
}
updated, _ := client.Get(key)
fmt.Println(string(updated.Value))
}go run .预期输出 Alice 和 Bob JSON。该客户端没有供调用者关闭整个连接池的公共 Close 方法,连接由内部复用并随进程生命周期释放;不能伪造不存在的 API。若应用要求显式关闭、TLS 或 Meta,应重新选择客户端。
17. 多语言互通
| 项目 | 固定值 |
|---|---|
| Key | dev:interop:user:1001:v1 |
| Value | {"name":"Alice","level":7} |
| Encoding | UTF-8 |
| TTL | 300 |
| Flags | 0 |
| Server list | mc-a=127.0.0.1:11211、mc-b=127.0.0.1:11212、mc-c=127.0.0.1:11213,配置顺序固定 |
| Weight | 三节点均为 1 |
| Hash | 项目锁定算法及其全部选项,例如 Ketama 的节点标识和 continuum 参数 |
| 测试向量 | dev:route:0000 到 dev:route:0999,UTF-8 原始字节 |
Java 写入后,Node、Python、Go 都必须读到相同字节,再依次交换写入方。这只能证明 Value 编解码兼容,不能证明路由一致。每种语言还要用真实客户端的节点选择器或可观测钩子,对同一组 Key 输出统一格式:
dev:route:0000<TAB>mc-b
dev:route:0001<TAB>mc-a
dev:route:0002<TAB>mc-cLC_ALL=C sort java.route.tsv > java.route.sorted.tsv
LC_ALL=C sort node.route.tsv > node.route.sorted.tsv
LC_ALL=C sort python.route.tsv > python.route.sorted.tsv
LC_ALL=C sort go.route.tsv > go.route.sorted.tsv
diff -u java.route.sorted.tsv node.route.sorted.tsv
diff -u java.route.sorted.tsv python.route.sorted.tsv
diff -u java.route.sorted.tsv go.route.sorted.tsv正确结果是三个 diff 都没有输出且退出码为 0;出现任何一行 + 或 - 都表示至少一个 Key 的目标节点不同。客户端无法直接暴露目标节点时,先记录每个节点 cmd_get/cmd_set 的窗口增量,再逐个发送测试 Key;增量只能旁证请求落点,不能替代完整测试向量。节点顺序是否影响某个算法要由这组真实客户端实验回答,不能仅凭算法名称推断。
17.1 缓存池蓝绿迁移
Memcached 不能可靠枚举业务 Key,也不会在节点之间搬迁 item,因此缓存池迁移不能设计成“扫描旧池后复制全部 Key”。蓝绿过程必须把事实源和真实业务访问作为重建入口。
阶段 0:应用只读写旧池 A;记录 30 分钟基线
阶段 1:读旧池 A,写入同时发往 A 和新池 B;B 写失败不阻断事实写入
阶段 2:灰度读 B;B miss 时受限回源事实源,必要时先读 A 并回填 B
阶段 3:按 1% -> 5% -> 20% -> 50% -> 100% 提升 B 的读流量
阶段 4:全部读 B,继续双写一个最长业务 TTL 或约定观察窗
阶段 5:停止写 A,观察无回滚需求后下线 A每个灰度台阶至少观察一个完整流量周期,并同时比较 B 命中率、事实源回源 QPS、业务 P99、缓存错误率、数据库连接池和拒绝率。阈值必须在迁移前写死,例如“B 命中率较 A 基线下降不超过 5 个百分点、回源 QPS 不超过下游安全上限 70%、P99 增幅不超过 10%、缓存错误率低于 0.1%”;任一指标连续两个五分钟窗口越线就停止放量。
回滚时把读路由立即切回 A,保留 A/B 双写并限制 B 的重建流量,不能在指标恶化时继续增加 B 比例。只有 B 在 100% 读流量下稳定跨过既定观察窗、无持续路由差异且下游余量充足,才能停止双写 A。停止双写后继续保留 A 一个回滚窗口;确认应用配置全部收敛、A 的连接和请求自然归零,再关闭 A。若旧池回填新池,必须限制并发、只搬迁真实访问到的 Key,并接受旧值 TTL 与一致性窗口,不能把它当数据库迁移。
18. Cache Aside
value = cache.get(key)
if value exists:
return decode(value)
permit = singleflight.acquire(key)
if not permit:
wait briefly, then retry cache once
value = database.query(id)
if value exists:
cache.set(key, encode(value), ttl_with_jitter)
else:
cache.set(key, NULL_SENTINEL, short_ttl)
return value数据库更新常采用先提交数据库、再删除缓存。删除失败应进入可重试消息或补偿任务;不能先删除缓存、长时间更新数据库,否则并发读可能把旧值重新写回。
19. 空值缓存、TTL 抖动与热点单飞
不存在对象可以写短 TTL 空值:
Key: prod:user:999999:not-found:v1
Value: {"found":false}
TTL: 30 到 120 秒并加入抖动空值 TTL 过长会掩盖随后创建的数据,高基数空值会浪费内存。还应配合参数校验、访问控制和布隆过滤器。
TTL 抖动:
effective_ttl = base_ttl + random(0, jitter)
示例:1800 + random(0, 600)进程内 singleflight 合并同一进程的并发回源;多进程可评估 Meta 的 N、R、W token。重建者超时后,其他请求应读取可接受的 stale 值、返回降级内容或有限重试,不能永久等待。
20. 两级缓存
本地缓存降低热点 Key 对单节点的压力,但增加进程间不一致窗口。应让本地 TTL 明显短于 Memcached TTL,并通过版本化 Key、消息失效或可接受的短暂旧值定义边界。
21. Session、预热与主动失效
| Session 设计项 | 建议 |
|---|---|
| Key | 环境、应用、Session ID 和版本 |
| Value | 最小必要字段,不存大型权限快照 |
| TTL | 滑动 TTL 加绝对生命周期 |
| 故障 | miss 返回重新认证,不无限回源 |
| 安全 | 不保存明文敏感数据,网络受控 |
| 容量 | 活跃 Session 数 × 实际 chunk_size |
若业务要求无感会话故障转移,常规客户端分片 Memcached 可能不满足目标。
预热应按真实热点分批读取事实源,按正常序列化和 TTL 写入,并观察 bytes、evictions、网络和数据库负载。不要扫描整个数据库一次性写满缓存。
主动失效优先删除明确 Key,或切换版本化 Key:
旧 Key: prod:product:1001:v3
新 Key: prod:product:1001:v4旧 Key 会在 TTL 到期前继续占空间,因此版本切换需要容量余量。
22. stats、公式与客户端观测
printf "stats\r\n" | mc_raw 11211 > /tmp/memcached.stats
printf "stats settings\r\n" | mc_raw 11211 > /tmp/memcached.settings
printf "stats items\r\n" | mc_raw 11211 > /tmp/memcached.items
printf "stats slabs\r\n" | mc_raw 11211 > /tmp/memcached.slabs| 维度 | 指标 | 判断 |
|---|---|---|
| 请求 | cmd_get、cmd_set | 请求量和读写比例 |
| 命中 | get_hits、get_misses | 命中率及趋势 |
| item | curr_items、total_items | 当前数量和累计写入 |
| 内存 | bytes、limit_maxbytes | item 逻辑字节和预算 |
| 淘汰 | evictions、reclaimed | 内存压力与过期回收 |
| 连接 | curr_connections、total_connections、max_connections;若主 stats 无上限字段则读取 stats settings 的 maxconns | 当前、累计连接与配置上限 |
| 拒绝 | rejected_connections、listen_disabled_num | 连接上限或监听受压 |
| CPU | rusage_user、rusage_system | 进程 CPU 累计时间 |
| 网络 | bytes_read、bytes_written | 协议流量 |
命中率 = Δget_hits / (Δget_hits + Δget_misses)
内存表观使用率 = bytes / limit_maxbytes
淘汰速率 = Δevictions / 统计窗口秒数
连接使用率 = curr_connections / max_connections
若主 stats 没有 max_connections:连接使用率 = curr_connections / stats settings 中的 maxconns必须使用时间窗口增量。bytes 不是 RSS,服务端 stats 也不提供完整业务端到端 P95/P99。客户端要记录路由、连接等待、操作耗时、超时、错误、miss 和回源耗时。
23. 容量估算
单 item 原始需求 = item header + Key + Value + 对齐
单 item 实际占用 = 所属 class chunk_size
目标 item 内存 = 峰值 Key 数 × 平均实际 chunk_size
进程上限 > -m + Hash Table + 连接 + 线程 + 可选功能
节点数 = 总 item 预算 / 单节点安全 item 预算应按 P50、P90、P99 Value 分桶,用 stats slabs 和启用后的 stats sizes 校准 chunk 浪费。节点数还要满足网络、CPU、连接数和故障回源目标。
24. memtier_benchmark
memtier_benchmark \
--server=127.0.0.1 \
--port=11211 \
--protocol=memcache_text \
--clients=20 \
--threads=4 \
--ratio=1:10 \
--data-size=1024 \
--key-pattern=R:R \
--key-maximum=1000000 \
--requests=100000| 变量 | 建议取值 |
|---|---|
| Value | 128B、1KiB、16KiB、256KiB |
| clients | 1、20、100、生产连接规模 |
| threads | 1、合理值、过量值 |
| 读写比 | 1:1、1:10、纯读 |
| Key 分布 | 均匀、热点 |
| 命中 | 预热命中、完全 miss |
| 节点 | 单节点、三节点 |
报告应同时记录吞吐、平均延迟、P95/P99、客户端 CPU、服务端 CPU、网络、evictions、连接和错误。QPS 上升但 P99 超过应用超时线,不是可用提升。工具是否支持 Meta Text 要以当前版本为准。
25. SASL
SASL 是可选认证能力,不是 ACL。-S 会强制使用 Binary Protocol,未完成认证的连接不能执行缓存命令;Binary Protocol 已弃用且不再获得新协议能力。因此 SASL 适合明确接受历史 Binary 客户端约束的既有系统,不是新系统默认安全方案。SASL 只认证客户端,不加密 Key、Value、密码交换后的业务流量或服务器身份;跨不可信网络还必须使用 TLS 或受控隧道。
./configure --help | grep -i sasl
sudo apt-get install -y libsasl2-dev sasl2-bin
./configure --enable-sasl
make -j"$(nproc)"
./memcached -h | grep -i saslsudo install -d -m 0750 /etc/sasl2
sudo tee /etc/sasl2/memcached.conf >/dev/null <<'EOF'
mech_list: plain
sasldb_path: /etc/sasl2/memcached-sasldb2
EOF
sudo chmod 0640 /etc/sasl2/memcached.conf
read -rsp 'SASL password: ' MC_SASL_PASSWORD; echo
printf '%s' "$MC_SASL_PASSWORD" |
sudo saslpasswd2 -p -c -f /etc/sasl2/memcached-sasldb2 cacheapp
sudo sasldblistusers2 -f /etc/sasl2/memcached-sasldb2
sudo chown memcache:memcache /etc/sasl2/memcached-sasldb2
sudo chmod 0600 /etc/sasl2/memcached-sasldb2/etc/sasl2/memcached.conf、用户库路径和运行用户必须与当前发行版的 Cyrus SASL 搜索路径一致;如果服务读不到用户库,先修权限和配置路径,不把数据库改成全局可读。完整实验启动:
SASL_CONF_PATH=/etc/sasl2 /usr/local/bin/memcached \
-l 127.0.0.1 \
-p 11217 \
-U 0 \
-m 128 \
-c 512 \
-t 4 \
-I 1m \
-S \
-B binary使用发行版 libmemcached-tools 中明确支持 Binary SASL 的 memcstat 验证;先用 memcstat --help 确认当前构建存在 --binary、--username 和 --password,没有这些参数就换用已经过项目验证的 Binary SASL 客户端,不伪造 Basic Text 命令。
memcstat --help | grep -E -- '--binary|--username|--password'
memcstat --servers=127.0.0.1:11217 --binary
memcstat --servers=127.0.0.1:11217 --binary \
--username=cacheapp --password='definitely-wrong'
memcstat --servers=127.0.0.1:11217 --binary \
--username=cacheapp --password="$MC_SASL_PASSWORD"前两条访问应认证失败并返回非零退出码;第三条应输出 STAT 数据。随后用同一经过验证的客户端执行 set/get,错误密码、未认证、正确密码三条链路都必须单独留存结果。nc 不能完成 SASL 握手。示例中的命令行密码只适用于隔离实验,因为它可能短暂出现在进程列表;生产凭据必须从密钥系统注入,不能进入仓库、shell 历史或普通日志。
26. TLS
官方 TLS 文档说明 TLS 从 1.5.13+ 提供,需要 OpenSSL 和 ./configure --enable-tls。
memcached -h | grep -Ei 'ssl|tls'
sudo apt-get install -y libssl-dev
./configure --enable-tls
make -j"$(nproc)"
./memcached -h | grep -Ei 'ssl|tls'假设证书已由组织 CA 颁发:
/usr/local/bin/memcached \
-l 0.0.0.0 \
-p 11214 \
-U 0 \
-m 256 \
-c 1024 \
-Z \
-o ssl_chain_cert=/etc/memcached/tls/server.crt,ssl_key=/etc/memcached/tls/server.key,ssl_ca_cert=/etc/memcached/tls/ca.crt,ssl_verify_mode=2printf "version\r\n" |
openssl s_client \
-quiet \
-connect 127.0.0.1:11214 \
-CAfile /etc/memcached/tls/ca.crt上面的 ssl_verify_mode=2 要求客户端证书。没有 -cert/-key 的 openssl s_client 应在握手阶段失败;提供组织 CA 签发的客户端证书后才应返回 VERSION 1.6.45:
printf "version\r\nquit\r\n" |
timeout 3s openssl s_client -quiet \
-connect 127.0.0.1:11214 \
-CAfile /etc/memcached/tls/ca.crt \
-cert /etc/memcached/tls/client.crt \
-key /etc/memcached/tls/client.key应用客户端必须明确支持 Memcached TLS、服务端证书校验和客户端证书;只看到 TCP 建连成功不算验收。
27. warm restart
先确认当前二进制:
memcached -h | grep -Ei 'memory_file|restart'
sudo install -d -o memcache -g memcache /var/lib/memcached/restart示例启动形式:
memcached \
-l 127.0.0.1 \
-p 11215 \
-m 64 \
-I 1m \
-f 1.25 \
-n 48 \
-U 0 \
-e /var/lib/memcached/restart/memory_file \
-o modern,slab_reassign,slab_automove=1 \
-P /run/memcached-warm.pid \
-d写入实验 Key 后必须发送 SIGUSR1 并等待进程完全退出,再用相同二进制和关键参数启动:
pid=$(cat /run/memcached-warm.pid)
kill -USR1 "$pid"
timeout 120s sh -c 'while kill -0 "$1" 2>/dev/null; do sleep 1; done' sh "$pid"systemd 单元应让服务管理器发送 SIGUSR1 并等待主进程自行退出,不能使用只发信号就立即返回的异步 ExecStop:
[Service]
KillSignal=SIGUSR1
TimeoutStopSec=120重启前后必须保持 -m、-I、-f、-n、CAS 开关(不要一侧使用 -C)和是否允许 slab reassignment,并确保二进制版本兼容。停机期间系统时钟必须正确且不能向前或向后跳变,否则 TTL 会失真;不要把 memory file 当作可跨主机复制的备份。停机窗口内到达的 set、add、delete、incr/decr 等更新不会自动重放,必须暂停、缓冲或接受丢失。warm restart 只支持 SIGUSR1 干净退出,不是崩溃恢复;断电、OOM Kill、SIGKILL、损坏文件都应按全量 miss 处理。它与 extstore 不兼容,不能同时启用。
28. extstore
官方 flash storage 文档说明 extstore 从 1.6.0+ 提供。Key、Hash Table 和元数据仍在内存,Value 可以进入 flash;它不是持久化数据库。
memcached -h | grep -Ei 'ext_path|extstore'
sudo install -d -o memcache -g memcache /mnt/memcached-ext
memcached \
-l 127.0.0.1 \
-p 11216 \
-m 256 \
-I 1m \
-U 0 \
-o ext_path=/mnt/memcached-ext/data:4G,ext_wbuf_size=16,ext_threads=2,ext_item_size=512printf "stats settings\r\n" | mc_raw 11216
printf "stats\r\n" | mc_raw 11216 |
grep -Ei "extstore|ext_|bytes|evictions"
iostat -xz 1应使用真实 Value 尺寸和读写比比较启用前后的命中率、P95/P99、磁盘队列、吞吐和 CPU。extstore 只适合 flash/SSD,不适合机械盘;不能与 warm restart 组合,转储到磁盘的对象也不支持 UDP 读取。Key、Hash Table、小对象和 Value 元数据仍占 DRAM,转储到 flash 的 Value 也不是可重启恢复的数据。官方文档明确说明,已转储对象禁用 incr、decr、append 和 prepend;touch、delete、覆盖写和 TTL 到期通常不需要读取 flash。应用依赖这些原地修改命令时必须单独验证或禁用 extstore,不能假设它与纯内存行为等价。flash 文件存在不代表重启后可以像数据库一样恢复。
29. built-in proxy
官方 built-in proxy 文档说明该能力从 1.6.23+ 提供,并需要源码构建启用。
memcached -V
memcached -h | grep -i proxy
./configure --help | grep -i proxy
./configure --enable-proxy
make -j"$(nproc)"
./memcached -h | grep -i proxy使用标准 Route Library 的最小配置,不直接调用底层 mcp_config_*:
pools{
main = {
backends = {
"127.0.0.1:11211",
"127.0.0.1:11212",
"127.0.0.1:11213",
}
}
}
routes{
default = route_direct{ child = "main" }
}该文件只使用 pools{}、routes{} 和 route_direct{}。底层 mcp_config_pools/mcp_config_routes 属于开发或扩展 route library 的 API,不能与标准 Route Library 配置混写。启动形式:
memcached \
-l 127.0.0.1 \
-p 11220 \
-U 0 \
-t 4 \
-o proxy_config=routelib,proxy_arg=/etc/memcached/proxy.luaprintf "set demo:proxy 0 60 2\r\nok\r\nget demo:proxy\r\n" |
mc_raw 11220
for port in 11211 11212 11213; do
printf "get demo:proxy\r\n" | mc_raw "$port"
done代理入口应成功读写,Key 只落在一个后端。生产还要验证代理重启、配置错误、后端超时、连接上限、路由变更和代理自身容量。
30. 版本升级与回退
memcached -V
memcached -h > /tmp/memcached-help.before
printf "stats settings\r\n" | mc_raw 11211 > /tmp/settings.before
printf "stats\r\n" | mc_raw 11211 > /tmp/stats.before
systemctl cat memcached > /tmp/unit.before阅读当前版本到 1.6.45 的发布说明,重点检查启动参数、协议、统计字段、TLS/SASL、extstore、proxy 和默认行为。先启动独立节点,使用真实协议与客户端回归,再把少量流量路由到新节点。
缓存节点升级通常意味着冷缓存。应逐节点替换、控制回源、观察命中率和数据库负载。回退时恢复旧二进制、旧参数和旧 server list,不要期待缓存自动迁回。
memcached -V
diff -u /tmp/memcached-help.before <(memcached -h) || true
printf "version\r\n" | mc_raw 11211
printf "stats settings\r\n" | mc_raw 11211还要执行 Basic Text、Meta、真实客户端、CAS、TTL、节点路由和容量基线实验;高级能力分别执行对应验证。
四、问题处理
1. 连接被拒绝
现象: 客户端返回 Connection refused,nc 无法连接 11211。
影响: 该节点负责的 Key 全部进入超时、miss 或回源路径;若客户端没有快速摘除,线程池会先被连接等待占满。
根因: 进程未启动、参数错误、监听地址不匹配、容器端口未发布、服务持续崩溃或防火墙拒绝。
定位命令:
systemctl status memcached --no-pager
journalctl -u memcached -n 200 --no-pager
ss -lntp | grep 11211
docker compose ps memcached
docker compose logs --tail=200 memcached
nc -vz 127.0.0.1 11211输出判断: systemctl 非 running 或日志有参数错误先修进程;ss 无监听说明尚未到网络层;本机监听正常但远端失败再检查容器映射、防火墙和安全组。nc -vz 成功只证明 TCP,不能证明协议可用。
止损: 让应用快速转为受限回源,降低超时和重试;下游接近上限时限流或降级,不临时指向未知共享实例。
永久解决: 修正启动参数、监听地址、端口映射和防火墙,配置进程守护和协议探测。
验证: 执行 version 及一次 set/get,连续观察应用缓存错误率。
恢复标准: 协议读写成功,应用错误率、回源 QPS 和业务 P99 回到基线。
预防: 对每个实例同时部署进程、端口和 version 三层探针,启动参数变更先在隔离端口验证,并为回源设置硬并发上限。
2. 端口可连但操作超时
现象: TCP 建连成功,get/set 偶发或持续超时。
影响: 客户端连接被长期占用,重试会放大缓存流量并把同一批请求再次压向事实源。
根因: CPU 打满、热 Key、大 Value、网络拥塞、连接队列、宿主机抖动或 extstore I/O。
定位命令:
printf "stats\r\n" | mc_raw 11211 |
grep -E "cmd_get|cmd_set|bytes_read|bytes_written|curr_connections"
pidstat -p "$(pgrep -n memcached)" 1
sar -n DEV 1
ss -tin sport = :11211
docker stats输出判断: cmd_get/cmd_set 增长而响应吞吐停滞并伴随 CPU 高,优先查线程和热点;bytes_written、网卡接近上限时查大 Value;CPU 正常但 extstore 设备 await 高则是 I/O 尾延迟。必须按一分钟增量判断,不能用累计值下结论。
止损: 取消叠加重试,热点启用本地缓存或单飞,限制大 Value,必要时摘除异常节点并保护回源。
永久解决: 根据 CPU、网络、Value 尺寸和线程实测调整节点、-t、连接池与超时。
验证: 用相同请求模型比较 P95/P99、超时率、CPU、网络和回源量。
恢复标准: P99 低于客户端超时安全线,超时和重试不再增长。
预防: 固定客户端总超时、单次操作超时和零或一次重试预算,按 Value 尺寸分桶监控 P99,并定期执行热点与 extstore 延迟压测。
3. 容器内使用 localhost
现象: 宿主机能连接,应用容器连接 127.0.0.1:11211 失败。
影响: 仅容器化实例失去缓存,容易被误判为 Memcached 节点故障并触发无意义重启。
根因: 容器里的 localhost 指向应用容器自身。
定位命令:
docker compose ps
docker compose exec app getent hosts memcached
docker compose exec app sh -c 'nc -vz memcached 11211'
docker port demo-memcached输出判断: getent 无地址说明服务发现或网络不一致;能解析但 nc -vz 失败说明端口、网络或服务监听错误;docker port 只描述宿主机发布,不是容器间访问地址。
止损: 开发环境 endpoint 改为 memcached:11211,不改成生产 IP。
永久解决: 分离宿主机和容器配置,容器网络使用服务发现名。
验证: 在应用容器中发送 version。
恢复标准: 重建容器后仍可通过服务名稳定连接。
预防: 配置模型区分宿主机 endpoint 与容器服务名,CI 在应用容器内执行 DNS、TCP 和协议探针,禁止把 localhost 写死进共享配置。
4. 连接数耗尽或 fd 不足
现象: 新连接被拒绝,listen_disabled_num 或 rejected_connections 增长。
影响: 新请求无法获得连接,已有连接延迟升高;盲目扩应用实例会创建更多连接并恶化故障。
根因: 每请求创建客户端、连接池过大、连接泄漏、-c 太小或 fd 上限不足。
定位命令:
printf "stats\r\n" | mc_raw 11211 |
grep -E "curr_connections|total_connections|rejected_connections|listen_disabled_num"
pid=$(pgrep -n memcached)
cat /proc/"$pid"/limits | grep -i "open files"
ls /proc/"$pid"/fd | wc -l
ss -ant sport = :11211 | awk '{print $1}' | sort | uniq -c输出判断: curr_connections 接近 max_connections 或 stats settings 的 maxconns 且拒绝增量为正,说明服务端上限触发;fd 数接近进程软限制则先修 fd;大量 TIME-WAIT/短连接说明客户端未复用。
止损: 降低应用连接池和实例并发,关闭失控调用方流量,保留管理连接余量。
永久解决: 客户端单例复用,设置池上限和空闲回收,同步调整 -c、LimitNOFILE 与内核限制。
验证: 压测连接建立和复用,观察当前连接稳定且拒绝计数不增加。
恢复标准: 峰值连接率低于安全水位,无新拒绝。
预防: 按“应用实例数 × 每实例连接池上限”预算全池连接,设置 30% 管理余量,对 Δrejected_connections 和 Δlisten_disabled_num 建立即时告警。
5. object too large for cache
现象: 写入返回对象过大错误,读取为 miss。
影响: 指定对象持续绕过缓存,热点大对象会集中回源,并占用网络、序列化 CPU 和应用堆。
根因: 序列化后 item 超过 -I,压缩失效,或缓存了大报表、大 JSON 和文件片段。
定位命令:
printf "stats settings\r\n" | mc_raw 11211 |
grep -E "item_size_max|slab_page_size"
wc -c /tmp/value.bin输出判断: 序列化后字节数加 Key/item 元数据接近或超过 item_size_max 即为尺寸边界;若文件小于上限仍失败,检查客户端压缩后的真实字节、flags 和目标节点实际配置。
止损: 对该对象绕过缓存并限流回源,禁止无上限重试。
永久解决: 控制序列化后尺寸,大对象拆分或转对象存储;调大 -I 前评估 slab、网络和 P99。
验证: 边界尺寸对象正常写入,超限对象被应用预检拦截。
恢复标准: 不再出现服务端大对象错误,网络流量处于预算内。
预防: 在 SDK 写入前记录并限制序列化后尺寸,按 P95/P99 Value 告警;任何 -I 调整都必须配套网络和 slab 压测。
6. out of memory storing object
现象: item 未超过 -I,写入仍返回 out of memory。
影响: 新缓存写入失败导致命中率下降;容器 OOM 时整个节点重启,会把局部写失败扩大为冷启动。
根因: 使用 -M、目标 class 无空闲 chunk、分配失败、容器上限太小或系统内存压力。
定位命令:
printf "stats settings\r\n" | mc_raw 11211 |
grep -E "maxbytes|evictions"
printf "stats\r\n" | mc_raw 11211 |
grep -E "bytes |limit_maxbytes|evictions"
printf "stats slabs\r\n" | mc_raw 11211
dmesg -T | grep -Ei "oom|killed process"输出判断: -M 开启且目标 class 无 free chunk 时写失败而不是淘汰;bytes 未满但目标 class 无页属于 class 压力;内核有 OOM Kill 则容器/主机上限不足,不能只调 -m。
止损: 停止低价值缓存写入,缩短相关 TTL,保护回源;发生 OOM Kill 时先恢复进程和余量。
永久解决: 评估是否保留 -M,治理 Value 和 class 压力,容器上限高于 -m 加额外开销。
验证: 同尺寸压力下不再 OOM,系统无 OOM Kill。
恢复标准: 写入成功率稳定,节点和目标 class 有明确余量。
预防: 同时监控写失败、class free chunk、RSS 与容器上限;容量模型为 -m + Hash Table + 连接 + 线程 + TLS/extstore 缓冲 留安全余量。
7. 命中率持续下降
现象: 服务正常但 miss 增长,数据库 QPS 上升。
影响: 缓存价值下降并把压力转移到数据库、搜索或外部 API,最终可能演变为级联超时。
根因: TTL 太短、Key 版本变化、前缀或路由不一致、节点变更、淘汰、写入失败或访问模式变化。
定位命令:
printf "stats\r\n" | mc_raw 11211 |
grep -E "cmd_get|get_hits|get_misses|cmd_set|evictions|curr_items"
printf "stats items\r\n" | mc_raw 11211输出判断: 用同一窗口计算 Δget_hits/(Δget_hits+Δget_misses);eviction 不增但 miss 增长优先查 TTL、Key 和路由,eviction 与特定 class 同增则查容量与尺寸,节点变更时突降则查重映射。
止损: 保护数据库,热点单飞回源,暂缓节点变更和批量失效。
永久解决: 按业务和节点拆分命中率,统一 Key builder、TTL、路由和写入错误处理。
验证: 固定 Key 集合在多应用、多语言和多节点间写后读一致。
恢复标准: 命中率、回源、数据库延迟和淘汰率恢复基线。
预防: 按业务命名空间记录命中率、写失败和回源原因,Key builder 与 TTL 策略做跨语言契约测试,节点变更前计算重映射。
8. 节点上下线导致大面积 miss
现象: 扩容、缩容或故障后命中率骤降,服务端没有选主日志。
影响: 受重映射 Key 集合同时变冷,数据库承受的不是单节点故障量,而可能是算法导致的更大比例回源。
根因: 客户端映射改变;取模重映射范围大,一致性哈希也会让变化节点负责的 Key 失效。
定位命令:
for port in 11211 11212 11213; do
printf "version\r\n" | mc_raw "$port"
done
python3 hash_lab.py输出判断: 三个版本都正常但 hash_lab.py 或真实客户端向量显示映射变化,根因在路由而非服务端复制;实际 Δget_misses 应与估算重映射比例同量级,明显更高需再查配置漂移。
止损: 冻结继续扩缩容,限制回源,定向预热热点。
永久解决: 使用经验证的路由算法或 proxy,灰度变更并预先计算重映射和数据库承载能力。
验证: 演练中的实际重映射符合预期,下游未超过限额。
恢复标准: 节点集合稳定,命中率和下游负载恢复。
预防: 扩缩容前在全量测试向量上计算移动比例,预留对应回源容量;采用分批放量、热点预热和可一键恢复的旧 server list。
9. 哈希算法或 server list 不一致
现象: A 服务写入后 B 服务持续 miss,各节点都健康。
影响: 跨服务共享缓存失效,表现为随机 miss、重复计算和节点负载偏斜,且不会产生明显服务端错误。
根因: 客户端库、哈希函数、节点顺序、权重、地址格式、DNS 或 Key 编码不同。
定位命令:
for port in 11211 11212 11213; do
echo "PORT=$port"
printf "get dev:interop:user:1001:v1\r\n" | mc_raw "$port"
done再让每种语言输出固定 Key 的目标节点并做 diff。
输出判断: 某节点能直接读到 Value 但另一个客户端返回 miss,说明“Value 可读”与“客户端路由一致”是两件事;排序后的路由文件有任意 diff 即失败。若映射一致再检查序列化和 flags。
止损: 停止跨服务共享该命名空间,临时让读写使用同一客户端配置。
永久解决: 固化列表、顺序、权重、地址、算法和测试向量。
验证: 1000 个固定 Key 的路由逐项一致,四种语言互相读写成功。
恢复标准: 不再出现跨服务静默 miss,分节点命中率均衡。
预防: 把节点标识、顺序、权重、算法选项和 1000 个向量纳入版本化契约;每次客户端升级和 DNS 变更都重新跑 sort/diff。
10. 热点 Key
现象: 缓存池总 CPU 不高,单节点 CPU、网络和 P99 升高。
影响: 单个热点节点先超时,客户端重试和回源进一步放大,而池级平均指标可能仍看似健康。
根因: 相同 Key 固定路由到同一节点,一致性哈希不会复制热点。
定位命令:
for port in 11211 11212 11213; do
printf "stats\r\n" | mc_raw "$port" |
grep -E "cmd_get|get_hits|bytes_written|rusage_user|rusage_system"
done
sar -n DEV 1服务端 stats 不能直接列出热 Key,需要客户端采样或代理观测。
输出判断: 同一分钟内某节点 Δcmd_get、Δbytes_written 或 CPU 显著高于其余节点,且客户端采样集中到少量 Key,即可确认热点;只有总池 QPS 高不能证明热 Key。
止损: 启用进程内短 TTL、singleflight、限流或静态降级。
永久解决: 拆桶或复制只读热点,使用两级缓存并改进访问模式。
验证: 热点压测下目标节点 P99、CPU 和网络下降。
恢复标准: 单节点资源回到安全水位,负载不再严重偏斜。
预防: 监控节点负载离散度和 Key 采样 TopN,对已知超级热点预先使用进程内短 TTL、请求合并或可控分桶,并量化一分钟热点压测结果。
11. 某个 slab class 持续淘汰
现象: 只有某个 class 的 evicted 增长,其他 class 仍有空间。
影响: 该尺寸段的对象命中率下降,相关业务持续回源,而总内存和全池命中率可能掩盖局部问题。
根因: 该尺寸段写入多、TTL 长或复用低,page 不均,automove 来不及或未启用。
定位命令:
printf "stats items\r\n" | mc_raw 11211
printf "stats slabs\r\n" | mc_raw 11211
printf "stats settings\r\n" | mc_raw 11211 |
grep -Ei "slab|growth|automove"输出判断: 对同一 class 比较一分钟 Δevicted、age、used_chunks/free_chunks;目标 class 淘汰持续为正而其他 class 有空页,就是 class 压力。先把 class 的 chunk_size 映射到业务 Value 分桶再处理。
止损: 缩短该类低价值对象 TTL,暂停缓存低复用大对象。
永久解决: 优化 Value 尺寸,校准 growth factor,验证 automove,必要时增加容量。
验证: 相同流量下目标 class 淘汰下降,其他 class 无新异常。
恢复标准: class 级 eviction 和业务 miss 回到可接受范围。
预防: 为每个活跃 slab class 保存 chunk 尺寸、对象来源和淘汰速率,Value schema 变更先做尺寸分布回归,并持续观察 automove 效果。
12. 总内存未满却 eviction
现象: bytes / limit_maxbytes 不高,但 evictions 持续增加。
影响: 团队可能误判为指标错误或盲目扩大全池,实际受压 class 的 miss 和回源仍继续。
根因: 淘汰发生在 class 内,逻辑 bytes 不等于已分配 page;碎片、过期未回收和 class 不均衡都会造成该现象。
定位命令:
printf "stats\r\n" | mc_raw 11211 |
grep -E "bytes |limit_maxbytes|evictions|reclaimed"
printf "stats slabs\r\n" | mc_raw 11211
printf "stats items\r\n" | mc_raw 11211输出判断: bytes 是 item 逻辑字节,不等于 slab page 已分配量;某 class free_chunks 接近零且 Δevicted>0 即使表观使用率低也是真淘汰。reclaimed 增长则说明过期回收,不应与 eviction 混为一谈。
止损: 找到具体 class 后治理对应尺寸对象,不立即全池扩容。
永久解决: 使用 class 级监控、合理 TTL 和尺寸,验证 crawler、maintainer 与 automove。
验证: 目标 class 的 free chunk、page 和 eviction 趋势改善。
恢复标准: eviction 与容量模型一致,命中率不再下降。
预防: 容量面板同时展示逻辑 bytes、各 class page/chunk、eviction/reclaimed 增量,禁止只依据单一内存百分比扩容。
13. 内部碎片过高
现象: item 有效字节不大,slab 与 RSS 占用却高。
影响: 同等内存能保存的有效对象减少,成本上升并提前触发 class 淘汰;错误调参还可能增加 CPU 与管理开销。
根因: Value 集中在 chunk 边界之后、growth factor 不适合、序列化膨胀或小对象元数据占比高。
定位命令:
printf "stats slabs\r\n" | mc_raw 11211
printf "stats sizes\r\n" | mc_raw 11211
printf "stats settings\r\n" | mc_raw 11211 |
grep -E "growth_factor|chunk_size|slab_page_size"输出判断: 将对象原始尺寸分桶映射到 chunk_size,按 数量 × (chunk_size-原始尺寸) 估算内部碎片;若主要浪费集中在少数边界尺寸,先改序列化或对象结构,不先全局改 -f。
止损: 暂停高浪费低价值对象,避免盲目调大 -I。
永久解决: 优化序列化,按尺寸分布评估 -f、-n,移除极端大对象。
验证: 同数据集比较调整前后的实际 chunk、page、命中率和 P99。
恢复标准: 单位有效字节成本下降且性能没有回退。
预防: 保存 P50/P90/P99 Value 与 class 边界基线,schema 或压缩策略变化时重算碎片;-f、-n 只能通过同数据集 A/B 压测发布。
14. TTL 超过 30 天立即失效
现象: set 返回 STORED,马上 get 却为 END。
影响: 长 TTL 数据全部变成即时 miss,形成难以从服务端错误发现的持续回源。
根因: 超过 2592000 的 exptime 被解释为绝对 Unix 时间戳。
定位命令:
date +%s
printf "set ttl:wrong 0 3888000 2\r\nok\r\nget ttl:wrong\r\n" |
mc_raw 11211输出判断: 服务端时间戳远大于 3888000,写入返回 STORED 而读取 END,即可确认该值被当成过去的绝对时间。不要把它归因于 eviction,实验前后检查 eviction 增量应为零。
止损: 临时使用不超过 30 天的相对 TTL,或由统一库计算未来时间戳。
永久解决: 在缓存 SDK 封装边界,跨语言建立相同测试。
验证: 使用 当前时间 + 45 天 的绝对时间戳后可立即读回。
恢复标准: 长期 TTL Key 可读,各语言计算结果一致。
预防: SDK 统一封装 2592000 边界并做 30 天、30 天加 1 秒、45 天三个契约测试,禁止业务直接传裸秒数。
15. CAS 持续冲突
现象: cas 高频返回 EXISTS,重试后延迟和流量上升。
影响: 热点更新形成自旋式重试,放大读取、计算和网络;缓存中的聚合值可能长期更新失败。
根因: 多请求修改同一热点 Key、计算窗口过长、复用旧 token 或吞掉冲突。
定位命令:
printf "gets hot:cas\r\n" | mc_raw 11211
printf "stats\r\n" | mc_raw 11211 |
grep -E "cas_hits|cas_misses|cas_badval"输出判断: 一分钟 Δcas_badval 高且 Δcas_hits 低说明真实并发冲突;cas_misses 增长表示 Key 已过期或被删。Shell 提取 token 时必须删除行尾 \r,否则是请求格式问题而非 CAS 冲突。
止损: 限制重试次数和总时限,对热点更新排队、合并或转事实数据库。
永久解决: 缩短读写窗口,减少共享热点,重新判断是否应使用数据库事务。
验证: 并发实验中冲突可识别,超过上限后安全失败。
恢复标准: cas_badval、业务重试和热点延迟恢复基线。
预防: 每次 CAS 最多有限次数并受总时限约束,记录冲突率与 Key 维度热点;高争用事实更新迁移到数据库事务或专用原子模型。
16. noreply 隐藏失败
现象: 客户端没有错误,后续读取却持续 miss。
影响: 写失败完全不可见,命中率和下游负载恶化后才被发现,排障无法关联到具体请求。
根因: 写命令使用 noreply,对象过大、资源不足或参数错误没有返回调用方。
定位命令:
printf "stats\r\n" | mc_raw 11211 |
grep -E "cmd_set|store_too_large|store_no_memory"
printf "get suspected:key\r\n" | mc_raw 11211统计字段以当前版本实际输出为准,同时检查客户端请求和 Value 尺寸。
输出判断: cmd_set 增长但目标 Key 不存在不能单独证明服务端失败;关闭 noreply 重放相同请求,若得到 CLIENT_ERROR/SERVER_ERROR 才能确认。统计字段不存在时不要编造告警,改用客户端结果计数。
止损: 关闭关键路径 noreply,恢复响应校验并限制回源。
永久解决: 只在可忽略结果且有独立错误指标时使用,客户端预检尺寸和参数。
验证: 超限对象收到错误,正常对象返回 STORED 并可读回。
恢复标准: 写失败可观测,写后异常 miss 恢复。
预防: 默认禁用 noreply;例外流量必须有写后抽样、Value 尺寸预检和独立 miss/回源告警,并在客户端升级时回归错误传播。
17. flush_all 引发雪崩
现象: 大量 Key 同时 miss,下游突然过载。
影响: 缓存雪崩会在秒级把全量读流量推向事实源,可能导致数据库连接耗尽和业务级联失败。
根因: 人工、GUI、脚本或 CI 执行 flush_all,实例未使用 -F。
定位命令:
printf "stats settings\r\n" | mc_raw 11211 |
grep flush_enabled
printf "stats\r\n" | mc_raw 11211 |
grep -E "get_hits|get_misses|curr_items|cmd_flush"输出判断: Δcmd_flush>0 与 curr_items、命中率在同一时间窗骤降可确认 flush;若 cmd_flush 不变则继续查重启、路由和批量 Key 版本切换。
止损: 立即限制回源、启用降级并按热点分批预热,不再次 flush。
永久解决: 生产使用 -F,主动失效走明确 Key 或版本化 Key。
验证: flush_all 返回 CLIENT_ERROR flush_all not allowed,单 Key 删除仍可用。
恢复标准: miss 洪峰结束,下游资源和热点命中恢复。
预防: 生产统一启用 -F,网络层限制管理来源,GUI/脚本移除 flush 权限;主动失效只允许明确 Key 或版本化命名空间并配置回源保护。
18. 冷启动回源风暴
现象: 进程已恢复,但应用延迟和数据库负载持续异常。
影响: 节点恢复不等于业务恢复;事实源可能在缓存升温前被压垮,使 RTO 从分钟延长到数十分钟。
根因: 重启后缓存为空,所有请求同时回源;没有单飞、预热、限流或两级缓存。
定位命令:
printf "stats\r\n" | mc_raw 11211 |
grep -E "uptime|curr_items|get_hits|get_misses"
journalctl -u memcached --since "-30 min" --no-pager输出判断: uptime 很小、curr_items 从低位增长且窗口命中率逐步恢复,说明冷启动;若 uptime 未变化则查 flush 或路由。把 miss 峰值与数据库 QPS、连接等待放在同一时间轴确认放大链。
止损: 限制回源并发,启用 stale 或本地缓存,按热点预热,降低非核心流量。
永久解决: 把冷启动纳入容量设计,实施 singleflight、TTL 抖动、分批替换和下游熔断。
验证: 预生产清空单节点,测量回源峰值、命中恢复曲线和 P99。
恢复标准: 命中率达到目标,下游 QPS、连接池和业务错误恢复。
预防: 每季度演练单节点清空,记录命中率恢复到 80%、90%、基线所需时间;预置 singleflight、TTL 抖动、热点清单和数据库回源令牌桶。
19. 多语言序列化不兼容
现象: Key 路由一致,但另一语言读取乱码、反序列化失败或当作 miss。
影响: 同一缓存池出现数据解释分裂,轻则重复回源,重则把错误对象返回业务或触发不安全反序列化。
根因: Java 原生序列化、Python pickle、压缩算法、flags、字符集或 schema 不一致。
定位命令:
printf "gets dev:interop:user:1001:v1\r\n" | mc_raw 11211保存原始 Value 字节、flags 和 CAS,分别让各客户端解码。
输出判断: 直接协议读取的 bytes 和 flags 一致但某语言失败,是编码/schema/压缩问题;目标节点不同则先修路由。JSON 字节能读不代表语义兼容,还要断言字段类型和版本。
止损: 停止共享该命名空间并回源,不用错误反序列化器解析不可信字节。
永久解决: 统一 UTF-8 JSON/MessagePack、flags、压缩阈值和 schema 版本。
验证: 四种语言轮流写入,其他语言逐一读回并断言。
恢复标准: 互通测试通过,反序列化错误和异常 miss 归零。
预防: 共享池只允许版本化 UTF-8 JSON/MessagePack 契约,禁用 Java 原生序列化和 Python pickle;四语言轮流写读的黄金样本进入 CI。
20. 客户端超时重试放大
现象: Memcached 轻微变慢后,请求、连接和下游回源同时增加。
影响: 一次用户请求触发多次缓存尝试和多次回源,系统负载按重试层数乘法放大。
根因: 多层重试叠加、超时预算不一致、失败后无上限回源、每次重试新建连接。
定位命令:
printf "stats\r\n" | mc_raw 11211 |
grep -E "cmd_get|curr_connections|total_connections|bytes_read"
ss -s结合客户端指标统计每个业务请求的缓存尝试和回源次数。
输出判断: Δcmd_get / 业务读取数 或 Δtotal_connections / 业务请求数 明显高于基线,且客户端重试计数同步增长,即为放大;连接增长但请求不增则查连接泄漏。
止损: 把重试降为零或一次,启用快速失败、单飞、熔断和限流。
永久解决: 建立统一超时预算;缓存超时短于业务和数据库超时,重试带上限和抖动。
验证: 连续注入 10 分钟延迟,要求“缓存尝试次数 / 业务请求数”不超过 1.1、“回源次数 / 业务请求数”不超过 1,故障窗口连接数不超过故障前基线的 120%,并确认没有指数增长。
恢复标准: 连接、回源、重试和业务 P99 稳定。
预防: 端到端预算明确每层最多尝试次数,缓存失败默认快速降级而不是多层重试;故障注入验收放大倍数不得超过设计值。
21. SASL 认证失败
现象: SASL 客户端返回认证错误,Basic Text nc 不能代表真实连接。
影响: 连接池无法建立导致缓存全量旁路;若错误回退为无认证端口,会把可用性故障升级为安全暴露。
根因: 二进制未编译 SASL、启动参数、数据库路径或服务名错误,客户端不支持对应流程。
定位命令:
memcached -h | grep -i sasl
ps -ef | grep '[m]emcached'
sudo sasldblistusers2 -f /etc/sasl2/memcached-sasldb2
journalctl -u memcached -n 200 --no-pager输出判断: memcached -h 无 SASL 或进程没有 -S 是构建/启动问题;用户库无目标用户或服务用户不可读是配置问题;正确凭据失败、错误凭据也失败要看 SASL 日志,未认证却成功则是严重配置错误。
止损: 不把端口临时开放成无认证公网服务;回退到受防火墙保护的旧路径。
永久解决: 固定 SASL 构建、配置和客户端,轮换凭据,评估弃用 Binary Protocol 的风险。
验证: 错误密码失败,正确密码可 set/get,未认证连接不能访问。
恢复标准: 认证成功率正常,无匿名绕过,连接池完成凭据切换。
预防: 构建产物记录 SASL 能力,发布门禁分别跑未认证、错误密码、正确密码三条 Binary 链路;凭据轮换先双凭据灰度,不开放无认证回退。
22. TLS 握手失败
现象: TCP 可连,但返回未知 CA、证书过期、主机名不匹配或 handshake failure。
影响: 所有新建 TLS 连接失败,连接池耗尽后缓存访问中断;关闭校验的临时止损会引入中间人风险。
根因: 二进制无 TLS、证书链不完整、私钥不匹配、CA 错误、客户端不支持或端口混用。
定位命令:
memcached -h | grep -Ei 'ssl|tls'
openssl x509 -in server.crt -noout -subject -issuer -dates
openssl x509 -noout -modulus -in server.crt | openssl sha256
openssl rsa -noout -modulus -in server.key | openssl sha256
openssl s_client -connect 127.0.0.1:11214 -CAfile ca.crt -showcerts输出判断: 证书与私钥 modulus 摘要不同是配对错误;Verify return code 非 0 查 CA/链/SAN/期限;mTLS 模式下无客户端证书必须失败,有证书仍失败再查 ssl_ca_cert 和 ssl_verify_mode=2。
止损: 回退到私网和防火墙保护的已验证 endpoint,不关闭证书校验。
永久解决: 修复证书链、SAN、CA 和 TLS 参数,建立到期监控和轮换。
验证: 通过 openssl s_client 发送 version,错误证书应失败。
恢复标准: 校验和协议读写成功,未授权客户端按策略失败。
预防: 对服务端和客户端证书做 30/14/7 天到期告警,轮换前并行信任新旧 CA;发布门禁禁止 insecure/跳过主机名校验。
23. UDP 暴露
现象: 安全扫描发现 11211/udp 可访问。
影响: 未授权数据访问之外,还可能被利用进行 UDP 反射放大,扩大到互联网安全事件。
根因: 旧配置显式启用 UDP、防火墙放行或升级沿用历史参数。
定位命令:
ss -lunp | grep 11211
printf "stats settings\r\n" | mc_raw 11211 | grep udpport
nmap -sU -p 11211 127.0.0.1输出判断: ss 出现 memcached UDP socket 或 udpport 非 0 即配置失败;扫描显示 open 需要立即阻断,filtered 只说明网络过滤,仍要确认进程没有监听。
止损: 立即在安全组和防火墙阻断 UDP 11211。
永久解决: 启动显式使用 -U 0,加入配置审计和端口扫描。
验证: ss 无 UDP 监听,网络扫描关闭或过滤,TCP 业务正常。
恢复标准: 所有节点 UDP 关闭,安全告警消失。
预防: 所有启动模板显式 -U 0,镜像与 systemd 变更后自动检查 socket;云安全组同时拒绝公网 TCP/UDP 11211。
24. stats sizes 无输出
现象: 返回 STAT sizes_status disabled,或团队按旧资料担心查询会阻塞实例。
影响: 容量团队缺少对象尺寸分布,可能错误调节 growth factor;为临时查询重启生产又会造成冷启动。
根因: 1.6.45 需要启动时通过 -o track_sizes 开启持续尺寸统计;旧版本实现不同。
定位命令:
memcached -V
printf "stats settings\r\n" | mc_raw 11211 | grep track_sizes
printf "stats sizes\r\n" | mc_raw 11211输出判断: sizes_status disabled 且 settings 未启用 track_sizes 是预期配置结果,不是故障;旧版本没有该字段时必须按对应版本文档判断,不能套用 1.6.45 行为。
止损: 巡检改用 stats、stats items、stats slabs,不为临时查询重启生产。
永久解决: 在压测环境评估 track_sizes 持续开销,随受控发布启用。
验证: 测试实例返回尺寸分布,资源开销在预算内。
恢复标准: 巡检不再依赖错误假设,生产无性能回退。
预防: 版本升级时归档 stats settings 与支持字段,尺寸统计优先在影子节点长期采样;启用 track_sizes 前记录 CPU/P99 基线。
25. extstore I/O 异常
现象: 命中请求 P99 升高,磁盘队列和 I/O wait 增长。
影响: 原本微秒级内存缓存变成受设备尾延迟控制,超时会触发回源;设备故障可使单节点大量 Value 不可读。
根因: flash 性能不足、访问局部性差、设备异常或参数与对象尺寸不匹配。
定位命令:
printf "stats\r\n" | mc_raw 11216 | grep -Ei "extstore|ext_|evictions"
iostat -xz 1
pidstat -d -p "$(pgrep -n memcached)" 1
df -h /mnt/memcached-ext
dmesg -T | grep -Ei "nvme|i/o error|reset"输出判断: 设备 await、队列深度和业务 P99 同窗上升,且 extstore 读写计数增长,说明 I/O 路径受压;内核 reset/error 表示设备问题。机械盘、UDP 或同时启用 warm restart 属于不支持的部署组合,应直接整改。
止损: 降低写入和大 Value 流量,摘除异常节点并限制回源。
永久解决: 重新选择设备、容量和参数,按真实工作集压测并监控设备健康。
验证: 使用与修复前相同的 15 分钟负载,要求块设备错误增量为 0,业务 P99 不高于健康基线的 110%;await 和队列深度不得超过该设备在容量压测中确定的退出阈值。
恢复标准: extstore 错误停止,业务延迟和回源稳定。
预防: 只使用有耐久与延迟预算的 flash,监控寿命、错误、队列和空间;按 append/prepend、CAS、算术等真实操作集做转储对象回归,禁止把 extstore 文件纳入恢复承诺。
26. 升级后参数或协议变化
现象: 新版本启动失败、未知参数、统计字段消失或客户端测试失败。
影响: 滚动升级中节点集合反复变化并产生冷缓存,参数漂移还可能关闭安全能力或改变内存行为。
根因: 未阅读 ReleaseNotes、沿用变化的 -o 参数、构建能力不同或依赖旧协议行为。
定位命令:
memcached -V
memcached -h > /tmp/memcached-help.after
diff -u /tmp/memcached-help.before /tmp/memcached-help.after || true
journalctl -u memcached -n 200 --no-pager
printf "stats settings\r\n" | mc_raw 11211输出判断: help diff 中参数删除/默认变化要逐项解释;启动日志 unknown option 不能继续滚动;协议测试或真实客户端路由向量任一失败都应回退。统计字段变名需同步监控而非静默丢数据。
止损: 停止继续滚动,恢复旧二进制、旧参数和原 server list,限制冷启动回源。
永久解决: 固定制品和构建参数,逐版本审阅发布说明,建立协议、CAS、TTL、客户端和路由回归。
验证: 独立节点通过完整实验,灰度指标与旧版基线一致。
恢复标准: 节点版本和参数一致,错误率、命中率、P99、淘汰与回源正常。
预防: 固定二进制摘要和完整启动参数,维护逐补丁 ReleaseNotes 差异;canary 必须跨过峰值窗口并通过 Basic、Meta、路由、安全和容量回归。
27. built-in proxy 路由异常
现象: 代理端口可连但 Key 落点错误、后端超时或所有请求集中到单节点。
影响: 统一入口成为共享故障域,错误路由会造成大面积 miss、热点或后端不可达,且影响所有接入方。
根因: 二进制未启用 proxy、Lua API 与版本不匹配、后端地址或规则错误、代理容量不足。
定位命令:
memcached -h | grep -i proxy
journalctl -u memcached-proxy -n 200 --no-pager
printf "version\r\n" | mc_raw 11220
for port in 11211 11212 11213; do
printf "stats\r\n" | mc_raw "$port" |
grep -E "cmd_get|cmd_set|curr_connections"
done输出判断: 帮助无 proxy 表示制品错误;启动日志出现 Route Library 解析错误时不能接流量;固定 Key 后只有预期后端 Δcmd_get/set 增长才算路由正确,所有流量集中一节点则检查 pool 和 Key 分布。
止损: 回退到已验证客户端分片,冻结代理配置发布并限制回源。
永久解决: 按 1.6.45 官方示例校准 Lua API,建立固定 Key 路由测试、灰度和容量监控。
验证: 用固定种子的 1000 个 Key 比较期望路由与实际后端增量,要求路由差异数为 0、代理错误率为 0,各后端请求占比与设计权重的偏差不超过 10%;再分别执行后端故障和代理重启实验。
恢复标准: 代理错误率和延迟正常,后端分布符合预期。
预防: 配置只使用经过版本锁定的 pools/routes/route_direct 词汇,发布前做语法加载、1000 Key 映射、后端故障和代理容量测试;保留客户端直连回退但不自动双重重试。
28. warm restart 未恢复 item
现象: 使用 memory file 重启后 Key 全部丢失。
影响: 预期热启动实际变为全冷缓存,回源容量不足时业务恢复时间显著延长。
根因: 非干净退出、启动参数变化、文件权限或路径错误、版本不兼容或二进制不支持。
定位命令:
memcached -h | grep -Ei 'memory_file|restart'
ls -lh /var/lib/memcached/restart/
journalctl -u memcached -n 200 --no-pager
ps -ef | grep '[m]emcached'输出判断: 日志没有成功恢复记录、memory file/.meta 缺失或权限错误即未恢复;对比前后 -m、-I、-f、-n、CAS、slab reassignment 和版本,任一不兼容都不能强行复用文件。SIGKILL/OOM 后失败属于能力边界。
止损: 按普通冷启动处理,启动回源限流和预热,不反复重启碰运气。
永久解决: 固化启动参数、干净停止方式、文件权限和版本回归,并保留全丢假设。
验证: 隔离环境写入带 TTL Key,干净停止和重启后验证 Value 与 TTL。
恢复标准: 业务先从冷缓存恢复;实验可重复时才保留该能力。
预防: systemd 使用 KillSignal=SIGUSR1,由服务管理器发送信号并等待进程退出;每次版本或参数变更做干净停止恢复实验,同时保留全量 miss 限流方案。禁止与 extstore 组合。
29. 节点反复加入和移除
现象: 节点网络抖动,客户端反复剔除和加入,命中率与回源呈锯齿。
影响: 每次集合变化都重新映射 Key,短暂网络问题被扩大为持续缓存失效和连接风暴。
根因: 失败阈值、重试间隔、dead timeout 和恢复探测过于激进,各应用状态不一致。
定位命令:
for port in 11211 11212 11213; do
nc -vz -w 1 127.0.0.1 "$port"
done
ss -s结合客户端日志统计节点移除、恢复、重连和映射变化。
输出判断: 节点 add/remove 事件与命中率下降按相同周期重复,且服务本身 uptime 未变化,即为成员抖动;不同应用观察到不同集合说明客户端健康状态没有收敛。
止损: 从配置层固定移除抖动节点,限制回源,停止自动反复加入。
永久解决: 调整失败阈值、dead timeout 和恢复探测;恢复节点先观察和预热。
验证: 配置连续失败 3 次后摘除,恢复连续成功 5 次并稳定观察 60 秒后加入;注入短于失败窗口的抖动不得改变集合,持续故障必须按该参数摘除,随后连续观察 10 分钟,非计划成员变化次数应为 0。
恢复标准: 节点集合稳定,命中率不再锯齿。
预防: 失败阈值覆盖短网络毛刺,恢复使用连续成功探测和观察窗;统一客户端参数并告警每十分钟成员变化次数,节点重新加入前先验证容量和路由。
30. Session 大面积失效
现象: 节点重启或扩缩容后大量用户被要求重新登录。
影响: 用户会话中断并形成登录洪峰,身份服务、短信和风控系统可能同时过载。
根因: Session 只在无复制 Memcached,路由变化或 item 丢失;应用没有可接受的重新认证机制。
定位命令:
for port in 11211 11212 11213; do
printf "stats\r\n" | mc_raw "$port" |
grep -E "uptime|curr_items|get_hits|get_misses"
done同时核对身份服务登录量、Session miss 和节点变更时间。
输出判断: 节点 uptime 变小或集合变化与 Session miss、登录 QPS 同时突增即可确认缓存丢失链;普通业务 Key 命中正常但 Session miss 增长则检查 Session TTL、命名空间和滑动续期。
止损: 限制登录洪峰,延长仍存活 Session 的 TTL,暂停节点变更并保护身份服务。
永久解决: 明确允许重新登录的边界;不允许时迁移到具备复制和恢复的会话存储,或采用可验证签名令牌。
验证: 节点故障演练先使用业务批准的会话 SLO;例如允许重新认证的系统可要求登出用户比例不超过 1%、登录错误率不超过 0.1%、身份服务 QPS 不超过压测容量的 70%、5 分钟内恢复。任何一项不可接受时,结论不是继续调 Memcached,而是迁移到具备复制与恢复能力的会话存储。
恢复标准: 登录错误率、身份服务负载和 Session 命中率恢复,业务 RTO 达标。
预防: 明确单节点与全池丢失时可接受的登出比例,季度演练并对登录链路限流;不允许会话中断的系统不得把无复制 Memcached 作为唯一 Session 存储。
