Leia o algoritmo de topologia SuperEdge em um artigo

Prefácio

Cupom de compra https://m.cqfenfa.com/

O grupo de serviço SuperEdge usa application-grid-wrapper para perceber a percepção da topologia e concluir o acesso de loop fechado aos serviços na mesma unidade de nó

Antes de analisar o aplicativo-grade-wrapper em profundidade, aqui está uma breve introdução aos recursos de topologia com suporte nativo pela comunidade Kubernetes

A versão alfa do recurso de reconhecimento de topologia do serviço Kubernetes foi lançada na v1.17 para implementar a topologia de roteamento e recursos de acesso nas proximidades. O usuário precisa adicionar o campo topologyKeys ao serviço para indicar o tipo de chave de topologia. Apenas os pontos de extremidade com o mesmo domínio de topologia serão acessados. Atualmente, há três topologyKeys para escolher:

  • "kubernetes.io/hostname": acesse kubernetes.io/hostnameo endpoint neste nó (o mesmo valor de rótulo), se não houver endpoint, o acesso ao serviço falhará
  • "topology.kubernetes.io/zone": topology.kubernetes.io/zonepontos de extremidade de acesso na mesma zona ( mesmo valor de rótulo), caso contrário, o acesso ao serviço falhará
  • "topology.kubernetes.io/region": topology.kubernetes.io/regionpontos de extremidade de acesso na mesma região ( mesmo valor de rótulo), caso contrário, o acesso ao serviço falhará

Além de preencher uma das chaves topológicas acima individualmente, você também pode construir essas chaves em uma lista para preencher, por exemplo :, ["kubernetes.io/hostname", "topology.kubernetes.io/zone", "topology.kubernetes.io/region"]isso significa: acesso prioritário ao terminal neste nó; se ele não existir, acesso a o endpoint na mesma zona; se se não existir mais, acesse o endpoint na mesma região; se não existir, o acesso falhará

Além disso, você também pode adicionar "*" no final da lista (apenas o último item) para indicar: se os domínios da topologia anterior falharem, qualquer endpoint válido é acessado, ou seja, não há restrição na topologia. Os exemplos são os seguintes:

# A Service that prefers node local, zonal, then regional endpoints but falls back to cluster wide endpoints.
apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    app: my-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 9376
  topologyKeys:
    - "kubernetes.io/hostname"
    - "topology.kubernetes.io/zone"
    - "topology.kubernetes.io/region"
    - "*"

O reconhecimento da topologia e a comparação da comunidade implementada pelo grupo de serviço têm as seguintes diferenças:

  • A chave de topologia do grupo de serviço pode ser personalizada, ou seja, gridUniqKey, que é mais flexível de usar. Atualmente, existem apenas três opções para implementação da comunidade: "kubernetes.io/hostname", "topology.kubernetes.io/zone" e " topology.kubernetes. io / region "
  • O grupo de serviço pode preencher apenas uma chave de topologia, ou seja, pode acessar apenas pontos de extremidade válidos neste domínio de topologia e não pode acessar pontos de extremidade em outros domínios de topologia; a comunidade pode acessar outros pontos de extremidade de domínio de topologia alternativa por meio da lista de chaves de topologia e "* "

Conscientização da topologia implementada pelo grupo de serviço, a configuração do serviço é a seguinte:

# A Service that only prefers node zone1al endpoints.
apiVersion: v1
kind: Service
metadata:
  annotations:
    topologyKeys: '["zone1"]'
  labels:
    superedge.io/grid-selector: servicegrid-demo
  name: servicegrid-demo-svc
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    appGrid: echo

Depois de apresentar o reconhecimento da topologia implementado pelo grupo de serviço, mergulhamos na análise do código-fonte e nos detalhes de implementação. Da mesma forma, aqui está um exemplo de uso para iniciar a análise:

# step1: labels edge nodes
$ kubectl  get nodes
NAME    STATUS   ROLES    AGE   VERSION
node0   Ready    <none>   16d   v1.16.7
node1   Ready    <none>   16d   v1.16.7
node2   Ready    <none>   16d   v1.16.7
# nodeunit1(nodegroup and servicegroup zone1)
$ kubectl --kubeconfig config label nodes node0 zone1=nodeunit1  
# nodeunit2(nodegroup and servicegroup zone1)
$ kubectl --kubeconfig config label nodes node1 zone1=nodeunit2
$ kubectl --kubeconfig config label nodes node2 zone1=nodeunit2
...
# step3: deploy echo ServiceGrid
$ cat <<EOF | kubectl --kubeconfig config apply -f -
apiVersion: superedge.io/v1
kind: ServiceGrid
metadata:
  name: servicegrid-demo
  namespace: default
spec:
  gridUniqKey: zone1
  template:
    selector:
      appGrid: echo
    ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
EOF
servicegrid.superedge.io/servicegrid-demo created
# note that there is only one relevant service generated
$ kubectl  get svc
NAME                   TYPE        CLUSTER-IP        EXTERNAL-IP   PORT(S)   AGE
kubernetes             ClusterIP   192.168.0.1       <none>        443/TCP   16d
servicegrid-demo-svc   ClusterIP   192.168.6.139     <none>        80/TCP    10m
# step4: access servicegrid-demo-svc(service topology and closed-looped)
# execute on node0
$ curl 192.168.6.139|grep "node name"
        node name:      node0
# execute on node1 and node2
$ curl 192.168.6.139|grep "node name"
        node name:      node2
$ curl 192.168.6.139|grep "node name"
        node name:      node1

Depois que o ServiceGrid CR é criado, o ServiceGrid Controller é responsável por gerar o serviço correspondente (incluindo as anotações topologyKeys compostas de serviceGrid.Spec.GridUniqKey) de acordo com o ServiceGrid; o aplicativo-grid-wrapper percebe a consciência da topologia de acordo com o serviço, e a seguinte análise está em ordem.

Análise do controlador ServiceGrid

A lógica do ServiceGrid Controller é a mesma do DeploymentGrid Controller como um todo, da seguinte maneira:

  • 1. Crie e mantenha uma série de CRDs (incluindo: ServiceGrid) exigidos pelo grupo de serviço
  • 2. Monitore o evento ServiceGrid e preencha o ServiceGrid na fila de trabalho; retire ciclicamente o ServiceGrid da fila para análise, crie e mantenha o serviço correspondente
  • 3. Monitore o evento de serviço e coloque o ServiceGrid relacionado na fila de trabalho para o processamento acima para auxiliar a lógica acima para alcançar a lógica de reconciliação geral

Observe que isso é diferente do DeploymentGrid Controller:

  • Um objeto ServiceGrid gera apenas um serviço
  • Apenas precisa monitorar o evento de serviço adicionalmente, não há necessidade de monitorar o evento do nó. Porque o CRUD do nó não tem nada a ver com ServiceGrid
  • ServiceGrid corresponde ao serviço gerado, denominado como:{ServiceGrid}-svc
func (sgc *ServiceGridController) syncServiceGrid(key string) error {
    startTime := time.Now()
    klog.V(4).Infof("Started syncing service grid %q (%v)", key, startTime)
    defer func() {
        klog.V(4).Infof("Finished syncing service grid %q (%v)", key, time.Since(startTime))
    }()
    namespace, name, err := cache.SplitMetaNamespaceKey(key)
    if err != nil {
        return err
    }
    sg, err := sgc.svcGridLister.ServiceGrids(namespace).Get(name)
    if errors.IsNotFound(err) {
        klog.V(2).Infof("service grid %v has been deleted", key)
        return nil
    }
    if err != nil {
        return err
    }
    if sg.Spec.GridUniqKey == "" {
        sgc.eventRecorder.Eventf(sg, corev1.EventTypeWarning, "Empty", "This service grid has an empty grid key")
        return nil
    }
    // get service workload list of this grid
    svcList, err := sgc.getServiceForGrid(sg)
    if err != nil {
        return err
    }
    if sg.DeletionTimestamp != nil {
        return nil
    }
    // sync service grid relevant services workload
    return sgc.reconcile(sg, svcList)
}
func (sgc *ServiceGridController) getServiceForGrid(sg *crdv1.ServiceGrid) ([]*corev1.Service, error) {
    svcList, err := sgc.svcLister.Services(sg.Namespace).List(labels.Everything())
    if err != nil {
        return nil, err
    }
    labelSelector, err := common.GetDefaultSelector(sg.Name)
    if err != nil {
        return nil, err
    }
    canAdoptFunc := controller.RecheckDeletionTimestamp(func() (metav1.Object, error) {
        fresh, err := sgc.crdClient.SuperedgeV1().ServiceGrids(sg.Namespace).Get(context.TODO(), sg.Name, metav1.GetOptions{})
        if err != nil {
            return nil, err
        }
        if fresh.UID != sg.UID {
            return nil, fmt.Errorf("orignal service grid %v/%v is gone: got uid %v, wanted %v", sg.Namespace,
                sg.Name, fresh.UID, sg.UID)
        }
        return fresh, nil
    })
    cm := controller.NewServiceControllerRefManager(sgc.svcClient, sg, labelSelector, util.ControllerKind, canAdoptFunc)
    return cm.ClaimService(svcList)
}
func (sgc *ServiceGridController) reconcile(g *crdv1.ServiceGrid, svcList []*corev1.Service) error {
    var (
        adds    []*corev1.Service
        updates []*corev1.Service
        deletes []*corev1.Service
    )
    sgTargetSvcName := util.GetServiceName(g)
    isExistingSvc := false
    for _, svc := range svcList {
        if svc.Name == sgTargetSvcName {
            isExistingSvc = true
            template := util.KeepConsistence(g, svc)
            if !apiequality.Semantic.DeepEqual(template, svc) {
                updates = append(updates, template)
            }
        } else {
            deletes = append(deletes, svc)
        }
    }
    if !isExistingSvc {
        adds = append(adds, util.CreateService(g))
    }
    return sgc.syncService(adds, updates, deletes)
}
func CreateService(sg *crdv1.ServiceGrid) *corev1.Service {
    svc := &corev1.Service{
        ObjectMeta: metav1.ObjectMeta{
            Name:      GetServiceName(sg),
            Namespace: sg.Namespace,
            // Append existed ServiceGrid labels to service to be created
            Labels: func() map[string]string {
                if sg.Labels != nil {
                    newLabels := sg.Labels
                    newLabels[common.GridSelectorName] = sg.Name
                    newLabels[common.GridSelectorUniqKeyName] = sg.Spec.GridUniqKey
                    return newLabels
                } else {
                    return map[string]string{
                        common.GridSelectorName:        sg.Name,
                        common.GridSelectorUniqKeyName: sg.Spec.GridUniqKey,
                    }
                }
            }(),
            Annotations: make(map[string]string),
        },
        Spec: sg.Spec.Template,
    }
    keys := make([]string, 1)
    keys[0] = sg.Spec.GridUniqKey
    keyData, _ := json.Marshal(keys)
    svc.Annotations[common.TopologyAnnotationsKey] = string(keyData)
    return svc
}

Uma vez que a lógica é semelhante ao DeploymentGrid, os detalhes não serão expandidos aqui e focaremos na parte do wrapper da grade do aplicativo

application-grid-wrapper 分析

Depois que o ServiceGrid Controller cria o serviço, a função de application-grid-wrapper é iniciada:

apiVersion: v1
kind: Service
metadata:
  annotations:
    topologyKeys: '["zone1"]'
  creationTimestamp: "2021-03-03T07:33:30Z"
  labels:
    superedge.io/grid-selector: servicegrid-demo
  name: servicegrid-demo-svc
  namespace: default
  ownerReferences:
  - apiVersion: superedge.io/v1
    blockOwnerDeletion: true
    controller: true
    kind: ServiceGrid
    name: servicegrid-demo
    uid: 78c74d3c-72ac-4e68-8c79-f1396af5a581
  resourceVersion: "127987090"
  selfLink: /api/v1/namespaces/default/services/servicegrid-demo-svc
  uid: 8130ba7b-c27e-4c3a-8ceb-4f6dd0178dfc
spec:
  clusterIP: 192.168.161.1
  ports:
  - port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    appGrid: echo
  sessionAffinity: None
  type: ClusterIP
status:
  loadBalancer: {}

Para atingir a intrusão zero do Kubernetes, é necessário adicionar uma camada de invólucro entre a comunicação entre o kube-proxy e o apiserver. A arquitetura é a seguinte:

img

O link de chamada é o seguinte:

kube-proxy -> application-grid-wrapper -> lite-apiserver -> kube-apiserver

Portanto, application-grid-wrapper atenderá e aceitará solicitações de kube-proxy, da seguinte maneira:

func (s *interceptorServer) Run(debug bool, bindAddress string, insecure bool, caFile, certFile, keyFile string) error {
    ...
    klog.Infof("Start to run interceptor server")
    /* filter
     */
    server := &http.Server{Addr: bindAddress, Handler: s.buildFilterChains(debug)}
    if insecure {
        return server.ListenAndServe()
    }
    ...
    server.TLSConfig = tlsConfig
    return server.ListenAndServeTLS("", "")
}
func (s *interceptorServer) buildFilterChains(debug bool) http.Handler {
    handler := http.Handler(http.NewServeMux())
    handler = s.interceptEndpointsRequest(handler)
    handler = s.interceptServiceRequest(handler)
    handler = s.interceptEventRequest(handler)
    handler = s.interceptNodeRequest(handler)
    handler = s.logger(handler)
    if debug {
        handler = s.debugger(handler)
    }
    return handler
}

Aqui, o interceptorServer será criado primeiro e, em seguida, as funções de processamento serão registradas, da seguinte forma de fora para dentro:

  • debug: aceita o pedido de debug e retorna as informações de execução do wrapper pprof

  • logger: imprimir registro de solicitação

  • nó: aceitar a solicitação GET (/ api / v1 / nodes / {node}) do nó kube-proxy e retornar as informações do nó

  • evento: aceita a solicitação POST (/ events) de eventos do kube-proxy e encaminha a solicitação para lite-apiserver

    func (s *interceptorServer) interceptEventRequest(handler http.Handler) http.Handler {
      return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
          if r.Method != http.MethodPost || !strings.HasSuffix(r.URL.Path, "/events") {
              handler.ServeHTTP(w, r)
              return
          }
          targetURL, _ := url.Parse(s.restConfig.Host)
          reverseProxy := httputil.NewSingleHostReverseProxy(targetURL)
          reverseProxy.Transport, _ = rest.TransportFor(s.restConfig)
          reverseProxy.ServeHTTP(w, r)
      })
    }
    
  • serviço: aceite a solicitação de lista e observação do serviço kube-proxy (/ api / v1 / services) e retorne de acordo com o conteúdo do storageCache (GetServices)

  • endpoint: aceite a solicitação Kube-proxy endpoint List & Watch (/ api / v1 / endpoints) e retorne de acordo com o conteúdo do storageCache (GetEndpoints)

Vamos nos concentrar na análise da lógica da parte do cache e, em seguida, voltar para analisar a lógica de processamento List & Watch do manipulador http específico

Para realizar o reconhecimento da topologia, o wrapper mantém um cache, incluindo nó, serviço e terminal. Você pode ver que as funções de processamento para esses três tipos de recursos estão registradas em setupInformers:

type storageCache struct {
    // hostName is the nodeName of node which application-grid-wrapper deploys on
    hostName         string
    wrapperInCluster bool
    // mu lock protect the following map structure
    mu           sync.RWMutex
    servicesMap  map[types.NamespacedName]*serviceContainer
    endpointsMap map[types.NamespacedName]*endpointsContainer
    nodesMap     map[types.NamespacedName]*nodeContainer
    // service watch channel
    serviceChan chan<- watch.Event
    // endpoints watch channel
    endpointsChan chan<- watch.Event
}
...
func NewStorageCache(hostName string, wrapperInCluster bool, serviceNotifier, endpointsNotifier chan watch.Event) *storageCache {
    msc := &storageCache{
        hostName:         hostName,
        wrapperInCluster: wrapperInCluster,
        servicesMap:      make(map[types.NamespacedName]*serviceContainer),
        endpointsMap:     make(map[types.NamespacedName]*endpointsContainer),
        nodesMap:         make(map[types.NamespacedName]*nodeContainer),
        serviceChan:      serviceNotifier,
        endpointsChan:    endpointsNotifier,
    }
    return msc
}
...
func (s *interceptorServer) Run(debug bool, bindAddress string, insecure bool, caFile, certFile, keyFile string) error {
    ...
    if err := s.setupInformers(ctx.Done()); err != nil {
        return err
    }
    klog.Infof("Start to run interceptor server")
    /* filter
     */
    server := &http.Server{Addr: bindAddress, Handler: s.buildFilterChains(debug)}
    ...
    return server.ListenAndServeTLS("", "")
}
func (s *interceptorServer) setupInformers(stop <-chan struct{}) error {
    klog.Infof("Start to run service and endpoints informers")
    noProxyName, err := labels.NewRequirement(apis.LabelServiceProxyName, selection.DoesNotExist, nil)
    if err != nil {
        klog.Errorf("can't parse proxy label, %v", err)
        return err
    }
    noHeadlessEndpoints, err := labels.NewRequirement(v1.IsHeadlessService, selection.DoesNotExist, nil)
    if err != nil {
        klog.Errorf("can't parse headless label, %v", err)
        return err
    }
    labelSelector := labels.NewSelector()
    labelSelector = labelSelector.Add(*noProxyName, *noHeadlessEndpoints)
    resyncPeriod := time.Minute * 5
    client := kubernetes.NewForConfigOrDie(s.restConfig)
    nodeInformerFactory := informers.NewSharedInformerFactory(client, resyncPeriod)
    informerFactory := informers.NewSharedInformerFactoryWithOptions(client, resyncPeriod,
        informers.WithTweakListOptions(func(options *metav1.ListOptions) {
            options.LabelSelector = labelSelector.String()
        }))
    nodeInformer := nodeInformerFactory.Core().V1().Nodes().Informer()
    serviceInformer := informerFactory.Core().V1().Services().Informer()
    endpointsInformer := informerFactory.Core().V1().Endpoints().Informer()
    /*
     */
    nodeInformer.AddEventHandlerWithResyncPeriod(s.cache.NodeEventHandler(), resyncPeriod)
    serviceInformer.AddEventHandlerWithResyncPeriod(s.cache.ServiceEventHandler(), resyncPeriod)
    endpointsInformer.AddEventHandlerWithResyncPeriod(s.cache.EndpointsEventHandler(), resyncPeriod)
    go nodeInformer.Run(stop)
    go serviceInformer.Run(stop)
    go endpointsInformer.Run(stop)
    if !cache.WaitForNamedCacheSync("node", stop,
        nodeInformer.HasSynced,
        serviceInformer.HasSynced,
        endpointsInformer.HasSynced) {
        return fmt.Errorf("can't sync informers")
    }
    return nil
}
func (sc *storageCache) NodeEventHandler() cache.ResourceEventHandler {
    return &nodeHandler{cache: sc}
}
func (sc *storageCache) ServiceEventHandler() cache.ResourceEventHandler {
    return &serviceHandler{cache: sc}
}
func (sc *storageCache) EndpointsEventHandler() cache.ResourceEventHandler {
    return &endpointsHandler{cache: sc}
}

Aqui, analisamos NodeEventHandler, ServiceEventHandler e EndpointsEventHandler, por sua vez, da seguinte maneira:

1 、 NodeEventHandler

NodeEventHandler é responsável por monitorar eventos relacionados a recursos do nó e adicionar rótulos de nó e nó a storageCache.nodesMap (a chave é nodeName, o valor é nó e rótulos de nó)

func (nh *nodeHandler) add(node *v1.Node) {
    sc := nh.cache
    sc.mu.Lock()
    nodeKey := types.NamespacedName{Namespace: node.Namespace, Name: node.Name}
    klog.Infof("Adding node %v", nodeKey)
    sc.nodesMap[nodeKey] = &nodeContainer{
        node:   node,
        labels: node.Labels,
    }
    // update endpoints
    changedEps := sc.rebuildEndpointsMap()
    sc.mu.Unlock()
    for _, eps := range changedEps {
        sc.endpointsChan <- eps
    }
}
func (nh *nodeHandler) update(node *v1.Node) {
    sc := nh.cache
    sc.mu.Lock()
    nodeKey := types.NamespacedName{Namespace: node.Namespace, Name: node.Name}
    klog.Infof("Updating node %v", nodeKey)
    nodeContainer, found := sc.nodesMap[nodeKey]
    if !found {
        sc.mu.Unlock()
        klog.Errorf("Updating non-existed node %v", nodeKey)
        return
    }
    nodeContainer.node = node
    // return directly when labels of node stay unchanged
    if reflect.DeepEqual(node.Labels, nodeContainer.labels) {
        sc.mu.Unlock()
        return
    }
    nodeContainer.labels = node.Labels
    // update endpoints
    changedEps := sc.rebuildEndpointsMap()
    sc.mu.Unlock()
    for _, eps := range changedEps {
        sc.endpointsChan <- eps
    }
}
...

Ao mesmo tempo, como a mudança de nó afetará o endpoint, rebuildEndpointsMap será chamado para atualizar storageCache.endpointsMap

// rebuildEndpointsMap updates all endpoints stored in storageCache.endpointsMap dynamically and constructs relevant modified events
func (sc *storageCache) rebuildEndpointsMap() []watch.Event {
    evts := make([]watch.Event, 0)
    for name, endpointsContainer := range sc.endpointsMap {
        newEps := pruneEndpoints(sc.hostName, sc.nodesMap, sc.servicesMap, endpointsContainer.endpoints, sc.wrapperInCluster)
        if apiequality.Semantic.DeepEqual(newEps, endpointsContainer.modified) {
            continue
        }
        sc.endpointsMap[name].modified = newEps
        evts = append(evts, watch.Event{
            Type:   watch.Modified,
            Object: newEps,
        })
    }
    return evts
}

rebuildEndpointsMap é a função central do cache e também é a realização do algoritmo com reconhecimento de topologia:

// pruneEndpoints filters endpoints using serviceTopology rules combined by services topologyKeys and node labels
func pruneEndpoints(hostName string,
    nodes map[types.NamespacedName]*nodeContainer,
    services map[types.NamespacedName]*serviceContainer,
    eps *v1.Endpoints, wrapperInCluster bool) *v1.Endpoints {
    epsKey := types.NamespacedName{Namespace: eps.Namespace, Name: eps.Name}
    if wrapperInCluster {
        eps = genLocalEndpoints(eps)
    }
    // dangling endpoints
    svc, ok := services[epsKey]
    if !ok {
        klog.V(4).Infof("Dangling endpoints %s, %+#v", eps.Name, eps.Subsets)
        return eps
    }
    // normal service
    if len(svc.keys) == 0 {
        klog.V(4).Infof("Normal endpoints %s, %+#v", eps.Name, eps.Subsets)
        return eps
    }
    // topology endpoints
    newEps := eps.DeepCopy()
    for si := range newEps.Subsets {
        subnet := &newEps.Subsets[si]
        subnet.Addresses = filterConcernedAddresses(svc.keys, hostName, nodes, subnet.Addresses)
        subnet.NotReadyAddresses = filterConcernedAddresses(svc.keys, hostName, nodes, subnet.NotReadyAddresses)
    }
    klog.V(4).Infof("Topology endpoints %s: subnets from %+#v to %+#v", eps.Name, eps.Subsets, newEps.Subsets)
    return newEps
}
// filterConcernedAddresses aims to filter out endpoints addresses within the same node unit
func filterConcernedAddresses(topologyKeys []string, hostName string, nodes map[types.NamespacedName]*nodeContainer,
    addresses []v1.EndpointAddress) []v1.EndpointAddress {
    hostNode, found := nodes[types.NamespacedName{Name: hostName}]
    if !found {
        return nil
    }
    filteredEndpointAddresses := make([]v1.EndpointAddress, 0)
    for i := range addresses {
        addr := addresses[i]
        if nodeName := addr.NodeName; nodeName != nil {
            epsNode, found := nodes[types.NamespacedName{Name: *nodeName}]
            if !found {
                continue
            }
            if hasIntersectionLabel(topologyKeys, hostNode.labels, epsNode.labels) {
                filteredEndpointAddresses = append(filteredEndpointAddresses, addr)
            }
        }
    }
    return filteredEndpointAddresses
}
func hasIntersectionLabel(keys []string, n1, n2 map[string]string) bool {
    if n1 == nil || n2 == nil {
        return false
    }
    for _, key := range keys {
        val1, v1found := n1[key]
        val2, v2found := n2[key]
        if v1found && v2found && val1 == val2 {
            return true
        }
    }
    return false
}

A lógica do algoritmo é a seguinte:

  • Determine se o endpoint é o serviço Kubernetes padrão, em caso afirmativo, converta o endpoint para o endereço lite-apiserver (127.0.0.1) e a porta (51003) do nó de borda onde o wrapper está localizado
apiVersion: v1
kind: Endpoints
metadata:
  annotations:
    superedge.io/local-endpoint: 127.0.0.1
    superedge.io/local-port: "51003"
  name: kubernetes
  namespace: default
subsets:
- addresses:
  - ip: 172.31.0.60
  ports:
  - name: https
    port: xxx
    protocol: TCP
func genLocalEndpoints(eps *v1.Endpoints) *v1.Endpoints {
    if eps.Namespace != metav1.NamespaceDefault || eps.Name != MasterEndpointName {
        return eps
    }
    klog.V(4).Infof("begin to gen local ep %v", eps)
    ipAddress, e := eps.Annotations[EdgeLocalEndpoint]
    if !e {
        return eps
    }
    portStr, e := eps.Annotations[EdgeLocalPort]
    if !e {
        return eps
    }
    klog.V(4).Infof("get local endpoint %s:%s", ipAddress, portStr)
    port, err := strconv.ParseInt(portStr, 10, 32)
    if err != nil {
        klog.Errorf("parse int %s err %v", portStr, err)
        return eps
    }
    ip := net.ParseIP(ipAddress)
    if ip == nil {
        klog.Warningf("parse ip %s nil", ipAddress)
        return eps
    }
    nep := eps.DeepCopy()
    nep.Subsets = []v1.EndpointSubset{
        {
            Addresses: []v1.EndpointAddress{
                {
                    IP: ipAddress,
                },
            },
            Ports: []v1.EndpointPort{
                {
                    Protocol: v1.ProtocolTCP,
                    Port:     int32(port),
                    Name:     "https",
                },
            },
        },
    }
    klog.V(4).Infof("gen new endpoint complete %v", nep)
    return nep
}

O objetivo disso é tornar o apiserver acessado pelo serviço no nó de borda no modo de cluster (InCluster) como o apiserver local lite em vez do apiserver de nuvem

  • Recupere o serviço correspondente do cache storageCache.servicesMap de acordo com o nome do terminal (namespace / nome). Se o serviço não tiver topologyKeys, nenhuma conversão de topologia (grupo sem serviço) será necessária.
func getTopologyKeys(objectMeta *metav1.ObjectMeta) []string {
    if !hasTopologyKey(objectMeta) {
        return nil
    }
    var keys []string
    keyData := objectMeta.Annotations[TopologyAnnotationsKey]
    if err := json.Unmarshal([]byte(keyData), &keys); err != nil {
        klog.Errorf("can't parse topology keys %s, %v", keyData, err)
        return nil
    }
    return keys
}
  • Chame filterConcernedAddresses para filtrar endpoint.Subsets Addresses e NotReadyAddresses e apenas retenha os endpoints no mesmo serviço topologyKeys
// filterConcernedAddresses aims to filter out endpoints addresses within the same node unit
func filterConcernedAddresses(topologyKeys []string, hostName string, nodes map[types.NamespacedName]*nodeContainer,
    addresses []v1.EndpointAddress) []v1.EndpointAddress {
    hostNode, found := nodes[types.NamespacedName{Name: hostName}]
    if !found {
        return nil
    }
    filteredEndpointAddresses := make([]v1.EndpointAddress, 0)
    for i := range addresses {
        addr := addresses[i]
        if nodeName := addr.NodeName; nodeName != nil {
            epsNode, found := nodes[types.NamespacedName{Name: *nodeName}]
            if !found {
                continue
            }
            if hasIntersectionLabel(topologyKeys, hostNode.labels, epsNode.labels) {
                filteredEndpointAddresses = append(filteredEndpointAddresses, addr)
            }
        }
    }
    return filteredEndpointAddresses
}
func hasIntersectionLabel(keys []string, n1, n2 map[string]string) bool {
    if n1 == nil || n2 == nil {
        return false
    }
    for _, key := range keys {
        val1, v1found := n1[key]
        val2, v2found := n2[key]
        if v1found && v2found && val1 == val2 {
            return true
        }
    }
    return false
}

Nota: Se o nó de extremidade onde o wrapper está localizado não tiver o rótulo de topologyKeys do serviço, o serviço também não poderá ser acessado

De volta ao rebuildEndpointsMap, depois de chamar pruneEndpoints para atualizar os terminais no mesmo domínio de topologia, os terminais modificados serão atribuídos a storageCache.endpointsMap [endpoint] .modified (este campo registra os terminais modificados após o reconhecimento da topologia).

func (nh *nodeHandler) add(node *v1.Node) {
    sc := nh.cache
    sc.mu.Lock()
    nodeKey := types.NamespacedName{Namespace: node.Namespace, Name: node.Name}
    klog.Infof("Adding node %v", nodeKey)
    sc.nodesMap[nodeKey] = &nodeContainer{
        node:   node,
        labels: node.Labels,
    }
    // update endpoints
    changedEps := sc.rebuildEndpointsMap()
    sc.mu.Unlock()
    for _, eps := range changedEps {
        sc.endpointsChan <- eps
    }
}
// rebuildEndpointsMap updates all endpoints stored in storageCache.endpointsMap dynamically and constructs relevant modified events
func (sc *storageCache) rebuildEndpointsMap() []watch.Event {
    evts := make([]watch.Event, 0)
    for name, endpointsContainer := range sc.endpointsMap {
        newEps := pruneEndpoints(sc.hostName, sc.nodesMap, sc.servicesMap, endpointsContainer.endpoints, sc.wrapperInCluster)
        if apiequality.Semantic.DeepEqual(newEps, endpointsContainer.modified) {
            continue
        }
        sc.endpointsMap[name].modified = newEps
        evts = append(evts, watch.Event{
            Type:   watch.Modified,
            Object: newEps,
        })
    }
    return evts
}

Além disso, se os pontos de extremidade (pontos de extremidade modificados após o reconhecimento da topologia) mudarem, um evento de observação será construído e passado para o manipulador de pontos de extremidade (interceptEndpointsRequest) para processamento

2 、 ServiceEventHandler

A chave da estrutura storageCache.servicesMap é o nome do serviço (namespace / nome) e o valor é serviceContainer, que contém os seguintes dados:

  • svc: objeto de serviço
  • keys : TopologyKeys do serviço

Para alterações nos recursos de serviço, use o evento Update para ilustrar:

func (sh *serviceHandler) update(service *v1.Service) {
    sc := sh.cache
    sc.mu.Lock()
    serviceKey := types.NamespacedName{Namespace: service.Namespace, Name: service.Name}
    klog.Infof("Updating service %v", serviceKey)
    newTopologyKeys := getTopologyKeys(&service.ObjectMeta)
    serviceContainer, found := sc.servicesMap[serviceKey]
    if !found {
        sc.mu.Unlock()
        klog.Errorf("update non-existed service, %v", serviceKey)
        return
    }
    sc.serviceChan <- watch.Event{
        Type:   watch.Modified,
        Object: service,
    }
    serviceContainer.svc = service
    // return directly when topologyKeys of service stay unchanged
    if reflect.DeepEqual(serviceContainer.keys, newTopologyKeys) {
        sc.mu.Unlock()
        return
    }
    serviceContainer.keys = newTopologyKeys
    // update endpoints
    changedEps := sc.rebuildEndpointsMap()
    sc.mu.Unlock()
    for _, eps := range changedEps {
        sc.endpointsChan <- eps
    }
}

A lógica é a seguinte:

  • Obtenha chaves de topologia de serviço
  • Evento de serviço de construção. Evento modificado
  • Compare as teclas de topologia de serviço com as existentes para qualquer diferença
  • Se houver uma diferença, atualize as topologyKeys e chame rebuildEndpointsMap para atualizar os pontos de extremidade correspondentes ao serviço. Se os pontos de extremidade mudarem, construa o evento de observação de pontos de extremidade e passe-o para o manipulador de pontos de extremidade (interceptEndpointsRequest) para processamento

3 、 EndpointsEventHandler

A chave da estrutura storageCache.endpointsMap é o nome dos endpoints (namespace / nome) e o valor é endpointsContainer, que contém os seguintes dados:

  • endpoints: endpoints antes da modificação da topologia
  • modificado: pontos de extremidade após modificação da topologia

Com relação às mudanças nos recursos de endpoints, use o evento Update para ilustrar:

func (eh *endpointsHandler) update(endpoints *v1.Endpoints) {
    sc := eh.cache
    sc.mu.Lock()
    endpointsKey := types.NamespacedName{Namespace: endpoints.Namespace, Name: endpoints.Name}
    klog.Infof("Updating endpoints %v", endpointsKey)
    endpointsContainer, found := sc.endpointsMap[endpointsKey]
    if !found {
        sc.mu.Unlock()
        klog.Errorf("Updating non-existed endpoints %v", endpointsKey)
        return
    }
    endpointsContainer.endpoints = endpoints
    newEps := pruneEndpoints(sc.hostName, sc.nodesMap, sc.servicesMap, endpoints, sc.wrapperInCluster)
    changed := !apiequality.Semantic.DeepEqual(endpointsContainer.modified, newEps)
    if changed {
        endpointsContainer.modified = newEps
    }
    sc.mu.Unlock()
    if changed {
        sc.endpointsChan <- watch.Event{
            Type:   watch.Modified,
            Object: newEps,
        }
    }
}

A lógica é a seguinte:

  • Atualize endpointsContainer.endpoint para o novo objeto endpoints
  • Chame pruneEndpoints para obter os endpoints após a atualização da topologia
  • Compare endpointsContainer.modified com os endpoints recém-atualizados
  • Se houver diferenças, atualize endpointsContainer.modified, construa o evento de observação de endpoints e transmita-o ao manipulador de endpoints (interceptEndpointsRequest) para processamento

Depois de analisar NodeEventHandler, ServiceEventHandler e EndpointsEventHandler, retornamos à lógica de processamento List & Watch do manipulador http específico, aqui está um exemplo de endpoints:

func (s *interceptorServer) interceptEndpointsRequest(handler http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodGet || !strings.HasPrefix(r.URL.Path, "/api/v1/endpoints") {
            handler.ServeHTTP(w, r)
            return
        }
        queries := r.URL.Query()
        acceptType := r.Header.Get("Accept")
        info, found := s.parseAccept(acceptType, s.mediaSerializer)
        if !found {
            klog.Errorf("can't find %s serializer", acceptType)
            w.WriteHeader(http.StatusBadRequest)
            return
        }
        encoder := scheme.Codecs.EncoderForVersion(info.Serializer, v1.SchemeGroupVersion)
        // list request
        if queries.Get("watch") == "" {
            w.Header().Set("Content-Type", info.MediaType)
            allEndpoints := s.cache.GetEndpoints()
            epsItems := make([]v1.Endpoints, 0, len(allEndpoints))
            for _, eps := range allEndpoints {
                epsItems = append(epsItems, *eps)
            }
            epsList := &v1.EndpointsList{
                Items: epsItems,
            }
            err := encoder.Encode(epsList, w)
            if err != nil {
                klog.Errorf("can't marshal endpoints list, %v", err)
                w.WriteHeader(http.StatusInternalServerError)
                return
            }
            return
        }
        // watch request
        timeoutSecondsStr := r.URL.Query().Get("timeoutSeconds")
        timeout := time.Minute
        if timeoutSecondsStr != "" {
            timeout, _ = time.ParseDuration(fmt.Sprintf("%ss", timeoutSecondsStr))
        }
        timer := time.NewTimer(timeout)
        defer timer.Stop()
        flusher, ok := w.(http.Flusher)
        if !ok {
            klog.Errorf("unable to start watch - can't get http.Flusher: %#v", w)
            w.WriteHeader(http.StatusMethodNotAllowed)
            return
        }
        e := restclientwatch.NewEncoder(
            streaming.NewEncoder(info.StreamSerializer.Framer.NewFrameWriter(w),
                scheme.Codecs.EncoderForVersion(info.StreamSerializer, v1.SchemeGroupVersion)),
            encoder)
        if info.MediaType == runtime.ContentTypeProtobuf {
            w.Header().Set("Content-Type", runtime.ContentTypeProtobuf+";stream=watch")
        } else {
            w.Header().Set("Content-Type", runtime.ContentTypeJSON)
        }
        w.Header().Set("Transfer-Encoding", "chunked")
        w.WriteHeader(http.StatusOK)
        flusher.Flush()
        for {
            select {
            case <-r.Context().Done():
                return
            case <-timer.C:
                return
            case evt := <-s.endpointsWatchCh:
                klog.V(4).Infof("Send endpoint watch event: %+#v", evt)
                err := e.Encode(&evt)
                if err != nil {
                    klog.Errorf("can't encode watch event, %v", err)
                    return
                }
                if len(s.endpointsWatchCh) == 0 {
                    flusher.Flush()
                }
            }
        }
    })
}

A lógica é a seguinte:

  • Se for uma solicitação List, chame GetEndpoints para obter uma lista de endpoints após a modificação da topologia e retorne
func (sc *storageCache) GetEndpoints() []*v1.Endpoints {
    sc.mu.RLock()
    defer sc.mu.RUnlock()
    epList := make([]*v1.Endpoints, 0, len(sc.endpointsMap))
    for _, v := range sc.endpointsMap {
        epList = append(epList, v.modified)
    }
    return epList
}
  • Se for uma solicitação Watch, ele continuará a receber eventos watch do pipeline storageCache.endpointsWatchCh e retornará

A lógica interceptServiceRequest é consistente com interceptEndpointsRequest, então não vou repeti-la aqui.

Resumindo

  • O grupo de serviço SuperEdge usa application-grid-wrapper para perceber a percepção da topologia e concluir o acesso de loop fechado aos serviços na mesma unidade de nó
  • Comparando o reconhecimento da topologia implementado pelo grupo de serviço e a implementação nativa na comunidade Kubernetes, existem as seguintes diferenças:
    • A chave de topologia do grupo de serviço pode ser personalizada, ou seja, gridUniqKey, que é mais flexível de usar. Atualmente, existem apenas três opções para implementação da comunidade: "kubernetes.io/hostname", "topology.kubernetes.io/zone" e " topology.kubernetes. io / region "
    • O grupo de serviço pode preencher apenas uma chave de topologia, ou seja, pode acessar apenas pontos de extremidade válidos neste domínio de topologia e não pode acessar pontos de extremidade em outros domínios de topologia; a comunidade pode acessar outros pontos de extremidade de domínio de topologia alternativa por meio da lista de chaves de topologia e "* "
  • O ServiceGrid Controller é responsável por gerar o serviço correspondente (incluindo as anotações topologyKeys compostas por serviceGrid.Spec.GridUniqKey) de acordo com o ServiceGrid, e a lógica é a mesma do DeploymentGrid Controller como um todo, conforme segue:
    • Crie e mantenha vários CRDs exigidos pelo grupo de serviço (incluindo: ServiceGrid)
    • Monitore o evento ServiceGrid e preencha o ServiceGrid na fila de trabalho; remova ciclicamente o ServiceGrid da fila para análise, crie e mantenha o serviço correspondente
    • Monitore o evento de serviço e coloque o ServiceGrid relacionado na fila de trabalho para o processamento acima para ajudar a lógica acima a atingir a lógica de reconciliação geral
  • Para atingir a intrusão zero do Kubernetes, é necessário adicionar uma camada de invólucro entre a comunicação entre o kube-proxy e o apiserver, e o link de chamada é o seguinte:kube-proxy -> application-grid-wrapper -> lite-apiserver -> kube-apiserver
  • application-grid-wrapper é um servidor http que aceita solicitações de kube-proxy e mantém um cache de recursos ao mesmo tempo. As funções de processamento são as seguintes de fora para dentro:
    • debug: aceita o pedido de debug e retorna as informações de execução do wrapper pprof
    • logger: imprimir registro de solicitação
    • nó: aceitar a solicitação GET (/ api / v1 / nodes / {node}) do nó kube-proxy e retornar as informações do nó
    • evento: aceita a solicitação POST (/ events) de eventos do kube-proxy e encaminha a solicitação para lite-apiserver
    • serviço: aceita a solicitação e retorno (GetServices) do serviço kube-proxy List & Watch (/ api / v1 / services) de acordo com o conteúdo do storageCache.
    • endpoint: aceite a solicitação de List & Watch (/ api / v1 / endpoints) do endpoint do kube-proxy e retorne (GetEndpoints) de acordo com o conteúdo do storageCache.
  • Para realizar o reconhecimento da topologia, o wrapper mantém um cache de recursos, incluindo nó, serviço e terminal, e registra funções de processamento de eventos relacionados. A lógica do algoritmo de topologia principal é: chame filterConcernedAddresses para filtrar endpoint.Subsets Addresses e NotReadyAddresses e apenas retenha os endpoints na mesma topologyKeys de serviço. Além disso, se o nó de borda onde o wrapper está localizado não tiver um rótulo de topologyKeys de serviço, o serviço também não poderá ser acessado.
  • O wrapper aceita solicitações List & Watch do kube-proxy para endpoints e serviços. Considere os endpoints como exemplo: se for uma solicitação List, chame GetEndpoints para obter uma lista de endpoints após a modificação da topologia e retorne-a; se for uma solicitação Watch, continue a canalizar de storageCache. Aceite o evento de observação e retorne. A lógica do serviço é consistente com os terminais.

Panorama

No momento, as funções do algoritmo de topologia implementadas pelo grupo de serviço SuperEdge são mais flexíveis e convenientes. Vale a pena explorar como lidar com o relacionamento com o conhecimento da topologia de serviço da comunidade Kubernetes. Recomenda-se enviar o algoritmo de topologia SuperEdge para a comunidade

Refs

  • duyanghao kubernetes-reading-notes

Acho que você gosta

Origin blog.csdn.net/nidongla/article/details/115179430
Recomendado
Clasificación