dduo518 / hexo-blog

hexo静态blog点击 https://github.com/chong0808/hexo-blog/issues
3 stars 0 forks source link

定位CPU爆炸增长 #46

Open dduo518 opened 3 years ago

dduo518 commented 3 years ago

定位CPU爆炸增长

起因

昨天突然接到运维同事注意到测试环境有几个服务的CPU出现爆炸增长,更有甚至是刚重启直接吃掉快3个核的CPU资源 2021-06-22_01 2021-06-22_02 2021-06-22_03

  回想了下最近没有加入什么新的功能,刚好这个版本重构了TCP服务模块,但是这几个服务又并没有用到TCP模块   这个时候能向到的是不是某个地方的代码出现了死循环导致,认真看代码也没有找到哪个地方有死循环出现,注意观察了一段时间重启之后马上涨上去了。

不要谎,上pprof

将启动模块加入以下代码

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 非可用命令)

go tool pprof -http=:8081 xxxxx.out

然后跳出 localhost:8081/ui界面 点击生成火焰图,一眼就看出来吃CPU的地方 2021-06-22_04

这个地方是本次版本接入TCP端口健康检查的新模块,定位到问题 将测试环境K8S的端口健康检查先关闭,暂时性的解决问题

分析原因

tcp端口健康检查

  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-WAITFIN_WAIT_2状态 2021-06-22_05 2021-06-22_06 CLOSE-WAIT状态是客户端发送关闭,服务端收到关闭报文,第一次返回关闭报文ack之后, FIN_WAIT_2状态是客户端接收到关闭报文的ack等待第二次释放连接报文,但是此时服务端连接被读取消息阻塞了,不断的新创建协程死循环调用io.Copy

修复 fix

其实在这个bug的出现是很不应该的,想当然的不断阻塞住读取消息,修复办法就是将continue改成return,然后调用close函数