skynet 版的 cache server 改进
去年我实现的 Unity cache server 的替代品 已经逐渐在公司内部取代 Unity 官方的版本。反应还不错,性能,可维护性以及稳定性都超过官方版本。
最近疫情严重,公司安排所有人员在家办公,今天是开工第三天。前两天比较混乱,毕竟在家办公的决定是在假期中间做出的,并没有预先准备,我们的这个 cacheserver 也在第一天受到了极大的考验,暴露出一个问题,好在只是内存用量预警,并没有出任何差错。我花了一天多的时间排查和解决问题,感觉这是一个极好的案例,值得记录一下。
周一复工那一天,公司内网的 cache server 服务器突然内存使用量峰值超过了 20G ,接近硬件配置的 32G ,SA 预警。好在不久以后就回落,并稳定下来。
这个服务器是基于 skynet 编写的,一开始只有 200 行左右的一个单一 lua 文件,非常简单。我认为设计和实现都是清晰可靠的。设计的时候就考虑过大用户量大数据量的应用场景(我们在生产环境运行的这个服务器实际管理数据已经超过了 800G),最多会有几百用户的并发。重新实现的原因是因为原版占用内存太多,所以内存使用量本来就是设计的主要考量因素。
我没有采用一个连接一个 agent 的方案,而是使用了固定数量的 agent ,用简单的负载均衡方法把外部连接分摊进去。
另外,还加了一个几十行的 C 模块,用来绕过 lua 虚拟机直接发送大文件。因为,如果把文件读入 Lua 虚拟机后,会增加额外的内存以及 gc 的负担。skynet 提供了 C API ,直接从 C 层打开读取本地文件并从网络发送走更直接高效。
考虑到大数据文件可能很大,在 C 模块里,我还特地按 4K 一个数据块的处理,并没有把大文件一次读入内存。
结合实际情况,我猜测了内存峰值出现的原因:大量数据积压在 skynet 内部的待发送队列中。
这是 skynet 当初设计的一个权衡下的结果。skynet 的网络发送 api 被设计成非阻塞,原子性,且在连接没有断开时一定发送成功的。这样可以方面业务层使用。业务层不用像使用 posix 标准 send 那样,还要检查返回值,看真的发送了多少字节。
而 skynet 的大多数应用场景,重头在业务处理,而不是 IO 消耗,一般不会出现 IO 积压的情况。但 cacheserver 这个业务不同,是典型的重 IO 环境。虽然通讯协议是串行的,一个一个文件处理,但客户端却可以一次性提交很多文件请求,每个请求都只有几十个字节,但回应的文件却可以很大。加上发送 api 是非阻塞的,这样就会导致大量待发送的数据进入 skynet 的网络层。
为什么这半年一直没有出现问题呢?因为我们一直是在速度很快的内网使用这个服务器(也是官方推荐的使用方法)。而且使用是渐进的,不会突然请求很多文件。而这次,大量同事在家办公,使用 vpn 连入内网,连接速度很慢。在第一天,家中的开发环境是全新的,需要全量拉所有的资源数据。种种因素加起来,在高并发高数据量的情况下,问题就暴露了出来。
其实这个问题之前也有人遇到过,见这个 issue 。只是我虽然帮别人解决了需求,自己并未实践此类业务。解决方法并不复杂,使用 socket.warning 设置回调即可。当发送缓冲区过载,skynet 会发送一个 warning 消息给发送服务,告知缓冲区太长;这个时候业务应该停止发送数据(或进一步的测试一下带宽,限制流量),等到缓冲区清空(同样也会收到消息),再继续发送。
值得注意的是,如果加入 socket.warning 消息的监控,就不能再在 C 库函数中发送整个数据文件了。因为,如果文件过大,在发送的过程中,是没有机会处理新的消息,并挂起发送过程的。
我的修改方案是给 C 的发送函数增加了 offset 选项,并可以返回已经发送的字节数。当一次发送量超过一个阈值后,就中断返回。由 lua 调用方来决策是否应该挂起等待,或继续发送。最终添加了几十行代码就解决了这次的问题。
2 月 6 日补充:修改后的版本,这两天生产环境监控,内存峰值在 30M 以下。
Comments
Posted by: Anonymous | (5) April 26, 2020 09:43 AM
Posted by: Vincent | (4) March 27, 2020 08:40 PM
Posted by: lubaosheng | (3) March 9, 2020 11:57 AM
Posted by: noname | (2) February 12, 2020 11:23 AM
Posted by: dwing | (1) February 6, 2020 11:51 AM