Service
Service 是应用服务的抽象,通过 labels 为应用提供负载均衡和服务发现。匹配 labels 的Pod IP 和端口列表组成 endpoints,由 kube-proxy 负责将服务 IP 负载均衡到这些endpoints 上。
每个 Service 都会自动分配一个 cluster IP(仅在集群内部可访问的虚拟地址)和 DNS 名,其他容器可以通过该地址或 DNS 来访问服务,而不需要了解后端容器的运行。
Pod、RC与Service的逻辑关系。
可以通过type
在ServiceSpec
中指定一个需要的类型的 Service,Service的四种type:
- ClusterIP(默认): 在集群中内部IP上暴露服务。此类型使Service只能从群集中访问。
- NodePort :通过每个 Node 上的 IP 和静态端口(NodePort)暴露服务。NodePort 服务会路由到 ClusterIP 服务,这个 ClusterIP 服务会自动创建。通过请求 nodeip: nodeport,可以从集群的外部访问一个 NodePort 服务。
- LoadBalancer :使用云提供商的负载均衡器(如果支持),可以向外部暴露服务。外部的负载均衡器可以路由到 NodePort 服务和 ClusterIP 服务。
- ExternalName :通过返回 CNAME 和它的值,可以将服务映射到 externalName 字段的内容,没有任何类型代理被创建。这种类型需要v1.7版本或更高版本kube-dnsc才支持。
首先,Node IP是Kubernetes集群中每个节点的物理网卡的IP地址,是一个真实存在的物理网络,所有属于这个网络的服务器都能通过这个网络直接通信,不管其中是否有部分节点不属于这个Kubernetes集群。这也表明在Kubernetes集群之外的节点访问Kubernetes集群之内的某个节点或者TCP/IP服务时,都必须通过Node IP通信。
其次,Pod IP是每个Pod的IP地址,它是Docker Engine根据docker0网桥的IP地址段进行分配的,通常是一个虚拟的二层网络,前面说过,Kubernetes要求位于不同Node上的Pod都能够彼此直接通信,所以Kubernetes里一个Pod里的容器访问另外一个Pod里的容器时,就是通过Pod IP所在的虚拟二层网络进行通信的,而真实的TCP/IP流量是通过Node IP所在的物理网卡流出的。最后说说Service的Cluster IP,它也是一种虚拟的IP,但更像一个“伪造”的IP网络,原因有以下几点。
- Cluster IP仅仅作用于Kubernetes Service这个对象,并由Kubernetes管理和分配IP地址(来源于Cluster IP地址池)。
- Cluster IP无法被Ping,因为没有一个“实体网络对象”来响应。
- Cluster IP只能结合Service Port组成一个具体的通信端口,单独的Cluster IP不具备TCP/IP通信的基础,并且它们属于Kubernetes集群这样一个封闭的空间,集群外的节点如果要访问这个通信端口,则需要做一些额外的工作。
在Kubernetes集群内,Node IP网、Pod IP网与Cluster IP网之间的通信,采用的是Kubernetes自己设计的一种编程方式的特殊路由规则,与我们熟知的IP路由有很大的不同。
Service的Cluster IP属于Kubernetes集群内部的地址,无法在集群外部直接使用这个地址。那么矛盾来了:实际上在我们开发的业务系统中肯定多少有一部分服务是要提供给Kubernetes集群外部的应用或者用户来使用的,典型的例子就是Web端的服务模块,那么用户怎么访问它?采用NodePort是解决上述问题的最直接、有效的常见做法。
apiVersion: v1
kind: Service
metadata:
name: tomcat-service
spec:
type: NodePort #
ports:
- port: 8080
nodePort: 31002 #
selector:
tier: frontend
nodePort:31002
指定tomcat-service的NodePort的为31002,否则kubernetes会自动分配一个可用的端口。但NodePort还没有完全解决外部访问Service的所有问题,比如负载均衡问题。假如集群中有10个Node,则此时最好有一个负载均衡器,外部的请求只需访问此负载均衡器的IP地址,由负载均衡器负责转发流量到后面某个Node的NodePort上
图中的Load balancer组件独立于Kubernetes集群之外,通常是一个硬件的负载均衡器,或者是以软件方式实现的,例如HAProxy或者Nginx。对于每个Service,我们通常需要配置一个对应的Load balancer实例来转发流量到后端的Node上,这的确增加了工作量及出错的概率。
于是Kubernetes提供了自动化的解决方案,如果我们的集群运行在谷歌的公有云GCE上,那么只要把Service的type=NodePort
改为type=LoadBalancer
,Kubernetes就会自动创建一个对应的Load balancer实例并返回它的IP地址供外部客户端使用。其他公有云提供商只要实现了支持此特性的驱动,则也可以达到上述目的。
Headless Service
在某些应用场景中,开发人员希望自己控制负载均衡策略,不使用Service提供的默认负载均衡功能,或者服务希望知道属于同组的其他实例。此时可以使用Headless Service来实现,即不为Service设置ClusterIP,仅通过Label Selector将后端的Pod列表返回给调用的客户端。
spec:
ports:
- port: 80
clusterIP: None
selector:
app: nginx
这样,Service就不再具有一个特定的ClusterIP地址,对其进行访问将获得包含Label“app=nginx”的全部Pod列表,然后客户端程序自行决定如何处理这个Pod列表。例如,StatefulSet就是使用Headless Service为客
户端返回多个服务地址的。对于“去中心化”类的应用集群,Headless Service将非常有用。
service selector
service通过selector和pod建立关联。
k8s会根据service关联到pod的podIP信息组合成一个endpoint。
若service定义中没有selector字段,service被创建时,endpoint controller不会自动创建endpoint。
service 负载分发策略有两种
-
RoundRobin:轮询模式,即轮询将请求转发到后端的各个pod上(默认模式);
-
SessionAffinity:基于客户端IP地址进行会话保持的模式,第一次客户端访问后端某个pod,之后的请求都转发到这个pod上。
服务发现
虽然Service解决了Pod的服务发现问题,但不提前知道Service的IP,怎么发现service服务呢?
k8s提供了两种方式进行服务发现:
环境变量: 当创建一个Pod的时候,kubelet会在该Pod中注入集群内所有Service的相关环境变量。需要注意的是,要想一个Pod中注入某个Service的环境变量,则必须Service要先比该Pod创建。这一点,几乎使得这种方式进行服务发现不可用。
一个ServiceName为redis-master的Service,对应的ClusterIP:Port
为10.0.0.11:6379
,则其在pod中对应的环境变量为:
REDIS_MASTER_SERVICE_HOST=10.0.0.11 REDIS_MASTER_SERVICE_PORT=6379 REDIS_MASTER_PORT=tcp://10.0.0.11:6379 REDIS_MASTER_PORT_6379_TCP=tcp://10.0.0.11:6379 REDIS_MASTER_PORT_6379_TCP_PROTO=tcp
REDIS_MASTER_PORT_6379_TCP_PORT=6379 REDIS_MASTER_PORT_6379_TCP_ADDR=10.0.0.11
DNS:可以通过cluster add-on的方式轻松的创建KubeDNS来对集群内的Service进行服务发现————这也是k8s官方强烈推荐的方式。为了让Pod中的容器可以使用kube-dns来解析域名,k8s会修改容器的/etc/resolv.conf
配置。
Ingress
service的作用体现在两个方面,对集群内部,它不断跟踪pod的变化,更新endpoint中对应pod的对象,提供了ip不断变化的pod的服务发现机制,对集群外部,他类似负载均衡器,可以在集群内外部对pod进行访问。但是,单独用service暴露服务的方式,在实际生产环境中不太合适:
- ClusterIP 只能在集群内部访问
- NodePort 测试环境使用还行,当有几十上百的服务在集群中运行时,NodePort的端口管理简直是灾难
- LoadBalance 这种方式受限于云平台,且通常在云平台部署ELB还需要额外的费用。
所幸k8s还提供了一种集群维度暴露服务的方式,也就是ingress。ingress可以简单理解为service的service,他通过独立的ingress对象来制定请求转发的规则,把请求路由到一个或多个service中。这样就把服务与请求规则解耦了,可以从业务维度统一考虑业务的暴露,而不用为每个service单独考虑。
举个例子,现在集群有api、文件存储、前端3个service,可以通过一个ingress对象来实现图中的请求转发:
ingress与ingress-controller
要理解ingress,需要区分两个概念,ingress和ingress-controller:
- ingress对象:指的是k8s中的一个api对象,一般用yaml配置。作用是定义请求如何转发到service的规则,可以理解为配置模板。
- ingress-controller:具体实现反向代理及负载均衡的程序,对ingress定义的规则进行解析,根据配置的规则来实现请求转发。
简单来说,ingress-controller才是负责具体转发的组件,通过各种方式将它暴露在集群入口,外部对集群的请求流量会先到ingress-controller,而ingress对象是用来告诉ingress-controller该如何转发请求,比如哪些域名哪些path要转发到哪些服务等等。
ingress-controller
实际上ingress-controller只是一个统称,用户可以选择不同的ingress-controller实现,目前,由k8s维护的ingress-controller只有google云的GCE与ingress-nginx两个,其他还有很多第三方维护的ingress-controller,具体可以参考官方文档。但是不管哪一种ingress-controller,实现的机制都大同小异。
一般来说,ingress-controller的形式都是一个pod,里面跑着daemon程序和反向代理程序。daemon负责不断监控集群的变化,根据ingress对象生成配置并应用新配置到反向代理,比如nginx-ingress就是动态生成nginx配置,动态更新upstream,并在需要的时候reload程序应用新配置。
ingress
ingress是一个API对象,和其他对象一样,通过yaml文件来配置。ingress通过http或https暴露集群内部service,给service提供外部URL、负载均衡、SSL/TLS能力以及基于host的方向代理。ingress要依靠ingress-controller来具体实现以上功能。上面的图如果用ingress来表示,大概就是如下配置:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: abc-ingress
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/use-regex: "true"
spec:
tls:
- hosts:
- api.abc.com
secretName: abc-tls
rules:
- host: api.abc.com
http:
paths:
- backend:
serviceName: apiserver
servicePort: 80
- host: www.abc.com
http:
paths:
- path: /image/*
backend:
serviceName: fileserver
servicePort: 80
- host: www.abc.com
http:
paths:
- backend:
serviceName: feserver
servicePort: 8080
与其他k8s对象一样,ingress配置也包含了apiVersion
、kind
、metadata
、spec
等关键字段。有几个关注的在spec字段中,tls用于定义https密钥、证书。rule用于指定请求路由规则。
ingress的部署方式
ingress的部署,需要考虑两个方面:
- ingress-controller是作为pod来运行的,以什么方式部署比较好?
- ingress解决了把如何请求路由到集群内部,那它自己怎么暴露给外部比较好?
下面列举一些目前常见的部署和暴露方式,具体使用哪种方式还是得根据实际需求来考虑决定。
Deployment+LoadBalancer模式
如果要把ingress部署在公有云,那用这种方式比较合适。用Deployment部署ingress-controller,创建一个type为LoadBalancer的service关联这组pod。大部分公有云,都会为LoadBalancer的service自动创建一个负载均衡器,通常还绑定了公网地址。只要把域名解析指向该地址,就实现了集群服务的对外暴露。
Deployment+NodePort模式
同样用deployment模式部署ingress-controller,并创建对应的服务,但是type为NodePort。这样,ingress就会暴露在集群节点ip的特定端口上。由于nodeport暴露的端口是随机端口,一般会在前面再搭建一套负载均衡器来转发请求。该方式一般用于宿主机是相对固定的环境ip地址不变的场景。NodePort方式暴露ingress虽然简单方便,但是NodePort多了一层NAT,在请求量级很大时可能对性能会有一定影响。
DaemonSet+HostNetwork+nodeSelector
用DaemonSet结合Nodeselector来部署ingress-controller到特定的node上,然后使用HostNetwork直接把该pod与宿主机node的网络打通,直接使用宿主机的80/433端口就能访问服务。
这时,ingress-controller所在的node机器就很类似传统架构的边缘节点,比如机房入口的nginx服务器。该方式整个请求链路最简单,性能相对NodePort模式更好。缺点是由于直接利用宿主机节点的网络和端口,一个node只能部署一个ingress-controller pod。比较适合大并发的生产环境使用。
关于ingress-nginx
具体可以参考ingress-nginx的官方文档。同时,在生产环境使用ingress-nginx还有很多要考虑的地方,这篇文章写得很好,总结了不少最佳实践,值得参考。
最后
- ingress是k8s集群的请求入口,可以理解为对多个service的再次抽象
- 通常说的ingress一般包括ingress资源对象及ingress-controller两部分组成
- ingress-controller有多种实现,社区原生的是ingress-nginx,根据具体需求选择
- ingress自身的暴露有多种方式,需要根据基础环境及业务类型选择合适的方式
服务发现原理
endpoint
endpoint是k8s集群中的一个资源对象,存储在etcd中,用来记录一个service对应的所有pod的访问地址。
service配置selector,endpoint controller才会自动创建对应的endpoint对象;否则,不会生成endpoint对象.
Endpoint地址会随着Pod的销毁和重新创建而发生改变,因为新Pod的IP地址与之前旧Pod的不同。而Service一旦被创建,Kubernetes就会自动为它分配一个可用的Cluster IP,而且在Service的整个生命周期内,它的Cluster IP不会发生改变。于是,服务发现这个棘手的问题在Kubernetes的架构里也得以轻松解决:只要用Service的Name与Service的Cluster IP地址做一个DNS域名映射即可完美解决问题。
例如,k8s集群中创建一个名为k8s-classic-1113-d3的service,就会生成一个同名的endpoint对象,如下图所示。其中ENDPOINTS就是service关联的pod的ip地址和端口。
endpoint controller
endpoint controller是k8s集群控制器的其中一个组件,其功能如下:
- 负责生成和维护所有endpoint对象的控制器
- 负责监听service和对应pod的变化
- 监听到service被删除,则删除和该service同名的endpoint对象
- 监听到新的service被创建,则根据新建service信息获取相关pod列表,然后创建对应endpoint对象
- 监听到service被更新,则根据更新后的service信息获取相关pod列表,然后更新对应endpoint对象
- 监听到pod事件,则更新对应的service的endpoint对象,将podIp记录到endpoint中
CoreDNS
CoreDNS总体架构图
kube-proxy
kube-proxy负责service的实现,即实现了k8s内部从pod到service和外部从node port到service的访问。
kube-proxy采用iptables的方式配置负载均衡,基于iptables的kube-proxy的主要职责包括两大块:一块是侦听service更新事件,并更新service相关的iptables规则,一块是侦听endpoint更新事件,更新endpoint相关的iptables规则(如 KUBE-SVC-链中的规则),然后将包请求转入endpoint对应的Pod。如果某个service尚没有Pod创建,那么针对此service的请求将会被drop掉。
kube-proxy的架构如下:
kube-proxy iptables
kube-proxy监听service和endpoint的变化,将需要新增的规则添加到iptables中。
kube-proxy只是作为controller,而不是server,真正服务的是内核的netfilter,体现在用户态则是iptables。
kube-proxy的iptables方式也支持RoundRobin(默认模式)和SessionAffinity负载分发策略。
kubernetes只操作了filter和nat表。
Filter:在该表中,一个基本原则是只过滤数据包而不修改他们。filter table的优势是小而快,可以hook到input,output和forward。这意味着针对任何给定的数据包,只有可能有一个地方可以过滤它。
NAT:此表的主要作用是在PREROUTING和POSTROUNTING的钩子中,修改目标地址和原地址。与filter表稍有不同的是,该表中只有新连接的第一个包会被修改,修改的结果会自动apply到同一连接的后续包中。
kube-proxy对iptables的链进行了扩充,自定义了KUBE-SERVICES,KUBE-NODEPORTS,KUBE-POSTROUTING,KUBE-MARK-MASQ和KUBE-MARK-DROP五个链,并主要通过为KUBE-SERVICES chain增加rule来配制traffic routing 规则。
同时,kube-proxy也为默认的prerouting、output和postrouting chain增加规则,使得数据包可以跳转至k8s自定义的chain,规则如下:
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
如果service类型为nodePort,(从LB转发至node的数据包均属此类)那么将KUBE-NODEPORTS链中每个目的地址是NODE节点端口的数据包导入这个“KUBE-SVC-”链:
-A KUBE-SERVICES -m comment --comment "kubernetes service nodeports; NOTE: this must be the last rule in this chain" -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/es1:http" -m tcp --dport 32135 -j KUBE-MARK-MASQ
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/es1:http" -m tcp --dport 32135 -j KUBE-SVC-LAS23QA33HXV7KBL
Iptables chain支持嵌套并因为依据不同的匹配条件可支持多种分支,比较难用标准的流程图来体现调用关系,建单抽象为下图:
举个例子,在k8s集群中创建了一个名为my-service的服务,其中:
service vip:10.11.97.177
对应的后端两副本pod ip:10.244.1.10、10.244.2.10
容器端口为:80
服务端口为:80
则kube-proxy为该service生成的iptables规则主要有以下几条:
-A KUBE-SERVICES -d 10.11.97.177/32 -p tcp -m comment --comment "default/my-service: cluster IP" -m tcp --dport 80 -j KUBE-SVC-BEPXDJBUHFCSYIC3
-A KUBE-SVC-BEPXDJBUHFCSYIC3 -m comment --comment “default/my-service:” -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-U4UWLP4OR3LOJBXU #50%的概率轮询后端pod
-A KUBE-SVC-BEPXDJBUHFCSYIC3 -m comment --comment "default/my-service:" -j KUBE-SEP-QHRWSLKOO5YUPI7O
-A KUBE-SEP-U4UWLP4OR3LOJBXU -s 10.244.1.10/32 -m comment --comment "default/my-service:" -j KUBE-MARK-MASQ
-A KUBE-SEP-U4UWLP4OR3LOJBXU -p tcp -m comment --comment "default/my-service:" -m tcp -j DNAT --to-destination 10.244.1.10:80
-A KUBE-SEP-QHRWSLKOO5YUPI7O -s 10.244.2.10/32 -m comment --comment "default/my-service:" -j KUBE-MARK-MASQ
-A KUBE-SEP-QHRWSLKOO5YUPI7O -p tcp -m comment --comment "default/my-service:" -m tcp -j DNAT --to-destination 10.244.2.10:80
kube-proxy通过循环的方式创建后端endpoint的转发,概率是通过probability后的1.0/float64(n-i)计算出来的,譬如有两个的场景,那么将会是一个0.5和1也就是第一个是50%概率第二个是100%概率,如果是三个的话类似,33%、50%、100%。
kube-proxy iptables的性能缺陷
k8s集群创建大规模服务时,会产生很多iptables规则,非增量式更新会引入一定的时延。iptables规则成倍增长,也会导致路由延迟带来访问延迟。大规模场景下,k8s 控制器和负载均衡都面临这挑战。例如,若集群中有N个节点,每个节点每秒有M个pod被创建,则控制器每秒需要创建N×M个endpoints,需要增加的iptables则是N*M的数倍。以下是k8s不同规模下访问service的时延:
从上图中可以看出,当集群中服务数量增长时,因为 IPTables天生不是被设计用来作为 LB 来使用的,IPTables 规则则会成倍增长,这样带来的路由延迟会导致的服务访问延迟增加,直到无法忍受。
目前有以下几种解决方案,但各有不足:
● 将endpoint对象拆分成多个对像
优点:减小了单个endpoint大小
缺点:增加了对象的数量和请求量
● 使用集中式负载均衡器
优点:减少了跟apiserver的连接和请求数
缺点:服务路由中又增加了一跳,并且需要集中式LB有很高的性能和高可用性
● 定期任务,批量创建/更新endpoint
优点:减少了每秒的处理数
缺点:在定期任务执行的间隔时间内,端对端延迟明显增加
K8s 1.8 新特性—ipvs
ipvs与iptables的性能差异
随着服务的数量增长,IPTables 规则则会成倍增长,这样带来的问题是路由延迟带来的服务访问延迟,同时添加或删除一条规则也有较大延迟。不同规模下,kube-proxy添加一条规则所需时间如下所示:
可以看出当集群中服务数量达到5千个时,路由延迟成倍增加。添加 IPTables 规则的延迟,有多种产生的原因:
添加规则不是增量的,而是先把当前所有规则都拷贝出来,再做修改然后再把修改后的规则保存回去,这样一个过程的结果就是 IPTables 在更新一条规则时会把 IPTables 锁住,这样的后果在服务数量达到一定量级的时候,性能基本不可接受:在有5千个服务(4万条规则)时,添加一条规则耗时11分钟;在右2万个服务(16万条规则)时,添加一条规则需要5个小时。
这样的延迟时间,对生产环境是不可以的,那该性能问题有哪些解决方案呢?从根本上解决的话,可以使用 “IP Virtual Server”(IPVS )来替换当前 kube-proxy 中的 IPTables 实现,这样能带来显著的性能提升以及更智能的负载均衡功能如支持权重、支持重试等等。
ipvs介绍
k8s 1.8 版本中,社区 SIG Network 增强了 NetworkPolicy API,以支持 Pod 出口流量策略,以及允许策略规则匹配源或目标 CIDR 的匹配条件。这两个增强特性都被设计为 beta 版本。 SIG Network 还专注于改进 kube-proxy,除了当前的 iptables 和 userspace 模式,kube-proxy 还引入了一个 alpha 版本的 IPVS 模式。
作为 Linux Virtual Server(LVS) 项目的一部分,IPVS 是建立于 Netfilter之上的高效四层负载均衡器,支持 TCP 和 UDP 协议,支持3种负载均衡模式:NAT、直接路由(通过 MAC 重写实现二层路由)和IP 隧道。ipvs(IP Virtual Server)安装在LVS(Linux Virtual Server)集群作为负载均衡主节点上,通过虚拟出一个IP地址和端口对外提供服务。客户端通过访问虚拟IP+端口访问该虚拟服务,之后访问请求由负载均衡器调度到后端真实服务器上。
ipvs相当于工作在netfilter中的input链。
配置方法:IPVS 负载均衡模式在 kube-proxy 处于测试阶段还未正式发布,完全兼容当前 Kubernetes 的行为,通过修改 kube-proxy 启动参数,在 mode=userspace 和 mode=iptables 的基础上,增加 mode=IPVS 即可启用该功能。
ipvs转发模式
DR模式(Direct Routing)
特点:
-
数据包在LB转发过程中,源/目的IP和端口都不会变化。LB只修改数据包的MAC地址为RS的MAC地址
-
RS须在环回网卡上绑定LB的虚拟机服务IP
-
RS处理完请求后,响应包直接回给客户端,不再经过LB
缺点:LB和RS必须位于同一子网
NAT模式(Network Address Translation)
特点:
-
LB会修改数据包地址:对于请求包,进行DNAT;对于响应包,进行SNAT
-
需要将RS的默认网关地址配置为LB的虚拟IP地址
缺点:LB和RS必须位于同一子网,且客户端和LB不能位于同一子网
FULLNAT模式
特点:
-
LB会对请求包和响应包都做SNAT+DNAT
-
LB和RS对于组网结构没有要求
-
LB和RS必须位于同一子网,且客户端和LB不能位于同一子网
三种转发模式性能从高到低:DR > NAT >FULLNAT
ipvs 负载均衡器常用调度算法
轮询(Round Robin)
LB认为集群内每台RS都是相同的,会轮流进行调度分发。从数据统计上看,RR模式是调度最均衡的。
加权轮询(Weighted Round Robin)
LB会根据RS上配置的权重,将消息按权重比分发到不同的RS上。可以给性能更好的RS节点配置更高的权重,提升集群整体的性能。
最少连接调度
LB会根据和集群内每台RS的连接数统计情况,将消息调度到连接数最少的RS节点上。在长连接业务场景下,LC算法对于系统整体负载均衡的情况较好;但是在短连接业务场景下,由于连接会迅速释放,可能会导致消息每次都调度到同一个RS节点,造成严重的负载不均衡。
加权最少连接调度
最小连接数算法的加权版。
原地址哈希,锁定请求的用户
根据请求的源IP,作为散列键(Hash Key)从静态分配的散列表中找出对应的服务器。若该服务器是可用的且未超载,将请求发送到该服务器。