知识卡片

池化技术的收益代价与过度优化反思

普通读书笔记卡

内容

在[[Go协程GC优化的三个教训]]之外,团队还尝试过更激进的GC优化手段——内存池和对象池,但这条路的经验教训更值得记录,因为它包含一次对”是否值得”的自我纠偏。多实例拆分是解决GC时长最直接的方法(实例越多、单实例内存压力越小),但360的外网只能用80和443端口,常规只能开两个实例,SO_REUSEPORT因为内核版本较低没有实践过——这说明看似最直接的优化手段,实际能不能用,还要看具体的基础设施约束。内存池和对象池的另一层代价是:使用池内资源必须加互斥锁或做原子操作(CAS)才能保证安全(实测原子操作通常更快),这种方式会降低程序的并行度,而且代码可读性会越来越像C语言——每次要手动申请、用完后要释放,对象池归还前还要reset。团队一度在应用层尝试实现了一个按大小分块、用atomic做CAS的”无锁队列”来复用内存,但复盘后得出一个更有分量的教训:在百万协程规模下做自旋操作申请复用对象,本质上是在应用层非常不优雅地重新实现Go runtime本该负责的调度工作,除非能给池化策略加上更多减少忙等的处理,否则普遍情况下这类黑科技的”使用开销”会大于它带来的收益,最终团队后续去掉了大部分这类应用层池化的”黑科技”。真正值得保留池化/复用思路的场景,是那些开销确定、集中处理数据的区域——比如RPC库、codec库、任务池内部这些地方可以尝试改造;对于固定对象(比如心跳包这类),可以用全局对象复用;针对应用层数据,做具体设计的对象池、只在部分环节复用,往往比”无差别地设计一个通用池”更容易被真实评估出效果。这条经验揭示了一般性的架构优化原则:某项优化技术(如池化)理论上确实能提升某个指标(如GC时间),但只有先算清楚它引入的额外复杂度和并发开销,再和收益比较,才知道这项优化在自己的场景下到底值不值得做——”能优化”和”值得优化”是两回事。

参考来源

- 位置:《高可用架构(第1卷)》第1章《高可用架构案例精选》"1.4 如何实现支持数亿用户的长连消息系统:Golang高并发案例"节,"1.4.4 GO语言开发问题与解决方案"(源文件:_epub-src/OEBPS/Text/Chapter1_4_5.xhtml) - 结论依据:原文说明"我们消息系统后续去除了部分此类的黑科技,试想在百万个协程里面做自旋操作申请复用的……感觉是在把runtime做的事情,非常不优雅地在应用层实现。普遍使用开销理论就大于收益。但对于RPC库或者codec库、任务池内部……可以尝试改造",直接支撑本卡片结论。 - 原始内容:实际上,我们消息系统后续去除了部分此类的黑科技……感觉是在把runtime做的事情,非常不优雅地在应用层实现。普遍使用开销理论就大于收益。但对于RPC库或者codec库、任务池内部,这些开定量协程,集中处理数据的区域,可以尝试改造。