Go Conference'19 Summer in Fukuoka
Yuichi Watanabe @Nulab Inc.
Yuichi Watanabe
After replacement
Before replacement
Gitコマンドのclone、push、fetch等で発生するリモートリポジトリへのHTTP通信を捌く。
HTTPのBASIC認証で認証処理を行う。
スマートプロトコルと呼ばれる、Gitでネットワーク越しにデータを受け渡しするための規約を実装している。
Gitリポジトリの読み書きは、Gitコマンドのupload-packとreceive-packを実行することで実現している。
Description
Perl to Go
以前はPerlで実装。PSGIサーバとして Starletを使用。
ワーカプロセスを指定数だけプリフォークして、クライアントからのリクエストに待機。
GO版ではnet/http パッケージを使用したマルチスレッド方式に転換。
1つのプロセスが、リクエスト毎にゴルーチンを生成して並行に捌く。
内部では、execパッケージのexec.Commandを使用してGitコマンドを高頻度で実行する。
Git HTTP ServiceのSSHプロトコル版。リモートリポジトリへのSSHを使用したアクセスを捌く。
ユーザがBacklogにアップロードした公開鍵をもとに公開鍵認証を行う。
SSHのリクエストの種類としては、コマンドの単体実行を許可するexecのみをサポートする。一般的なshellアクセスは受け付けない。
Description
Python to Go
Support new public key and key exchange algorithms
Pickup:
Leaks SSH connection
GOプロセスが保有しているゴルーチンのメトリクスを見ていると、徐々に増加していることに気づく。
同時にサーバ全体でESTABLISHEDなコネクションも徐々に増加していた。
サーバに入りnetstatでコネクションの状態とIPとその数を見ると、特定IPからの接続が多数ESTABLISHEDのままになっていた。
// is dummy IP address
$ netstat -tan | grep ${PORT} | grep 'ESTABLISHED' | awk '{print $5}' | cut -d : -f1 | sort -n | uniq -c | sort -n
...
79 198.51.100.24
type tcpKeepAliveListener struct {
*net.TCPListener
}
func (ln tcpKeepAliveListener) Accept()
(net.Conn, error) {
tc, err := ln.AcceptTCP()
if err != nil {
return nil, err
}
tc.SetKeepAlive(true)
tc.SetKeepAlivePeriod(3 * time.Minute)
return tc, nil
}
type SSHServer struct {
// ...
}
func (srv *SSHServer) ListenAndServe(port int)
error {
ln, _ := net.Listen("tcp",
fmt.Sprintf(":%d", port))
tcpln := TcpKeepAliveListener{
ln.(*net.TCPListener)}
return srv.Serve(ln)
}
Backlog Webからリモートリポジトリを操作するためののRPCを提供する。
提供するRPCの例
ブランチ一覧の取得
タグ一覧の取得
コミットログの取得
差分計算
プルリクエスト作成
マージ等...
Description
以前はJavaで実装。
サービス間通信のフレームワークとしてMessagePack-RPCを使用してRPCを提供。
リポジトリの操作はJGitを使用。
Go版では
サービス間通信にgRPCを使用。例えば、容量の大きなファイルを取得するRPCでは、gRPCのサーバサイドストリーミングRPCを使用して効率化。
実際のGitリポジトリの操作は、CGO経由でCプログラムのlibgit2を使用している。
Java to Go
Pickup: How to migrate in increments each RPC
Git RPC Serviceは、多数のRPCを持つため、いっぺんに刷新するのではなく、RPC単位で段階的な移行を試みた。
そのために、MessagePack-RPCクライアントとgRPCクライアントをラップして、異なるRPCプロトコルを透過的に制御するクライアントライブラリを作成した。
設定ファイルにより、RPC毎にどちらを使用するのか選択する。問題があればRPC単位で切り替える。
Description
Perl to Go
Pickup: Concurrency limit
Perl版の旧ワーカはTheSchwartzを使用しており、1ワーカが1度に捌けるジョブは1つ。そのため、ジョブの同時実行数はワーカのプロセス数となる。
新ワーカはchannel経由で受信するジョブを取得してゴルーチンで並行に処理する。
同時実行数はスケールしやすくなるが、ワーカの処理の中には一部サーバに負荷をかける重たい処理もある。それらの同時実行数を制限しなければならない。ゴルーチンは作り放題ではない。
func (w *Worker) Work() {
w.conn, _ = amqp.Dial(amqpURI)
w.ch, _ = w.conn.Channel()
// 省略...
jobs, _ := w.ch.Consume(q.Name,
consumerName, true, false,
false, false, nil)
// limiter channel
limit := make(chan struct{}, limitSize)
for job := range jobs {
limit <- struct{}{} // put in
go func(j *amqp.Delivery) {
w.Handler(j)
<-limit // put out
}(&job)
}
}
netutilパッケージのnetutil.LimitListenerを参考に、channelのキャパシティを最大同時実行数として活用する。
キャパシティに到達したchannelに対して、さらに値を追加すると、キャパシティが空くまでブロックされる。そのため、それ以上ジョブを処理しない。
各サービスの主要技術の遷移
Service | Before |
After |
---|---|---|
Git HTTP Service |
Perl, Starlet | GO, net/http |
Git SSH Service | Python, Twisted |
GO, crypto/ssh |
Git RPC Service | Java, MessagePack-RPC, JGit |
GO, gRPC, Libgit2 |
Git Hook Workers | Perl, TheSchwartz |
GO, Rabbit MQ |
Benefits by Go
Gitサービス群全体のパフォーマンスが向上した。
サーバリソースの節約、e2eの応答速度、突然の大量アクセスへの応答率
Benefits by Monoglot