
2026/02/11 2:44
**Show HN:** *Deadlog – Go のデッドロックをデバッグするためのほぼ即座に使用できるミューテックス*
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
改善された要約
Deadlog は軽量な Go ライブラリで、標準の
sync.Mutex/sync.RWMutex をデバッグ有効版(deadlog.Mutex)に置き換えます。インストールは次のように行います:
go get github.com/stevenctl/deadlog
そして、名前付きミューテックスを作成して使用します:
m := deadlog.New(deadlog.WithName("my-service"))
API は
sync.Mutex/sync.RWMutex と同等で、Lock()/Unlock() および RLock()/RUnlock() を提供します。さらに、開発者は LockFunc() または RLockFunc() を呼び出すことで「RELEASED」イベントを発行するアンロック関数を取得できます。ロック操作にはラベル付け(WithLockName("name"))やスタックトレースの深さ調整(WithTrace(n))が可能です。
デフォルトでは、ログは JSON 形式で stdout に書き込まれます。カスタムロギングは
deadlog.WithLogger() または deadlog.WriterLogger(f) を使用してサポートされます。ログエントリには次のフィールドが含まれます:type(LOCK、RLOCK、WLOCK、RWLOCK)、state(START、ACQUIRED、RELEASED)、name、id、ts およびオプションでスタックトレース。
CLI 分析ツールは次のようにインストールできます:
go install github.com/stevenctl/deadlog/cmd/deadlog@latest
そして次のように使用します:
deadlog analyze file.log
分析器は、すべてのタイプで「START だけが ACQUIRED されていない」スタックロック(stuck locks)と、トラッキングされたタイプ(
LockFunc、RLockFunc)に対してのみ「ACQUIRED だけが RELEASED されていない」保持ロックを特定します。分析ライブラリ(github.com/stevenctl/deadlog/analyze)はこの機能をプログラム的に公開しています。
Deadlog は MIT ライセンスの下で配布されています。将来の拡張として、より豊富なロックタイプ追跡、観測スタックとの緊密な統合、および深い事後調査のための CLI 機能拡張が予定されています。
本文
deadlog(デッドロック検出ツール)
Go 用のライブラリで、ミューテックスのデッドロックをログ付きラッパーと解析ツールでデバッグできます。
インストール
go get github.com/stevenctl/deadlog
使い方
sync.Mutex または sync.RWMutex を deadlog.Mutex に置き換えます:
import "github.com/stevenctl/deadlog" // Before var mu sync.RWMutex // After var mu = deadlog.New(deadlog.WithName("my-service"))
API は
sync.Mutex と sync.RWMutex の両方に互換性があります。
ロック操作
// 書き込みロック(sync.Mutex と同等) mu.Lock() defer mu.Unlock() // 読み取りロック(sync.RWMutex と同等) mu.RLock() defer mu.RUnlock()
未解放ロックの追跡
LockFunc() や RLockFunc() を使うと、関連付けられた RELEASED イベントを取得できます:
unlock := mu.LockFunc() defer unlock() // 同じ correlation ID で START, ACQUIRED, RELEASED がログに残ります
名前付き呼び出し元
同一ミューテックス上の個別ロック操作にラベルを付けます:
mu := deadlog.New( deadlog.WithName("player-state"), deadlog.WithTrace(1), ) // 各呼び出し元はログで独自の名前が表示されます unlock := mu.LockFunc(deadlog.WithLockName("update-health")) defer unlock()
WithTrace(1) と組み合わせると、JSON イベントに正確な位置情報が付与されます:
{"type":"LOCK","state":"START","name":"update-health","id":4480578,"trace":"updateHealth:25","ts":1770746273707970140} {"type":"LOCK","state":"ACQUIRED","name":"update-health","id":4480578,"trace":"updateHealth:25","ts":1770746273707993939} {"type":"LOCK","state":"START","name":"add-item","id":9375956,"trace":"addItem:29","ts":1770746273707996887} {"type":"LOCK","state":"ACQUIRED","name":"add-item","id":9375956,"trace":"addItem:29","ts":1770746273707998734} {"type":"LOCK","state":"START","name":"apply-damage","id":6439038,"trace":"applyDamage:33","ts":1770746273708002604}
解析ツールはこれをわかりやすいレポートに変換します — apply‑damage が待機中で、update-health と add-item がロックを保持しています:
=============================================== LOCK CONTENTION ANALYSIS =============================================== === STUCK: Started but never acquired (waiting for lock) === LOCK | apply-damage | ID: 6439038 Trace: applyDamage:33 === HELD: Acquired but never released (holding lock) === LOCK | update-health | ID: 4480578 Trace: updateHealth:25 LOCK | add-item | ID: 9375956 Trace: addItem:29 === SUMMARY === Stuck waiting: 1 Held: 2
スタックトレースの取得
ロック取得場所を確認したい場合は、スタックトレースを有効にします:
mu := deadlog.New( deadlog.WithName("my-mutex"), deadlog.WithTrace(5), // 5 フレーム深さで取得 )
カスタムログ
デフォルトではイベントは JSON として stdout に書き出されます。カスタムロガーを指定することも可能です:
mu := deadlog.New( deadlog.WithLogger(func(e deadlog.Event) { log.Printf("[DEADLOG] %s %s %s id=%d", e.Type, e.State, e.Name, e.ID) }), )
または特定の writer に書き込むこともできます:
f, _ := os.Create("locks.jsonl") mu := deadlog.New(deadlog.WithLogger(deadlog.WriterLogger(f)))
解析
CLI ツール
CLI をインストールします:
go install github.com/stevenctl/deadlog/cmd/deadlog@latest
ログファイルを解析します:
deadlog analyze <file>
またはアプリから直接パイプで渡すことも可能です:
go run ./myapp 2>&1 | deadlog analyze -
ライブラリ
プログラムから解析ライブラリを使う場合:
import "github.com/stevenctl/deadlog/analyze" result, err := analyze.AnalyzeFile("app.log") if err != nil { log.Fatal(err) } fmt.Printf("Stuck: %d, Held: %d\n", len(result.Stuck), len(result.Held)) // フォーマット済みレポートを出力 analyze.PrintReport(os.Stdout, result)
ログ形式
イベントは JSON で記録されます:
{"type":"LOCK","state":"START","name":"my-mutex","id":1234567,"ts":1704067200000000000} {"type":"LOCK","state":"ACQUIRED","name":"my-mutex","id":1234567,"ts":1704067200000001000} {"type":"LOCK","state":"RELEASED","name":"my-mutex","id":1234567,"ts":1704067200000002000}
| フィールド | 説明 |
|---|---|
| ロック種別(下記参照) |
| , , のいずれか |
| で設定したミューテックス名 |
| 相関 ID(ランダム、同じロック操作の START/ACQUIRED/RELEASED で共通) |
| Unix ナノ秒タイムスタンプ |
| スタックトレース( 有効時のみ) |
ロック種別
| メソッド | type | Tracked? | 説明 |
|---|---|---|---|
| | はい | 書き込みロック(RELEASED を追跡) |
| | はい | 読み取りロック(RELEASED を追跡) |
| | いいえ | 書き込みロック、RELEASED イベントなし |
| | いいえ | 読み取りロック、RELEASED イベントなし |
Tracked な種別 (
LOCK, RLOCK) は unlock 関数で RELEASED を発行するため、解析ツールは保持中のロックを検知できます。Untracked 種別 (WLOCK, RWLOCK) は sync.Mutex / sync.RWMutex と同等に使えますが、RELEASED イベントがないので「保持」として報告されません。
最初は untracked メソッドで競合を検出し、必要に応じて tracked メソッドへ切り替えてどのロックが保持されているかを特定します。
動作原理
| フェーズ | 説明 |
|---|---|
| ロック取得を試みる直前にログ |
| ロック取得後にログ |
| unlock 関数呼び出し時にログ(/ のみ) |
解析ツールは次の状態を検知します:
- Stuck:
があるがSTART
がない(ゴルーチンがロック待ち中) – すべての種別で対象ACQUIRED - Held:
はあるがACQUIRED
がない(ロックが解放されていない) – tracked 種別 (RELEASED
,LOCK
) のみRLOCK
ライセンス
MITライセンス
MIT LICENSE