东亚银行、岚图汽车带你解锁 AIGC 时代的数字化人才培养各赛道新模式! 了解详情
写点什么

高频 golang 服务接口超时排查以及性能调优

  • 2020-01-10
  • 本文字数:4553 字

    阅读完需:约 15 分钟

高频 golang 服务接口超时排查以及性能调优

0. 目录

  1. 背景

  2. 解决思路

  3. 现场分析

  4. 网络分析

  5. 负载分析

  6. redis sdk 问题排查

  7. runtime 问题排查及优化

  8. 抓 trace ,查看调用栈

  9. 原理分析

  10. 总结


1. 背景

最近,策略组同学反馈有个服务上线后 redis 超时非常严重,严重到什么地步呢,内网读写 redis 毛刺超过 100ms! 而且不是随机出现,非常多,而且均匀,导致整个接口超时严重。因为用的 redis 库是由我们稳定性与业务中间件组维护,所以任务落我们组小伙伴头上了。


这个项目有非常复杂的业务逻辑,然后要求接口在 100 ms 左右能返回。这个服务有密集型 io(调度问题)+定时任务(cpu 问题)+常驻内存 cache(gc 问题)。频繁访问 redis,在定时逻辑中,业务逻辑一个 request 可能达到上千次 redis Hmget/Get (先不讨论合理性)。 复杂业务给问题排查带来比较多干扰因素,这些因素都可能导致抖动。


go version : 1.8,机器是 8 核+16G 容器,没有开 runtime 监控,redis 的同事初步反馈没有 slowlog。因为 rd 也追了很久,到我们这边来的时候,redis 的超时指标监控已经给我们加了,每个 key 的查询都会打印日志方便 debug。


redis get 接口的耗时监控显示如下,因为高频请求,大部分耗时是小于 10ms 的,但是这毛刺看着非常严重,是不可忍受了。



系统 cpu 问题比较严重,抖动非常大,内存并没有太大问题,但是占用有点大。因为用了 local-cache,也可能引起 gc 问题。




因为没有加 runtime 监控,其他信息暂不可知。

2. 解决思路

因为追查接口毛刺比较复杂,我们的原则是不影响业务的情况下,尽量少上线的将业务问题解决。


第一、首先排查是不是网络问题,查一段时间的 redis slowlog(slowlog 最直接简单);


第二、 本地抓包,看日志中 redis 的 get key 网络耗时跟日志的时间是否对的上;


第三、查机器负载,是否对的上毛刺时间(弹性云机器,宿主机情况比较复杂);


第四、查 redis sdk,这库我们维护的,看源码,看实时栈,看是否有阻塞(sdk 用了 pool,pool 逻辑是否可能造成阻塞);


第五、查看 runtime 监控,看是否有协程暴增,看 gc stw 时间是否影响 redis(go 版本有点低,同时内存占用大);


第六、抓 trace ,看调度时间和调度时机是否有问题(并发协程数,GOMAXPROCS cpu 负载都会影响调度);


整个分析下来,只能用排除法了。

3. 现场分析

网络分析

因为服务的并发量比较大,其实查起来非常耗时。


  1. redis slowlog 经查是正常的,显示没有超过 10ms 的 get 请求。

  2. 我们该抓包了,将日志里的 key 跟 tcpdump 的 key 对起来。因为并发量非常大,tcpdump 出来的数据简直没法看,这里我们线上 dump,线下分析。 tcpdump 抓了几分钟时间。grep 日志里超时的 case, 例如 key 是 rec_useraction_bg_2_user_319607672835 这个 key 显示 126ms, 但是 wireshark 显示仅仅不到 2ms。




分析了其他的 key,得到的结论都类似,redis 返回非常快,根本没什么问题。

负载分析

我们是有 cpu 监控的,可惜的是,是宿主机的 cpu 监控。这里 cpu 占用看,因为资源隔离,宿主机没问题,但是这个进程的 cpu 抖动比较厉害,这里常用 40%,但是定时任务起起来的时候,接近全部打满!抖动是否跟定时任务有关?但看监控,其实相关性没有那么明显,redis 超时更频繁。


redis sdk 问题排查

sdk 是存在阻塞的可能的,怎么判断 sdk 是否阻塞了?两个方式,一种是源码级别追查,第二种是查看实时栈,看是否有 lock,wait 之类的逻辑。初步看并没有阻塞逻辑。dump 了下线上的栈,看起来,也没有什么问题。但日志确实显示,到底怎么回事?



这里有个认知问题,有 runtime 的语言,sdk 都是受 runtime 影响的,sdk 写的再好,并不能保证延时,比如你跑下下面这个 demo,50 个并发,你访问 redis 都各种超时。程序的 runtime 我们也需要查下。


package main

import ( "flag" "fmt" "net/http" _ "net/http/pprof" "sync" "time"

"github.com/gomodule/redigo/redis")

var redisAddr string

func main() { flag.StringVar(&redisAddr, "redis", "127.0.0.1:6379", "-redis use redis addr ") flag.Parse() go func() { http.ListenAndServe("0.0.0.0:8003", nil) }() wg := sync.WaitGroup{} wg.Add(1) for i := 0; i < 200; i++ { go go_get(wg) } wg.Wait()}

func go_get(wg sync.WaitGroup) { client, err := redis.Dial("tcp", redisAddr) if err != nil { fmt.Println("connect redis error :", err) return } defer client.Close() for { start := time.Now() client.Do("GET", "test1234") if cost := time.Now().Sub(start); cost > 10*time.Millisecond { fmt.Printf("time cost %v \n", cost) } }}
复制代码

runtime 排查及优化

抓了下线上 heap 图,查看历史的 gc stw 信息:curl http://localhost:8003/debug/pprof/heap?debug=1



上面的数据,是历史 stw 时间,没 runtime 历史监控只能看这了,数据简直没法看,上面是个 256 的数组,单位是 ns,循环写入。gc 的策略一般主动触发是 2min 一次,或者内存增长到阈值,先初步认为 2min 一个点吧。gc 得必须得优化,但是跟日志毛刺的密度比,还对不上。


查看具体的问题在哪,graph 如下:



大头在两个地方,一个是 hmget, 一个是 json。优化 gc 的思路有很多,也不复杂,有实例,参考我个人博客。最简单有效的,你先把版本升下吧,高频服务,1.8 一个定时器问题你 qps 就上不去了,示例参考


这里是业务同学升版本后的 graph:



看下 hmget 和 json encode 的区别,现在大头全在 hmget 上了,效果立杆见影。这里为啥 hmget 这么多?问了下,这是业务逻辑实现,这样做是有问题的。不能影响业务,所有暂时作罢。只升级大版本,stw 好了很多,虽然偶尔也有几十毫秒的毛刺,对比图如下(单位 ms):



可惜,问题并没有解决,redis 的抖动没有明显变化:



为什么一直追这 redis 查 ,不追着其他接口查,因为 redis 要求比较高,是 ms 级别,其他的 api 是百 ms 级别,毛刺就不明显了。


目前看 gc 问题其实还有优化空间,但是成本就高了,得源码优化,改动会比较大,业务方不能接受。而且,当前的 gc 大幅改善对接口耗时并没起到立竿见影效果,这里需从其他方面寻找问题。

抓 trace ,查看调用栈

curl http://127.0.0.1:8080/debug/pprof/trace?seconds=300 > trace.out
go tool trace trace.out
复制代码


线上 trace,记录了采样阶段 go 都在干嘛,不过因为混合业务代码,你想直接看图,基本是不可能了,打开都得等几分钟(没办法,有业务逻辑)…




单独看 gc,和 heap 之中数据基本一致。这里问题比较严重的是调度,按 scheduler wait 排序后数据如下:



影响调度的,主要是协程数量和线程数量。


加了监控后,看到其实并发的协程数量并不太多,也没出现协程暴增的情况,照理调度并不会这么严重。



线程数查看 ps -T -p pid,线程数是 200+,这个数量很奇怪,有点超出预期,8 核的容器,默认 p 是 8,线程数超过 p 数量 20 多倍。而从 trace 图上看 p 是 48,看着是 go 获取了物理机的核心数。线程数会影响调度,这个影响不好评估,但是要优化。我们建议将 GOMAXPROC 设置成 8,然后重新上线。然后抖动立刻降下来了,效果如下:



同时,pprof 显示,目前的线程数,降到了以前的 1/3。通过 perf 看线程的上下文切换也少了,同时调度本身也少了。



到此,问题基本查到根因,并解决了。

4. 原理分析

为什么获取到错误的 cpu 数,会导致业务耗时增长这么严重?主要还是对延迟要求太敏感了,然后又是高频服务,runtime 的影响被放大了。


关于怎么解决获取正确核数的问题,目前 golang 服务的解决方式主要是两个,第一是设置环境变量 GOMAXPROCS 启动,第二是显式调用 uber/automaxprocs。


golang 是如何设置 cpu 核数,并取成宿主的核数的呢,追 runtime.NumCPU() 发现,其实现细追,发现是 getproccount, 查 linux 下源码发现,其实调用的是 sched_getaffinity,直接通过系统调用获取的宿主机核数。


func getproccount() int32 {    // This buffer is huge (8 kB) but we are on the system stack    // and there should be plenty of space (64 kB).    // Also this is a leaf, so we're not holding up the memory for long.    // See golang.org/issue/11823.    // The suggested behavior here is to keep trying with ever-larger    // buffers, but we don't have a dynamic memory allocator at the    // moment, so that's a bit tricky and seems like overkill.    const maxCPUs = 64 * 1024    var buf [maxCPUs / 8]byte    r := sched_getaffinity(0, unsafe.Sizeof(buf), &buf[0])    if r < 0 {        return 1    }    n := int32(0)    for _, v := range buf[:r] {        for v != 0 {            n += int32(v & 1)            v >>= 1        }    }    if n == 0 {        n = 1    }    return n}
复制代码


uber/aotomaxprocs 是如何算正确的 cpu 核数?读取 cgroup 接口 /sys/fs/cgroup/cpu/cpu.cfs_quota_us ,然后除以 /sys/fs/cgroup/cpu/cpu.cfs_period_us 并向上取整:


// CPUQuota returns the CPU quota applied with the CPU cgroup controller.// It is a result of `cpu.cfs_quota_us / cpu.cfs_period_us`. If the value of// `cpu.cfs_quota_us` was not set (-1), the method returns `(-1, nil)`.func (cg CGroups) CPUQuota() (float64, bool, error) {    cpuCGroup, exists := cg[_cgroupSubsysCPU]    if !exists {        return -1, false, nil    }

cfsQuotaUs, err := cpuCGroup.readInt(_cgroupCPUCFSQuotaUsParam) if defined := cfsQuotaUs > 0; err != nil || !defined { return -1, defined, err }
cfsPeriodUs, err := cpuCGroup.readInt(_cgroupCPUCFSPeriodUsParam)if err != nil { return -1, false, err }
return float64(cfsQuotaUs) / float64(cfsPeriodUs), true, nil
}
复制代码


像 java ,jdk8u191 以后,都已经能自适应容器和实体服务器了,go 也应该考虑考虑自适应容器核数,毕竟是容器时代。


##5. 总结


这个问题排查优化结束了,但怎么让公司其他项目受益?


我们怎么解决其他项目线上线下容器获取错 cpu 数的问题?现在很多项目没开 runtime 监控,线上异常,怎么方便定位问题?这个项目本身还有哪些可以优化的?等等。


第一个问题,我们在镜像里注入环境变量 GOMAXPROCS,并算出 cgroup 限制的核数。如果大家有需求自定义 p 的数量,可以自己显式调用 https://github.com/uber-go/automaxprocs 。镜像注入环境变量,相当于无感帮大家解决了获取错 cpu 的问题。


第二个问题,给我们部门 go 项目加上 runtime 监控,定时上报 runtime 信息同时提供采集 pprof 能力。


第三个问题,优化 redis sdk hmget 函数,减少对象分配,业务流程也使用 pipeline 模式,合并 io。


作者介绍


万鼎铭


滴滴 | 高级软件开发工程师


本文转载自公众号滴滴技术(ID:didi_tech)。


原文链接


https://mp.weixin.qq.com/s?__biz=MzI1NDA3NzY4NA==&mid=2247486007&idx=2&sn=b5d177963dfa208aea834545d76ea230&chksm=e9cbf5acdebc7cbab0b795002d412c48e80b251fc390bc761260caeddefce384d73637256991&scene=27#wechat_redirect


2020-01-10 09:304326

评论

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

网络安全kali web安全 Kali之msf简单的漏洞利用

学神来啦

网络安全 渗透测试 WEB安全 kali kali Linux

教你如何搭建一个骗子举报/信息查询的平台

H

搭建平台 网络安全信息安全、

还在用递归,试试迭代吧

爱笑的小雨

Python 学习路线(2022)

AlwaysBeta

Python django 编程语言 学习路线 编程入门

猿桌派第 2 季回归,报名赢现场录制机会!

融云 RongCloud

程序员

数字孪生:如何撑起一个万亿市场的产业变革?

知心宝贝

行业资讯 数字孪生 冬奥 3月月更

2022官方文档部署MAVEN最新最全

北极的大企鹅

中间件 环境安装 部署与维护

乘数智之风,为世界造舟筏:女性在当下如何创造?

脑极体

gRPC 简介实践

yuexin_tech

gRPC

融云通信周边能力上新啦!一键 Get 美颜、CDN 服务

融云 RongCloud

CDN 人脸识别

好书推荐 ——《噪声:人类判断的缺陷》

天择

好书推荐 认知偏差 噪声 3月月更

商品库存管理和秒杀系统设计(19/100)

hackstoic

技术方案 互联网应用技术方案

专注自主研发,加速大数据基础软件国产化进程

星环科技

数据库 大数据 基础软件

把家电科技产出摆出来!三家实力一目了然

脑极体

Go语言实战之切片的内部实现和基础功能

山河已无恙

Go 语言 3月月更

VuePress 博客优化之开启 Algolia 全文搜索

冴羽

Vue 搜索 vuepress 博客搭建 Algolia

模块 9 作业(毕业设计)

miliving

python方法——defaultdict详解

Wjq

Python 字典 3月程序媛福利 3月月更 defaultdict

同人于野,平常无边 | 对话 StarRocks 的三位女性工程师

StarRocks

数据工程师 38妇女节

Committer 蔡正昕专访:勇敢迈出第一步,做开源没有那么难

Apache Pulsar

架构 云原生 中间件 Apache Pulsar 开源社区

2021年第4季度规模达1381.8亿元!跨境电商结合酒店场景将成亮点

易观分析

跨境电商

模块九

撿破爛ぃ

架构训练营

华为被卡脖子,到底卡的是什么?

坚果

英特尔Sierra Forest,市场最需要的能效核至强处理器

科技新消息

图文详解:Kafka到底有哪些秘密让我对它情有独钟呢?

浅羽技术

超级群、群组、聊天室,IM 产品的场景化「特异功能」

融云 RongCloud

即时通讯 IM

活动预告 | ArchSummit全球架构师峰会

第四范式开发者社区

人工智能 机器学习 数据库 架构师 热门活动

Flutter 构建常见的App页面框架

岛上码农

flutter ios 安卓 移动端开发 3月月更

安全实战:webshell的几种免杀方式

网络安全学海

黑客 网络安全 信息安全 渗透测试 安全漏洞

Discuz! ML远程代码执行(CVE-2019-13956)

喀拉峻

网络安全

基于区块链技术的超级账本(Hyperledger) - 从理论到实战

Jerry Wang

区块链 智能合约 云平台 Go 语言 2月月更

高频 golang 服务接口超时排查以及性能调优_容器_万鼎铭_InfoQ精选文章