Open dduo518 opened 3 years ago
昨天突然接到运维同事注意到测试环境有几个服务的CPU出现爆炸增长,更有甚至是刚重启直接吃掉快3个核的CPU资源
回想了下最近没有加入什么新的功能,刚好这个版本重构了TCP服务模块,但是这几个服务又并没有用到TCP模块 这个时候能向到的是不是某个地方的代码出现了死循环导致,认真看代码也没有找到哪个地方有死循环出现,注意观察了一段时间重启之后马上涨上去了。
将启动模块加入以下代码
import ( _ "net/http/pprof" "net/http" ) func createPProfServer(){ if err:=http.ListenAndServe("0.0.0.0:6060", nil);err!=nil{ panic(err) } }
重新打包镜像,重新部署服务,暴露6060端口。
接下来就是在本地利用工具加载CPUprofile
go tool pprof http://xxxx:6060/debug/pprof/profile?seconds=30
加载到本地,利用工具将CPU火焰图输出,需要安装工具graphviz(按照google方法进行安装,不然中间会报错dot 非可用命令)
graphviz
go tool pprof -http=:8081 xxxxx.out
然后跳出 localhost:8081/ui界面 点击生成火焰图,一眼就看出来吃CPU的地方
这个地方是本次版本接入TCP端口健康检查的新模块,定位到问题 将测试环境K8S的端口健康检查先关闭,暂时性的解决问题。
暂时性的解决问题
k8s集群的负载均衡会对应用的生存状态进行检测,如果是web服务,会对某个http端口指定的接口进行定期的请求,检测服务是否正常运行,如果检测到不能正常的返回200状态码,那么会判断服务异常,然后会对服务进行重新调度. 如果是TCP服务那么就是对TCP的端口进行telnet,主要是监控服务的IP地址网络可达性、端口可用性、延时等指标,但是正常的telnet会对服务造成影响,为了防止健康检查对我们应用的原来的业务端口造成影响,需要开启tcp专用的端口。
tip:简单粗暴的方式来逃避健康检查好像不太对,但是由于端口对一对连接,具有竞争性登录的端口只能另开端口
var Discard io.Writer = discard{} type discard struct{} func (d discard) Write(p []byte) (int, error) { return len(p), nil } func Health(ctx context.Context) error { port := 9000 tAddr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("0.0.0.0:%d", port)) if err != nil { return err } ln, err := net.ListenTCP("tcp", tAddr) if err != nil { return err } go func() { defer ln.Close() for { select { case <-ctx.Done(): return default: } conn, err := ln.AcceptTCP() if err != nil { continue } go onConn(ctx, conn) } }() return nil } // 这个方法造成了CPU的爆涨 func onConn(ctx context.Context, conn *net.TCPConn) { defer conn.Close() for { select { case <-ctx.Done(): return default: } _, _ = io.Copy(Discard, conn) continue } }
其实在开始写代码的时候是没有想到为什么端代码造成吃CPU的,当拉出火焰图的时候想了一下马上就想明白了,当从conn中读取数据的时候,如果没有数据会一直for循环阻塞读取数据,然后导致连接不释放
写一段代码进行测试,每秒钟不断的对端口进行连接,然后进行关闭
func healthCheck() { for { conn, _ := net.Dial("tcp", "127.0.0.1:9000") time.Sleep(time.Second) _ = conn.Close() } }
循环执行一段时间之后,执行脚本
netstat -an |findstr "9000"
有大量的CLOSE-WAIT跟FIN_WAIT_2状态 CLOSE-WAIT状态是客户端发送关闭,服务端收到关闭报文,第一次返回关闭报文ack之后, FIN_WAIT_2状态是客户端接收到关闭报文的ack等待第二次释放连接报文,但是此时服务端连接被读取消息阻塞了,不断的新创建协程死循环调用io.Copy。
CLOSE-WAIT
FIN_WAIT_2
io.Copy
其实在这个bug的出现是很不应该的,想当然的不断阻塞住读取消息,修复办法就是将continue改成return,然后调用close函数
continue
return
close
定位CPU爆炸增长
起因
昨天突然接到运维同事注意到测试环境有几个服务的CPU出现爆炸增长,更有甚至是刚重启直接吃掉快3个核的CPU资源
回想了下最近没有加入什么新的功能,刚好这个版本重构了TCP服务模块,但是这几个服务又并没有用到TCP模块 这个时候能向到的是不是某个地方的代码出现了死循环导致,认真看代码也没有找到哪个地方有死循环出现,注意观察了一段时间重启之后马上涨上去了。
不要谎,上pprof
将启动模块加入以下代码
重新打包镜像,重新部署服务,暴露6060端口。
生成火焰图,定位问题
接下来就是在本地利用工具加载CPUprofile
加载到本地,利用工具将CPU火焰图输出,需要安装工具
graphviz
(按照google方法进行安装,不然中间会报错dot 非可用命令)然后跳出 localhost:8081/ui界面 点击生成火焰图,一眼就看出来吃CPU的地方
这个地方是本次版本接入TCP端口健康检查的新模块,定位到问题 将测试环境K8S的端口健康检查先关闭,
暂时性的解决问题
。分析原因
tcp端口健康检查
k8s集群的负载均衡会对应用的生存状态进行检测,如果是web服务,会对某个http端口指定的接口进行定期的请求,检测服务是否正常运行,如果检测到不能正常的返回200状态码,那么会判断服务异常,然后会对服务进行重新调度. 如果是TCP服务那么就是对TCP的端口进行telnet,主要是监控服务的IP地址网络可达性、端口可用性、延时等指标,但是正常的telnet会对服务造成影响,为了防止健康检查对我们应用的原来的业务端口造成影响,需要开启tcp专用的端口。
研究代码
为什么会暴涨
其实在开始写代码的时候是没有想到为什么端代码造成吃CPU的,当拉出火焰图的时候想了一下马上就想明白了,当从conn中读取数据的时候,如果没有数据会一直for循环阻塞读取数据,然后导致连接不释放
测试
写一段代码进行测试,每秒钟不断的对端口进行连接,然后进行关闭
循环执行一段时间之后,执行脚本
有大量的
CLOSE-WAIT
跟FIN_WAIT_2
状态CLOSE-WAIT
状态是客户端发送关闭,服务端收到关闭报文,第一次返回关闭报文ack之后,FIN_WAIT_2
状态是客户端接收到关闭报文的ack等待第二次释放连接报文,但是此时服务端连接被读取消息阻塞了,不断的新创建协程死循环调用io.Copy
。修复 fix
其实在这个bug的出现是很不应该的,想当然的不断阻塞住读取消息,修复办法就是将
continue
改成return
,然后调用close
函数