istio源碼解析系列(三)-Mixer工作流程淺析
來自專欄 Istio源碼解析系列
前言
本系列文章主要從源碼(35e2b904)出發,對istio做深入剖析,讓大家對istio有更深的認知,從而方便平時排查問題。不了解Service Mesh和Istio的同學請先閱讀敖小劍老師如下文章進行概念上的理解:
Service Mesh:下一代微服務 | 敖小劍的博客服務網格新生代-Istio | 敖小劍的博客概念介紹
Mixer提供三個核心功能:
- 前置條件檢查(Precondition Checking):某一服務響應外部請求前,通過Envoy向Mixer發送Check請求,檢查該請求是否滿足一定的前提條件,包括白名單檢查、ACL檢查等。
- 配額管理(Quota Management):當多個請求發生資源競爭時,通過配額管理機制可以實現對資源的有效管理。
- 遙測報告上報(elemetry Reporting):該服務處理完請求後,通過Envoy向Mixer上報日誌和監控等數據。
要深入了解Mixer,我們先對如下幾個概念理解:
attribute(屬性)
大部分attributes由Envoy提供。Istio用attributes來控制服務在Service Mesh中運行時行為。attributes是有名稱和類型的元數據,用來描述入口和出口流量和流量產生時的環境。attributes攜帶了一些具體信息,比如:API請求狀態碼、請求響應時間、TCP連接的原始地址等。
refrencedAttributes(被引用的屬性)
refrencedAttributes是Mixer Check時進行條件匹配後被使用的屬性的集合。Envoy向Mixer發送的Check請求中傳遞的是屬性的全集,refrencedAttributes只是其中一個子集。
舉個例子,Envoy某次發送的Check請求中發送的attributes為{request.path: xyz/abc, request.size: 234,source.ip: 192.168.0.1}
,如Mixer中調度到的多個adapters只用到了request.path
和request.size
這兩個屬性。那麼Check後返回的refrencedAttributes為{request.path: xyz/abc, request.size: 234}
。
為防止每次請求時Envoy都向Mixer中發送Check請求,Mixer中建立了一套複雜的緩存機制,使得大部分請求不需要向Mixer發送Check請求。
request.path: xyz/abcrequest.size: 234request.time: 12:34:56.789 04/17/2017source.ip: 192.168.0.1destination.service: example
屬性辭彙由[_.a-z0-9]組成,其中"."為命名空間分隔符,所有屬性辭彙可以查看這裡,屬性類型可以查看這裡。
adapter(適配器)
Mixer是一個高度模塊化、可擴展組件,內部提供了多個適配器(adapter)。
Envoy提供request級別的屬性(attributes)數據。adapters基於這些attributes來實現日誌記錄、監控指標採集展示、配額管理、ACL檢查等功能。Istio內置的部分adapters舉例如下:
- circonus:一個微服務監控分析平台。
- cloudwatch:一個針對AWS雲資源監控的工具。
- fluentd:一款開源的日誌採集工具。
- prometheus:一款開源的時序資料庫,非常適合用來存儲監控指標數據。
- statsd:一款採集匯總應用指標的工具。
- stdio:stdio適配器使Istio能將日誌和metrics輸出到本地,結合內置的ES、Grafana就可以查看相應的日誌或指標了。
template(模板)
不同的adapter需要不同的attributes,template定義了attributes到adapter輸入數據的映射的schema,一個適配器可以支持任意多個template。一個上報metric數據的模板如下所示:
apiVersion: "config.istio.io/v1alpha2"kind: metricmetadata: name: requestsize namespace: istio-systemspec: value: request.size | 0 dimensions: source_service: source.service | "unknown" source_version: source.labels["version"] | "unknown" destination_service: destination.service | "unknown" destination_version: destination.labels["version"] | "unknown" response_code: response.code | 200 monitored_resource_type: "UNSPECIFIED"
模板欄位的值可以是字面量或者表達式,如果時表達式,則表達式的值類型必須與欄位的數據類型一致。
Mixer配置文件
Mixer的yaml配置可以抽象成下述三種類型:Handler、Instance、Rule
這三種類型主要通過yaml中的kind欄位做區分,kind值有如下幾種:- adapter kind:表示配置段為Handler。
- template kind:表示配置段為Instance。
- "rule":表示配置段為Rule。
Handler(處理者)
一個handler是配置好的adpater的實例。handler從yaml配置文件中取出adapter需要的配置數據。一個典型的promethues handler配置如下所示:
apiVersion: config.istio.io/v1alpha2kind: prometheusmetadata: name: handler namespace: istio-systemspec: metrics: - name: request_count instance_name: requestcount.metric.istio-system kind: COUNTER label_names: - destination_service - destination_version - response_code
對於handler而言,{metadata.name}.{kind}.{metadata.namespace}
是其完全限定名(Fully Qualified name),上述handler的完全限定名是handler.prometheus.istio-system
,完全限定名是全局唯一的。
adapter的配置信息定義在spec段中,每個adapter配置的格式都有所區別,可以從[這裡](https://istio.io/docs/reference/config/adapters/)查看指定的adapter配置格式。
Instance(請求實例)
instance定義了attributes到adapter輸入的映射,一個處理requestduration metric數據的實例配置如下所示:
apiVersion: config.istio.io/v1alpha2kind: metricmetadata: name: requestduration namespace: istio-systemspec: value: response.duration | "0ms" dimensions: destination_service: destination.service | "unknown" destination_version: destination.labels["version"] | "unknown" response_code: response.code | 200 monitored_resource_type: "UNSPECIFIED"
完全限定名(Full Qualified Name)上述instance的
Rule(規則)
Rule定義了什麼instance生效的條件,一個簡單的Rule配置如下所示:
apiVersion: config.istio.io/v1alpha2kind: rulemetadata: name: promhttp namespace: istio-systemspec: match: destination.service == "service1.ns.svc.cluster.local" && request.headers["x-user"] == "user1" actions: - handler: handler.prometheus instances: - requestduration.metric.istio-system
上述例子中,定義的Rule為:對目標服務為service1.ns.svc.cluster.local且request.headers["x-user"] 為user1的請求,我們才應用requestduration.metric.istio-system
這個instance。
Mixer工作流程源碼分析
上面簡單介紹了Mixer相關概念,下面我們從源碼出發來對Mixer工作流程做分析。
編譯mixer二進位文件和docker鏡像
先看Makfile:
···MIXER_GO_BINS:=${ISTIO_OUT}/mixs ${ISTIO_OUT}/mixcmixc: # Mixer客戶端,通過mixc我們可以和運行的mixer進行交互。 bin/gobuild.sh ${ISTIO_OUT}/mixc istio.io/istio/pkg/version ./mixer/cmd/mixcmixs: # Mixer服務端,和Envoy、adapter交互。部署Istio的時候隨之啟動。 bin/gobuild.sh ${ISTIO_OUT}/mixs istio.io/istio/pkg/version ./mixer/cmd/mixs···include tools/istio-docker.mk # 引入編譯docker鏡像的Makefile文件。...
Makefile中定義了mixs(mixer server)和mixc(mixer client)的編譯流程。使用指令make mixs mixc
編譯好二進位文件後,再編譯docker鏡像。istio-docker.mk中編譯mixer鏡像相關指令如下:
...MIXER_DOCKER:=docker.mixer docker.mixer_debug$(MIXER_DOCKER): mixer/docker/Dockerfile$$(suffix $$@) $(ISTIO_DOCKER)/ca-certificates.tgz $(ISTIO_DOCKER)/mixs | $(ISTIO_DOCKER) $(DOCKER_RULE)...
執行make docker.mixer
會在本地編譯mixer鏡像,依據的dockerfile是mixer/docker/Dockerfile.mixer,如下所示:
FROM scratch# obtained from debian ca-certs deb using fetch_cacerts.shADD ca-certificates.tgz /ADD mixs /usr/local/bin/ENTRYPOINT ["/usr/local/bin/mixs", "server"]CMD ["--configStoreURL=fs:///etc/opt/mixer/configroot","--configStoreURL=k8s://"]
可以知道容器啟動時執行的mixs指令為/usr/local/bin/mixs server --configStoreURL=fs:///etc/opt/mixer/configroot --configStoreURL=k8s://
Mixer Server啟動流程
mixs啟動入口:
// supportedTemplates 從mixer/pkg/template包獲取所有註冊的模板信息。func supportedTemplates() map[string]template.Info { return generatedTmplRepo.SupportedTmplInfo}// supportedAdapters 從mixer/pkg/adapter包獲取所有註冊的適配器信息。func supportedAdapters() []adptr.InfoFn { return adapter.Inventory()}func main() { // 構造cobra.Command實例,mixs server子命令設計在serverCmd中定義。 rootCmd := cmd.GetRootCmd(os.Args[1:], supportedTemplates(), supportedAdapters(), shared.Printf, shared.Fatalf) if err := rootCmd.Execute(); err != nil { os.Exit(-1) }}
mixs server子命令在istio/mixer/cmd/mixs/cmd/server.go#serverCmd中定義:
func serverCmd(info map[string]template.Info, adapters []adapter.InfoFn, printf, fatalf shared.FormatFn) *cobra.Command { ... serverCmd := &cobra.Command{ Use: "server", Short: "Starts Mixer as a server", Run: func(cmd *cobra.Command, args []string) { // 用戶執行mixs server命令時,啟動mixer gRPC server runServer(sa, printf, fatalf) }, } ...}// runServer函數啟動mixer gRPC serverfunc runServer(sa *server.Args, printf, fatalf shared.FormatFn) { s, err := server.New(sa) ... s.Run() ...}
gRPC server啟動主要邏輯在istio/mixer/pkg/server/server.go#newServer:
func newServer(a *Args, p *patchTable) (*Server, error) { ... s := &Server{} // 初始化API worker線程池 s.gp = pool.NewGoroutinePool(apiPoolSize, a.SingleThreaded) s.gp.AddWorkers(apiPoolSize) // 初始化adapter worker線程池 s.adapterGP = pool.NewGoroutinePool(adapterPoolSize, a.SingleThreaded) s.adapterGP.AddWorkers(adapterPoolSize) // 構造存放Mixer模板倉庫 tmplRepo := template.NewRepository(a.Templates) // 構造存放adapter的map adapterMap := config.AdapterInfoMap(a.Adapters, tmplRepo.SupportsTemplate) ... // 構造Mixer runtime實例。runtime實例是Mixer運行時環境的主要入口。 // 它會監聽配置變更,配置變更時會動態構造新的handler實例和dispatcher實例。 // dispatcher會基於配置和attributes對請求進行調度,調用相應的adapters處理請求。 rt = p.newRuntime(st, templateMap, adapterMap, a.ConfigIdentityAttribute, a.ConfigDefaultNamespace, s.gp, s.adapterGP, a.TracingOptions.TracingEnabled()) // runtime實例開始監聽配置變更,一旦配置變更,runtime實例會構造新的dispatcher。 p.runtimeListen(rt) s.dispatcher = rt.Dispatcher() ... // 註冊Mixer gRPC server mixerpb.RegisterMixerServer(s.server, api.NewGRPCServer(s.dispatcher, s.gp)) // 啟動ControlZ監聽器,ControlZ提供了Istio的內省功能。Mixer與ctrlz集成時,會啟動一個 // web service監聽器用於展示Mixer的環境變數、參數版本信息、內存信息、進程信息、metrics等。 go ctrlz.Run(a.IntrospectionOptions, nil) return s, nil}
其中istio/mixer/pkg/api/grpcServer.go#NewGRPCServer函數中初始化了保存attributes的list和全局字典
func NewGRPCServer(dispatcher dispatcher.Dispatcher, gp *pool.GoroutinePool) mixerpb.MixerServer { // 從globalList拷貝出list切片,list形如[]string{"source.ip","source.port","request.id"...} list := attribute.GlobalList() // 將以attribute.name作為key,index作為value,構造map。形如:map[string][int]{"source.ip":1, "source.port":2, "request.id":3...} globalDict := make(map[string]int32, len(list)) for i := 0; i < len(list); i++ { globalDict[list[i]] = int32(i) } return &grpcServer{ dispatcher: dispatcher, gp: gp, globalWordList: list, globalDict: globalDict, }}
Mixer啟動的gRPC server定義了兩個rpc:Check、Report。
istio/vendor/http://istio.io/api/mixer/v1/service.proto#48行
service Mixer { // Check 基於活動配置和Envoy提供的attributes,執行前置條件檢查和配額管理。 rpc Check(CheckRequest) returns (CheckResponse) {} // Reports 基於活動配置和Envoy提供的attribues上報遙測數據(如logs和metrics)。 rpc Report(ReportRequest) returns (ReportResponse) {}}
CheckRequest、CheckResponse結構如下所示:
message CheckRequest { // QuotaParams 定義了配額管理相關的參數。 message QuotaParams { int64 amount = 1; // amount 為可分配的配額總數 bool best_effort = 2; // best_effort 為真時,表示返回的配額數小於請求的配額數 } // CompressedAttributes 為壓縮過的本次請求的attributes CompressedAttributes attributes = 1 [(gogoproto.nullable) = false]; // global_word_count 為attribute字典單詞總數,用於判斷客戶端和Mixer gRPC server所用的字典是否同步 uint32 global_word_count = 2; // deduplication_id 用於某次rpc請求失敗後重試 string deduplication_id = 3; // quotas 進行分配的配額表,key為用戶自定義的配額名如「requestCount」 map<string, QuotaParams> quotas = 4 [(gogoproto.nullable) = false];}message CheckResponse { // PreconditionResult 前置條件檢查結果 message PreconditionResult { // status 請求結果狀態碼,0表示成功 google.rpc.Status status = 1 [(gogoproto.nullable) = false]; // valid_duration 用於判斷本次結果是否合法的時間總數 google.protobuf.Duration valid_duration = 2 [(gogoproto.nullable) = false, (gogoproto.stdduration) = true]; // valid_use_count 用於判斷本次結果是否合法的使用次數總數 int32 valid_use_count = 3; // CompressedAttributes 返回的attributes數據,是請求的attributes和Mixer配置產生的attributes的集合 CompressedAttributes attributes = 4 [(gogoproto.nullable) = false]; // ReferencedAttributes Mixer adapters引用過的attritbues ReferencedAttributes referenced_attributes = 5 [(gogoproto.nullable) = false]; } // QuotaResult 配額檢查結果 message QuotaResult { google.protobuf.Duration valid_duration = 1 [(gogoproto.nullable) = false, (gogoproto.stdduration) = true]; // 授予的配額總數 int64 granted_amount = 2; ReferencedAttributes referenced_attributes = 5 [(gogoproto.nullable) = false]; } PreconditionResult precondition = 2 [(gogoproto.nullable) = false]; map<string, QuotaResult> quotas = 3 [(gogoproto.nullable) = false];}
ReportRequest、ReportResponse結構如下所示:
message ReportRequest { // CompressedAttributes 本次請求的attributes數據 repeated CompressedAttributes attributes = 1 [(gogoproto.nullable) = false]; // default_words 默認的message級別的attributes字典 repeated string default_words = 2; // global_word_count 全局attribute字典總數 uint32 global_word_count = 3;}message ReportResponse {}
Check請求執行細節
func (s *grpcServer) Check(legacyCtx legacyContext.Context, req *mixerpb.CheckRequest) (*mixerpb.CheckResponse, error) { // 構造基於proto的屬性包protoBag。protoBag提供了對一組attributes進行訪問、修改的機制。 protoBag := attribute.NewProtoBag(&req.Attributes, s.globalDict, s.globalWordList) defer protoBag.Done() // 構造可變的(執行check方法後會變化)屬性包checkBag checkBag := attribute.GetMutableBag(protoBag) defer checkBag.Done() // 執行dispatcher的預處理過程,s.dispatcher為runtime實例impl。 // impl的Preprocess方法會調度生成屬性相關的adapter,比如kubernetes adapter。 s.dispatcher.Preprocess(legacyCtx, protoBag, checkBag); // 獲取屬性包中被引用的屬性快照snapApa,snapApa能在每次check和quota處理中重複使用。 snapApa := protoBag.SnapshotReferencedAttributes() // 執行dispatcher的前置條件檢查,Check方法內部會計算被引用的屬性並同步到protoBag中。 cr, err := s.dispatcher.Check(legacyCtx, checkBag) ... // 構造Check rpc response實例 resp := &mixerpb.CheckResponse{ Precondition: mixerpb.CheckResponse_PreconditionResult{ ValidDuration: cr.ValidDuration, ValidUseCount: cr.ValidUseCount, Status: cr.Status, ReferencedAttributes: protoBag.GetReferencedAttributes(s.globalDict, globalWordCount), }, } // 如果前置條件檢查通過且配額表總數大於0,則計算新的配額 if status.IsOK(resp.Precondition.Status) && len(req.Quotas) > 0 { resp.Quotas = make(map[string]mixerpb.CheckResponse_QuotaResult, len(req.Quotas)) // 遍歷配額表,計算每個配額是否為引用配額 for name, param := range req.Quotas { qma := &dispatcher.QuotaMethodArgs{ Quota: name, Amount: param.Amount, DeduplicationID: req.DeduplicationId + name, BestEffort: param.BestEffort, } protoBag.RestoreReferencedAttributes(snapApa) crqr := mixerpb.CheckResponse_QuotaResult{} var qr *adapter.QuotaResult // 執行dispacher的配額處理方法。istio/mixer/pkg/runtime/dispatcher/dispatcher.go#func (d *Impl) Quota() qr, err = s.dispatcher.Quota(legacyCtx, checkBag, qma) if err != nil { err = fmt.Errorf("performing quota alloc failed: %v", err) log.Errora("Quota failure:", err.Error()) } else if qr == nil { crqr.ValidDuration = defaultValidDuration crqr.GrantedAmount = qma.Amount } else { if !status.IsOK(qr.Status) { log.Debugf("Quota denied: %v", qr.Status) } crqr.ValidDuration = qr.ValidDuration crqr.GrantedAmount = qr.Amount } // 根據全局attribute字典來計算被引用的attributes crqr.ReferencedAttributes = protoBag.GetReferencedAttributes(s.globalDict, globalWordCount) resp.Quotas[name] = crqr } } // 返回Check gRPC相應結果 return resp, nil}
Report請求執行整體邏輯和Check相似,本文暫不做解析。
Mixer適配器工作流程
- Mixer server啟動。
- 初始化adapter worker線程池
- 初始化Mixer模板倉庫。
- 初始化adapter builder表。
- 初始化runtime實例。
- 註冊並啟動gRPC server。
- 某一服務外部請求被envoy攔截,envoy根據請求生成指定的attributes,attributes作為參數之一向Mixer發起Check rpc請求。
- Mixer 進行前置條件檢查和配額檢查,調用相應的adapter做處理,並返回相應結果。
- Envoy分析結果,決定是否執行請求或拒絕請求。若可以執行請求則執行請求。請求完成後再向Mixer gRPC服務發起Report rpc請求,上報遙測數據。
- Mixer後端的adapter基於遙測數據做進一步處理。
作者
鄭偉,小米信息部技術架構組
招聘
小米信息部武漢研發中心,信息部是小米公司整體系統規劃建設的核心部門,支撐公司國內外的線上線下銷售服務體系、供應鏈體系、ERP體系、內網OA體系、數據決策體系等精細化管控的執行落地工作,服務小米內部所有的業務部門以及 40 家生態鏈公司。
同時部門承擔微服務體系建設落地及各類後端基礎平台研發維護,語言涉及 Go、PHP、Java,長年虛位以待對微服務、基礎架構有深入理解和實踐、或有大型電商後端系統研發經驗的各路英雄。
歡迎投遞簡歷:jin.zhang(a)http://xiaomi.com
更多技術文章:小米信息部技術團隊
推薦閱讀:
※快速開始istio
※第一篇:spring boot入門:通過官網:start.spring.io開啟Spring Boot之路
※重磅消息-Service Fabric 正式開源
※微服務落地第三課-Spring Cloud Config Client搭建
※《Cloud Native Go》筆記(七)構建數據服務
TAG:微服務架構 | Kubernetes | Go語言 |