SecHack365 2019 in Fukuoka
Yuichi Watanabe @Nulab Inc.
### Backlogが提供するGitホスティング
### アーキテクチャ概観
### リモートリポジトリへのHTTPアクセスを捌くGitHTTPサービス
git clone、push、fetch等で発生するリモートリポジトリへのHTTP通信を捌く。
スマートプロトコルと呼ばれる、Gitでネットワーク越しにデータを受け渡しするための規約を実装している。
Gitリポジトリの読み書きは、Gitコマンドのupload-packとreceive-packを実行することで実現している。
HTTPのBASIC認証で認証処理を行う。
### リモートリポジトリへのSSHアクセスを捌くGit SSHサービス
Git HTTP ServiceのSSHプロトコル版。リモートリポジトリへのSSHを使用したアクセスを捌く。
ユーザがBacklogにアップロードした公開鍵をもとに公開鍵認証を行う。
### リモートリポジトリへのRPCを提供するGitRPCサービス
Backlog Webからリモートリポジトリを操作するためののRPCを提供する。
提供するRPCの例
ブランチ一覧の取得
タグ一覧の取得
コミットログの取得
コミットDiffの計算
ツリー情報の取得
プルリクエスト作成
マージ等...
### Gitフックを遅延実行するGitフックワーカ
Gitフック(※特定のGitコマンドが実行された時に、スクリプトを実行する仕組み)の処理をジョブとしてキューに投入し、Git Hook Workersが遅延実行する。
ジョブの種類によってワーカを分けている。
ユーザのディスク使用量の確認
Webフックの実行
コミット情報の登録
お知らせメール送信等...
### Git HTTPの裏側、Gitスマートプロトコル
スマートプロトコルによるgit cloneの通信フロー
### Git HTTPの裏側、Gitスマートプロトコル
スマートプロトコルによるgit pushの通信フロー
### Go言語で実装するSSHサーバ
SSHプロトコルの通信フローの概観
### Go言語で実装するSSHサーバ
SSHの中核を担うSSHの3つのプロトコルを実装した、準標準パッケージのx/crypto/ssh を使用。
x/crypto/sshはサーバとしての機能は提供していない。そのため、複数のSSHのリクエストを並行に捌くデーモンプロセスとして、net/httpを参考にSSHサーバのフレームワークを実装。
メインループ内でTCP接続を確立したら、ゴルーチンを生成して非同期にネゴシエーション、認証、データ転送を実行する。
SSHのリクエストの種類としては、コマンドの単体実行を許可するexecのみをサポートする。一般的なshellアクセスは受け付けない。
func main() {
ln := createListener(port)
cfg := &ssh.ServerConfig{
PublicKeyCallback: func(conn ssh.ConnMetadata,
key ssh.PublicKey) (*ssh.Permissions, error) {
// handle public key auth
},
}
cfg.AddHostKey(privateKey)
for {
conn, err := ln.Accept()
if err != nil {
return err
}
go func() {
sConn, chans, gReqs, err := ssh.NewServerConn(
conn, cfg)
go ssh.DiscardRequests(gReqs)
for newChan := range chans {
if newChan.ChannelType() != "session" {
newChan.Reject(ssh.UnknownChannelType,
"unknown channel type")
continue
}
ch, reqs, err := newChan.Accept()
for req := range reqs {
if req.Type != "exec" {
continue
}
// handling exec request
}
}
}()
}
}
※ 簡略化したサンプルコード
### [TIPS] SSHコネクションリークを阻止するために
ある日、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
### [TIPS] SSHコネクションリークを阻止するために
### [TIPS] SSHコネクションリークを阻止するために
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本体とGit RPCサービスを繋ぐgRPC
なぜgRPCを使うのか
「自動生成する強い型付けのサービス間インターフェイス」
### Backlog本体とGit RPCサービスを繋ぐgRPC
「様々な言語およびプラットフォームで使用できる柔軟性」
「HTTP/2によるサービス間の効率的な通信」
gRPCはトランスポートプロトコルとして、HTTP/2を使用している。
特徴として、1つのコネクション上で複数の並列要求を多重化できることや、クライアントとサーバーの双方向で通信ができること等が上げられる。
gRPCはそのようなHTTP/2の機能をベースに、サービス間のストリーミングによる通信を実現してる。
バックエンドのBacklog本体とGit RPC サービス間の通信がより柔軟で高速になることを期待している。
### Backlog本体とGit RPCサービスを繋ぐgRPC
gRPCストリーミング
「Server-side streaming RPC」
「Client-side streaming RPC」
### Backlog本体とGit RPCサービスを繋ぐgRPC
「Bidirectional streaming RPC」
### Backlog本体とGit RPCサービスを繋ぐgRPC
「Bidirectional streaming RPC」
チェックした商品の関連商品
### CGOを用いたCプログラム呼び出し
できればGoのプログラムからGitのローレベルなAPIを使って少ないオーバーヘッドでGitリポジトリを操作したい。
なぜlibgit2を使うのか
「Gitコマンドによるオーバーヘッド」
### CGOを用いたCプログラム呼び出し
「libgit2をGo言語(cgo)から実行するという選択」
package main
// #include <stdio.h>
// #include <stdlib.h>
//
// static void myprint(char* s) {
// printf("%s\n", s);
// }
import "C"
import "unsafe"
func main() {
cs := C.CString("Hello from stdio")
C.myprint(cs)
C.free(unsafe.Pointer(cs))
}
libgit2ならCプログラムから使える。
Cプログラムから使えればGo言語のcgoから使える
cgoとはGoからCのプログラムにアクセスするための標準パッケージ。
さらに、cgoを使った公式のバインディングライブラリも提供されている。
また、Cプログラム自体も静的リンクで1つのバイナリに含めることが可能。
### ジョブキューとしてのRabbitMQ
なぜRabbitMQを使用するのか
RabbitMQ(ラビットエムキュー)は、Advanced Message Queuing Protocol (AMQP) を使用した、オープンソースのメッセージ指向ミドルウェア。RabbitMQ Serverは、Erlang言語で記述されている。
### [TIPS] GO言語のチャンネルを用いたワーカの同時実行数制御
Goで実装されたGitフックワーカは、Goのchannel経由で受信するジョブを取得して、ゴルーチンで並行に処理する。
同時実行数はスケールしやすくなるが、Gitフックワーカの処理の中には一部サーバに負荷をかける重たい処理もある。それらの同時実行数を制限しなければならない。ゴルーチンは作り放題ではない。
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)
}
}
そこで、Go言語が提供するchannelのキャパシティを最大同時実行数として活用する。
キャパシティに到達したchannelに対して、さらに値を追加すると、キャパシティが空くまでブロックされる。そのため、それ以上ジョブを処理しない。
ほんの数行、シンプルな構文でスレッドセーフなブロッキングを実現できるのも、GO言語の強みでもある。
「Backlog」が提供するGitホスティングは、役割の異なる複数のサービスによって構成されている。
それらのサービスの中核を担うのはGo言語である。全てのGitサービスはGoで実装されており、サービス特有のプロトコルに併せて、Goが提供する機能を活用している。
net/httpパッケージをベースに、Gitのスマートプロコトルを実装したGit HTTPサービス
crypto/sshパッケージをベースに、SSHサーバを実装したGit SSHサービス
cgoでCプログラムと連携して、 gRPCでサービス間通信を行うGit RPCサービス
Gitフックの処理をジョブとしてRabbitMQに投入し、遅延実行するGitフックワーカ。
現在も新たなサービスをGo言語で開発中 三GO三GO三GO
### GO言語に支えられたGitサービス