大型的网络应用服务器(比如即时通讯及游戏服务器等)通常有大量的服务器内部数据通讯需求,几个月前曾经介绍过通讯层的实现再谈点对点通讯模块的故障问题,目前这个程序已经经历了数个版本的重构。今天进行了一次小组code review,讨论了当前版本的一些问题。
I. Background
我们为什么需要一个独立的通讯层
对于服务器内部的通讯功能,已经有TCP提供可靠传输,已经有类似Apache MINA这样的框架来提供上层的封装,业务逻辑代码完全可以直接基于socket或MINA这样的框架来直接传送数据,我们为什么还需要包装一个独立的通讯层?
目前考虑到的原因有
- Connection pool作用。为了节点间更高的throughput, 通常两节点之间会使用n个固定连接来传输。n可设为CPU内核数大小。
- 多节点互相连接的处理。一个节点需要连接n个节点发送信息。如果这部分逻辑放在业务调用层来实现,业务层会变得臃肿。
- 处理自动重连逻辑,单个节点可以停用并重新启用。将这部分逻辑封装到通讯层,上层调用会更优雅。
- 服务器之间通讯很短时间内不稳定(比如10s左右的中断)的处理。如果IDC内部或IDC之间通讯不稳定会造成这种现象。我们希望这种短暂的中断,不要引起业务逻辑重新选择节点,造成大量数据迁移及cache重构带来的内部振荡。
- 海量数据传输最优化,大部分时间单个节点单向传输会>10k个数据包/s,独立的通讯层容易测试观察该环节是否存在瓶颈。
通讯层不做什么
- 序列化/反序列化,这个通常使用其他的框架或技术来做,比如protocol buffers, json等
- RPC, 我们不做rpc的事情,已有的各种RPC技术已经做得足够好了,如果有RPC需求,我们会使用其他现有的技术。
II. Code Review过程
逻辑介绍
首先由主程介绍了目前的数据流程,架构图及核心代码片断。比较核心的重发机制图示意如下
(Figure 1: 重发的流程)
过程说明:
- 每个数据包有个TTL值,TTL>0之前会一直重试发给同一节点。这主要避免短期网络中断带来集群内振荡。
- 如果正常的节点投递失败,则继续投递到backup节点。Backup节点由调用方在投递时候指定。类似在邮政寄快递,需要填写如果投递不到收件人怎么处理。通讯层只是根据投递地址表(list)来进行二次投递。
- 如果backup也失败,则返回调用方的callback FailureHandler。这点有点类似Erlang里面失败处理的思路。
听众提出的问题
- 重发那一段流程是否必要,因为TCP本身已经有重发逻辑。应用层再加上一层重发处理,原理上可以应付短暂的网络中断,但是实际运行过程中能否真正有很大效果。(目前运行的情况也没有很明确的数据来支持这一点)
- 目前一个节点需要连接的多个节点是静态配置的,即由程序启动时候决定,没有做自动发现及删除。目标节点是否可用由发送方自行判断。因此提出一个问题,backup节点选择由调用方来指定是否最佳?可否让通讯层自动协商,内部选择另一个同一类型的节点来做替代节点?但实现通讯层节点自动协商机制会增加程序复杂度。毕竟我们不是P2P程序,节点故障发生的情况很少。因此需要综合来权衡。
- 目前的结构处理几十到上百个节点的集群问题不大,如果要用在上千个节点的场合,有哪些环节需要调整?
其他提出的一些问题是针对具体代码写法的,由于不具有广泛借鉴意义,就不一一赘述。
在上千个节点的场合,节点故障就不会是“很少”了。
所以,是否要实现额外的复杂度,还是取决于目标的规模
楼主的重发逻辑感觉是把UDP网络层做的工作加到TCP上,这样严格的重发会导致许多问题(接收发对数据包的确认,发送方对确认包的接收,接收方的处理速度,接收方对收到重复包的处理,发送发ttl_X的时间设置,等等都需要根据实际情况调整),总之,搞起来很麻烦,而且实现后会不断有问题要进行调整
大规模集群系统中,完美的方案是引入一个类似google chubby的paxos仲裁组件来充当配置系统.
节点的增/删都会由仲裁组件发现并callback通知到调用方.