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

軒脈刃發表於2020-12-21

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

相關文章