- 后端
【免费下载链接】twemproxy
A fast, light-weight proxy for memcached and redis
本指南以 twemproxy(nutcracker)仓库中的 notes/memcache.md 为骨架,系统梳理 twemproxy 对 Memcached 协议的支持范围与实现原理:仅实现 ASCII 文本协议、不包含二进制协议;完整覆盖存储、检索、删除、算术、touch、version 等命令的请求/响应格式,并深入 src/proto/nc_memcache.c 的逐字节状态机解析、多键get/gets的分片与响应合并逻辑,以及测试与配置层面的实战验证。读完本文,你将能准确判断 twemproxy 支持哪些 Memcached 命令、各命令的线上格式与返回值语义,并理解其"分片-转发-合并"的底层工作方式。
概览:只做 ASCII,不做 Binary
twemproxy 定位为 memcached 与 redis 的快速轻量代理(见 README.md),其 Memcached 协议实现遵循一条明确的边界:
- 仅实现 Memcached ASCII 文本命令;
- 二进制(Binary)协议目前不支持。
这与 src/proto/nc_memcache.c 中纯字符流逐字节解析的实现方式完全一致——解析器直接对\r\n分隔的文本行和<data>数据块做状态机处理,不存在任何二进制帧头解析。因此,凡是依赖 Memcached Binary 协议(如 SASL 认证、某些客户端库的默认二进制模式)的场景,需要将客户端切换为 ASCII 文本协议才能经由 twemproxy 访问后端。
请求命令支持矩阵
twemproxy 当前支持的 Memcached 请求命令共 17 种,下面按类别完整列出(沿用原文档的"支持状态 + 标准格式"表达,格式中的\r\n为行结束符)。
ASCII 存储命令(Storage Command)
| 命令 | 支持? | 格式 |
|---|---|---|
| set | 是 | set <key> <flags> <expiry> <datalen> [noreply]\r\n<data>\r\n |
| add | 是 | add <key> <flags> <expiry> <datalen> [noreply]\r\n<data>\r\n |
| replace | 是 | replace <key> <flags> <expiry> <datalen> [noreply]\r\n<data>\r\n |
| append | 是 | append <key> <flags> <expiry> <datalen> [noreply]\r\n<data>\r\n |
| prepend | 是 | prepend <key> <flags> <expiry> <datalen> [noreply]\r\n<data>\r\n |
| cas | 是 | cas <key> <flags> <expiry> <datalen> <cas> [noreply]\r\n<data>\r\n |
其中各字段类型为:
<flags>:uint32_t,客户端自定义的数据相关标志位;<expiry>:uint32_t,过期时间(秒);<datalen>:uint32_t,数据块大小(字节);<data>:uint8_t[],数据块本体;<cas>:uint64_t,CAS 令牌。
从源码看,存储类命令在解析完成后进入专门的SW_VAL状态按<datalen>精确消费数据块(见 src/proto/nc_memcache.c),并允许<datalen>为 0(此时数据块为空)。memcache_storage()函数将set/add/replace/append/prepend/cas六类统一归为存储命令(src/proto/nc_memcache.c)。
ASCII 检索命令(Retrieval Command)
| 命令 | 支持? | 格式 |
|---|---|---|
| get | 是 | get <key> [<key>]+\r\n |
| gets | 是 | gets <key> [<key>]+\r\n |
get支持一次请求携带多个 key,gets则在返回时附带 CAS 令牌。这两者是 twemproxy 中唯一允许携带多键的 Memcached 命令,也是唯一会被"分片(fragment)"处理的命令(详见下文)。
ASCII 删除命令(Delete)
| 命令 | 支持? | 格式 |
|---|---|---|
| delete | 是 | delete <key> [noreply]\r\n |
ASCII 算术命令(Arithmetic Command)
| 命令 | 支持? | 格式 |
|---|---|---|
| incr | 是 | incr <key> <value> [noreply]\r\n |
| decr | 是 | decr <key> <value> [noreply]\r\n |
其中<value>为uint64_t,表示增量/减量。
ASCII 其他命令(Misc Command)
| 命令 | 支持? | 格式 |
|---|---|---|
| touch | 是 | touch <key> <expiry>[noreply]\r\n |
| gat | 计划中 | gat <expiry> <key>+\r\n |
| gats | 计划中 | gats <expiry> <key>+\r\n |
| quit | 是 | quit\r\n |
| flush_all | 否 | flush_all [<delay>] [noreply]\r\n |
| version | 是 | version\r\n |
| verbosity | 否 | verbosity <num> [noreply]\r\n |
| stats | 否 | stats\r\n |
| stats | 否 | stats <args>\r\n |
需要特别留意"计划中/不支持"列:
gat/gats(get-and-touch)标注为Planned,当前版本尚未实现;flush_all、verbosity、stats均标注No,twemproxy 不会转发这些命令,客户端直接使用时会得到错误响应。
命令识别的源码依据
在 src/proto/nc_memcache.c 的memcache_parse_req()中,命令名按字节长度 + 逐字符比对识别:3 字符匹配get/set/add/cas,4 字符匹配gets/incr/decr/quit,5 字符匹配touch,6 字符匹配append/delete,7 字符匹配prepend/replace/version。长度比对使用 src/proto/nc_proto.h 定义的str4cmp~str12cmp宏(小端平台上按 32 位整型一次比较 4 字节,减少逐字符比较开销)。所有消息类型统一在 src/nc_message.h 的MSG_TYPE_CODEC中声明,如MSG_REQ_MC_SET、MSG_REQ_MC_GETS、MSG_RSP_MC_VALUE等。
响应格式
twemproxy 会解析后端 Memcached 返回的 ASCII 响应并原样转发给客户端。以下按类别完整列出各类响应的文本格式。
错误响应(Error Responses)
ERROR\r\n CLIENT_ERROR [error]\r\n SERVER_ERROR [error]\r\n语义:
ERROR:客户端发送了不存在的命令名;CLIENT_ERROR:客户端命令不符合协议规范;SERVER_ERROR:服务端处理命令时出错,导致无法完成。
对应地,src/proto/nc_memcache.c 在解析响应时识别ERROR、CLIENT_ERROR、SERVER_ERROR三种错误类型(MSG_RSP_MC_ERROR等),并进入SW_RUNTO_CRLF状态吞掉错误详情直到行尾。
存储命令响应(Storage Command Responses)
STORED\r\n NOT_STORED\r\n EXISTS\r\n NOT_FOUND\r\n语义:
STORED:存储成功;NOT_STORED:因add或replace的条件未满足而未存储;EXISTS:执行cas时,目标条目在你上次获取之后已被修改;NOT_FOUND:执行cas时,目标条目不存在。
删除命令响应(Delete Command Responses)
NOT_FOUND\r\n DELETED\r\n检索命令响应(Retrieval Responses)
END\r\n VALUE <key> <flags> <datalen> [<cas>]\r\n<data>\r\nEND\r\n VALUE <key> <flags> <datalen> [<cas>]\r\n<data>\r\n[VALUE <key> <flags> <datalen> [<cas>]\r\n<data>]+\r\nEND\r\n单键命中时返回一行VALUE ...加数据块再加END;多键命中时返回多个VALUE行,最后统一以END\r\n收尾。[<cas>]仅在gets时出现。twemproxy 的响应解析器(src/proto/nc_memcache.c)通过MSG_RSP_MC_VALUE状态链(SW_SPACES_BEFORE_KEY→SW_KEY→SW_FLAGS→SW_VLEN→SW_RUNTO_VAL→SW_VAL→SW_VAL_LF)逐字段解析并校验数据长度。
算术命令响应(Arithmetic Responses)
NOT_FOUND\r\n <value>\r\n其中<value>为uint64_t,即incr/decr操作后的新键值。解析时以纯数字行进入SW_RSP_NUM状态识别为MSG_RSP_MC_NUM。
touch 命令响应(Touch Command Responses)
NOT_FOUND\r\n TOUCHED\r\n统计响应(Statistics Response)
[STAT <name> <value>\r\n]+END\r\n需要说明:stats命令本身在 twemproxy 侧标注为No(不支持转发),此处仅按 Memcached 协议标准列出其响应形态,供了解协议完整性。
其他响应(Misc Responses)
OK\r\n VERSION <version>\r\n协议语义要点(Notes)
原文档总结的 Memcached 语义细节,在配置 twemproxy 后端时同样适用,逐条列出如下:
set无条件创建映射,无论条目是否存在;add仅当映射不存在时才添加;replace仅当映射存在时才替换;append与prepend会忽略 flags 和 expiry 值;noreply指示服务端即使出错也不发送回复;decr到 0 则结果为 0,incr超过UINT64_MAX则回绕为 0;- key 最大长度为250 字符(该常量在 src/proto/nc_memcache.c 定义为
MEMCACHE_MAX_KEY_LENGTH 250,解析时超出即报错,见 src/proto/nc_memcache.c); expiry为 0 表示条目永不过期(但仍可能因缓存淘汰被驱逐);- 非零
expiry要么是 Unix 时间(自 1970-01-01 起的秒数),要么是相对当前时间的秒数偏移(小于60 × 60 × 24 × 30秒 = 30 天); - expiry 以服务端时间为准,而非客户端时间;
<datalen>可以为 0,此时<data>数据块为空。
noreply 的解析实现
noreply选项在存储、算术、删除、touch 命令中均可选。解析器在SW_RUNTO_CRLF状态遇到n时进入SW_NOREPLY,校验后续恰好是 7 字符的noreply并置r->noreply = 1(src/proto/nc_memcache.c)。存储类命令在 noreply 之后仍需继续读取<data>数据块。
多键 get/gets 的分片与响应合并
这是 twemproxy 处理 Memcached 请求时最核心、也最能体现其分片代理价值的一环。
何时分片
memcache_should_fragment()(src/proto/nc_memcache.c)规定:只有get/gets且携带多个 key 时才分片。原因有二:
- 分片需要额外分配数组跟踪"哪个 key 去了哪台后端",代价更高,因此单键请求绝不分片、直接路由到一台后端;
- 多键
get的各个 key 可能被哈希到不同后端,必须按后端拆分后并发请求,再合并响应。
分片过程
memcache_fragment_retrieval()(src/proto/nc_memcache.c)按以下步骤构造子请求:
- 为每台后端建立对应的子消息
sub_msg; - 遍历请求中的每个 key,用
msg_backend_idx()依据哈希/分布算法决定其归属后端,并把 key 追加到该后端的子消息中; - 依据原命令类型为每个子消息前置
get(4 字节)或gets(5 字节)头,并追加\r\n结尾; - 记录
frag_id、frag_owner用于后续合并,并把子消息串入碎片队列。
由此,形如get k1 k2 k3的多键请求会被拆成若干get k1\r\n、get k2 k3\r\n等单后端请求并行发出。
响应合并
- pre-coalesce(src/proto/nc_memcache.c):每当收到一个碎片响应时执行。对
VALUE/END类型响应,会裁掉每台后端返回的END\r\n标记(只保留VALUE行),避免合并时出现多余的 END;对异常响应则标记请求错误并返回SERVER_ERROR。 - post-coalesce(src/proto/nc_memcache.c):当所有碎片响应到齐后执行,遍历每个 key 对应的子响应,通过
memcache_copy_bulk()(src/proto/nc_memcache.c)把各VALUE <key> <flags> <len> [<cas>]\r\n<data>\r\n块(mbuf 层面直接搬移/切分,接近零拷贝)复制到合并响应中,最后统一追加一个END\r\n收尾,返回给客户端。
这套"分片请求 → 并行转发 → 裁剪 END → 合并 VALUE → 补 END"的流程,让客户端可以像访问单机 Memcached 一样发送多键get/gets,而实际数据被透明地分布到多台后端。
实战:配置 Memcached 池并验证
用配置声明 Memcached 池
twemproxy 的每个池通过 conf/nutcracker.yml 中的 YAML 块定义。池默认即为 Memcached 协议,只有显式写redis: true才会启用 Redis 协议(该字段在 src/nc_conf.c 中被默认置为CONF_DEFAULT_REDIS,即默认关闭)。例如 conf/nutcracker.yml 中的gamma、delta、omega池没有写redis: true,其监听端口对接的1121x正是 Memcached 常用端口;而 conf/nutcracker.leaf.yml 中的leaf池同样面向11212/11213两台 Memcached,并使用fnv1a_64哈希与ketama一致性分布:
leaf: listen: 127.0.0.1:22121 hash: fnv1a_64 distribution: ketama auto_eject_hosts: true server_retry_timeout: 2000 server_failure_limit: 1 servers: - 127.0.0.1:11212:1 - 127.0.0.1:11213:1启动方式(详见 README.md):
$ src/nutcracker -c conf/nutcracker.leaf.yml -d # 指定配置文件并守护进程化 $ src/nutcracker -t -c conf/nutcracker.leaf.yml # 仅校验配置语法用 telnet 手工验证协议
由于是 ASCII 协议,可直接用 telnet 或 nc 手工敲命令验证(这正是原文档"ASCII 协议更易调试"观点的实践):
$ telnet 127.0.0.1 22121 Trying 127.0.0.1... Connected to 127.0.0.1. set foo 0 0 3\r\n bar\r\n STORED\r\n get foo\r\n VALUE foo 0 3\r\n bar\r\n END\r\n incr counter 5\r\n 5\r\n version\r\n VERSION 1.4.x\r\n quit\r\n自动化测试佐证
仓库的测试目录 tests/test_memcache/ 提供了可运行的验证脚本,例如 tests/test_memcache/test_gets.py:
- 起两个 Memcached 后端(
127.0.0.1:2200/2201)与一个 nutcracker 实例(监听127.0.0.1:4100,is_redis=False); - 用 python-memcache 客户端连接 nutcracker,覆盖
set/get/incr/delete基础用例; test_mget_mset验证多键get_multi/set_multi经 twemproxy 分片合并后的正确性;test_mget_mset_large将键数量从 179 递增到T_LARGE(默认 1000),覆盖大数量多键请求;test_mget_mset_key_not_exists验证部分 key 不存在(返回空)时多键get_multi行为正确。
这说明多键分片合并不仅是设计文档中的规划,更是有自动化测试保障的既定能力。
调试思路与未来方向
原文档在 Notes 末尾记录了两点技术考量,在此一并转述:
- ASCII 协议更易调试:相比二进制协议,ASCII 文本协议在线上形态直观,可以用
strace、tcpdump抓包观察线上的字节流,也可以用 telnet、netcat、socat 直接构造 Memcached 请求与响应来做实验——这也是上文手工验证方法的理论来源。 - meta-text 协议的演进规划:twemproxy 计划在 meta-text 协议被标记为稳定、且使用它的 Memcached 服务端经过数个版本发布之后,再支持这一更高效的协议。也就是说,在本文所述的 ASCII 支持之外,meta-text 属于文档明确记载的、面向未来的扩展方向。
小结
围绕 notes/memcache.md,本文完整还原了 twemproxy 的 Memcached 协议支持全貌:仅 ASCII、共支持 17 种命令(存储 6、检索 2、删除 1、算术 2、其他 6),其中gat/gats计划中、flush_all/verbosity/stats不支持;各类请求与响应的文本格式与字段类型、noreply、250 字符 key 上限、30 天内的相对过期时间等语义细节均已列出;并进一步结合 src/proto/nc_memcache.c 的状态机解析与多键分片合并实现、conf/nutcracker.yml 的 Memcached 池配置示例及 tests/test_memcache/test_gets.py 的测试用例,形成了从"命令格式"到"实现原理"再到"可运行验证"的完整链路。若需对照 Redis 协议侧的能力,可继续阅读 notes/redis.md。
- 后端
【免费下载链接】twemproxy
A fast, light-weight proxy for memcached and redis
相关推荐
memcached网络协议全解析:从ASCII到二进制协议
memcached网络协议全解析:从ASCII到二进制协议 本文全面解析Memcached的网络协议体系,从基础的ASCII文本协议到高性能的二进制协议,深入探
缓存后端高可用Cal.diy Embed 生命周期详解:握手协议、命令队列与事件状态机
Cal.diy Embed 生命周期详解:握手协议、命令队列与事件状态机 导读 Cal.diy (Scheduling infrastructure for a
后端前端企业应用DiceDB PFMERGE 命令详解:多 HyperLogLog 基数合并与跨分片实现
DiceDB PFMERGE 命令详解:多 HyperLogLog 基数合并与跨分片实现 PFMERGE 是 DiceDB 中用于合并多个 HyperLogLo
数据库缓存后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考