istio源码解析系列(二)-服务治理配置生效流程解析

前言

本系列文章主要从源码(35e2b904)出发,对istio做深入剖析,让大家对istio有更深的认知,从而方便平时排查问题。不了解Service Mesh和Istio的同学请先阅读敖小剑老师如下文章进行概念上的理解:

服务治理配置生效流程解析

如果大家安装bookinfo并执行过文档中的task,可以了解到,所有服务治理流程都是通过istioctl工具,执行指定yaml配置文件来实现。那么从执行istioctl指令到配置文件生效,整个流程到底是什么样的呢?下面给大家做一个简单的介绍。

整个配置生效的流程图如下所示:


image.png

配置文件解析

以task request-routing为例,我们的需求是把名为jason的用户访问reviews服务的版本切换为v2。route-rule-reviews-test-v2.yaml内容如下所示:

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

解析并执行istioctl create指令

通过istioctl create -f samples/bookinfo/kube/route-rule-reviews-test-v2.yaml指令来使规则生效,执行istioctl create指令运行的相关代码入口如下:

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为转换成功的istio内部model.Config切片,包括routeRule、gateway、ingressRule、egressRule、policy等
                    // others是不能转换成model.Config的k8s object wrapper切片,后面会当成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函数解析create命令的相关参数(比如-f),如果是-f指定的文件是有效文件,则会调用pilot/pkg/config/kube/crd包的ParseInputs函数解析该文件。

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函数内部逻辑:

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实例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内部的配置单元,包含匿名ConfigMeta以及ConfigMeta序列化的protobuf message;用户指定的yaml配置会被解析成相应的实例。
  • pilot/pkg/config/kube/crd#[]IstioKind
    IstioKind为k8s API object的一层封装,内部包含两个匿名结构体和一个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
      一个存储Spec数据的map。

上述代码将string类型的配置反序列化成IstioKind实例后,通过model.IstioConfigTypes.GetByType()方法获取istio的[]ProtoSchema实例。

// ConfigDescriptor 是一个由ProtoSchema组成的切片
type ConfigDescriptor []ProtoSchema
// ProtoSchema结构体定义了配置类型名称和protobuf消息的双向映射
type ProtoSchema struct {
    Type        string // 配置的proto类型,如route-rule
    Plural      string // type复数形式,如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校验函数
}

拿到schema后,通过ConvertObject方法,将k8s风格的object实例转换成istio内部的Config模型实例,并根据schema类型调用相应的校验函数对protobuf message进行校验。

将配置变更提交到k8s

istio/cmd/istioctl/main.go#postCmd#140行。

for _, config := range varr {
    // 初始化namespace数据
    if config.Namespace, err = handleNamespaces(config.Namespace); err != nil {
        return err
    }

    // 构造k8s crd.Client实例,crd.Client包含初始化的apiVerison到restClient映射的map。
    // 对每一种apiVerison(由schema.Group、"istio.io"、schema.Version组成的string,如"config.istio.io/v1alpha2"、"networking.istio.io/v1alpha3"等)
    // 都对应一个crd.restClient实例。
    var configClient *crd.Client
    if configClient, err = newClient(); err != nil {
        return err
    }
    var rev string
    // 通过k8s REST接口执行配置
    if rev, err = configClient.Create(config); err != nil {
        return err
    }
    fmt.Printf("Created config %v at revision %v\n", config.Key(), rev)
}

configClient.Create方法执行流程如下:

func (cl *Client) Create(config model.Config) (string, error) {
    rc, ok := cl.clientset[apiVersionFromConfig(&config)]
    ...
    // 根据config.Type获取schema
    schema, exists := rc.descriptor.GetByType(config.Type)
    ...
    // 调用schema指定的Validate函数,对Spec这个protobuff进行校验
    if err := schema.Validate(config.Spec); err != nil {
        return "", multierror.Prefix(err, "validation error:")
    }
    // ConvertConfig函数将model.Config实例转换成IstioObject实例。
    // IstioObject是一个k8s API object的接口,crd包下有很多结构体实现了该接口,如MockConfig、RouteRule等
    out, err := ConvertConfig(schema, config)
    ...

    // 检索clientset map,用指定的restClient实例发送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行,构造discoveryServer实例。

...
discoveryServer, err := bootstrap.NewServer(serverArgs)
if err != nil {
    return fmt.Errorf("failed to create discovery service: %v", err)
}
...

监听k8s相关资源变更

NewServer函数内部流程如下:

func NewServer(args PilotArgs) (*Server, error) {
    ...
    // 初始化pilot配置控制器,根据pilot-discovery启动指令,初始化配置控制器。
    // 默认只会初始化kube配置控制器(kubeConfigController,它实现了model.ConfigStoreCache接口)。
    // kubeConfigController会watch k8s pod registration 、ingress resources、traffic rules等变化。
    if err := s.initConfigController(&args); err != nil {
        return nil, err
    }
    // 初始化服务发现控制器,控制器内部会构造K8sServiceControllers。
    if err := s.initServiceControllers(&args); err != nil {
        return nil, err
    }
    // 初始化DiscoveryService实例,实例内部注册了envoy xDS路由。
    // kubeConfigController中watch到变更后,envoy轮询xDS接口,获取变更。
    if err := s.initDiscoveryService(&args); err != nil {
        return nil, err
    }
    ...
}

注册envoy xDS路由

initDiscoveryServic方法内部流程如下:

func (s *Server) initDiscoveryService(args *PilotArgs) error {
    // 构造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,
    }
    // 构造DiscoveryService实例。
    discovery, err := envoy.NewDiscoveryService(
        s.ServiceController,
        s.configController,
        environment,
        args.DiscoveryOptions,
    )
}

NewDiscoveryService方法内部流程如下:

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方法内部流程如下:

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协同工作,来构建用户指定的路由拓扑(如流量切换、蓝绿部署等)。
    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可以从通过这个接口动态获取需要新的Listener信息,从而在运行时动态实例化Listener。
    // Listener可以用来处理不同的代理任务(如速率限制、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中相关配置,返回给Envoy实例需要配置的路由信息。
  • LDS路由绑定的ds.ListListeners方法读取environment中相关配置,返回给Envoy实例需要的Listener信息。
    Envoy实例轮询xDS接口,获取变更的配置信息,最终执行具体的服务治理策略。

总结

结合上文中贴出的流程图


image.png

总结如下:

  • istio的piolit-discovery启动
    1. 初始化kube配置控制器,控制器中watch k8s pod、ingress以及流量管理规则等变更。
    2. 初始化envoy各发现服务,注册envoy xDS路由,绑定相应的配置变更handler。
    3. pilot-discovery等待envoy实例轮询xDS接口,将变更返给envoy实例。
  • 用户通过istioctl应用配置
    1. istioctl解析指令(create、delete等),通过k8s REST接口,将变更推送的k8s。
    2. k8s产生变更,变更同步到kubeConfigController中。
    3. envoy实例轮询xDS接口,应用变更。

作者

郑伟,小米信息部技术架构组

招聘

小米信息部武汉研发中心,信息部是小米公司整体系统规划建设的核心部门,支撑公司国内外的线上线下销售服务体系、供应链体系、ERP体系、内网OA体系、数据决策体系等精细化管控的执行落地工作,服务小米内部所有的业务部门以及 40 家生态链公司。

同时部门承担微服务体系建设落地及各类后端基础平台研发维护,语言涉及 Go、PHP、Java,长年虚位以待对微服务、基础架构有深入理解和实践、或有大型电商后端系统研发经验的各路英雄。

欢迎投递简历:jin.zhang(a)xiaomi.com

更多技术文章:小米信息部技术团队

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容