Git LFS in Go
Yuichi Watanabe at Nulab Inc.
BacklogにおけるGoの活用事例
Who Am I?
me := Profile{
Name: "Yuichi Watanabe",
Org: "Nulab Inc.",
Job: "SRE/Backend",
Twitter: "vvvatanabe",
GitHub: "vvatanabe",
Lives: "Fukuoka",
}
Agenda
-
BacklogにおけるGoの活用事例
-
Git LFSとは
-
Goを採用したモチベーション
-
アーキテクチャ概観
-
開発Tips
-
まとめ
BacklogにおけるGoの活用例
-
一枚岩なScala製のアプリが大黒柱
-
機能によって小規模なアプリが複数
-
特にGitホスティング周りでGoを多用
-
net/http
-
grpc/grpc-go
-
x/crypt/ssh
-
cgo
-
-
先月Go1.16へアップデート完了:beer:
Gitホスティング in Go
詳細は以下のスライドへ
- BacklogのGitを使った開発を補助するGo製のツールも非公式で提供
- Backlogの「Gitホスティング」を開発する中で実際に使用
補助ツール in GO
BacklogにおけるGoの活用例
Agenda
-
BacklogにおけるGoの活用例
-
Git LFSとは
-
Goを採用したモチベーション
-
アーキテクチャ概観
-
開発Tips
-
まとめ
Git LFSとは
-
Git Large File Storageの略
-
Gitの拡張機能 (Git LFSコマンド)
Gitが苦手な大きなファイルを効率的に取り扱うための拡張
特徴
-
大きなファイルを必要な分だけダウンロード
-
ワークツリーに展開する時にそのバージョンで必要なファイルだけダウンロード
-
-
大きなファイルの実体をリポジトリの外で管理
-
リポジトリにはポインタファイルのみ
-
実体は任意のストレージへ分けて管理
-
Git LFSの仕組み
-
Git LFSコマンドとGit LFSサーバーがHTTPで通信する
-
Git LFSコマンドはGitのフィルター機能とGitフックを使って適切な処理を挟む
-
clean、smudgeフィルター
-
pre-pushフック
-
-
Git LFSサーバーは規約に従いAPIを提供する
-
Batch API
-
Basic Transfer API
-
Locking API [Option]
-
-
上記のGit LFSサーバーをGoで実装した
Git LFSとは
Agenda
-
BacklogにおけるGoの活用例
-
Git LFSとは
-
Goを採用したモチベーション
-
アーキテクチャ概観
-
開発Tips
-
まとめ
Goを採用したモチベーション
-
Gitホスティングの各サービスを単一言語で構成したい
-
今までの知見を活かす
-
監視、デバッグ、開発Tips...
-
-
-
OSSで公開されているGit LFSコマンドがGoで実装されている
-
ソースコードレベルで挙動を把握しやすい
-
-
コンテナで稼働させやすい
-
ベースイメージにバイナリを含めるだけ
-
省メモリ、効率的なCPUの使用
-
-
簡潔な並列処理で大きなファイルの効率よく捌きたい
-
goroutine、channel、syncパッケージ
-
Agenda
-
BacklogにおけるGoの活用例
-
Git LFSとは
-
Goを採用したモチベーション
-
アーキテクチャ概観
-
開発Tips
-
まとめ
アーキテクチャ概観
-
実行環境:
-
Amazon EKS
-
-
ファイルの保存先:
-
Amazon S3 (実体)
-
Amazon Aurora (メタ情報)
-
-
監視:
-
Prometeus
-
Grafana
-
Mackerel
-
※その他のEKS上で稼働しているアプリは省略
Agenda
-
BacklogにおけるGoの活用例
-
Git LFSとは
-
Goを採用したモチベーション
-
アーキテクチャ概観
-
開発Tips
-
まとめ
- Git LFSサーバーは大きなファイルの取り扱いが前提
-
送受信するデータをメモリを一度に展開するとリソースの枯渇に繋がる
io.CopyBufferとsync.Poolを使ったデータ転送の効率化
// global vars
var bufPool = sync.Pool{
New: func() interface{} {
return make([]byte, size)
},
}
func main() {
// プールになければ新規生成される
buf := bufPool.Get().([]byte)
// 取得したオブジェクトはdeferで確実にプールへ返却する
defer bufPool.Put(bufIn)
// srcからbufのサイズずつ読み取りdstに書き込む
size, err := io.CopyBuffer(dst, src, buf)
//...
}
-
「コピー元」src から「バッファ」buf のサイズずつ読み取り「コピー先」dst に書き込む
-
src を一度に展開しないのでアロケーションを抑えられメモリ効率が良い
-
buf のサイズをチューニングする余地もある
io.CopyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error)
sync.Pool
-
スレッドセーフなオブジェクトプール
-
フィールドのNewにオブジェクトを生成する関数を定義する
-
Getメソッド呼び出し時にオブジェクトがNewで生成される
-
-
io.CopyBufferへ渡すbufをこのオブジェクトプールに持たせる
マルチアップロード by aws-sdk-go
-
ファイルの実体はS3に保持しているためaws-sdk-goを使用している
-
aws-sdk-goには大きなファイルを効率的に転送する仕組みがある
-
転送するファイルを分割してマルチアップロードする仕組みを追ってみる
func (u *multiuploader) upload(
firstBuf io.ReadSeeker,
cleanup func()) (*UploadOutput, error) {
// ... 省略
ch := make(chan chunk, u.cfg.Concurrency)
for i := 0; i < u.cfg.Concurrency; i++ {
u.wg.Add(1) // sync.WaitGroup
go u.readChunk(ch)
}
// ... 省略
for u.geterr() == nil && err == nil {
var (
reader io.ReadSeeker
nextChunkLen int
ok bool
)
reader, nextChunkLen, cleanup, err = u.nextReader()
ok, err = u.shouldContinue(num, nextChunkLen, err)
if !ok {
cleanup()
if err != nil {
u.seterr(err)
}
break
}
num++
ch <- chunk{buf: reader, num: num, cleanup: cleanup}
}
close(ch)
u.wg.Wait()
// ... 省略
}
マルチアップロード by aws-sdk-go
-
設定値をcapacityにしたchannelを作る
-
設定値の数だけgoroutineを作る
-
goroutineはreadChunk()にchannelを渡す
-
channel経由でchunkが伝播届くのを待つ
-
chunkが届いたらsendする
-
func (u *multiuploader) upload(
firstBuf io.ReadSeeker,
cleanup func()) (*UploadOutput, error) {
// ... 省略
ch := make(chan chunk, u.cfg.Concurrency)
for i := 0; i < u.cfg.Concurrency; i++ {
u.wg.Add(1) // sync.WaitGroup
go u.readChunk(ch)
}
// ... 省略
for u.geterr() == nil && err == nil {
var (
reader io.ReadSeeker
nextChunkLen int
ok bool
)
reader, nextChunkLen, cleanup, err = u.nextReader()
ok, err = u.shouldContinue(num, nextChunkLen, err)
if !ok {
cleanup()
if err != nil {
u.seterr(err)
}
break
}
num++
ch <- chunk{buf: reader, num: num, cleanup: cleanup}
}
close(ch)
u.wg.Wait()
// ... 省略
}
マルチアップロード by aws-sdk-go
-
設定値をcapacityにしたchannelを作る
-
設定値の数だけgoroutineを作る
-
goroutineはreadChunk()にchannelを渡す
-
channel経由でchunkが伝播届くのを待つ
-
chunkが届いたらsendする
-
func (u *multiuploader) upload(
firstBuf io.ReadSeeker,
cleanup func()) (*UploadOutput, error) {
// ... 省略
ch := make(chan chunk, u.cfg.Concurrency)
for i := 0; i < u.cfg.Concurrency; i++ {
u.wg.Add(1) // sync.WaitGroup
go u.readChunk(ch)
}
// ... 省略
for u.geterr() == nil && err == nil {
var (
reader io.ReadSeeker
nextChunkLen int
ok bool
)
reader, nextChunkLen, cleanup, err = u.nextReader()
ok, err = u.shouldContinue(num, nextChunkLen, err)
if !ok {
cleanup()
if err != nil {
u.seterr(err)
}
break
}
num++
ch <- chunk{buf: reader, num: num, cleanup: cleanup}
}
close(ch)
u.wg.Wait()
// ... 省略
}
マルチアップロード by aws-sdk-go
-
設定値をcapacityにしたchannelを作る
-
設定値の数だけgoroutineを作る
-
goroutineはreadChunk()にchannelを渡す
-
channel経由でchunkが伝播届くのを待つ
-
chunkが届いたらsendする
-
func (u *multiuploader) upload(
firstBuf io.ReadSeeker,
cleanup func()) (*UploadOutput, error) {
// ... 省略
ch := make(chan chunk, u.cfg.Concurrency)
for i := 0; i < u.cfg.Concurrency; i++ {
u.wg.Add(1) // sync.WaitGroup
go u.readChunk(ch)
}
// ... 省略
for u.geterr() == nil && err == nil {
var (
reader io.ReadSeeker
nextChunkLen int
ok bool
)
reader, nextChunkLen, cleanup, err = u.nextReader()
ok, err = u.shouldContinue(num, nextChunkLen, err)
if !ok {
cleanup()
if err != nil {
u.seterr(err)
}
break
}
num++
ch <- chunk{buf: reader, num: num, cleanup: cleanup}
}
close(ch)
u.wg.Wait()
// ... 省略
}
func (u *multiuploader) readChunk(ch chan chunk) {
defer u.wg.Done()
for {
data, ok := <-ch
if !ok {
break
}
if u.geterr() == nil {
if err := u.send(data); err != nil {
u.seterr(err)
}
}
data.cleanup()
}
}
マルチアップロード by aws-sdk-go
-
設定値をcapacityにしたchannelを作る
-
設定値の数だけgoroutineを作る
-
goroutineはreadChunk()にchannelを渡す
-
channel経由でchunkが伝播届くのを待つ
-
chunkが届いたらsendする
-
func (u *multiuploader) readChunk(ch chan chunk) {
defer u.wg.Done()
for {
data, ok := <-ch
if !ok {
break
}
if u.geterr() == nil {
if err := u.send(data); err != nil {
u.seterr(err)
}
}
data.cleanup()
}
}
func (u *multiuploader) upload(
firstBuf io.ReadSeeker,
cleanup func()) (*UploadOutput, error) {
// ... 省略
ch := make(chan chunk, u.cfg.Concurrency)
for i := 0; i < u.cfg.Concurrency; i++ {
u.wg.Add(1) // sync.WaitGroup
go u.readChunk(ch)
}
// ... 省略
for u.geterr() == nil && err == nil {
var (
reader io.ReadSeeker
nextChunkLen int
ok bool
)
reader, nextChunkLen, cleanup, err = u.nextReader()
ok, err = u.shouldContinue(num, nextChunkLen, err)
if !ok {
cleanup()
if err != nil {
u.seterr(err)
}
break
}
num++
ch <- chunk{buf: reader, num: num, cleanup: cleanup}
}
close(ch)
u.wg.Wait()
// ... 省略
}
マルチアップロード by aws-sdk-go
-
設定値をcapacityにしたchannelを作る
-
設定値の数だけgoroutineを作る
-
goroutineはreadChunk()にchannelを渡す
-
channel経由でchunkが伝播届くのを待つ
-
chunkが届いたらsendする
-
func (u *multiuploader) readChunk(ch chan chunk) {
defer u.wg.Done()
for {
data, ok := <-ch
if !ok {
break
}
if u.geterr() == nil {
if err := u.send(data); err != nil {
u.seterr(err)
}
}
data.cleanup()
}
}
func (u *multiuploader) upload(
firstBuf io.ReadSeeker,
cleanup func()) (*UploadOutput, error) {
// ... 省略
ch := make(chan chunk, u.cfg.Concurrency)
for i := 0; i < u.cfg.Concurrency; i++ {
u.wg.Add(1) // sync.WaitGroup
go u.readChunk(ch)
}
// ... 省略
for u.geterr() == nil && err == nil {
var (
reader io.ReadSeeker
nextChunkLen int
ok bool
)
reader, nextChunkLen, cleanup, err = u.nextReader()
ok, err = u.shouldContinue(num, nextChunkLen, err)
if !ok {
cleanup()
if err != nil {
u.seterr(err)
}
break
}
num++
ch <- chunk{buf: reader, num: num, cleanup: cleanup}
}
close(ch)
u.wg.Wait()
// ... 省略
}
マルチアップロード by aws-sdk-go
-
forループ内で送信するデータを読み続ける
-
nextReaderでreq.Bodyを分割して取得
-
shouldContinueで処理を継続するか判定
-
分割取得したデータをchunkとしてchannel経由で送信用のgoroutineに渡す
-
-
データの分割読み取りが完了したらループを抜ける
-
channelをcloseする
-
読み取り先のgoroutineに読み取り終了を通知
-
-
sync.WaitGroupのWait()を実行する
-
データ転送中の全てのgoroutineの終了を待つ
-
func (u *multiuploader) upload(
firstBuf io.ReadSeeker,
cleanup func()) (*UploadOutput, error) {
// ... 省略
ch := make(chan chunk, u.cfg.Concurrency)
for i := 0; i < u.cfg.Concurrency; i++ {
u.wg.Add(1) // sync.WaitGroup
go u.readChunk(ch)
}
// ... 省略
for u.geterr() == nil && err == nil {
var (
reader io.ReadSeeker
nextChunkLen int
ok bool
)
reader, nextChunkLen, cleanup, err = u.nextReader()
ok, err = u.shouldContinue(num, nextChunkLen, err)
if !ok {
cleanup()
if err != nil {
u.seterr(err)
}
break
}
num++
ch <- chunk{buf: reader, num: num, cleanup: cleanup}
}
close(ch)
u.wg.Wait()
// ... 省略
}
マルチアップロード by aws-sdk-go
-
forループ内で送信するデータを読み続ける
-
nextReaderでreq.Bodyを分割して取得
-
shouldContinueで処理を継続するか判定
-
分割取得したデータをchunkとしてchannel経由で送信用のgoroutineに渡す
-
-
データの分割読み取りが完了したらループを抜ける
-
channelをcloseする
-
読み取り先のgoroutineに読み取り終了を通知
-
-
sync.WaitGroupのWait()を実行する
-
データ転送中の全てのgoroutineの終了を待つ
-
func (u *multiuploader) upload(
firstBuf io.ReadSeeker,
cleanup func()) (*UploadOutput, error) {
// ... 省略
ch := make(chan chunk, u.cfg.Concurrency)
for i := 0; i < u.cfg.Concurrency; i++ {
u.wg.Add(1) // sync.WaitGroup
go u.readChunk(ch)
}
// ... 省略
for u.geterr() == nil && err == nil {
var (
reader io.ReadSeeker
nextChunkLen int
ok bool
)
reader, nextChunkLen, cleanup, err = u.nextReader()
ok, err = u.shouldContinue(num, nextChunkLen, err)
if !ok {
cleanup()
if err != nil {
u.seterr(err)
}
break
}
num++
ch <- chunk{buf: reader, num: num, cleanup: cleanup}
}
close(ch)
u.wg.Wait()
// ... 省略
}
マルチアップロード by aws-sdk-go
-
forループ内で送信するデータを読み続ける
-
nextReaderでreq.Bodyを分割して取得
-
shouldContinueで処理を継続するか判定
-
分割取得したデータをchunkとしてchannel経由で送信用のgoroutineに渡す
-
-
データの分割読み取りが完了したらループを抜ける
-
channelをcloseする
-
読み取り先のgoroutineに読み取り終了を通知
-
-
sync.WaitGroupのWait()を実行する
-
データ転送中の全てのgoroutineの終了を待つ
-
func (u *multiuploader) upload(
firstBuf io.ReadSeeker,
cleanup func()) (*UploadOutput, error) {
// ... 省略
ch := make(chan chunk, u.cfg.Concurrency)
for i := 0; i < u.cfg.Concurrency; i++ {
u.wg.Add(1) // sync.WaitGroup
go u.readChunk(ch)
}
// ... 省略
for u.geterr() == nil && err == nil {
var (
reader io.ReadSeeker
nextChunkLen int
ok bool
)
reader, nextChunkLen, cleanup, err = u.nextReader()
ok, err = u.shouldContinue(num, nextChunkLen, err)
if !ok {
cleanup()
if err != nil {
u.seterr(err)
}
break
}
num++
ch <- chunk{buf: reader, num: num, cleanup: cleanup}
}
close(ch)
u.wg.Wait()
// ... 省略
}
マルチアップロード by aws-sdk-go
-
forループ内で送信するデータを読み続ける
-
nextReaderでreq.Bodyを分割して取得
-
shouldContinueで処理を継続するか判定
-
分割取得したデータをchunkに包み、channel経由で送信用のgoroutineに渡す
-
-
データの分割読み取りが完了したらループを抜ける
-
channelをcloseする
-
読み取り先のgoroutineに読み取り終了を通知
-
-
sync.WaitGroupのWait()を実行する
-
データ転送中の全てのgoroutineの終了を待つ
-
func (u *multiuploader) upload(
firstBuf io.ReadSeeker,
cleanup func()) (*UploadOutput, error) {
// ... 省略
ch := make(chan chunk, u.cfg.Concurrency)
for i := 0; i < u.cfg.Concurrency; i++ {
u.wg.Add(1) // sync.WaitGroup
go u.readChunk(ch)
}
// ... 省略
for u.geterr() == nil && err == nil {
var (
reader io.ReadSeeker
nextChunkLen int
ok bool
)
reader, nextChunkLen, cleanup, err = u.nextReader()
ok, err = u.shouldContinue(num, nextChunkLen, err)
if !ok {
cleanup()
if err != nil {
u.seterr(err)
}
break
}
num++
ch <- chunk{buf: reader, num: num, cleanup: cleanup}
}
close(ch)
u.wg.Wait()
// ... 省略
}
マルチアップロード by aws-sdk-go
-
forループ内で送信するデータを読み続ける
-
nextReaderでreq.Bodyを分割して取得
-
shouldContinueで処理を継続するか判定
-
分割取得したデータをchunkに包み、channel経由で送信用のgoroutineに渡す
-
-
データの分割読み取りが完了したらループを抜ける
-
channelをcloseする
-
読み取り先のgoroutineに読み取り終了を通知
-
-
sync.WaitGroupのWait()を実行する
-
データ転送中の全てのgoroutineの終了を待つ
-
func (u *multiuploader) upload(
firstBuf io.ReadSeeker,
cleanup func()) (*UploadOutput, error) {
// ... 省略
ch := make(chan chunk, u.cfg.Concurrency)
for i := 0; i < u.cfg.Concurrency; i++ {
u.wg.Add(1) // sync.WaitGroup
go u.readChunk(ch)
}
// ... 省略
for u.geterr() == nil && err == nil {
var (
reader io.ReadSeeker
nextChunkLen int
ok bool
)
reader, nextChunkLen, cleanup, err = u.nextReader()
ok, err = u.shouldContinue(num, nextChunkLen, err)
if !ok {
cleanup()
if err != nil {
u.seterr(err)
}
break
}
num++
ch <- chunk{buf: reader, num: num, cleanup: cleanup}
}
close(ch)
u.wg.Wait()
// ... 省略
}
再起動時の瞬断をできるだけ避ける
-
大きなファイルの処理が多いので時間がかかりやすい
-
リリースでコンテナを入れ替える時の切断を減らしたい
-
古いコンテナの処理をできるだけ継続させる
-
Pod停止時のTERMシグナルを起点にGraceful Shutdownする
-
-
// go versio >= 1.16
srv := &http.Server{
// ..
}
go srv.Serve(ln)
// got 1.16から追加された
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM)
<-ctx.Done()
err = srv.Shutdown(context.Background())
// go version < 1.16
srv := &http.Server{
// ..
}
go srv.Serve(ln)
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGTERM)
<-sigCh
err = srv.Shutdown(context.Background())
Goプロセスの監視
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/gorilla/mux"
)
r := mux.NewRouter()
r.Path("/metrics").
Methods(http.MethodGet).Handler(promhttp.Handler())
prometheus.MustRegister(&DBStatsCollector{DB: db})
-
PrometheusでGoプロセスの状態を取得
-
Prometheusのカスタムエクスポーターを作ってsql.DBの状態を取得
-
GrafanaでPrometheusのデータソースを可視化
-
ダッシュボードはGoのテンプレを元に
-
-
BacklogにおけるGoの活用例
-
Git LFSとは
-
Goを採用したモチベーション
-
アーキテクチャ概観
-
開発Tips
-
まとめ
Agenda
まとめ
-
Backlogの「Gitホスティング」はGoに支えられている
-
Gitの「大きなファイル苦手」問題の緩和策としてGit LFS サーバーをGoで実現した
-
Git LFSコマンド自体もGoで開発されている
-
Go標準の強力な標準パッケージで大きなファイルも十分効率的に取り扱える
-
aws-sdk-goのマルチアップロードの仕組みにgoroutineとchannelの理解が深まった
-
各種ミドルウェアの手厚いサポートでGoプロセスの状態を監視・可視化が捗った
-
Prometeusの公式Goエクスポーター
-
GrafanaのGoダッシュボードテンプレート
-
WE ARE HIRING
ヌーラボやBacklogの開発に興味を持っていただいた方!
是非お話ししてみませんか?
shuuu-mai> git commit -m "ご静聴ありがとうございましたʕ◔ϖ◔ʔ"
Git LFS in Go
By Yuichi Watanabe
Git LFS in Go
- 1,219