Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
ホットリロードツールの作り方
Search
MakKi
June 13, 2021
Programming
1
1.2k
ホットリロードツールの作り方
Go Conference Online 2021 Spring
MakKi
June 13, 2021
Tweet
Share
More Decks by MakKi
See All by MakKi
SQLだけでマイグレーションしたい!
makki_d
0
1.2k
Recap: An Operating System in Go
makki_d
1
110
XSLTで作るBrainfuck処理系
makki_d
0
290
眼鏡と視力についての誤解を解く
makki_d
0
150
標準ライブラリの動向とイテレータのパフォーマンス
makki_d
3
740
range over funcのエラー処理
makki_d
1
1.8k
GoとテストとインプロセスDB
makki_d
3
650
君は古の言語M4を知っているか (LT)
makki_d
0
500
型パラメータが使えるようになったのでLINQを実装してみた
makki_d
2
1.5k
Other Decks in Programming
See All in Programming
ゲームの物理 剛体編
fadis
0
370
リリース時」テストから「デイリー実行」へ!開発マネージャが取り組んだ、レガシー自動テストのモダン化戦略
goataka
0
140
Pythonではじめるオープンデータ分析〜書籍の紹介と書籍で紹介しきれなかった事例の紹介〜
welliving
2
520
Canon EOS R50 V と R5 Mark II 購入でみえてきた最近のデジイチ VR180 事情、そして VR180 静止画に活路を見出すまで
karad
0
130
AI Agent Tool のためのバックエンドアーキテクチャを考える #encraft
izumin5210
3
800
Graviton と Nitro と私
maroon1st
0
130
AI前提で考えるiOSアプリのモダナイズ設計
yuukiw00w
0
180
AIコーディングエージェント(NotebookLM)
kondai24
0
220
チームをチームにするEM
hitode909
0
370
Denoのセキュリティに関する仕組みの紹介 (toranoana.deno #23)
uki00a
0
150
Rubyで鍛える仕組み化プロヂュース力
muryoimpl
0
150
認証・認可の基本を学ぼう後編
kouyuume
0
250
Featured
See All Featured
Taking LLMs out of the black box: A practical guide to human-in-the-loop distillation
inesmontani
PRO
3
1.9k
Measuring & Analyzing Core Web Vitals
bluesmoon
9
710
The Limits of Empathy - UXLibs8
cassininazir
1
190
We Analyzed 250 Million AI Search Results: Here's What I Found
joshbly
0
250
Unsuck your backbone
ammeep
671
58k
A Modern Web Designer's Workflow
chriscoyier
698
190k
The Cost Of JavaScript in 2023
addyosmani
55
9.4k
Navigating Team Friction
lara
191
16k
How to optimise 3,500 product descriptions for ecommerce in one day using ChatGPT
katarinadahlin
PRO
0
3.4k
Self-Hosted WebAssembly Runtime for Runtime-Neutral Checkpoint/Restore in Edge–Cloud Continuum
chikuwait
0
180
Noah Learner - AI + Me: how we built a GSC Bulk Export data pipeline
techseoconnect
PRO
0
72
Google's AI Overviews - The New Search
badams
0
870
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