grafana 的主體架構是如何設計的?

​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 的類型

若是咱們本身定義 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 的具體實現還有待研究。

20201216121821

相關文章
相關標籤/搜索