ngo:go 語言開發利器
網易傳媒在 2021 年的時候就開始嘗試將線上的一些核心集羣使用 Go 語言進行重構,筆者有幸主導了本次 Go 語言重構,在本次重構中,爲了減少各業務不必要的調研時間,我們成立了一個攻堅小組,就業務用到的各種依賴進行了統一調研、封裝、測試,給各業務提供了統一的 Go 語言基礎框架,極大的增加了業務開發效率,避免了重複研究。
之前筆者也就 Go 語言的一些特性、優勢及問題進行了闡述,說明了傳媒使用 Go 語言進行業務重構的一些目的、收益和重構過程中遇到的一些坑,感興趣的讀者可以參考筆者以前撰寫的文章。
作爲 Go 語言的通用框架,ngo 除了在傳媒內部大量使用,也在網易集團內部其他 BU 使用,這些 BU 也作爲了參與者,貢獻了自己的一份力量,能讓 ngo 支持更多的特性。
在本文中,筆者將對 ngo 建設的目的做一些闡述,對一些我們開發應用要用到的基礎服務做詳細闡述,希望能給讀者帶來一些收益。
1
開源 Go 語言框架調研
筆者主要調研了以下幾個 Go 開源框架
-
Beego(https://beego.vip/)
-
go-zero(https://go-zero.dev/)
-
jupiter(https://jupiter.douyu.com/)
-
go-spring(https://go-spring.com/)
以上的 Go 開源框架我們都做了一些調研和驗證,最後都沒有直接使用這些開源框架,主要有以下幾個原因:
-
缺乏大量業務所需庫,比如 kafka、redis、rpc、xxlJob 等,如果在其基礎上開發不如從零選擇更適合的庫。
-
對集團內部的一些基礎服務需要做集成。
-
HTTP Server 的性能不理想。
-
大部分庫無法注入回調函數,也就難以增加無感的監控。
-
若干模塊如 ORM 不夠好用。
2
ngo 框架介紹
在傳媒技術團隊中推廣 Go 語言的過程中,急需一個通用基礎框架提供給業務開發使用,這個基礎框架要包含業務開發常用到的庫,並且能夠無感知的自動監控數據上報,實現一些自動化能力,Ngo 框架在這樣的背景下誕生了。
ngo 主要的目標包括
-
提供比原有 Java 框架更高性能和更低資源佔用率。
-
爲業務提供所需全部工具庫。
-
接入監控,業務無需開發即可自動上傳監控數據。
-
自動加載配置和初始化程序環境,開發者能直接使用各種庫。
-
與線上的健康檢查、運維接口等運行環境匹配,無需用戶手動開發配置。
ngo 目前已經支持了 Redis、Kafka、memcache 等絕大部分的中間件和數據庫客戶端的 SDK,也支持了監控、配置中心、openID、xxljob 等基礎服務。
以下筆者將對最基礎的服務作一些介紹。
3
應用健康檢查
一個可運行的應用,除了業務邏輯本身之外,還要能夠監控應用的當前狀態,比如:應用是否在線、是否健康、啓動完成後需要做哪些動作、停止之前需要做哪些動作等等。
ngo 爲應用提供了統一的健康檢查入口(類似於 SpringBoot 的 Actuator),應用通過這些健康檢查接口,可以查看應用當前的健康狀態。
ngo 預製了以下四個檢查入口
-
/health/online:流量灰度中容器上線時調用,允許服務開始接受請求。
-
/health/offline:流量灰度中容器下線時調用,停止接收請求。
-
/health/check:提供 k8s liveness 探針,展示當前進程存活狀態,如果檢查失敗,POD 將會重啓。
-
/health/status:提供 k8s readiness 探針,表明當前服務狀態,是否能提供服務,如果檢查失敗,EndPoint 將會被刪除,不會對外提供服務。
下線的接口,當 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 邏輯。
-
app.PreStart():啓動前執行的函數,使用方可以在這裏執行一些自定義的初始化。
-
app.AfterStop():關閉後執行的函數,使用方可以在這裏執行一些自定義的資源釋放操作。
具體代碼如下
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 是請求在分佈式系統中的整個鏈路視圖,一次調用會有一個全局唯一個 traceID,請求會一直攜帶 traceID 往下游服務傳遞,用來串聯鏈路。
-
span 則代表整個鏈路中不同服務內部的視圖,span 組合在一起就是整個 trace 的視圖,span 同樣會生成一個 spanID,並和 traceID 一起傳遞給下游服務,用來被下游服務記做 parentSpanID。
一次調用鏈路(單個 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
-
db
-
redis
-
httpclient
這裏拿 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 兩個參數負責配置模塊的初始化,其中
-
-c 參數指定數據源,格式爲 {schema}://{addr},schema 爲數據源類型,不指定默認爲本地文件 file。
-
-w 參數爲 bool 類型,默認爲 false,表示不監聽配置變化,如果設置爲 true,則監聽配置變化,並且發送事件。
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
}
方式二相對方式一有明顯的優勢
-
在使用層面,用戶不需要理解第三方 api,直接使用全局內置的配置模塊 api 即可。
-
在設計層面,ngo 的配置模塊可以快速的增加其他數據源,用戶在切換數據源時不需要修改代碼。
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 地址如下:
- https://github.com/NetEase-Media/ngo
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/xAj3zuZd3ZmOzj2yaAbRvA