滴滴Go实战:高频服务接口超时排查&性能调优
桔妹导读:业务中超时抖动是大家平时比较容易遇到的一种技术问题,本文详细记录了一次线上容器中高频 go 服务超时的排查过程。本文可以给大家提供查服务业务超时问题的一些思路,理解为什么 go 服务会获取错 cpu 核数,了解获取宿主 cpu 核数会有多大影响并怎样最小成本避开。
0.
1.
2.
第一、首先排查是不是网络问题,查一段时间的 redis slowlog(slowlog 最直接简单);
第二、 本地抓包,看日志中 redis 的 get key 网络耗时跟日志的时间是否对的上;
第三、查机器负载,是否对的上毛刺时间(弹性云机器,宿主机情况比较复杂);
第四、查 redis sdk,这库我们维护的,看源码,看实时栈,看是否有阻塞(sdk 用了pool,pool 逻辑是否可能造成阻塞);
第五、查看 runtime 监控,看是否有协程暴增,看 gc stw 时间是否影响 redis(go 版本有点低,同时内存占用大);
第六、抓 trace ,看调度时间和调度时机是否有问题(并发协程数,GOMAXPROCS cpu负载都会影响调度);
整个分析下来,只能用排除法了。
3.
▍网络分析
▍负载分析
我们是有 cpu 监控的,可惜的是,是宿主机的 cpu 监控。这里 cpu 占用看,因为资源隔离,宿主机没问题,但是这个进程的 cpu 抖动比较厉害,这里常用40%,但是定时任务起起来的时候,接近全部打满!抖动是否跟定时任务有关?但看监控,其实相关性没有那么明显,redis 超时更频繁。
▍redis sdk 问题排查
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 排查及优化
▍抓trace ,查看调用栈
curl http://127.0.0.1:8080/debug/pprof/trace?seconds=300 > trace.out
go tool trace trace.out
4.
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
}
// 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
}
5.
推荐阅读
喜欢本文的朋友,欢迎关注“Go语言中文网”: