grafana 的主體架構是如何設計的?
grafana 是非常強大的視覺化專案,它最早從 kibana 生成出來,漸漸也已經形成了自己的生態了。研究完 grafana 生態之後,只有一句話:視覺化,grafana 就夠了。
這篇就想了解下它的主體架構是如何設計的。如果你對 grafana 有興趣,不妨讓這篇成為入門讀物。
入口程式碼
grafana 的最外層就是一個 build.go,它並不是真正的入口,它只是用來編譯生成 grafana-server 工具的。
grafana 會生成兩個工具,grafana-cli 和 grafana-server。
go run build.go build-server 其實就是執行
go build ./pkg/cmd/grafana-server -o ./bin/xxx/grafana-server
這裡可以劃重點學習一下:
如果你的專案要生成多個命令列工具,又或者有多個引數,又或者有多個操作,使用 makefile 已經很複雜了,我們是可以這樣直接寫個 build.go 或者 main.go 在最外層,來負責編譯的事情。
所以真實的入口在 ./pkg/cmd/grafana-server/main.go 中。可以跟著這個入口進入。
設計結構
這篇不說細節,從巨集觀角度說下 grafana 的設計結構。帶著這個架構再去看 granfana 才更能理解其中一些細節。
grafana 中最重要的結構就是 Service。 grafana 設計的時候希望所有的功能都是 Service。是的,所有,包括使用者認證 UserAuthTokenService,日誌 LogsService, 搜尋 LoginService,報警輪訓 Service。 所以,這裡需要設計出一套靈活的 Service 執行機制。
理解這套 Service 機制就很重要了。這套機制有下列要處理的地方:
序號產生器制
首先,需要有一個 Service 的序號產生器制。
grafana 提供的是一種有優先順序的,服務序號產生器制。grafana 提供了 pkg/registry 包。
在 Service 外層包了一個結構,包含了服務的名字和服務的優先順序。
type Descriptor struct {
Name string
Instance Service
InitPriority Priority
}
這個包提供的三個註冊方法:
RegisterServiceWithPriority
RegisetrService
Register
這三個註冊方法都是把 Descriptior(本質也就是 Service)註冊到一個全域性的陣列中。
取的時候也很簡單,就是把這個全域性陣列按照優先順序排列就行。
那麼什麼時候執行註冊操作呢?答案就是在每個 Service 的 init() 函式中進行註冊操作。所以我們可以看到程式碼中有很多諸如:
_ "github.com/grafana/grafana/pkg/services/ngalert"
_ "github.com/grafana/grafana/pkg/services/notifications"
_ "github.com/grafana/grafana/pkg/services/provisioning"
的 import 操作,就是為了註冊服務的。
Service 的型別
如果我們自己定義 Service,差不多定義一個 interface 就好了,但是實際這裡是有問題的。我們有的服務需要的是後端啟動,有的服務並不需要後端啟動,而有的服務需要先建立一個資料表才能啟動,而有的服務需要根據配置檔案判斷是否開啟。要定義一個 Service 介面滿足這些需求,其實也是可以的,只是比較醜陋,而 grafana 的寫法就非常優雅了。
grafana 定義了基礎的 Service 介面,僅僅需要實現一個 Init() 方法:
type Service interface {
Init() error
}
而定義了其他不同的介面,比如需要後端啟動的服務:
type BackgroundService interface {
Run(ctx context.Context) error
}
需要資料庫註冊的服務:
type DatabaseMigrator interface {
AddMigration(mg *migrator.Migrator)
}
需要根據配置決定是否啟動的服務:
type CanBeDisabled interface {
IsDisabled() bool
}
在具體使用的時候,根據判斷這個 Service 是否符合某個介面進行判斷。
service, ok := svc.Instance.(registry.BackgroundService)
if !ok {
continue
}
這樣做的優雅之處就在於在具體定義 Service 的時候就靈活很多了。不會定義很多無用的方法實現。
這個也是 golang 鴨子型別的好處。
Service 的依賴
這裡還有一個麻煩的地方,Service 之間是有互相依賴的。比如 sqlstore.SQLStore 這個服務,是負責資料儲存的。它會在很多服務中用到,比如使用者許可權認證的時候,需要去資料儲存中獲取使用者資訊。那麼這裡如果在每個 Service 初始化的時候進行例項化,也是頗為痛苦的事情。
grafana 使用的是 facebook 的 inject.Graph 包處理這種依賴的問題的。https://github.com/facebookarchive/inject。
這個 inject 包使用的是依賴注入的解決方法,把一堆例項化的例項放進包裡面,然後使用反射技術,對於一些結構中有指定 tag 標籤的欄位,就會把對應的例項注入進去。
比如 grafana 中的:
type UserAuthTokenService struct {
SQLStore *sqlstore.SQLStore `inject:""`
ServerLockService *serverlock.ServerLockService `inject:""`
Cfg *setting.Cfg `inject:""`
log log.Logger
}
這裡可以看到 SQLStore 中有額外的注入 tag。那麼在 pkg/server/server.go 中的
services := registry.GetServices()
if err := s.buildServiceGraph(services); err != nil {
return err
}
這裡會把所有的 Service (包括這個 UserAuthTokenService) 中的 inject 標籤標記的欄位進行依賴注入。
這樣就完美解決了 Service 的依賴問題。
Service 的執行
Service 的執行在 grafana 中使用的是 errgroup, 這個包是 “golang.org/x/sync/errgroup”。
使用這個包,不僅僅可以並行 go 執行 Service,也能獲取每個 Service 返回的 error,在最後 Wait 的時候返回。
大體程式碼如下:
s.childRoutines.Go(func() error {
...
err := service.Run(s.context)
...
})
}
defer func() {
if waitErr := s.childRoutines.Wait(); waitErr != nil && !errors.Is(waitErr, context.Canceled) {
s.log.Error("A service failed", "err", waitErr)
if err == nil {
err = waitErr
}
}
}()
總結
理解了 Service 機制之後,grafana 的主流程就很簡單明瞭了。如圖所示。當然,這個只是 grafana 的主體流程,它的每個 Service 的具體實現還有待研究。