上传日志时
avg-cpu: %user %nice %system %iowait %steal %idle 6.98 0.00 7.81 7.71 0.00 77.50 Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util vda 0.00 0.00 13.40 0.00 242.40 0.00 36.18 0.02 1.63 1.63 0.00 0.19 0.26 vdb 0.40 2.40 269.60 1.20 67184.80 14.40 496.30 4.58 15.70 15.77 0.33 1.39 37.74 avg-cpu: %user %nice %system %iowait %steal %idle 7.06 0.00 8.00 4.57 0.00 80.37 Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util vda 0.00 0.00 0.60 0.00 75.20 0.00 250.67 0.00 2.67 2.67 0.00 2.00 0.12 vdb 0.20 0.00 344.80 0.00 65398.40 0.00 379.34 5.66 16.42 16.42 0.00 1.27 43.66从 iostat 的结果中发现,压缩时程序 r_await 和 w_await 都到了一百多,且 iowait 高达 80.88%,几乎耗尽了所有的 CPU,上传时 iowait 是可以接受的,因为只是单纯的读取压缩文件,且压缩文件也很小。
分析问题上述结果中发现程序主要运行消耗在压缩日志,那优化也着重日志压缩的逻辑上。
压缩时日志会先压缩成 lzo 文件,然后再上传 lzo 文件到阿里云 OSS 上,这中间发生了几个过程:
读取原始日志文件
压缩数据
写入 lzo 文件
读取 lzo 文件
http 发送读取的内容
压缩时 r_await 和 w_await 都很高,主要发生在读取原始日志文件,写入 lzo 文件, 怎么优化呢?
先想一下原始需求,读取原始文件 -> 上传数据。但是直接上传原始文件,文件比较大,网络传输慢,而且存储费用也比较高,怎么办呢?
这个时候我们期望可以上传的是压缩文件,所以就有了优化前的逻辑,这里面产生了一个中间过程,即使用 lzop 命令压缩文件,而且产生了一个中间文件 lzo 文件。
读取原始文件和上传数据是必须的,那么可以优化的就是压缩的流程了,所以 r_await 是没有办法优化的,那么只能优化 w_await,w_await 是怎么产生的呢,恰恰是写入lzo 时产生的,可以不要 lzo 文件吗?这个文件有什么作用?
如果我们压缩文件数据流,在 读取原始文件 -> 上传数据 流程中对上传的数据流进行实时压缩,把压缩的内容给上传了,实现边读边压缩,对数据流进行处理,像是一个中间件,这样就不用写 lzo 文件了,那么 w_await 就被完全优化没了。
lzo 文件有什么作用?我想只有在上传失败之后可以节省一次文件压缩的消耗。上传失败的次数多吗?我用阿里云 OSS 好几年了,除了一次内网故障,再也没有遇到过上传失败的文件,我想是不需要这个文件的,而且生成 lzo 文件还需要占用磁盘空间,定时清理等等,增加了资源消耗和维护成本。
优化后根据之前的分析看一下优化之后备份文件需要哪些过程:
读取原始日志
在内存中压缩数据流
http 发送压缩后的内容
这个流程节省了两个步骤,写入 lzo 文件和 读取 lzo 文件,不仅没有 w_await,就连 r_await 也得到了小幅度的优化。
优化方案确定了,可是怎么实现 lzo 对文件流进行压缩呢,去 Github 上找一下看看有没有 lzo 的压缩算法库,发现 github.com/cyberdelia/lzo ,虽然是引用 C 库实现的,但是经典的两个算法(lzo1x_1 和 lzo1x_999)都提供了接口,貌似 Go 可以直接用了也就这一个库了。
发现这个库实现了 io.Reader 和 io.Writer 接口,io.Reader 读取压缩文件流,输出解压缩数据,io.Writer 实现输入原始数据,并写入到输入的 io.Writer。
想实现压缩数据流,看来需要使用 io.Writer 接口了,但是这个输入和输出都是 io.Writer,这可为难了,因为我们读取文件获得是 io.Reader,http 接口输入也是 io.Reader,貌似没有可以直接用的接口,没有办法实现了吗,不会我们自已封装一下,下面是封装的 lzo 数据流压缩方法:
package lzo import ( "bytes" "io" "github.com/cyberdelia/lzo" ) type Reader struct { r io.Reader rb []byte buff *bytes.Buffer lzo *lzo.Writer err error } func NewReader(r io.Reader) *Reader { z := &Reader{ r: r, rb: make([]byte, 256*1024), buff: bytes.NewBuffer(make([]byte, 0, 256*1024)), } z.lzo, _ = lzo.NewWriterLevel(z.buff, lzo.BestSpeed) return z } func (z *Reader) compress() { if z.err != nil { return } var nr, nw int nr, z.err = z.r.Read(z.rb) if z.err == io.EOF { if err := z.lzo.Close(); err != nil { z.err = err } } if nr > 0 { nw, z.err = z.lzo.Write(z.rb[:nr]) if z.err == nil && nr != nw { z.err = io.ErrShortWrite } } } func (z *Reader) Read(p []byte) (n int, err error) { if z.err != nil { return 0, z.err } if z.buff.Len() <= 0 { z.compress() } n, err = z.buff.Read(p) if err == io.EOF { err = nil } else if err != nil { z.err = err } return } func (z *Reader) Reset(r io.Reader) { z.r = r z.buff.Reset() z.err = nil z.lzo, _ = lzo.NewWriterLevel(z.buff, lzo.BestSpeed) }