Memcached 部署方式、客户端分片与提效工具手册
从“加了三个节点却没有高可用”开始
一个缓存池从一台扩到三台后,某台节点重启,命中率突然下跌,数据库流量陡升。服务端日志里没有选主失败,因为 Memcached 服务端之间本来就不复制、不协商、不选主;客户端根据 key 选择节点,节点消失后,那部分 key 只能 miss 并回源。它不是 Redis 的简化版,而是一套把路由和容错责任明确交给客户端、代理或平台的内存对象缓存。
下面用 Docker Compose 启动单实例,再用 text protocol 观察 item、TTL、slab 和淘汰证据。需要 Docker Engine 或 Docker Desktop、空闲的 11211 端口,以及 nc 或 telnet。语言客户端实验再准备对应项目的本地 profile。生产 endpoint、内网地址、真实 key/value 和带连接信息的截图不能进入仓库。
先确认本机状态:
docker version
docker compose version
docker ps --format "table {{.Names}}\t{{.Ports}}"Windows:
netstat -ano | findstr 11211Linux / macOS:
lsof -i :11211如果端口被占用,先确认旧实例来源。不要把本地应用临时指向共享缓存池。
Windows 可用 netstat,Linux 或 macOS 可用 lsof 确认端口来源。端口被占用时先识别旧实例,不要把应用临时指向共享缓存池。
把 Memcached 跑起来
示例固定为 memcached:1.6.45,避免 latest 在团队机器间漂移。上游采用 BSD 3-Clause 许可;Docker Official Image 的基础系统和依赖还有各自许可,进入制品仓库时仍要按镜像清单审查。该版本的默认 TCP 端口是 11211,UDP 自 1.5.6 起默认关闭;模板仍显式传入 -U 0,让安全意图能在代码审查中被看见。
Memcached 的入口比 Redis / Valkey 简单,但越简单越容易被写错。
| 方式 | 适合场景 | 注意点 |
|---|---|---|
| 本机包管理器 | 需要本机服务或 CLI 协议验证 | 发行版版本和编译选项不一致 |
| 源码安装 | 验证特定编译参数和高级特性 | 需要 libevent 和构建环境 |
| Docker 单容器 | 临时验证 text protocol 或客户端 | 不适合作为长期团队模板 |
| Docker Compose | 项目级联调和新人一键启动 | 推荐写入项目依赖模板 |
| 共享缓存池 | 多服务共用、稳定联调 | 必须有 owner、容量、key 命名、清理规则和误连生产防护 |
本机安装边界
不同系统包版本不同。安装后先看:
memcached -V
memcached -h | sed -n "1,80p"源码安装从 memcached.org/downloads 获取 tarball,并按其依赖和构建流程执行。团队开发模板仍适合用 Docker / Compose 固定版本和启动参数。
Docker 单容器
临时启动:
docker run --rm --name memcached-smoke -p 127.0.0.1:11211:11211 memcached:1.6.45查看版本和帮助:
docker run --rm memcached:1.6.45 memcached -V
docker run --rm memcached:1.6.45 memcached -h单容器没有团队命名、容量、连接、清理和项目接入,不作为最终模板。
主流部署方式
| 方式 | 架构组成 | 适合场景 | 核心风险 |
|---|---|---|---|
| 单机单实例 | 一个 Memcached 进程 | 本机开发、低价值缓存、边车缓存 | 重启丢缓存,容量和连接受单机限制 |
| 单机多实例 | 一台机器多个端口和实例 | 测试环境、多项目轻量隔离 | 共享硬件故障域,资源抢占 |
| 多节点缓存池 | 多个 Memcached 节点,客户端按 hash 分片 | 常见生产缓存池、读多写少、可回源缓存 | 节点上下线带来 miss 风暴,客户端配置必须一致 |
| built-in proxy | 前端 proxy 连接后端池 | 连接收敛、路由治理、复杂缓存池 | 高级主题,配置和排障复杂 |
| 云托管 | 云厂商 Memcached 兼容服务 | 少运维、统一网络和监控 | 版本、命令、TLS、扩缩容和费用以云官方为准 |
Memcached 服务端之间没有复制、选主或一致性协议。增加节点是增加缓存容量和吞吐,不是增加数据可靠性。任何节点失败都应被应用视为 cache miss,并能回源。
推荐目录:
your-project/
compose.yaml
.env.example
cache/
memcached/
verify/
smoke.txt
docs/
dependency-setup.md
scripts/
memcached-stats.sh
memcached-flush-dry-run.sh.env.example
MEMCACHED_IMAGE=memcached:1.6.45
MEMCACHED_HOST_PORT=11211
MEMCACHED_MEMORY_MB=128
MEMCACHED_MAX_CONNECTIONS=1024
MEMCACHED_THREADS=4
MEMCACHED_ITEM_SIZE=1m
MEMCACHED_KEY_PREFIX=dev:your-project:没有密码不是疏忽。Memcached 常规开源形态更依赖网络隔离;SASL / TLS 需要编译和客户端支持,不能把它写成默认可用。共享环境必须用防火墙、安全组、Kubernetes NetworkPolicy、私网 endpoint 或云托管访问控制兜住。
compose.yaml
services:
memcached:
image: ${MEMCACHED_IMAGE:-memcached:1.6.45}
container_name: your-project-memcached
command:
- memcached
- -m
- "${MEMCACHED_MEMORY_MB:-128}"
- -c
- "${MEMCACHED_MAX_CONNECTIONS:-1024}"
- -t
- "${MEMCACHED_THREADS:-4}"
- -I
- "${MEMCACHED_ITEM_SIZE:-1m}"
- -U
- "0"
- -F
ports:
- "127.0.0.1:${MEMCACHED_HOST_PORT:-11211}:11211"
restart: unless-stopped说明:
宿主机端口只绑定 127.0.0.1。-m 明确 item storage 上限。-c 明确连接上限。
-t 明确线程数,不盲目调大。-I 明确单 item 上限。-U 0 明确关闭 UDP。
-F 禁用 flush_all,避免把危险清理当普通操作。
这份 Compose 不内置 healthcheck,因为 Docker Official Image 不应被默认假设带有 nc 或 telnet。团队如果需要容器内 healthcheck,要用自建镜像补齐探测工具;项目默认验证放在宿主机或 CI 脚本中执行。
如果你要测试 flush_all 的语义,不要在项目默认 Compose 里启用。单独起一个临时容器验证。
启动:
docker compose up -d memcached
docker compose ps memcached
docker compose logs --tail=80 memcached查看版本和运行配置:
printf "version\r\n" | nc 127.0.0.1 11211
printf "stats settings\r\n" | nc 127.0.0.1 11211 | sed -n "1,40p"写入、读取、延长 TTL、删除:
{
printf "set dev:your-project:smoke 0 60 2\r\n"
printf "ok\r\n"
printf "get dev:your-project:smoke\r\n"
printf "touch dev:your-project:smoke 120\r\n"
printf "delete dev:your-project:smoke\r\n"
printf "get dev:your-project:smoke\r\n"
} | nc 127.0.0.1 11211预期看到:
STORED
VALUE dev:your-project:smoke 0 2
ok
END
TOUCHED
DELETED
END查看容量和 slab:
printf "stats\r\n" | nc 127.0.0.1 11211 | sed -n "1,60p"
printf "stats items\r\n" | nc 127.0.0.1 11211
printf "stats slabs\r\n" | nc 127.0.0.1 11211 | sed -n "1,80p"当前模板没有启用 -o track_sizes,执行 stats sizes 会返回 STAT sizes_status disabled,不会给出尺寸分布。Memcached 1.4.27 之前的旧实现会扫描 item,存在阻塞实例的风险;当前版本改为启动时开启额外计数。需要尺寸分布时,应先在压测环境评估 track_sizes 的内存与计数开销,再随配置变更重启,而不是在巡检脚本里临时假设它可用。
验证 flush_all 被禁用:
printf "flush_all\r\n" | nc 127.0.0.1 11211若启用了 -F,1.6.45 返回 CLIENT_ERROR flush_all not allowed。若返回 OK,说明项目默认模板没有防住高风险命令;同时检查 stats settings 中的 flush_enabled 应为 no。
item 怎样进入 slab,又怎样被替换
text protocol 的一条 set 会携带 key、flags、过期时间和 value 长度。服务端先按 value 与 item 元数据所需空间选择 slab class,再从对应 class 的固定大小 chunk 中分配位置,并把 key 放入 hash table 以便查找。不同 class 的 chunk 大小不同,所以 -m 128 只限定 item storage 的预算,不保证 128MB 都能被当前 value 尺寸充分利用;进程还会消耗连接、线程、hash table 和其他元数据内存。
读取通过 hash table 找到 item,命中会影响其热度位置;内存压力下,淘汰在 slab class 内发生。于是“总容量尚有余量但某类大对象持续 eviction”并不矛盾:对应 slab class 可能已经紧张。先用 stats items 看各 class 的 item、年龄和淘汰,再用 stats slabs 看 chunk 使用情况,不能只盯进程 RSS。
CAS 能直接看到并发更新的竞争分支。下面先读取 CAS token,再让另一个写入改变 token,最后用旧 token 更新:
printf "set dev:your-project:cas 0 60 1\r\nA\r\n" | nc 127.0.0.1 11211
cas_token=$(
printf "gets dev:your-project:cas\r\n" | nc 127.0.0.1 11211 |
awk '/^VALUE / {print $5}'
)
printf "set dev:your-project:cas 0 60 1\r\nB\r\n" | nc 127.0.0.1 11211
printf "cas dev:your-project:cas 0 60 1 %s\r\nC\r\n" "$cas_token" | nc 127.0.0.1 11211
printf "get dev:your-project:cas\r\n" | nc 127.0.0.1 11211
printf "delete dev:your-project:cas\r\n" | nc 127.0.0.1 11211预期旧 token 的更新返回 EXISTS,随后读到 B。这不是网络错误,而是服务端发现 item 的 CAS 值已经变化,拒绝覆盖竞争者的新值。若客户端把 EXISTS 当成功吞掉,就会制造静默丢更新;正确做法是重新读取、重新计算,并为重试设置次数和总时限。
TTL 也有一个容易误判的分支:ASCII 协议中 0 表示不过期,最多 30 天的数值按相对秒数解释,超过 30 天则按 Unix 时间戳解释。把“45 天”直接写成 3888000 会落在 Unix 纪元起点附近并立即过期;长期 TTL 应由客户端转换成未来绝对时间戳,并用读回实验验证。
Memcached 的项目接入不是“装一个客户端”。要把缓存语义写进应用和模板。
Java / Spring
如果项目使用 Spring Cache 或自选 Memcached client,评审这些点:
客户端是否还在维护。是否支持连接池、超时、失败重试、TLS / SASL。序列化格式是否跨语言可读。
server list 是否固定顺序和一致 hash 策略。应用是否把 Memcached miss 当正常路径处理。
示例配置只表达字段,不指定唯一客户端:
cache.memcached.servers=127.0.0.1:11211
cache.memcached.key-prefix=dev:your-project:
cache.memcached.connect-timeout=500ms
cache.memcached.operation-timeout=1s
cache.memcached.default-ttl=300s
cache.memcached.max-value-bytes=1048576Node / Python / Go
同样不指定唯一客户端。接入检查:
| 维度 | 检查点 |
|---|---|
| 协议 | text / binary / meta text 是否与服务端一致 |
| 连接 | 是否复用长连接和连接池 |
| 超时 | 缓存慢故障是否快速失败 |
| 分片 | 多节点 server list 和 hash 算法是否全语言一致 |
| 序列化 | JSON / MessagePack / pickle / Java serialization 是否可治理 |
| TTL | 默认 TTL 是否明确,永久 key 是否有 owner |
| key | 是否包含环境和项目 prefix,是否控制 250 bytes 限制 |
客户端分片
Memcached 多节点池通常由客户端做分片:
app -> client hash(key) -> memcached-a
-> memcached-b
-> memcached-c团队必须统一:
server list 内容和顺序。hashing / consistent hashing 算法。节点下线时的失败策略。
是否做预热和渐进扩容。是否允许不同语言客户端访问同一缓存池。
如果不同服务使用不同 hash 算法,同一个 key 会落到不同节点,表现为命中率突然下降,而不是显式报错。
启动和停止:
docker compose up -d memcached
docker compose logs -f memcached
docker compose stop memcached
docker compose down查看 stats:
printf "stats\r\n" | nc 127.0.0.1 11211 | grep -E "curr_items|bytes|limit_maxbytes|get_hits|get_misses|evictions|listen_disabled_num"查看配置:
printf "stats settings\r\n" | nc 127.0.0.1 11211 | grep -E "maxbytes|maxconns|tcpport|udpport|num_threads|item_size_max|evictions"查看 item/slab:
printf "stats items\r\n" | nc 127.0.0.1 11211
printf "stats slabs\r\n" | nc 127.0.0.1 11211本地清理建议重启容器,因为 Memcached 常规缓存可丢:
docker compose restart memcached共享环境不要用 flush_all。如果必须清理,只能通过应用侧 prefix 列表和 delete 脚本处理,或者按维护窗口重建专用缓存池。
配置升级与回退要把“缓存必然变冷”写进步骤。先保存旧镜像 tag、启动参数、客户端 server list 和 hash 配置;新节点或新参数先承接一小部分流量,观察错误率、尾延迟、get_misses、evictions 和回源负载。回退时恢复旧镜像与参数,再按原顺序恢复客户端节点列表;不要指望重启后的实例保留旧 item。验收标准不是 key 还在,而是应用能在受控回源压力下恢复命中率,且数据库或下游没有被冷启动流量压垮。
连接不上 11211
现象:
Connection refused
Operation timed out判断:
docker compose ps memcached
docker port your-project-memcached
printf "version\r\n" | nc -v 127.0.0.1 11211处理:
确认宿主机端口绑定。应用容器内连接 service name memcached:11211,不是 localhost:11211。共享环境查防火墙、安全组、NetworkPolicy。
不要把应用临时指向生产缓存池。
SERVER_ERROR object too large for cache
原因:
value 超过 -I 限制,默认常见为 1MB。序列化膨胀,JSON 或对象序列化后比预期大。把列表、报表、整页 HTML 或大对象塞进 Memcached。
处理:
控制单 item 大小。大 value 拆分或不要缓存。必须调整 -I 时,同步评估 slab、网络和热点 key。
命中率低但服务没报错
判断:
printf "stats\r\n" | nc 127.0.0.1 11211 | grep -E "get_hits|get_misses|cmd_get|evictions|curr_items"原因:
key prefix 不一致。多语言客户端 hash 算法不同。节点上下线导致 key 重新分布。
TTL 太短或应用写后立即删除。内存不足导致大量 eviction。
处理:
统一 key builder。统一 server list 和 hash 算法。节点变更做灰度和预热。
看 evictions、bytes、limit_maxbytes 判断容量。
连接耗尽
判断:
printf "stats\r\n" | nc 127.0.0.1 11211 | grep -E "curr_connections|total_connections|listen_disabled_num"原因:
每次请求创建新客户端。连接池过大或没有关闭。连接上限 -c 太小。
短连接和网络抖动造成连接堆积。
处理:
客户端单例或连接池复用。配置连接超时和最大连接数。留出管理连接余量。
需要调大 -c 时同时看系统 fd 限制。
flush_all 误用
现象:全站缓存同时 miss,数据库或下游 API 突然被打满。
判断:
检查操作记录、CI 脚本、GUI 和排障命令。查看 stats 的 miss 变化。确认是否启用了 -F。
治理:
项目默认启用 -F。清理只按业务 key 列表或重建专用缓存池。共享环境破坏性操作必须有 owner 和窗口。
Memcached 本身不是 HTTP 工具。代理影响的是镜像拉取、依赖下载和云控制台。
docker pull memcached:1.6.45权限边界:
常规 Memcached 没有 Redis / Valkey 式命令级 ACL。SASL 是认证边界,需要服务端和客户端支持 binary protocol。TLS 需要服务端编译支持、证书和客户端支持。
最小权限主要靠网络隔离、专用缓存池、端口绑定和云 IAM / 安全组。
凭证治理:
如果启用 SASL,账号密码不能进仓库。TLS 私钥、证书和 CA 不能进仓库。GUI 或客户端截图隐藏 endpoint、key、value 和客户数据。
CI 和共享环境配置分开。
项目模板至少保留:
compose.yaml
.env.example
cache/memcached/verify/smoke.txt
docs/dependency-setup.md
scripts/memcached-stats.sh共享缓存池规则:
必须有 owner。缓存池用途只写“可丢失缓存”,不能存事实源。key 必须有环境和项目 prefix。
默认 TTL 必须写进应用配置。多语言服务共用时统一 server list、hash 算法、序列化格式。flush_all 禁用或只在维护窗口由 owner 执行。
节点上下线要评估 miss 风暴。监控至少看 get_hits、get_misses、evictions、bytes、limit_maxbytes、listen_disabled_num。
Memcached 重启丢数据不是故障,是前提
现象:重启容器后 key 全没了。
结论:这正是常规 Memcached 行为。应用必须能回源和重建缓存。
治理:
任何不可丢状态不能进 Memcached。缓存 miss 要是正常路径。重启前评估下游回源压力。
warm restart 只作为特殊能力,不写成可靠持久化。
客户端分片不一致会让命中率掉到地上
现象:A 服务写入,B 服务读不到;没有报错,只有 miss 增多。
判断:
比对 server list 和顺序。比对 hash 算法。比对 key prefix 和序列化。
比对节点上下线时间。
治理:
server list 配置进入统一模板。多语言客户端要有兼容性测试。节点变更先灰度,观察 hit rate。
关键缓存做预热或短期回源保护。
热点 key 不会自动分散
现象:多节点池整体 CPU 不高,但单节点延迟高。
原因:单个热点 key 被 hash 到一个节点。Memcached 不会把一个 key 自动分到多个节点。
治理:
热点 key 拆桶。客户端做本地缓存或请求合并。对热点读做限流和降级。
指标按节点看,不只看缓存池总览。
item 过大会拖垮内存和网络
现象:缓存写入失败、网络流量高、slab 利用率差。
判断:
printf "stats settings\r\n" | nc 127.0.0.1 11211 | grep item_size_max
printf "stats slabs\r\n" | nc 127.0.0.1 11211治理:
控制 value 大小。大对象拆分或换存储。不用 Memcached 存报表、大 JSON 和文件片段。
调大 -I 前先评估命中率收益和资源代价。
flush_all 会制造回源风暴
现象:清理缓存后数据库、搜索、第三方 API 被打满。
结论:flush_all 不是“清一下缓存”这么轻。它会让所有现有 item 失效,接下来请求集中回源。
治理:
默认 -F 禁用。清理走 prefix 和业务 key 列表。共享缓存池要有维护窗口。
必要时分批失效或预热。
SASL / TLS 不是默认安全外衣
现象:文档写了“支持 SASL/TLS”,实际镜像或包没有编译支持,客户端也连不上。
判断:
docker run --rm memcached:1.6.45 memcached -h | grep -E "SASL|ssl|TLS|ext"治理:
以实际二进制 memcached -h 为准。TLS / SASL 要做客户端连接验证。即便启用认证,也不能暴露到不可信网络。
不把 SASL 当 key 级权限系统。
stats sizes 的新旧版本语义不同
现象:当前实例执行 stats sizes 没有得到预期分布,团队却按旧资料担心它会扫描并锁住整个实例。
原因:1.4.27 之前的实现会扫描 item,确实可能阻塞缓存;当前版本需要以 -o track_sizes 在启动时开启额外尺寸统计,不再靠命令临时遍历全部 item。未开启时没有可供查询的尺寸直方图,开启后则要承担持续计数的资源开销。
治理:
常规巡检优先使用 stats、stats items、stats slabs 和应用指标。需要尺寸直方图时,在压测环境加入 -o track_sizes,重启后确认 stats settings 与 stats sizes 输出。升级旧实例前先确认版本,不能把 1.4.27 之前的扫描风险套到 1.6.45,也不能忽略开启持续统计后的内存成本。
extstore 不是廉价持久化
现象:以为启用 extstore 后内存不足问题都消失,结果延迟和淘汰更复杂。
结论:extstore 把 value 放到 flash 类存储,key 和元数据仍在内存;它适合大 value、长 TTL、命中率收益明显的场景,不适合随手打开。
治理:
先证明 value 大小和命中率收益。评估 flash 延迟、写放大和设备寿命。监控 eviction、I/O 和尾延迟。
不把 extstore 当数据库。
节点便宜不等于缓存池成本低
Memcached 不复制数据,单份缓存的内存成本通常低于带副本的高可用存储,但节点上下线会把成本转移到回源链路。容量评估要同时记录 item 有效字节、slab 内部浪费、进程额外内存、网络流量和冷启动期间的数据库或 API 负载。扩容前先比较“增加内存后的命中率收益”与“更多节点导致的重分片和运维成本”;命中率没有改善时,继续堆内存通常只是在保存从不复用的数据。
安装后:
镜像 tag 固定为 memcached:1.6.45,需要更换时同步验证启动参数。version 可返回版本。宿主机端口只绑定 127.0.0.1。
stats settings 中 maxbytes、maxconns、tcpport、num_threads、item_size_max 符合预期。UDP 已确认是否关闭。
接入前:
key prefix 包含环境和项目。默认 TTL 已写入应用配置。单 item 大小上限已确认。
客户端复用连接,不每次请求新建 client。超时和回源策略明确。序列化格式已统一。
多节点池 server list、顺序和 hash 算法已统一。
共享前:
owner 已明确。flush_all 禁用或有维护窗口。监控覆盖 hit/miss、evictions、bytes、连接和节点维度。
节点上下线有预热和回源保护。不存事实源、不存不可丢状态、不存敏感原文。SASL / TLS 如果启用,已验证服务端编译和客户端连接。
Memcached 的专业点不在命令多,而在边界清楚:它快、简单、可丢、靠客户端分片。把这四个词写进部署、接入、排障和治理,团队才不会把一个缓存池误用成小型数据库。
