grafana 的主體架構是如何設計的?git
grafana 是很是強大的可視化項目,它最先從 kibana 生成出來,漸漸也已經造成了本身的生態了。研究完 grafana 生態以後,只有一句話:可視化,grafana 就夠了。github
這篇就想了解下它的主體架構是如何設計的。若是你對 grafana 有興趣,不妨讓這篇成爲入門讀物。golang
grafana 的最外層就是一個 build.go,它並非真正的入口,它只是用來編譯生成 grafana-server 工具的。sql
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,差很少定義一個 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 之間是有互相依賴的。好比 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 的運行在 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 的具體實現還有待研究。