Análisis del marco de filtrado de datos Yurthub de OpenYurt

Autor: Ying Jianjian, Centro de computación en la nube de Xinhua Zhi

OpenYurt es el primer proyecto de código abierto nativo de la nube de computación de borde no intrusivo de la industria. Brinda a los usuarios una experiencia de borde de nube integrada a través de autonomía de borde, colaboración de borde de nube, unificación de borde y capacidades de circuito cerrado de tráfico de borde. En Openyurt, la red perimetral puede utilizar el marco de filtrado de datos para lograr la capacidad de ciclo cerrado del tráfico perimetral en diferentes grupos de nodos.

Análisis del marco de filtrado de datos de Yurthub

Yurthub es esencialmente una capa de proxy kube-apiserver, y se agrega una capa de caché sobre la base del proxy para garantizar que la caché local se pueda usar para garantizar la estabilidad comercial cuando el nodo perimetral está fuera de línea, lo que resuelve efectivamente el problema de autonomía de borde. En segundo lugar, puede reducir la carga en la API en la nube causada por una gran cantidad de operaciones de lista y observación.

El filtrado de datos de Yurthub se envía al kube-apiserver a través del pod en el nodo y la solicitud de kubelet a través del Load Balancer. El proxy recibe el mensaje de respuesta y realiza el procesamiento de filtrado de datos, y luego devuelve los datos filtrados al solicitante. Si el nodo es un nodo perimetral, almacenará en caché localmente los recursos en el cuerpo de la solicitud de respuesta según el tipo de solicitud. Si es un nodo en la nube, no almacenará en caché localmente considerando que el estado de la red es bueno.

Diagrama esquemático de la implementación del marco de filtrado de Yurthub:

Yurthub actualmente contiene cuatro reglas de filtrado. El agente de usuario, el recurso y el verbo solicitado por los complementos se utilizan para determinar qué filtro filtrar los datos correspondientes.

Cuatro funciones de reglas de filtrado e implementación

ServicioTopologíaFiltro 

El filtrado de datos es principalmente para los recursos de EndpointSlice, pero la función Endpoint Slice debe admitirse en Kubernetes v1.18 o superior. Si es anterior a la versión 1.18, se recomienda usar el filtro endpointsFilter. Al pasar por este filtro, primero busque el recurso de servicios correspondiente al recurso endpointSlice a través de kubernetes.io/service-name, y luego juzgue si el recurso de servicio tiene una anotación de openyurt.io/topologyKeys.Si existe, juzgue los datos regla de filtrado por el valor de esta anotación.Finalmente actualice los datos de respuesta y devuélvalos a los complementos.

Los valores de las anotaciones se dividen en dos grandes categorías:

1. kubernetes.io/hostname: solo filtre la IP del punto final del mismo nodo

2. openyurt.io/nodepool o kubernetes.io/zone: obtenga el grupo de nodos correspondiente a través de estas anotaciones y, finalmente, recorra los recursos de endpointSlice y encuentre los puntos finales correspondientes en el objeto endpointSlice a través del campo kubernetes.io/hostname en la topología campo en el endpointSlice Luego reorganice los Endpoints en el endpointSlice y devuélvalo a los complementos. 

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
}
复制代码

EndpointsFilter

Realice el filtrado de datos correspondiente para los recursos de los puntos finales. Primero, determine si el punto final tiene un servicio correspondiente, obtenga el grupo de nodos a través de la etiqueta del nodo: apps.openyurt.io/nodepool, luego obtenga todos los nodos bajo el grupo de nodos y recorra los recursos bajo endpoints.Subsets to find La dirección del pod Ready y la dirección del pod NotReady del mismo grupo de nodos se reorganizan en nuevos puntos finales y se devuelven a los complementos.

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 项目!​

Supongo que te gusta

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