今日头条Go建千亿级微服务的实践(3)
这是一个基础存储服务,提供 SetData 和 GetDataByRange 两个方法,分别实现批量存储数据和按照时间区间批量获取数据的功能.为了提高性能,存储的方式是以用户 ID 和一段时间作为 key,时间区间内的所有数据作为 value 存储到 KV 数据库中.因此,当需要增加新的存储数据时候就需要先从数据库中读取数据,拼接到对应的时间区间内再存到数据库中. 对于读取数据的请求,则会根据请求的时间区间计算对应的 key 列表,然后循环从数据库中读取数据. 这种情况下,高峰期服务的接口响应时间比较高,严重影响服务的整体性能.通过上述性能分析方法对于高峰期服务进行分析之后,得出如下结论: 问题点:
优化思路:
分析服务接口功能可以发现,数据解压缩,反序列化这个过程是最频繁的,这也符合性能分析得出来的结论.仔细分析解压缩和反序列化的过程,发现对于反序列化操作而言,需要一个”io.Reader”的接口,而对于解压缩,其本身就实现了”io.Reader“接口.在 Go 语言中,“io.Reader”的接口定义如下: 这个接口定义了 Read 方法,任何实现该接口的对象都可以从中读取一定数量的字节数据.因此只需要一段比较小的内存 Buffer 就可以实现从解压缩到反序列化的过程,而不需要将所有数据解压缩之后再进行反序列化,大量节省了内存的使用. 为了避免频繁的 Buffer 申请和释放,使用“sync.Pool”实现了一个对象池,达到对象复用的目的. 此外,对于获取历史数据接口,从原先的循环读取多个 key 的数据,优化为从数据库并发读取各个 key 的数据.经过这些优化之后,服务的高峰 PCT99 从100ms降低到15ms. 上述是一个比较典型的 Go 语言服务优化案例.概括为两点:
优化的过程中使用了 pprof 工具发现性能瓶颈点,然后发现“io.Reader”接口具备的 Pipeline 的数据处理方式,进而整体优化了整个服务的性能. 服务监控Go 语言的 runtime 包提供了多个接口供开发者获取当前进程运行的状态.在 kite 框架中集成了协程数量,协程状态,GC 停顿时间,GC 频率,堆栈内存使用量等监控.实时采集每个当前正在运行的服务的这些指标,分别针对各项指标设置报警阈值,例如针对协程数量和 GC 停顿时间.另一方面,我们也在尝试做一些运行时服务的堆栈和运行状态的快照,方便追查一些无法复现的进程重启的情况. 编程思维和工程性相对于传统 Web 编程语言,Go 在编程思维上的确带来了许多的改变.每一个 Go 开发服务都是一个独立的进程,任何一个请求处理造成 Panic,都会让整个进程退出,因此当启动一个协程的时候需要考虑是否需要使用 recover 方法,避免影响其它协程.对于 Web 服务端开发,往往希望将一个请求处理的整个过程能够串起来,这就非常依赖于 Thread Local 的变量,而在 Go 语言中并没有这个概念,因此需要在函数调用的时候传递 context. 最后,使用 Go 开发的项目中,并发是一种常态,因此就需要格外注意对共享资源的访问,临界区代码逻辑的处理,会增加更多的心智负担.这些编程思维上的差异,对于习惯了传统 Web 后端开发的开发者,需要一个转变的过程. 关于工程性,也是 Go 语言不太所被提起的点.实际上在 Go 官方网站关于为什么要开发 Go 语言里面就提到,目前大多数语言当代码量变得巨大之后,对代码本身的管理以及依赖分析变得异常苦难,因此代码本身成为了最麻烦的点,很多庞大的项目到最后都变得不敢去动它.而 Go 语言不同,其本身设计语法简单,类C的风格,做一件事情不会有很多种方法,甚至一些代码风格都被定义到 Go 编译器的要求之内.而且,Go 语言标准库自带了源代码的分析包,可以方便地将一个项目的代码转换成一颗 AST 树. 下面以一张图形象地表达下 Go 语言的工程性:
同样是拼成一个正方形,Go 只有一种方式,每个单元都是一致.而 Python 拼接的方式可能可以多种多样. 写在最后(编辑:ASP站长网) |