Yuichi Watanabe at Nulab Inc.
BacklogにおけるGoの活用事例
me := Profile{
Name: "Yuichi Watanabe",
Org: "Nulab Inc.",
Job: "SRE/Backend",
Twitter: "vvvatanabe",
GitHub: "vvatanabe",
Lives: "Fukuoka",
}
一枚岩なScala製のアプリが大黒柱
機能によって小規模なアプリが複数
特にGitホスティング周りでGoを多用
net/http
grpc/grpc-go
x/crypt/ssh
cgo
先月Go1.16へアップデート完了:beer:
Gitホスティング in Go
詳細は以下のスライドへ
補助ツール in GO
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ホスティングの各サービスを単一言語で構成したい
今までの知見を活かす
監視、デバッグ、開発Tips...
OSSで公開されているGit LFSコマンドがGoで実装されている
ソースコードレベルで挙動を把握しやすい
コンテナで稼働させやすい
ベースイメージにバイナリを含めるだけ
省メモリ、効率的なCPUの使用
簡潔な並列処理で大きなファイルの効率よく捌きたい
goroutine、channel、syncパッケージ
実行環境:
Amazon EKS
ファイルの保存先:
Amazon S3 (実体)
Amazon Aurora (メタ情報)
監視:
Prometeus
Grafana
Mackerel
※その他のEKS上で稼働しているアプリは省略
送受信するデータをメモリを一度に展開するとリソースの枯渇に繋がる
// 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をこのオブジェクトプールに持たせる
ファイルの実体は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()
// ... 省略
}
設定値を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()
// ... 省略
}
設定値を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()
// ... 省略
}
設定値を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()
}
}
設定値を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()
// ... 省略
}
設定値を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()
// ... 省略
}
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()
// ... 省略
}
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()
// ... 省略
}
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()
// ... 省略
}
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()
// ... 省略
}
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())
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の「Gitホスティング」はGoに支えられている
Gitの「大きなファイル苦手」問題の緩和策としてGit LFS サーバーをGoで実現した
Git LFSコマンド自体もGoで開発されている
Go標準の強力な標準パッケージで大きなファイルも十分効率的に取り扱える
aws-sdk-goのマルチアップロードの仕組みにgoroutineとchannelの理解が深まった
各種ミドルウェアの手厚いサポートでGoプロセスの状態を監視・可視化が捗った
Prometeusの公式Goエクスポーター
GrafanaのGoダッシュボードテンプレート
ヌーラボやBacklogの開発に興味を持っていただいた方!
是非お話ししてみませんか?
shuuu-mai> git commit -m "ご静聴ありがとうございましたʕ◔ϖ◔ʔ"