前言
本系列文章主要從源碼(35e2b904)出發(fā),對istio做深入剖析,讓大家對istio有更深的認(rèn)知,從而方便平時(shí)排查問題。不了解Service Mesh和Istio的同學(xué)請先閱讀敖小劍老師如下文章進(jìn)行概念上的理解:
服務(wù)治理配置生效流程解析
如果大家安裝bookinfo并執(zhí)行過文檔中的task,可以了解到,所有服務(wù)治理流程都是通過istioctl工具,執(zhí)行指定yaml配置文件來實(shí)現(xiàn)。那么從執(zhí)行istioctl指令到配置文件生效,整個(gè)流程到底是什么樣的呢?下面給大家做一個(gè)簡單的介紹。
整個(gè)配置生效的流程圖如下所示:

配置文件解析
以task request-routing為例,我們的需求是把名為jason的用戶訪問reviews服務(wù)的版本切換為v2。route-rule-reviews-test-v2.yaml內(nèi)容如下所示:
apiVersion: config.istio.io/v1alpha2
kind: RouteRule
metadata:
name: reviews-test-v2
spec:
destination:
name: reviews
precedence: 2
match:
request:
headers:
cookie:
regex: "^(.*?;)?(user=jason)(;.*)?$"
route:
- labels:
version: v2
解析并執(zhí)行istioctl create指令
通過istioctl create -f samples/bookinfo/kube/route-rule-reviews-test-v2.yaml指令來使規(guī)則生效,執(zhí)行istioctl create指令運(yùn)行的相關(guān)代碼入口如下:
istio/cmd/istioctl/main.go#postCmd#113行。
postCmd = &cobra.Command{
Use: "create",
Short: "Create policies and rules",
Example: "istioctl create -f example-routing.yaml",
RunE: func(c *cobra.Command, args []string) error {
if len(args) != 0 {
c.Println(c.UsageString())
return fmt.Errorf("create takes no arguments")
}
// varr為轉(zhuǎn)換成功的istio內(nèi)部model.Config切片,包括routeRule、gateway、ingressRule、egressRule、policy等
// others是不能轉(zhuǎn)換成model.Config的k8s object wrapper切片,后面會當(dāng)成mixer配置來處理
varr, others, err := readInputs()
if err != nil {
return err
}
if len(varr) == 0 && len(others) == 0 {
return errors.New("nothing to create")
}
...
}
}
解析出model.Config切片、crd.istioKind切片流程
- model.Config 為istio配置單元
- crd.IstioKind 對k8s API對象做了一層封裝
readInput函數(shù)解析create命令的相關(guān)參數(shù)(比如-f),如果是-f指定的文件是有效文件,則會調(diào)用pilot/pkg/config/kube/crd包的ParseInputs函數(shù)解析該文件。
func readInputs() ([]model.Config, []crd.IstioKind, error) {
var reader io.Reader
...
// 讀取指定yaml文件
if in, err = os.Open(file); err != nil {
return nil, nil, err
}
defer func() {
if err = in.Close(); err != nil {
log.Errorf("Error: close file from %s, %s", file, err)
}
}()
reader = in
...
input, err := ioutil.ReadAll(reader)
...
return crd.ParseInputs(string(input))
}
ParseInputs函數(shù)內(nèi)部邏輯:
func ParseInputs(inputs string) ([]model.Config, []IstioKind, error) {
var varr []model.Config
var others []IstioKind
reader := bytes.NewReader([]byte(inputs))
var empty = IstioKind{}
// We store configs as a YaML stream; there may be more than one decoder.
yamlDecoder := kubeyaml.NewYAMLOrJSONDecoder(reader, 512*1024)
for {
obj := IstioKind{}
// 從reader中反序列化出IstioKind實(shí)例obj
err := yamlDecoder.Decode(&obj)
...
schema, exists := model.IstioConfigTypes.GetByType(CamelCaseToKabobCase(obj.Kind))
...
config, err := ConvertObject(schema, &obj, "")
...
if err := schema.Validate(config.Spec); err != nil {
return nil, nil, fmt.Errorf("configuration is invalid: %v", err)
}
varr = append(varr, *config)
}
return varr, others, nil
}
ParseInputs返回三種類型的值[]Config、[]IstioKind、error。
-
istio/pilot/pkg/model#[]Config
其中Config為Istio內(nèi)部的配置單元,包含匿名ConfigMeta以及ConfigMeta序列化的protobuf message;用戶指定的yaml配置會被解析成相應(yīng)的實(shí)例。 -
pilot/pkg/config/kube/crd#[]IstioKind
IstioKind為k8s API object的一層封裝,內(nèi)部包含兩個(gè)匿名結(jié)構(gòu)體和一個(gè)map:type IstioKind struct { meta_v1.TypeMeta `json:",inline"` meta_v1.ObjectMeta `json:"metadata"` Spec map[string]interface{} `json:"spec"` }- IstioKindk8s.io/apimachinery/pkg/apis/meta/v1#TypeMeta
TypeMeta包含了k8s REST資源類型(如RouteRule)、k8s API版本號(如config.istio.io/v1alpha2)。 - k8s.io/apimachinery/pkg/apis/meta/v1#ObjectMeta
ObjectMeta包含了k8s 資源對象包含的各必要字段,包括Name、Namespace、UID等。 - Spec
一個(gè)存儲Spec數(shù)據(jù)的map。
- IstioKindk8s.io/apimachinery/pkg/apis/meta/v1#TypeMeta
上述代碼將string類型的配置反序列化成IstioKind實(shí)例后,通過model.IstioConfigTypes.GetByType()方法獲取istio的[]ProtoSchema實(shí)例。
// ConfigDescriptor 是一個(gè)由ProtoSchema組成的切片
type ConfigDescriptor []ProtoSchema
// ProtoSchema結(jié)構(gòu)體定義了配置類型名稱和protobuf消息的雙向映射
type ProtoSchema struct {
Type string // 配置的proto類型,如route-rule
Plural string // type復(fù)數(shù)形式,如route-rules
Group string // 配置的proto組名,如config
Version string // 配置API的版本號,如一lpha2
MessageName string // 配置的proto message名,如istio.routing.v1alpha1.RouteRule
Gogo bool // 是否為gogo protobuf編碼
Validate func(config proto.Message) error // protobuf校驗(yàn)函數(shù)
}
拿到schema后,通過ConvertObject方法,將k8s風(fēng)格的object實(shí)例轉(zhuǎn)換成istio內(nèi)部的Config模型實(shí)例,并根據(jù)schema類型調(diào)用相應(yīng)的校驗(yàn)函數(shù)對protobuf message進(jìn)行校驗(yàn)。
將配置變更提交到k8s
istio/cmd/istioctl/main.go#postCmd#140行。
for _, config := range varr {
// 初始化namespace數(shù)據(jù)
if config.Namespace, err = handleNamespaces(config.Namespace); err != nil {
return err
}
// 構(gòu)造k8s crd.Client實(shí)例,crd.Client包含初始化的apiVerison到restClient映射的map。
// 對每一種apiVerison(由schema.Group、"istio.io"、schema.Version組成的string,如"config.istio.io/v1alpha2"、"networking.istio.io/v1alpha3"等)
// 都對應(yīng)一個(gè)crd.restClient實(shí)例。
var configClient *crd.Client
if configClient, err = newClient(); err != nil {
return err
}
var rev string
// 通過k8s REST接口執(zhí)行配置
if rev, err = configClient.Create(config); err != nil {
return err
}
fmt.Printf("Created config %v at revision %v\n", config.Key(), rev)
}
configClient.Create方法執(zhí)行流程如下:
func (cl *Client) Create(config model.Config) (string, error) {
rc, ok := cl.clientset[apiVersionFromConfig(&config)]
...
// 根據(jù)config.Type獲取schema
schema, exists := rc.descriptor.GetByType(config.Type)
...
// 調(diào)用schema指定的Validate函數(shù),對Spec這個(gè)protobuff進(jìn)行校驗(yàn)
if err := schema.Validate(config.Spec); err != nil {
return "", multierror.Prefix(err, "validation error:")
}
// ConvertConfig函數(shù)將model.Config實(shí)例轉(zhuǎn)換成IstioObject實(shí)例。
// IstioObject是一個(gè)k8s API object的接口,crd包下有很多結(jié)構(gòu)體實(shí)現(xiàn)了該接口,如MockConfig、RouteRule等
out, err := ConvertConfig(schema, config)
...
// 檢索clientset map,用指定的restClient實(shí)例發(fā)送POST請求,使配置生效。
obj := knownTypes[schema.Type].object.DeepCopyObject().(IstioObject)
err = rc.dynamic.Post().
Namespace(out.GetObjectMeta().Namespace).
Resource(ResourceName(schema.Plural)).
Body(out).
Do().Into(obj)
if err != nil {
return "", err
}
return obj.GetObjectMeta().ResourceVersion, nil
}
pilot-discovery初始化
pilot/cmd/pilot-discovery/main.go#57行,構(gòu)造discoveryServer實(shí)例。
...
discoveryServer, err := bootstrap.NewServer(serverArgs)
if err != nil {
return fmt.Errorf("failed to create discovery service: %v", err)
}
...
監(jiān)聽k8s相關(guān)資源變更
NewServer函數(shù)內(nèi)部流程如下:
func NewServer(args PilotArgs) (*Server, error) {
...
// 初始化pilot配置控制器,根據(jù)pilot-discovery啟動指令,初始化配置控制器。
// 默認(rèn)只會初始化kube配置控制器(kubeConfigController,它實(shí)現(xiàn)了model.ConfigStoreCache接口)。
// kubeConfigController會watch k8s pod registration 、ingress resources、traffic rules等變化。
if err := s.initConfigController(&args); err != nil {
return nil, err
}
// 初始化服務(wù)發(fā)現(xiàn)控制器,控制器內(nèi)部會構(gòu)造K8sServiceControllers。
if err := s.initServiceControllers(&args); err != nil {
return nil, err
}
// 初始化DiscoveryService實(shí)例,實(shí)例內(nèi)部注冊了envoy xDS路由。
// kubeConfigController中watch到變更后,envoy輪詢xDS接口,獲取變更。
if err := s.initDiscoveryService(&args); err != nil {
return nil, err
}
...
}
注冊envoy xDS路由
initDiscoveryServic方法內(nèi)部流程如下:
func (s *Server) initDiscoveryService(args *PilotArgs) error {
// 構(gòu)造pilot runtime environment。environment中保存了kubeConfigController、serviceController等。
environment := model.Environment{
Mesh: s.mesh,
IstioConfigStore: model.MakeIstioStore(s.configController),
ServiceDiscovery: s.ServiceController,
ServiceAccounts: s.ServiceController,
MixerSAN: s.mixerSAN,
}
// 構(gòu)造DiscoveryService實(shí)例。
discovery, err := envoy.NewDiscoveryService(
s.ServiceController,
s.configController,
environment,
args.DiscoveryOptions,
)
}
NewDiscoveryService方法內(nèi)部流程如下:
func NewDiscoveryService(ctl model.Controller, configCache model.ConfigStoreCache,
environment model.Environment, o DiscoveryServiceOptions) (*DiscoveryService, error) {
out := &DiscoveryService{
Environment: environment, // 將environment賦值給Environment成員。
sdsCache: newDiscoveryCache("sds", o.EnableCaching),
cdsCache: newDiscoveryCache("cds", o.EnableCaching),
rdsCache: newDiscoveryCache("rds", o.EnableCaching),
ldsCache: newDiscoveryCache("lds", o.EnableCaching),
}
container := restful.NewContainer()
...
// 注冊web service容器。
out.Register(container)
}
out.Register方法內(nèi)部流程如下:
func (ds *DiscoveryService) Register(container *restful.Container) {
ws := &restful.WebService{}
ws.Produces(restful.MIME_JSON)
...
// 注冊Envoy xDS(SDS、CDS、RDS、LDS)路由
// 注冊 Envoy RDS(Route discovery service)路由。https://www.envoyproxy.io/docs/envoy/latest/api-v1/route_config/rds
// RDS可以與SDS、EDS協(xié)同工作,來構(gòu)建用戶指定的路由拓?fù)洌ㄈ缌髁壳袚Q、藍(lán)綠部署等)。
ws.Route(ws.
GET(fmt.Sprintf("/v1/routes/{%s}/{%s}/{%s}", RouteConfigName, ServiceCluster, ServiceNode)).
To(ds.ListRoutes).
Doc("RDS registration").
Param(ws.PathParameter(RouteConfigName, "route configuration name").DataType("string")).
Param(ws.PathParameter(ServiceCluster, "client proxy service cluster").DataType("string")).
Param(ws.PathParameter(ServiceNode, "client proxy service node").DataType("string")))
// 注冊 Envoy LDS(Listener discovery service)路由。https://www.envoyproxy.io/docs/envoy/latest/configuration/listeners/lds
// Envoy可以從通過這個(gè)接口動態(tài)獲取需要新的Listener信息,從而在運(yùn)行時(shí)動態(tài)實(shí)例化Listener。
// Listener可以用來處理不同的代理任務(wù)(如速率限制、HTTP連接管理、原始TCP代理等)。
ws.Route(ws.
GET(fmt.Sprintf("/v1/listeners/{%s}/{%s}", ServiceCluster, ServiceNode)).
To(ds.ListListeners).
Doc("LDS registration").
Param(ws.PathParameter(ServiceCluster, "client proxy service cluster").DataType("string")).
Param(ws.PathParameter(ServiceNode, "client proxy service node").DataType("string")))
...
}
- RDS路由綁定的ds.ListRoutes方法讀取environment中相關(guān)配置,返回給Envoy實(shí)例需要配置的路由信息。
- LDS路由綁定的ds.ListListeners方法讀取environment中相關(guān)配置,返回給Envoy實(shí)例需要的Listener信息。
Envoy實(shí)例輪詢xDS接口,獲取變更的配置信息,最終執(zhí)行具體的服務(wù)治理策略。
總結(jié)
結(jié)合上文中貼出的流程圖

總結(jié)如下:
- istio的piolit-discovery啟動
- 初始化kube配置控制器,控制器中watch k8s pod、ingress以及流量管理規(guī)則等變更。
- 初始化envoy各發(fā)現(xiàn)服務(wù),注冊envoy xDS路由,綁定相應(yīng)的配置變更handler。
- pilot-discovery等待envoy實(shí)例輪詢xDS接口,將變更返給envoy實(shí)例。
- 用戶通過istioctl應(yīng)用配置
- istioctl解析指令(create、delete等),通過k8s REST接口,將變更推送的k8s。
- k8s產(chǎn)生變更,變更同步到kubeConfigController中。
- envoy實(shí)例輪詢xDS接口,應(yīng)用變更。
作者
鄭偉,小米信息部技術(shù)架構(gòu)組
招聘
小米信息部武漢研發(fā)中心,信息部是小米公司整體系統(tǒng)規(guī)劃建設(shè)的核心部門,支撐公司國內(nèi)外的線上線下銷售服務(wù)體系、供應(yīng)鏈體系、ERP體系、內(nèi)網(wǎng)OA體系、數(shù)據(jù)決策體系等精細(xì)化管控的執(zhí)行落地工作,服務(wù)小米內(nèi)部所有的業(yè)務(wù)部門以及 40 家生態(tài)鏈公司。
同時(shí)部門承擔(dān)微服務(wù)體系建設(shè)落地及各類后端基礎(chǔ)平臺研發(fā)維護(hù),語言涉及 Go、PHP、Java,長年虛位以待對微服務(wù)、基礎(chǔ)架構(gòu)有深入理解和實(shí)踐、或有大型電商后端系統(tǒng)研發(fā)經(jīng)驗(yàn)的各路英雄。
歡迎投遞簡歷:jin.zhang(a)xiaomi.com
更多技術(shù)文章:小米信息部技術(shù)團(tuán)隊(duì)