[istio源碼分析][pilot] pilot之configController (mcp client)

1. 前言

轉載請說明原文出處, 尊重他人勞動成果!

源碼位置: https://github.com/nicktming/istio
分支: tming-v1.3.6 (基于1.3.6版本)

1. [istio源碼分析][galley] galley之上游(source)
2. [istio源碼分析][galley] galley之runtime
3. [istio源碼分析][galley] galley之下游(mcp)
在前面幾篇文章中已經分析了galley的整個流程, galley中最終把從source(fs, k8s) 中獲得的數據會從mcp serverpushmcp client, 那本文將會分析pilotconfigController是如何使用mcp client來接收數據并如何處理的.

2. ConfigController

先看一下configControllerpilot是如何初始化的.

// pilot/cmd/pilot-discovery/main.go
var (
    serverArgs = bootstrap.PilotArgs{
        CtrlZOptions:     ctrlz.DefaultOptions(),
        KeepaliveOptions: keepalive.DefaultOption(),
    }
    ...
)
...
discoveryServer, err := bootstrap.NewServer(serverArgs)
...

// pilot/pkg/bootstrap/server.go
func NewServer(args PilotArgs) (*Server, error) {
    if err := s.initMesh(&args); err != nil {
        return nil, fmt.Errorf("mesh: %v", err)
    }
    ...
    if err := s.initConfigController(&args); err != nil {
        return nil, fmt.Errorf("config controller: %v", err)
    }
}

對分析不影響的代碼直接刪減了.

func (s *Server) initConfigController(args *PilotArgs) error {
    if len(s.mesh.ConfigSources) > 0 {
        // 如果有config source的配置 則配置mcp client
        if err := s.initMCPConfigController(args); err != nil {
            return err
        }
    } 
    ...
    // Create the config store.
    s.istioConfigStore = model.MakeIstioStore(s.configController)
    return nil
}

1. 可以看到s.istioConfigStore實質上就是s.configController.
2. 主要關注mcp configuration, 關于mesh配置信息可以參考 [istio源碼分析] istio源碼開發調試版簡單安裝 .

2.1 initMCPConfigController

func (s *Server) initMCPConfigController(args *PilotArgs) error {
    clientNodeID := ""
    collections := make([]sink.CollectionOptions, len(model.IstioConfigTypes))
    for i, t := range model.IstioConfigTypes {
        // 都是istio crd資源 沒有原生的k8s資源 比如pod, service等
        collections[i] = sink.CollectionOptions{Name: t.Collection, Incremental: false}
    }

    options := coredatamodel.Options{
        DomainSuffix: args.Config.ControllerOptions.DomainSuffix,
        // 后面會用到
        ClearDiscoveryServerCache: func() {
            s.EnvoyXdsServer.ConfigUpdate(&model.PushRequest{Full: true})
        },
    }
    ...
    for _, configSource := range s.mesh.ConfigSources {
        if strings.Contains(configSource.Address, fsScheme+"://") {
            ...
        }
        // 設置安全訪問的情況 在以后分析policy的時候會用到
        securityOption := grpc.WithInsecure()
        if configSource.TlsSettings != nil &&
            configSource.TlsSettings.Mode != istio_networking_v1alpha3.TLSSettings_DISABLE {
            ...
        }
        ...
        conn, err := grpc.DialContext(
            ctx, configSource.Address,
            securityOption, msgSizeOption, keepaliveOption, initialWindowSizeOption, initialConnWindowSizeOption)
        ...
        // 創建一個controller
        mcpController := coredatamodel.NewController(options)
        sinkOptions := &sink.Options{
            CollectionOptions: collections,
            Updater:           mcpController,
            ID:                clientNodeID,
            Reporter:          reporter,
        }
        // 創建mcp client
        cl := mcpapi.NewResourceSourceClient(conn)
        mcpClient := sink.NewClient(cl, sinkOptions)
        configz.Register(mcpClient)
        clients = append(clients, mcpClient)

        conns = append(conns, conn)
        // 將該controller加入到configStores
        configStores = append(configStores, mcpController)
    }
    ...
    // Wrap the config controller with a cache.
    aggregateMcpController, err := configaggregate.MakeCache(configStores)
    if err != nil {
        return err
    }
    s.configController = aggregateMcpController
    return nil
}

1. 關注options.ClearDiscoveryServerCache, 后面會用到.
2. coredatamodel.NewController(options)創建一個controller.
3. sink.NewClient(cl, sinkOptions)創建一個mcp client, 注意sinkOptions.Updater就是2.中創建的controller. 另外mcp server端在galley.
4. configaggregate.MakeCache(configStores) 是將所有的controller按照其支持的collection(比如virtualService對應了哪些controller)進行分類起來.

2.2 mcp client

// pkg/mcp/sink/client_sink.go
func NewClient(client mcp.ResourceSourceClient, options *Options) *Client {
    return &Client{
        Sink:     New(options),
        reporter: options.Reporter,
        client:   client,
    }
}
// pkg/mcp/sink/sink.go
func New(options *Options) *Sink { // nolint: lll
    nodeInfo := &mcp.SinkNode{
        Id:          options.ID,
        Annotations: options.Metadata,
    }
    state := make(map[string]*perCollectionState)
    // state來自options.CollectionOptions
    for _, collection := range options.CollectionOptions {
        state[collection.Name] = &perCollectionState{
            versions:           make(map[string]string),
            requestIncremental: collection.Incremental,
        }
    }
    return &Sink{
        ...
    }
}

這里需要關注如下:
1. 可以看到Sink中的state來自options.CollectionOptions, 往上追溯到initMCPConfigControllermodel.IstioConfigTypes.

IstioConfigTypes = ConfigDescriptor{
        VirtualService,
        Gateway,
        ServiceEntry,
        DestinationRule,
        EnvoyFilter,
        Sidecar,
        HTTPAPISpec,
        HTTPAPISpecBinding,
        QuotaSpec,
        QuotaSpecBinding,
        AuthenticationPolicy,
        AuthenticationMeshPolicy,
        ServiceRole,
        ServiceRoleBinding,
        RbacConfig,
        ClusterRbacConfig,
    }

可以看到model.IstioConfigTypes中看到的都是istio中的一些crd資源, 也就是說從galley中得到的config resource都是這些資源, 沒有k8s中的原生資源, 比如Pod等.

2.3 mcp client Start

// pkg/mcp/sink/client_sink.go
func (c *Client) Run(ctx context.Context) {
    ...
    for {
        // 建立連接
        for {
            ...
            stream, err := c.client.EstablishResourceStream(ctx)
            ...
        }
        // 處理
        err := c.ProcessStream(c.stream)
        ...
    }
}
// pkg/mcp/sink/sink.go
func (sink *Sink) ProcessStream(stream Stream) error {
    // send initial requests for each supported type
    // 為每一個支持的類型發送一個初始的請求
    initialRequests := sink.createInitialRequests()
    for {
        var req *mcp.RequestResources
        if len(initialRequests) > 0 {
            // 發送初始request
            req = initialRequests[0]
            initialRequests = initialRequests[1:]
        } else {
            // 從server端接收response
            resources, err := stream.Recv()
            if err != nil {
                if err != io.EOF {
                    sink.reporter.RecordRecvError(err, status.Code(err))
                    scope.Errorf("Error receiving MCP resource: %v", err)
                }
                return err
            }
            // client端處理后需要發送ACK/NACK
            // 所以處理response后組裝了一個request
            req = sink.handleResponse(resources)
        }

        sink.journal.RecordRequestResources(req)
        // 向server端發送request
        if err := stream.Send(req); err != nil {
            sink.reporter.RecordSendError(err, status.Code(err))
            scope.Errorf("Error sending MCP request: %v", err)
            return err
        }
    }
}

關于mcp中的clientserver之間的交互在 [istio源碼分析][galley] galley之下游(mcp) 中已經有介紹, 這里再次說明一下.

關于mcp可以參考 https://github.com/istio/api/tree/master/mcp, 這里用此圖可以增加理解

mcp.png

對比此圖和ProcessStream來進行說明:
1. client端為每一個支持的類型initialRequests發送一個初始的請求.
2. server端會返回一個response.
3. client端需要返回一個ACK/NACK, 所以ProcessStream中的sink.handleResponse(resources)中處理完response又構造了一個新的request來返回給server端.

所以先看一下都發了哪些類型:

func (sink *Sink) createInitialRequests() []*mcp.RequestResources {
    sink.mu.Lock()

    initialRequests := make([]*mcp.RequestResources, 0, len(sink.state))
    // sink.state 來源自 initMCPConfigController中的model.IstioConfigTypes
    for collection, state := range sink.state {
        var initialResourceVersions map[string]string
        if state.requestIncremental {
            ...
        }
        req := &mcp.RequestResources{
            SinkNode:                sink.nodeInfo,
            Collection:              collection,
            InitialResourceVersions: initialResourceVersions,
            Incremental:             state.requestIncremental,
        }
        initialRequests = append(initialRequests, req)
    }
    sink.mu.Unlock()
    return initialRequests
}

可以看到發送的類型就是initMCPConfigController中的model.IstioConfigTypes.

2.4 handleResponse

func (sink *Sink) handleResponse(resources *mcp.Resources) *mcp.RequestResources {
    if handleResponseDoneProbe != nil {
        defer handleResponseDoneProbe()
    }
    // 必須是支持的類型
    state, ok := sink.state[resources.Collection]
    if !ok {
        errDetails := status.Errorf(codes.Unimplemented, "unsupported collection %v", resources.Collection)
        return sink.sendNACKRequest(resources, errDetails)
    }

    change := &Change{
        Collection:        resources.Collection,
        Objects:           make([]*Object, 0, len(resources.Resources)),
        Removed:           resources.RemovedResources,
        Incremental:       resources.Incremental,
        SystemVersionInfo: resources.SystemVersionInfo,
    }
    
    for _, resource := range resources.Resources {
        var dynamicAny types.DynamicAny
        if err := types.UnmarshalAny(resource.Body, &dynamicAny); err != nil {
            return sink.sendNACKRequest(resources, err)
        }

        // TODO - use galley metadata to verify collection and type_url match?
        object := &Object{
            TypeURL:  resource.Body.TypeUrl,
            Metadata: resource.Metadata,
            Body:     dynamicAny.Message,
        }
        change.Objects = append(change.Objects, object)
    }

    if err := sink.updater.Apply(change); err != nil {
        // 發送NACK
        errDetails := status.Error(codes.InvalidArgument, err.Error())
        return sink.sendNACKRequest(resources, errDetails)
    }
    ...
    // ACK
    sink.reporter.RecordRequestAck(resources.Collection, 0)
    req := &mcp.RequestResources{
        SinkNode:      sink.nodeInfo,
        Collection:    resources.Collection,
        ResponseNonce: resources.Nonce,
        Incremental:   useIncremental,
    }
    return req
}

1. 根據response發送回來的數據組裝成change, 并將該change作為參數調用sink.updater.Apply方法.
2. 有任何錯誤會發送NACKserver端, 如果沒有錯誤就發送ACKserver端.

sink.updater是什么呢? 在initMCPConfigController中可以看到:

        options := coredatamodel.Options{
            DomainSuffix: args.Config.ControllerOptions.DomainSuffix,
             // 后面會用到
            ClearDiscoveryServerCache: func() {
                s.EnvoyXdsServer.ConfigUpdate(&model.PushRequest{Full: true})
             },
        }
        ...
        mcpController := coredatamodel.NewController(options)
        sinkOptions := &sink.Options{
            CollectionOptions: collections,
            Updater:           mcpController,
            ID:                clientNodeID,
            Reporter:          reporter,
        }
        // 創建mcp client
        cl := mcpapi.NewResourceSourceClient(conn)
        mcpClient := sink.NewClient(cl, sinkOptions)

sink.updater就是mcpController.

3. Controller

func NewController(options Options) CoreDataModel {
    descriptorsByMessageName := make(map[string]model.ProtoSchema, len(model.IstioConfigTypes))
    synced := make(map[string]bool)
    for _, descriptor := range model.IstioConfigTypes {
        // don't register duplicate descriptors for the same collection
        if _, ok := descriptorsByMessageName[descriptor.Collection]; !ok {
            descriptorsByMessageName[descriptor.Collection] = descriptor
            synced[descriptor.Collection] = false
        }
    }
    return &Controller{
        ...
    }
}

關注一下descriptorsByMessageName是如何生成的即可.

3.1 Apply

func (c *Controller) Apply(change *sink.Change) error {
    descriptor, ok := c.descriptorsByCollection[change.Collection]
    if !ok {
        return fmt.Errorf("apply type not supported %s", change.Collection)
    }

    schema, valid := c.ConfigDescriptor().GetByType(descriptor.Type)
    if !valid {
        return fmt.Errorf("descriptor type not supported %s", descriptor.Type)
    }

    c.syncedMu.Lock()
    c.synced[change.Collection] = true
    c.syncedMu.Unlock()

    // innerStore is [namespace][name]
    innerStore := make(map[string]map[string]*model.Config)
    // 根據change的信息生成以innerStore
    for _, obj := range change.Objects {
        //構造innerStore
    }

    var prevStore map[string]map[string]*model.Config

    c.configStoreMu.Lock()
    prevStore = c.configStore[descriptor.Type]
    c.configStore[descriptor.Type] = innerStore
    c.configStoreMu.Unlock()

    if descriptor.Type == model.ServiceEntry.Type {
        c.serviceEntryEvents(innerStore, prevStore)
    } else {
        c.options.ClearDiscoveryServerCache()
    }

    return nil
}

1. 根據change構造innerStore, 進而更新該類型在c.configStore中的內容.
2. 根據舊內容prevStore和新內容innerStore來做分發工作.

2.1 如果是ServiceEntry, 調用serviceEntryEvents方法.

func (c *Controller) serviceEntryEvents(currentStore, prevStore map[string]map[string]*model.Config) {
    dispatch := func(model model.Config, event model.Event) {}
    if handlers, ok := c.eventHandlers[model.ServiceEntry.Type]; ok {
        dispatch = func(model model.Config, event model.Event) {
            log.Debugf("MCP event dispatch: key=%v event=%v", model.Key(), event.String())
            for _, handler := range handlers {
                handler(model, event)
            }
        }
    }

    // add/update
    for namespace, byName := range currentStore {
        for name, config := range byName {
            if prevByNamespace, ok := prevStore[namespace]; ok {
                if prevConfig, ok := prevByNamespace[name]; ok {
                    if config.ResourceVersion != prevConfig.ResourceVersion {
                        dispatch(*config, model.EventUpdate)
                    }
                } else {
                    dispatch(*config, model.EventAdd)
                }
            } else {
                dispatch(*config, model.EventAdd)
            }
        }
    }
    ...
}
func (c *Controller) RegisterEventHandler(typ string, handler func(model.Config, model.Event)) {
    c.eventHandlers[typ] = append(c.eventHandlers[typ], handler)
}

通過注冊好了的handler來處理這些生成的事件.

2.2 如果不是ServiceEntry, 調用ClearDiscoveryServerCache方法.

s.EnvoyXdsServer.ConfigUpdate(&model.PushRequest{Full: true})

所以這個放到以后分析.

4. 總結

pilot.png

mcp server中接收數據后通過handleResponse調用controllerApply方法, 通過類型來進行處理. 處理完向server端返回ACK/NACK.

5. 參考

1. istio 1.3.6源碼
2. https://cloud.tencent.com/developer/article/1409159

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容