Go Conference '20 in Autumn SENDAI

Gitホスティングにおける

GoとgRPCを用いた

複製と分散のアプローチ

Yuichi Watanabe at Nulab Inc.

Agenda

  • 概要

  • 取り扱うデータの特性

  • スケールアウトの課題

  • 複製と分散のアプローチ

  • Goで実装するgRPCの動的プロキシ

  • まとめ

本日お話しすること

Gitホスティングの可用性を高めるためにはどのような方法があるのか?
可用性を高める手段を考える
  • どのようにスケールアウトさせるか

  • リポジトリを持つストレージ周りの課題
  • アプリの分割

  • ストレージの複製・分散の仕組みを検証

手段を実装に落とし込む

  • github.com/vvtanabe/git-ha-poc

    • Goを用いたアプリの分割

    • grpc-goによるサービス連携

    • Go製のgRPC動的プロキシ

    • 非同期レプリケーション

手段を実装に落とし込む

  • github.com/vvtanabe/git-ha-poc

    • Goを用いたアプリの分割

    • grpc-goによるサービス連携

    • Go製のgRPC動的プロキシ

    • 非同期レプリケーション

可用性を高める手段を考える
  • どのようにスケールアウトさせるか

  • リポジトリを持つストレージ周りの課題
  • アプリの分割

  • ストレージの複製・分散の仕組みを検証

本日お話しすること

Gitホスティングの可用性を高めるためにはどのような方法があるのか?

Agenda

  • 概要

  • 取り扱うデータの特性

  • スケールアウトの課題

  • 複製と分散のアプローチ

  • Goで実装するgRPCの動的プロキシ

  • まとめ

取り扱うデータの特性

サーバで保持するベアリポジトリ

  • 作業ディレクトリをもたないリポジトリ
     

  • .gitディレクトリ以下のファイル群を持つ

    • リポジトリのconfig、hook script等...

    • tagやbranch等の参照情報

    • Gitオブジェクト

      • tree, blob, commit, tag

      • 上記のpack(圧縮データ)
         

  • リポジトリという単位で世界が区切られたオブジェクトデータベース
├── branches
├── config
├── description
├── HEAD
├── hooks
├── index
├── info
├── objects
│   ├── 01
│   │   └── a4798bf8e9872b0ae0301657d9908a0d8bd789
│   ├── cb
│   │   └── 311c3d61afbe9f979770be8f999b66e7550ec8
│   ├── d1
│   │   └── 85f2630d528949f038e055d6b35d5954a8a0d5
│   ├── info
│   └── pack
└── refs
    ├── heads
    │   └── master
    └── tags
        └── v1.0.0

Agenda

  • 概要

  • 取り扱うデータの特性

  • スケールアウトの課題

  • 複製と分散のアプローチ

  • Goで実装するgRPCの動的プロキシ

  • まとめ

スケールアウトの課題

  1. 限定されるストレージの種類

  2. ストレージ間のデータの複製と一貫性

  3. ストレージの分割

スケールアウトの課題

  • NFS等のファイルストレージは?

    • Gitはリポジトリの状態によってCPU、ディスクIOが跳ね上がりやすく
      ブロックストレージの10倍程度遅くなる場合も
       

  • S3等のオブジェクトストレージは?

    • s3fs(FUSE)等のファイルシステムとしてマウントさせるツールが必要

    • 読み書きするファイルが増えると線形に増える通信コスト
       

  • パフォーマンスを考慮するとブロックストレージ一択

    • 一般的に複数のホストから取り扱いできない

      • 同領域へ書き込むと衝突する恐れがある

    • 複数のホストから安全に取り使うために

      • 物理的に異なるストレージへ複製が必要

限定されるストレージの種類

  • 物理的に異なるストレージ間でリアルタイムにデータを複製する安全な方法

    • GitそのものはMySQLやPostgreSQLといったRDBが持つレプリケーション機能は提供していない
       

  • 複製データの不整合を回避したい

    • 予期しないエラー発生時も、Gitリポジトリとしての整合性を保つ

    • 書き込みトランザクションの担保

スケールアウトの課題

ストレージ間のデータの複製と一貫性

  • パーティショニング

    • データを一定の集合で異なるストレージへ分割して負荷を分散させる
       

  • 適切なノードへのルーティング

    • 処理対象のリポジトリを持つノードを動的に解決してルーティングする仕組み

スケールアウトの課題

データを持つストレージ分割

Agenda

  • 概要

  • 取り扱うデータの特性

  • スケールアウトの課題

  • 複製と分散のアプローチ

  • Goで実装するgRPCの動的プロキシ

  • まとめ

基本的な方針

  • リポジトリをある一定の数の集合で分割して保存するストレージを分散する
    => 処理するノードの分母を増やす
     

  • 分割したストレージを複製する
    => 1リポジトリに対して処理できるノードを増やす
     

  • サーバーは状態を持つ者と持たない者で分割する
    => アプリケーションとRDBミドルウェアのような関係性

アプリケーションの分割

Git Smart HTTP (net/http)
  • httpでgit pullやgit pushする経路
  • Basic認証、アクセス制限
Git SSH (golang.org/x/crypto/ssh)
  • sshでgit pullやgit pushする経路
  • 公開鍵認証、アクセス制限
ステートレスなフロントエンド
Git Web (net/http)
  • リポジトリの情報をWebブラウザで表示するためのエンドポイントを提供する

アプリケーションの分割

Git Smart HTTP (net/http)
  • httpでgit pullやgit pushする経路
  • Basic認証、アクセス制限
Git SSH (golang.org/x/crypto/ssh)
  • sshでgit pullやgit pushする経路
  • 公開鍵認証、アクセス制限
ステートレスなフロントエンド
Git Web (net/http)
  • リポジトリの情報をWebブラウザで表示するためのエンドポイントを提供する

アプリケーションの分割

Git Smart HTTP (net/http)
  • httpでgit pullやgit pushする経路
  • Basic認証、アクセス制限
Git SSH (golang.org/x/crypto/ssh)
  • sshでgit pullやgit pushする経路
  • 公開鍵認証、アクセス制限
ステートレスなフロントエンド
Git Web (net/http)
  • リポジトリの情報をWebブラウザで表示するためのエンドポイントを提供する

アプリケーションの分割

ステートフルなバックエンド
Git RPC (google.golang.org/grpc)
  • リポジトリの読み書き
    • Gitリポジトリの読み書きに特化した
      データベースミドルウェア
  • gRPCで各種操作のRPCを提供

なぜgRPCで繋ぐのか

リクエストの特性に応じた通信方式の選択

  • 大容量データの読み込み

    • git clone, pull, blobのダウンロード

      • Server Streaming RPC

        • One-to-many

  • 大容量データの書き込み

    • git push

      • Client Streaming RPC

        • Meny-to-one

  • その他小さなデータのやり取り

    • commit, branch, tag等の情報取得

      • Unary RPC

        • One-to-one

ストレージの分割

リポジトリ単位で保存ストレージを分割
  • リポジトリ1〜3をストレージA
  • リポジトリ4〜6をストレージB
    
フロントからの通信を適切なノードへ中継
  • Git Dynamic Proxy

    • gRPCの動的プロキシ

    • データのセマンティクスは知らない

    • ヘッダ情報のみ読み取る

  • Metadata Store

    • リポジトリと保存先のノードの関連付けを永続化する外部永続機構

ストレージの分割

リポジトリ単位で保存ストレージを分割
  • リポジトリ1〜3をストレージA
  • リポジトリ4〜6をストレージB
フロントからの通信を適切なノードへ中継
  • Git Dynamic Proxy

    • gRPCの動的プロキシ

    • データのセマンティクスは知らない

    • ヘッダ情報のみ読み取る

  • Metadata Store

    • リポジトリと保存先のノードの関連付けを永続化する外部永続機構

ストレージの複製

ストレージ複製によるReaderノードの追加
  • リポジトリ1〜3をストレージa-2へ複製

  • リポジトリ4〜6をストレージb-2へ複製

ストレージの複製

イベント通知による非同期な複製

  • Replication Workerが複製イベントを受信

  • Readerノードの同期RPCを実行

Plane Gitコマンドを用いた安全なデータ同期

  • Writerのリポジトリへgit fetchを実行

  • git fetchのトランザクションにより予期しないエラー発生時もデータの一貫性を担保

  • 冪等なので複数回実行しても差分のみ処理

ストレージの複製

イベント通知による非同期な複製

  • Replication Workerが複製イベントを受信

  • Readerノードの同期RPCを実行

Plane Gitコマンドを用いた安全なデータ同期

  • Writerのリポジトリへgit fetchを実行

  • git fetchのトランザクションにより予期しないエラー発生時もデータの一貫性を担保

  • 冪等なので複数回実行しても差分のみ処理

書き込み処理のシーケンス

1. UserからのPush

2. 中継先のノードを選択

3. 適切なノードへ中継

4. Replication Logを発行

5. Userへ成功レスポンス

6. Replication Logを受信

7. 同期処理開始

8. Replication Logを削除

書き込み処理のシーケンス

1. UserからのPush

2. 中継先のノードを選択

3. 適切なノードへ中継

4. Replication Logを発行

5. Userへ成功レスポンス

6. Replication Logを受信

7. 同期処理開始

8. Replication Logを削除

書き込み処理のシーケンス

1. UserからのPush

2. 中継先のノードを選択

3. 適切なノードへ中継

4. Replication Logを発行

5. Userへ成功レスポンス

6. Replication Logを受信

7. 同期処理開始

8. Replication Logを削除

書き込み処理のシーケンス

1. UserからのPush

2. 中継先のノードを選択

3. 適切なノードへ中継

4. Replication Logを発行

5. Userへ成功レスポンス

6. Replication Logを受信

7. 同期処理開始

8. Replication Logを削除

書き込み処理のシーケンス

1. UserからのPush

2. 中継先のノードを選択

3. 適切なノードへ中継

4. Replication Logを発行

5. Userへ成功レスポンス

6. Replication Logを受信

7. 同期処理開始

8. Replication Logを削除

書き込み処理のシーケンス

1. UserからのPush

2. 中継先のノードを選択

3. 適切なノードへ中継

4. Replication Logを発行

5. Userへ成功レスポンス

6. Replication Logを受信

7. 同期処理開始

8. Replication Logを削除

書き込み処理のシーケンス

1. UserからのPush

2. 中継先のノードを選択

3. 適切なノードへ中継

4. Replication Logを発行

5. Userへ成功レスポンス

6. Replication Logを受信

7. 同期処理開始

8. Replication Logを削除

書き込み処理のシーケンス

1. UserからのPush

2. 中継先のノードを選択

3. 適切なノードへ中継

4. Replication Logを発行

5. Userへ成功レスポンス

6. Replication Logを受信

7. 同期処理開始

8. Replication Logを削除

Agenda

  • 概要

  • 取り扱うデータの特性

  • スケールアウトの課題

  • 複製と分散のアプローチ

  • Goで実装するgRPCの動的プロキシ

  • まとめ

gRPCをプロキシする主要なライブラリ

grpc/grpc-go
  • gRPCの公式のGo実装

  • プロキシサーバーの土台も公式の仕組みに乗っかる

mwitkow/grpc-proxy

  • 中継処理の中核となるGoの薄いライブラリ

  • L7のリバースプロキシとしての機能だけをgrpc-goのAPIに準拠して提供

  • プロキシ先を選択する処理を関数で柔軟に記述できる

  • 本ライブラリはあくまでProof Of Concept

    • 最新のgrpc-goのAPIに準拠するにはforkしてpatchをあてる必要あり

gRPCをプロキシする主要なライブラリ

mwitkow/grpc-proxy

  • 中継処理の中核となるGoの薄いライブラリ

  • L7のリバースプロキシとしての機能だけをgrpc-goのAPIに準拠して提供

  • プロキシ先を選択する処理を関数で柔軟に記述できる

  • 本ライブラリはあくまでProof Of Concept

    • 最新のgrpc-goのAPIに準拠するにはforkしてpatchをあてる必要あり

grpc/grpc-go
  • gRPCの公式のGo実装

  • プロキシサーバーの土台も公式の仕組みに乗っかる
プロキシサーバーの実装例

gRPC動的プロキシのコード解説

  • プロキシ用カスタムコーデックを登録する
    • ​プロキシするメッセージのエンコードとデコードに使用する

  • プロキシ用のgRPCのHandlerを作る
    
    • proxy.TransparentHandler

  • 未登録のRPCを処理するHandlerを作る
    
    • grpc.UnknownServiceHandle

  • gRPCサーバーにHandlerを登録して Listen And Serve!
import (
	"context"
	"net"

	"github.com/github.com/mwitkow/grpc-proxy"
	"google.golang.org/grpc"
	"google.golang.org/grpc/encoding"
	"google.golang.org/grpc/metadata"
)

func main() {
	encoding.RegisterCodec(proxy.NewCodec())
	transparent := proxy.TransparentHandler(director)
	unknown := grpc.UnknownServiceHandler(transparent)
	server := grpc.NewServer(unknown)
	lis, _ := net.Listen("tcp", ":50051")
	_ = server.Serve(lis)
}

var director = func(ctx context.Context, fullMethodName string) (
	context.Context, *grpc.ClientConn, error) {

	md, _ := metadata.FromIncomingContext(ctx)
	user := md.Get(":user")[0]
	repo := md.Get(":repo")[0]

	addr := getNodeAddr(fullMethodName, user, repo)

	conn, err := grpc.DialContext(ctx, addr,
		grpc.WithDefaultCallOptions(
			grpc.ForceCodec(proxy.NewCodec())),
		grpc.WithInsecure())

	return ctx, conn, err
}
プロキシサーバーの実装例

gRPC動的プロキシのコード解説

import (
	"context"
	"net"

	"github.com/github.com/mwitkow/grpc-proxy"
	"google.golang.org/grpc"
	"google.golang.org/grpc/encoding"
	"google.golang.org/grpc/metadata"
)

func main() {
	encoding.RegisterCodec(proxy.NewCodec())
	transparent := proxy.TransparentHandler(director)
	unknown := grpc.UnknownServiceHandler(transparent)
	server := grpc.NewServer(unknown)
	lis, _ := net.Listen("tcp", ":50051")
	_ = server.Serve(lis)
}

var director = func(ctx context.Context, fullMethodName string) (
	context.Context, *grpc.ClientConn, error) {

	md, _ := metadata.FromIncomingContext(ctx)
	user := md.Get(":user")[0]
	repo := md.Get(":repo")[0]

	addr := getNodeAddr(fullMethodName, user, repo)

	conn, err := grpc.DialContext(ctx, addr,
		grpc.WithDefaultCallOptions(
			grpc.ForceCodec(proxy.NewCodec())),
		grpc.WithInsecure())

	return ctx, conn, err
}
  • プロキシ用カスタムコーデックを登録する
    • ​プロキシするメッセージのエンコードとデコードに使用する

  • プロキシ用のgRPCのHandlerを作る
    
    • proxy.TransparentHandler

  • 未登録のRPCを処理するHandlerを作る
    
    • grpc.UnknownServiceHandle

  • gRPCサーバーにHandlerを登録して Listen And Serve!
プロキシサーバーの実装例

gRPC動的プロキシのコード解説

import (
	"context"
	"net"

	"github.com/github.com/mwitkow/grpc-proxy"
	"google.golang.org/grpc"
	"google.golang.org/grpc/encoding"
	"google.golang.org/grpc/metadata"
)

func main() {
	encoding.RegisterCodec(proxy.NewCodec())
	transparent := proxy.TransparentHandler(director)
	unknown := grpc.UnknownServiceHandler(transparent)
	server := grpc.NewServer(unknown)
	lis, _ := net.Listen("tcp", ":50051")
	_ = server.Serve(lis)
}

var director = func(ctx context.Context, fullMethodName string) (
	context.Context, *grpc.ClientConn, error) {

	md, _ := metadata.FromIncomingContext(ctx)
	user := md.Get(":user")[0]
	repo := md.Get(":repo")[0]

	addr := getNodeAddr(fullMethodName, user, repo)

	conn, err := grpc.DialContext(ctx, addr,
		grpc.WithDefaultCallOptions(
			grpc.ForceCodec(proxy.NewCodec())),
		grpc.WithInsecure())

	return ctx, conn, err
}
  • プロキシ用カスタムコーデックを登録する
    • ​プロキシするメッセージのエンコードとデコードに使用する

  • プロキシ用のgRPCのHandlerを作る
    
    • proxy.TransparentHandler

  • 未登録のRPCを処理するHandlerを作る
    
    • grpc.UnknownServiceHandle

  • gRPCサーバーにHandlerを登録して Listen And Serve!
プロキシサーバーの実装例

gRPC動的プロキシのコード解説

import (
	"context"
	"net"

	"github.com/github.com/mwitkow/grpc-proxy"
	"google.golang.org/grpc"
	"google.golang.org/grpc/encoding"
	"google.golang.org/grpc/metadata"
)

func main() {
	encoding.RegisterCodec(proxy.NewCodec())
	transparent := proxy.TransparentHandler(director)
	unknown := grpc.UnknownServiceHandler(transparent)
	server := grpc.NewServer(unknown)
	lis, _ := net.Listen("tcp", ":50051")
	_ = server.Serve(lis)
}

var director = func(ctx context.Context, fullMethodName string) (
	context.Context, *grpc.ClientConn, error) {

	md, _ := metadata.FromIncomingContext(ctx)
	user := md.Get(":user")[0]
	repo := md.Get(":repo")[0]

	addr := getNodeAddr(fullMethodName, user, repo)

	conn, err := grpc.DialContext(ctx, addr,
		grpc.WithDefaultCallOptions(
			grpc.ForceCodec(proxy.NewCodec())),
		grpc.WithInsecure())

	return ctx, conn, err
}
  • プロキシ用カスタムコーデックを登録する
    • ​プロキシするメッセージのエンコードとデコードに使用する

  • プロキシ用のgRPCのHandlerを作る
    
    • proxy.TransparentHandler

  • 未登録のRPCを処理するHandlerを作る
    
    • grpc.UnknownServiceHandle

  • gRPCサーバーにHandlerを登録して Listen And Serve!
プロキシサーバーの実装例

gRPC動的プロキシのコード解説

import (
	"context"
	"net"

	"github.com/github.com/mwitkow/grpc-proxy"
	"google.golang.org/grpc"
	"google.golang.org/grpc/encoding"
	"google.golang.org/grpc/metadata"
)

func main() {
	encoding.RegisterCodec(proxy.NewCodec())
	transparent := proxy.TransparentHandler(director)
	unknown := grpc.UnknownServiceHandler(transparent)
	server := grpc.NewServer(unknown)
	lis, _ := net.Listen("tcp", ":50051")
	_ = server.Serve(lis)
}

var director = func(ctx context.Context, fullMethodName string) (
	context.Context, *grpc.ClientConn, error) {

	md, _ := metadata.FromIncomingContext(ctx)
	user := md.Get(":user")[0]
	repo := md.Get(":repo")[0]

	addr := getNodeAddr(fullMethodName, user, repo)

	conn, err := grpc.DialContext(ctx, addr,
		grpc.WithDefaultCallOptions(
			grpc.ForceCodec(proxy.NewCodec())),
		grpc.WithInsecure())

	return ctx, conn, err
}
  • プロキシ用カスタムコーデックを登録する
    • ​プロキシするメッセージのエンコードとデコードに使用する

  • プロキシ用のgRPCのHandlerを作る
    
    • proxy.TransparentHandler

  • 未登録のRPCを処理するHandlerを作る
    
    • grpc.UnknownServiceHandle

  • gRPCサーバーにHandlerを登録して Listen And Serve!

gRPC動的プロキシのコード解説

プロキシ先を決定するdirector関数の例
  • fullMethodName?
    • gRPCが内部で持つユニークなパス
      • 例. /foo.BarService/baz
        
        
  • メタデータからプロキシ先のノードを決定するための情報を取得する
    • user, repo
      
      
  • fullMethodName, user, repo を元にプロキシ先のノードのアドレスを取得する
    
    
  • プロキシ先ノードへのコネクションを生成する
import (
	"context"
	"net"

	"github.com/github.com/mwitkow/grpc-proxy"
	"google.golang.org/grpc"
	"google.golang.org/grpc/encoding"
	"google.golang.org/grpc/metadata"
)

func main() {
	encoding.RegisterCodec(proxy.NewCodec())
	transparent := proxy.TransparentHandler(director)
	unknown := grpc.UnknownServiceHandler(transparent)
	server := grpc.NewServer(unknown)
	lis, _ := net.Listen("tcp", ":50051")
	_ = server.Serve(lis)
}

var director = func(ctx context.Context, fullMethodName string) (
	context.Context, *grpc.ClientConn, error) {

	md, _ := metadata.FromIncomingContext(ctx)
	user := md.Get(":user")[0]
	repo := md.Get(":repo")[0]

	addr := getNodeAddr(fullMethodName, user, repo)

	conn, err := grpc.DialContext(ctx, addr,
		grpc.WithDefaultCallOptions(
			grpc.ForceCodec(proxy.NewCodec())),
		grpc.WithInsecure())

	return ctx, conn, err
}

gRPC動的プロキシのコード解説

proxy.TransparentHandlerの実装​

  • ServerStreamからfullMethodNameを取得する
     

  • director関数でプロキシ先ノードのコネクションを取得する
     

  • コネクションを元にClientStreamを生成する

// grpc-proxy/proxy/handler.go

var (
	clientStreamDescForProxying = &grpc.StreamDesc{
		ServerStreams: true,
		ClientStreams: true,
	}
)

func (s *handler) handler(srv interface{},
	serverStream grpc.ServerStream) error {

	fullMethodName, err := grpc.MethodFromServerStream(
		serverStream)
        
	outgoingCtx, backendConn, err := s.director(
		serverStream.Context(), fullMethodName)

	clientCtx, clientCancel := context.WithCancel(
		outgoingCtx)
        
	clientStream, err := grpc.NewClientStream(clientCtx,
		clientStreamDescForProxying, backendConn,
		fullMethodName)

	// ...
   

※1 ServerStream:

フロントエンドとプロキシサーバーの双方向ストリーム

※2 ClientStream: 

プロキシサーバーとバックエンドの双方向ストリーム

gRPC動的プロキシのコード解説

  • ServerStreamとClientStreamの双方向フォワーディング
    • forwardServerToClient
      • フロントエンドのアプリが送信するデータをバックエンドへ
    • forwardClientToServer
      • バックエンドのアプリが送信するデータをフロントエンドへ
        
// grpc-proxy/proxy/handler.go

	// ...
    
	s2cErrChan := s.forwardServerToClient(
		serverStream, clientStream)
	
	c2sErrChan := s.forwardClientToServer(
		clientStream, serverStream)
    
	for i := 0; i < 2; i++ {
		select {
		case s2cErr := <-s2cErrChan:
			if s2cErr == io.EOF {
				clientStream.CloseSend()
				break
			} else {
				clientCancel()
				return status.Errorf(
					codes.Internal,
					"failed proxying s2c: %v",
					s2cErr)
			}
		case c2sErr := <-c2sErrChan:
			serverStream.SetTrailer(
				clientStream.Trailer())
			if c2sErr != io.EOF {
				return c2sErr
			}
			return nil
		}
	}
	return status.Errorf(codes.Internal,
		"gRPC proxying " +
		"should never reach this stage.")
}
  • 非同期で処理され結果はchannelを介して返される
    
    
  • 2つのchannelをselectで待受け
    • ​io.EOFが返れば正常終了

gRPC動的プロキシのコード解説

  • ServerStreamとClientStreamの双方向フォワーディング
    • forwardServerToClient
      • フロントエンドのアプリが送信するデータをバックエンドへ
    • forwardClientToServer
      • バックエンドのアプリが送信するデータをフロントエンドへ
        
// grpc-proxy/proxy/handler.go

	// ...
    
	s2cErrChan := s.forwardServerToClient(
		serverStream, clientStream)
	
	c2sErrChan := s.forwardClientToServer(
		clientStream, serverStream)
    
	for i := 0; i < 2; i++ {
		select {
		case s2cErr := <-s2cErrChan:
			if s2cErr == io.EOF {
				clientStream.CloseSend()
				break
			} else {
				clientCancel()
				return status.Errorf(
					codes.Internal,
					"failed proxying s2c: %v",
					s2cErr)
			}
		case c2sErr := <-c2sErrChan:
			serverStream.SetTrailer(
				clientStream.Trailer())
			if c2sErr != io.EOF {
				return c2sErr
			}
			return nil
		}
	}
	return status.Errorf(codes.Internal,
		"gRPC proxying " +
		"should never reach this stage.")
}
  • 非同期で処理され結果はchannelを介して返される
    
    
  • 2つのchannelをselectで待受け
    • ​io.EOFが返れば正常終了
// grpc-proxy/proxy/handler.go

type frame struct {
	payload []byte
}

func (s *handler) forwardServerToClient(src grpc.ServerStream,
	dst grpc.ClientStream) chan error {

	ret := make(chan error, 1)
	go func() {
		f := &frame{}
		for i := 0; ; i++ {
			if err := src.RecvMsg(f); err != nil {
				ret <- err
				break
			}

			if err := dst.SendMsg(f); err != nil {
				ret <- err
				break
			}
		}
	}()
	return ret
}

func (s *handler) forwardClientToServer(src grpc.ClientStream,
	dst grpc.ServerStream) chan error {

	// ...
}

gRPC動的プロキシのコード解説

中継処理の実装

  • ServerToClientとClientToServerも基本的な処理は同じ
     

  • srcのStreamから受信したメッセージをdetのStreamに送信する
     

  • frameとして定義されたバイト配列を持つ構造体へ受信したメッセージをデコードする

Agenda

  • 概要

  • 取り扱うデータの特性

  • スケールアウトの課題

  • 複製と分散のアプローチ

  • Goで実装するgRPCの動的プロキシ

  • まとめ

まとめ

  • Gitホスティングをスケールアウトするために
    • ブロックストレージに保持するリポジトリの複製と分散を検討
       

  • 手段を​概念実証としてGoとgRPCを用いて実装した

  • 状態の有無でアプリケーションサーバを分割

    • ステートレスなフロントエンド

    • ステートフルなバックエンド

    • フロントエンドとバックエンドを繋ぐプロキシ

  • バックエンドのWriterとReaderでクラスタを構成した

    • WriterとReaderの非同期レプリケーション​​

まとめ

  • Gitのリクエストの特性(データの量)に応じたgRPCのStreaming RPCの活用
    • Server Streaming RPC (One-to-many)

    • Client Streaming RPC (Meny-to-one)

    • Unary RPC (One-to-one)
       

  • grpc-goとgrpc-proxyで動的プロキシ実現した
    • Goの機能やエコシステムをフルに使って動的ルーティングを記述できる
    • 必要なもの(grpcの動的proxy)だけを小さく提供できる
    • grpc-proxy自体も薄くて中の挙動を理解しやすい
GoConSendai> git commit -m "ご静聴ありがとうございましたʕ◔ϖ◔ʔ"

Gitホスティングにおける複製と分散のアプローチ

By Yuichi Watanabe

Gitホスティングにおける複製と分散のアプローチ

  • 2,823