a.在import中引入 _ "net/http/pprof"
b.在任意处开启9099端口
go func() {
log.Info(http.ListenAndServe("localhost:9099", nil))
}()
a.暴露的节点如下:
go tool pprof http://127.0.0.1:9099
• /debug/pprof/profile:访问这个链接会自动进行 CPU profiling,持续 30s,并生成一个文件供下载
• /debug/pprof/heap:Memory Profiling 的路径,访问这个链接会得到一个内存 Profiling 结果的文件
• /debug/pprof/block:block Profiling 的路径
• /debug/pprof/goroutines:运行的 goroutines 列表,以及调用关系
b.如果需要分析cpu耗时,则在控制台执行:
go tool pprof http://127.0.0.1:9099/debug/pprof/profile
如果需要分析堆的话,则在控制台执行:
go tool pprof http://127.0.0.1:9099/debug/pprof/heap
即在地址后面拼上需要分析的路径
func init() {
rootCmd.AddCommand(serverCmd)
go func() {
log.Info(http.ListenAndServe("localhost:9099", nil))
}()
}
go tool pprof http://127.0.0.1:9099/debug/pprof/profile
出现(pprof),则表示分析完毕,可以进行操作 :
输入 top100 ,查看排名前面100的函数名:
输入tree 查看调用链,可以看到函数之间的串联:
上面的命令不太直观时,就导出svg图进行过查看,导出需要依赖graphviz,用yum安装即可:
yum install graphviz
安装完graphviz后 ,再执行:
go tool pprof http://127.0.0.1:9099/debug/pprof/profile
待程序分析完后,输入svg:
在当前目录下就会生成svg文件,scp拷贝到本地机器上:
scp profile001.svg admin@10.60.100.139:/Users/admin/Desktop
在本地电脑上直接打开svg文件,即可显示出调用链图:
火焰图可以使用gops,这里主要用调用链图。
三、优化措施
查看调用链图看谁最大最红,百分比占最高就行: 比如看mallocgc,占了13%.85的性能,这里是占进程cpu的总百分比。malloc函数是内存分配地址的系统函数。如果是逻辑问题的话,很容易看出来。
观察在导出的调用链图,看出其中占比比较大的函数。
这个是send函数的主逻辑。 系统是用kafka发送3份重要的数据日志,用来做广告数据分析,此处的send如果不采用批量发送的话,频次会是流量的3-5倍,这个量是极为巨大的。
到在发送消息时调用,未优化前,占比达到21.85%不能接受。根据业务的商量,将kafka client采用批量发送的模式。kafka默认配置是不批量发送,不符合性能要求,将它改为每500ms批量发送1次,降低send函数的调用次数:
config.Producer.Flush.Frequency = 500 * time.Millisecond
在初始化kafka的sender时,添加上Frequency配置,示例:
// 初始化sender
func initSender(config *sarama.Config, broker string, topic string) (pp *producerProxy, err error) {
config.Producer.Return.Successes = viper.GetBool("kafka.producer.retry.enable")
config.Producer.Retry.Max = viper.GetInt("kafka.producer.retry.max")
config.Producer.Timeout = viper.GetDuration("kafka.producer.timeout") * time.Millisecond
// Flush batches every 500ms
config.Producer.Flush.Frequency = viper.GetDuration("kafka.producer.flush.frequency") * time.Millisecond
return newProducerProxy(config, strings.Split(broker, ","), topic)
}
优化后:
函数占用48.64% ,系统是采用普罗米修斯做打点监控,统计qps和latency等指标,其中summary是统计latency的百分比函数。
这个asyncFlush函数是summary在本机进行指标合并,500个指标,就需要释放一次。其中的merge函数使用了大量的stream、slice copy 、move操作、以及双层for循环。
func (s *stream) merge(samples Samples) {
// TODO(beorn7): This tries to merge not only individual samples, but
// whole summaries. The paper doesn't mention merging summaries at
// all. Unittests show that the merging is inaccurate. Find out how to
// do merges properly.
var r float64
i := 0
for _, sample := range samples {
for ; i < len(s.l); i++ {
c := s.l[i]
if c.Value > sample.Value {
// Insert at position i.
s.l = append(s.l, Sample{})
copy(s.l[i+1:], s.l[i:])
s.l[i] = Sample{
sample.Value,
sample.Width,
math.Max(sample.Delta, math.Floor(s.ƒ(s, r))-1),
// TODO(beorn7): How to calculate delta correctly?
}
i++
goto inserted
}
r += c.Width
}
s.l = append(s.l, Sample{sample.Value, sample.Width, 0})
i++
inserted:
s.n += sample.Width
r += sample.Width
}
s.compress()
}
...
func (s *stream) compress() {
if len(s.l) < 2 {
return
}
x := s.l[len(s.l)-1]
xi := len(s.l) - 1
r := s.n - 1 - x.Width
for i := len(s.l) - 2; i >= 0; i-- {
c := s.l[i]
if c.Width+x.Width+x.Delta <= s.ƒ(s, r) {
x.Width += c.Width
s.l[xi] = x
// Remove element at i.
copy(s.l[i:], s.l[i+1:])
s.l = s.l[:len(s.l)-1]
xi -= 1
} else {
x = c
xi = i
}
r -= c.Width
}
}
问题就在这个merge和compress函数,是极其地消耗性能和内存。网上也有人在说summary这种百分位的计算上是比较耗客户端性能的。且在流量越高的情况下,时间复杂度是非线性增长。
这个没有太好的办法,只好采用试错法定位到具体的指标上,降低其调用次数。直接说结果,1. latency只保留1个p98 2.关闭最经常打点的指标,做了阿波罗开关,需要用来分析的时候开启。
示例:
/**
* 监控dsp的响应时长
*/
DspProcessTime = prometheus.NewSummaryVec(prometheus.SummaryOpts{
Name: "dsp_latency",
Help: "dsp latency",
Objectives: map[float64]float64{0.98: 0.001},
}, []string{"channel_name"})
adx有10个stage,此处消耗的cpu比较大,做阿波罗开关。
/**
* stage耗时:比较耗费性能,由apollo控制开关
*/
func AddStageLatency(stageName string, latency int64) {
if apollo.GetAdxPrometheusBool("stage_latency_enable", false) {
StageProcessTime.WithLabelValues(stageName).Observe(float64(latency))
}
}
处理后见不着普罗米修斯了,cpu使用下降一半,效果比较明显:
gc和gmp调优: 函数是gmp用来寻找可用的grouptine,这里占15%可以优化下,这里应该都会遇到。
先想到的就是gc和process的不合理使用。
在启动命令前加上 GODEBUG=gctrace=1可以查看系统golang进程的gc日志,可以看到默认是100的情况下1秒内多次gc,并且耗费6%的cpu用于gc操作,不可接受,go默认值的GOGC=100的意思是当内存增长1倍时执行gc,高并发项目很容易达到。设置GOGC=2000,可以降低gc频次和cpu消耗:
GODEBUG=gctrace=1 GOGC=2000 GOTRACEBACK= ./caifeng-adx server --config /home/services/caifeng-adx/bin/prod/application.properties > /dev/null &
优化后的gc:
gc占cpu降为0%,且频次为2-3秒1次,性能达到最佳。
就是内存从443mb升为1.3G(不会增长),牺牲些空间换取时间。
将执行的process数量置为容器的cpu核数,process数量有限,改为采用配置文件设置process数量,注意此处,golang默认是runtime.GOMAXPROCS(runtime.NumCPU()),因为进程部署到k8s容器内,runtime.NumCPU()获取的是物理机的cpu,物理机的cpu核数是大于pod的cpu核数:
// 设置cpu个线程去运行所有的协程,提高在docker内的性能,配置在文件里,7核心
cpuNum := viper.GetInt("instance.cpu")
runtime.GOMAXPROCS(cpuNum)
优化后的findrunnable函数降为:
可以了,gmp调度本身也需要耗费cpu。
ants协程池在logrr和call dsp:由于协程占用的资源极小,使用协程池来复用协程无明显效果
fasthttp在call dsp: 无明显效果,还略有下降
协程池和http都用回原生,原生的效率在高并发情形下最高且经济。
目前除call dsp、pb proto、json marshal、codis必需外,无较大耗时的函数。adx的qps从2k升3.8k,提升85%,挺明显的。
golang函数 | 处理方式 |
---|---|
kafka: saram.withRecover | 设置每500ms批量发送 |
promethes summary asyncFlush | 只保留p98,且去除大的打点 |
runtime.findrunnable | 设置GOGC=2000 runtime.GOMAXPROCS(cpuNum) |
json.marshal | 设置为全局变量,设置阿波罗监听改变key值,降低由序列化的损耗 |
centos默认的ip端口数是28231,并不是很大,容易堆满。ssp 当qps达到96k时,出现timewait数堆积,数目过大,会造成go http连接池创建不了连接,go协程出现泄漏。
pod默认端口数:
查看timewait数:
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
timewait出现34922,过大,已超过端口数:
go协程池出现突展,程序宕机:
timewait是tcp/ip的客户端主端关闭连接造成,继而排查程序中使用http的地方,在去除反作弊之后,timewait数迅速下降到588,得出结论由反作弊的context超时关闭了客户端的http连接,出现timewait。
定位问题除主要问题是反作弊超时,反作弊系统需要扩容外,星河ssp的pod的连接没有回收也是主要原因。
治理措施:将pod和物理机都设置开头的优化参数后:
timewait固定在1万以下,不再出现连接打满的情况。目前 在一天星河流量最高峰时 115K不再宕机,协程稳定,效果比较明显。上线后timewait问题解决,然后处理超时的依赖程序。
治理措施,将物理机和pod容器的参数设置为下列,其中包括端口数调大、timewait、keepalive参数调整等,其中最主要的是timewait的回收、重用、超时、最大数量限制:
# ip port调大
net.ipv4.ip_local_port_range='1024 65535'
# 连接复用
net.ipv4.tcp_tw_reuse='1'
# 连接回收
net.ipv4.tcp_tw_recycle='1'
# fin超时控制
net.ipv4.tcp_fin_timeout='10'
# tcp keep alive
net.ipv4.tcp_keepalive_time='1800'
# 探测包的发送次数
net.ipv4.tcp_keepalive_probes='3'
# keepalive探测包的发送间隔
net.ipv4.tcp_keepalive_intvl=15
# 网络设备接收数据包的速率比内核处理这些包的速率快时,允许送到队列的数据包的最大数目
net.core.netdev_max_backlog='262144'
# time wait最大数量
net.ipv4.tcp_max_tw_buckets='10000'
# sync cookie
net.ipv4.tcp_syncookies='1'
# 表示SYN队列长度,默认1024,改成8192,可以容纳更多等待连接的网络连接数。
net.ipv4.tcp_max_syn_backlog='8192'
net.ipv4.tcp_synack_retries='2'
net.ipv4.tcp_syn_retries='1'
文章来自一点资讯商业化产品技术团队