使用 skywalking 監控 gin web 服務
【導讀】本文介紹了分佈式開源分佈式 tracing 框架 Skywalking 的使用。
簡介
SkyWalking 是一個開源的 APM 系統,包括對 Cloud Native 架構中分佈式系統的監控、跟蹤、診斷能力。核心功能如下。
-
服務、服務實例、端點指標分析
-
根本原因分析。在運行時分析代碼
-
服務拓撲圖分析
-
服務、服務實例和端點依賴分析
-
檢測到緩慢的服務和端點
-
性能優化
-
分佈式跟蹤和上下文傳播
-
數據庫訪問指標。檢測慢速數據庫訪問語句(包括 SQL 語句)
-
警報
-
瀏覽器性能監控
-
基礎設施(VM、網絡、磁盤等)監控
-
跨指標、跟蹤和日誌的協作
快速安裝 skywalking
skywalking 支持多種安裝方式,二進制安裝,docker 安裝以及 helm、k8s 安裝等。
這裏直接使用 docker-compose 進行快速部署測試
docker-compose.yaml
version: '3.3'
services:
# storage
elasticsearch:
image: elasticsearch:6.8.16
container_name: elasticsearch
restart: always
ports:
- 9200:9200
environment:
discovery.type: single-node
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- ./elasticsearch/logs:/usr/share/elasticsearch/logs
- ./elasticsearch/data:/usr/share/elasticsearch/data
- /etc/localtime:/etc/localtime
# server
oap:
image: apache/skywalking-oap-server:8.6.0-es6
container_name: oap
depends_on:
- elasticsearch
links:
- elasticsearch
restart: always
ports:
- 11800:11800
- 12800:12800
environment:
SW_STORAGE: elasticsearch # 默認爲 es6,es7 爲 elasticsearch7
SW_STORAGE_ES_CLUSTER_NODES: elasticsearch:9200
volumes:
- /etc/localtime:/etc/localtime
# dashboard
ui:
image: apache/skywalking-ui:8.6.0
container_name: ui
depends_on:
- oap
links:
- oap
restart: always
ports:
- 8080:8080
environment:
SW_OAP_ADDRESS: oap:12800
volumes:
- /etc/localtime:/etc/localtime
運行
1docker-compose up -d
訪問控制檯
瀏覽器訪問 http://localhost:8080
gin 接入 skywalking
這裏使用第三方庫 go2sky 以及 go2sky 的 gin 中間件
1go get -u github.com/SkyAPM/go2sky
2go get github.com/SkyAPM/go2sky-plugins/gin/v3
代碼示例
兩個 gin 服務 demo-server1 和 demo-server2,demo-server1 調用 demo-server2 demo-server2 提供 POST /user/info 接口 demo-server1 提供 GET /tracer 接口
demo-server2 代碼如下
package main
import (
"fmt"
"time"
"github.com/SkyAPM/go2sky"
"github.com/SkyAPM/go2sky/reporter"
"github.com/gin-gonic/gin"
v3 "github.com/SkyAPM/go2sky-plugins/gin/v3"
)
const (
serverName = "demo-server2"
serverPort = 8082
)
var skyAddr = "localhost:11800"
type Params struct {
Name string
}
func panicErr(err error) {
if err != nil {
panic(err)
}
}
func main() {
r := gin.Default()
// skyAddr 是 skywaling 的 grpc 地址,默認是 localhost:11800, 默認心跳檢測時間是 1s
rp, err := reporter.NewGRPCReporter(skyAddr, reporter.WithCheckInterval(5*time.Second))
panicErr(err)
// 初始化一個 tracer,一個服務只需要一個 tracer,其含義是這個服務名稱
tracer, err := go2sky.NewTracer(serverName, go2sky.WithReporter(rp))
panicErr(err)
// gin 使用 sky 自帶的 middleware
r.Use(v3.Middleware(r, tracer))
// 自定義一個接口
r.POST("/user/info", func(context *gin.Context) {
// LocalSpan 可以理解爲本地日誌的 tracer,一般用戶當前應用
span, ctx, err := tracer.CreateLocalSpan(context.Request.Context())
panicErr(err)
// 每一個 span 都有一個名字去標實操作的名稱!
span.SetOperationName("UserInfo")
// 記住重新設置一個 ctx,再其次這個 ctx 不是 gin 的 ctx,而是 http request 的 ctx
context.Request = context.Request.WithContext(ctx)
params := new(Params)
err = context.BindJSON(params)
panicErr(err)
// 記錄日誌信息
span.Log(time.Now(), "[UserInfo]", fmt.Sprintf(serverName+" satrt, req : %+v", params))
local := gin.H{
"msg": fmt.Sprintf(serverName+" time : %s", time.Now().Format("15:04:05.9999")),
}
context.JSON(200, local)
span.Log(time.Now(), "[UserInfo]", fmt.Sprintf(serverName+" end, resp : %s", local))
// 切記最後要設置 span - end,不然就是一個非閉環的
span.End()
})
r.Run(fmt.Sprintf(":%d", serverPort))
}
demo-server1 代碼如下
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"time"
"github.com/SkyAPM/go2sky"
"github.com/SkyAPM/go2sky/reporter"
"github.com/gin-gonic/gin"
v3 "github.com/SkyAPM/go2sky-plugins/gin/v3"
agentv3 "skywalking.apache.org/repo/goapi/collect/language/agent/v3"
)
const (
serverName = "demo-server1"
serverPort = 8081
remoteServerName = "demo-server2"
remoteServerAddr = "localhost:8082"
remotePath = "/user/info"
)
var skyAddr = "localhost:11800"
func panicErr(err error) {
if err != nil {
log.Fatal(err.Error())
}
}
type Params struct {
Name string
}
var tracer *go2sky.Tracer
func skyMiddleware(r *gin.Engine) {
var err error
rp, err := reporter.NewGRPCReporter(skyAddr, reporter.WithCheckInterval(5*time.Second))
panicErr(err)
tracer, err = go2sky.NewTracer(serverName, go2sky.WithReporter(rp))
panicErr(err)
r.Use(v3.Middleware(r, tracer))
}
func trace(context *gin.Context) {
span, ctx, err := tracer.CreateLocalSpan(context.Request.Context())
panicErr(err)
span.SetOperationName("Trace")
context.Request = context.Request.WithContext(ctx)
span.Log(time.Now(), "[Trace]", fmt.Sprintf(serverName+" satrt, params : %s", time.Now().Format("15:04:05.9999")))
result := make([]map[string]interface{}, 0)
//1、請求一次
{
url := fmt.Sprintf("http://%s%s", remoteServerAddr, remotePath)
params := Params{
Name: serverName + time.Now().Format("15:04:05.9999"),
}
buffer := &bytes.Buffer{}
_ = json.NewEncoder(buffer).Encode(params)
req, err := http.NewRequest(http.MethodPost, url, buffer)
panicErr(err)
// op_name 是每一個操作的名稱
reqSpan, err := tracer.CreateExitSpan(context.Request.Context(), "invoke - "+remoteServerName, "localhost:8082/user/info", func(headerKey, headerValue string) error {
req.Header.Set(headerKey, headerValue)
return nil
})
panicErr(err)
reqSpan.SetComponent(2)
reqSpan.SetSpanLayer(agentv3.SpanLayer_RPCFramework) // rpc 調用
resp, err := http.DefaultClient.Do(req)
panicErr(err)
defer resp.Body.Close()
reqSpan.Log(time.Now(), "[HttpRequest]", fmt.Sprintf("開始請求,請求服務:%s, 請求地址:%s, 請求參數:%+v", remoteServerName, url, params))
body, err := ioutil.ReadAll(resp.Body)
panicErr(err)
fmt.Printf("接受到消息: %s\n", body)
reqSpan.Tag(go2sky.TagHTTPMethod, http.MethodPost)
reqSpan.Tag(go2sky.TagURL, url)
reqSpan.Log(time.Now(), "[HttpRequest]", fmt.Sprintf("結束請求,響應結果:%s", body))
reqSpan.End()
res := map[string]interface{}{}
err = json.Unmarshal(body, &res)
panicErr(err)
result = append(result, res)
}
//2 、再請求一次
{
url := fmt.Sprintf("http://%s%s", remoteServerAddr, remotePath)
params := Params{
Name: serverName + time.Now().Format("15:04:05.9999"),
}
buffer := &bytes.Buffer{}
_ = json.NewEncoder(buffer).Encode(params)
req, err := http.NewRequest(http.MethodPost, url, buffer)
panicErr(err)
// 出去必須用這個攜帶 header
reqSpan, err := tracer.CreateExitSpan(context.Request.Context(), "invoke - "+remoteServerName, "localhost:8082/user/info", func(headerKey, headerValue string) error {
req.Header.Set(headerKey, headerValue)
return nil
})
panicErr(err)
reqSpan.SetComponent(2)
reqSpan.SetSpanLayer(agentv3.SpanLayer_RPCFramework) // rpc 調用
resp, err := http.DefaultClient.Do(req)
panicErr(err)
defer resp.Body.Close()
reqSpan.Log(time.Now(), "[HttpRequest]", fmt.Sprintf("開始請求,請求服務:%s, 請求地址:%s, 請求參數:%+v", remoteServerName, url, params))
body, err := ioutil.ReadAll(resp.Body)
panicErr(err)
fmt.Printf("接受到消息: %s\n", body)
reqSpan.Tag(go2sky.TagHTTPMethod, http.MethodPost)
reqSpan.Tag(go2sky.TagURL, url)
reqSpan.Log(time.Now(), "[HttpRequest]", fmt.Sprintf("結束請求,響應結果:%s", body))
reqSpan.End()
res := map[string]interface{}{}
err = json.Unmarshal(body, &res)
panicErr(err)
result = append(result, res)
}
// 設置響應結果
local := gin.H{
"msg": result,
}
context.JSON(200, local)
span.Log(time.Now(), "[Trace]", fmt.Sprintf(serverName+" end, resp : %s", local))
span.End()
{
span, ctx, err := tracer.CreateEntrySpan(context.Request.Context(), "Send", func(s string) (string, error) {
return "", nil
})
context.Request = context.Request.WithContext(ctx)
panicErr(err)
span.SetOperationName("Send")
span.Log(time.Now(), "[Info]", "send resp")
span.End()
}
}
func main() {
// 這些都一樣
r := gin.Default()
// 使用 go2sky gin 中間件
skyMiddleware(r)
// 調用接口
r.GET("/trace", trace)
r.Run(fmt.Sprintf(":%d", serverPort))
}
調用測試
1for i in $(seq 100); do curl -s http://localhost:8081/trace >/dev/null; sleep 0.1 ; done
demo-server2
demo-server1
dashboard
拓撲
追蹤
總結
-
gin 接入 skywalking 總體還是比較簡單的,正式使用的時候還需要再次封裝,簡化操作;
-
應用程序和 skywalking 之前不是強依賴關係,skywalking 關閉的情況下能夠正常響應請求。
轉自:
huangzhongde.cn/post/Golang/%E4%BD%BF%E7%94%A8skywalking%E7%9B%91%E6%8E%A7gin_web%E6%9C%8D%E5%8A%A1/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/2uSN1SHtQGSoVZkWPFVwRw