惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

K
Kaspersky official blog
罗磊的独立博客
F
Fortinet All Blogs
人人都是产品经理
人人都是产品经理
量子位
V
Visual Studio Blog
Blog — PlanetScale
Blog — PlanetScale
M
MIT News - Artificial intelligence
B
Blog RSS Feed
腾讯CDC
博客园_首页
aimingoo的专栏
aimingoo的专栏
博客园 - 三生石上(FineUI控件)
博客园 - Franky
S
SegmentFault 最新的问题
N
Netflix TechBlog - Medium
小众软件
小众软件
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
L
LINUX DO - 热门话题
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Martin Fowler
Martin Fowler
D
Docker
P
Privacy & Cybersecurity Law Blog
S
Securelist
V
V2EX
Jina AI
Jina AI
阮一峰的网络日志
阮一峰的网络日志
T
Tor Project blog
The Hacker News
The Hacker News
Microsoft Azure Blog
Microsoft Azure Blog
AWS News Blog
AWS News Blog
The GitHub Blog
The GitHub Blog
有赞技术团队
有赞技术团队
T
The Exploit Database - CXSecurity.com
Help Net Security
Help Net Security
酷 壳 – CoolShell
酷 壳 – CoolShell
Application and Cybersecurity Blog
Application and Cybersecurity Blog
博客园 - 叶小钗
Recent Announcements
Recent Announcements
Cloudbric
Cloudbric
Y
Y Combinator Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Latest news
Latest news
MongoDB | Blog
MongoDB | Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Recorded Future
Recorded Future
V2EX - 技术
V2EX - 技术

蛮荆

如何获取更多的免费服务器 Kubernetes 调度器队列 - 设计与实现 Kubernetes 调度器 - 核心流程 Kubernetes Networking Model & CNI Kubernetes 控制器管理总结 Kubernetes CronJob 设计与实现 Kubernetes Job 设计与实现 Kubernetes HPA 设计与实现 Kubernetes Deployment 滚动更新实现原理 Kubernetes GC 设计与实现 Kubernetes Pod 驱逐 - 设计与实现 Kubernetes Daemonset 设计与实现 Kubernetes ReplicaSet 设计与实现 Kubernetes Informer 设计与实现 降本增效之应用优化 (三) 日志存储与检索 Kubernetes Pod 设计与实现 - 创建流程 Kubernetes 探针设计与实现 Unix 编程艺术名句摘录 Kubernetes - CRI 概述 Golang 编译速度为什么这么快? Kubernetes Pod 设计与实现 - Pause 容器 Kubernetes - kube-proxy 代理模式工程优化 Kubernetes 应用最佳实践 - 优雅关闭长连接 Kubernetes Service 类型和会话亲和性 Kubernetes 为什么需要 Ingress Kubernetes 架构 - 控制平面和数据平面 降本增效之应用优化 (二) 大报表 Go 语言如何获取 CPU 利用率 降本增效之应用优化 (一) Redis 业务规则引擎演变过程简述 微服务中的熔断算法 漏桶算法和令牌桶算法 jsonparser 为什么比标准库的 encoding/json 快 10 倍 ? zap 高性能设计与实现 HTTP Router 算法演进 fastcache 高性能设计与实现 Web 常见的三个安全问题 ants Code Reading Go 线程安全 map 方案选型 布隆过滤器 死锁、活锁、饥饿、自旋锁 sync.Pool Code Reading Go 内存管理概述 Go netpoll Code Reading goroutine 泄漏与检测 time/Timer Code Reading GMP Scheduler Code Reading Go channel 的 15 条规则和底层实现 为什么 Linux “一切皆文件” context.Context Code Reading runtime/HACKING.md Goland 最佳实践 互联网开发与金庸武学 为什么 Redis 6.0 引入多线程模型? Kubernetes 应用最佳实践 - 金丝雀发布 容器中如何正确配置 GOMAXPROCS ? singleflight Code Reading sync.Map Code Reading sync.Cond Code Reading sync.WaitGroup Code Reading sync.RWMutex Code Reading sync.Mutex Code Reading sync.Once Code Reading Go 无锁编程 sync/atomic Code Reading goroutine 交替打印奇偶数 GODEBUG Go 并发模式 Go 汇编 UUID 通用技术选型 Kubernetes 应用最佳实践 - 水平自动伸缩 Go 高性能 Tips fasthttp 为什么比标准库 net/http 快 10 倍 ? 技术文章配图指南 ChatGPT 初体验 Docker 网络原理概览 iptables 的五表五链 Kubernetes 应用最佳实践 - 亲和性和污点容忍度 Go 的反射与三大定律 Docker 官方提供的最佳实践 Go 语言内置的设计模式 HTTP1 到 HTTP3 的工程优化 Kubernetes 应用最佳实践 - Sidecar 模式 Kubernetes 应用最佳实践 - init 容器和钩子函数 为什么 recover 必须在 defer 中调用? 为什么 defer 的执行顺序和注册顺序不同? Go map 设计与实现 Go 切片扩容底层实现 Go 语言中的零拷贝 Go Delve 云原生和边缘计算简介 Kubernetes Pod 服务质量等级 Kubernetes 应用最佳实践 - 探针 Kubernetes 应用最佳实践 - 资源请求和限制 CDN 原理 Kubernetes 应用最佳实践 - 开篇 缓存策略和模式 Go 内存模型 Kubernetes 核心概念 软件版本语义化
Kubernetes EndPoint 设计与实现
2023-12-08 · via 蛮荆

2023-12-08 Cloud Native Kubernetes 读代码

概述

EndPoint 的概念和作用在 这篇文章 中已经介绍过了,本文不再赘述,这里使用一张图来回顾一下 Service, EndPoint, Pod 三者之间的关系。

Service, EndPoint, Pod 关系图

EndpointSubset

EndpointSubset 是一组具有公共端口集的地址,扩展的端点集是 Addresses (Pod IP 地址) 和 Ports (Service 名称和端口号) 的笛卡尔积。

下面是一个典型的 EndpointSubset 示例:

  Name: "test",
  Subsets: [
    {
      Addresses: [
        {
          "ip": "10.10.1.1"
        },
        {
          "ip": "10.10.2.2"
        }
      ],
      Ports: [
        {
          "name": "a",
          "port": 8675
        },
        {
          "name": "b",
          "port": 309
        }
      ]
    }
]

将上面的 Subset 转换为对应的端点集合:

a: [ 10.10.1.1:8675, 10.10.2.2:8675 ]
b: [ 10.10.1.1:309, 10.10.2.2:309 ]

源码说明

本文着重从源代码的角度分析一下 EndPoint 的实现原理,EndPoint 功能对应的源代码位于 Kubernetes 项目的 pkg/controller/endpoint/ 目录,本文以 Kubernetes v1.28 版本源代码进行分析。

EndPoint 源代码目录

流程图

EndPoint 控制器执行流程图

下面我们跟着流程图一起看下源代码的具体实现。


EndPointController

首先来看看 Endpoints 控制器对象,该对象是实现 Endpoints 功能的核心对象。

// Controller manages selector-based service endpoints.
type Controller struct {
    // Service 列表
	serviceLister corelisters.ServiceLister

    // Pod 列表
	podLister corelisters.PodLister

	// EndPoint 列表
	endpointsLister corelisters.EndpointsLister

	// 队列中存储发生了变化 (需要同步) 的 Service
	queue workqueue.RateLimitingInterface
}

初始化

NewEndpointController 方法用于 EndPoint 控制器对象的初始化工作,并返回一个实例化对象,控制器对象同时订阅了 Service, Pod, EndPoint 三种资源的变更事件。

func NewEndpointController(podInformer ...) *Controller {
	e := &Controller{
        ...
	}

	// 增加 Service informer 监听回调方法
	serviceInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
        ...
	})

    // 增加 Pod informer 监听回调方法
	podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
        ...
	})

    // 增加 EndPoint informer 监听回调方法
	endpointsInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
		...
	})
	
	...

	return e
}

启动控制器

根据控制器的初始化方法 NewEndpointController 的调用链路,可以找到控制器开始启动和执行的地方。

// cmd/kube-controller-manager/app/core.go

func startEndpointsController(ctx context.Context, ...) (controller.Interface, bool, error) {
    // 启动一个单独的 goroutine 来完成 {初始化 && 运行}
	go endpointcontroller.NewEndpointController(
		...
	).Run(ctx, int(controllerContext.ComponentConfig.EndpointController.ConcurrentEndpointSyncs))
	
	return nil, true, nil
}

具体逻辑方法

Controller.Run 方法执行具体的初始化逻辑。

func (e *Controller) Run(ctx context.Context, workers int) {
	...
	
    // (根据参数配置) 启动多个 goroutine 处理逻辑 (默认为 5 个)
	for i := 0; i < workers; i++ {
		go wait.UntilWithContext(ctx, e.worker, e.workerLoopPeriod)
	}
	
	<-ctx.Done()
}

Controller.worker 方法本质上就是一个无限循环轮询器,不断从队列中取出 EndPoint 对象,然后进行对应的操作。

func (e *Controller) worker(ctx context.Context) {
	// 内部调用 processNextWorkItem 方法
	for e.processNextWorkItem(ctx) {
	}
}
func (e *Controller) processNextWorkItem(ctx context.Context) bool {
	// 从队列获取 EndPoint 对象
	eKey, quit := e.queue.Get()

	...
	
	// 调用 syncService 方法同步
	err := e.syncService(ctx, eKey.(string))

	return true
}

EndPoint 同步

Controller 的回调处理方法是 syncService 方法,该方法是 EndPoint 控制器操作的核心方法,通过方法的命名,可以知道 EndPoint 主要关注的对象是 Service。

func (e *Controller) syncService(ctx context.Context, key string) error {
	// 通过 key 解析出 Service 对象对应的 命名空间和名称
	namespace, name, err := cache.SplitMetaNamespaceKey(key)

    // 获取 Service 对象
	service, err := e.serviceLister.Services(namespace).Get(name)

    // Service 类型为 ExternalName
	// 直接返回
	if service.Spec.Type == v1.ServiceTypeExternalName {
		return nil
	}
    // Service 的标签选择器为 nil
    // 这种情况下关联不到 EndPoint 对象
	// 直接返回
	if service.Spec.Selector == nil {
		return nil
	}

	// 获取 Service 的标签选择器关联的 Pod 列表
	pods, err := e.podLister.Pods(service.Namespace).List(labels.Set(service.Spec.Selector).AsSelectorPreValidated())
	
	// 初始化端点集合对象
	subsets := []v1.EndpointSubset{}
	// 初始化已就绪的 EndPoint 对象计数
	var totalReadyEps int
	// 初始化未就绪的 EndPoint 对象计数
	var totalNotReadyEps int

	// 遍历 Pod 列表
	for _, pod := range pods {
		// 如果 Pod 的 IP 未分配
		//   或者 
		// Pod 的状态已经终止或者正在终止中... 
		if !endpointsliceutil.ShouldPodBeInEndpoints(pod, service.Spec.PublishNotReadyAddresses) {
			continue
		}

		// 实例化一个 EndPointAddress 对象
		// 例如
		// {
		//   "ip": "10.10.1.1"
        // }
		ep, err := podToEndpointAddressForService(service, pod)

		epa := *ep
		
		if len(service.Spec.Ports) == 0 {
			if service.Spec.ClusterIP == api.ClusterIPNone {
				// Headless 模式下,ClusterIP 的值设置为 "None" 
				// 构建一个新的对象追加到端点集合里
				subsets, totalReadyEps, totalNotReadyEps = addEndpointSubset(logger, subsets, pod, ...)
			}
		} else {
			// 遍历 Service 端口对象
			for i := range service.Spec.Ports {
				servicePort := &service.Spec.Ports[i]
				portNum, err := podutil.FindPort(pod, servicePort)

				// 根据 Service 端口对象 + 端口号构建一个对象
				epp := endpointPortFromServicePort(servicePort, portNum)
				
				// 将构建好的对象追加到端点集合里
				var readyEps, notReadyEps int
				subsets, readyEps, notReadyEps = addEndpointSubset(logger, subsets, pod, ...)
				
				// 累加已就绪的 EndPoint 对象计数
				totalReadyEps = totalReadyEps + readyEps
				// 累加未就绪的 EndPoint 对象计数
				totalNotReadyEps = totalNotReadyEps + notReadyEps
			}
		}
	}
	
	// 计算并确定最后的 EndPoint 对象集合 (新的 EndPoint Set)
	subsets = endpoints.RepackSubsets(subsets)
	
	// 通过 informer 获取 Service 对象对应的 EndPoint Set
	// 也就是当前的 EndPoint Set (旧的 EndPoint Set)
	currentEndpoints, err := e.endpointsLister.Endpoints(service.Namespace).Get(service.Name)

	// 如果 Service 的资源版本号未设置,就需要创建新的 EndPoints 
	createEndpoints := len(currentEndpoints.ResourceVersion) == 0

	// 对新的和旧的 EndPoint Set进行排序 + 比较操作
    // 如果新的 Set 和旧的 Set 比较之后,没有任何差异
    //   并且 Service 的版本号也不需要创建
    // 直接返回就可以了
	if !createEndpoints && ...) {
		return nil
	}
	
	// 深度拷贝当前的 EndPoint Set
	newEndpoints := currentEndpoints.DeepCopy()
	
	// 重新设置相关的 (最新) 属性
	newEndpoints.Subsets = subsets
	newEndpoints.Labels = service.Labels
	
	...
	
	if createEndpoints {
        // 创建新的 EndPoints
		_, err = e.client.CoreV1().Endpoints(service.Namespace).Create(ctx, newEndpoints, metav1.CreateOptions{})
	} else {
        // 更新已有 EndPoints
		_, err = e.client.CoreV1().Endpoints(service.Namespace).Update(ctx, newEndpoints, metav1.UpdateOptions{})
	}
	
    ...
}

通过 Controller.syncService 方法的源代码,我们可以看到: EndPoint 对象每次同步时,都会执行如下的操作:

  1. 根据参数 key 获取指定的 Service 对象
  2. 获取 Service 对象的标签选择器关联的 Pod 列表
  3. 通过 Service 和 Pod 列表计算出最新的 EndPoint 对象 (新) 集合
  4. 通过 informer 获取 Service 对象对应的 EndPoint 对象 (旧) 集合
  5. 如果新集合与旧集合对比,没有任何差异,说明不需要更新,直接退出方法即可
  6. 根据 Service 资源版本号确定 EndPoints 对象的操作 (创建或更新) 并执行

小结

EndPoint 控制器核心作用是订阅 Service 和 Pod 两类资源的变更事件,计算两者的映射关系集合并记录,最后对计算后的映射关系集合创建/更新响应的 EndPoints 资源。

EndPoint 控制器执行流程图

Reference