Redis 的协议简单到可以用 telnet 手敲。但服务器如何处理不规矩的输入?半条命令会怎样?多大的请求会被拒绝?这篇用原始 socket 发送各种正确与畸形的请求,把服务器的每一种回应记下来。
multibulk PING 发送: b'*1\r\n$4\r\nPING\r\n' 收到: +PONG⏎ inline PING 发送: b'PING\r\n' 收到: +PONG⏎ inline SET / GET(引号) 发送: b'SET k "hello world"\r\nGET k\r\n' 收到: +OK⏎$11⏎hello world⏎
标准的 multibulk 请求以 *<参数个数> 开头,每个参数 $<长度> + 内容。Inline 格式是直接的一行文本,以空白分隔参数,支持双引号和单引号,方便 telnet 调试。
multibulk 长度不是数字 发送: b'*abc\r\n' 收到: -ERR Protocol error: invalid multibulk length⏎<对端关闭连接> multibulk 长度为负 发送: b'*-5\r\n' 收到: bulk 长度不是数字 发送: b'*1\r\n$x\r\nfoo\r\n' 收到: -ERR Protocol error: invalid bulk length⏎<对端关闭连接> multibulk 元素前缀不是 $ 发送: b'*1\r\n:3\r\nfoo\r\n' 收到: -ERR Protocol error: expected '$', got ':'⏎<对端关闭连接> bulk 长度超过 proto-max-bulk-len 发送: b'*2\r\n$3\r\nSET\r\n$2000000\r\n' 收到: -ERR Protocol error: invalid bulk length⏎<对端关闭连接> bulk 长度声明 10 实际 3 + 结尾错 发送: b'*1\r\n$10\r\nabc\r\n' 收到: multibulk 元素数超过 1024*1024 发送: b'*2000000\r\n' 收到: inline 引号未闭合 发送: b'SET k "abc\r\n' 收到: -ERR Protocol error: unbalanced quotes in request⏎<对端关闭连接>
服务器对畸形输入的处理不是忽略,而是回一条 -ERR Protocol error: … 并关闭连接(因为字节流已经失去同步,无法继续解析)。其中没有回复的几种情形(负的 multibulk 长度、bulk 数据没到齐、元素数很大)不是错误——服务器在等更多数据,或把负长度当作空请求跳过。
发送到 "he" 就停: (0.5 秒内没有任何回复) → 补上 "llo\r\n" 之后: b'+OK\r\n'
半条命令不会触发任何回复;补全后才执行。这是 TCP 字节流上的增量解析:一条命令可以被拆成多个包,服务器把未解析的部分放在客户端的查询缓冲里。
发送 3600000 字节后:ConnectionResetError;读:ConnectionResetError 服务器日志里的相关行: ['Closing client that reached max query buffer length: id=18 addr=127.0.0.1:58805 laddr=127.0.0.1:8090 fd=9 name= age=0 idle=0 flags=A db=0 sub=0 psub=0 ssub=0 multi=-1 wat']
client-query-buffer-limit 限制单个客户端未处理的输入缓冲。我把它设成 2 MB、把 proto-max-bulk-len 放宽到 100 MB,发一个 5 MB 的 bulk:在约 3.6 MB 时连接被关闭,服务器日志是 Closing client that reached max query buffer length。要存大 value,两个参数都要调。
1 MB:SET 往返 1 ms(930 MB/s)
10 MB:SET 往返 9 ms(1164 MB/s)
100 MB:SET 往返 61 ms(1631 MB/s)
500 MB:SET 往返 438 ms(1143 MB/s)SET 一个 1 / 10 / 100 / 500 MB 的 value,往返 1 / 9 / 61 / 438 ms,约每秒 1 GB(本机回环,客户端一次发送)。协议解析本身不是瓶颈;大 value 的问题在于它占用主线程的时间。