ngo:go 語言開發利器

網易傳媒在 2021 年的時候就開始嘗試將線上的一些核心集羣使用 Go 語言進行重構,筆者有幸主導了本次 Go 語言重構,在本次重構中,爲了減少各業務不必要的調研時間,我們成立了一個攻堅小組,就業務用到的各種依賴進行了統一調研、封裝、測試,給各業務提供了統一的 Go 語言基礎框架,極大的增加了業務開發效率,避免了重複研究。

之前筆者也就 Go 語言的一些特性、優勢及問題進行了闡述,說明了傳媒使用 Go 語言進行業務重構的一些目的、收益和重構過程中遇到的一些坑,感興趣的讀者可以參考筆者以前撰寫的文章。

作爲 Go 語言的通用框架,ngo 除了在傳媒內部大量使用,也在網易集團內部其他 BU 使用,這些 BU 也作爲了參與者,貢獻了自己的一份力量,能讓 ngo 支持更多的特性。

在本文中,筆者將對 ngo 建設的目的做一些闡述,對一些我們開發應用要用到的基礎服務做詳細闡述,希望能給讀者帶來一些收益。

1

開源 Go 語言框架調研

筆者主要調研了以下幾個 Go 開源框架

以上的 Go 開源框架我們都做了一些調研和驗證,最後都沒有直接使用這些開源框架,主要有以下幾個原因:

2

ngo 框架介紹

在傳媒技術團隊中推廣 Go 語言的過程中,急需一個通用基礎框架提供給業務開發使用,這個基礎框架要包含業務開發常用到的庫,並且能夠無感知的自動監控數據上報,實現一些自動化能力,Ngo 框架在這樣的背景下誕生了。

ngo 主要的目標包括

ngo 目前已經支持了 Redis、Kafka、memcache 等絕大部分的中間件和數據庫客戶端的 SDK,也支持了監控、配置中心、openID、xxljob 等基礎服務。

以下筆者將對最基礎的服務作一些介紹。

3

應用健康檢查

一個可運行的應用,除了業務邏輯本身之外,還要能夠監控應用的當前狀態,比如:應用是否在線、是否健康、啓動完成後需要做哪些動作、停止之前需要做哪些動作等等。

ngo 爲應用提供了統一的健康檢查入口(類似於 SpringBoot 的 Actuator),應用通過這些健康檢查接口,可以查看應用當前的健康狀態。

ngo 預製了以下四個檢查入口

下線的接口,當 K8S 調用 offline 接口時,ngo 首先會檢查當前的請求是不是已經全部處理完成了,如果全部處理完成,則返回 200,否則,ngo 將會返回 400,默認情況下,K8S 會繼續探測,直到返回 200,這樣也就實現了業務的優雅停服,不會因爲應用的停服導致大量請求失敗或在處理的請求被強制中斷。

func (s *Server) offlineHandler(c *gin.Context) {
    atomic.StoreInt32(&s.active, 0)  //active置爲0,表示應用不可用
    if s.requestsFinished() {        //當前的處理請求是否已經完成
        c.String(http.StatusOK, "ok")
        log.Info("Server offline requested!")
    } else {
        c.String(http.StatusBadRequest, "bad")
        log.Info("Server offline failed!")
    }
}

應用上線接口的預置邏輯,當 K8S 調用 online 接口時,ngo 將框架的預置變量 active 置爲 1,並返回 200,表示應用上線完成,可以接收流量。

    atomic.StoreInt32(&s.active, 1)     //active置爲1,表示應用可接收流量
    c.String(http.StatusOK, "ok")
    log.Info("Server online requested!")
}

應用 check 接口的預置邏輯,當調用 check 時,會調用 healthz 接口,判斷是否正常,接下來將檢查 server 服務的健康狀態。

  ss := Get()
  if !ss.Healthz() {       //查看http://localhost/healthz接口的狀態
    return
  }
  if s.healthy != nil && !s.healthy() {        //查看當前Server的狀態
    c.String(http.StatusForbidden, "error")
    return
  }
  c.String(http.StatusOK, "ok")
  log.Info("Server check requested!")
 }

應用 status 接口的預置邏輯比較簡單,就是查看當前 active 的狀態是不是爲 1。

    if atomic.LoadInt32(&s.active) == 1 {
        c.String(http.StatusOK, "ok")
    } else {
        c.String(http.StatusForbidden, "error")
    }
}

ngo 預置了應用的一些健康檢查基本接口,同時,框架也支持業務增加自己的一些特殊邏輯,讓我們看看 ngo 框架支持的一些擴展點。

ngo 支持在應用啓動前或停止時增加預置邏輯,也支持增加自己的 check 邏輯。

具體代碼如下

func main() {
    app := ngo.Init()
    s := http.Get()
    ....
    app.PreStart = func() error {       //應用啓動前執行的業務邏輯
        return nil
    }
    app.AfterStop = func() error {      //應用停止時執行的業務邏輯
        return nil
    }
    admin.Get().SetHealthyFn(func() bool {return true})     //應用增加的check業務邏輯
    app.Start()
    ...
}

當應用接收到操作系統的 shutdown signal 時,將執行停止邏輯,具體代碼如下

func (a *application) waitSignals()
    signals.Shutdown(func(grace bool) {
        if grace {
            a.GracefulStop()
        } else {
            a.Stop()
        }
    })
}

ngo 通過監聽 syscall.SIGQUIT、syscall.SIGINT、syscall.SIGTERM 任意一個信號進行關服,服務在 stop 時,會關閉內置組件 http server、pprof server、redis client、kafka client、httplib、 tracer、 xxljob。

筆者已將 ngo 框架對健康檢查的邏輯進行了闡述,大家可以看到,一個健壯應用的健康檢查邏輯是很複雜的,如果做到無損、平滑的啓動停止都需要做很多的工作,接入 ngo 後,這些邏輯都已經被封裝,大大降低了開發的工作量。

4

Tracer 集成

隨着業務複雜度的增加,開發者會根據業務劃分出多個微服務,一個對外接口請求可能就需要多個微服務協同完成,如果一個服務出現問題,就可能導致接口異常。面對這種情況,我門需要追蹤整個鏈路,看具體發生了什麼,問題出在哪裏。分佈式追蹤系統就是我們排查系統問題和系統性能的重要工具。

在分佈式鏈路跟蹤中有兩個重要的概念,跟蹤(trace)和跨度(span):

一次調用鏈路(單個 trace)圖下如

市場上大部分產品都支持 opentracing 規範,例如 jaeger、zipkin、skywalking 以及傳媒自研的 optimus,同時也存在非 opentracing 規範的產品,比如 pinpoint。

ngo 的 tracing 模塊整合了這兩類產品,提供了一個統一的接口,支持多種 tracing 方案,用戶可以無需更改代碼快速切換。

核心 API 如下

type Tracer interface {
   Enabled() bool
   Type() string
   StartSpanFromCarrier(ctx context.Context, op string, carrier interface{}) (Span, context.Context)
   StartSpanFromContext(ctx context.Context, op string) (Span, context.Context)
   Inject(ctx context.Context, carrier interface{})
   SetSamplingRate(rate int)
   Stop()
}
type Span interface {
   SetTag(key string, value interface{}) Span
   Finish()
   GetTraceId() string
}
func SpanFromContext(ctx context.Context) Span {
   v := ctx.Value(contextKey)
   if v != nil {
      return v.(Span)
   }
   return nil
}
func ContextWithSpan(ctx context.Context, span Span) context.Context {
   return context.WithValue(ctx, contextKey, span)
}
func GetTraceId(ctx context.Context) string {
   span := SpanFromContext(ctx)
   if span == nil {
      return ""
   }
   return span.GetTraceId()
}

整體採集鏈路如下

同時提供 ngo 內部組件的採集支持,包含

這裏拿 httpserver 舉例,實現爲 gin 的 middleware

func ServerTraceMiddleware() gin.HandlerFunc {
   return func(c *gin.Context) {
      if tracing.Enabled() && !strings.HasPrefix(c.Request.RequestURI, "/health") {
         // 生成根span和新ctx 新ctx 包含span信息,需要向下傳遞
         span, ctx := tracing.StartSpanFromCarrier(c.Request.Context(), "server", c.Request.Header)
         tracing.SpanKind.Set(span, "server")
         tracing.SpanType.Set(span, "HTTP_SERVER")
         tracing.HttpServerRequestUrl.Set(span, c.FullPath())
         tracing.HttpServerRequestHost.Set(span, c.Request.Host)
         tracing.HttpServerRequestMethod.Set(span, c.Request.Method)
         if remoteIp, remotePortStr, err := net.SplitHostPort(c.Request.RemoteAddr); err == nil {
            tracing.HttpServerPeerHost.Set(span, remoteIp)
            if remotePort, err := strconv.Atoi(remotePortStr); err == nil {
               tracing.HttpServerPeerPort.Set(span, uint16(remotePort))
            }
         }
         tracing.HttpServerRequestPath.Set(span, c.Request.URL.Path)
         tracing.HttpServerRequestSize.Set(span, c.Request.ContentLength)
         c.Request = c.Request.WithContext(ctx)
         defer func() {
            code := c.Writer.Status()
            tracing.HttpServerResponseStatus.Set(span, uint16(code))
            tracing.HttpServerResponseSize.Set(span, c.Writer.Size())
            span.Finish()
         }()
      }
      c.Next()
   }
}

5

Apollo 集成

Apollo 的接入有兩種方式

方式一:直接使用外部 sdk(官方 sdk / 第三方 sdk),通過調用 sdk api 完成 apollo 的接入。

方式二:使用 ngo 配置模塊的 apollo 數據源。

ngo 配置模塊提供讀取配置的統一 API,支持從本地文件、遠程數據源兩種方式讀取配置,並且支持監聽配置變化,發送事件。本地文件支持父子文件 include 方式接入,遠程數據源包含攜程的 apollo、etcd、網易配置中心,並且提供了數據源擴展接口,方便用戶對接其他數據源。

接入方式也比較簡單,ngo 對外提供了 - c 和 - w 兩個參數負責配置模塊的初始化,其中

apollo 數據源配置示例

-c apollo://host:port?appId=xx&cluster=xxx&namespaceNames=xx.yaml,xx.yaml -w true

apollo 數據源實現了 config datasource 的接口,並且我們支持 apollo 多 namespace 的 properties 和 yaml 兩種格式的讀取

   ReadConfig() ([]byte, error)
   IsConfigChanged() <-chan struct{}
   io.Closer
}

方式二相對方式一有明顯的優勢

6

OpenId 集成

ngo 提供了 OpenIdClient 的支持,OpenIdClient 提供了獲取 token,根據 token 獲取用戶信息等接口,用戶可以很方便的對接 openid 來完成自己的認證登錄,不過一般情況系統會很少直接使用 openid 的 token 來作爲前後端的傳輸憑證,大部分系統都會自己維護登錄狀態,有的用 session、有的用 token。

這裏 ngo 提供了 jwt+openid 的登錄模式,jwt 用於對外認證,openid 負責提供登錄入口,大體流程如下

認證會以 middeware 方式進行集成

httpServer:
  middlewares:
    jwtAuth:
      enabled: false -- 這裏默認爲false,需要改成true開啓認證
      authHeader: Authorization --默認認證頭
      tokenType: Bearer --默認value前綴
      accessTokenExpiresIn: 3600 --默認訪問token有效時間,單位s
      refreshTokenExpiresIn: 7200 --默認刷新token有效時間,單位s
      encryption: HS256 --默認加密方式
      oidc:  -- https://login.netease.com/sitemgnt/create/ 建立新站點,會生成id和secret
        clientId: xxxxxxxxx
        clientSecret: xxxxxxxxx
        encryption: HS256
      routePathPrefix: "" --內置接口前綴,默認爲空 
      ignorePaths:
        - /xxx --忽略路徑,這裏爲前綴匹配

並且提供內置接口,用戶可以使用配置中的 routePathPrefix 來添加和系統格式匹配的通用前綴。

獲取 token, detail=true 會調 oidc 獲取用戶信息

GET /auth/access-token?code=&redirectUri=&detail=true|false

刷新 token,用戶可以在臨近失效時進行 token 刷新,防止使用過程中失效跳出登錄頁

GET /auth/refresh-token?refreshToken=
Header
Authorization: Bearer {accessToken}

考慮到有些業務要根據用戶去後臺做二次驗證,這裏提供了擴展方法,用戶可以實現 Authenticator,然後調用 server 的 AddAdminAuthHandler 添加進去,同時該接口也可以自定義響應信息,給用戶提供了足夠的靈活性

// AddAdminAuthHandler 擴展處理方法
// param auth openid通過後的回調,用來驗證業務後臺用戶邏輯,返回的第二個參數需要存入token的信息,返回的信息儘量少,否則token很長
// param gtRsp 獲取token和刷新token後回調的方法,用來自定義響應報文格式
// param unauthRsp  token驗證失敗的回調方法,用來自定義響應報文格式
func (s *Server) AddAdminAuthHandler(auth Authenticator, gtRsp GenTokenResponse, unauthRsp UnauthenticatedResponse) *Server

當然,如果不想使用內置接口,可以自己添加路由,使用系統內置函數即可。

筆者將先介紹下最常使用的服務,更多的服務,可以從 Git 中獲取最新的代碼及說明。

ngo 的 git 地址如下:

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/xAj3zuZd3ZmOzj2yaAbRvA