起:timed out waiting for server handshake

我们组的服务都是同时提供 GRPC 和 HTTP 接口的,其中大部分 HTTP 接口都是直接通过 grpc-gateway 从 GRPC 转换而来的,但是,突然有一天,在我更新了 grpc 版本之后,出现问题了,访问 HTTP 接口报错了:

  1. [root@liqiang.io]# GET http://192.168.67.41/api/v3/metrics:query?query=host_cpu_usage_overall%5B2h%5D
  2. HTTP/1.1 503 Service Unavailable
  3. Server: nginx/1.10.2
  4. Date: Wed, 10 Apr 2019 11:51:45 GMT
  5. Content-Type: application/json
  6. Transfer-Encoding: chunked
  7. Connection: keep-alive
  8. Trailer: Grpc-Trailer-Content-Type
  9. {
  10. "error": "all SubConns are in TransientFailure, latest connection error: timed out waiting for server handshake",
  11. "code": 14,
  12. "details": []
  13. }

承: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 环境变量为 offGRPC_GO_REQUIRE_HANDSHAKE=off,但是,因为觉得这种方式比较恶心,所以就回滚 grpc 版本先了。

这件事情也就先这样过去了。

转:timed out waiting for server handshake again

最近,因为尝试将依赖管理工具从 dep 转到 go module,问题又来了,于是 go module 对于版本管理只有最低版本,没有最高和指定版本,所以 grpc 版本又被升级上去了,一下子到了 v1.33.0,那么我以为旧方法依旧可行,但是被啪啪打脸了,我遇到了:

  1. [root@liqiang.io]# tail log
  2. panic: rpc error: code = Unavailable desc = timed out waiting for server handshake
  3. goroutine 1 [running]:
  4. main.main()
  5. /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.

这里就直接了当得说了两个重要的点:

合:解决问题

这一次时间比较宽裕,我不准备回避了,所以选择了第二种方式 cmux 加上参数 MatchWithWriters 解决问题,最终的代码为:

  1. [root@liqiang.io]# cat main.go
  2. ... ...
  3. import "github.com/soheilhy/cmux"
  4. ... ...
  5. l, err := net.Listen("tcp", fmt.Sprintf(":%d", host, port))
  6. if err != nil {
  7. log.Fatalf("[E] Failed to listen on :%d: %v", port, err)
  8. }
  9. var (
  10. m = cmux.New(l)
  11. grpcListener = m.MatchWithWriters(
  12. cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc"),
  13. )
  14. httpListener = m.Match(cmux.HTTP1Fast())
  15. )
  16. ... ...

这其实也是使用同个端口提供 http 和 grpc 的其中一种实现。那么问题又来了,GRPC 使用的是 HTTP2,那 HTTP2 的连接建立过程又是怎么样的呢?(又给自己挖了一个坑)

updated at 2021-02-22 22:22:22

此坑已填:http2 的连接建立过程

Ref