111
Posted 拾月凄辰
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了111相关的知识,希望对你有一定的参考价值。
为什么需要 Service
在 K8s 集群里面会通过 pod 去部署应用,与传统的应用部署不同,传统应用部署在给定的机器上面去部署,我们知道怎么去调用别的机器的 IP 地址。但是在 K8s 集群里面应用是通过 pod 去部署的, 而 pod 生命周期是短暂的。在 pod 的生命周期过程中,比如它创建或销毁,它的 IP 地址都会发生变化,这样就不能使用传统的部署方式,不能指定 IP 去访问指定的应用。
另外在 K8s 的应用部署里,之前虽然学习了 deployment 的应用部署模式,但还是需要创建一个 pod 组,然后这些 pod 组需要提供一个统一的访问入口,以及怎么去控制流量负载均衡到这个组里面。比如说测试环境、预发环境和线上环境,其实在部署的过程中需要保持同样的一个部署模板以及访问方式。因为这样就可以用同一套应用的模板在不同的环境中直接发布。
Service:Kubernetes 中的服务发现与负载均衡
最后应用服务需要暴露到外部去访问,需要提供给外部的用户去调用的。我们上节了解到 pod 的网络跟机器不是同一个段的网络,那怎么让 pod 网络暴露到去给外部访问呢?这时就需要服务发现。
在 K8s 里面,服务发现与负载均衡就是 K8s Service。上图就是在 K8s 里 Service 的架构,K8s Service 向上提供了外部网络以及 pod 网络的访问,即外部网络可以通过 service 去访问,pod 网络也可以通过 K8s Service 去访问。
向下,K8s 对接了另外一组 pod,即可以通过 K8s Service 的方式去负载均衡到一组 pod 上面去,这样相当于解决了前面所说的复发性问题,或者提供了统一的访问入口去做服务发现,然后又可以给外部网络访问,解决不同的 pod 之间的访问,提供统一的访问地址。
用例解读
下面进行实际的一个用例解读,看 pod K8s 的 service 要怎么去声明、怎么去使用?
1. Service 语法
首先来看 K8s Service 的一个语法,上图实际就是 K8s 的一个声明结构。这个结构里有很多语法,跟之前所介绍的 K8s 的一些标准对象有很多相似之处。比如说标签 label 去做一些选择、selector 去做一些选择、label 去声明它的一些 label 标签等。
这里有一个新的知识点,就是定义了用于 K8s Service 服务发现的一个协议以及端口。继续来看这个模板,声明了一个名叫 my-service 的一个 K8s Service,它有一个 app:my-service 的 label,它选择了 app:MyApp 这样一个 label 的 pod 作为它的后端。
最后是定义的服务发现的协议以及端口,这个示例中我们定义的是 TCP 协议,端口是 80,目的端口是 9376,效果是访问到这个 service 80 端口会被路由到后端的 targetPort,就是只要访问到这个 service 80 端口的都会负载均衡到后端 app:MyApp 这种 label 的 pod 的 9376 端口。
2. 创建和查看 Service
Service 创建之后如上图所示,它会在集群里面创建一个虚拟的 IP 地址以及端口,在集群里,所有的 pod 和 node 都可以通过这样一个 IP 地址和端口去访问到这个 service。
我们通过 Endpoints 可以看到:通过前面所声明的 selector 去选择了哪些 pod?以及这些 pod 都是什么样一个状态?比如说通过 selector,我们看到它选择了这些 pod 的一个 IP,以及这些 pod 所声明的 targetPort 的一个端口。
实际的架构如上图所示。在 service 创建之后,它会在集群里面创建一个虚拟的 IP 地址以及端口,在集群里,所有的 pod 和 node 都可以通过这样一个 IP 地址和端口去访问到这个 service。这个 service 会把它选择的 pod 及其 IP 地址都挂载到后端。这样通过 service 的 IP 地址访问时,就可以负载均衡到后端这些 pod 上面去。
当 pod 的生命周期有变化时,比如说其中一个 pod 销毁,service 就会自动从后端摘除这个 pod。这样实现了:就算 pod 的生命周期有变化,它访问的端点是不会发生变化的。
集群内访问 Service
集群内有三种访问 service 的方式:
1. 通过虚拟 IP
首先我们可以通过 service 的虚拟 IP 去访问,比如说刚创建的 my-service 这个服务,通过kubectl get svc 或者 kubectl discribe service 都可以看到它的虚拟 IP 地址是 172.29.3.27,端口是 80,然后就可以通过这个虚拟 IP 及端口在 pod 里面直接访问到这个 service 的地址。比如:curl 172.29.3.27:80 或者 wget -O- 172.29.3.27:80。
2. 直接访问服务名,依靠 DNS 解析
第二种方式直接访问服务名,依靠 DNS 解析,就是同一个 namespace 里 pod 可以直接通过 service 的名字去访问到刚才所声明的这个 service。不同的 namespace 里面,我们可以通过<service名称>.<namespace名称> 去访问这个 service,例如我们直接用 curl 去访问,就是 curl my-service:80 就可以访问到这个 service。
3. 通过环境变量访问
第三种是通过环境变量访问,在同一个 namespace 里的 pod 启动时,K8s 会把 service 的一些 IP 地址、端口,以及一些简单的配置,通过环境变量的方式放到 K8s 的 pod 里面 **(注意:在该service创建之前就已经启动的容器,在该service被创建之后不会自动配置该service的环境变量,可以通过容器里面执行 env 命令进行验证)**。在 K8s pod 的容器启动之后,通过读取系统的环境变量读取到 namespace 里面其他 service 配置的一个地址,或者是它的端口号等等。比如在集群的某一个 pod 里面,取到环境变量 MY_SERVICE_SERVICE_HOST 就是刚刚创建的 service 的一个 IP 地址,MY_SERVICE 就是刚才我们声明的 MY_SERVICE,SERVICE_PORT 就是它的端口号,这样也可以请求到集群里面的 MY_SERVICE 这个 service。
Headless Service
service 有一个特别的形态就是 Headless Service。service 创建的时候可以指定 clusterIP:None,告诉 K8s 说我不需要 clusterIP(就是刚才所说的集群里面的一个虚拟 IP),然后 K8s 就不会分配给这个 service 一个虚拟 IP 地址,它没有虚拟 IP 地址怎么做到负载均衡以及统一的访问入口呢?
它是这样来操作的:pod 可以直接通过 service_name 用 DNS 的方式解析到所有后端 pod 的 IP 地址,通过 DNS 的 A 记录(从域名解析 IP 的记录)的方式会解析到所有后端的 Pod 的地址,由客户端选择一个后端的 IP 地址,这个 A 记录会随着 pod 的生命周期变化,返回的 A 记录列表也发生变化,这样就要求客户端应用要从 A 记录把所有 DNS 返回到 A 记录的列表里面 IP 地址中,客户端自己去选择一个合适的地址去访问 pod。
可以从上图看一下跟刚才我们声明的模板的区别,就是在中间加了一个 clusterIP:None,即表明不需要虚拟 IP。实际效果就是集群的 pod 访问 my-service 时,会直接解析到所有的 service 对应 pod 的 IP 地址,返回给 pod,然后 pod 里面自己去选择一个 IP 地址去直接访问。
向集群外暴露 Service
前面介绍的都是在集群里面 node 或者 pod 去访问 service,service 怎么去向外暴露呢?怎么把应用实际暴露给公网去访问呢?这里 service 也有两种类型去解决这个问题,一个是 NodePort,一个是 LoadBalancer。
- NodePort 的方式就是在集群的 node 上面(即集群的节点的宿主机上面)去暴露节点上的一个端口,这样相当于在节点的一个端口上面访问到之后就会再去做一层转发,转发到虚拟的 IP 地址上面,就是刚刚宿主机上面 service 虚拟 IP 地址。
- LoadBalancer 类型就是在 NodePort 上面又做了一层转换,刚才所说的 NodePort 其实是集群里面每个节点上面一个端口,LoadBalancer 是在所有的节点前又挂一个负载均衡。比如在阿里云上挂一个 SLB,这个负载均衡会提供一个统一的入口,并把所有它接触到的流量负载均衡到每一个集群节点的 NodePort 上面去。然后 NodePort 再转化成 ClusterIP,去访问到实际的 pod 上面。
Service 与 DNS 的关系
在 Kubernetes 中,Service 和 Pod 都会被分配对应的 DNS A 记录(从域名解析 IP 的记录)。对于 ClusterIP 模式的 Service 来说(比如我们上面的例子),它的 A 记录的格式是:<service名称>.<namespace名称>.svc.cluster.local。当你访问这条 A 记录的时候,它解析到的就是该 Service 的 VIP 地址。
而对于指定了 clusterIP=None 的 Headless Service 来说,它的 A 记录的格式也是:<service名称>.<namespace名称>.svc.cluster.local。但是,当你访问这条 A 记录的时候,它返回的是所有被代理的 Pod 的 IP 地址的集合。当然,如果你的客户端没办法解析这个集合的话,它可能会只会拿到第一个 Pod 的 IP 地址。
此外,对于 ClusterIP 模式的 Service 来说,它代理的 Pod 被自动分配的 A 记录的格式是:<service名称>.<namespace名称>.pod.cluster.local。这条记录指向 Pod 的 IP 地址。
而对 Headless Service 来说,它代理的 Pod 被自动分配的 A 记录的格式是:<service名称>.<namespace名称>.svc.cluster.local。这条记录也指向 Pod 的 IP 地址。
Service Controller
Service 有多种不同的类型,其中一种是 LoadBalancer service,从基础设施中请求一个负载均衡器使得service可以从外部被访问。Service Controller就是用来在 LoadBalancer 类型的 service 被创建或删除时,从基础设施中请求和释放负载均衡器的。
Endpoints Controller
Service 不会直接连接到 pod,而是包含一个 endpoint 列表 (IP和端口),这个 endpoints 列表要么是手动创建或更新,要么是根据 Service 中定义的 pod selector 来自动创建、更新。Endpoints Controller 会根据匹配标签选择器的 pod 的 IP、端口自动更新 endpoints 列表。
如下图所示,Endpoints Controller 同时监听了 Service 和 Pod。当 Service 或者 Pod 被添加、修改或者删除时,Endpoints Controller 会根据匹配标签选择器的 pod 的 IP、端口自动更新 endpoints 列表。
注意:只有处于 Running 状态,且 readinessProbe 检查通过的 Pod(处于 ready 状态),才会出现在 Service 的 Endpoints 列表里。并且,当某一个 Pod 出现问题时,Kubernetes 会自动把它从 Service 里摘除掉。
kube-proxy 的工作模式
userspace 模式
在 userspace 模式下,kube-proxy会为每一个Service创建一个监听端口,发向Cluster IP的请求被Iptables规则重定向到kube-proxy监听的端口上,kube-proxy根据LB算法选择一个提供服务的Pod并和其建立链接,以将请求转发到Pod上。
为什么 userspace 模式要建立 iptables 规则,因为 kube-proxy 监听的端口在用户空间,这个端口不是服务的访问端口也不是服务的 nodePort,因此需要一层 iptables 把访问服务的连接重定向给 kube-proxy 服务。
由于kube-proxy运行在userspace中,在进行转发处理时会增加内核和用户空间之间的数据拷贝,虽然比较稳定,但是效率比较低。
iptables 模式
iptables 模式是目前默认的代理方式,基于 netfilter 实现。当客户端请求 service 的 ClusterIP 时,根据 iptables 规则路由到各 pod 上,iptables 使用 DNAT 来完成转发,其采用了随机数实现负载均衡。
iptables 模式与 userspace 模式最大的区别在于,iptables 模块使用 DNAT 模块实现了 service 入口地址到 pod 实际地址的转换,免去了一次内核态到用户态的切换,另一个与 userspace 代理模式不同的是,user space代理模式以轮询模式对连接做负载均衡,而iptables代理模式不会, 它随机选择pod。如果 iptables 代理最初选择的那个 pod 没有响应,它不会自动重试其他 pod。
iptables 模式最主要的问题是在 service 数量大的时候会产生太多的 iptables 规则,使用非增量式更新会引入一定的时延,大规模情况下有明显的性能问题。
ipvs 模式
当集群规模比较大时,iptables 规则刷新会非常慢,难以支持大规模集群,因其底层路由表的实现是链表,对路由规则的增删改查都要涉及遍历一次链表,ipvs 的问世正是解决此问题的。ipvs 是 LVS 的负载均衡模块,与 iptables 比较像的是,ipvs 的实现虽然也基于 netfilter 的钩子函数,但是它却使用哈希表作为底层的数据结构并且工作在内核态,也就是说 ipvs 在重定向流量和同步代理规则有着更好的性能,几乎允许无限的规模扩张。
IPVS 模式的工作原理,其实跟 iptables 模式类似。当我们创建了前面的 Service 之后,kube-proxy 首先会在宿主机上创建一个虚拟网卡(叫作:kube-ipvs0),并为它分配 Service VIP 作为 IP 地址,如下所示:
# ip addr
...
73:kube-ipvs0:<BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN qlen 1000
link/ether 1a:ce:f5:5f:c1:4d brd ff:ff:ff:ff:ff:ff
inet 10.0.1.175/32 scope global kube-ipvs0
valid_lft forever preferred_lft forever
而接下来,kube-proxy 就会通过 Linux 的 IPVS 模块,为这个 IP 地址设置三个 IPVS 虚拟主机,并设置这三个虚拟主机之间使用轮询模式 (rr) 来作为负载均衡策略。我们可以通过 ipvsadm 查看到这个设置,如下所示:
# ipvsadm -ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 10.102.128.4:80 rr
-> 10.244.3.6:9376 Masq 1 0 0
-> 10.244.1.7:9376 Masq 1 0 0
-> 10.244.2.3:9376 Masq 1 0 0
可以看到,这三个 IPVS 虚拟主机的 IP 地址和端口,对应的正是三个被代理的 Pod。
这时候,任何发往 10.102.128.4:80 的请求,就都会被 IPVS 模块转发到某一个后端 Pod 上了。
而相比于 iptables,IPVS 在内核中的实现其实也是基于 Netfilter 的 NAT 模式,所以在转发这一层上,理论上 IPVS 并没有显著的性能提升。但是,IPVS 并不需要在宿主机上为每个 Pod 设置 iptables 规则,而是把对这些“规则”的处理放到了内核态,从而极大地降低了维护这些规则的代价。这也正印证了我在前面提到过的,“将重要操作放入内核态”是提高性能的重要手段。
备注:这里你可以再回顾下第 33 篇文章《深入解析容器跨主机网络》中的相关内容。
不过需要注意的是,IPVS 模块只负责上述的负载均衡和代理功能。而一个完整的 Service 流程正常工作所需要的包过滤、SNAT 等操作,还是要靠 iptables 来实现。只不过,这些辅助性的 iptables 规则数量有限,也不会随着 Pod 数量的增加而增加。
所以,在大规模集群里,我非常建议你为 kube-proxy 设置–proxy-mode=ipvs 来开启这个功能。它为 Kubernetes 集群规模带来的提升,还是非常巨大的。
Service 是如何实现的
引入 kube-proxy
和 Service 相关的任何事情都由每个节点上 运行的 kube-proxy 进程处理。开始的时候,kube-proxy 确实是一个 proxy,等待连接,对每个进来的连接,连接到 一 个pod。这称为userspace (用户空间)代理模式。后来,性能更好的 iptables 模式取代了它。iptables 模式是目前默认的模式,如果你有需要也仍然可以配置 Kubemetes 使用旧模式。
我们之前了解过, 每个 Service 有其自己稳定的 IP 地址和端口。客户端(通常为 pod)通过连接该 IP 和端口使用 Service 。Service 的 IP地址是虚拟的, 没有被分配给任何网络接口, 当数据包离开节点时也不会列为数据包的源或目的IP地址。Service的一个关键细节是, 它们包含一个IP、端口对(endpoints)列表(或者针对多端口Service有多个IP、端口对),所以服务 IP本身并不代表任何东西。这就是为什么你不能够ping它们。
kube-proxy 如何使用 iptables
当在 API Server 中创建一个 Service 时, 就会立刻分配给它一个虚拟 IP 地址。之后 API Server 会通知所有运行在工作节点上的 kube-proxy 客户端进程有一个新的 Service 已经被创建了。之后每个工作节点上的 kube-proxy 都会让该 Service 在自己的节点上可寻址。原理是通过建立一些 iptables 规则,确保每个目的地为 Service IP/port 对的数据包被拦截,并修改其目的地址为这个 Service 对应的后端的 pod 地址,这样数据包就会被重定向到 Service 后端的一个pod上。
除了watch API 对 Service 的更改, kube-proxy 也 watch 对 Endpoint 对象的更改变化。毕竟,每当创建或删除一个新的后端pod时,以及当 pod 的 ready 状态改变或 pod 的标签改变并且它属于或超出 Service 范围时,endpoint对象都会发生变化。当endpoint对象发生变化后,kube-proxy会动态的刷新iptables中的规则。比如当该Service对应的endpoint中增加一个pod的时候,kube-proxy就会在 iptables 中添加一个规则,这样以后访问Service的时候流量经过iptables就能匹配到这个新的pod上了。
kube-proxy 的工作过程
如下图所示,图中描述kube-proxy做了什么, 以及数据包如何通过客户端pod发送到Service后端的一 个pod上。
Node A中的kube-proxy 通过watch机制得知 Service B 创建之后,就在其iptables中增加了Service转发到 Endpoints B的规则。
让我们检查一下当通过客户端pod (图中的podA) 向 Service B 发送数据包时发生了什么。
包目的地初始设置为 Service B 的IP和端口(在本例中, Service B 是在172.30.0.1:80)。发送到网络之前, Node A的内核会根据配置在该节点上的iptables规则处理数据包。
Node A的内核首先会检查数据包是否会匹配iptables中的任意一条规则。比如其中一条规则为:目的地址为 172.30.0.1 并且端口号为80的数据包,其目的地址将被替换为Endpoints B 中随机选出来的一个 pod 。如下图的例子所示,当 Pod A 中发出的数据包经过了 iptables的检查之后,其目的地址改为了 Pod B2的地址。就好像是 Pod A直接发数据包给 Pod B而不是通过 Service B。
Kubernetes 服务发现架构
如上图所示,K8s 服务发现以及 K8s Service 是这样整体的一个架构。
K8s 分为 master 节点和 worker 节点:
- master 里面主要是 K8s 管控的内容;
- worker 节点里面是实际跑用户应用的一个地方。
在 K8s master 节点里面有 APIServer,就是统一管理 K8s 所有对象的地方,所有的组件都会注册到 APIServer 上面去监听这个对象的变化,比如说我们刚才的组件 pod 生命周期发生变化的这些事件。
这里面最关键的有三个组件:
- 一个是 Cloud Controller Manager,负责去配置 LoadBalancer 的一个负载均衡器给外部去访问;
- 另外一个就是 Coredns,就是通过 Coredns 去观测 APIServer 里面的 service 后端 pod 的一个变化,去配置 service 的 DNS 解析,实现可以通过 service 的名字直接访问到 service 的虚拟 IP,或者是 Headless 类型的 Service 中的 IP 列表的解析;
- 然后在每个 node 里面会有 kube-proxy 这个组件,它通过监听 service 以及 pod 变化,然后实际去配置集群里面的 NodePort 或者是虚拟 IP 地址的一个访问。
实际访问链路是什么样的呢?比如说从集群内部的一个 Client Pod3 去访问 Service,就类似于刚才所演示的一个效果。Client Pod3 首先通过 Coredns 这里去解析出 ServiceIP,Coredns 会返回给它 ServiceName 所对应的 service IP 是什么,这个 Client Pod3 就会拿这个 Service IP 去做请求,它的请求到宿主机的网络之后,就会被 kube-proxy 所配置的 iptables 或者 IPVS 去做一层拦截处理,之后去负载均衡到每一个实际的后端 pod 上面去,这样就实现了一个负载均衡以及服务发现。
对于外部的流量,比如说刚才通过公网访问的一个请求。它是通过外部的一个负载均衡器 Cloud Controller Manager 去监听 service 的变化之后,去配置的一个负载均衡器,然后转发到节点上的一个 NodePort 上面去,NodePort 也会经过 kube-proxy 的一个配置的一个 iptables,把 NodePort 的流量转换成 ClusterIP,紧接着转换成后端的一个 pod 的 IP 地址,去做负载均衡以及服务发现。这就是整个 K8s 服务发现以及 K8s Service 整体的结构。
参考资料
CNCF × Alibaba 云原生技术公开课 - 第14讲:Kubernetes Service
《Kubernetes in Action (英文版)》第11.1.6节,第11.5节
极客时间《深入剖析Kubernetes》:37 | 找到容器不容易:Service、DNS与服务发现
以上是关于111的主要内容,如果未能解决你的问题,请参考以下文章