Tech practice for replace

Backlog's Git services

in 三GOGOGO

Go Conference'19 Summer in Fukuoka

Yuichi Watanabe @Nulab Inc.

Yuichi Watanabe

Intoroduction

Overview Backlog's Git services

After replacement

Before replacement

Git HTTP Service

  • 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

Git SSH Service

  • Git HTTP ServiceのSSHプロトコル版。リモートリポジトリへのSSHを使用したアクセスを捌く。

  • ユーザがBacklogにアップロードした公開鍵をもとに公開鍵認証を行う。

  • SSHのリクエストの種類としては、コマンドの単体実行を許可するexecのみをサポートする。一般的なshellアクセスは受け付けない。

Description

  • Go版では、よりセキュアな公開鍵と鍵交換アルゴリズムをサポート。
    • 公開鍵 Ed25519
    • 鍵交換アルゴリズム curve25519-sha256@libssh.org

Python to Go

  • 以前はPythonで実装。中核を担うSSHプロトコルの実装は、イベント駆動モデルのTwistedを使用して実現。
  • 中核を担うSSHのネゴシエーションはgolang.org/x/crypto/ssh を使用して実装。
  • 複数のリクエストを並行に捌くために、net/httpを参考にSSHサーバのフレームワークを実装。

Git SSH Service

Support new public key and key exchange algorithms

Pickup:

Leaks SSH connection

  • GOプロセスが保有しているゴルーチンのメトリクスを見ていると、徐々に増加していることに気づく。
  • 同時にサーバ全体でESTABLISHEDなコネクションも徐々に増加していた。
  • サーバに入りnetstatでコネクションの状態とIPとその数を見ると、特定IPからの接続が多数ESTABLISHEDのままになっていた。
  • 対象のIPをGit SSH Serviceのログから検索すると、SSHのハンドシェイクで段階で相手が応答せずにタイムアウトしている。正常なアクセスのログはヒットしない

Git SSH Service

// 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
  • さらに、対象のIPを調査するとウクライナを示しており、abuseipdb.com でも、このIPアドレスからの脅威が多数報告されていた。
  • SYNフラッド攻撃で意図的に応答を返さず、サーバリソースを枯渇させようとしている可能性あると推測。
  • 対処法として、クライアントから一定時間応答が無い場合に、それを検知してコネクションを切断したい。
  • もともとGit SSH Serviceのベースとして参考にしていたnet/httpはなにか対処しているのか?コードを読み直す。
  • すると、TcpKeepAliveListenerなるリスナーを発見。カーネルの機能であるTCP KeepAliveを有効にしてくれる。
  • TCP KeepAliveは、TCPで接続されたホスト間で、相手が生きているか定期的に確認し、もし一定期間応答が無ければコネクションを切断してくれる。
  • net/httpのTcpKeepAliveListenerを流用して、Git SSH Serviceに適用。
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
}

Git SSH Service

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)
}

Git RPC Service

  • Backlog Webからリモートリポジトリを操作するためののRPCを提供する。
  • 提供するRPCの例
    • ブランチ一覧の取得
    • タグ一覧の取得
    • コミットログの取得
    • 差分計算
    • プルリクエスト作成
    • マージ等...

Description

  • 以前はJavaで実装。

    • サービス間通信のフレームワークとしてMessagePack-RPCを使用してRPCを提供。

    • リポジトリの操作はJGitを使用。

  • Go版では

    • サービス間通信にgRPCを使用。例えば、容量の大きなファイルを取得するRPCでは、gRPCのサーバサイドストリーミングRPCを使用して効率化。

    • 実際のGitリポジトリの操作は、CGO経由でCプログラムのlibgit2を使用している。

Java to Go

Git RPC Service

Pickup: How to migrate in increments each RPC

  • Git RPC Serviceは、多数のRPCを持つため、いっぺんに刷新するのではなく、RPC単位で段階的な移行を試みた。
  • そのために、MessagePack-RPCクライアントとgRPCクライアントをラップして、異なるRPCプロトコルを透過的に制御するクライアントライブラリを作成した。
  • 設定ファイルにより、RPC毎にどちらを使用するのか選択する。問題があればRPC単位で切り替える。
  • ビッグリリースになるのを防ぎ、小さく少しづつリリースすることで、問題の発生箇所を特定しやすくなった。

Git RPC Service

Git Hook Workers

  • Gitフック(特定のGitコマンドが実行された時に、スクリプトを実行する仕組み)の処理をジョブとしてキューに投入し、Git Hook Workersが遅延実行する。
  • ジョブの種類によってワーカを分けている。
    • ユーザのディスク使用量の確認
    • Webフックの実行
    • コミット情報の登録
    • お知らせメール送信等...

Description

  • 以前はPerlで実装。
    • TheSchwartzを使用してワーカを実装。
    • SQLiteをキューとして使用。
  • GO版ではRabbitMQを使用。

Perl to Go

Git Hook Workers

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に対して、さらに値を追加すると、キャパシティが空くまでブロックされる。そのため、それ以上ジョブを処理しない。

Summary

各サービスの主要技術の遷移

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

Benefits by Go

  • Gitサービス群全体のパフォーマンスが向上した。

    • サーバリソースの節約、e2eの応答速度、突然の大量アクセスへの応答率​

  • 基本的に標準/準標準パッケージをベースに実装できたので、GO自身の頻繁なアップデートの恩恵を受けやすくなった。
  • 問題が起こった時に、標準パッケージという最高のお手本に幾度となく助けられた。

Benefits by ​Monoglot

  • 基盤となるようなコードや、監視、CI/CDの仕組みを共通化でき開発/運用の効率が上がった。
    • ​ロギングのルールが統一しやすくなりサービス間の追跡性が上がった。
    • 監視メトリクスの共通化により各サービスの状態を比較しやすくなった。
    • 新たなサービスの開発が始まった時にスタートダッシュしやすくなった。
  • 新たに発見した言語特有の良い技法が、他のサービスに伝播しやすくなった。
  • GOさえ読み書きできれば、新メンバが参入しやすくなった。