Goim 是毛剑同学写的 IM 服务,纯 Golang 实现,目前应用在 Bilibili 产品线上。最近写了两个项目反垃圾和广告系统,都是内部服务,而长连接 IM 类的对我而言非常陌生。业余时间研究 goim 代码,颇受启发,比如分层设计,数据合并,定时器优化,对象优化和 RPC 实现优化,基本上常用的组件都被 goim 重写或改造。做个笔记,先分析业务代码,优化代码放到后几篇,和大家分享,轻拍,轻虐...
Comet 角色定位
在整个架构中,系统被分成 Comet, Logic, Job, Router 四大模块,各个模块通过 RPC 通信,参考官方中文文档,Comet 程序是连接层,暴露给公网,所有的业务处理推给 Logic 模块,通过 RPC 通信。这样设计的好处在于,长连接逻辑很少变动,稳定的保持公网连接,而后端 Logic, Router 模块经常变动,重启不会影响连接层。
几个重要的结构体
做为典型代码即注释的开源项目,goim 基本无太多阅读障碍,几个逻辑点梳理下很快就会明白。
Bucket: 每个 Comet 程序拥有若干个 Bucket, 可以理解为 Session Management, 保存着当前 Comet 服务于哪些 Room 和 Channel. 长连接具体分布在哪个 Bucket 上呢?根据 SubKey 一致性 Hash 来选择。
Room: 可以理解为房间,群组或是一个 Group. 这个房间内维护 N 个 Channel, 即长连接用户。在该 Room 内广播消息,会发送给房间内的所有 Channel.
Channel: 维护一个长连接用户,只能对应一个 Room. 推送的消息可以在 Room 内广播,也可以推送到指定的 Channel.
Proto: 消息结构体,存放版本号,操作类型,消息序号和消息体。
多协议支持
Goim 支持 Tcp, Http, WebSocket, TLS WebSocket. 非常强大,底层原理一样,下面的分析都是基于 Tcp 协议。
Bucket
先来看看结构体的定义
定义很明了,维护当前消息通道和房间的信息,方法也很简单,加减 Channel 和 Room. 一个 Comet Server 默认开启 1024 Bucket, 这样做的好处是减少锁 ( Bucket.cLock ) 争用,在大并发业务上尤其明显。
Room
Room 结构体稍显复杂一些,不但要维护所属的消息通道 Channel, 还要消息广播的合并写,即 Batch Write, 如果不合并写,每来一个小的消息都通过长连接写出去,系统 Syscall 调用的开销会非常大,Pprof 的时候会看到网络 Syscall 是大头。
Logic Server 通过 RPC 调用,将广播的消息发给 Room.Push, 数据会被暂存在 vers, ops, msgs 里,每个 Room 在初始化时会开启一个 groutine 用来处理暂存的消息,达到 Batch Num 数量或是延迟一定时间后,将消息批量 Push 到 Channel 消息通道。
Channel
总是觉得起名叫 Session 更直观,并且不和语言层面的 "channel" 冲突。Writer/Reader 就是对网络 Conn 的封装,SvrProto 是一个 Ring Buffer,保存 Room 广播或是直接发送过来的消息体。
消息流转
这里只分析 Comet 代码,所以消息生成暂时不提
1. Client 连接到 Comet Server, 握手认证
2. 新建当前长连接的 Channel, 由于 Comet 服务不处理业务逻辑,需要 RPC 去 Logic Server 获取该 Channel 的订阅信息。同时 Channel 开启一个 dispatchTCP groutine, 阻塞等待 Ring Buffer 数据可用,发送到 Client。
3. Logic 服务通过 RPC, 将消息写到 Room (广播)或是直接写到指定 Channel (单播)。注意这里,广播是有写合并 BatchWrite,
而单播没有,消息生成后立刻发送。4. Room 里的广播消息到达一定数量 Batch Num, 或是延迟等待一定时间后,将消息写到 Channel Ring Buffer。
小结
Goim 分层很合理,上面只是对 Comet Server 进行业务层面的解读。还有很多性能优化上的,比如 bytes.Pool, time.Timer, RPC 等优化留到最后慢慢分享吧。
大家玩的开心!
补充-来自毛剑
1. bucket按照key的cityhash求余命中的,没有用一致性hash,因为这里不涉及迁移
2. 私信发送其实也有合并的,和room合并不同的是,在ringbuffer取消息饥饿时候才会真正flush
3. 还有一个优化可以改进,因为room有个特点大家消息可能都一样,所以在room提前合并成字节buffer,然后广播所有人,避免每个人都序列化一次,然后利用gc来处理这个buffer的释放,这样可以节省大量cpu,目前这个优化还没做