« 支持部分共享的树结构 | 返回首页 | 浅谈《守望先锋》中的 ECS 构架 »

skynet 网络线程的一点优化

skynet 是一个注重并行业务处理的框架,设计它的初衷是可以充分利用多核 CPU 更好的处理那些比较消耗 CPU 的,天然可以并行的业务,比如网络游戏。网络 I/O 并不是优化重点。

基于这个设计动机,skynet 的网络层使用单线程实现。因为我认为,即使是代码量稍大一些的单线程程序,也会比代码量较小的多线程程序更容易理解,出 bug 的机会也更少。而且经典的网络服务程序,如 redis nginx 并没有因为单线程处理网络 IO 而变现得不堪,反而有不错的口碑。

所以,skynet 的 epoll 循环并不像 erlang 那样,只关注读写事件,而让每个 actor 自己去处理真正的 socket 读写。那样固然可以获得更高的网络处理能力,但势必让网络 API (由存在多个工作线程里的多个 actor 分别调用)依赖锁来保证正确性。这是我不太希望看到的。目前的设计是,所有网络请求,都通过把指令写到一个进程内的 pipe ,串行化到网络处理线程,依次处理,然后再把结果投递到 skynet 的其它服务中。

这个做法未必最好,但也恰恰能用,一般网络游戏服务器,根据我们的实际项目数据,在其它业务处理的 CPU 占用到极限时,单台机器网络带宽不大会超过 30MB 左右的上下行带宽。一个核每秒处理 60MB 的数据是绰绰有余的。

不过我一直有个想法,或许可以优化一下这部分,让 skynet 可以适应一些重 IO 而不仅仅是重业务处理的场合。前些年和一个使用 skynet 做流媒体广播的同学交流过,他们的生产环境上,一台机器会配置几块千兆网卡,skynet 在处理高带宽的 udp 广播时,跑不满硬件的带宽。

前几天,skynet 的 issue #646 又让我想到这件事情。就这个 issue 而言,我不认为达到网络线程处理极限,可能尚有未发现的其它因素影响,不过就这个机会,我试着实现了一下以前的想法。

我的想法是,可以把网络写操作从网络线程中拿出来。当每次要写数据时,先检查一下该 fd 中发送队列是否为空,如果为空的话,就尝试直接在当前工作线程发送(这往往是大多数情况)。发送成功就皆大欢喜,如果失败或部分发送,则把没发送的数据放在 socket 结构中,并开启 epoll 的可写事件。

网络线程每次发送待发队列前,需要先检查有没有直接发送剩下的部分,有则加到队列头,然后再依次发送。

当然 udp 会更简单一些,一是 udp 包没有部分发送的可能,二是 udp 不需要保证次序。所以 udp 立即发送失败后,可以直接按原流程扔到发送队列尾即可。


加上这个优化后,就必须在每个 socket 结构上增加一个 spinlock 。直接发送的逻辑可以用 try lock 尝试加锁,而不一定要获得锁;只在网络线程发送队列期间这一个地方才需要加锁。所以这个锁几乎不会竞争。

毕竟是多线程代码容易出 bug ,而且也不太容易做测试。所以我暂时把它放在一个独立分支上,希望感兴趣的同学可以帮助 review 一下,以后再考虑是否合并到主干。

Comments

已经在issue提了大概的对比数据和目前存在的问题

分离io线程和work线程,就要对socket对象做引用计数,引用计数本身可以用原子变量实现无锁。

另外,只有ref到0的时候才能真正的close fd。

但是如果允许work直接操作socket对象里的fd,就要对socket上锁,粒度是socket级别,因为多个work写fd,总有可能写不全,所以必须加锁,写fd,剩余的pipe到io线程。

大多数情况下网络不拥塞,那么基本都是work线程并行直接write fd,而不需要通过pipe回传io线程来转串行。

最简单的 单个进程只能处理30mb左右的带宽 你就多开几个进程 前面的流量路由一下 哈哈

不错

Post a comment

非这个主题相关的留言请到:留言本