在我们的Python应用程序中,我们编写了一个服务-客户端通信协议,允许服务客户端向服务服务器发出请求,服务器运行在我们的服务器基础设施中的不同进程或不同机器上。该协议使用Redis作为后端,到目前为止,它工作得很好(例如,比使用HTTP更快)。
对于上下文,我们的Redis键总是< 64字节,当我使用术语“消息”时,我指的是msgpacked二进制字符串总是<100 KB,但通常在3 KB左右。在我们测试的特定服务的情况下,客户端请求消息几乎每次都是500字节,而服务器响应消息几乎每次都是2KB。我们使用的是Redis 3.0.6。
我们的协议的初始版本是这样工作的(Redis命令名的样式类似于THIS
):
- 生成一个唯一的请求ID用作密钥
- 调用LUA脚本:
LLEN
如果列表中有太多的消息,带有键“server-requests”的请求列表将失败SET
带有请求消息值的请求ID键EXPIRE
请求ID密钥为60秒RPUSH
请求ID键到请求列表中,键为“服务器请求”EXPIRE
请求列表与关键“服务器请求”到60秒BLPOP
从请求列表中以“server-requests”键获取下一个请求IDGET
请求ID密钥,用于获取请求消息DELETE
请求ID密钥- 生成一个唯一的响应ID用作密钥
- 调用LUA脚本:
LLEN
如果列表中有太多消息,则具有键“client-id-123456789-responses”的响应列表将失败SET
带有响应消息值的响应ID键EXPIRE
将响应ID密钥设置为60秒RPUSH
响应ID键到响应列表中,键为“client-id-123456789-responses”EXPIRE
响应列表与关键字“客户端-id-123456789-响应”到60秒BLPOP
从响应列表中键入“client-id-123456789-responses”以获取响应IDGET
响应ID密钥,用于获取响应消息DELETE
响应ID密钥
这是有效的,在每秒800到1,600个请求的波动负载下,以下每一个的平均时间大约是2 ms:1)向Redis发布请求,2)从Redis获取请求,3)向Redis发布响应,4)从Redis获取响应。
我们不喜欢这样的性能,所以我们决定用第二个版本来改进协议:
- 调用LUA脚本:
LLEN
如果列表中有太多消息,则带有键“server-requests”的请求列表将失败RPUSH
将请求消息添加到请求列表中,关键字为“服务器请求”EXPIRE
请求列表与关键“服务器请求”到60秒BLPOP
从请求列表中以“server-requests”键获取下一个请求消息- 调用LUA脚本:
LLEN
如果列表中有太多消息,则具有键“client-id-123456789-responses”的响应列表将失败RPUSH
将响应消息添加到响应列表中,键为“client-id-123456789-responses”EXPIRE
响应列表与关键字“客户端-id-123456789-响应”到60秒BLPOP
从响应列表中键入“client-id-123456789-responses”以获取响应消息
我们的想法是,由于Redis的操作要少得多,这应该会快得多,并允许我们更好地扩展。早期的迹象表明我们是对的。在800个请求/秒时,大约2毫秒已经变成了大约1毫秒。然而,在1,600个请求/秒时,Redis摔倒了,吐得到处都是。CPU使用率飙升到接近100%,请求被拒绝,响应时间从~ 1 ms增加到100 ms到8 Seconds 的范围。但是有一件事我们从来没有看到过,那就是LUA脚本中的一个错误,说列表中有太多的项目(上面的LLEN
)。
我完全不知所措。这些都没有意义。直觉上,它应该更好。测试似乎表明,在列表中存储0.5-2KB字符串在一定负载下的性能非常糟糕(除非我在这里遗漏了其他变量),但文档并不支持这一点。作为链表,推送和弹出列表应该是一个恒定的时间操作,而不管值的大小,我们甚至没有接近每个项目512 MB的最大大小。
有没有人见过这样的行为,或者有关于问题可能是什么的建议?
1条答案
按热度按时间ztmd8pv51#
完整的讨论可以在above-linked Redis Google group discussion中回顾,但当今天早上这篇文章回到我的办公桌上时,似乎有必要总结一下我们得出的结论,我将把这些结论分成我们知道的和我们只能理论化的两部分。
"我们所知道的"
我们错误地使用了RedisPy库中的
sentinel.master_for
。每次你使用它,都会创建一个到Redis主机的全新连接。这种极快的连接创建速度极大地增加了我们的资源使用。我们做了一个更改,只调用一次sentinel.master_for
并缓存该连接,直到检测到故障,然后再次调用它以获得到新主机的连接。等等,事情有了显著的改善:redis_total_connections_received
直线下降(这是新的连接,它是数以千计),CPU使用率下降,内存使用率下降,磁盘消耗减少,网络流量减少,Redis总密钥从约600万到约150,000,“发送请求到Redis的时间”从2ms到0.2ms。第二版协议的问题就消失了。"我们只能推测"
对于为什么这个问题只在我们的协议的第二个版本中出现,我们可以得出的最好解释是,第二个版本确实更快,而且由于这个原因,
sentinel.master_for
被调用得更快,因为应用程序每秒处理的“请求”更多,从而导致新Redis连接创建的速率更高。该理论还可以解释问题的循环性质,因为当Redis陷入新连接的困境时,它会降低我们的应用程序的速度,从而降低我们调用sentinel.master_for
的速度,从而降低新Redis连接创建的速度,从而允许Redis返回到更正常的行为,等等。这个理论很难用现有的数据来证明,所以团队中的每个人都认为这是最有可能的解释,并不觉得有必要花费更多的时间和金钱来为一个已经解决的问题最终证明这个理论。