起:timed out waiting for server handshake
我们组的服务都是同时提供 GRPC 和 HTTP 接口的,其中大部分 HTTP 接口都是直接通过 grpc-gateway 从 GRPC 转换而来的,但是,突然有一天,在我更新了 grpc 版本之后,出现问题了,访问 HTTP 接口报错了:
[root@liqiang.io]# GET http://192.168.67.41/api/v3/metrics:query?query=host_cpu_usage_overall%5B2h%5D
HTTP/1.1 503 Service Unavailable
Server: nginx/1.10.2
Date: Wed, 10 Apr 2019 11:51:45 GMT
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
Trailer: Grpc-Trailer-Content-Type
{
"error": "all SubConns are in TransientFailure, latest connection error: timed out waiting for server handshake",
"code": 14,
"details": []
}
承:Add protocol handshake to ‘READY’ connectivity requirements
根据问题的描述,很快我们就定位到是 GRPC 升级导致的问题,但是为什么会出现问题还未知。但是,从日志中可以看到是 handshake 出现了问题,于是,就先看下 grpc-go 的 release note,看下和 handshake 相关的修改有哪些,很快在 release note v1.18.0 中就可以发现是这一条:
client: make handshake required ‘on’ by default, not ‘hybrid’ (#2565)
See issue #2406 for more information
然后看一下具体的 issue 描述以及 pr 修改,可以总结出问题(#2406)是这个:
Go 的 http2 实现会区分 “connection ready” 和 “connection successful” ,这和其他的实现不吻合,从而会使其他的客户端通信异常。并且就 Go 的 Grpc 客户端来说,也不是严谨地遵循这两个元语,在实际使用中,可能连接完成之后,加密通道还没有建立起来就进行通信了,实际上这存在一些例如类似于 DOS 攻击之类的隐患。
因此,从 1.18 开始就默认等待连接加密完成之后再进行通信。并且在 1.19 版本之后取消非加密通道的通信,这就是我这里发生这个问题的根因。因为版本时间比较紧急,所以我就看到一个简单的处理方案,设置 GRPC_GO_REQUIRE_HANDSHAKE 环境变量为 off:GRPC_GO_REQUIRE_HANDSHAKE=off
,但是,因为觉得这种方式比较恶心,所以就回滚 grpc 版本先了。
这件事情也就先这样过去了。
转:timed out waiting for server handshake again
最近,因为尝试将依赖管理工具从 dep 转到 go module,问题又来了,于是 go module 对于版本管理只有最低版本,没有最高和指定版本,所以 grpc 版本又被升级上去了,一下子到了 v1.33.0,那么我以为旧方法依旧可行,但是被啪啪打脸了,我遇到了:
[root@liqiang.io]# tail log
panic: rpc error: code = Unavailable desc = timed out waiting for server handshake
goroutine 1 [running]:
main.main()
/gopath/src/github.smartx.com/xxxxx/xxxxx/cmd/client/main.go:24 +0x245
我以为加个环境变量或者设置一下 Client 的 Options 就可以了,然后事情还是发生了。所以又得回去认真得看一遍之前 issue(还好我们做好了 case 的记录),然后我发现之前太着急,傻叉地错过了很重要的一段话:
During development for the 1.19 release, support for changing this behavior via the environment variable will be removed entirely. Also, the grpc.WithWaitForHandshake() DialOption (was “experimental”; now “deprecated”) will be removed.
Users impacted: as far as we are aware, the only usage that may be impacted by the new behavior is cmux. cmux has a workaround for Java using MatchWithWriters to allow it to continue working in the face of this behavior.
这里就直接了当得说了两个重要的点:
- 1.19 之后,无论是环境变量还是
grpc.WithWaitForHandshake()
选项都无效了; - cmux(正是我选择的)可以通过添加
MatchWithWriters
来解决 Go 的实现问题。
合:解决问题
这一次时间比较宽裕,我不准备回避了,所以选择了第二种方式 cmux 加上参数 MatchWithWriters
解决问题,最终的代码为:
[root@liqiang.io]# cat main.go
... ...
import "github.com/soheilhy/cmux"
... ...
l, err := net.Listen("tcp", fmt.Sprintf(":%d", host, port))
if err != nil {
log.Fatalf("[E] Failed to listen on :%d: %v", port, err)
}
var (
m = cmux.New(l)
grpcListener = m.MatchWithWriters(
cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc"),
)
httpListener = m.Match(cmux.HTTP1Fast())
)
... ...
这其实也是使用同个端口提供 http 和 grpc 的其中一种实现。那么问题又来了,GRPC 使用的是 HTTP2,那 HTTP2 的连接建立过程又是怎么样的呢?(又给自己挖了一个坑)
—
updated at 2021-02-22 22:22:22
此坑已填:http2 的连接建立过程