上一篇文章《使用压缩文件优化io (一)》中记录了日志备份 io 优化方案,使用文件流数据压缩方案优化 io 性能,效果十分显著。这篇文章记录数据分析前置清洗、格式化数据的 io 优化方案,我们有一台专用的日志前置处理服务器,所有业务日志通过这台机器从 OSS 拉取回来清洗、格式化,最后进入到数据仓储中便于后续的分析。
随着业务扩展这台服务器压力越来越大,高峰时数据延迟越来越厉害,早期也是使用 Python 脚本 + awk 以及一些 shell 命令完成相关工作,在数据集不是很大的时候这种方案很好,效率也很高,随着数据集变大,发现服务器负载很高,经过分析是还是 io 阻塞,依旧采用对数据流进行处理的方案优化io,以下记录优化的过程。
背景介绍
服务器配置:4 核 8G; 磁盘:1T
分析前置服务会根据业务不同分为十分钟、一小时两个阶段拉取分析日志,每隔一个阶段会去 OSS 拉取日志回到服务器进行处理,处理过程因 io 阻塞,导致 CPU 和 load 异常高,且处理效率严重下降,这次优化主要就是降低 io 阻塞,提升 CPU 利用率 (处理业务逻辑而不是等待 io) 和处理效率。
后文中会详细描述优化前后的方案,并用 go 编写测试,使用一台 2 核4G的服务器进行测试,测试数据集大小为:
- 文件数量:432个
- 压缩文件:17G
- 解压后文件:63G
- 压缩方案:lzo
- Goroutine 数量:20
优化前
优化前日志处理流程:
- 获取待处理文件列表
- 拉取 OSS 日志到本地磁盘 (压缩文件)
- 解压缩日志文件
- 读取日志数据
- 业务处理……
- 导入到数据仓储中
导致 io 阻塞的部分主要是: 拉取 OSS 日志、解压缩日志文件及读取日志数据,优化也主要从这三块着手。
这里有一段公共的日志读取方法,该方法接收一个 io.Reader
, 并按行读取日志,并简单切分日志字段,并没有实质的处理日志数据,后面的优化方案也将使用这个方法读取日志。
1 | package main |
日志按 \r\r\n
分隔行,使用 \x01
切分字段,读取方法使用 bufio.ReadSlice
方法,避免内存分配,且当 bufio
缓冲区满之后使用 rwaBuffer
作为本地可扩展缓冲,每次扩展之后会保留最大的扩展空间,因为业务日志每行大小差不多,这样可以极大的减少内存分配,效率是 bufio.ReadLine
方法的好几倍。
1 | package main |
运行程序输出如下:
1 | 待处理文件数量:432 |
通过 iostat -m -x 5 10000
分析各个阶段结果如下:
- 下载时:
1 | avg-cpu: %user %nice %system %iowait %steal %idle |
- 解压时:
1 | avg-cpu: %user %nice %system %iowait %steal %idle |
- 读取时:
1 | avg-cpu: %user %nice %system %iowait %steal %idle |
通过 iostat
结果可以看出,在解压和读取日志时 io
阻塞比较严重,且运行时间较长,下载时 io
阻塞也存在,但还可以接受,通过下面两个方案逐渐消除掉 io
。
优化方案一
优化前的方案反应出在解压和读取日志时 io
阻塞比较严重,那么是否可以通过读取 lzo
压缩文件,以此来消除解压缩日志耗时太大、io
太高的问题呢?并且读取 lzo
压缩文件远比解压后文件小,来降低读取日志耗时太大、io
太高的问题呢?
优化后日志处理流程:
- 获取待处理文件列表
- 拉取 OSS 日志到本地磁盘 (压缩文件)
- 读取压缩日志数据
- 业务处理……
- 导入到数据仓储中
1 | package main |
这个方案消除了解压缩日志,并且直接读取压缩日志,使用 github.com/cyberdelia/lzo
包对压缩文件数据流进行边读取边解压,这次不用单独封装新的方法了,直接使用 lzo
包中的接口即可。
程序运行结果如下:
1 | 待处理文件数量:432 |
这个方案效果非常明显,总耗时从 1375.187261
降低到 418.942862
提升了 3 倍的效率,不仅消除了压缩的时间,还大大缩短了读取文件耗时,成果显著。
通过 iostat -m -x 5 10000
分析各个阶段结果如下:
下载时:
1 | avg-cpu: %user %nice %system %iowait %steal %idle |
读取时:
1 | avg-cpu: %user %nice %system %iowait %steal %idle |
通过 iostat
结果分析,下载时 io
阻塞和优化前波动不是很大,读取时的 io
优化已经非常好了,iowait
从 92.19%
降低到 14.5%
,CPU 更多的任务用来处理解压缩日志,而不是处理 io
阻塞。
优化方案二
本来优化到上面的效果已经非常满意了,不过既然开始做优化就不能草草结束了,仔细思考业务场景,需要 本地 lzo
文件?重新处理日志的频率高吗?本地 lzo
日志清理方便吗?
通过上面的几个问题发现,除非程序出现问题或者数据存储出现故障,否者极少需要重新处理日志,一年里面这种情况也是极少的,甚至不会发生。
那么思考一下,不下载日志,直接读取网络数据流,实现边下边解压边读取,这样岂不是没有 io
了吗?
优化后日志处理流程:
- 获取待处理文件列表
- 拉取 OSS 日志,在内存中解压并读取分析日志
- 业务处理……
- 导入到数据仓储中
具体实现如下:
1 | package main |
优化后只有一个流程了,代码简洁了不少,看看效率如何吧!
程序运行结果如下:
1 | 待处理文件数量:432 |
天啊发生了什么,我使劲擦了擦眼睛,太不可思议了,居然只消耗了下载日志的耗时,较上一个方案总耗时从 418.942862
降低到 285.993717
,提升了近 2 倍的效率,让我们看看上个方案下载文件耗时 286.146603
,而新方案总耗时是 285.993717
居然只用了上个优化版本的下载时间,究竟发生了什么?
通过 iostat -m -x 5 10000
分析结果如下:
1 | avg-cpu: %user %nice %system %iowait %steal %idle |
通过 iostat
结果分析,在程序运行期间没有任何 io
开销,CPU 居然还有一半的空闲,前面两个版本 CPU 是没有空闲的啊,由此看来之前 CPU 更多的消耗在 io
阻塞上了,并没有用来处理业务逻辑。
由此来看也就不足为奇了,为啥优化后只需要下载日志的时间就能处理完所有日志了,没有了 io
阻塞,CPU 更多了用来处理业务,把之前下载时写文件 io
的耗时,用来解压缩数据,读取数据,且还有更多的空闲,跑出这样的结果也就很正常了。
总结
从优化前耗时 1375.187261
秒到 285.993717
秒,性能提升 80%, 从 iowait
92.19%
到 0.31%
提升近 100%
,从没有任何 CPU 空闲到有一半空闲,这个过程中有很多值得总结的事情。
io
对性能的影响非常大,对 CPU 占用非常严重,导致 CPU 处理业务逻辑的时间片非常少。从 io
转移到 CPU 对性能提升非常明显。CPU 计算效率十分的高,从 io
密集到密集计算,只要符合业务场景,往往能给我们带来意想不到的效果。
往往优化业务并不需要十分高大上的技术,只是转变一下思路,不仅代码更少,且程序更简短、好维护、逻辑更清晰。
一定要结合实际业务场景进行思考,减少理所当然和业务无关的中间环节,往往就可以极大的提升程序效率。