关于silly

自从写了第一个假server之后, 我就一直在想真正的处理高并发的server是如何做的。然后我就研究了skynet, nginx, Node.js这些服务器程序框架。

这些框架除了skynet, 另外两个框架则仅仅是从使用上进行了了解, 并没有去通读他们的源码。所以基本了解下来,skynet是一个基本actor模式,Node.js则为纯异步模式, nignx则使用了master-worker模式。

纸上得来终觉浅,绝知此事要躬行。 我觉得只是单纯的研究是不够的,还是要自己去实现一下,才会遇到并解决问题,这样才能领会到巨人的思想及解决方法。由于见识浅薄,难免有许多错误的设计, 因此定名为silly。


最初的想法是这样的:

由于最近的工作频繁使用lua, 发现用lua写起业务逻辑来非方便, 而skynet也使用了lua来作为业务逻辑编写的语言, 有大神在我也就不用怕了, 直接嵌入luaVM.

Node.js的纯异步理论看起来非常不错,他使用一个线程来跑逻辑, 然后开一个线程池来处理那些耗时的操作请求, 所有耗时操作都是通过线程池来异步完成的。

然而我一直在想,在一个纯逻辑服务器,除了网络I/O,磁盘I/O之及定时器外,应该几乎其他的事都是要通过CPU来做的,如果是这样,将那些耗费cpu的操作同样放入线程池中对于一般的服务器来说似乎意义不大(Node.js可能是要做的更统一,更通用)。即然这样,我仅仅在网络I/O,磁盘I/O及定时器上去采用异步模式,其他计算等耗费cpu的操作直接阻塞完成就好了。为了不使所有socket都因为一个socket的耗cpu操作而全部阻塞, 同时也为了更好的利用多核cpu,可以开多个进程/线程来处理socket连接。

最近一直看《Unix编程艺术》, 所以受多进程单线程的影响颇为严重,首先便选了master-worker模式来进行实现。

master与每个worker建立一条UNIX域socket以htohs(sizeof(SOCKET_FD) + PACKET_LEN),SOCKET_FD,PACKET的数据格式进行通信。

master处理的事情很简单,就是负责处理高并发连接以及数据缓存打包工作,以及以后的加密解密工作。每接到一个socket发过来的数据时, 就是去检测是否已经接收到一个完整的包,如果是则通过负载均横算法将其通过Unix域socket转发到相应的worker上去处理相应的逻辑。这样就把网络I/O剥离到master上去处理, worker就不必再关注网络I/O的问题了。

然后为每一个worker实现一个定时器线程及log线程(也许直接用syslog就可以获得速度还不错的性能, 这个有待测试),用来异步处理定时器和磁盘I/O。

每一个worker程序仅仅只需要处理一条连接,而且接到的数据一定是完整的数据包, 因此不必将cpu开销浪费在连接处理以及数据缓存合并上。这样worker逻辑仅仅处理数据包逻辑,而不心关心数据包的缓存合并问题则内部逻辑可以更简单。

而且由于大部分情况下程序都是顺序执行的, 在写代码时就可以更放心, 而不必去踩很多异步的坑。


然而随着我采用这套程序与同事搭档做三国杀之后,我发现我考虑的似乎有些少了。上而的程序结构似乎有很多缺陷。

worker上的逻辑需要连接数据库,这样以前专门由master来做的socket连接处理以前数据缓存合并的操作,就会有一部分转接到worker上做,逻辑切分就显得不那么的干净了。
worker没有办法主动请求master去关闭一个socket连接
而且之前master与worker的数据格式都是约定死的, htons(PACKET_LEN),PACKET, 而与数据库的socket协议则可能是字符串协议, 以”\r\n\r”结束, 这样势必会造成对master和worker之间的连接进行特殊处理。

想要解决这些问题势必要在woker和master之间再建立一条commad unix域socket来通信, 但是这样,会大大增加worker中逻辑的复杂度。


仔细想想,《Unix编程艺术》很反对多线程的一点就是资源以及内存的共享会使程序员不知不觉间步入深渊(死锁,资源访问冲突等)。

如果使用多进程的设计思路来使用多线程, 那么多线程出问题的概率及开发复杂度应该会大大降低。

于是一咬牙将其重构成单进程多线程模式, 将master上的功能单独用一个_socket线程来实现。将每一个worker同样使用一个_worker线程来实现。同时将之前所有worker上的timer合并为一个timer

重构后在数据的缓存和解包上做了一点小小的改变,为了解决不同连接可能有不同数据包格式的情况,_socket线程在接收到数据之后不再缓存解包,而是直接将数据转发到lua中相应的数据包处理函数中, 由业务逻辑来选择数据包的解析方式, 这一点其实是抄的skynet的做法。

btw, 即使我已经尽力去使用多进程的设计思想, 但是还是避免不了加了一些锁, 所以整个程序依然看起来比多进程要复杂的多, 但是这些锁正是为了解决“要在woker和master之间再建立一条commad unix域socket来通信”的问题,
所以相比而言, 重构后整个程序的复杂度, 还是要低于解决上述多进程的缺陷之后所带来的复杂度。



发表评论