所属技术领域:
Kubernetes
|名词定义|
Kubernetes中一个应用服务会有一个或多个实例(Pod),每个实例(Pod)的IP地址由网络插件动态随机分配(Pod重启后IP地址会改变)。为屏蔽这些后端实例的动态变化和对多实例的负载均衡,引入了Service这个资源对象,如下所示:
根据创建Service的type类型不同,可分成4种模式:
ClusterIP: 默认方式。根据是否生成ClusterIP又可分为普通Service和Headless Service两类:
普通Service:通过为Kubernetes的Service分配一个集群内部可访问的固定虚拟IP(Cluster IP),实现集群内的访问。为最常见的方式。
Headless Service:该服务不会分配Cluster IP,也不通过kube-proxy做反向代理和负载均衡。而是通过DNS提供稳定的网络ID来访问,DNS会将headless service的后端直接解析为podIP列表。主要供StatefulSet使用。
NodePort:除了使用Cluster IP之外,还通过将service的port映射到集群内每个节点的相同一个端口,实现通过nodeIP:nodePort从集群外访问服务。
LoadBalancer:和nodePort类似,不过除了使用一个Cluster IP和nodePort之外,还会向所使用的公有云申请一个负载均衡器(负载均衡器后端映射到各节点的nodePort),实现从集群外通过LB访问服务。
ExternalName:是 Service 的特例。此模式主要面向运行在集群外部的服务,通过它可以将外部服务映射进k8s集群,且具备k8s内服务的一些特征(如具备namespace等属性),来为集群内部提供服务。此模式要求kube-dns的版本为1.7或以上。这种模式和前三种模式(除headless service)最大的不同是重定向依赖的是dns层次,而不是通过kube-proxy。
比如,在service定义中指定externalName的值"my.database.example.com":
此时k8s集群内的DNS服务会给集群内的服务名 ..svc.cluster.local 创建一个CNAME记录,其值为指定的"my.database.example.com"。
当查询k8s集群内的服务my-service.prod.svc.cluster.local时,集群的 DNS 服务将返回映射的CNAME记录"foo.bar.example.com"。
备注:
前3种模式,定义服务的时候通过selector指定服务对应的pods,根据pods的地址创建出endpoints作为服务后端;Endpoints Controller会watch Service以及pod的变化,维护对应的Endpoint信息。kube-proxy根据Service和Endpoint来维护本地的路由规则。当Endpoint发生变化,即Service以及关联的pod发生变化,kube-proxy都会在每个节点上更新iptables,实现一层负载均衡。
而ExternalName模式则不指定selector,相应的也就没有port和endpoints。
ExternalName和ClusterIP中的Headles Service同属于Headless Service的两种情况。Headless Service主要是指不分配Service IP,且不通过kube-proxy做反向代理和负载均衡的服务。
针对以上各发布方式,会涉及一些相应的Port和IP的概念。
|技术特点|
- 在 Kubernetes 上部署可扩展的 Web 应用
- 分析日志并监控 Kubernetes 应用的运行状况
- 持续部署到 Kubernetes
- 创建集群
|相关词|
Service
我们可以先来看一下 ServiceController 在 Service 对象变动时发生了什么事情,每当有服务被创建或者销毁时,Informer 都会通知 ServiceController,它会将这些任务投入工作队列中并由其本身启动的 Worker 协程消费:
不过 ServiceController 其实只处理了负载均衡类型的 Service 对象,它会调用云服务商的 API 接口,不同的云服务商会实现不同的适配器来创建 LoadBalancer 类型的资源。
我们以 GCE 为例简单介绍一下 Google Cloud 是如何对实现负载均衡类型的 Service:
上述代码会先判断是否应该先删除已经存在的负载均衡资源,随后会调用一个内部的方法 ensureExternalLoadBalancer 在 Google Cloud 上创建一个新的资源,这个方法的调用过程比较复杂:
- 检查转发规则是否存在并获取它的 IP 地址;
- 确定当前 LoadBalancer 使用的 IP 地址;
- 处理防火墙的规则的创建和更新;
- 创建和删除指定的健康检查;
想要了解 GCE 是如何对 LoadBalancer 进行支持的可以在 Kubernetes 中的 gce package 中阅读相关的代码,这里面就是 gce 对于云服务商特定资源的实现方式。
iptables
另一种常见的代理模式就是直接使用 iptables 转发当前节点上的全部流量,这种脱离了用户空间在内核空间中实现转发的方式能够极大地提高 proxy 的效率,增加 kube-proxy 的吞吐量。
iptables 作为一种代理模式,它同样实现了 OnServiceUpdate、OnEndpointsUpdate 等方法,这两个方法会分别调用相应的变更追踪对象。
sequenceDiagram participant SC as ServiceConfig participant P as Proxier participant SCT as ServiceChangeTracker participant SR as SyncRunner participant I as iptable SC->>+P: OnServiceAdd P->>P: OnServiceUpdate P->>SCT: Update SCT-->>P: Return ServiceMap deactivate P loop Every minSyncPeriod ~ syncPeriod SR->>+P: syncProxyRules P->>I: UpdateChain P->>P: writeLine x N P->>I: RestoreAll deactivate P end
变更追踪对象会根据 Service 或 Endpoint 对象的前后变化改变 ServiceChangeTracker 本身的状态,这些变更会每隔一段时间通过一个 700 行的巨大方法 syncProxyRules 同步,在这里就不介绍这个方法的具体实现了,它的主要功能就是根据 Service 和 Endpoint 对象的变更生成一条一条的 iptables 规则,比较感兴趣的读者,可以点击 proxier.go#L640-1379 查看代码。
当我们使用 iptables 的方式启动节点上的代理时,所有的流量都会先经过 PREROUTING 或者 OUTPUT 链,随后进入 Kubernetes 自定义的链入口 KUBE-SERVICES、单个 Service 对应的链 KUBE-SVC-XXXX 以及每个 Pod 对应的链 KUBE-SEP-XXXX,经过这些链的处理,最终才能够访问当一个服务的真实 IP 地址。
虽然相比于用户空间来说,直接运行在内核态的 iptables 能够增加代理的吞吐量,但是当集群中的节点数量非常多时,iptables 并不能达到生产级别的可用性要求,每次对规则进行匹配时都会遍历 iptables 中的所有 Service 链。
规则的更新也不是增量式的,当集群中的 Service 达到 5,000 个,每增加一条规则都需要耗时 11min,当集群中的 Service 达到 20,000 个时,每增加一条规则都需要消耗 5h 的时间,这也就是告诉我们在大规模集群中使用 iptables 作为代理模式是完全不可用的。