Kubernetes网络自学系列 | 找到你并不容易:从集群内访问服务
Posted COCOgsta
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Kubernetes网络自学系列 | 找到你并不容易:从集群内访问服务相关的知识,希望对你有一定的参考价值。
素材来源:《Kubernetes网络权威指南》
一边学习一边整理内容,并与大家分享,侵权即删,谢谢支持!
附上汇总贴:Kubernetes网络自学系列 | 汇总_COCOgsta的博客-CSDN博客
3.5 找到你并不容易:从集群内访问服务
Kubernetes里服务(Service)的概念即我们通常说的微服务(micro-service)。至于引入Service这个概念的初衷,可以从一个简单的例子说起。客户端访问容器应用,最简单的方式莫过于容器IP+端口,如图3-14所示。
图3-14 客户端直接访问容器
但是,当有多个后端实例时:
· 如何做到负载均衡?
· 如何保持会话亲和性?
· 容器迁移,IP发生变化如何访问?
· 健康检查怎么做?
· 怎么通过域名访问?
Kubernetes提供的解决方案是在客户端和后端Pod之间引入一个抽象层:Service。那么,什么是Kubernetes的Service呢?
在Kubernetes中,用户可以为任何Kubernetes资源分配成为Labels(标签)的任意键值。Kubernetes使用Labels将多个相关的Pod组合成一个逻辑单元,称为Service。Service具有稳定的IP地址(区别于容器不固定的IP地址)和端口,并会在一组匹配的后端Pod之间提供负载均衡,匹配的条件就是Service的Label Selector与Pod的Labels相匹配。
图3-15 所示为两个独立服务,每个服务都由多个Pod组成。图中的每个Pod都带有app=demo标签,但这些Pod的其他标签又有所不同。服务“frontend”匹配所有带有app=demo和component=frontend的Pod,而服务“users”匹配所有带有app=demo和component=users的Pod。客户端Pod与任意服务选择器都不匹配,因此它不是任意一个服务的一部分。客户端Pod在同一集群内运行,因此它可与任意服务进行通信。
图3-15 两个独立服务
3.5.1 Kubernetes Service详解
简单说,Kubernetes的Service代表的是Kubernetes后端服务的入口,它主要包含服务的访问IP(虚IP)和端口,因此工作在L4。既然Service只存储服务入口信息,那如何关联后端Pod呢?前文已经提到Service通过Label Selector选择与之匹配的Pod。那么被Service选中的Pod,当它们运行且能对外提供服务后,Kubernetes的Endpoints Controller会生成一个新的Endpoints对象,记录Pod的IP和端口,这就解决了前文提到的后端实例健康检查问题。另外,Service的访问IP和Endpoints/Pod IP都会在Kubernetes的DNS服务器里存储域名和IP的映射关系,因此用户可以在集群内通过域名的方式访问Service和Pod。Service与Endpoints的关系如图3-16所示。
图3-16 Service与Endpoints的关系
Kubernetes会从集群的可用服务IP池中为每个新创建的服务分配一个稳定的集群内访问IP地址,称为Cluster IP。Kubernetes还会通过添加DNS条目为Cluster IP分配主机名。Cluster IP和主机名在集群内是独一无二的,并且在服务的整个生命周期内不会更改。只有将服务从集群中删除,Kubernetes才会释放Cluster IP和主机名。用户可以使用服务的Cluster IP或主机名访问正常运行的Pod。
用户不用担心服务出现单点故障问题,Kubernetes会尽可能均匀地将流量分布到在多个节点上运行的Pod,因此一个或若干个(但不是所有)节点的服务中断情况不会影响服务的整体可用性。
Kubernetes使用Kube-proxy组件管理各服务与之后端Pod的连接,该组件在每个节点上运行。Kube-proxy是一个基于出站流量的负载平衡控制器,它监控Kubernetes API Service并持续将服务IP(包括Cluster IP等)映射到运行状况良好的Pod,落实到主机上就是iptables/IPVS等路由规则。访问服务的IP会被这些路由规则直接DNAT到Pod IP,然后走底层容器网络送到对应的Pod。
一个最简单的Kubernetes Service的定义如下所示:
其中,spec.ClusterIP就是Service的(其中一个)访问IP,俗称虚IP(Virtual IP,即VIP)。如果用户不指定的话,那么Kubernetes Master会自动从一个配置范围内随机分配一个。
注意:服务分配的Cluster IP是一个虚拟IP,刚接触Kubernetes Service的人经常犯的错误是试图ping这个IP,然后发现它没有任何响应。实际上,这个虚拟IP只有和它的port一起使用才有作用,直接访问该IP或者想访问该IP的其他端口都是徒劳。
我们注意到,该Service的selector是app:nginx,即匹配那些被打上app=nginx标签的Pod。
spec.ports[].port是Service的访问端口,而与之对应的spec.ports[].targetPort是后端Pod的端口,Kubernetes会自动做一次映射(80->8080),具体实现机制后面会详细解释。Kubernetes Service能够支持TCP、UDP和SCTP三种协议,默认是TCP协议。
上文提到,当Service的后端Pod准备就绪后,Kubernetes会生成一个新的Endpoints对象,而且这个Endpoints对象和Service同名。一个Endpoints的定义如下所示:
用户可以通过以下命令得到这个Endpoints对象:
其中,subsets[].addresses[].ip是后端Pod的IP,subsets[].ports是后端Pod的端口,与Service的targetPort对应。
如果觉得通过yaml方式创建Service太麻烦,则kubectl还提供了expose子命令直接将deployment暴露成服务。
先创建一个两副本的deployment,如下所示:
然后为上面的deployment创建出来的两个Nginx副本暴露一个Service:
这等价于使用kubectl create-f命令创建Service,对应如下的yaml文件:
查看Service的详情可以看到它的Endpoints列表,如下所示:
测试发现,访问whoami服务会随机转发给后端Pod,如下所示:
访问服务Cluster IP:Port返回了不同后端Pod的响应。
在默认情况下,服务会随机转发到可用的后端。如果希望保持会话(同一个client永远都转发到相同的Pod),可以把service.spec.sessionAffinity设置为ClientIP,即基于客户端源IP的会话保持,而且默认会话保持时间是10800秒。这会起到什么样的效果呢?即在3小时内,同一个客户端访问同一服务的请求都会被转发给第一个Pod。例如,给上面的Service配置了sessionAffinity=ClientIP后,再发起请求的效果如下所示:
3.5.2 Service的三个port
先来看一个最简单的Service定义:
Service的几个port的概念很容易混淆,它们分别是port、targetPort和NodePort。
port表示Service暴露的服务端口,也是客户端访问用的端口,例如Cluster IP:port是提供给集群内部客户访问Service的入口。需要注意的是,port不仅是Cluster IP上暴露的端口,还可以是external IP和Load Balancer IP。Service的port并-不监听在节点IP上,即无法通过节点IP:port的方式访问Service。
NodePort是Kubernetes提供给集群外部访问Service入口的一种方式(另一种方式是Load Balancer),所以可以通过Node IP:nodePort的方式提供集群外访问Service的入口。需要注意的是,我们这里说的集群外指的是Pod网段外,例如Kubernetes节点或因特网。
targetPort很好理解,它是应用程序实际监听Pod内流量的端口,从port和NodePort上到来的数据,最终经过Kube-proxy流入后端Pod的targetPort进入容器。
在配置服务时,可以选择定义port和targetPort的值重新映射其监听端口,这也被称为Service的端口重映射。Kube-proxy通过在节点上iptables规则管理此端口的重新映射过程。
3.5.3 你的服务适合哪种发布形式
1.Cluster IP
Kubernetes Service有几种类型:Cluster IP、Load Balancer和NodePort。其中,Cluster IP是默认类型,自动分配集群内部可以访问的虚IP——Cluster IP。我们随便创建一个Service,只要不做特别指定,都是Cluster IP类型。Cluster IP的主要作用是方便集群内Pod到Pod之间的调用。一个典型的Cluster IP类型的Service如下:
Cluster IP主要在每个node节点使用iptables,将发向Cluster IP对应端口的数据转发到后端Pod中。针对iptables的更详细分析见后面章节。
2. Load Balancer
我们已经了解了Kubernetes如何使用Service为Pod内运行的应用提供稳定的IP地址。在默认情况下,Pod不会公开一个外部IP地址,而是由每个节点上的Kube-proxy管理所有流量。集群内的Pod之间可以自由通信,但集群外的连接无法访问服务。例如,集群外部的客户端无法通过Cluster IP访问Service。
Load Balancer(简称LB)类型的Service需要Cloud Provider的支持。Kubernetes原生支持的Cloud Provider有GCE和AWS,因此和不同云平台的网络方案耦合较大,而且只能在特定的云平台上使用,局限性也较大。除了“外用”,Load Balancer还可以“内服”,即如果要在集群内访问Load Balancer类型的Service,则Kube-proxy用iptables或ipvs实现云服务提供商Load Balancer(一般都是L7的)的部分功能:L4转发、安全组规则等。
说到安全组规则,在默认情况下,来自任何外部IP地址的流量都可以访问Load Balancer 类型的服务。创建Service时,通过配置serviceSpec.loadBalancerSourceRanges字段,可以限制哪些IP地址范围可以访问集群内的服务。loadBalancerSourceRanges可以指定多个范围,并且支持随时更新。Kube-proxy会配置该节点的iptables规则。以拒绝与指定loadBalancerSourceRanges不匹配的所有流量。这样就不需要额外配置VPC的防火墙规则了。
一个Load Balancer类型Service的定义如下所示:
查看这个服务的细节:
内部可以使用Cluster-IP加端口来访问该服务。在我们这个例子中就是19.97.121.42:8086。
外部可以使用EXTERNAL-IP加端口来访问该服务,这是一个云供应商提供的负载均衡器IP,在我们这个例子中就是10.13.242.236:8086。
Load Balancer类型Service的原理如图3-17所示。
图3-17 Load Balancer类型Service的原理
3.NodePort
NodePort类似Service,被称为乞丐版的Load Balancer类型Service,这也暗示了Node-Port Service可以用于集群外部访问Service,而且成本低廉(无须一个外部Load Balancer)。
NodePort为Service在Kubernetes集群的每个节点上分配一个真实的端口,即NodePort。集群内/外部可基于集群内任何一个节点的IP:NodePort的形式访问Service。NodePort支持TCP、UDP、SCTP,默认端口范围是30000-32767,Kubernetes在创建NodePort类型Service对象时会随机选取一个。用户也可以在Service的spec.ports.nodePort中自己指定一个NodePort端口,就像指定Cluster IP那样。如果觉得默认端口范围不够用或者太大,可以修改API Server的--service-node-port-range的参数。修改默认NodePort的范围。例如--service-node-port-range=8000-9000。
一个典型的NodePort类型Service如下:
使用kubectl describe service可以看到,虽然Service的类型是NodePort,但是Kubernetes依然为其分配了一个Cluster IP,输出如下:
以上输出的IP字段即该Service的Cluster IP。
NodePort的实现机制是Kube-proxy会创建一个iptables规则,所有访问本地NodePort的网络包都会被直接转发至后端Port。NodePort会在主机上打开(但不监听)一个实际的端口,当NodePort类型服务创建并且被Kube-proxy感知后,可以通过以下命令验证某个端口(例如9000)是否打开:
某个NodePort被打开后,主机上其他进程将无法再使用该端口,除非Service被删除该端口才会被释放。
在一般情况下,不建议用户自己指定NodePort,而是应该让Kubernetes选择,否则维护的成本会很高。
NodePort是解决服务对外暴露的最简单方法,对于那些没有打通容器网络和主机网络的用户,NodePort成了他们从外部访问Service的首选。但NodePort的问题集中体现在性能和可对宿主机端口占用方面。一旦服务多起来,NodePort在每个节点上开启的端口会变得非常庞大且难以维护。
3.5.4 Kubernetes Service发现
Kubernetes Service创建好后,如何使用它,即如何进行服务发现呢?
Kubernetes提出了Service的概念,使得用户可以通过服务IP地址(不管是虚IP,还是Node IP,还是外部Load Balancer的IP)访问后端Pod提供的服务。但是在使用时有一个鸿沟(Gap),即客户端如何知道服务的访问IP?一个典型的例子是,假设有个应用要访问后端数据库(DB),后端DB已经通过Service对外暴露服务。我们只知道DB应用的名称和开放的端口,并不知道该服务的访问地址。
一个办法是使用Kubernetes提供的API或者kubectl查询Service的信息。但这是一个糟糕的做法,导致每个应用都要在启动时编写查询依赖服务的逻辑,让应用程序做运维操作。这无疑是重复劳动而且增加了应用的复杂度。这也导致应用耦合Kubernetes,使得应用程序无法单独部署和运行。
最早的时候,Kubernetes采用了Docker曾经使用过的方法——环境变量。Kubelet创建每个Pod时,会把系统当前所有服务的IP和端口信息都通过环境变量的方式注入容器。这样Pod中的应用可以通过读取环境变量获取所需服务的地址信息。
Kubelet为每个Pod注入所有Service的环境变量信息,形如:
不难发现,环境变量注入这种方式,对服务和环境变量的匹配关系有一定的规范,使用起来也相对简单。
但这种方式的缺点也很明显:
· 容易环境变量洪泛,Docker启动参数过长会影响性能,甚至直接导致容器启动失败;
· Pod想要访问的任何Service必须在Pod自己被创建之前创建,否则这些环境变量就不会被注入。
更理想的方案是,应用能够直接使用服务的名字,不需要关心它实际的IP地址,中间的转换能够自动完成。名字和IP之间的转换即DNS,DNS的方式并没有以上两个限制。
在Kubernetes中使用域名服务,即假设Service(my-svc)在namespace(my-ns)中,暴露名为http的TCP端口,那么在Kubernetes的DNS服务器中会生成两种记录,分别是A记录:域名(my-svc.my-ns)到Cluster IP的映射和SRV记录,例如_http._tcp.my-svc.my-ns到一个http端口号的映射。我们会在Kube-dns一节做更详细的介绍。
3.5.5 特殊的无头Service
所谓的无头(headless)Service即没有selector的Service。Service抽象了该如何访问Kubernetes Pod,也能够抽象其他类型的backend,例如:
· 希望在生产环境中使用外部的数据库集群,但在测试环境使用自己的数据库;
· 希望服务指向另一个namespace中或其他集群中的服务;
· 正在将工作负载转移到Kubernetes集群,以及运行在Kubernetes集群之外的backend。在任何场景中,都能够定义没有selector的Service:
这个Service没有selector,就不会创建相关的Endpoints对象。可以手动将Service映射到指定的Endpoints:
注意:Endpoint IP地址不能是loopback(127.0.0.0/8)、link-local(169.254.0.0/16)或linklocal多播(224.0.0.0/24)。
访问没有selector的Service,与有selector的Service的原理相同。请求将被路由到用户定义的Endpoint(该示例中为1.2.3.4:9376)。
ExternalName Service是Service的特例,它没有selector,也没有定义任何的端口和Endpoint。相反,对于运行在集群外部的服务,它通过返回该外部服务的别名这种方式提供服务。
当查询主机my-service.prod.svc.CLUSTER时,集群的DNS服务将返回一个值为my.database.example.com的CNAME记录。访问这个服务的工作方式与其他的相同,唯一不同的是重定向发生在DNS层,而且不会进行代理或转发。如果后续决定要将数据库迁移到Kubernetes集群中,则可以启动对应的Pod,增加合适的Selector或Endpoint,修改Service的type。
3.5.6 怎么访问本地服务
当访问NodePort或Load Balancer类型Service的流量到底节点时,流量可能会被转发到其他节点上的Pod。这可能需要额外一跳的网络。如果要避免额外的跃点,则用户可以指定流量必须转到最初接收流量的节点上的Pod。
要指定流量必须转到同一节点上的Pod,可以将serviceSpec.externalTrafficPolicy设置为Local(默认是Cluster):
将externalTrafficPolicy设置为Local时,负载平衡器仅将流量发送到具有属于服务的正常Pod所在的节点。每个节点上的Kube-proxy都健康运行,检查服务器对外提供该节点上的Endpoint信息,以便系统确定哪些节点具有适当的Pod。
那么问题来了,为什么externalTrafficPolicy只支持NodePort和Load Balancer的Service,不支持Cluster IP呢?原因在于externalTrafficPolicy的设定是当流量到达确定的节点后,再由Kube-proxy在该节点上找Service的Endpoint。有些节点上存在Service Endpoint,有些则没有,再配合Kube-proxy的健康检查就能确定哪些节点上有符合要求的后端Pod。访问NodePort和Load Balancer都能指定节点,但Cluster IP无法指定节点,因此Service流量就永远出不了发 起访问的客户端的那个节点,这也不是externalTrafficPolicy这个特性的设计初衷。
以上是关于Kubernetes网络自学系列 | 找到你并不容易:从集群内访问服务的主要内容,如果未能解决你的问题,请参考以下文章
《Kubernetes网络权威指南》读书笔记 | 找到你并不容易:从集群内访问服务
《Kubernetes网络权威指南》读书笔记 | 找到你并不容易:从集群外访问服务
Kubernetes网络自学系列 | 打通CNI与Kubernetes:Kubernetes网络驱动
Kubernetes网络自学系列 | 终于等到你:Kubernetes网络