# Git RPC Service
with
Go + gRPC + libgit2
Fukuoka.go #13 by Yuichi Watanabe @nulab
## Overview
- Backlog本体となるWebアプリからリモートリポジトリを操作するためのRPCを提供するGit RPC Serviceについて、その開発で得た知見をかいつまんで紹介します。
提供するRPCの例- ブランチ一覧の取得
- タグ一覧の取得
- コミットログの取得
- 差分計算
- プルリクエスト作成
- マージ等...
- Keywords: #gRPC #libgit2 #modules #cgo
## What’s Git RPC Service
- Backlogのアプリケーション本体からRPCでリクエストを受取り、対象のGitリポジトリへの操作を請け負う
- Backlogが提供するGit機能は、複数のサブシステムによって構成されている。
- Git RPC Service はそのサブシステムの一つ。
## Key technologies
1. gRPC / Protocol Buffers
2. libgit2
## Why gRPC
### 自動生成する強い型付けのサービス間インターフェイス
- gRPCは、デフォルトでProtocol Buffersを、型付けされたIDL(インタフェース定義言語)としてサポート。
- そのIDLで定義したデータやメソッドから各プラグラミング言語のコードへコンパイルするツールとして使用できる。
- クライアントスタブとサーバーインターフェースをProtocol Buffersで自動生成できる。
- サーバー間で通信するためのめんどくさいコードを人力で書く必要がなくなり、より効率的で安全なサービス間の通信が期待できる。
### HTTP/2によるサービス間の効率的な通信
- gRPCはトランスポートプロトコルとして、HTTP/2を使用している。
- 特徴として、1つのコネクション上で複数の並列要求を多重化できることや、クライアントとサーバーの双方向で通信ができること等が上げられる。
- gRPCはそのようなHTTP/2の機能をベースに、サービス間のストリーミングによる通信を実現してる。
- バックエンドのBacklog本体とGit RPC Server間の通信がより柔軟で高速になることを期待している。
### 様々な言語およびプラットフォームで使用できる柔軟性
- gRPCのクライアントとサーバーは、Windows、Linux、およびMacといった様々な環境で動作し通信できる。
- また、様々なプログラミング言語をサポートしており、例えば、クライアントをScalaで実装して、サーバーはGoといった他の言語を使用することも可能。
- 一定のプラットフォームや言語にロックインされにくい。
## gRPC stack
### gRPCのstack
-
gRPCは複数の言語をサポートしているため、言語によってはリポジトリ自体を分けて作られている。
-
多くの言語はCで実装されたコアライブラリをラップした形で提供されている。(github.com/grpc/grpc)
-
Go版やJava版はコアライブラリ自体は使用言語そのもので実装されている。(github.com/grpc/grpc-go, grpc-java)
-
Go版と他の言語のフレームワークのスタックの違いを比較してみる。
### github.com/grpc/grpc
Cコアライブラリをラップした言語
参考: Visualizing gRPC Language Stacks
https://grpc.io/blog/grpc-stacks
### github.com/grpc/grpc-go
gRPCのGo実装
参考: Visualizing gRPC Language Stacks
https://grpc.io/blog/grpc-stacks
## gRPC streaming
### Server-side streaming RPC
- クライアントから1以上のリクエストを送信して、サーバーは複数のレスポンスを返却できる
### Client-side streaming RPC
- クライアントから複数のリクエストを送信して、サーバーは一つのレスポンスを返却する。
### Bidirectional streaming RPC
- クライアントとサーバー間でから任意の数のメッセージをやりとりできる。
## Example
Server-side streaming RPC
// file.proto
syntax = "proto3";
package file;
message FileRequest {
string name = 1;
}
message FileResponse {
bytes data = 1;
}
service FileService {
rpc Download (FileRequest) returns (stream FileResponse);
}
### サービスのインターフェースを定義する
- Protobufのメッセージとサービスを定義する。
- メソッドの返り値に`stream`を付与する。
$ protoc -I ./proto --go_out=plugins=grpc:./proto/ ./proto/file.proto
### サーバーインターフェースとクライアントスタブを生成する
- protocコマンドで.protoファイルをコンパイルしてサーバーインターフェースとクライアントスタブを生成する。
type FileServiceServer interface {
Download(*FileRequest, FileService_DownloadServer) error
}
略
type FileService_DownloadServer interface {
Send(*FileResponse) error
grpc.ServerStream
}
### 生成されたサーバーインターフェースを実装する
- 生成された`xxx.pb.go`ファイル内の`FileServiceServer`インターフェースを実装する。
- streamで返却するレスポンスは返り値に定義されてない
- 引数に定義されている`FileService_DownloadServer`インターフェースの`Send()`メソッドを呼び出してレスポンスを返却する。
type FileService struct{}
func (s FileService) Download(req *pb.FileRequest,
stream pb.FileService_DownloadServer) error {
fs, _ := os.Open(filepath.Join("./resource", req.Name))
defer fs.Close()
buf := make([]byte, 1000*1024)
for {
n, err := fs.Read(buf)
if err != nil {
if err != io.EOF {
return err
}
break
}
err = stream.Send(&pb.FileResponse{
Data: buf[:n],
})
if err != nil {
return err
}
}
return nil
}
### 生成されたサーバーインターフェースを実装する
package main
import (
"log"
"net"
"google.golang.org/grpc"
pb "example.com/server_stream/proto"
)
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterFileServiceServer(s, &FileService)
log.Println("start server on port :50051")
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
### gRPCサーバーを作成してサービスを登録する
略
"google.golang.org/grpc"
pb "example.com/server_stream/proto"
)
func main() {
conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
defer conn.Close()
c := pb.NewFileServiceClient(conn)
name := os.Args[1]
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
stream, _ := c.Download(ctx, &pb.FileRequest{Name: name})
var blob []byte
for {
c, err := stream.Recv()
if err != nil {
if err == io.EOF {
break
}
panic(err)
}
blob = append(blob, c.Data...)
}
ioutil.WriteFile(name, blob, 0644)
}
### クライアントプログラムを実装する
## Why libgit2
### Gitコマンドによるオーバーヘッド
- Gitコマンドが直接使うと、RPCを処理する度に毎回外部プロセスを作ることになりオーバーヘッドが発生する。
-
できればGoのプログラムからGitのローレベルなAPIを使って少ないオーバーヘッドでGitリポジトリを操作したい。
### リモートリポジトリではGitコマンドが提供する一部の機能が使えない
-
リモートリポジトリはワークツリーを持たないBaraリポジトリとして展開されている。
-
そのため、Gitコマンドが提供する一部の機能が使えない。例えば`git merge`等。
-
正直GitのローレベルなAPIをフルスクラッチで実装するのは、それだけで大きなプロジェクトになってしまう。
### libgit2をGoから使うという選択
- libgit2ならCプログラムから使える。
- Cプログラムから使えればGoから使える。
_人人人_
> cgo <
 ̄Y^Y^Y^ ̄
### cgo
- GoからCのプログラムにアクセスするための標準パッケージ。
- cgoのおかげでGoプログラムからlibgit2が利用できる。
- cgoを使ってバインディングライブラリを書く?
### git2go
- libgit2公式のGoバインディングライブラリ。
## static linking libgit2
### 静的リンクで1バイナリを目指す
- バインディングのgit2goをインポートしてGoアプリをビルドした場合、libgit2のCプログラムは動的リンクされるようになってる。
- git rpc server を稼働させるサーバーにlibgit2を入れてもよかったが、プロビジョニングのコード書く手間、バージョンの互換性を保守する未来を考えると、静的リンクでGoのバイナリ一枚で完結するのが望ましい。
- バインディングライブラリであるgit2go自体が、静的リンクしやすいような仕組みを用意していた。
### 静的リンクの手順
- 依存ライブラリとして落としてきたgit2goのリポジトリ内で、サブモジュールとして管理されているlibgit2のソースを、git2goのリポジトリの`./vender/` 直下へダウンロードする。
- ダウンロードしたlibgit2のソースを、出力先をgit2goのリポジトリ内 `${ROOT}/static-build/install`に指定してビルドする。
- バインディングのgit2goをインポートしたGoアプリを `--tags "static"` のように staicというタグ付きでビルドする。
### staicタグで切り分けているもの
- git2goのリポジトリをgrepしてみる。
- 以下2つのファイルに付与されている。
### staicタグで切り分けているもの
- staticオプション無しのデフォルトの挙動では、`git_dynamic.go`が読み込まれる。有りでは `git_static.go` が読み込まれる。
### libgit2のビルドをいつ/どこではさむのか
- git rpc server の依存ライブラリの管理はmodulesを使っている。
- 前述したlibgit2のビルドをいつ・どのタイミングで挟むのか。
- modulesはbuild時に依存ライブラリをダウンロードする。
### libgit2のビルドをいつ/どこではさむのか
-
modulesライブラリのダウンロード先は`$GOPAPTH/pkg/mod`以下。
-
それに無理矢理手を加えるの現実的ではない。
-
そもそもダウンロードされたときに.gitディレクトリは存在しないので、Gitの機能であるサブモジュールを使ってlibgit2をダウンロードできない。
### replace ディレクティブ
-
modulesにはreplaceディレクティブなるものがあった。
-
これは指定したパッケージのみローカルやフォーク先に向けることができる。
`replace example.com/project/foo => ./foo`
-
これで参照先を変えることができるはず
### replace ディレクティブ
-
git-rpc-serverのリポジトリ直下にビルドしたlibgi2を同梱したgit2goを用意する。
-
そして、replaceディレクティブでgit2goの向き先を変えることで成功。
module example.com/vvatanabe/git-rpc-server
require (
github.com/golang/protobuf v1.2.0
google.golang.org/grpc v1.17.0
gopkg.in/libgit2/git2go.v27 v27.0.0-20190104134018-ecaeb7a21d47
)
replace gopkg.in/libgit2/git2go.v27 v27.0.0-20190104134018-ecaeb7a21d47 => ./local/git2go
Git RPC Service with Go + gRPC + libgit2
By Yuichi Watanabe
Git RPC Service with Go + gRPC + libgit2
- 1,074