记录link包的重构(3)

Go设计分享 by 达达 at 2014-12-28

自从上次记录link包的重构之后,link包又有了很大的变化,期间加入了Buffer的设计,然后进一步的接口化Buffer,之后又去掉了接口化的Buffer设计,回到内置Buffer类型,再之后又加入了Buffer对象池,删除了手工重用Buffer相关的API,昨天又对Buffer池做了性能优化,改为无锁结构。这些结构变化之间是一系列的思路转变,今天整理一篇日志记录一下这个过程。

首先先说一下Buffer的引入,在引入Buffer类型之前,也就是之前分享link包重构思路的文章中,link包的消息首发是直接传递[]byte类型的。直接传递[]byte的问题是约束性低,要提供统常用的消息封包解包函数也不方便。于是我把消息收发的[]byte类型,改为了InMessage和OutMessage类型。(对应版本

在引入InMessage和OutMessage类型之后,我认为不够灵活,如果项目需要在消息封包过程中加密解包过程中解密或者需要计算校验和又或者不用大小端数据格式而是自己打乱的数据格式,InMessage和OutMessage类型就会成为束缚。所以我又把InMessage和OutMessage抽象为接口,同时因为已经存在Message接口,所以为了避免混淆概念,把新的接口命名为InBuffer和OutBuffer。(对应版本

有了InBuffer和OutBuffer接口之后,link包的使用者可以根据自己需要实现自己的InBuffer和OutBuffer,这样就可以在消息封包解包过程中做任何事情。但是我自己在review代码并尝试做一个用md5做校验和的自定义Buffer实现demo时,发现InBuffer和OutBuffer的接口实现很繁琐,需要自己实现一整套各种数据类型的写入和读取。

经过重新思考,我发觉InBuffer和OutBuffer替代[]byte类型是必要的,这样可以约束接口同时提供便利的数据解析方法,但是接口化的InBuffer和OutBuffer并不是必须的,如果要对数据加解密,或者做更高级别的定制,只需要能方便的获取到原始的[]byte数据就可以了,大部分时候调用者是不需要关心原始数据的。所以我又去掉了InBuffer和OutBuffer接口,改为直接内置InBuffer和OutBuffer数据类型,同时这两个数据类型实现了io.Reader和io.Writer接口,这样就可以很方便的和数据加解密或者编解码的包配合使用了。

在简化InBuffer和OutBuffer的过程中我顺便也简化了PacketWriter和PacketReader接口,把它们并入到Protocol接口。又因为InBuffer和OutBuffer转为内置类型,所以创建和销毁变得更可控了,于是我又利用sync.Pool实现了一个对象池,然后去掉了原先为手工重用Buffer而设计的API。

这里需要说明一下为什么要用对象池替代掉原先的手工重用Buffer。手工重用Buffer的问题有两个,首先是dirty,本来简单的消息收发接口,却要为Buffer重用设计两套;其次是对象重用率没有对象池高,因为手工重用Buffer缺乏统一管理,只能单个Session自己重用,如果Session频繁创建和销毁,重用的Buffer也会跟着频繁创建销毁。

在引入sync.Pool实现的对象池之后,又遇到一个纠结的问题,Buffer是要自动释放回对象池还是手工释放。于是我测试了一下runtime.SetFinalizer函数的性能,发现调用代价挺高,于是决定土一些,又调用者自己释放Buffer。不释放Buffer的结果也不会很惨,至少不会内存泄漏,只是对象池里面一直没有可重用的对象,所以等效于没有用对象池,外加对象池自身的一些开销。

那对象池开销多大呢?是否比每次创建新对象划算呢?我又做了一个性能测试,发现单线测试时,sync.Pool的性能跟创建一个4K的[]byte接近,多线测试时性能有所下降。我就有点纠结了,能不能进一步提高性能呢?

最后我实现了一个基于无锁链表的内存池,替代掉了sync.Pool的实现,单线性能测试和多线性能测试都有稳定的表现,并且比sync.Pool高了不少。

在这段时间的重构过程中,异步消息发送的接口也做了一点改进,异步消息发送后返回一个AsyncWork对象,调用者可以决定在什么时机等待和检查异步发送的结果。

对于之后的版本,我打算进一步优化,目标是去掉BufferConn,现在的BufferConn虽然利用bufio减少了系统io的次数,但是bufio内部维护了一份预读数据,每次都要做数据复制,因为link包自己有InBuffer类型,可以用bufio的思路把预读实现在InBuffer中,从而减少掉一次数据复制。(更新:经尝试发现每次Read用到的Buffer对象无法保持同一个,所以预读的数据会丢失,无法做到预读,只能老老实实的用buffio了。)