【Go】使用压缩文件优化io (一) (3)

这个库会固定消耗 512k 内存,并不是很大,我们需要创建一个读取 buf 和一个压缩缓冲 buf, 都是256k的大小,实际压缩缓冲的 buf 并不需要 256k,毕竟压缩后数据会比原始数据小,考虑空间并不是很大,直接分配 256k 避免运行时分配。

实现原理当 http 从输入的 io.Reader (实际就是我们上面封装的 lzo 库), 读取数据时,这个库检查压缩缓冲是否为空,为空的情况会从文件读取 256k 数据并压缩输入到压缩缓冲中,然后从压缩缓冲读取数据给 http 的 io.Reader,如果压缩缓冲区有数据就直接从压缩缓冲区读取压缩数据。

这并不是线程安全的,并且固定分配 512k 的缓冲,所以也提供了一个 Reset 方法,来复用这个对象,避免重复分配内存,但是需要保证一个lzo 对象实例只能被一个 Goroutine 访问, 这可以使用 sync.Pool 来保证,下面的代码我用另一种方法来保证。

package main import ( "fmt" "os" "path/filepath" "sync" "time" ".../pkg/aliyun_oss" ".../pkg/lzo" ) func main() { var oss *aliyun_oss.AliyunOSS files := os.Args[1:] if len(files) < 1 { fmt.Println("请输入要上传的文件") os.Exit(1) } fmt.Printf("待备份文件数量:%d\n", len(files)) startTime := time.Now() defer func() { fmt.Printf("共耗时:%s\n", time.Now().Sub(startTime).String()) }() var wg sync.WaitGroup n := 4 c := make(chan string) // 压缩日志 wg.Add(n) for i := 0; i < n; i++ { go func() { defer wg.Done() var compress *lzo.Reader for file := range c { r, err := os.Open(file) if err != nil { panic(err) } if compress == nil { compress = lzo.NewReader(r) } else { compress.Reset(r) } name := filepath.Base(file) err = oss.PutObject("tmp/"+name+"1.lzo", compress) r.Close() if err != nil { panic(err) } } }() } for _, file := range files { c <- file } close(c) wg.Wait() }

程序为每个 Goroutine 分配一个固定的 compress ,当需要压缩文件的时候判断是创建还是重置,来达到复用的效果。

该程序运行输出:

待备份文件数量:336 共耗时 18m20.162441931s

实际耗时比优化前提升了 28%, 实际通过 iostat 命令分析也发现,资源消耗也有了明显的改善,下面是 iostat -m -x 5 10000 命令采集各个阶段数据。

avg-cpu: %user %nice %system %iowait %steal %idle 15.72 0.00 6.58 74.10 0.00 3.60 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.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 vdb 3.80 3.40 1374.20 1.20 86484.00 18.40 125.79 121.57 87.24 87.32 1.00 0.73 100.00 avg-cpu: %user %nice %system %iowait %steal %idle 26.69 0.00 8.42 64.27 0.00 0.62 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.20 426.80 0.80 9084.80 4.00 42.51 2.69 6.29 6.30 1.00 0.63 26.92 vdb 1.80 0.00 1092.60 0.00 72306.40 0.00 132.36 122.06 108.45 108.45 0.00 0.92 100.02

通过 iostat 发现只有 r_await, w_await 被完全优化,iowait 有明显的改善,运行时间更短了,效率更高了,对 io 产生影响的时间也更短了。

优化期间遇到的问题

首先对找到的 lzo 算法库进行测试,确保压缩和解压缩没有问题,并且和 lzop 命令兼容。

在这期间发现使用压缩的数据比 lzop 压缩数据大了很多,之后阅读了源码实现,并没有发现任何问题,尝试调整缓冲区大小,发现对生成的压缩文件大小有明显改善。

这个发现让我也很为难,究竟多大的缓冲区合适呢,只能去看 lzop 的实现了,发现 lzop 默认压缩块大小为 256k, 实际 lzo 算法支持的最大块大小就是 256k,所以实现 lzo 算法包装是创建的是 256k 的缓冲区的,这个缓冲区的大小就是压缩块的大小,大家使用的时候建议不要调整了。

总结

这个方案上线之后,由原来需要近半分钟上传的,改善到大约只有十秒(Go 语言本身效率也有很大帮助),而且 load 有了明显的改善。

优化前每当运行日志备份,CPU 经常爆表,优化后备份时 CPU 增幅 20%,可以从容应对业务扩展问题了。

测试是在一台空闲的机器上进行的,实际生产服务器本身 w_await 会有 20 左右,如果使用固态硬盘,全双工模式,读和写是分离的,那么优化掉 w_await 对业务的帮助是非常大的,不会阻塞业务日志写通道了。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpygxy.html