skynet 的启动流程中的异步 IO 问题
有同学向我反应,自从 skynet 的 IO 库重写后,Mac OSX 上便无法启动了。
我检查了一下,直接原因是 kqueue 部分写的不对。kqueue 和 epoll 的 api 设计还是有些区别的,epoll 的 api 可以合并读写消息,但 kqueue 读是读,写是写。当时随手写好后一直没有在真实环境下测试,所以一直是有问题的,只是这次另外一个问题将它暴露了。
我新写的 IO 库是异步操作的。异步处理针对任何 IO 请求,其中也包括了建立连接以及监听连接。skynet 启动的时候需要启动一个 master 服务,然后再各个分布节点上启动 harbor 服务通过 master 互联。这些连接是通过 IO 模块的 socket connect 建立的。
我希望在 skynet 启动完成前把这些连接(至少是主节点的)都正确建立起来。
按计划,我希望在未来的版本中弱化 skynet 的分布式特性,逐渐把分布式功能提到核心层以外来实现。所以这次重写 IO 模块只是简单的按功能重新实现了 master/harbor 模块,并没有深究如何实现的更好。
连接是异步的就意味着很难准确的将连接建立完成后再进行后续的操作。我用了一个变通但不完善的方案:先注册了消息回调函数只监听连接成功建立的消息。等连接建立成功后,再换成完整的回调函数。这里存在一个明显的问题,当 skynet 节点内部有远程消息需要发出,这个需求先于分布式模块初始化前(master/harbor 连接建立前)的情况下,远程消息便无法发出。
一开始我考虑的是,如果分布式模块未初始化后,该节点就无法获取外界任何消息,也不可能有远程消息的需求,所以我在处理连接的回调函数里加了一处 assert 了事。没想到考虑是不周全的,在 kqueue 模块错误实现的情况下,连接无法建立,问题也就暴露了。
对于这个将来想重新设计的部分,我不想增加太多的代码,比如增加一个消息缓存队列将提前收到的消息先记下来。那么不考虑时序的话,可以在收到不想处理的消息的时候利用 skynet 内置的 forward 功能把消息 forward 回自己。可以简单的记录一下第一个被 forward 的消息,如果再次见到它的时候就 sleep 一下,等待连接建立成功。
后一个方案我花时间考虑过。但这样做会导致消息时序不正确,总感觉不舒服,最后还是决定增加一个阻塞 connect 接口比较简单。
修改过后程序依然有问题,是因为 listen 也是异步操作的。当然 listen 本身无所谓非阻塞操作,只是因为当初设计 IO 模块的时候希望统一。我需要把所有 socket 都从文件 handle 映射成一个内部数据结构,用异步方法把所有操作都发送到一个线程里处理可以简化并行引起的线程安全问题。但如果异步操作 listen 请求的话,由于 skynet 的 bootstrap 阶段并没有启动工作线程,会导致 IO 请求永远不会被处理到,这样,连接当前进程内开启的 master 服务就会失败。
解决这个问题的方案是在发起 listen 请求时,直接调用系统 api 把 socket 建立好,再将 fd 发送到 socket 线程去和自定义数据结构绑定起来,以及做后续的向 epoll/kqueue 添加消息的处理就可以了。
等这个月忙完,我想认真考虑一下我们的项目里如何更好的处理分布式游戏服务器的问题了。我觉得想让 skynet 底层完全将进程内服务和通过 socket 连接在一起的外部服务的区别透明化是不现实的。现实项目一定不可能随意的把工作模块分布在不同的机器上。毕竟在进程内的消息传递以及信息共享的效率和可靠性都是远高于跨机节点的。
Comments
Posted by: 天空 | (6) March 6, 2014 11:55 AM
Posted by: 范云华 | (5) October 24, 2013 12:50 PM
Posted by: boost | (4) October 22, 2013 10:51 AM
Posted by: 如何管理好一个团队? | (3) October 14, 2013 10:25 AM
Posted by: Cloud | (2) September 11, 2013 05:34 PM
Posted by: zcpro | (1) September 11, 2013 04:17 PM