最近同事有个变更上线,暂且称为服务B,它的上游是服务A。服务B是thrift server,上线后,服务A报EOF
错误。据此怀疑以下几点:
- C/S端协议不一致。CR 代码,发现 S 端协议中增加了新的 required 字段。改为 optional 重新上线,还是有 EOF 错误。如果协议错误,应该每个请求都失败,但发现只是部分失败,应该还有其他问题。(注:有文章说协议不一致会出现EOF,不过我用go测试了下,如果S端req增加 required 字段,s端会提示
error processing request: Not enought frame size
,s端的新字段是个随机值,c端一切正常。如果C端req增加 required 字段,S端收不到请求,C端提示 read io timeout) - S 端关闭连接后,C 端继续 read,就会读到 EOF。服务B在什么情况下会主动关闭连接呢,检查了超时,没问题。检查了进程启动时间,并没有挂掉重启。结合 thrift 源码,剩下一种可能,就是 B 在处理某个请求的时候 panic 了,但整个 server 并未 panic,这样才会出现部分失败。检查 B 的handler,看到一开始便有一个 defer func,主要是为了打印请求和响应,并接住该协程栈下层的 panic。但是写法有点问题,大致如下:
defer func() {
breq, _ := json.Marshal(*request)
bresp, _ := json.Marshal(*response)
logger.Info("inout||req=%+v||resp=%+v||trace_id=%v", string(req), string(resp), traceId)
if err := recover(); err != nil {
// log
}
}()
看日志,并未发现有panic记录。这里看到有指针取对象的操作,这个操作有风险,就是指针为空时就会panic。request可以保证不空,但response是后面的操作生成的,是否为空不敢保证。因此怀疑点落在这里。加日志上线,发现确有为空的情况。其实 go 里面的 Marshal 参数用指针就可以,没必要取对象,去掉取对象操作再上线,问题得到了修复。
这里的panic,是怎么导致连接关闭但server保活的呢?看一下这几段代码:
Server 的 AcceptLoop中,会为新建连接单独开协程处理
go func() {
if err := p.processRequests(client); err != nil {
log.Println("error processing request:", err)
}
}()
processRequests 函数中,最终调用 handler 函数。后者 panic,会往上抛,直到这里的 Process()。此处遇到panic会return,在此之前先依次退出defer栈函数并执行。可以看到,这里实际是把 transport 也就是 socket 关闭了。由于栈顶有 recover 接住 panic,因此 server 不会挂掉,只会影响当前的连接。服务A就会收到 EOF。
defer func() {
if e := recover(); e != nil {
log.Printf("panic in processor: %s: %s", e, debug.Stack())
}
}()
if inputTransport != nil {
defer inputTransport.Close()
}
if outputTransport != nil {
defer outputTransport.Close()
}
for {
ok, err := processor.Process(inputProtocol, outputProtocol)
//...
}
此外用 go 中的 tcp 编程也很好验证, server 收到请求后直接 conn.Close()
,client 就会读到 EOF
了,在此不赘述。