Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
ホットリロードツールの作り方
Search
MakKi
June 13, 2021
Programming
0
1k
ホットリロードツールの作り方
Go Conference Online 2021 Spring
MakKi
June 13, 2021
Tweet
Share
More Decks by MakKi
See All by MakKi
標準ライブラリの動向とイテレータのパフォーマンス
makki_d
3
520
range over funcのエラー処理
makki_d
1
1.4k
GoとテストとインプロセスDB
makki_d
3
490
君は古の言語M4を知っているか (LT)
makki_d
0
320
型パラメータが使えるようになったのでLINQを実装してみた
makki_d
2
1.3k
mallocしただけでメモリが確保できるって本当ですか?
makki_d
0
190
JavaプログラムをGoに移植するためのテクニック――継承と例外
makki_d
1
1.6k
JavaプログラムをGoに移植するためのテクニック――継承と例外
makki_d
4
4k
mallocしただけでメモリが確保できるって本当ですか?
makki_d
4
680
Other Decks in Programming
See All in Programming
初めてDefinitelyTypedにPRを出した話
syumai
0
420
3 Effective Rules for Using Signals in Angular
manfredsteyer
PRO
0
100
「今のプロジェクトいろいろ大変なんですよ、app/services とかもあって……」/After Kaigi on Rails 2024 LT Night
junk0612
5
2.2k
エンジニアとして関わる要件と仕様(公開用)
murabayashi
0
300
ECS Service Connectのこれまでのアップデートと今後のRoadmapを見てみる
tkikuc
2
250
Quine, Polyglot, 良いコード
qnighy
4
640
Contemporary Test Cases
maaretp
0
140
Jakarta EE meets AI
ivargrimstad
0
110
Click-free releases & the making of a CLI app
oheyadam
2
120
とにかくAWS GameDay!AWSは世界の共通言語! / Anyway, AWS GameDay! AWS is the world's lingua franca!
seike460
PRO
1
880
Enabling DevOps and Team Topologies Through Architecture: Architecting for Fast Flow
cer
PRO
0
330
watsonx.ai Dojo #4 生成AIを使ったアプリ開発、応用編
oniak3ibm
PRO
1
140
Featured
See All Featured
Understanding Cognitive Biases in Performance Measurement
bluesmoon
26
1.4k
Building a Modern Day E-commerce SEO Strategy
aleyda
38
6.9k
Build your cross-platform service in a week with App Engine
jlugia
229
18k
Fontdeck: Realign not Redesign
paulrobertlloyd
82
5.2k
CoffeeScript is Beautiful & I Never Want to Write Plain JavaScript Again
sstephenson
159
15k
A Tale of Four Properties
chriscoyier
156
23k
The Cult of Friendly URLs
andyhume
78
6k
Teambox: Starting and Learning
jrom
133
8.8k
RailsConf 2023
tenderlove
29
900
Learning to Love Humans: Emotional Interface Design
aarron
273
40k
Save Time (by Creating Custom Rails Generators)
garrettdimon
PRO
27
840
Designing for Performance
lara
604
68k
Transcript
ホットリロードツールの作り方 Go Conference 2021 Spring 牧内大輔 (MakKi, @makki_d)
この発表について 発表の内容 • ホットリロードツールとは ◦ どのような場面で利用するのか ◦ Goのホットリロードツールについて • ホットリロードツールの作り方
◦ ファイル変更の監視方法 ◦ プロセスの再起動方法 この発表のゴール • ホットリロードツールを自作できるようになること
自己紹介 • 牧内大輔 ◦ MakKi ◦ twitter: @makki_d ◦ github:
makiuchi-d • KLab株式会社 ◦ スマホゲームつくってます • 過去の発表 ◦ JavaプログラムをGoに移植するためのテクニック ――継承と例外 ▪ Go Conference 2019 Spring ▪ Go Conference'19 Summer in Fukuoka
KLabでのGo言語の利用場面 • 常時接続型のゲームサーバ(対戦やMO) ◦ 並行処理を活かして多人数を収容 • インフラ管理ツール ◦ シングルバイナリなので別環境にもっていきやすい •
開発補助ツール ◦ クロスコンパイルで開発職以外の人にも • Slack bot ◦ 2020年新卒技術研修の紹介 〜Go研修編〜 ▪ https://www.klab.com/jp/blog/tech/2020/2020-bootcamp.html • 他…
こんな経験ありませんか?
サーバ開発中のこんな経験 • 書き換えたはずのものが反映されない ◦ 必ず通るパスに仕込んだログが出ない ◦ ソース変更前の挙動から変わっていない • 原因は大抵ケアレスミス ◦
編集したソースの保存し忘れ ◦ ビルドし忘れ ◦ サーバの再起動し忘れ
コンソールプログラムの動作確認 1. ビルド 2. 実行 • go run が便利 ◦
ビルド〜実行を1コマンドでできる ◦ ビルドし忘れのケアレスミスを防げる
サーバプログラムの動作確認 1. すでに動いているサーバを停止 2. ビルド 3. サーバを起動 4. クライアントからアクセス •
ケアレスミスのポイントが多い、かつ、気づきにくい ◦ サーバを再起動せずにクライアントを起動できてしまう ◦ 古いサーバが動いているとクライアントから接続できてしまう
こんなとき ホットリロードツール が便利
ホットリロードツールとは 基本的な機能 • ソースコードの変更を検知 • プログラムをビルドして再起動 何が解決されるか • ソースコードの保存忘れ防止 ◦
保存することがビルドと再起動のトリガー • ビルド忘れ、再起動忘れの防止 ◦ 自動的に行われる
Goのホットリロードツール • github.com/codegangsta/gin • github.com/gravityblast/fresh • github.com/oxequa/realize • github.com/go-task/task •
github.com/cosmtrek/air • github.com/kanataxa/fresher • 少し前まではrealizeがよく使われていた ◦ Go Modulesに対応しないまま開発停止、現在は使いにくい • 最近はairが使われていそう
なぜ自作するのか • 要求を全て満たすツールがほしい ◦ 拡張子のないファイルを監視対象にしたい ▪ 監視対象を拡張子でしか指定できない ◦ go runコマンドで実行したい
▪ 指定できない、もしくは動作に問題がある ◦ 子プロセスを利用したい ▪ 子プロセスを正しく停止できない ◦ Go以外でも使いたい ▪ GoやGoのウェブアプリフレームワークを前提としている • 作れそうだったから
ホットリロードツール「arelo」 github.com/makiuchi-d/arelo • コマンドラインベース ◦ 設定ファイル無し ▪ 保存したかったらシェルスクリプト • シンプルな機能と実装
◦ 現在400行強 • 汎用的 ◦ どのようなコマンドでも利用可能 • 安全なプロセス制御 ◦ 子プロセスも正しく停止
ホットリロードツール の作り方
必要な機能 本当に必要な機能はたったの2つ • ファイル変更の監視 ◦ 特定ディレクトリ以下の全てのファイル ◦ 再起動するべきファイルの変更か判定 • プロセスの再起動
◦ ビルド〜実行まで1コマンドでできるものを指定すればよい ▪ go run ▪ シェルスクリプト
ファイル変更の監視
fsnotify • github.com/fsnotify/fsnotify ◦ ファイル変更の監視の定番ライブラリ ▪ realizeやairも利用 ◦ クロスプラットフォーム ▪
Linux / Android ▪ BSD / macOS / iOS ▪ Windows
fsnotifyの基本的な使い方 1. Watcherを初期化 2. 監視対象を登録 3. Eventsチャネルに流れてくる ◦ イベント種別 (event.Op)
◦ ファイル名 (event.Name) package main import ( "fmt" "github.com/fsnotify/fsnotify" ) func main() { w, _ := fsnotify.NewWatcher() // (1) defer w.Close() w.Add("./") // (2) for { event := <-w.Events // (3) fmt.Println(event) } }
監視対象の登録 • func (w *Watcher) Add(name string) error ◦ 監視したいファイルまたはディレクトリのパスを指定して登録
実装のポイント: • ディレクトリを登録する ◦ ディレクトリ直下のファイルのイベントも取得できる ◦ ファイルを指定した場合 ▪ ファイルを削除すると監視対象から外れる ▪ 同名ファイルを作っても監視対象に復帰しない • 再帰的にサブディレクトリも登録する
fsnotifyのイベント • Create ◦ ディレクトリの場合は監視対象として登録する ◦ 他の場所から移動してきた場合も Create • Write
◦ ファイルへの書き込み • Remove ◦ 監視対象だった場合、自動的に監視も解除される • Rename ◦ リネーム前のファイル名 ◦ リネーム後のパスが監視対象ディレクトリ直下の場合、別途 Createが届く • Chmod ◦ 属性変更だけでなく、タイムスタンプの変更も Chmod
ファイル名のパターンマッチング ファイル名から再起動するか無視するか判定 • 拡張子によるマッチング ◦ Go標準ライブラリ ▪ path.Ext(), filepath.Ext() •
globパターンによるマッチング ◦ 拡張子より柔軟に指定できる ▪ *_test.goを無視、など ◦ Go標準ライブラリ ▪ path.Match(), filepath.Match() ▪ 拡張パターンは使えない • {alt1,alt2...} や **
拡張globが使えるライブラリ github.com/bmatcuk/doublestar • BashやZshの拡張globと同等のパターンが使える ◦ {alt1,alt2,...} ▪ 「,」で区切られたうちのいずれかとマッチ ◦ **
▪ ディレクトリと再帰的にマッチ • 使い方がGo標準ライブラリと同じ • 余談:BashとZshの "**" (globstar) の挙動の違い ◦ http://makiuchi-d.github.io/2020/04/11/bash-zsh-globstar.ja.html
ファイル変更監視の処理まとめ 1. fsnotify.Watcherに監視対象のディレクトリを登録 ◦ サブディレクトリも再帰的に登録 2. イベントの処理 ◦ ディレクトリがCreateされたら監視対象に追加 3.
変更されたファイル名のパターンマッチング 4. トリガー通知チャネルで通知 ◦ プロセス再起動処理のトリガー
プロセスの再起動
プロセスの再起動の処理 1. プロセスを起動 2. トリガー通知チャネルを待つ 3. 動いているプロセスの停止 4. 1に戻る
プロセスの起動 • 起動するだけなら簡単 ◦ Go標準ライブラリ ▪ exec.Command() ▪ exec.Cmd.Start() ▪
exec.Cmd.Wait() 実装のポイント: • コマンド名・引数リスト ◦ realize, air:設定ファイル ◦ arelo:コマンドライン引数 ▪ arelo -p '**/*.go' -i '**/.*' -- go run -v main.go package main import ( "os" "os/exec" ) func main() { cmd := exec.Command("echo", "-n", "hello") cmd.Stdout = os.Stdout _ = cmd.Start() _ = cmd.Wait() }
余談:CLIの“--”について • “--”があったらそこでオプションの解析を終了する ◦ それ以降はオプションではなく引数 • 元はPOSIXのガイドライン • 多くのライブラリが対応している ◦
特定言語に限らない ◦ Go標準ライブラリflagも対応 ▪ 他主要なライブラリも対応 • areloでは github.com/spf13/pflag を利用
プロセスの停止 • 子プロセスも含めて停止する必要がある ◦ リソースを開放しないといけない ▪ 新しいプロセスが使えない • Go標準ライブラリの問題 ◦
os.Process.Kill() ▪ 該当プロセスのみ停止、子プロセスは止めない • Unix系OSではSIGKILLを送信 ◦ プロセスは子プロセスを処理できない ▪ SIGKILL, SIGSTOP にはシグナルハンドラを設定できない ◦ exec.CommandContext() ▪ コンテキスト完了時に os.Process.Kill()しているだけ
Windowsの場合 • TASKKILLコマンド ◦ /pid <processID> ▪ 停止するプロセスIDを指定 ▪ Goのos.Process.Pidをそのまま指定すればよい
◦ /t ▪ 子プロセスも含めて停止する ▪ このスイッチを付けるだけでよい
Unix系OSの場合 1. プロセス起動時にPGIDを設定する ◦ PGID: プロセスグループID ◦ 子プロセスのPGIDの初期値は親プロセスと一緒 2. PGIDを指定してシグナルを送信
◦ killシステムコールに負の値を渡す 3. SIGKILL以外のシグナルを送る ◦ 子プロセスのPGIDを変更している場合対策 ▪ 親プロセスが子を処理することを期待 • 詳しくは:Goで子プロセスを確実にKillする方法 ◦ http://makiuchi-d.github.io/2020/05/10/go-kill-child-process.ja.html cmd := exec.Command(...) cmd.SysProcAttr = &syscall.SysProcAttr{ Setpgid: true, // (1) } _ = cmd.Start() syscall.Kill( -cmd.Process.Pid, // (2) syscall.SIGTERM) // (3)
どのシグナルを送るか • SIGTERM ◦ 終了させるためのシグナル、 killコマンドのデフォルト • SIGINT ◦ Ctrl+Cで送信されるシグナル
• その他の選択肢 ◦ SIGHUP ▪ 端末切断時のシグナル ◦ SIGUSR1, SIGUSR2 ▪ ユーザ定義として用意されているシグナル ◦ SIGQUIT, SIGWINCH ▪ ApacheやnginxでGraceful shutdownとして使われている
ホットリロードツール自身のシグナル処理 • 起動したプロセスを停止してから自身も終了 ◦ 停止しないとプロセスが動き続けてしまう • 処理するべきシグナル ◦ SIGTERM ▪
killコマンド ◦ SIGINT ▪ ターミナルでのCtrl+C ◦ SIGHUP ▪ 端末の切断 • signal.Notify()、signal.NotifyContext() ◦ 指定したシグナルをチャネルで受け取れる s := make(chan os.Signal, 1) signal.Notify(s, syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP) <-s killCmd(cmd)
プロセスの終了の待機 • 起動したプロセスはwaitする必要がある ◦ waitされないとリソースを開放できない ◦ ゾンビプロセス ▪ 終了したのにwaitされていないプロセス ◦
exec.Cmd.Wait() 実装のポイント: • 起動したらすぐgoroutineでCmd.Wait() ◦ 他の処理をブロックしない • チャネルで終了を通知 ◦ context.Contextやtime.Timerとの共存 _ = cmd.Start() var cerr error done := make(chan struct{}) go func() { cerr = cmd.Wait() close(done) }()
プロセスが終了してくれない場合 • シグナルを送っても終了しない場合 • 一定時間待ってからSIGKILLを送る • タイマーとチャネルを同時に待つ ◦ selectの出番 go
func() { cerr = cmd.Wait() close(done) }() <-trigger killCmd(cmd) select { case <-time.NewTimer(waitForTerm).C: killCmdForce(cmd) <-done case <-done: }
短期間に届くトリガーを無視 • 短期間にトリガーが何度も届くケース ◦ ディレクトリ移動やgitの操作 ◦ 頻繁なファイル保存 • 何度も再起動したくない ◦
一定時間待ってから再起動 実装のポイント: • 容量1のチャネルを使ったトリック 1. goroutineでチャネル移し替え ▪ selectのdefaultを活用 2. チャネルを待つ前に取り出す ▪ ここでもselectのdefault trg := make(chan struct{}, 1) go func() { for { <-trigger select { // (1) case trg <- struct{}{}: default: } } }() for { cmd := runCmd() select { // (2) case <-trg: default: } <-trg <-time.NewTimer(delay).C killCmd(cmd) }
まとめ
ホットリロードツールの作り方 • ファイル変更の監視 ◦ fsnotifyの使い方 ◦ ファイル名のパターンマッチング • プロセスの再起動 ◦
execパッケージによるプロセスの起動 ◦ 子プロセスを含めた停止 ▪ Windowの場合とUnix系OSの場合 ◦ プロセス終了の待機 ◦ 短期間に届くトリガーを無視
おまけ
コンソールプログラムでもareloを使う • ソースを保存すると自動的にビルドと実行 • ターミナルを操作する必要がなくなる ◦ エディタとターミナルを行き来しなくてよい • ターミナルを見える場所に表示しておくだけ ◦
プログラムの標準出力は areloの標準出力に表示 • 意外と便利でした
Dockerでareloを使う • バイナリを置くだけで使える ◦ スタティックリンクなシングルバイナリ ▪ CGO_ENABLED=0でビルド • pflagがnetをimport ◦
どんなコンテナでもOK • 監視対象ディレクトリ ◦ 起動時にバインドマウントしておく ◦ 手元のファイルを書き換えるとリロード FROM hello-world COPY arelo / CMD ["/arelo", "-t", "/trg", \ "-p", "**", "--", "/hello"] $ docker build -t arelo-hello . $ docker run -v `pwd`:/trg arelo-hello
areloを複数使ったトリック • ビルド〜実行までを1コマンドで指定する弊害 ◦ ビルド中もサーバが停止してしまう ◦ 同一バイナリを複数実行するとき複数ビルドされてしまう • ビルド担当と実行担当で分ける ◦
ビルド担当arelo ▪ ソースコードを監視 ▪ ビルドするコマンドを実行 ◦ 実行担当arelo ▪ 実行バイナリを監視 ▪ それを実行
areloの開発にareloを使う • ちゃんと動きました ◦ 正直びっくり • シンプルに作ることが大事
おわり ご清聴ありがとうございました areloをぜひ使ってみてください https://github.com/makiuchi-d/arelo