4.11 endpointSlice controller

EndpointSlice 是什么?相比于我们熟知的 endpoint ,有什么区别?

这里我们可以查看官方文档:

https://github.com/kubernetes/enhancements/tree/master/keps/sig-network/0752-endpointslices


使用Endpoints API,服务只有一个Endpoints资源。这意味着它需要能够为支持相应服务的每个Pod存储IP地址和端口(网络端点)。这耗费了巨大的API资源。为了解决此问题,kube-proxy在每个节点上运行,并会监视Endpoints资源的任何更新。如果在Endpoints资源中甚至只有一个网络端点发生了更改,则整个对象也必须发送到kube-proxy的每个实例。

Endpoints API的另一个限制是它限制了可以为服务跟踪的网络端点的数量。**存储在etcd中的对象的默认大小限制为1.5MB。在某些情况下,可能会将Endpoints资源限制为5,000个Pod IP。**对于大多数没有超过5000个pod的用户而言,这不是问题,但是对于服务接近此大小的用户而言,这将成为一个重大问题。

为了说明这些问题在多大程度上变得重要,举一个简单的例子是有帮助的。考虑具有5,000个Pod的服务,它最终可能具有1.5MB的端点资源。如果该列表中的单个网络端点都发生了更改,则需要将完整的端点资源分配给集群中的每个节点。在具有3,000个节点的大型群集中,这成为一个很大的问题。每次更新将涉及跨集群发送4.5GB数据(1.5MB端点* 3,000个节点)。这几乎足以耗费大量资源,并且每次端点更改都会发生这种情况。想象一下,如果滚动更新会导致全部5,000个Pod都被替换,那么传输的数据量超过22TB(等同于5000张DVD存储量)

使用EndpointSlice API拆分端点

EndpointSlice API旨在通过类似于分片的方法来解决此问题。我们没有使用单个Endpoints资源跟踪服务的所有Pod IP,而是将它们拆分为多个较小的EndpointSlice。

考虑一个示例,其中一个服务由15个容器支持。我们最终将获得一个跟踪所有端点的单个Endpoints资源。如果将EndpointSlices配置为每个存储5个端点,则最终将得到3个不同的EndpointSlices:

默认情况下,EndpointSlices每个存储多达100个端点,尽管可以使用–max-endpoints-per-slicekube-controller-manager上的标志进行配置。

入口函数

入口函数位于 cmd/kube-controller-manager/app/discovery.go

func startEndpointSliceController(ctx context.Context, controllerContext ControllerContext, controllerName string) (controller.Interface, bool, error) {
	go endpointslicecontroller.NewController(
		ctx,
		controllerContext.InformerFactory.Core().V1().Pods(),
		controllerContext.InformerFactory.Core().V1().Services(),
		controllerContext.InformerFactory.Core().V1().Nodes(),
		controllerContext.InformerFactory.Discovery().V1().EndpointSlices(),
		controllerContext.ComponentConfig.EndpointSliceController.MaxEndpointsPerSlice,
		controllerContext.ClientBuilder.ClientOrDie("endpointslice-controller"),
		controllerContext.ComponentConfig.EndpointSliceController.EndpointUpdatesBatchPeriod.Duration,
	).Run(ctx, int(controllerContext.ComponentConfig.EndpointSliceController.ConcurrentServiceEndpointSyncs))
	return nil, true, nil
}

构造函数

  • maxEndpointsPerSlice 每组切片的最大 endpoint 数量。
  • triggerTimeTracker 计算 service 和 pods 最后一次更新时间,并存到缓存,然会 2 者中最后一次更新的时间
  • reconciler 控制器的核心逻辑所在
  • features.TopologyAwareHints 是否开启拓扑感知提示特性,就近路由,比如节点 A B 属于同一区域,C D 属于另一个区域,pod 在 A B C D 节点上各有一个,查看 A B 节点上面的 ipvs 规则,会发现,通往该 pod service 的流量的 ipvs 后端,只有 A B 节点上的 pod ip ,C D 同理 ,可以参考这篇文章,说得很直白:Kubernetes Service 开启拓扑感知(就近访问)能力
// NewController creates and initializes a new Controller
func NewController(ctx context.Context, podInformer coreinformers.PodInformer,
    serviceInformer coreinformers.ServiceInformer,
    nodeInformer coreinformers.NodeInformer,
    endpointSliceInformer discoveryinformers.EndpointSliceInformer,
    maxEndpointsPerSlice int32,
    client clientset.Interface,
    endpointUpdatesBatchPeriod time.Duration,
) *Controller {
    broadcaster := record.NewBroadcaster(record.WithContext(ctx))
    recorder := broadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: "endpoint-slice-controller"})

    endpointslicemetrics.RegisterMetrics()

    c := &Controller{
       client: client,
       // This is similar to the DefaultControllerRateLimiter, just with a
       // significantly higher default backoff (1s vs 5ms). This controller
       // processes events that can require significant EndpointSlice changes,
       // such as an update to a Service or Deployment. A more significant
       // rate limit back off here helps ensure that the Controller does not
       // overwhelm the API Server.
       queue: workqueue.NewTypedRateLimitingQueueWithConfig(
          workqueue.NewTypedMaxOfRateLimiter(
             workqueue.NewTypedItemExponentialFailureRateLimiter[string](defaultSyncBackOff, maxSyncBackOff),
             // 10 qps, 100 bucket size. This is only for retry speed and its
             // only the overall factor (not per item).
             &workqueue.TypedBucketRateLimiter[string]{Limiter: rate.NewLimiter(rate.Limit(10), 100)},
          ),
          workqueue.TypedRateLimitingQueueConfig[string]{
             Name: "endpoint_slice",
          },
       ),
       workerLoopPeriod: time.Second,
    }

    serviceInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
       AddFunc: c.onServiceUpdate,
       UpdateFunc: func(old, cur interface{}) {
          c.onServiceUpdate(cur)
       },
       DeleteFunc: c.onServiceDelete,
    })
    c.serviceLister = serviceInformer.Lister()
    c.servicesSynced = serviceInformer.Informer().HasSynced

    podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
       AddFunc:    c.addPod,
       UpdateFunc: c.updatePod,
       DeleteFunc: c.deletePod,
    })
    c.podLister = podInformer.Lister()
    c.podsSynced = podInformer.Informer().HasSynced

    c.nodeLister = nodeInformer.Lister()
    c.nodesSynced = nodeInformer.Informer().HasSynced

    logger := klog.FromContext(ctx)
    endpointSliceInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
       AddFunc: c.onEndpointSliceAdd,
       UpdateFunc: func(oldObj, newObj interface{}) {
          c.onEndpointSliceUpdate(logger, oldObj, newObj)
       },
       DeleteFunc: c.onEndpointSliceDelete,
    })

    c.endpointSliceLister = endpointSliceInformer.Lister()
    c.endpointSlicesSynced = endpointSliceInformer.Informer().HasSynced
    c.endpointSliceTracker = endpointsliceutil.NewEndpointSliceTracker()

    c.maxEndpointsPerSlice = maxEndpointsPerSlice

    c.triggerTimeTracker = endpointsliceutil.NewTriggerTimeTracker()

    c.eventBroadcaster = broadcaster
    c.eventRecorder = recorder

    c.endpointUpdatesBatchPeriod = endpointUpdatesBatchPeriod

    if utilfeature.DefaultFeatureGate.Enabled(features.TopologyAwareHints) {
       nodeInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
          AddFunc: func(obj interface{}) {
             c.addNode(logger, obj)
          },
          UpdateFunc: func(oldObj, newObj interface{}) {
             c.updateNode(logger, oldObj, newObj)
          },
          DeleteFunc: func(obj interface{}) {
             c.deleteNode(logger, obj)
          },
       })

       c.topologyCache = topologycache.NewTopologyCache()
    }

    c.reconciler = endpointslicerec.NewReconciler(
       c.client,
       c.nodeLister,
       c.maxEndpointsPerSlice,
       c.endpointSliceTracker,
       c.topologyCache,
       c.eventRecorder,
       controllerName,
       endpointslicerec.WithTrafficDistributionEnabled(utilfeature.DefaultFeatureGate.Enabled(features.ServiceTrafficDistribution)),
    )

    return c
}

监听

监听 service pod node endpointSlice 对象。

service 对象
  • AddFunc

    onServiceUpdate 缓存 service Selector ,并加入令牌桶队列。

  • UpdateFunc

    onServiceUpdate 缓存 service Selector ,并加入令牌桶队列。

  • DeleteFunc

    onServiceDelete 删除缓存的 service Selector ,并加入令牌桶队列。

pod 对象
  • AddFunc

    addPod

    根据 pod 获取 service 对象,并把对应的 service 加入到延迟队列。

  • UpdateFunc

    updatePod 同上。

  • DeleteFunc

    deletePod

    如果 pod 对象不为 nil ,调用 addPod 事件函数处理。

node 对象

只有启用了 TopologyAwareHints 特性,才有对应的监听事件。

  • addNode

    调用 c.checkNodeTopologyDistribution() 检查节点拓扑分布情况。

  • updateNode

    检查节点状态,调用 c.checkNodeTopologyDistribution() 检查节点拓扑分布情况。

  • deleteNode

    调用 c.checkNodeTopologyDistribution() 检查节点拓扑分布情况。

endpointSlice 对象
  • AddFunc

    onEndpointSliceAdd

    调用 c.queueServiceForEndpointSlice() 接口,获取 service 唯一 key ,并计算更新延迟,按照延迟时间加入到延迟队列。

  • UpdateFunc

    onEndpointSliceUpdate

    最终调用 c.queueServiceForEndpointSlice() 接口,获取 service 唯一 key ,并计算更新延迟,按照延迟时间加入到延迟队列。

  • DeleteFunc

    onEndpointSliceDelete

    判断是否需要被删除,如果不希望被删除,则调用 c.queueServiceForEndpointSlice() 接口,获取 service 唯一 key ,并计算更新延迟,按照延迟时间加入到延迟队列。

syncService

核心逻辑入口 syncService ,实际最终调用的是 r.finalize() 函数。

syncService
  1. 获取 service 对象。
  2. 根据 service 的标签获取 pods (这里获取到的 pods 就是 slicesToCreate 凭据的点)。
  3. 根据 service 命名空间和标签获取 apiserver 已有的所有关联的 endpointSlices 。
  4. 过滤掉被标记为删除的 endpointSlice 。
  5. 实际最终调用 c.reconciler.reconcile() 。

reconcile

c.reconciler.reconcile()

存放切片的变量:数组 slicesToDelete , map slicesByAddressType

  1. 检查 endpointSlice 的 AddressType ,没匹配到类型的加入到 slicesToDelete 数组等待删除。匹配到响应的地址类型的 endpointSlice 加入到 slicesByAddressType 数组。
  2. 不同地址类型的 endpointSlice 都会调用 r.reconcileByAddressType() 函数去调谐,传的参数里面就包含了地址类型。

r.reconcileByAddressType()

  1. 数组 slicesToCreate 、 slicesToUpdate 、 slicesToDelete 。
  2. 构建一个用于存放 endpointSlice 存在状态的结构体 existingSlicesByPortMap 。
  3. 构建一个用于存放 endpointSlice 期望状态的结构体 desiredEndpointsByPortMap 。
  4. 确定每组 endpointSlice 是否需要更新,调用 r.reconcileByPortMapping() 计算需要更新的 endpointSlice ,并返回 slicesToCreate, slicesToUpdate, slicesToDelete, numAdded, numRemoved 对象(计算过程遍历每个 slice 并填满至设定好的 endpoint 个数,默认 100 个,总长度不满 100 的单独一个 slice )给 r.finalize() 函数处理。
  5. 调用 r.finalize() 创建、更新或删除指定的 endpointSlice 对象。

r.finalize()

  1. 当同时有需要删除和新增的 slice 时,会优先把要删除的 slice 名替换到需要新增的 slice 上,再执行 slice 更新(意图是减少开销? 比如,要新增 A B C 三个,要删除 D E 两个,会遍历需要新增的 slice ,把 A 名替换成 D 的,B 替换成 E 的,再执行更新)
  2. 之后依次执行新增,更新和删除 slices 。

总结

  1. 总的来说,跟其他的控制器的逻辑是差不多的,都是先监听相关资源的事件,然后调谐。

  2. 从上面的代码我们也不难看出,endpointslice 有个特点就是,默认情况下,每个 slice 都是满 100 个条目就 new 一个新的切片,把每个切片的容量都控制在 100 个条目以内。

  3. 我们看完 endpointslice ,该控制器具有新增,更新和删除 slices 的功能,但是我们还发现源码里头还有 endpointslicemirroring 控制器。

  4. endpointslicemirroring:在某些场合,应用会创建定制的 Endpoints 资源。为了保证这些应用不需要并发的更改 Endpoints 和 EndpointSlice 资源,集群的控制面将大多数 Endpoints 映射到对应的 EndpointSlice 之上。

    控制面对 Endpoints 资源进行映射的例外情况有:

    • Endpoints 资源上标签 endpointslice.kubernetes.io/skip-mirror 值为 true。
    • Endpoints 资源包含标签 control-plane.alpha.kubernetes.io/leader。
    • 对应的 Service 资源不存在。
    • 对应的 Service 的选择算符不为空。
  5. endpointslicemirroring 控制器我们等有时间再看看,我们先看看其他组件。
    想要原文可以加作者v:mkjnnm

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值