原文连接: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
压缩方案:lzo
Goroutine 数量:4
优化前优化前日志备份流程:
根据备份规则扫描需要备份的文件
使用 lzop 命令压缩日志
上传压缩后的日志到 OSS
下面是代码实现,这里不再包含备份文件规则,仅演示压缩上传逻辑部分,程序接受文件列表,并对文件列表压缩上传至 OSS 中。
.../pkg/aliyun_oss 是我自己封装的基于阿里云 OSS 操作的包,这个路径是错误的,仅做演示,想运行下面的代码,OSS 交互这部分需要自己实现。
package main import ( "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.79 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 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.43 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 2.40 0.00 8.00 0.00 0.67 0.67 0.00 0.67 0.04 vdb 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.00 avg-cpu: %user %nice %system %iowait %steal %idle 10.06 0.00 7.19 79.06 0.00 3.70 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 1.60 0.00 103.20 0.00 129.00 0.01 3.62 3.62 0.00 0.50 0.08 vdb 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