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
- 获取 service 对象。
- 根据 service 的标签获取 pods (这里获取到的 pods 就是 slicesToCreate 凭据的点)。
- 根据 service 命名空间和标签获取 apiserver 已有的所有关联的 endpointSlices 。
- 过滤掉被标记为删除的 endpointSlice 。
- 实际最终调用 c.reconciler.reconcile() 。
reconcile
c.reconciler.reconcile()
存放切片的变量:数组 slicesToDelete , map slicesByAddressType
- 检查 endpointSlice 的 AddressType ,没匹配到类型的加入到 slicesToDelete 数组等待删除。匹配到响应的地址类型的 endpointSlice 加入到 slicesByAddressType 数组。
- 不同地址类型的 endpointSlice 都会调用 r.reconcileByAddressType() 函数去调谐,传的参数里面就包含了地址类型。
r.reconcileByAddressType()
- 数组 slicesToCreate 、 slicesToUpdate 、 slicesToDelete 。
- 构建一个用于存放 endpointSlice 存在状态的结构体 existingSlicesByPortMap 。
- 构建一个用于存放 endpointSlice 期望状态的结构体 desiredEndpointsByPortMap 。
- 确定每组 endpointSlice 是否需要更新,调用 r.reconcileByPortMapping() 计算需要更新的 endpointSlice ,并返回 slicesToCreate, slicesToUpdate, slicesToDelete, numAdded, numRemoved 对象(计算过程遍历每个 slice 并填满至设定好的 endpoint 个数,默认 100 个,总长度不满 100 的单独一个 slice )给 r.finalize() 函数处理。
- 调用 r.finalize() 创建、更新或删除指定的 endpointSlice 对象。
r.finalize()
- 当同时有需要删除和新增的 slice 时,会优先把要删除的 slice 名替换到需要新增的 slice 上,再执行 slice 更新(意图是减少开销? 比如,要新增 A B C 三个,要删除 D E 两个,会遍历需要新增的 slice ,把 A 名替换成 D 的,B 替换成 E 的,再执行更新)
- 之后依次执行新增,更新和删除 slices 。
总结
-
总的来说,跟其他的控制器的逻辑是差不多的,都是先监听相关资源的事件,然后调谐。
-
从上面的代码我们也不难看出,endpointslice 有个特点就是,默认情况下,每个 slice 都是满 100 个条目就 new 一个新的切片,把每个切片的容量都控制在 100 个条目以内。
-
我们看完 endpointslice ,该控制器具有新增,更新和删除 slices 的功能,但是我们还发现源码里头还有 endpointslicemirroring 控制器。
-
endpointslicemirroring:在某些场合,应用会创建定制的 Endpoints 资源。为了保证这些应用不需要并发的更改 Endpoints 和 EndpointSlice 资源,集群的控制面将大多数 Endpoints 映射到对应的 EndpointSlice 之上。
控制面对 Endpoints 资源进行映射的例外情况有:
- Endpoints 资源上标签 endpointslice.kubernetes.io/skip-mirror 值为 true。
- Endpoints 资源包含标签 control-plane.alpha.kubernetes.io/leader。
- 对应的 Service 资源不存在。
- 对应的 Service 的选择算符不为空。
-
endpointslicemirroring 控制器我们等有时间再看看,我们先看看其他组件。
想要原文可以加作者v:mkjnnm