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


​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 的具體實現還有待研究。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM