Análise da estrutura de filtragem de dados Yurthub do OpenYurt

Autor: Ying Jianjian, Xinhua Zhi Cloud Computing Center

O OpenYurt é o primeiro projeto de código aberto nativo da nuvem não intrusivo de computação de borda do setor. Ele oferece aos usuários uma experiência integrada de borda de nuvem por meio de autonomia de borda, colaboração de borda de nuvem, unitização de borda e recursos de circuito fechado de tráfego de borda. No Openyurt, a rede de borda pode usar a estrutura de filtragem de dados para obter capacidade de loop fechado de tráfego de borda em diferentes pools de nós.

Análise da estrutura de filtragem de dados do Yurthub

O Yurthub é essencialmente uma camada de proxy kube-apiserver, e uma camada de cache é adicionada com base no proxy para garantir que o cache local possa ser usado para garantir a estabilidade dos negócios quando o nó de borda estiver offline, o que efetivamente resolve o problema de autonomia de ponta. Em segundo lugar, pode reduzir a carga na API na nuvem causada por um grande número de operações de lista e observação.

A filtragem de dados do Yurthub é enviada para o kube-apiserver por meio do pod no nó e a solicitação do kubelet por meio do Load Balancer. O proxy recebe a mensagem de resposta e executa o processamento de filtragem de dados e, em seguida, retorna os dados filtrados ao solicitante. Se o nó for um nó de borda, ele armazenará os recursos no corpo da solicitação de resposta localmente de acordo com o tipo de solicitação. Se for um nó de nuvem, não armazenará em cache localmente considerando que o status da rede é bom.

Diagrama esquemático da implementação da estrutura de filtragem do Yurthub:

Atualmente, o Yurthub contém quatro regras de filtragem: o agente do usuário, o recurso e o verbo solicitados pelos complementos são usados ​​para determinar qual filtro filtrar os dados correspondentes.

Quatro funções e implementação de regras de filtragem

ServiceTopologyFilter 

A filtragem de dados é principalmente para recursos EndpointSlice, mas o recurso Endpoint Slice precisa ter suporte no Kubernetes v1.18 ou superior. Se estiver abaixo da versão 1.18, é recomendável usar o filtro endpointsFilter. Ao passar por esse filtro, primeiro encontre o recurso de serviços correspondente ao recurso endpointSlice por meio de kubernetes.io/service-name e, em seguida, julgue se o recurso de serviço tem uma anotação de openyurt.io/topologyKeys. Se existir, julgue os dados regra de filtragem pelo valor desta anotação. Por fim, atualize os dados da resposta e devolva-os aos addons.

Os valores das anotações se dividem em duas grandes categorias:

1. kubernetes.io/hostname: filtre apenas o ip do endpoint do mesmo nó

2. openyurt.io/nodepool ou kubernetes.io/zone: Obtenha o pool de nós correspondente por meio deste Annotations e, finalmente, percorra os recursos endpointSlice e encontre os Endpoints correspondentes no objeto endpointSlice por meio do campo kubernetes.io/hostname na topologia campo no endpointSlice. Em seguida, reorganize os Endpoints no endpointSlice e devolva-o aos addons. 

Código:

func (fh *serviceTopologyFilterHandler) reassembleEndpointSlice(endpointSlice *discovery.EndpointSlice) *discovery.EndpointSlice {
   var serviceTopologyType string
   // get the service Topology type
   if svcName, ok := endpointSlice.Labels[discovery.LabelServiceName]; ok {
      svc, err := fh.serviceLister.Services(endpointSlice.Namespace).Get(svcName)
      if err != nil {
         klog.Infof("skip reassemble endpointSlice, failed to get service %s/%s, err: %v", endpointSlice.Namespace, svcName, err)
         return endpointSlice
      }
 
      if serviceTopologyType, ok = svc.Annotations[AnnotationServiceTopologyKey]; !ok {
         klog.Infof("skip reassemble endpointSlice, service %s/%s has no annotation %s", endpointSlice.Namespace, svcName, AnnotationServiceTopologyKey)
         return endpointSlice
      }
   }
 
   var newEps []discovery.Endpoint
   // if type of service Topology is 'kubernetes.io/hostname'
   // filter the endpoint just on the local host
   if serviceTopologyType == AnnotationServiceTopologyValueNode {
      for i := range endpointSlice.Endpoints {
         if endpointSlice.Endpoints[i].Topology[v1.LabelHostname] == fh.nodeName {
            newEps = append(newEps, endpointSlice.Endpoints[i])
         }
      }
      endpointSlice.Endpoints = newEps
   } else if serviceTopologyType == AnnotationServiceTopologyValueNodePool || serviceTopologyType == AnnotationServiceTopologyValueZone {
      // if type of service Topology is openyurt.io/nodepool
      // filter the endpoint just on the node which is in the same nodepool with current node
      currentNode, err := fh.nodeGetter(fh.nodeName)
      if err != nil {
         klog.Infof("skip reassemble endpointSlice, failed to get current node %s, err: %v", fh.nodeName, err)
         return endpointSlice
      }
      if nodePoolName, ok := currentNode.Labels[nodepoolv1alpha1.LabelCurrentNodePool]; ok {
         nodePool, err := fh.nodePoolLister.Get(nodePoolName)
         if err != nil {
            klog.Infof("skip reassemble endpointSlice, failed to get nodepool %s, err: %v", nodePoolName, err)
            return endpointSlice
         }
         for i := range endpointSlice.Endpoints {
            if inSameNodePool(endpointSlice.Endpoints[i].Topology[v1.LabelHostname], nodePool.Status.Nodes) {
               newEps = append(newEps, endpointSlice.Endpoints[i])
            }
         }
         endpointSlice.Endpoints = newEps
      }
   }
   return endpointSlice
}
复制代码

Filtro de pontos de extremidade

Execute a filtragem de dados correspondente para recursos de endpoints. Primeiro, determine se o endpoint tem um serviço correspondente, obtenha o pool de nós por meio do rótulo do nó: apps.openyurt.io/nodepool, obtenha todos os nós no pool de nós e percorra os recursos em endpoints.Subsets to find O endereço do pod Ready e o endereço do pod NotReady do mesmo pool de nós são reorganizados em novos endpoints e retornados aos addons.

func (fh *endpointsFilterHandler) reassembleEndpoint(endpoints *v1.Endpoints) *v1.Endpoints {
   svcName := endpoints.Name
   _, err := fh.serviceLister.Services(endpoints.Namespace).Get(svcName)
   if err != nil {
      klog.Infof("skip reassemble endpoints, failed to get service %s/%s, err: %v", endpoints.Namespace, svcName, err)
      return endpoints
   }
   // filter the endpoints on the node which is in the same nodepool with current node
   currentNode, err := fh.nodeGetter(fh.nodeName)
   if err != nil {
      klog.Infof("skip reassemble endpoints, failed to get current node %s, err: %v", fh.nodeName, err)
      return endpoints
   }
   if nodePoolName, ok := currentNode.Labels[nodepoolv1alpha1.LabelCurrentNodePool]; ok {
      nodePool, err := fh.nodePoolLister.Get(nodePoolName)
      if err != nil {
         klog.Infof("skip reassemble endpoints, failed to get nodepool %s, err: %v", nodePoolName, err)
         return endpoints
      }
      var newEpSubsets []v1.EndpointSubset
      for i := range endpoints.Subsets {
         endpoints.Subsets[i].Addresses = filterValidEndpointsAddr(endpoints.Subsets[i].Addresses, nodePool)
         endpoints.Subsets[i].NotReadyAddresses = filterValidEndpointsAddr(endpoints.Subsets[i].NotReadyAddresses, nodePool)
         if endpoints.Subsets[i].Addresses != nil || endpoints.Subsets[i].NotReadyAddresses != nil {
            newEpSubsets = append(newEpSubsets, endpoints.Subsets[i])
         }
      }
      endpoints.Subsets = newEpSubsets
      if len(endpoints.Subsets) == 0 {
         // this endpoints has no nodepool valid addresses for ingress controller, return nil to ignore it
         return nil
      }
   }
   return endpoints
}
复制代码

MasterServiceFilter

针对 services 下的域名进行 ip 以及端口替换,这个过滤器的场景主要在于边缘端的 pod 无缝使用 InClusterConfig 访问集群资源。

func (fh *masterServiceFilterHandler) ObjectResponseFilter(b []byte) ([]byte, error) {
   list, err := fh.serializer.Decode(b)
   if err != nil || list == nil {
      klog.Errorf("skip filter, failed to decode response in ObjectResponseFilter of masterServiceFilterHandler, %v", err)
      return b, nil
   }
 
   // return data un-mutated if not ServiceList
   serviceList, ok := list.(*v1.ServiceList)
   if !ok {
      return b, nil
   }
 
   // mutate master service
   for i := range serviceList.Items {
      if serviceList.Items[i].Namespace == MasterServiceNamespace && serviceList.Items[i].Name == MasterServiceName {
         serviceList.Items[i].Spec.ClusterIP = fh.host
         for j := range serviceList.Items[i].Spec.Ports {
            if serviceList.Items[i].Spec.Ports[j].Name == MasterServicePortName {
               serviceList.Items[i].Spec.Ports[j].Port = fh.port
               break
            }
         }
         klog.V(2).Infof("mutate master service into ClusterIP:Port=%s:%d for request %s", fh.host, fh.port, util.ReqString(fh.req))
         break
      }
   }
 
   // return the mutated serviceList
   return fh.serializer.Encode(serviceList)
}
复制代码

DiscardCloudService

该过滤器针对两种 service 其中的一种类型是 LoadBalancer,因为边缘端无法访问 LoadBalancer 类型的资源,所以该过滤器会将这种类型的资源直接过滤掉。另外一种是针对 kube-system 名称空间下的 x-tunnel-server-internal-svc,这个 services 主要存在 cloud 节点用于访问 yurt-tunnel-server,对于 edge 节点会直接过滤掉该 service。

func (fh *discardCloudServiceFilterHandler) ObjectResponseFilter(b []byte) ([]byte, error) {
   list, err := fh.serializer.Decode(b)
   if err != nil || list == nil {
      klog.Errorf("skip filter, failed to decode response in ObjectResponseFilter of discardCloudServiceFilterHandler %v", err)
      return b, nil
   }
 
   serviceList, ok := list.(*v1.ServiceList)
   if ok {
      var svcNew []v1.Service
      for i := range serviceList.Items {
         nsName := fmt.Sprintf("%s/%s", serviceList.Items[i].Namespace, serviceList.Items[i].Name)
         // remove lb service
         if serviceList.Items[i].Spec.Type == v1.ServiceTypeLoadBalancer {
            if serviceList.Items[i].Annotations[filter.SkipDiscardServiceAnnotation] != "true" {
               klog.V(2).Infof("load balancer service(%s) is discarded in ObjectResponseFilter of discardCloudServiceFilterHandler", nsName)
               continue
            }
         }
 
         // remove cloud clusterIP service
         if _, ok := cloudClusterIPService[nsName]; ok {
            klog.V(2).Infof("clusterIP service(%s) is discarded in ObjectResponseFilter of discardCloudServiceFilterHandler", nsName)
            continue
         }
 
         svcNew = append(svcNew, serviceList.Items[i])
      }
      serviceList.Items = svcNew
      return fh.serializer.Encode(serviceList)
   }
 
   return b, nil
}
复制代码

过滤框架现状

目前的过滤框架比较僵硬,将资源过滤硬编码至代码中,只能是已注册的资源才能进行相应的过滤,为了解决这个问题,需要对过滤框架进行相应的改造。

解决方案

方案一:

使用参数或者环境变量的形式自定义过滤配置,但是这种方式有以下弊端:

1、配置复杂需要将所以需要自定义的配置写入到启动参数或者读取环境变量 例如下格式:

--filter_serviceTopology=coredns/endpointslices#list,kube-proxy/services#list;watch --filter_endpointsFilter=nginx-ingress-controller/endpoints#list;watch
复制代码

2、无法热更新,每次修改配置都需要重启 Yurthub 生效。

方案二:

1、使用 configmap 的形式自定义过滤配置降低配置复杂度配置格式(user-agent/resource#list,watch) 多个资源通过逗号隔开。如下所示:

filter_endpoints: coredns/endpoints#list;watch,test/endpoints#list;watch
filter_servicetopology: coredns/endpointslices#list;watch
filter_discardcloudservice: ""
filter_masterservice: ""
复制代码

2、利用 Informer 机制保证配置实时生效

综合以上两点在 OpenYurt 中我们选择了解决方案二。

开发过程中遇到的问题

在边缘端 Informer watch 的 api 地址是 Yurthub 的代理地址,那么 Yurthub 在启动代理端口之前都是无法保证 configmap 的数据是正常的。如果在启动完成之后 addons 的请求先于 configmap 数据更新 这个时候会导致数据在没有过滤的情况下就返回给了 addons,这样会导致很多预期以外的问题。

为了解决这个问题 我们需要在 apporve 中加入 WaitForCacheSync 保证数据同步完成之后才能返回相应的过滤数据,但是在 apporve 中加入 WaitForCacheSync 也直接导致 configmap 进行 watch 的时候也会被阻塞,所以需要在 WaitForCacheSync 之前加入一个白名单机制,当 Yurthub 使用 list & watch 访问 configmap 的时候我们直接不进行数据过滤,相应的代码逻辑如下:

func (a *approver) Approve(comp, resource, verb string) bool {
   if a.isWhitelistReq(comp, resource, verb) {
      return false
   }
   if ok := cache.WaitForCacheSync(a.stopCh, a.configMapSynced); !ok {
      panic("wait for configMap cache sync timeout")
   }
   a.Lock()
   defer a.Unlock()
   for _, requests := range a.nameToRequests {
      for _, request := range requests {
         if request.Equal(comp, resource, verb) {
            return true
         }
      }
   }
   return false
}
复制代码

总结

1、通过上述的扩展能力可以看出,YurtHub 不仅仅是边缘节点上的带有数据缓存能力的反向代理。而是对 Kubernetes 节点应用生命周期管理加了一层新的封装,提供边缘计算所需要的核心管控能力。

2、YurtHub 不仅仅适用于边缘计算场景,其实可以作为节点侧的一个常备组件,适用于使用 Kubernetes 的任意场景。相信这也会驱动 YurtHub 向更高性能,更高稳定性发展。

点击​此处​​,立即了解 OpenYurt 项目!​

Acho que você gosta

Origin juejin.im/post/7082857921402372132
Recomendado
Clasificación