公司行情推送采用golang开发,推送部分的代码历史包袱很重,重写要花比较大的精力,所以一直处于有bug就修的阶段,前段时间出现了一次生产不推送的问题,经过排查最后确定问题在锁等待,导致整个推送服务都处于等锁的阶段。
从app上看到的现象是偶尔不推送,我们推送节点有高可用,出现偶尔不推送肯定是流量打到一些节点上,节点有问题所以就不推送。
首先查看grafana监控,发现有个推送节点cpu使用非常低,我们行情数据来源与kafka,单纯消费kafka都会造成很高的cpu消耗,所以就断定这个节点有问题。
接下来看监控看该节点是否有收到订阅请求,在某一个时刻之后,就失去订阅了,也就是说这个节点当前处于既未消费kafka行情,也未收到订阅行情的请求。
有了这两个信息之后,基本判定是要么内部卡住,死锁了,要么就是网络都断了,于是找运维抓包,抓包之后,发现给推送节点响应给另外一个服务的tcp数据窗口大小都为0
tcp窗口为0,要么网络异常,要么就是接收端卡住了,一直没有取底层tcp数据,导致tcp的接收窗口被塞满。服务都在一个内网当中,而且连接也没有断,网络异常几率比较低。
以上两个信息都指向一个共同的问题——程序卡住了,没有取数据,也没有消费行情。
有了这个判断之后,接下来就是把当前goroutine信息打印出来,我们服务均采用docker容器进行部署,容器通信方式采用端口映射,但是我们的pprof端口并没有对外暴露。于是只能在宿主机上通过curl命令获取goroutine信息。
curl "http:/172.17.0.3:9090/debug/pprof/goroutine?debug=2" > pprof.txt
goroutine信息获取完之后,发现很多处于semacquire状态
goroutine 117 [semacquire, 6274 minutes]:
sync.runtime_SemacquireMutex(0xc000771904, 0x0)
/usr/local/go/src/runtime/sema.go:71 +0x3d
sync.(*Mutex).Lock(0xc000771900)
/usr/local/go/src/sync/mutex.go:134 +0x109
goroutine 131 [semacquire, 6776 minutes]:
sync.runtime_SemacquireMutex(0xc000771904, 0xc00dd84d00)
/usr/local/go/src/runtime/sema.go:71 +0x3d
sync.(*Mutex).Lock(0xc000771900)
/usr/local/go/src/sync/mutex.go:134 +0x109
并且等待很久了,并且这些都是试图对**同一个锁Lock造成的,**一开始判断是不是锁循环等待了,但是并没有发现其他锁等待,所以基本上可以断定是有个锁未释放,问题确定了之后,有了方向去排查,很快就定位到代码了。确实是有个锁可能会造成未解锁的情况,但是出现的条件极为苛刻,所以有时候可能跑了很久都没出现。我试图重现这个bug,发现并不容易
在排查过程中间有个插曲,我们有两个机房,有个还处于灰度阶段,用户量比较少,突然有一天同事说完全不推送了,这个就有点慌了,不可能完全不推送啊,我拿客户端订阅行情,发现没问题啊,正常在推。
第二天上班再确认下,不是偶尔不推送,是完全不推送,然后上机器查日志,在灰度的机房根本就没有这个用户的日志,也就是用户根本就没有在这台机器上线,那就是说获取的接入层网关的地址有误?我们内部有个负载均衡的节点,通过这个负载均衡器找到一个相对较优的接入节点,于是我手动获取,拉到的了两个地址,发现是已经被运维下线的aliyun节点,但是按道理讲下线之后,健康检查失败应该会删除这两个节点的,于是发现这个负载均衡有个bug。
这个bug被其他同事修复之后,我又想了下发现还是不没有解决偶尔不推送的情况,于是继续排查,找运维要了1天k8s容器访问的权限,然后进行排查。反复测试发现,有的订阅似乎在推送系统中间环节就断了,并没有真正订阅到推送的节点上,每次出问题,都是没有在推送节点上找到推送消息,这时候又开始怀疑网络是不是有问题,于是使用tcpdump抓包,发现正常啊,有数据的,我试图找到对应的推送节点的ip,发现有两个ip并不是推送节点的,并且能通过健康检查然后用netstat查看网络连接状况,发现确实多出了两个ip的连接,于是找运维求解,最后排查发现,多出了两个推送节点,而且是几个月前的版本了,这期间我们进行过行情topic的调整,老的推送节点使用老的topic消费kafka根本消费不出来行情,自然就不推送了。
当然这是一个插曲,除了发现负载均衡的一个bug之外,并没有解决任何问题。