VPN 上的 Redis 很慢:削减每次请求的往返次数
VPN 不会把远程 Redis 调用变成本地调用。为每次请求规划往返次数,复用连接,并把 pipeline 与事务分开。
一个对着本地 Redis 200 毫秒就能返回的页面,一旦客户端位于 VPN 之后就要四秒,而服务器那边的计数器却闲着。Slow Log 里没有任何超过一毫秒的记录,CPU 曲线是一条直线,实例对其它客户端也毫无怨言。变化的是距离,而让距离成倍放大的,是 你的代码停下来等待的次数。沿着请求路径从客户端向外交代,修复几乎总是减少串行等待,而不是换一台更快的服务器。
从客户端出发,而不是从服务器指标出发
观察客户端进程在第一条命令与最后一条命令之间的表现。如果轨迹显示一个响应、一段停顿、然后才是下一条命令,那么你为每条命令都在付一次完整往返,再怎么调服务器也改变不了。Slow Log 在这里是错的工具,因为它记录的是执行时间,并且有意排除了通信 I/O。慢接口旁边却有一份干净的 Slow Log,正是往返问题的签名。
先测量路径本身,再动代码。redis-cli -h redis.example.internal --latency会重复发送 PING 并打印请求与应答的平均时间,那就是你每一次交互的成本。把它乘以一个用户动作里的串行命令数,你就得到了这个接口的下限。
数一数一个用户动作实际需要多少次往返
挑一个接口,按顺序写下每一次 Redis 调用,包括藏在 ORM、session middleware 或 feature flag 查询里的那些。大多数慢接口在三条调用就够的地方做了二三十条,而隐藏的那些通常占多数。
依赖链无法压缩:如果第二条命令需要第一条返回的键,这个等待就是真实的,唯一的选项是换一种数据模型,或者用一次服务器端操作把整个步骤表达出来。相互独立的读取是另一回事,因为没有任何东西强迫你在发出第二条之前等待第一条的应答。在改动任何东西之前,把每次调用标记为依赖或独立,修复的形状就显而易见了。
真正重要的是串行等待的次数,而不是命令总数。一个 batch 里的三十条命令仍然只算一次等待。
只对那些互不依赖的命令做 batch
pipeline 把彼此独立的命令排进队列并按顺序收集应答,把 N 次等待变成一次。大多数客户端把它暴露为代码块、回调或上下文管理器,而不是单独的连接模式,所以先查一下你安装的版本提供了什么 API。收益随往返时间放大,这正是它在 VPN 上重要、在 localhost 上几乎感觉不到的原因。
import redis
r = redis.Redis(host="redis.example.internal", port=6379, socket_timeout=2)
pipe = r.pipeline(transaction=False)
for key in ("user:1001", "user:1002", "user:1003"):
pipe.get(key)
print(pipe.execute())
只想要批量发送时,传入transaction=False即可。用默认值时,客户端会把整个 batch 包进 MULTI 与 EXEC,这使它具备原子性,也改变了其它客户端在执行途中能看到什么。原子性是一个正确性决策,不是性能旋钮;pipeline 也不因为两者都减少往返就成了事务。
要读每一份应答,而不只是把 batch 提交出去。排队的命令可能在整批完成的同时返回错误,而把应答列表丢掉的客户端会把一次失败的写入变成一次静默的写入。把应答列表的长度和你排入的命令数对一遍。
让连接在多个动作之间保持存活
每个新连接都要付名字解析、TCP 握手、TLS 握手和一次 AUTH 的代价,而在 VPN 上,其中每一项都是你没法批掉的往返。每个进程只创建一次客户端并复用它,或者用你的库提供的连接池并给它设个上限。按请求重连是这个问题第二常见的原因,仅次于逐命令等待。
把私有主机名留在配置里,而不是写成一个字面地址。证书校验与服务身份在私有网络上同样适用,而把 IP 钉死会在服务商重新编址子网的那一刻失效。如果慢的是名字解析,就修 resolver 的路径,而不是改连接串。
显式设置 socket 超时,让卡住的隧道快速失败,而不是把 worker 挂住。然后通过在客户端记录连接创建,确认连接池真的被复用。
在 batch 变成内存问题之前先定好尺寸
Redis 在一整批完成之前一直持有排队的应答,客户端随后持有解析好的结果。一万条大 value 的 pipeline 是用网络等待换来了内存尖峰和两端的长时间停顿。
按应答体积而不是命令条数来选 batch 大小。一个 batch 里几百条小命令通常很舒服,而一百条几 KB 的 payload 就不是了。既按字节也按条数切分,并把每一批控制在接口真实拥有的延迟预算之内。
改动之后做端到端测量:redis-cli -h redis.example.internal --latency-history会告诉你路径本身是否稳定,你自己的请求计时器则告诉你接口有没有变好。如果接口仍然慢、而延迟历史是平的,那么剩下的成本在服务器端的工作或你自己的序列化上,下一步该去剖析 handler,而不是再改一次 batch。