本文来自正在规划的Go语言&云原生自我提升系列,欢迎关注后续文章。
Kubernetes是一套非常动态的系统。该系统涉及到将Pod放到节点上、保障其启动运行以及在需要时重新进行调度。有根据负载自动调节Pod数量的方式(比如横向Pod自动扩展,参见副本集一章中的自动扩展副本集)。系统API驱动的属性鼓励大家创建更高级的自动化。
Kubernetes的动态属性虽然简化了很多运行步骤,但对于查找它们也带来了问题。大部分传统的网络基础架构都不是为Kubernetes这种级别的动态化而创建的。
什么是服务发现?
这类问题和解决问题的通用名称是服务发现。服务发现工具有助于查找哪些进程在监听哪些服务的哪些地址。一套好的服务发现系统可以让用户快速可靠地解析这类信息。好系统还应是低延迟的,服务发生变化时客户端应马上更新相关信息。最后,好的服务发现系统可以存储服务相关更丰富的定义。比如,一个服务可能有多个关联端口。
域名系统(DNS)是因特网上传统的服务发现系统。DNS用于相对稳定的名称解析,具备广泛有效的缓存。对于因特网来说它是一套很好的系统,但对于Kubernetes的动态世界则捉襟见肘。
不幸的是,很多系统(如Java默认)会直接查找DNS中的名称并且不会重新解析。这会导致客户端缓存过期映射,从而与错误的IP进行对话。即使对比较短的 TTL (time-to-live)以及运转良好的客户端,名称解析变化与客户端收到通知之前也存在着延时。典型的DNS查询也存在着信息量和类型上的限制。在单个名称超过20到30个A记录时就开始出问题了。服务(SRV)记录能解决部分问题,但通常用起来很麻烦。最后,客户端处理DNS中的多IP时通常会拿第一个IP地址,然后依赖于DNS服务端随机会或轮询记录的顺序。没有专门的负载均衡替代品。
Service对象
Kubernetes中真正的服务从Service对象开始。Service对象是一种创建有名标签选择器的方式。读者会可了解到,Service对象还有其它的功能。
就像kubectl run
命令是一种创建Kubernetes部署的简易方式,我们可以使用kubectl expose
来创建服务。我们会在部署一章中讨论部署相关的知识,现在读者可以把部署看成一个微服务的实例。我们来创建一些部署和服务以便了解其原理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
$ kubectl create deployment alpaca-prod \ --image=gcr.io/kuar-demo/kuard-amd64:blue \ --port=8080 $ kubectl scale deployment alpaca-prod --replicas 3 $ kubectl expose deployment alpaca-prod $ kubectl create deployment bandicoot-prod \ --image=gcr.io/kuar-demo/kuard-amd64:green \ --port=8080 $ kubectl scale deployment bandicoot-prod --replicas 2 $ kubectl expose deployment bandicoot-prod $ kubectl get services -o wide NAME CLUSTER-IP ... PORT(S) ... SELECTOR alpaca-prod 10.43.174.153 ... 8080/TCP ... app=alpaca-prod bandicoot-prod 10.43.233.13 ... 8080/TCP ... app=bandicoot-prod kubernetes 10.43.0.1 ... 443/TCP ... <none> |
运行这些命令之后,会存在3个服务。刚刚创建的有alpaca-prod
和bandicoot-prod
。kubernetes
服务是自动创建的,这样我们就可以在应用内查看并与Kubernetes API进行对话。
查看SELECTOR
这一列,会看到alpaca-prod
服务只给出了选择器的名称,指定可与该服务对话的端口。kubectl expose
命令会协助从部署定义中拉取标签选择器和相关端口(本例中为8080)。
另外,这些服务会分配一种虚拟IP称之为集群IP。这是一种特殊的IP地址,系统会对通过选择器标识的Pod进行负载均衡。
我们会将端口转发至alpaca
的一个Pod来与这些服务进行交互。启动这一命令并在终端中保持运行。可访问http://localhost:48858来查看对alpaca
Pod的端口转发是否生效:
1 2 3 |
$ ALPACA_POD=$(kubectl get pods -l app=alpaca-prod \ -o jsonpath='{.items[0].metadata.name}') $ kubectl port-forward $ALPACA_POD 48858:8080 |
在Windows下执行会报:ALPACA_POD : 无法将“ALPACA_POD”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请
确保路径正确,然后再试一次。只需要将ALPACA_POD修改为$ALPACA_POD即可。
服务DNS
集群IP是虚拟的,因而它也是稳定的,适合给定DNS地址。所有关于客户端缓存DNS的问题也就不再存在了。在同一个命名空间内,只需要使用服务名称来连接服务所能识别的Pod。
Kubernetes提供了一个暴露给集群中Pod的DNS服务。这一Kubernetes DNS服务在集群首次创建时会安装为系统组件。DNS服务本身由Kubernetes管理,是一个在Kubernetes之上构建Kubernetes的很好的示例。Kubernetes DNS服务为集群IP提供DNS名称。
可以打开kuard
服务端状态页中的DNS Query测试。查询alpaca-prod
的A记录。输出如下:
1 2 3 4 5 6 7 8 |
;; opcode: QUERY, status: NOERROR, id: 58815 ;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 ;; QUESTION SECTION: ;alpaca-prod.default.svc.cluster.local. IN A ;; ANSWER SECTION: alpaca-prod.default.svc.cluster.local. 5 IN A 10.43.174.153 |
这里完整的DNS名称是alpaca-prod.default.svc.cluster.local.
。我们拆开来看:
alpaca-prod
:所查询服务的名称。
default
:服务所处的命名空间。
svc
:识别其为一个服务。这样Kubernetes未来可以将其它类型的内容暴露为DNS。
cluster.local.
:集群的基础域名。这是默认值,大部分集群都是这个名称。管理员可以修改这一名称,这样在跨集群中可以有独立的DNS名称。
在引用当前命名空间中的服务时,可以只使用服务名(alpaca-prod
)。而在其它命名空间中可以使用alpaca-prod.default
来引用这一服务。当然,也可以使用完全限定服务名称e (alpaca-prod.default.svc.cluster.local.
)。可以在上图所示kuard
的DNS Query页面中尝试查询
就绪检测
通常应用一启动就可以处理请求了。一般一些地方会有一定量的初始化,花费一秒以下用至数分钟。Service对象的一个好处是可以通过就绪检测跟踪哪些Pod已准备就绪。我们来修改部署为Pod添加就绪检测,这在Pods一章中已进行过讨论。
1 |
$ kubectl edit deployment/alpaca-prod |
这个命名会获取当前版本的alpaca-prod
部署并在编辑器中打开。保存、退出编辑器后,会将对象写回到Kubernetes中。这是快速编辑对象又不写入YAML文件的方式。
添加如下部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
spec: ... template: ... spec: containers: ... name: alpaca-prod readinessProbe: httpGet: path: /ready port: 8080 periodSeconds: 2 initialDelaySeconds: 0 failureThreshold: 3 successThreshold: 1 |
通过这一配置,这个部署中的Pod会通过 HTTP GET
请求对8080端口上的/ready
进行就绪性检测。这一检测在Pod启动后会每两秒执行一次。如果出现连续三次的失败,该Pod就会被视作未就绪。但只要检测中有一次成功,该Pod就会再次视为就绪。
仅会对就绪的Pod发送流量。
这样更新部署定义会删除、重建alpaca
的各个Pod。因此我们需要使用此前的命令重启port-forward
:
1 2 3 |
$ ALPACA_POD=$(kubectl get pods -l app=alpaca-prod \ -o jsonpath='{.items[0].metadata.name}') $ kubectl port-forward $ALPACA_POD 48858:8080 |
在浏览器中访问http://localhost:48858,应该会在kuard
实例中看到这一调试页面。打开Readiness Probe页。应该会在系统每次进行就绪检测时看到页面更新,这里每两秒刷新一次。
再打开一个终端窗口,对alpaca-prod
服务的端点启动watch
命令。端点是查找服务向谁发送流量的底层方式,在本章稍后讲解。这里的--watch
选项会使用kubectl
命令悬停输出所有更新。这是查看Kubernetes对象变量的简易方式:
1 |
$ kubectl get endpoints alpaca-prod --watch |
此时回到浏览器点击就绪检查页面的Fail链接。会看到此时服务端会返回状态码为500的错误。三次后会在端点列表中删除这一服务器。点击Succeed会发现在一次就绪检查后,该端点又会加回来。
就绪检测是一种过载或出故障的服务端向系统发送信息表明不希望再接收流量的方式。它是一种实现优雅关机很好的方式。服务端可发送信息它不希望再接收流量,等待已有连接关闭,然后再退出。
在port-forward
和watch
命令的终端中点击Ctrl+C退出。
集群外的世界
本章我们到现在所学的都是在集群内暴露服务。通常Pod的IP只能在集群内访问。不过有时我们需要允许新流量进入。
实现的最便捷方式是使用NodePort特性,它对服务的增加不止于此。除了集群IP,系统会选取一个端口(或者由用户指定),然后集群中的各个节点会将这一端口的流量转发到该服务。
有了这一特性,只要能触达集群中的节点,就可以访问到服务。在使用NodePort时可以不用知道该服务所运行的Pod在哪里。可与硬件或软件负载均衡集成来进一步暴露服务。
可修改alpaca-prod
服务进行尝试:
1 |
$ kubectl edit service alpaca-prod |
将spec.type
修改为NodePort
。也可以在通过kubectl expose
创建服务时指定--type=NodePor
实现同样的效果。系统会分配一个新的NodePort:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$ kubectl describe service alpaca-prod Name: alpaca-prod Namespace: default Labels: app=alpaca Annotations: <none> Selector: app=alpaca Type: NodePort IP: 10.115.245.13 Port: <unset> 8080/TCP NodePort: <unset> 32711/TCP Endpoints: 10.112.1.66:8080,10.112.2.104:8080,10.112.2.105:8080 Session Affinity: None No events. |
这里可以看到系统为该服务分配了一个端口32711。现在可通过各个集群节点的该端口访问服务。如果处于同一个网络,可以直接访问到它。如果集群位于云端,可以使用SSH访问,类似下面这样:
1 |
$ ssh <node> -L 8080:localhost:32711 |
这时在浏览器中访问http://localhost:8080,就会连接到该服务。对服务所发送的每个请求会随机跳转到实现了该服务的Pod上。多重新加载几次页面,会看到被随机分配到不同的Pod上。
完成操作后退出SSH会话。
集成负载均衡
如果集群配置了与外部负载均衡器相集成,可以使用LoadBalancer
类型。这种构建基于NodePort
类型,同时要配置云端创建新的负载均衡,将其定向至集群中的节点上。大部云上Kubernetes集群都提供负载均衡集成,也有一些项目集成了常见物理负载均衡,但这种需要更多手动与集群的集成。
再次编辑alpaca-prod
(kubectl edit service alpaca-prod
),修改spec.type
为LoadBalancer
。
注:创建LoadBalancer
类型的服务会将该服务暴露到公网上。在做这一操作前,应当确定将其暴露出去就安全的。本节中会进一步讨论安全风险。此外部署和Istio两章中会包含应用安全相关的内容。
此时执行kubectl get services
,会看到alpaca-prod
的EXTERNAL-IP
列中显示为<pending>
。等待片刻,就可以看到云端为你分配的公有地址。可以在云账号的终端中查看Kubernetes 所做的配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
$ kubectl describe service alpaca-prod Name: alpaca-prod Namespace: default Labels: app=alpaca Selector: app=alpaca Type: LoadBalancer IP: 10.115.245.13 LoadBalancer Ingress: 104.196.248.204 Port: <unset> 8080/TCP NodePort: <unset> 32711/TCP Endpoints: 10.112.1.66:8080,10.112.2.104:8080,10.112.2.105:8080 Session Affinity: None Events: FirstSeen ... Reason Message --------- ... ------ ------- 3m ... Type NodePort -> LoadBalancer 3m ... CreatingLoadBalancer Creating load balancer 2m ... CreatedLoadBalancer Created load balancer |
这里可以看到为alpaca-prod
服务分配了地址104.196.248.204。在浏览器中打开看效果。
注:上例使用了Google云平台GKE启动、管理的集群。负载均衡的配置各云厂商产不相同。一些云使用基于 DNS的负载均衡(比如AWS的弹性负载均衡ELB)。这时,看到的不是IP而是主机名。根据云服务商的不同,负载均衡生效花费的时间也不相同。
创建云端负载均衡会花费一些时间。很多云服务提供商会用上几分钟,不必意外。
我们至此所学的是外部负载均衡,即负载均衡与公网相连。虽然将服务暴露给全世界很棒,有时我们也需要将应用仅暴露在私网以内。这时就要用到内部负载均衡了。可惜内部负载均衡后来才在Kubernetes中添加,使用的是对象注解这种特别的方式。例如,要在Azure Kubernetes服务集群中创建内部负载均衡,要对Service
资源添加注解service.beta.kubernetes.io/azure-load-balancer-internal: "true"
。一些知名云平台的配置如下:
微软Azure:service.beta.kubernetes.io/azure-load-balancer-internal: "true"
亚马逊云AWS:service.beta.kubernetes.io/aws-load-balancer-internal: "true"
阿里云:service.beta.kubernetes.io/alibaba-cloud-loadbalancer-address-type: "intranet"
谷歌云平台:cloud.google.com/load-balancer-type: "Internal"
在对服务添加这一注解时,会类似下面这样:
1 2 3 4 5 6 7 |
... metadata: ... name: some-service annotations: service.beta.kubernetes.io/azure-load-balancer-internal: "true" ... |
在使用这些注解创建服务时,创建的服务仅暴露于内部,公网不可见。
小贴士:还有一些注解可扩展负载均衡,包括可使用预先存在的IP地址。各服务商的具体扩展可参见其官方文档。
进阶详解
Kubernetes是一套可扩展的系统。因此存在一些更高级集成的分层。掌握服务是如何实现的这类复杂概念有助于分析或创建更高级的集成。本节会进行一些深挖。
Endpoint
一些应用(以及系统本身)希望能够在使用集群IP的情况下使用服务。这通过另一种对象实现,称为Endpoint(端点)对象。Kubernetes 会为每个Service对象创建一个包含该服务IP地址的端点对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$ kubectl describe endpoints alpaca-prod Name: alpaca-prod Namespace: default Labels: app=alpaca Subsets: Addresses: 10.112.1.54,10.112.2.84,10.112.2.85 NotReadyAddresses: <none> Ports: Name Port Protocol ---- ---- -------- <unset> 8080 TCP No events. |
要使用服务,高级应用可直接与Kubernetes API对话查找端点并进行调用。Kubernetes API甚至可以监听对象,在其发生变化时收到通知。这样客户端在服务关联的IP发生改变时可心立即响应。
我们在终端窗口中进行演示,使用如下命令启动、保持运行:
1 |
$ kubectl get endpoints alpaca-prod --watch |
它会输出端点的当前状态然后挂起:
1 2 |
NAME ENDPOINTS AGE alpaca-prod 10.112.1.54:8080,10.112.2.84:8080,10.112.2.85:8080 1m |
这时再打开一个终端窗口,删除并重建alpaca-prod
部署:
1 2 3 4 5 |
$ kubectl delete deployment alpaca-prod $ kubectl create deployment alpaca-prod \ --image=gcr.io/kuar-demo/kuard-amd64:blue \ --port=8080 $ kubectl scale deployment alpaca-prod --replicas=3 |
查看监听端点的输出,会看到在删除、重建这些Pod后,会看到命令的输出中反映了最新与服务关联的IP地址。看到的输出类似下面这样:
1 2 3 4 5 6 7 |
NAME ENDPOINTS AGE alpaca-prod 10.112.1.54:8080,10.112.2.84:8080,10.112.2.85:8080 1m alpaca-prod 10.112.1.54:8080,10.112.2.84:8080 1m alpaca-prod <none> 1m alpaca-prod 10.112.2.90:8080 1m alpaca-prod 10.112.1.57:8080,10.112.2.90:8080 1m alpaca-prod 10.112.0.28:8080,10.112.1.57:8080,10.112.2.90:8080 1m |
端点对象对于从头编写运行于Kubernetes上的新代码非常棒。但大部分项目都不是这样。大部分现有系统构建时都是使用固定IP地址并不太会发生变化。
手动服务发现
Kubernetes服务的构建基于Pod标签选择器。这意味着可以使用Kubernetes API完成一些基础的服务发现,完全不使用Service对象。我们来演示一下。
借助kubectl
(及相关API),我们可以轻松查看示例部署中各个Pod所分配的IP地址:
1 2 3 4 5 6 7 8 |
$ kubectl get pods -o wide --show-labels NAME ... IP ... LABELS alpaca-prod-12334-87f8h ... 10.112.1.54 ... app=alpaca alpaca-prod-12334-jssmh ... 10.112.2.84 ... app=alpaca alpaca-prod-12334-tjp56 ... 10.112.2.85 ... app=alpaca bandicoot-prod-5678-sbxzl ... 10.112.1.55 ... app=bandicoot bandicoot-prod-5678-x0dh8 ... 10.112.2.86 ... app=bandicoot |
这样很好,但在有成千上万个Pod时该怎么办呢?可以通过部分部署的标签来进行过滤。因此可以使用如下命令过滤出alpaca
应用
1 2 3 4 5 6 |
$ kubectl get pods -o wide --selector=app=alpaca-prod NAME ... IP ... alpaca-prod-3408831585-bpzdz ... 10.112.1.54 ... alpaca-prod-3408831585-kncwt ... 10.112.2.84 ... alpaca-prod-3408831585-l9fsq ... 10.112.2.85 ... |
此时就拥有了基础的服务发现。可以使用标签来标识出一组想查看的Pod,获取这些标签的所有Pod并挖出相应的IP地址。但同步保持正确的标签集合不太容易。这也是创建服务对象的原因。
kube-proxy和集群IP
集群IP是一些稳定的虚拟IP,可在服务的所有端点间进行流量负载均衡。这一神奇功能由运行集群中每个节点上的kube-proxy
实现。
上图中,kube-proxy
通过API服务端监听新服务。然后在该主机的内核中配置了一系列的iptables
规则,重写包的目的地,从而定向至服务的一个端点上。如果服务的端点发生了改变(因Pod的创建和删除或是就绪检测失败),iptables
规则也会重写。
集群IP本身通常在服务创建时由API服务端分配。但在创建服务时,用户可以指定具体的集群IP。设置后,除非删除或重建服务对象集群IP保持不变。
注:Kubernetes的服务地址使用kube-apiserver
的--service-cluster-ip-range
参数进行配置。服务地址的范围不应与IP子网或是分配给Docker网桥及Kubernetes节点的IP范围重合。此外,显式请求的集群IP应处于该范围中并且未使用。
集群IP环境变量
大部分用户应使用DNS服务查找集群IP,有些更老的机制可能仍在使用。其中之一是在Pod启动时为其注入一组环境变量。
要实际查看,可以看kuard
实例bandicoot
的控制台。在终端中输入如下命令:
1 2 |
$ BANDICOOT_POD=$(kubectl get pods -l app=bandicoot-prod -o jsonpath='{.items[0].metadata.name}') $ kubectl port-forward $BANDICOOT_POD 48858:8080 |
在浏览器中访问http://localhost:48858,查看服务的状态页。打开Server Env页可以看到alpaca
服务的一组环境变量。状态页的内容类似下表:
Key | Value |
---|---|
ALPACA_PROD_PORT | tcp://10.43.174.153:8080 |
ALPACA_PROD_PORT_8080_TCP | tcp://10.43.174.153:8080 |
ALPACA_PROD_PORT_8080_TCP_ADDR | 10.43.174.153 |
ALPACA_PROD_PORT_8080_TCP_PORT | 8080 |
ALPACA_PROD_PORT_8080_TCP_PROTO | tcp |
ALPACA_PROD_SERVICE_HOST | 10.43.174.153 |
ALPACA_PROD_SERVICE_PORT | 8080 |
所使用的两个主要变量是ALPACA_PROD_SERVICE_HOST
和ALPACA_PROD_SERVICE_PORT
。其它的环境变量是为兼容Docker连接变量而创建的(当前已弃用)。
环境变量的问题是它要求资源按指定的顺序创建。服务要在引用它的Pod之前完成创建。这对于部署一组构成大型应用的服务时会带来复杂性。此外,只使用环境变量会让很多用户不适应。因此,DNS可能是更好的选择。
与其它环境连接
虽然在自己的集群中有服务发现很不错,但真实世界中的应用要求集成将Kubernetes中部署的云原生应用与更古老环境部署的应用相集成。另外,可能会需要将云端的Kubernetes 集群与本地部署的基础设施相互集成。这一领域Kubernetes正在进行大量的解决方案探索和开发。
连接集群外资源
在Kubernetes连接集群外老资源时,可以使用无选择器服务声明一个手动分配集群外IP地址的Kubernetes服务。这样,通过DNS的Kubernetes服务发现如预想一样生效,但网络流量会流向外部资源。要创建无选择器服务,从资源中删除spec.selector
字段,metadata
和ports
保持不变。因为服务没有选择器,就不会对服务自动添加端点。这意味着必须进行手动添加。通常要添加的端点是固定IP地址(如数据库服务器的IP地址),所以只需添加一次。但如果服务的IP地址发生了变化,则需要更新相应的端点资源。要创建或更新端点资源,使用类似如下的端点:
1 2 3 4 5 6 7 8 9 10 11 12 |
apiVersion: v1 kind: Endpoints metadata: # This name must match the name of your service name: my-database-server subsets: - addresses: # Replace this IP with the real IP of your server - ip: 1.2.3.4 ports: # Replace this port with the port(s) you want to expose - port: 1433 |
外部资源连接集群内服务
外部资源连接Kubernetes服务就略有些麻烦了。如果云服务商支持的话,最简单的是使用前面描述过的内部负载均衡,它位于虚拟私有网络中,可将来自固定IP地址的流量分发到集群中。然后可以使用传统的DNS服务来让这个IP地址对外部资源可访问。如果没有内部负载均衡,可以使用NodePort
服务将服务暴露于集群中节点的IP地址之上。然后要么让物理负载均衡器将流量发往这些节点,要么使用DNS负载均衡在节点间分发流量。
如果这些解决方案都无法使用,更为复杂的选择中在外部资源中运行完整的kube-proxy
,编程让该机器使用Kubernetes集群中的DNS服务。这种配置更复杂,应当仅有自有环境中使用。不家一些开源项目(如HashiCorp的Consul),可用于管理集群内和集群外资源的连接。这些选择要求对网络和Kubernetes都有比较好的理解,才能正确使用,应当在没有更好的方案再选择。
清理
运行如下命令清理本章中所创建的所有对象:
1 |
$ kubectl delete services,deployments -l app |
小结
Kubernetes是一套挑战传统命名方法和网络中服务连接的动态系统。Service对象提供了在集群及之外暴露服务的灵活且强大的方式。通过本章所学的技术,读者可以进行服务间的连接并暴露到集群外。
Kubernetes的动态服务发现机制引入了一些新概念,初看起来比较复杂,掌握、适应这些技术是解锁Kubernetes强大功能的关键。一旦应用可动态查找服务并响应应用的动态位置,就不必担心所运行的服务位于哪里以及何时迁移。理性思考服务并让Kubernetes处理容器放置的细节是重要的一环。
当然,服务发现只是应用网络与Kubernetes协作的开始。使用 Ingress 做 HTTP 负载均衡一章会讲解Ingress网络,它是7层(HTTP)负载均衡和路由,Pods 安全一章中会涉及服务网格,这种后来开发一种方法,用于为云原生网络提供服务发现和负载均衡之外的能力。