# 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自体が、静的リンクしやすいような仕組みを用意していた。

### 静的リンクの手順

  1. 依存ライブラリとして落としてきたgit2goのリポジトリ内で、サブモジュールとして管理されているlibgit2のソースを、git2goのリポジトリの`./vender/` 直下へダウンロードする。
     
  2. ダウンロードしたlibgit2のソースを、出力先をgit2goのリポジトリ内 `${ROOT}/static-build/install`に指定してビルドする。
     
  3. バインディングの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