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

【Go】使用压缩文件优化io (一),第1张

概述原文连接:https://blog.thinkeridea.com/201906/go/compress_file_io_optimization1.html 最近遇到一个日志备份 io 过高的问题,

原文连接:https://blog.thinkeridea.com/201906/go/compress_file_io_optimization1.html

最近遇到一个日志备份 io 过高的问题,业务日志每十分钟备份一次,本来是用 Python 写一个根据规则扫描备份日志问题不大,但是随着业务越来越多,单机上的日志文件越来越大,文件数量也越来越多,导致每每备份的瞬间 io 阻塞严重, cpu 和 load 异常的高,好在备份速度很快,对业务影响不是很大,这个问题会随着业务增长,越来越明显,这段时间抽空对备份方式做了优化,效果十分显著,整理篇文章记录一下。

背景说明

服务器配置:4 核 8G; 磁盘:500G

每十分钟需要上传:18 个文件,高峰时期约 10 G 左右

业务日志为了保证可靠性,会先写入磁盘文件,每10分钟切分日志文件,然后在下十分钟第一分时备份日志到 OSS,数据分析服务会从在备份完成后拉取日志进行分析,日志备份需要高效快速,在最短的时间内备份完,一般备份均能在几十秒内完成。

备份的速度和效率并不是问题,足够的快,但是在备份时 io 阻塞严重导致的 cpu 和 load 异常,成为业务服务的瓶颈,在高峰期业务服务仅消耗一半的系统资源,但是备份时 cpu 经常 100%,且 iowait 可以达到 70 多,空闲资源非常少,这样随着业务扩展,日志备份虽然时间很短,却成为了系统的瓶颈。

后文中会详细描述优化前后的方案,并用 go 编写测试,使用一台 2 核4G的服务器进行测试,测试数据集大小为:

文件数:336原始文件:96G压缩文件:24G压缩方案:lzoGoroutine 数量:4优化前

优化前日志备份流程:

根据备份规则扫描需要备份的文件使用 lzop 命令压缩日志上传压缩后的日志到 OSS

下面是代码实现,这里不再包含备份文件规则,仅演示压缩上传逻辑部分,程序接受文件列表,并对文件列表压缩上传至 OSS 中。

.../pkg/aliyun_oss 是我自己封装的基于阿里云 OSS *** 作的包,这个路径是错误的,仅做演示,想运行下面的代码,OSS 交互这部分需要自己实现。

package mainimport (	"bytes"	"fmt"	"os"	"os/exec"	"path/filepath"	"sync"	"time"	".../pkg/aliyun_oss")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(startTime time.Time) {		fmt.Printf("共耗时:%s\n",time.Now().Sub(startTime).String())	}(startTime)	var wg sync.WaitGroup	n := 4	c := make(chan string)	// 压缩日志	wg.Add(n)	for i := 0; i < n; i++ {		go func() {			defer wg.Done()			for file := range c {				cmd := exec.Command("lzop",file)				cmd.Stderr = &bytes.Buffer{}				err := cmd.Run()				if err != nil {					panic(cmd.Stderr.(*bytes.Buffer).String())				}			}		}()	}	for _,file := range files {		c <- file	}	close(c)	wg.Wait()	fmt.Printf("压缩耗时:%s\n",time.Now().Sub(startTime).String())	// 上传压缩日志	startTime = time.Now()	c = make(chan string)	wg.Add(n)	for i := 0; i < n; i++ {		go func() {			defer wg.Done()			for file := range c {				name := filepath.Base(file)				err := oss.PutObjectFromfile("tmp/"+name+".lzo",file+".lzo")				if err != nil {					panic(err)				}			}		}()	}	for _,file := range files {		c <- file	}	close(c)	wg.Wait()	fmt.Printf("上传耗时:%s\n",time.Now().Sub(startTime).String())}

程序运行时输出:

待备份文件数量:336压缩耗时:19m44.125314226s上传耗时:6m14.929371103s共耗时:25m59.118002969s

从运行结果中可以看出压缩文件耗时很久,实际通过 iostat 命令分析也发现,压缩时资源消耗比较高,下面是 iostat -m -x 5 10000 命令采集各个阶段数据。

程序运行前
avg-cpu:  %user   %nice %system %iowait  %steal   %IDle           2.35    0.00    2.86    0.00    0.00   94.79Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %utilvda               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.00vdb               0.00     0.60    0.00    0.60     0.00     4.80    16.00     0.00    0.67    0.00    0.67   0.67   0.04
压缩日志时
avg-cpu:  %user   %nice %system %iowait  %steal   %IDle          10.84    0.00    6.85   80.88    0.00    1.43Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %utilvda               0.00     0.00    0.60    0.00     2.40     0.00     8.00     0.00    0.67    0.67    0.00   0.67   0.04vdb              14.80  5113.80 1087.60   60.60 78123.20 20697.60   172.13   123.17  106.45  106.26  109.87   0.87 100.00avg-cpu:  %user   %nice %system %iowait  %steal   %IDle          10.06    0.00    7.19   79.06    0.00    3.70Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %utilvda               0.00     0.00    1.60    0.00   103.20     0.00   129.00     0.01    3.62    3.62    0.00   0.50   0.08vdb              14.20  4981.20  992.80   52.60 79682.40 20135.20   190.97   120.34  112.19  110.60  142.17   0.96 100.00
上传日志时
avg-cpu:  %user   %nice %system %iowait  %steal   %IDle           6.98    0.00    7.81    7.71    0.00   77.50Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %utilvda               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.26vdb               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.74avg-cpu:  %user   %nice %system %iowait  %steal   %IDle           7.06    0.00    8.00    4.57    0.00   80.37Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %utilvda               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.12vdb               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_awaitw_await 都到了一百多,且 iowait 高达 80.88%,几乎耗尽了所有的 cpu,上传时 iowait 是可以接受的,因为只是单纯的读取压缩文件,且压缩文件也很小。

分析问题

上述结果中发现程序主要运行消耗在压缩日志,那优化也着重日志压缩的逻辑上。

压缩时日志会先压缩成 lzo 文件,然后再上传 lzo 文件到阿里云 OSS 上,这中间发生了几个过程:

读取原始日志文件压缩数据写入 lzo 文件读取 lzo 文件http 发送读取的内容

压缩时 r_awaitw_await 都很高,主要发生在读取原始日志文件,写入 lzo 文件, 怎么优化呢?

先想一下原始需求,读取原始文件 -> 上传数据。但是直接上传原始文件,文件比较大,网络传输慢,而且存储费用也比较高,怎么办呢?

这个时候我们期望可以上传的是压缩文件,所以就有了优化前的逻辑,这里面产生了一个中间过程,即使用 lzop 命令压缩文件,而且产生了一个中间文件 lzo 文件。

读取原始文件和上传数据是必须的,那么可以优化的就是压缩的流程了,所以 r_await 是没有办法优化的,那么只能优化 w_awaitw_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_1lzo1x_999)都提供了接口,貌似 Go 可以直接用了也就这一个库了。

发现这个库实现了 io.Readerio.Writer 接口,io.Reader 读取压缩文件流,输出解压缩数据,io.Writer 实现输入原始数据,并写入到输入的 io.Writer

想实现压缩数据流,看来需要使用 io.Writer 接口了,但是这个输入和输出都是 io.Writer,这可为难了,因为我们读取文件获得是 io.Reader,http 接口输入也是 io.Reader,貌似没有可以直接用的接口,没有办法实现了吗,不会我们自已封装一下,下面是封装的 lzo 数据流压缩方法:

package lzoimport (	"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,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.BestSpeed)}

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

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

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

package mainimport (	"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.60Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %utilvda               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.00vdb               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.00avg-cpu:  %user   %nice %system %iowait  %steal   %IDle          26.69    0.00    8.42   64.27    0.00    0.62Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %utilvda               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.92vdb               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_awaitw_await 被完全优化,iowait 有明显的改善,运行时间更短了,效率更高了,对 io 产生影响的时间也更短了。

优化期间遇到的问题

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

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

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

总结

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

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

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

当然我们服务器是高速云盘(机械盘),由于机械盘物理特征只能是半双工,要么读、要么写,所以优化掉 w_await 确实效率会提升很多,但是依然会对业务服务写有影响。

转载:

本文作者: 戚银(thinkeridea)

本文链接: https://blog.thinkeridea.com/201906/go/compress_file_io_optimization1.html

版权声明: 本博客所有文章除特别声明外,均采用 CC BY 4.0 CN协议 许可协议。转载请注明出处!

总结

以上是内存溢出为你收集整理的【Go】使用压缩文件优化io (一)全部内容,希望文章能够帮你解决【Go】使用压缩文件优化io (一)所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/1259703.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-07
下一篇 2022-06-07

发表评论

登录后才能评论

评论列表(0条)

保存