golang.org/x/crypto/ssh で固まらないようにする

最近 CKE というベアメタル向けの Kubernetes 管理ツールを作っています。 ブートストラップツールではなく自律的・継続的にクラスタの構成を修正していくツールで、以下が特徴です。

  • ネットワークプラグイン非依存
  • HA 対応
  • CKE 自体の高可用性

今日は CKE の紹介が目的ではないのでこのあたりにします。

本題は、CKE が内部で使っている golang.org/x/crypto/ssh というライブラリが無期限に ブロックしてしまう問題があったので、どう対応したかです。

問題

以下のようなスタックトレースで動作が停止するケースが試験中に何度か発生しました。

goroutine 2153 [chan receive, 4 minutes]:
golang.org/x/crypto/ssh.(*mux).openChannel(0xc000105730, 0x13a0ec2, 0x7, 0x0, 0x0, 0x0, 0x0, 0x50, 0x50)
        /go/src/github.com/cybozu-go/cke/vendor/golang.org/x/crypto/ssh/mux.go:322 +0x1eb
golang.org/x/crypto/ssh.(*mux).OpenChannel(0xc000105730, 0x13a0ec2, 0x7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, ...)
        /go/src/github.com/cybozu-go/cke/vendor/golang.org/x/crypto/ssh/mux.go:298 +0x64
golang.org/x/crypto/ssh.(*Client).NewSession(0xc000c74d20, 0xc00089dd28, 0x92fb45, 0x8)
        /go/src/github.com/cybozu-go/cke/vendor/golang.org/x/crypto/ssh/client.go:130 +0x5d

当該箇所のコードは以下です。
crypto/mux.go at 0c41d7ab0a0ee717d4590a44bcb987dfd9e183eb · golang/crypto · GitHub

Go の既知の Issue を探すと、同様のケースがいくつか報告されているのが分かりました。

分析

当該の問題は、SSH サーバーが無応答になる際に発生するようです。実際我々のケースも、サーバーをいきなり再起動するテストケース中で観測されました。2016 年頃から何度か報告されているようですが、報告者が諦めてしまったり API が大きく変わる変更を入れるため fork したりで修正されていない(修正が困難な)問題のようです。

fork したプロジェクト を分析したところ、現時点で本家のコードとは diff が 10,000 行近くになっており、本家が追加した新暗号などには対応できていませんでした。

修正方針

ライブラリ本体を短期的に直すのは困難そうで、自前で fork ないしすでにある fork をメンテナンスするのもコストが高くつきそうです。なんとか元のライブラリのまま回避する方法がないか検討したところ、golang.org/x/crypto/ssh には別に用意した TCP 接続を利用する NewClientConn 関数があることを見つけました。

これを利用すれば、ライブラリはそのまま、適宜コネクションにデッドライン設定をすることができそうです。 また TCP keepalive も有効にして間隔を短くすることも可能です。

というわけで、元のライブラリ+ワークアラウンドコードで回避する方針にしました。

修正内容

以下の pull request が修正内容となります。

github.com

Go の TCP keepalive 設定については以下を頭に入れると理解できると思います。

まとめ

golang.org/x/crypto/ssh で無期限にブロックしてしまう問題をどのように回避したかを解説しました。

このライブラリは、一度 SSH サーバーに接続したコネクション上で何度もコマンド実行を繰り返せるため、たくさんリモートコマンド実行をする用途では高速に操作できる優れものです。今回は回避という方向でしたが、本体側でなにかできるようでしたら貢献していきたいと思います。