开工福利|免费学 2200+ 精品线上课,企业成员人人可得! 了解详情
写点什么

记一次获得 3 倍性能的 Go 程序优化实践

  • 2017-09-13
  • 本文字数:3120 字

    阅读完需:约 10 分钟

Go 的高性能真不是吹的,当然是要在足够的优化之后。获得 3 倍性能的优化实践,值得借鉴。

背景介绍

之前公司一直使用 Logstash 作为日志文件采集客户端程序。Logstash 功能强大,有丰富的数据处理插件及很好的扩展能力,但由于使用 JRuby 实现,性能堪忧。而 Filebeat 是后来出现的一个用 Go 语言实现的、更轻量级的日志文件采集客户端。性能不错、资源占用少,但几乎没有任何解析处理能力。

通常的使用场景是使用 Filebeat 采集到 Logstash 解析处理,然后再上传到 Kafka 或 Elasticsearch。值得注意的是,Logstash 和 Filebeat 都是 Elastic 公司的优秀开源产品。

为了提高客户端的日志采集性能,又减少数据传输环节和部署复杂度,并更充分地将 Go 语言的性能优势利用于日志解析,于是决定在 Filebeat 上通过开发插件的方式,实现针对公司日志格式规范的解析,直接作为 Logstash 的替代品。

实现与优化

Version 1.0

先做一个最简单的实现,即用 Go 自带的正则表达式包 regexp 做日志解析。性能已经比 Logstash(也是通过开发插件做规范日志解析)高出 30%。

这里的性能测试着眼于日志采集的瓶颈——解析处理 环节,指标是在限制只使用一个 CPU core 的条件下(在服务器上要尽量减少对业务应用的资源占用),采集并解析 1 百万条指定格式和长度的日志所花费的时间。

测试环境是 1 台主频为 3.2GHz 的 PC。为了避免 disk IO 及 page cache 的影响,将输入文件和输出文件都放在 /dev/shm 中。对于 Filebeat 的 CPU 限制,是通过启动时指定环境变量 GOMAXPROCS=1 实现的。

这一版本处理 1 百万条日志花费的时间为 122 秒,即每秒 8200 条日志。

Version 2.0

接下来尝试做一些优化,看看这个 Go 插件的性能还可不可以有些提升。首先想到的是替换 regexp 包。Linux 下有一个 C 实现的 PCRE 库, https://github.com/glenn-brown/golang-pkg-pcre 这个第三方包正是将 PCRE 库应用到 Golang 中。CentOS 下需要先安装 pcre-devel 这个包。

这个版本的处理时间为 97 秒,结果显示比第一个版本的处理性能提升了 25%。

Version 3.0

第三个版本,是完全不使用正则表达式,而是针对固定的日志格式规则,利用 strings.Index() 做字符串分解和提取操作。这个版本的处理时间为 70 秒,性能又大大的提升了将近 40%。

Version 4.0

那还有没有进一步提升的空间呢?有。就是 Filebeat 用作序列化输出的 JSON 包。我们的日志上传使用 JSON 格式,而 Filebeat 使用 Go 自带的 encoding/json 包是基于反射实现的,性能一直广受诟病。如果对 JSON 解析有优化的话,性能提高会是很可观的。

既然我们的日志格式是固定的,解析出来的字段也是固定的,这时就可以基于固定的日志结构体做 JSON 的序列化,而不必用低效率的反射来实现。Go 有多个针对给定结构体做 JSON 序列化 / 反序列化的第三方包,我们这里使用的是 easyjson https://github.com/mailru/easyjson。

在安装完 easyjson 包后,对定义了日志格式结构体的程序文件执行 easyjson 命令,会生成一个 xxx_easyjson.go 的文件,里面包含了这个结构体专用的 Marshal/Unmarshal 方法。

这样一来,处理时间又缩短为 61 秒,性能提高 15%。

这时,代码在我面前,已经想不出有什么大的方面还可以优化的了。是时候该本文的另一个主角,火焰图出场了。

on-cpu/off-cpu 火焰图

火焰图是性能分析的一个有效工具, http://www.brendangregg.com/flamegraphs.html 这里是它的说明。通常看到的火焰图,是指 on-cpu 火焰图,用来分析 CPU 都消耗在哪些函数调用上。

on-cpu 火焰图

安装完 FlameGraph https://github.com/brendangregg/FlameGraph 工具后,先对目前版本的程序运行一次性能测试,按照说明抓取数据生成火焰图如下。

:FlameGraph 对于 C/Go 程序是通用的。对于 Go 程序,也可以使用自带的 net/http/pprof 包作为数据源,然后安装 Uber 的 go-torch https://github.com/uber/go-torch 工具来自动调用 FlameGraph 脚本生成 on-cpu 火焰图,执行会稍为简便一些。参见 go-torch 说明。

(点击放大图像)

图中纵向代表的是函数调用栈,横向各个方块的宽度代表的是占用 CPU 时间的比例,需要留意的是靠近顶端的大长条。方块的颜色是随机的没有实际意义。

从上图可以看到 CPU 时间占用最多的主要有两块。一块是 Output 处理部分,稍为大头的是 JSON 处理,这块已经优化过没什么可以做的了。另一块就比较奇怪了,是 common.MapStr.Clone() 方法,居然占了 40% 的 CPU 时间。再往上看,主要是 Errorf 的处理。一看代码,马上明白了。

(点击放大图像)

common.MapStr 是在 pipeline 中存放日志内容的结构体,它的 Clone() 方法实现里判断一个子键值是否为嵌套的 MapStr 结构时,是通过判断 toMapStr() 方法是否返回 error。从这里看,生成 error 对象的代价是非常可观的。于是,一个显然的 fix,就是将 toMapStr() 中的判断方法移到 Clone() 中并避免生成 error。

Version 5.0

对修改后的代码重新生成一张火焰图如下。

(点击放大图像)

这时 common.MapStr.Clone() 从图中已经几乎找不见了,证明花费的 CPU 时间已经可以忽略不计。

测试时间一下子缩短到了 46 秒,节省了 33%,非常大的改善!

off-cpu 火焰图

到现在,还有一个之前未提到的问题没有解决——在限制使用一个 core 之后,测试运行时 CPU 利用率只能跑到 80% 多。是不是由于有锁存在影响了性能呢?

这时候,又该请 off-cpu 火焰图 出场了。off-cpu 火焰图,是用来分析程序没有有效利用 CPU 的时候,消耗在什么地方了,在 http://www.brendangregg.com/FlameGraphs/offcpuflamegraphs.html 有详细的介绍。

数据收集比 on-cpu 火焰图要复杂,可以使用大名鼎鼎的春哥提供的 openresty-systemtap-toolkit https://github.com/openresty/openresty-systemtap-toolkit 包。春哥的项目页面中没有详细说明的是 kernel-develdebuginfo 包的安装方法。在此也记录一下。

(点击放大图像)

安装完后按照说明生成了 off-cpu 火焰图如下:

(点击放大图像)

可以明显地看到,对 Registry 文件(Filebeat 用于记录文件采集列表和 offset 数据)的写操作占了一定比例。于是,尝试将 Filebeat 的 spool_size(每完成这么多条日志更新一次 Registry 文件)设置为 10240,默认值的 5 倍,运行测试 CPU 已经可以跑到 95% 以上。而将 Registry 设置到 /dev/shm/ 下也同样可以解决测试时 CPU 跑不满的问题。

这就否定了上面对锁使用不当影响性能的猜测。在实际应用时 spool_size 的设置应当依据结合了 output 端(如写入到 Kafka)的测试数据来决定。

至此,优化结束,性能达到了最初版本的 3 倍!

各个版本的具体运行性能数据如下图所示:

(点击放大图像)

需要稍作说明的是:

  • Filebeat 开发是基于 5.3.1 版本,Go 版本是 1.8
  • Logstash 的测试通过 -w 1 参数配置使用一个工作进程,并未限制使用一个 core
  • 执行时间包括了程序的启动时间(Logstash 的启动时间有将近 20 秒)

最终的优化结果是,针对特定格式和长度的日志解析能力在 PC 上达到了每秒 25000 条,即使在 CPU 主频较低的生产服务器上,也可以达到每秒 20000 条。

Go 的高性能真不是吹的,当然是要在足够的优化后:)

最后,关于 Go 的性能有一篇这样的讨论,有兴趣可以看看.

作者介绍

潘卫华 (Peter),唯品会架构师,关注基础架构方向。10 余年通信及IT 研发经验,曾任职爱立信,现任职唯品会基础架构部门。近年对基于ELK 的日志存储系统,以及基于Spark Streaming 的实时日志分析等技术有较多研究。喜欢对问题进行深入分析和创新思考,善于挖掘各种工具的潜力。


感谢雨多田光对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2017-09-13 17:158939

评论

发布
暂无评论
发现更多内容

泛型的类型擦除后,fastjson反序列化时如何还原?

码农参上

泛型 底层知识 Java 开发

深入理解和把握数字经济的基本特征

CECBC

再突破!阿里云进入Gartner云AI开发者服务挑战者象限

阿里云大数据AI技术

人工智能 机器学习 AI开发软件

观测云更新|观测云帮助文档全新上线;新增 Profile 可观测新增 Profile 可观测;新增 Deployment 网络详情及网络分布等

观测云

中国移动应用出海发展天地宽

BeeWorks

如何实现一套容器(C语言版)1

祖维

c 容器 泛型 迭代器

理论+案例,带你掌握Angular依赖注入模式的应用

华为云开发者联盟

程序员 前端 华为云

提高效率 Or 增加成本,开发人员应如何理解结对编程?

云智慧AIOps社区

团队协作 开发技巧 结对编程 开发技能 编程技巧

阻塞、非阻塞、多路复用、同步、异步、BIO、NIO、AIO 一文搞定

C++后台开发

网络编程 linux开发 epoll IO多路复用 C++开发

Go语言Redis API基本功能实践

FunTester

如何让销售管理更高效?

优秀

销售管理

20省市公布元宇宙路线图

CECBC

FastApi+Vue+LayUI实现前后端分离

Python研究所

6月月更

你了解shiro吗?手把手教你集成shiro

阿Q说代码

springboot 鉴权 shiro 身份验证

腾讯的技术牛人们,是如何完成全面上云这件事儿的?

科技热闻

A tour of gRPC:01 - 基础理论

BUG侦探

gRPC RPC buffer protocol

零信任安全模型-下一代网络安全架构

领创集团Advance Intelligence Group

架构 网络安全 零信任

Gartner最新报告:低代码应用开发平台在国内的发展

明道云

创新实力再获认可!腾讯安全MSS获2022年度云原生安全守护先锋

腾讯安全云鼎实验室

云原生 云安全

JDK动态代理为什么必须要基于接口?

码农参上

JAVA开发 动态代理 底层知识

ElasticSearch第二弹之分片原理

阿Q说代码

ES shard replica

Java基础:集合框架之Map

百思不得小赵

map Java’ 6月月更

Gartner:如何在中国成功应用多云模式

BeeWorks

数字经济时代文化消费新特征

CECBC

港股多支个股表现活跃,引发投资者对港股市场回暖猜想与关注

E科讯

NLP 论文领读|改善意图识别的语义表示:有监督预训练中的各向同性正则化方法

澜舟孟子开源社区

人工智能 自然语言处理 深度学习 预训练模型

2023年广州美博会时间地点详情

Geek_0b38bb

美博会 2023年广州美博会 春季广州美博会 广州春季美博会

区块链为化解信任危机带来新契机

CECBC

融云:让银行轻松上“云”

融云 RongCloud

Java—NIO

武师叔

6月月更

SaaS 云工具,产业互联网下的变革利器

小炮

记一次获得3倍性能的Go程序优化实践_语言 & 开发_潘卫华_InfoQ精选文章