大家好,我是腾意。

运行于Pod中的部分容器化应用是向客户端提供服务的守护进程,例如,nginx、tomcat和etcd等,它们受控于控制器资源对象,存在生命周期,在自愿或非自愿中断后只能被重构的新Pod对象所取代,属于非可再生类的组件。于是,在动态、弹性的管理模型下,Service资源用于为此类Pod对象提供一个固定、统一的访问接口及负载均衡的能力,并支持借助于新一代DNS系统的服务发现功能,解决客户端发现并访问容器化应用的难题。

然而,Service及Pod对象的IP地址都仅在Kubernetes集群内可达,它们无法接入集群外部的访问流量。解决此类问题的办法中,除了在单一节点上做端口暴露(hostPort)及让Pod资源共享使用工作节点的网络名称空间(hostNetwork)之外,更推荐用户使用的是NodePort或LoadBalancer类型的Service资源,或者是有着七层负载均衡能力的Ingress资源。

1.1 Service资源及其实现模型

Service是Kubernetes的核心资源类型之一,通常可看作微服务的一种实现。事实上它是一种抽象:通过规则定义出由多个Pod对象组合而成的逻辑集合,以及访问这组Pod的策略。Service关联Pod资源的规则要借助于标签选择器来完成,这一点类似于第5章讲到的Pod控制器。

1.1.1 Service资源概述

由Deployment等控制器管理的Pod对象中断后会由新建的资源对象所取代,而扩缩容后的应用则会带来Pod对象群体的变动,随之变化的还有Pod的IP地址访问接口等,这也是编排系统之上的应用程序必然要面临的问题。例如,当图6-1中的Nginx Pod作为客户端访问tomcat Pod中的应用时,IP的变动或应用规模的缩减会导致客户端访问错误,而Pod规模的扩容又会使得客户端无法有效地使用新增的Pod对象,从而影响达成规模扩展之目的。为此,Kubernetes特地设计了Service资源来解决此类问题。

img

图6-1 Pod及其客户端示例

Service资源基于标签选择器将一组Pod定义成一个逻辑组合,并通过自己的IP地址和端口调度代理请求至组内的Pod对象之上,如图6-2所示,它向客户端隐藏了真实的、处理用户请求的Pod资源,使得客户端的请求看上去就像是由Service直接处理并进行响应的一样。

img

图6-2 Kubernetes Service资源模型示意图

Service对象的IP地址也称为Cluster IP,它位于为Kubernetes集群配置指定专用IP地址的范围之内,而且是一种虚拟IP地址,它在Service对象创建后即保持不变,并且能够被同一集群中的Pod资源所访问。Service端口用于接收客户端请求并将其转发至其后端的Pod中应用的相应端口之上,因此,这种代理机制也称为“端口代理”(port proxy)或四层代理,它工作于TCP/IP协议栈的传输层。

通过其标签选择器匹配到的后端Pod资源不止一个时,Service资源能够以负载均衡的方式进行流量调度,实现了请求流量的分发机制。Service与Pod对象之间的关联关系通过标签选择器以松耦合的方式建立,它可以先于Pod对象创建而不会发生错误,于是,创建Service与Pod资源的任务可由不同的用户分别完成,例如,服务架构的设计和创建由运维工程师进行,而填充其实现的Pod资源的任务则可交由开发者进行。Service、控制器与Pod之间的关系如图6-3所示。

img

图6-3 Service、控制器与Pod

Service资源会通过API Server持续监视着(watch)标签选择器匹配到的后端Pod对象,并实时跟踪各对象的变动,例如,IP地址变动、对象增加或减少等。不过,需要特别说明的是,Service并不直接链接至Pod对象,它们之间还有一个中间层—Endpoints资源对象,它是一个由IP地址和端口组成的列表,这些IP地址和端口则来自于由Service的标签选择器匹配到的Pod资源。这也是很多场景中会使用“Service的后端端点”(Endpoints)这一术语的原因。默认情况下,创建Service资源对象时,其关联的Endpoints对象会自动创建。

1.1.2 虚拟IP和服务代理

简单来讲,一个Service对象就是工作节点上的一些iptables或ipvs规则,用于将到达Service对象IP地址的流量调度转发至相应的Endpoints对象指向的IP地址和端口之上。工作于每个工作节点的kube-proxy组件通过API Server持续监控着各Service及与其关联的Pod对象,并将其创建或变动实时反映至当前工作节点上相应的iptables或ipvs规则上。客户端、Service及其Pod对象的关系如图6-4所示。

提示 Netfilter是Linux内核中用于管理网络报文的框架,它具有网络地址转换(NAT)、报文改动和报文过滤等防火墙功能,用户借助于用户空间的iptables等工具可按需自由定制规则使用其各项功能。ipvs是借助于Netfilter实现的网络请求报文调度框架,支持rr、wrr、lc、wlc、sh、sed和nq等十余种调度算法,用户空间的命令行工具是ipvsadm,用于管理工作于ipvs之上的调度规则。

Service IP事实上是用于生成iptables或ipvs规则时使用的IP地址,它仅用于实现Kubernetes集群网络的内部通信,并且仅能够将规则中定义的转发服务的请求作为目标地址予以响应,这也是它被称为虚拟IP的原因之一。kube-proxy将请求代理至相应端点的方式有三种:userspace(用户空间)、iptables和ipvs。

1.userspace代理模型

此处的userspace是指Linux操作系统的用户空间。这种模型中,kube-proxy负责跟踪API Server上Service和Endpoints对象的变动(创建或移除),并据此调整Service资源的定义。对于每个Service对象,它会随机打开一个本地端口(运行于用户空间的kube-proxy进程负责监听),任何到达此代理端口的连接请求都将被代理至当前Service资源后端的各Pod对象上,至于会挑中哪个Pod对象则取决于当前Service资源的调度方式,默认的调度算法是轮询(round-robin),其工作逻辑如图6-5所示。另外,此类的Service对象还会创建iptables规则以捕获任何到达ClusterIP和端口的流量。在Kubernetes 1.1版本之前,userspace是默认的代理模型。

img

图6-4 kube-proxy和Service

img

图6-5 userspace代理模型

这种代理模型中,请求流量到达内核空间后经由套接字送往用户空间的kube-proxy,而后再由它送回内核空间,并调度至后端Pod。这种方式中,请求在内核空间和用户空间来回转发必然会导致效率不高。

2.iptables代理模型

同前一种代理模型类似,iptables代理模型中,kube-proxy负责跟踪API Server上Service和Endpoints对象的变动(创建或移除),并据此做出Service资源定义的变动。同时,对于每个Service,它都会创建iptables规则直接捕获到达ClusterIP和Port的流量,并将其重定向至当前Service的后端,如图6-6所示。对于每个Endpoints对象,Service资源会为其创建iptables规则并关联至挑选的后端Pod资源,默认算法是随机调度(random)。iptables代理模式由Kubernetes 1.1版本引入,并自1.2版开始成为默认的类型。

在创建Service资源时,集群中每个节点上的kube-proxy都会收到通知并将其定义为当前节点上的iptables规则,用于转发工作接口接收到的与此Service资源的ClusterIP和端口的相关流量。客户端发来的请求被相关的iptables规则进行调度和目标地址转换(DNAT)后再转发至集群内的Pod对象之上。

相对于用户空间模型来说,iptables模型无须将流量在用户空间和内核空间来回切换,因而更加高效和可靠。不过,其缺点是iptables代理模型不会在被挑中的后端Pod资源无响应时自动进行重定向,而userspace模型则可以。

3.ipvs代理模型

Kubernetes自1.9-alpha版本起引入了ipvs代理模型,且自1.11版本起成为默认设置。此种模型中,kube-proxy跟踪API Server上Service和Endpoints对象的变动,据此来调用netlink接口创建ipvs规则,并确保与API Server中的变动保持同步,如图6-7所示。它与iptables规则的不同之处仅在于其请求流量的调度功能由ipvs实现,余下的其他功能仍由iptables完成。

img

图6-7 ipvs代理模型

类似于iptables模型,ipvs构建于netfilter的钩子函数之上,但它使用hash表作为底层数据结构并工作于内核空间,因此具有流量转发速度快、规则同步性能好的特性。另外,ipvs支持众多调度算法,例如rr、lc、dh、sh、sed和nq等。

1.2 Service资源的基础应用

Service资源本身并不提供任何服务,真正处理并响应客户端请求的是后端的Pod资源,这些Pod资源通常由第5章中介绍的各类控制器对象所创建和管理,因此Service资源通常要与控制器资源(最为常用的控制器之一是Deployment)协同使用以完成应用的创建和对外发布。

1.2.1 创建Service资源

创建Service对象的常用方法有两种:一是直接使用“kubectl expose”命令,这在前面第3章中已经介绍过其使用方式;另一个是使用资源配置文件,它与此前使用资源清单文件配置其他资源的方法类似。定义Service资源对象时,spec的两个较为常用的内嵌字段分别为selector和ports,分别用于定义使用的标签选择器和要暴露的端口。下面的配置清单是一个Service资源示例:


kind: Service
apiVersion: v1
metadata:
  name: myapp-svc
spec:
  selector:
    app: myapp
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80

Service资源myapp-svc通过标签选择器关联至标签为“app=myapp”的各Pod对象,它会自动创建名为myapp-svc的Endpoints资源对象,并自动配置一个ClusterIP,暴露的端口由port字段进行指定,后端各Pod对象的端口则由targetPort给出,也可以使用同port字段的默认值。myapp-svc创建完成后,使用下面的命令即能获取相关的信息输出以了解资源的状态:


~]$ kubectl get svc myapp-svc
NAME        TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
myapp-svc   ClusterIP   10.107.208.93   <none>        80/TCP    56s

上面命令中的结果显示,myapp-svc的类型为默认的ClusterIP,其使用的地址自动配置为10.107.208.93。此类型的Service对象仅能通过此IP地址接受来自于集群内的客户端Pod中的请求。若集群上存在标签为“app=myapp”的Pod资源,则它们会被关联和创建,作为此Service对象的后端Endpoint对象,并负责接收相应的请求流量。类似下面的命令可用于获取Endpoint资源的端点列表,其相关的端点是由第5章中的Deployment控制器创建的Pod对象的套接字信息组成的:


~]$ kubectl get endpoints myapp-svc
NAME        ENDPOINTS                                         AGE
myapp-svc   10.244.1.109:80,10.244.2.249:80,10.244.3.93:80    2m

提示 也可以不为Service资源指定.spec.selector属性值,其关联的Pod资源可由用户手动创建Endpoints资源进行定义。

Service对象创建完成后即可作为服务被各客户端访问,但要真正响应这些请求,还是要依赖于各后端的资源对象。

1.2.2 向Service对象请求服务

Service资源的默认类型为ClusterIP,它仅能接收来自于集群中的Pod对象中的客户端程序的访问请求。下面创建一个专用的Pod对象,利用其交互式接口完成访问测试。为了简单起见,这里选择直接创建一个临时使用的Pod对象作为交互式使用的客户端进行,它使用CirrOS镜像,默认的命令提示符为“/#”:


/#”:
~]$ kubectl run cirros-$RANDOM --rm -it --image=cirros -- sh
/ #

提示 CirrOS是设计用来进行云计算环境测试的Linux微型发行版,它拥有HTTP客户端工具curl等。

而后,在容器的交互式接口中使用crul命令对myapp-svc服务的ClusterIP(10.107.208.93)和Port(80/tcp)发起访问请求测试:


/ # curl http://10.107.208.93:80/
Hello MyApp | Version: v1 | <a href="hostname.html">Pod Name</a>

myapp容器中的“/hostname.html”页面能够输出当前容器的主机名,可反复向myapp-svc的此URL路径发起多次请求以验证其调度的效果:


/ # for loop in 1 2 3 4; do curl http://10.107.208.93:80/hostname.html; done
deploy-myapp-86b4b8c75d-rbhfl
deploy-myapp-86b4b8c75d-mhbkd
deploy-myapp-86b4b8c75d-xk8qb

当前Kubernetes集群的Service代理模式为iptables,它默认使用随机调度算法,因此Service会将客户端请求随机调度至与其关联的某个后端Pod资源上。命令取样次数越大,其调度效果也越接近于算法的目标效果。

1.2.3 Service会话粘性

Service资源还支持Session affinity(粘性会话或会话粘性)机制,它能够将来自同一个客户端的请求始终转发至同一个后端的Pod对象,这意味着它会影响调度算法的流量分发功用,进而降低其负载均衡的效果。因此,当客户端访问Pod中的应用程序时,如果有基于客户端身份保存某些私有信息,并基于这些私有信息追踪用户的活动等一类的需求时,那么应该启用session affinity机制。

Session affinity的效果仅会在一定时间期限内生效,默认值为10800秒,超出此时长之后,客户端的再次访问会被调度算法重新调度。另外,Service资源的Session affinity机制仅能基于客户端IP地址识别客户端身份,它会把经由同一个NAT服务器进行源地址转换的所有客户端识别为同一个客户端,调度粒度粗糙且效果不佳,因此,实践中并不推荐使用此种方法实现粘性会话。此节仅用于为读者介绍其功能及实现。

Service资源通过.spec.sessionAffinity和.spec.sessionAffinityConfig两个字段配置粘性会话。spec.sessionAffinity字段用于定义要使用的粘性会话的类型,它仅支持使用“None”和“ClientIP”两种属性值。

·None:不使用sessionAffinity,默认值。

·ClientIP:基于客户端IP地址识别客户端身份,把来自同一个源IP地址的请求始终调度至同一个Pod对象。

在启用粘性会话机制时,.spec.sessionAffinityConfig用于配置其会话保持的时长,它是一个嵌套字段,使用格式如下所示,其可用的时长范围为“1~86400”,默认为10800秒:


spec:
  sessionAffinity: ClientIP
  sessionAffinityConfig:
clientIP:
      timeoutSeconds: <integer>

例如,基于默认的10800秒的超时时长,使用下面的命令修改此前的myapp-svc使用Session affinity机制:


~]$ kubectl patch services myapp-svc -p '{"spec": {"sessionAffinity": "ClientIP"}}'
service "myapp-svc" patched

而后再次于交互式客户端内测试其访问效果即可验证其会话粘性效果。


/ # for loop in 1 2 3 4; do curl http://10.107.208.93:80/hostname.html; done
deploy-myapp-86b4b8c75d-rbhfl
deploy-myapp-86b4b8c75d-rbhfl
deploy-myapp-86b4b8c75d-rbhfl

测试完成后,为了保证本章后续的其他使用效果测试不受其影响,建议将其关闭。当然,用户也可以使用“kubectl edit”命令直接编辑活动Service对象的配置清单。

1.3 服务发现

微服务意味着存在更多的独立服务,但它们并非独立的个体,而是存在着复杂的依赖关系且彼此之间通常需要进行非常频繁地交互和通信的群体。然而,建立通信之前,服务和服务之间该如何获知彼此的地址呢?在Kubernetes系统上,Service为Pod中的服务类应用提供了一个稳定的访问入口,但Pod客户端中的应用如何得知某个特定Service资源的IP和端口呢?这个时候就需要引入服务发现(Service Discovery)的机制。

1.3.1 服务发现概述

简单来说,服务发现就是服务或者应用之间互相定位的过程。不过,服务发现并非新概念,传统的单体应用架构时代也会用到,只不过单体应用的动态性不强,更新和重新发布的频度较低,通常以月甚至以年计,基本上不会进行自动伸缩,因此服务发现的概念无须显性强调。在传统的单体应用网络位置发生变化时,由IT运维人员手工更新一下相关的配置文件基本上就能解决问题。但在微服务应用场景中,应用被拆分成众多的小服务,它们按需创建且变动频繁,配置信息基本无法事先写入配置文件中并及时跟踪和反映动态变化,因此服务发现的重要性便随之凸显。

服务发现机制的基本实现,一般是事先部署好一个网络位置较为稳定的服务注册中心(也称为服务总线),服务提供者(服务端)向注册中心注册自己的位置信息,并在变动后及时予以更新,相应地,服务消费者则周期性地从注册中心获取服务提供者的最新位置信息从而“发现”要访问的目标服务资源。复杂的服务发现机制还能够让服务提供者提供其描述信息、状态信息及资源使用信息等,以供消费者实现更为复杂的服务选择逻辑。

实践中,根据服务发现过程的实现方式,服务发现还可分为两种类型:客户端发现和服务端发现。

·客户端发现:由客户端到服务注册中心发现其依赖到的服务的相关信息,因此,它需要内置特定的服务发现程序和发现逻辑。

·服务端发现:这种方式需要额外用到一个称为中央路由器或服务均衡器的组件;服务消费者将请求发往中央路由器或者负载均衡器,由它们负责查询服务注册中心获取服务提供者的位置信息,并将服务消费者的请求转发给服务提供者。

由此可见,服务注册中心是服务发现得以落地的核心组件。事实上,DNS可以算是最为原始的服务发现系统之一,不过,在服务的动态性很强的场景中,DNS记录的传播速度可能会跟不上服务的变更速度,因此它并不适用于微服务环境。另外,传统实践中,常见的服务注册中心是ZooKeeper和etcd等分布式键值存储系统,不过,它们只能提供基本的数据存储功能,距离实现完整的服务发现机制还有大量的二次开发任务需要完成。另外,它们更注重数据的一致性,这与有着更高的服务可用性要求的微服务发现场景中的需求不太相符。

Netflix的Eureka是目前较为流行的服务发现系统之一,它是专门开发用来实现服务发现的系统,以可用性目的为先,可以在多种故障期间保持服务发现和服务注册的功能可用,其设计原则遵从“存在少量的错误数据,总比完全不可用要好”。另一个同级别的实现是Consul,它是由HashiCorp公司提供的商业产品,不过该公司还提供了一个开源基础版本。它于服务发现的基础功能之外还提供了多数据中心的部署能力等一众出色的特性。

尽管传统的DNS系统不适于微服务环境中的服务发现,但SkyDNS项目(后来称kubedns)却是一个有趣的实现,它结合了古老的DNS技术和时髦的Go语言、Raft算法,并构建于etcd存储系统之上,为Kubernetes系统实现了一种服务发现机制。Service资源为Kubernetes提供了一个较为稳定的抽象层,这有点类似于服务端发现的方式,于是也就不存在DNS服务的时间窗口的问题。

Kubernetes自1.3版本开始,其用于服务发现的DNS更新为了kubeDNS,而类似的另一个基于较新的DNS的服务发现项目是由CNCF(Cloud Native Computing Foundation)孵化的CoreDNS,它基于Go语言开发,通过串接一组实现DNS功能的插件的插件链进行工作。自Kubernetes 1.11版本起,CoreDNS取代kubeDNS成为默认的DNS附件。不过,Kubernetes依然支持使用环境变量进行服务发现。

1.3.2 服务发现方式:环境变量

创建Pod资源时,kubelet会将其所属名称空间内的每个活动的Service对象以一系列环境变量的形式注入其中。它支持使用Kubernetes Service环境变量以及与Docker的links兼容的变量。

(1)Kubernetes Service环境变量

Kubernetes为每个Service资源生成包括以下形式的环境变量在内的一系列环境变量,在同一名称空间中创建的Pod对象都会自动拥有这些变量。

·{SVCNAME}_SERVICE_HOST

·{SVCNAME}_SERVICE_PORT

注意 如果SVCNAME中使用了连接线,那么Kubernetes会在定义为环境变量时将其转换为下划线。

(2)Docker Link形式的环境变量

Docker使用--link选项实现容器连接时所设置的环境变量形式,具体使用方式请参考Docker的相关文档。在创建Pod对象时,Kubernetes也会将与此形式兼容的一系列环境变量注入Pod对象中。

例如,在Service资源myapp-svc创建后创建的Pod对象中查看可用的环境变量,其中以MYAPP_SVC_SERVICE开头的表示Kubernetes Service环境变量,名称中不包含“SERVICE”字符串的环境变量为Docker Link形式的环境变量:


/ # printenv | grep MYAPP
MYAPP_SVC_PORT_80_TCP_ADDR=10.107.208.93
MYAPP_SVC_PORT_80_TCP_PORT=80
MYAPP_SVC_PORT_80_TCP_PROTO=tcp
MYAPP_SVC_PORT_80_TCP=tcp://10.107.208.93:80
MYAPP_SVC_SERVICE_HOST=10.107.208.93
MYAPP_SVC_SERVICE_PORT=80
MYAPP_SVC_PORT=tcp://10.107.208.93:80

基于环境变量的服务发现其功能简单、易用,但存在一定的局限,例如,仅有那些与创建的Pod对象在同一名称空间中且事先存在的Service对象的信息才会以环境变量的形式注入,那些处于非同一名称空间,或者是在Pod资源创建之后才创建的Service对象的相关环境变量则不会被添加。幸而,基于DNS的发现机制并不存在此类限制。

1.3.3 ClusterDNS和服务发现

Kubernetes系统之上用于名称解析和服务发现的ClusterDNS是集群的核心附件之一,集群中创建的每个Service对象,都会由其自动生成相关的资源记录。默认情况下,集群内各Pod资源会自动配置其作为名称解析服务器,并在其DNS搜索列表中包含它所属名称空间的域名后缀。

无论是使用kubeDNS还是CoreDNS,它们提供的基于DNS的服务发现解决方案都会负责解析以下资源记录(Resource Record)类型以实现服务发现。

(1)拥有ClusterIP的Service资源,需要具有以下类型的资源记录。

·A记录:

·SRV记录:

·PTR记录:

(2)Headless类型的Service资源,需要具有以下类型的资源记录。

·A记录:

·SRV记录:

·PTR记录:

(3)ExternalName类型的Service资源,需要具有CNAME类型的资源记录。

·CNAME记录:

名称解析和服务发现是Kubernetes系统许多功能得以实现的基础服务,它通常是集群安装完成后应该立即部署的附加组件。使用kubeadm初始化一个集群时,它甚至会自动进行部署。

1.3.4 服务发现方式:DNS

创建Service资源对象时,ClusterDNS会为它自动创建资源记录用于名称解析和服务注册,于是,Pod资源可直接使用标准的DNS名称来访问这些Service资源。每个Service对象相关的DNS记录包含如下两个。

·{SVCNAME}.{NAMESPACE}.{CLUSTER_DOMAIN}

·{SVCNAME}.{NAMESPACE}.svc.{CLUSTER_DOMAIN}

另外,在前面第2章的部署参数中,“--cluster-dns”指定了集群DNS服务的工作地址,“--cluster-domain”定义了集群使用的本地域名,因此,系统初始化时默认会将“cluster.local.”和主机所在的域“ilinux.io.”作为DNS的本地域使用,这些信息会在Pod创建时以DNS配置的相关信息注入它的/etc/resolv.conf配置文件中。例如,在此前创建的用于交互式Pod资源的客户端中查看其配置,命令如下:


/ # cat /etc/resolv.conf
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local ilinux.io

上述search参数中指定的DNS各搜索域,是以次序指定的几个域名后缀,具体如下所示。

·{NAMESPACE}.svc.{CLUSTER_DOMAIN}:如default.svc.cluster.local。

·svc.{CLUSTER_DOMAIN}:如svc.cluster.local。

·{CLUSTER_DOMAIN}:如cluster.local。

·{WORK_NODE_DOMAIN}:如ilinux.io。

例如,在此前创建的用于交互式Pod客户端中尝试请求解析myapp-svc的相关DNS记录:


/ # nslookup myapp-svc.default
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      myapp-svc
Address 1: 10.107.208.93 myapp-svc.default.svc.cluster.local

解析时,“myapp-svc”服务名称的搜索次序依次是default.svc.cluster.local、svc.cluster.local、cluster.local和ilinux.io,因此基于DNS的服务发现不受Service资源所在的名称空间和创建时间的限制。上面的解析结果也正是默认的default名称空间中创建的myapp-svc服务的IP地址。

1.4 服务暴露

Service的IP地址仅在集群内可达,然而,总会有些服务需要暴露到外部网络中接受各类客户端的访问,例如分层架构应用中的前端Web应用程序等。此时,就需要在集群的边缘为其添加一层转发机制,以实现将外部请求流量接入到集群的Service资源之上,这种操作也称为发布服务到外部网络中。

1.4.1 Service类型

Kubernetes的Service共有四种类型:ClusterIP、NodePort、LoadBalancer和ExternalName。

·ClusterIP:通过集群内部IP地址暴露服务,此地址仅在集群内部可达,而无法被集群外部的客户端访问,如图6-8所示。此为默认的Service类型。

·NodePort:这种类型建立在ClusterIP类型之上,其在每个节点的IP地址的某静态端口(NodePort)暴露服务,因此,它依然会为Service分配集群IP地址,并将此作为NodePort的路由目标。简单来说,NodePort类型就是在工作节点的IP地址上选择一个端口用于将集群外部的用户请求转发至目标Service的ClusterIP和Port,因此,这种类型的Service既可如ClusterIP一样受到集群内部客户端Pod的访问,也会受到集群外部客户端通过套接字: 进行的请求。

img

图6-8 NodePort Service类型

·LoadBalancer:这种类型建构在NodePort类型之上,其通过cloud provider提供的负载均衡器将服务暴露到集群外部,因此LoadBalancer一样具有NodePort和ClusterIP。简而言之,一个LoadBalancer类型的Service会指向关联至Kubernetes集群外部的、切实存在的某个负载均衡设备,该设备通过工作节点之上的NodePort向集群内部发送请求流量,如图6-9所示。例如Amazon云计算环境中的ELB实例即为此类的负载均衡设备。此类型的优势在于,它能够把来自于集群外部客户端的请求调度至所有节点(或部分节点)的NodePort之上,而不是依赖于客户端自行决定连接至哪个节点,从而避免了因客户端指定的节点故障而导致的服务不可用。

·ExternalName:其通过将Service映射至由externalName字段的内容指定的主机名来暴露服务,此主机名需要被DNS服务解析至CNAME类型的记录。换言之,此种类型并非定义由Kubernetes集群提供的服务,而是把集群外部的某服务以DNS CNAME记录的方式映射到集群内,从而让集群内的Pod资源能够访问外部的Service的一种实现方式,如图6-10所示。因此,这种类型的Service没有ClusterIP和NodePort,也没有标签选择器用于选择Pod资源,因此也不会有Endpoints存在。

前面章节中创建的myapp-svc即为默认的ClusterIP类型Service资源,它仅能接收来自于集群中的Pod对象中的客户端程序的访问请求。如若需要将Service资源发布至网络外部,应该将其配置为NodePort或LoadBalancer类型,而若要把外部的服务发布于集群内容供Pod对象使用,则需要定义一个ExternalName类型的Service资源。如若使用kube-dns,那么这种类型的实现将依赖于1.7及其以上版本的Kubernetes版本。

img

图6-9 LoadBalancer类型的Service

img

图6-10 ExternalName类型的Service

1.4.2 NodePort类型的Service资源

NodePort即节点Port,通常在安装部署Kubernetes集群系统时会预留一个端口范围用于NodePort,默认为30000~32767之间的端口。与ClusterIP类型的可省略.spec.type属性所不同的是,定义NodePort类型的Service资源时,需要通过此属性明确指定其类型名称。例如,下面配置清单中定义的Service资源对象myapp-svc-nodeport,它使用了NodePort类型,且人为指定其节点端口为32223:


kind: Service
apiVersion: v1
metadata:
  name: myapp-svc-nodeport
spec:
  type: NodePort
  selector:
    app: myapp
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
    nodePort: 32223

实践中,并不鼓励用户自定义使用的节点端口,除非事先能够明确知道它不会与某个现存的Service资源产生冲突。无论如何,只要没有特别需求,留给系统自动配置总是较好的选择。使用创建命令创建上面的Service对象后即可了解其运行状态:


~]$ kubectl get services myapp-svc-nodeport
NAME                TYPE     CLUSTER-IPEXTERNAL-IP     PORT(S)        AGE
myapp-svc-nodeport  NodePort 10.109.234.108   <none>   80:32223/TCP   3s

命令结果显示,NodePort类型的Service资源依然会被配置ClusterIP,事实上,它会作为节点从NodePort接入流量后转发的目标地址,目标端口则是与Service资源对应的spec.ports.port属性中定义的端口,如图6-11所示。

img

图6-11 请求流量转发过程

因此,对于集群外部的客户端来说,它们可经由任何一个节点的节点IP及端口访问NodePort类型的Service资源,而对于集群内的Pod客户端来说,依然可以通过ClusterIP对其进行访问。

1.4.3 LoadBalancer类型的Service资源

NodePort类型的Service资源虽然能够于集群外部访问得到,但外部客户端必须得事先得知NodePort和集群中至少一个节点的IP地址,且选定的节点发生故障时,客户端还得自行选择请求访问其他的节点。另外,集群节点很可能是某IaaS云环境中使用私有IP地址的VM,或者是IDC中使用私有地址的物理机,这类地址对互联网客户端不可达,因此,一般还应该在集群之外创建一个具有公网IP地址的负载均衡器,由它接入外部客户端的请求并调度至集群节点相应的NodePort之上。

IaaS云计算环境通常提供了LBaaS(Load Balancer as a Service)服务,它允许租户动态地在自己的网络中创建一个负载均衡设备。那些部署于此类环境之上的Kubernetes集群在创建Service资源时可以直接调用此接口按需创建出一个软负载均衡器,而具有这种功能的Service资源即为LoadBalancer类型。不过,如果Kubernetes部署于裸的物理服务器之上,系统管理员也可以自行手动部署一个负载均衡器(推荐使用冗余配置),并配置其将请求流量调度至各节点的NodePort之上即可。

下面是一个LoadBalancer类型的Service资源配置清单,若Kubernetes系统满足其使用条件,即可自行进行应用测试。需要注意的是,有些环境中可能还需要为Service资源的配置定义添加Annotations,必要时请自行参考Kubernetes文档中的说明:


kind: Service
apiVersion: v1
metadata:
  name: myapp-svc-lb
spec:
  type: LoadBalancer
  selector:
    app: myapp
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
    nodePort: 32223

进一步地,在IaaS环境支持手动指定IP地址时,用户还可以使用.spec.loadBalancerIP指定创建的负载均衡器使用的IP地址,并可使用.spec.loadBalancerSourceRanges指定负载均衡器允许的客户端来源的地址范围。

1.4.4 ExternalName Service

ExternalName类型的Service资源用于将集群外部的服务发布到集群中以供Pod中的应用程序访问,因此,它不需要使用标签选择器关联任何的Pod对象,但必须要使用spec.externalName属性定义一个CNAME记录用于返回外部真正提供服务的主机的别名,而后通过CNAME记录值获取到相关主机的IP地址。

下面是一个ExternalName类型的Service资源示例,名为external-redis-svc,相应的externalName为“redis.ilinux.io”:


kind: Service
apiVersion: v1
metadata:
  name: external-redis-svc
  namespace: default
spec:
  type: ExternalName
  externalName: redis.ilinux.io
  ports:
  - protocol: TCP
    port: 6379
    targetPort: 6379
    nodePort: 0
  selector: {}

待Service资源external-redis-svc创建完成后,各Pod对象即可通过external-redis-svc或其FQDN格式的名称external-redis-svc.default.svc.cluster.local访问相应的服务。ClusterDNS会将此名称以CNAME格式解析为.spec.externalName字段中的名称,而后通过DNS服务将其解析为相应的主机的IP地址。例如,通过此前创建的交互Pod资源客户端进行服务名称解析:


/ # nslookup external-redis-svc
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      external-redis-svc
Address 1: 45.54.44.100 100.44.54.45.ptr.anycast.net

由于ExternalName类型的Service资源实现于DNS级别,客户端将直接接入外部的服务而完全不需要服务代理,因此,它也无须配置ClusterIP,此种类型的服务也称为Headless Service。

1.5 Headless类型的Service资源

Service对象隐藏了各Pod资源,并负责将客户端的请求流量调度至该组Pod对象之上。不过,偶尔也会存在这样一类需求:客户端需要直接访问Service资源后端的所有Pod资源,这时就应该向客户端暴露每个Pod资源的IP地址,而不再是中间层Service对象的ClusterIP,这种类型的Service资源便称为Headless Service。

Headless Service对象没有ClusterIP,于是kube-proxy便无须处理此类请求,也就更没有了负载均衡或代理它的需要。在前端应用拥有自有的其他服务发现机制时,Headless Service即可省去定义ClusterIP的需求。至于如何为此类Service资源配置IP地址,则取决于它的标签选择器的定义。

·具有标签选择器:端点控制器(Endpoints Controller)会在API中为其创建Endpoints记录,并将ClusterDNS服务中的A记录直接解析到此Service后端的各Pod对象的IP地址上。

·没有标签选择器:端点控制器(Endpoints Controller)不会在API中为其创建Endpoints记录,ClusterDNS的配置分为两种情形,对ExternalName类型的服务创建CNAME记录,对其他三种类型来说,为那些与当前Service共享名称的所有Endpoints对象创建一条记录。

1.5.1 创建Headless Service资源

配置Service资源配置清单时,只需要将ClusterIP字段的值设置为“None”即可将其定义为Headless类型。下面是一个Headless Service资源配置清单示例,它拥有标签选择器:


kind: Service
apiVersion: v1
metadata:
  name: myapp-headless-svc
spec:
  clusterIP: None
  selector:
    app: myapp
  ports:
  - port: 80
    targetPort: 80
    name: httpport

使用资源创建命令“kubectl create”或“kubectl apply”完成资源创建后,使用相关的查看命令获取Service资源的相关信息便可以看出,它没有ClusterIP,不过,如果标签选择器能够匹配到相关的Pod资源,它便拥有Endpoints记录,这些Endpoints对象会作为DNS资源记录名称myapp-headless-svc查询时的A记录解析结果:


~]$ kubectl describe svc myapp-headless-svc
……
Endpoints:         10.244.1.113:80,10.244.2.13:80,10.244.3.104:80
……

1.5.2 Pod资源发现

根据Headless Service的工作特性可知,它记录于ClusterDNS的A记录的相关解析结果是后端Pod资源的IP地址,这就意味着客户端通过此Service资源的名称发现的是各Pod资源。下面依然选择创建一个专用的测试Pod对象,而后通过其交互式接口进行测试:


~]$ kubectl run cirros-$RANDOM --rm -it --image=cirros -- sh
/ #
/ # nslookup myapp-headless-svc
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      myapp-headless-svc
Address 1: 10.244.2.13
Address 2: 10.244.1.113
Address 3: 10.244.3.104

其解析结果正是Headless Service通过标签选择器关联到的所有Pod资源的IP地址。于是,客户端向此Service对象发起的请求将直接接入到Pod资源中的应用之上,而不再由Service资源进行代理转发,它每次接入的Pod资源则是由DNS服务器接收到查询请求时以轮询(roundrobin)的方式返回的IP地址。

1.6 Ingress资源

Kubernetes提供了两种内建的云端负载均衡机制(cloud load balancing)用于发布公共应用,一种是工作于传输层的Service资源,它实现的是“TCP负载均衡器”,另一种是Ingress资源,它实现的是“HTTP(S)负载均衡器”。

(1)TCP负载均衡器

无论是iptables还是ipvs模型的Service资源都配置于Linux内核中的Netfilter之上进行四层调度,是一种类型更为通用的调度器,支持调度HTTP、MySQL等应用层服务。不过,也正是由于工作于传输层从而使得它无法做到类似卸载HTTPS中的SSL会话等一类操作,也不支持基于URL的请求调度机制,而且,Kubernetes也不支持为此类负载均衡器配置任何类型的健康状态检查机制。

(2)HTTP(S)负载均衡器

HTTP(S)负载均衡器是应用层负载均衡机制的一种,支持根据环境做出更好的调度决策。与传输层调度器相比,它提供了诸如可自定义URL映射和TLS卸载等功能,并支持多种类型的后端服务器健康状态检查机制。

1.6.1 Ingress和Ingress Controller

Kubernetes中,Service资源和Pod资源的IP地址仅能用于集群网络内部的通信,所有的网络流量都无法穿透边界路由器(Edge Router)以实现集群内外通信。尽管可以为Service使用NodePort或LoadBalancer类型通过节点引入外部流量,但它依然是4层流量转发,可用的负载均衡器也为传输层负载均衡机制。

Ingress是Kubernetes API的标准资源类型之一,它其实就是一组基于DNS名称(host)或URL路径把请求转发至指定的Service资源的规则,用于将集群外部的请求流量转发至集群内部完成服务发布。然而,Ingress资源自身并不能进行“流量穿透”,它仅是一组路由规则的集合,这些规则要想真正发挥作用还需要其他功能的辅助,如监听某套接字,然后根据这些规则的匹配机制路由请求流量。这种能够为Ingress资源监听套接字并转发流量的组件称为Ingress控制器(Ingress Controller)。

注意 不同于Deployment控制器等,Ingress控制器并不直接运行为kube-controller-manager的一部分,它是Kubernetes集群的一个重要附件,类似于CoreDNS,需要在集群上单独部署。

Ingress控制器可以由任何具有反向代理(HTTP/HTTPS)功能的服务程序实现,如Nginx、Envoy、HAProxy、Vulcand和Traefik等。Ingress控制器自身也是运行于集群中的Pod资源对象,它与被代理的运行为Pod资源的应用运行于同一网络中,如图6-12中ingress-nginx与pod1、pod3等的关系所示。

img

图6-12 Ingress与Ingress Controller

另一方面,使用Ingress资源进行流量分发时,Ingress控制器可基于某Ingress资源定义的规则将客户端的请求流量直接转发至与Service对应的后端Pod资源之上,这种转发机制会绕过Service资源,从而省去了由kube-proxy实现的端口代理开销。如图6-12所示,Ingress规则需要由一个Service资源对象辅助识别相关的所有Pod对象,但ingress-nginx控制器可经由api.ilinux.io规则的定义直接将请求流量调度至pod3或pod4,而无须经由Service对象API的再次转发,WAP相关规则的作用方式与此类同。

1.6.2 创建Ingress资源

Ingress资源是基于HTTP虚拟主机或URL的转发规则,它在资源配置清单的spec字段中嵌套了rules、backend和tls等字段进行定义。下面的示例中定义了一个Ingress资源,它包含了一个转发规则,把发往www.ilinux.io 的请求代理给名为myapp-svc的Service资源:


apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: my-ingress
    annotations:
      kubernetes.io/ingress.class: "nginx"
spec:
  rules:
  - host: www.ilinux.io
    http:
      paths:
      - backend:
          serviceName: myapp-svc
          servicePort: 80

上面资源清单中的annotations用于识别其所属的Ingress控制器的类别,这一点在集群上部署有多个Ingress控制器时尤为重要。Ingress Spec中的字段是定义Ingress资源的核心组成部分,它主要嵌套如下三个字段。

·rules"Object" :用于定义当前Ingress资源的转发规则列表;未由rules定义规则,或者没有匹配到任何规则时,所有流量都会转发到由backend定义的默认后端。

·backend"Object" :默认的后端用于服务那些没有匹配到任何规则的请求;定义Ingress资源时,至少应该定义backend或rules两者之一;此字段用于让负载均衡器指定一个全局默认的后端。

·tls"Object" :TLS配置,目前仅支持通过默认端口443提供服务;如果要配置指定的列表成员指向了不同的主机,则必须通过SNI TLS扩展机制来支持此功能。

backend对象的定义由两个必选的内嵌字段组成:serviceName和servicePort,分别用于指定流量转发的后端目标Service资源的名称和端口。

rules对象由一系列配置Ingress资源的host规则组成,这些host规则用于将一个主机上的某个URL路径映射至相关的后端Service对象,它的定义格式如下:


spec:
  rules:
- host:  <String>
http:
    paths:
       backend:
         serviceName: <String>
         servicePort: <String>
        path: <String>

注意,.spec.rules.host属性值目前不支持使用IP地址,也不支持后跟“:PORT”格式的端口号,且此字段值留空表示通配所有的主机名。

tls对象由两个内嵌字段组成,仅在定义TLS主机的转发规则时才需要定义此类对象。

·hosts:包含于使用的TLS证书之内的主机名称字符串列表,因此,此处使用的主机名必须匹配tlsSecret中的名称。

·secretName:用于引用SSL会话的secret对象名称,在基于SNI实现多主机路由的场景中,此字段为可选。

1.6.3 Ingress资源类型

基于HTTP暴露的每个Service资源均可发布于一个独立的FQDN主机名之上,如“www.ik8s.io”;也可发布于某主机的URL路径之上,从而将它们整合到同一个Web站点,如“www.ik8s.io/grafana”。至于是否需要发布为HTTPS类型的应用则取决于用户的业务需求。

1.单Service资源型Ingress

暴露单个服务的方法有很多种,如服务类型中的NodePort、LoadBalancer等,不过一样可以考虑使用Ingress来暴露服务,此时只需要为Ingress指定“default backend”即可。例如下面的示例:


apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: my-ingress
spec:
  backend:
    serviceName: my-svc
    servicePort: 80

Ingress控制器会为其分配一个IP地址接入请求流量,并将它们转至示例中的my-svc后端。

2.基于URL路径进行流量分发

垂直拆分或微服务架构中,每个小的应用都有其专用的Service资源暴露服务,但在对外开放的站点上,它们可能是财经、新闻、电商、无线端或API接口等一类的独立应用,可通过主域名的URL路径(path)分别接入,例如,www.ilinux.io/apiwww.ilinux.io/wap 等,用于发布集群内名称为API和WAP的Services资源。于是,可对应地创建一个如下的Ingress资源,它将对www.ilinux.io/api 的请求统统转发至API Service资源,将对www.ilinux.io/wap 的请求转发至WAP Service资源:


apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: test
  annotations:
    ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  - host: www.ilinux.io
    http:
      paths:
      - path: /wap
        backend:
          serviceName: wap
          servicePort: 80
      - path: /api
        backend:
          serviceName: api
          servicePort: 80

注意 目前,ingress-nginx似乎尚且不能很好地支持基于annotations进行URL映射。这就意味着,在ingress-nginx上,此项功能尚且不能使用。具体信息请参考这个链接中的讨论,https://github.com/istio/istio/issues/585 。

3.基于主机名称的虚拟主机

上面类型2中描述的需求,也可以将每个应用分别以独立的FQDN主机名进行输出,如wap.ik8s.io和api.ik8s.io,这两个主机名解析到external LB(如图6-12所示)的IP地址之上,分别用于发布集群内部的WAP和API这两个Service资源。这种实现方案其实就是Web站点部署中的“基于主机名的虚拟主机”,将多个FQDN解析至同一个IP地址,然后根据“主机头”(Host header)进行转发。下面是以独立FQDN主机形式发布服务的Ingress资源示例:


apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: test
spec:
  rules:
  - host: api.ik8s.io
    http:
      paths:
      - backend:
          serviceName: api
          servicePort: 80
  - host: wap.ik8s.io
    http:
      paths:
      - backend:
          serviceName: wap
          servicePort: 80

4.TLS类型的Ingress资源

这种类型用于以HTTPS发布Service资源,基于一个含有私钥和证书的Secret对象(后面章节中会详细讲述)即可配置TLS协议的Ingress资源,目前来说,Ingress资源仅支持单TLS端口,并且还会卸载TLS会话。在Ingress资源中引用此Secret即可让Ingress控制器加载并配置为HTTPS服务。

下面是一个简单的TLS类型的Ingress资源示例:


apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: no-rules-map
spec:
  tls:
  - secretName: ikubernetesSecret
  backend:
    serviceName: homesite
    servicePort: 80

1.6.4 部署Ingress控制器(Nginx)

Ingress控制器自身是运行于Pod中的容器应用,一般是Nginx或Envoy一类的具有代理及负载均衡功能的守护进程,它监视着来自于API Server的Ingress对象状态,并以其规则生成相应的应用程序专有格式的配置文件并通过重载或重启守护进程而使新配置生效。例如,对于Nginx来说,Ingress规则需要转换为Nginx的配置信息。简单来说,Ingress控制器其实就是托管于Kubernetes系统之上的用于实现在应用层发布服务的Pod资源,它将跟踪Ingress资源并实时生成配置规则。那么,同样运行为Pod资源的Ingress控制器进程又该如何接入外部的请求流量呢?常用的解决方案有如下两种。

img

图6-13  使用专用的Service对象为Ingress控制器接入外部流量

·以Deployment控制器管理Ingress控制器的Pod资源,并通过NodePort或LoadBalancer类型的Service对象为其接入集群外部的请求流量,这就意味着,定义一个Ingress控制器时,必须在其前端定义一个专用的Service资源,如图6-13所示。

·借助于DaemonSet控制器,将Ingress控制器的Pod资源各自以单一实例的方式运行于集群的所有或部分工作节点之上,并配置这类Pod对象以hostPort(如图6-14a)或hostNetwork(如图6-14b)的方式在当前节点接入外部流量。

img

图6-14 以hostPort或hostNetwork的方式为Ingress控制器接入外部流量

以ingress-nginx项目为例,部署Ingress Nginx控制器的配置文件被切割存放在了多个不同的文件中,并集中存储于其源码deploy子目录下,同时,为了方便用户部署,它还将所需的资源全部集成为一个配置文件mandatory.yaml:


~]$  kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-
    nginx/master/deploy/mandatory.yaml

因为需要下载相关的镜像文件,因此前面部署过程中的Pod资源的创建需要等待一段时间才能完成,具体时长要取决于网络的可用状况。可使用如下命令持续监控创建过程,待其状态为“Running”之后即表示运行正常:


~]$ kubectl get pods -n ingress-nginx  --watch
NAME                         READY    STATUS   RESTARTS   AGE
default-http-backend-6586bc58b6-cw7c6    1/1    Running   0        3m
nginx-ingress-controller-7675fd6cdb-kvsh2   1/1   Running   0        3m

在线的配置清单中采用了基于Deployment控制器部署Ingress Nginx的方式,因此接入外部流量之前还需要手动为其创建相关的NodePort或LoadBalancer类型的Service资源对象,下面的配置清单示例中对类型定义了NodePort,并明确指定了易记的端口和IP地址,以方便用户使用:


apiVersion: v1
kind: Service
metadata:
  name: nginx-ingress-controller
  namespace: ingress-nginx
spec:
  type: NodePort
  clusterIP: 10.99.99.99
  ports:
    - port: 80
      name: http
      nodePort: 30080
    - port: 443
      name: https
      nodePort: 30443
  selector:
app.kubernetes.io/name: ingress-nginx

将上面的配置信息保存于文件中,如nginx-ingress-service.yaml,而后执行如下命令完成资源的创建。注意,其标签选择器应该与mandatory.yaml配置清单中的Deployment控制器nginx-ingress-controller的选择器保持一致:


~]$ kubectl apply -f nginx-ingress-service.yaml

注意 如果读者的集群运行支持LBaaS的IaaS云环境,则可以将其类型指定为LoadBalancer,这样直接就有了可用的external-LB。

确认Service对象nginx-ingress-controller的状态没有问题后即可于集群外部对其发起访问测试,目标URL为http://:30080http://:30443 ,确认可接收到响应报文后即表示Ingress Nginx部署完成。不过,本示例中尚且缺少一个可用的外部负载均衡器,如图6-13中所示的“external-LB”,因此,访问测试时暂时还只能使用http://: 进行。

1.7 案例:使用Ingress发布tomcat

假设有这样一套环境:Kubernetes集群上的tomcat-deploy控制器生成了两个运行于Pod资源中的tomcat实例,tomcat-svc是将它们统一暴露于集群中的访问入口。现在需要通过Ingress资源将tomcat-svc发布给集群外部的客户端访问。具体的需求和规划如图6-15所示。

为了便于理解,下面的测试操作过程将把每一步分解开来放在单独的一节中进行。

1.7.1 准备名称空间

假设本示例中创建的所有资源都位于新建的testing名称空间中,与其他的资源在逻辑上进行隔离,以方便管理。下面的配置信息保存于testing-namespace.yaml资源清单文件中:

img

图6-15 Ingress发布应用示例拓扑图


kind: Namespace
apiVersion: v1
metadata:
  name: testing
  labels:
    env: testing

而后运行创建命令完成资源的创建,并确认资源的存在:


~]$ kubectl apply -f testing-namespace.yaml
namespace "testing" created
~]$ kubectl get namespaces testing
NAME   STATUS    AGE
testing   Active    8s

1.7.2 部署tomcat实例

在此示例中,tomcat应用本身代表着运行于tomcat容器中的一个实际应用。具体实践中,它通常应该是包含了某应用程序的war文件的镜像文件。下面的配置清单使用了Deployment控制器于testing中部署tomcat相关的Pod对象,它保存于tomcat-deploy.yaml文件中:


apiVersion: apps/v1
kind: Deployment
metadata:
  name: tomcat-deploy
  namespace: testing
spec:
  replicas: 2
  selector:
    matchLabels:
      app: tomcat
  template:
    metadata:
      labels:
        app: tomcat
    spec:
      containers:
      - name: tomcat
        image: tomcat:8.0.50-jre8-alpine
        ports:
        - containerPort: 8080
          name: httpport
        - containerPort: 8009
          name: ajpport

运行资源创建命令完成Deployment控制器和Pod资源的创建,命令如下:


~]$ kubectl apply -f tomcat-deploy.yaml

接着运行命令以确认其成功完成,且各Pod已经处于正常运行状态中:


~]$ kubectl get pods -n testing
NAME                             READY     STATUS    RESTARTS   AGE
tomcat-deploy-6cf8468f7f-5d7tb   1/1       Running   0          1m
tomcat-deploy-6cf8468f7f-9fvxx   1/1       Running   0          1m

实践中,如果需要更多的Pod资源承载用户访问,那么使用Deployment控制器的规模伸缩机制即可完成,或者直接修改上面的配置文件并执行“kubectl apply”命令重新进行应用。

1.7.3 创建Service资源

Ingress资源仅通过Service资源识别相应的Pod资源,获取其IP和端口,而后Ingress控制器即可直接使用各Pod对象的IP地址与它直接进行通信,而不经由Service资源的代理和调度,因此Service资源的ClusterIP对Ingress控制器来说一无所用。不过,若集群内的其他Pod客户端需要与其通信,那么保留ClusterIP似乎也是很有必要的。

下面的配置文件中定义了Service资源tomcat-svc,它通过标签选择器将相关的Pod对象归于一组,并通过80/TCP端口暴露Pod对象的8080/TCP端口。如果需要暴露容器的8009/TCP端口,那么只需要将其以类似的格式配置于列表中即可:


apiVersion: v1
kind: Service
metadata:
  name: tomcat-svc
  namespace: testing
  labels:
    app: tomcat-svc
spec:
  selector:
    app: tomcat
  ports:
  - name: http
    port: 80
    targetPort: 8080
    protocol: TCP

运行资源创建命令完成Service资源的创建:


~]$ kubectl apply -f tomcat-svc.yaml

接着运行命令以确认其成功完成:


~]$ kubectl  get svc tomcat-svc -n testing
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
tomcat-svc   ClusterIP   10.108.72.237   <none>        80/TCP    8s

1.7.4 创建Ingress资源

通过Ingress资源的FQDN主机名或URL路径等类型发布的服务,只有用户的访问请求能够匹配到其.spec.rules.host字段定义的主机时才能被相应的规则处理。如果要明确匹配用户的处理请求,比如希望将那些发往tomcat.ilinux.io主机的所有请求代理至tomcat-svc资源的后端Pod,则可以使用如下命令配置文件中的内容:


apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: tomcat
  namespace: testing
  annotations:
    kubernetes.io/ingress.class: "nginx"
spec:
  rules:
  - host: tomcat.ilinux.io
    http:
      paths:
      - path:
        backend:
          serviceName: tomcat-svc
          servicePort: 80

运行资源创建命令完成Service资源的创建:


~]$ kubectl apply -f tomcat-ingress.yaml

而后通过详细信息确认其创建成功完成,并且已经正确关联到相应的tomcat-svc资源上:


~]$ kubectl describe ingresses -n testing
Name:             tomcat
Namespace:        testing
Address:
Default backend:  default-http-backend:80 (<none>)
Rules:
  Host                   Path  Backends
  ----                   ----  --------
  tomcat.ilinux.io
                            tomcat-svc:80 (<none>)
Annotations:
Events:
  Type    Reason  Age   From                      Message
  ----    ------  ----  ----                      -------
  Normal  CREATE  3s    nginx-ingress-controller  Ingress testing/tomcat

接下来即可通过Ingress控制器的前端Service资源的NodePort来访问此服务,在1.1.4节中,此Service资源的ClusterIP被明确定义为10.99.99.99,并以节点端口30080映射Ingress控制器的80端口。因此,这里使用Ingress中定义的主机名tomcat.ilinux.io:30080即可访问tomcat应用,图6-16所示的是访问页面的效果。当然,实践中,其前端应该有一个外部的负载均衡设备接收并调度此类请求。

img

图6-16 访问Ingress资源代理的tomcat应用

不过,用户对tomcat.ilinux.io主机之外的地址发起的被此Ingress规则匹配到的请求将发往Ingress控制器的默认的后端,即default-http-backend,它通常只能返回一个404提示信息。用户也可按需自定义默认后端,例如,如下面的配置文件片断所示,它通过.spec.backend定义了所有无法由此Ingress匹配的访问请求都由相应的后端default-svc这个Service资源来处理:


spec:
  backend:
    serviceName: default-svc
    servicePort: 80

1.7.5 配置TLS Ingress资源

一般来说,如果有基于HTTPS通信的需求,那么它应该由外部的负载均衡器(external-LB)予以实现,并在SSL会话卸载后将访问请求转发到Ingress控制器。不过,如果外部负载均衡器工作于传输层而不是工作于应用层的反向代理服务器,或者存在直接通过Ingress控制器接收客户端请求的需求,又期望它们能够提供HTTPS服务时,就应该配置TLS类型的Ingress资源。

将此类服务公开发布到互联网时,HTTPS服务用到的证书应由公信CA签署并颁发,用户遵循其相应流程准备好相关的数字证书即可。如果出于测试或内部使用之目的,那么也可以选择自制私有证书。openssl工具程序是用于生成自签证书的常用工具,这里使用它生成用于测试的私钥和自签证书:


~]$ openssl genrsa -out tls.key 2048
~]$ openssl req -new -x509 -key tls.key -out tls.crt \
    -subj /C=CN/ST=Beijing/L=Beijing/O=DevOps/CN=tomcat.ilinux.io -days 3650

注意 TLS Secret中包含的证书必须以tls.crt作为其键名,私钥文件必须以tls.key为键名,因此上面生成的私钥文件和证书文件名将直接保存为键名形式,以便于后面创建Secret对象时直接作为键名引用。

在Ingress控制器上配置HTTPS主机时,不能直接使用私钥和证书文件,而是要使用Secret资源对象来传递相关的数据。所以,接下来要根据私钥和证书生成用于配置TLS Ingress的Secret资源,在创建Ingress规则时由其将用到的Secret资源中的信息注入Ingress控制器的Pod对象中,用于为配置的HTTPS虚拟主机提供相应的私钥和证书。下面的命令会创建一个TLS类型名为tomcat-ingress-secret的Secret资源:


~]$ kubectl create secret tls tomcat-ingress-secret --cert=tls.crt --key=tls.
    key -n testing

可使用下面的命令确认Secrets资源tomcat-ingress-secret创建成功完成:


~]$ kubectl get secrets tomcat-ingress-secret -n testing
NAME                    TYPE                DATA      AGE
tomcat-ingress-secret   kubernetes.io/tls   2         20s

而后去定义创建TLS类型Ingress资源的配置清单。下面的配置清单通过spec.rules定义了一组转发规则,并通过.spec.tls将此主机定义为了HTTPS类型的虚拟主机,用到的私钥和证书信息则来自于Secrets资源tomcat-ingress-secret:


apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: tomcat-ingress-tls
  annotations:
    kubernetes.io/ingress.class: "nginx"
spec:
  tls:
  - hosts:
    - tomcat.ilinux.io
    secretName: tomcat-ingress-secret
  rules:
  - host: tomcat.ilinux.io
    http:
      paths:
      - path: /
        backend:
          serviceName: tomcat-svc
          servicePort: 80

运行资源创建命令完成Service资源的创建:


~]$ kubectl apply -f tomcat-ingress-tls.yaml

而后通过详细信息确认其创建成功完成,且已经正确关联到相应的tomcat-svc资源:


~]$ kubectl describe ingress tomcat-ingress-tls -n testing
Name:             tomcat-ingress-tls
Namespace:        testing
Address:
Default backend:  default-http-backend:80 (<none>)
TLS:
  tomcat-ingress-secret terminates tomcat.ilinux.io
Rules:
  Host                   Path  Backends
  ----                   ----  --------
  tomcat.ilinux.io
                         /   tomcat-svc:80 (<none>)
Annotations:
Events:
  Type    Reason  Age   From                      Message
  ----    ------  ----  ----                      -------
  Normal  CREATE  31s   nginx-ingress-controller  Ingress testing/tomcat-ingress-
      tls

接下来即可通过Ingress控制器的前端Service资源的NodePort来访问此服务,在1.1.4节中,此Service资源以节点端口30443映射控制器的443端口。因此,这里使用Ingress中定义的主机名tomcat.ilinux.io:30443即可访问tomcat应用,其访问到的页面效果类似于图6-16中的内容。另外,也可以使用curl进行访问测试,只要对应的主机能够正确解析tomcat.ilinux.io主机名即可,例如,下面的测试命令及其输出表明,TLS类型的Ingress已然配置成功:


~]# curl -k -v https://tomcat.ilinux.io:30443/
* About to connect() to tomcat.ilinux.io port 30443 (#0)
*   Trying 172.16.0.66...
……
* Server certificate:
*       subject: CN=tomcat.ilinux.io,O=DevOps,L=Beijing,ST=Beijing,C=CN
*       start date: Aug 23 07:05:31 2018 GMT
*       expire date: Aug 20 07:05:31 2028 GMT
*       common name: tomcat.ilinux.io
*       issuer: CN=tomcat.ilinux.io,O=DevOps,L=Beijing,ST=Beijing,C=CN
> GET / HTTP/1.1
……
< HTTP/1.1 200 OK
< Server: nginx/1.13.9
< Date: Thu, 23 Aug 2018 07:37:40 GMT
……

到此为止,实践配置目标已经全部达成。需要再次提醒的是,在实际使用中,在集群之外应该存在一个用于调度用户请求至各节点上Ingress控制器相关的NodePort的负载均衡器。如果不具有LBaaS的使用条件,用户也可以基于Nginx、Haproxy、LVS等手动构建,并通过Keepalived等解决方案实现其服务的高可用配置。

1.8 小结

本篇重点讲解了Kubernetes的Service资源及其发布方式,具体如下。

·Service资源通过标签选择器为一组Pod资源创建一个统一的访问入口,其可将客户端请求代理调度至后端的Pod资源。

·Service资源是四层调度机制,默认调度算法为随机调度。

·Service的实现模式有三种:userspace、iptables和ipvs。

·Service共用四种类型:ClusterIP、NodePort、LoadBalancer和ExternalName,它们用于发布服务。

·Headless service是一种特殊的Service资源,可用于Pod发现。

·Ingress资源是发布Service资源的另一种方式,它需要结合Ingress控制器才能正常工作。

·Ingress Controller的实现方式除了Nginx之外,还有Envoy、HAProxy、Traefik等。

版权声明:如无特殊说明,文章均为本站原创,版权所有,转载需注明本文链接

本文链接:http://www.bianchengvip.com/article/Kubernetes-Service-and-Ingress-case/