Go サービスでリンクトレースを行う方法を説明します。
Goでマイクロサービスを開発する場合、各リクエストのアクセスリンクを追跡する必要がありますが、Goにはこれに対する良いソリューションがありません。
これは、Javaでは、プロセス内でリクエストのRequestIdを共有するMDCを使うことで解決しやすくなります。
Goでリンクトレースを実装するには2つの考え方があります。1つはプロジェクトのグローバルマップを使い、キーはgoroutineのユニークId、値はRequestIdとする方法、もう1つはコンテキストを使って実装する考え方があります。
これを実装するために、ginフレームワークをベースにした以下のようなコードがあります。
1. グローバルマップを使用して実装する
マップソリューションを使うには、入ってくるリクエストごとに RequestId を生成するグローバルマップを維持し、ログが印刷されるたびに、このマップから goid で RequestId を取得してログに印刷することが必要です。
コードの実装はシンプルです。
var requestIdMap = make(map[int64]string) // global Map
func main() {
r := gin.Default()
r.Use(Logger()) // use middleware
r.GET("/index", func(c * gin.Context) {
Info("main goroutine") // print the log
c.JSON(200, gin.H{
"message": "index",
})
})
r.Run()
}
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
requestIdMap[goid.Get()] = uuid.New().String() // set in the logging middleware for each request
c.Next()
}
}
func Info(msg string) {
now := time.Now()
nowStr := now.Format("2006-01-02 15:04:05")
fmt.Printf("%s [%s] %s\n", nowStr, requestIdMap[goid.Get()], msg) // print log
}
これは単純な実装ですが、多くの問題点があります。
第一の問題は、Goプログラムでは一つのリクエストが複数のgoroutineを含むことがあり、このように複数のgotoutine間でRequestIdを渡すことが困難であることである。
以下のコードでは、新しいゴルーチンが開始された場合、RequestIdはログに取得されない。
func main() {
r := gin.Default()
r.Use(Logger())
r.GET("/index", func(c *gin.Context) {
Info("main goroutine")
go func() { // A new goroutine is started here
Info("goroutine1")
}()
c.JSON(200, gin.H{
"message": "index",
})
})
r.Run()
}
ゴルーチンIDの取得も正規のやり方ではなく、ハッキングで行うのが普通ですが、これはもう推奨できません。そして、このグローバルマップは、並行性安全のために実際にはロックが必要になることもあり、並行性が高い状況ではどうしても性能に影響が出てしまいます。
また、各リクエストの終了時にマップからrequestIdを手動で削除する必要があります。そうしないと、メモリリークの原因になります。
全体として、これはmapを使った実装としては、あまり良い方法とは言えません。
2. Contextを使った実装
上記のコードでは、ゴルーチンIDを取得するためにハックを使用していますが、これは長い間推奨されるものではなく、むしろContextのようなものです。
RequestIdを渡すシナリオでは、Contextを使って同じことを実現することもできます。Context を使うことの利点は明らかです。Context はリクエストと同じライフサイクルを持ち、手動で破棄する必要がありません。Context はリクエストごとに一意なので、並行処理のセキュリティを気にする必要はありません。また、Context はゴルーチン間で受け渡しすることができます。
Contextを使って実装したコードは以下の通りである。
func main() {
r := gin.Default()
r.Use(Logger())
r.GET("/index", func(c *gin.Context) {
ctx, _ := c.Get("ctx")
Info(ctx.(context.Context) , "main goroutine")
go func() {
Info(ctx.(context.Context), "goroutine1")
}()
c.JSON(200, gin.H{
"message": "index",
})
})
r.Run()
}
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
valueCtx := context.WithValue(c.Request.Context(), "RequestId", uuid.New().String())
c.Set("ctx", valueCtx)
c.Next()
}
}
func Info(ctx context.Context, msg string) {
now := time.Now()
nowStr := now.Format("2006-01-02 15:04:05")
fmt.Printf("%s [%s] %s\n", nowStr, ctx.Value("RequestId"), msg)
}
この方法では、リクエスト内のすべてのgotroutineは同じRequestIdを取得し、メモリリークや並行処理の安全性を心配する必要はありません。
しかし、Contextを使用する際の問題点として、毎回渡す必要があり、多くの人はそのような使い方に慣れていないことが挙げられます。実際、Go では長い間 Context の使用を推奨しており、通常は関数の最初の引数として使用します。関数が構造体を引数として使う場合は、構造体のフィールドとして Context を使うこともできます。
ContextはRequestIdを渡す以外にも、goroutineのライフサイクルを制御するために使うことができる。詳しくは前回のContextの記事で説明したので、興味のある方はそちらを参照してほしい。
3. 概要
このゴルーチンIDの取得方法は捨てるべきで、Goが長い間推奨してきたContextを使用することをお勧めします。上記では、RequestId を渡すために Context を使いましたが、認証トークンなどの単一のリクエストスコープの値を渡すために使うこともできます。Contextの使い方に慣れておくとよいでしょう。
[1]
https://blog.golang.org/context
Goサービスでリンクトレースを行う方法についての記事は以上です。Go サービスでリンク トレースを行う方法の詳細については、Script House の過去の記事を検索するか、次の記事を引き続き参照してください。
関連
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン