使用 skywalking 監控 gin web 服務

【導讀】本文介紹了分佈式開源分佈式 tracing 框架 Skywalking 的使用。

簡介

SkyWalking 是一個開源的 APM 系統,包括對 Cloud Native 架構中分佈式系統的監控、跟蹤、診斷能力。核心功能如下。

快速安裝 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

拓撲

追蹤

總結

  1. gin 接入 skywalking 總體還是比較簡單的,正式使用的時候還需要再次封裝,簡化操作;

  2. 應用程序和 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