Kubernetes Service

这段时间项目切换新的PaaS平台,在新的架构中需要使用Service,借此机会认真学习了一下Service概念,本文大多是官方文档的翻译,如有不妥之处还请多多指教。

Kubernetes的Pod是会死亡的。Pod出生、死亡,但是Pod不能复活。 ReplicaSet 动态地创建和销毁Pods(例如,在扩缩容的时候)。虽然每个Pod 都会得到一个IP地址,但是随着时间的推移,这些IP地址也是不稳定的。这导致了一个问题:在Kubernetes集群内有一组Pods(称为后端)向其他Pods(称为前端) 提供功能,那前端如何找出并跟踪该集合中的后端?

Kubernetes中的Service 可以解决这个问题。

Kubernetes中的Service 是一个抽象,有时候称为微服务,它定义了一组逻辑的Pods和访问的策略。Service 目标后端Pod 使用Label Selector 选择。

服务定义


在Kubernetes中,ServicePod一样都是REST对象。和其他REST对象一样,可以向apiserver发送Service的定义来创建一个新Service的实例。举个例子:假设你有一组拥有标签 "app=MyApp" 对外暴露9376端口的Pods,你可以使用下面的yaml定义一个Service

apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
  - protocol: TCP
    port: 80
    targetPort: 9376

这个定义会创建一个新的名叫"my-service"的Service对象,该Service指向所有拥有 "app=MyApp" 标签的Pod的9376端口。Kubernetes会给Service分配一个IP地址,通常称为”cluset IP“,服务代理会使用这个IP地址。服务的选择器将会持续的评估,并将结果发布到名为”my-service“的Endpoints对象。

Service可以将任何接入端口映射为targetPort 。默认targetPort的值和port一致,更有意思的是,targetPort可以是一个字符串,字符串指向后端Pods中的端口名称。每个后端Pod真正赋给端口名字的端口可能不同。这给Service的部署和演进提供了很大的灵活性。例如,你可以在下个版本中更改后端pod对外暴露的端口,而这不会影响客户端。

Service默认使用TCP协议,也可以使用任何支持的协议(TCP、UDP、HTTP、PROXY protocol和SCTP)。由于许多服务都需要对外暴露多个端口,因此kubernetes支持在Service对象上定义多个端口。每个端口都可以使用相同或不同的端口。

无selector的Services

Service通常是Kubernetes Pod访问的抽象,当然也可以是其他后端。例如:

  • 生产环境使用外部数据库,测试环境使用本地数据库。
  • Service可以可以访问其它 namespace或其他集群的Service
  • 正在将负载迁移到Kubernetes,但是有一些后端仍然运行在集群外。

以上的任何场景,都可以定义一个没有选择器的Service

apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  ports:
  - protocol: TCP
    port: 80
    targetPort: 9376

因为这个Service没有任何选择器,因此相关联的端点对象也不会被创建。你可以将服务手动映射到特殊的端点:

apiVersion: v1
kind: Endpoints
metadata:
  name: my-service
subsets:
  - addresses:
      - ip: 1.2.3.4
    ports:
      - port: 9376

注意: 端点的地址不能是环回地址(127.0.0.0/8),链路本地地址(169.254.0.0/16)或者链路本地多播地址(224.0.0.0/24)。因为kube-proxy 不支持目的地址为虚拟IP,因此这些地址不能是其他Kubernetes Service的cluster ip。

没有选择器服务的访问和有选择器的服务一样。流量最终会被路由到用户定义的端点(在这个例子中,流量最终会转发到1.2.3.4:9376)。ExternalName服务是一种特殊的服务,它没有选择器而是使用DNS名称。

虚拟IPs和服务代理

Kubernetes集群中的每个节点都运行一个kube-proxykube-proxy负责为ExternalName以外类型的Service实现某种形式的虚拟IP。
Service在Kubernetes中的简单里程碑:

功能 Kubernetes版本
Service 四层结构(用户态) v1.0
Ingress API七层结构(HTTP) v1.1
iptables 代理 v1.1
Ingress+iptables proxy 默认工作模式 v1.2
ipvs proxy v1.8.0-beta.0

Proxy-mode: userspace

在这种模式下,kube-proxy会监视Kubernetes的主以获取ServiceEndpoints对象的创建和删除。对于每一个Servicekube-proxy会在本地节点打开一个随机端口。和该端口建立的任何连接都会被代理到Service的一个后端Pod(通常称为端点),使用哪个后端PodServiceSessionAffinit决定。最后,kube-proxy会安装能够捕获到Service clusterIP和端口流量的iptables,并将流量转发到后端Pod的代理端口。默认后端Pod的选择使用轮训算法。

用户空间Service视图

Proxy-mode: iptables

在这种模式下,kube-proxy会监视Kubernetes的主以获取ServiceEndpoints对象的创建和删除。对于每一个Servicekube-proxy会安装可以捕获到Service的clusterIP(虚拟IP)和端口的iptables,并将流量转发到Service后端集合中的一个。对于每一个Endpoints对象,kube-proxy安装可以选择后端Pod的iptables。默认,后端的选择使用轮询算法。

显然,iptables不需要在用户空间和内核空间之间来回切换,比用户空间的代理更加快和可靠。但是,和用户空间代理不同,基于iptables的代理在首次选择的Pod没有响应时不会进行重试,因此基于iptables的代理依赖readiness probes。

基于iptables的service视图

Proxy-mode: ipvs

特性状态: Kubernetes v1.11 stable

在这种模式下,kube-proxy监听Kubernetes的ServiceEndpoints,调用netlink 接口创建ipvs规则,定期和Kubernetes ServiceEndponts同步ipvs规则来确保ipvs的状态满足期望。当访问服务时,流量被重定向到一个后端Pod

与iptables类似,Ipvs基于netfilter钩子函数,但使用哈希表作为底层数据结构并在内核空间中工作。这意味着ipvs重定向流量更快,同步代理规则也更加高效。此外,ipvs提供了更多的负载均衡算法选择,比如:

  • rr: round-robin 轮询

  • lc: least connection 最小连接

  • dh: destination hashing 目的地址哈希

  • sh: source hashing 源地址哈希

  • sed: shortest expected delay 最小期望延迟

  • nq: never queue 从不排队

注意: ipvs模式假定在运行kube-proxy之前节点已经安装了IPVS内核模块。当kube-proxy以ipvs代理模式启动,如果节点上已经安装IPVS模块,kube-proxy将会生效。如果没有安装IPVS内核模块,kube-proxy会使用iptables 代理模式。

基于ipvs的service视图

所有的代理模式,任何访问Service‘s IP:port的流量都会在客户端不了解Kuberntes或ServicePods的情况下被代理到合适的后端。可以通过将service.spec.sessionAffinity设置为“ClientIP”(默认为“None”)来选择基于客户端IP的会话亲和关系,通过 service.spec.sessionAffinityConfig.clientIP.timeoutSeconds 可以设置会话保持时间,前提是你已经配置service.spec.sessionAffinity` = “ClientIP”(默认值为“10800").

Proxy-mode: iptables下的规则

Service和Pod基本信息:

Service

iore-0c08c3c7        10.247.42.16     192.168.0.226   80/TCP                7s

Pod

iore-0c08c3c7-2398027392-kq47k               1/1       Running   0          25m       172.16.12.2     number07-192.168.0.168
iore-0c08c3c7-2398027392-z28ph               1/1       Running   1          25m       172.16.8.2      number05-192.168.0.171

获取节点iptables:

登陆任意节点,执行以下命令获取该节点所有的iptables:

iptables -L -v -n -t nat

在所有的iptables中找到cluster ip:10.247.42.16,发现cluster ip 出现在一个iptable chain中,

Chain KUBE-SERVICES (2 references)
    0     0 KUBE-SVC-RQXSPBFOTWEEDJ7V  tcp  --  *      *       0.0.0.0/0            192.168.0.226        /* manage/ioom-0c08c3c7: external IP */ tcp dpt:13888 ADDRTYPE match dst-type LOCAL
    0     0 KUBE-MARK-MASQ  tcp  --  *      *       0.0.0.0/0            10.247.42.16         /* manage/iore-0c08c3c7: cluster IP */ tcp dpt:80
    0     0 KUBE-SVC-QVYVXAABCXVAUXHW  tcp  --  *      *       0.0.0.0/0            10.247.42.16         /* manage/iore-0c08c3c7: cluster IP */ tcp dpt:80

但是访问10.247.42.16:13888的包是如何被转发到实际的后端pod的呢,为了搞清楚转发规则,需要查看以下Chain KUBE-SVC-QVYVXAABCXVAUXHW,在iptables中找到chain,具体内容如下:

Chain KUBE-SVC-QVYVXAABCXVAUXHW (3 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 KUBE-SEP-IBOMAIMAIT43CTBV  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* manage/iore-0c08c3c7: */ statistic mode random probability 0.50000000000
    0     0 KUBE-SEP-HSPFAMPI5V5GWUV5  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* manage/iore-0c08c3c7: */

这个Chain一共包含2个chain,流量以0.50000000000的概率向Chain KUBE-SEP-IBOMAIMAIT43CTBV转发流量,Chain KUBE-SEP-IBOMAIMAIT43CTBV的详细信息如下:

Chain KUBE-SEP-IBOMAIMAIT43CTBV (1 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 KUBE-MARK-MASQ  all  --  *      *       172.16.12.2          0.0.0.0/0            /* manage/iore-0c08c3c7: */
    0     0 DNAT       tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            /* manage/iore-0c08c3c7: */ tcp to:172.16.12.2:8000

经过两次转发,流量即可以被正确转发到我们期望的后端。

多端口 Services

许多Services 需要暴露不止一个端口。因为这个原因,kubernetes支持在Service对象上定义多个端口。当你使用多个端口时,每个端口必须都赋一个名称,这样可以消除端点歧义。一个多端口Service 的例子如下:

    apiVersion: v1
    kind: Service
    metadata:
      name: my-service
    spec:
      selector:
        app: MyApp
      ports:
      - name: http
        protocol: TCP
        port: 80
        targetPort: 9376
      - name: https
        protocol: TCP
        port: 443
        targetPort: 9377

注意: 端口名字仅支持小写字母、数字和- ,必须以小写字姆或数字开头和结尾。123-abc 和 web 都是合法的端口名,但是123_abc 和-web 是非法的端口名。

选择自己的 IP 地址

创建Service 时可以指定自己的cluster IP地址。为此需要设置 .spec.clusterIP 字段。例如,已经有一个DNS条目并想重用,或者在老系统配置了一个特定的IP地址并且重新配置该地址十分困难。用户自己选的IP地址必须是一个合法的IP地址而且要在 service-cluster-ip-range 定义的网段范围内,如果配置的IP地址不合法,apiserver将会返回422状态码以表明该地址非法。如果使用kubectl expose 命令导出一个服务,可以通过--cluster-ip 来设置cluster IP。

为什么不适用轮询 DNS?

一个时不时出现的问题是我们为什么用虚拟IP做这些事情而不仅仅是使用标准的轮询DNS。下面是一些原因:

  • DNS 库不支持DNS TTLS,不缓存名字查找的结果。
  • 许多应用使用DNS查询一次,然后缓存DNS查询结果。
  • 即使应用程序和库进行了适当的重新解析,每个客户端反复重新解析DNS的负载也难以管理。

我们试图阻止用户做出伤害自己的事情。 也就是说,如果有足够的人要求这样做,我们可以将其作为替代方案来实施。

服务发现

Kubernetes支持通过环境变量和DNS两种模式来发现一个服务。

环境变量

Pod在节点运行,kubelet为每一个处于活跃状态的Service添加一组环境变量。同时支持 Docker links compatible变量和简单的以{SVCNAME}_SERVICE_HOST{SVCNAME}_SERVICE_PORT 格式命名的变量,其中服务名都是大写字母,.变转换为_

例如, "redis-master" 服务对外暴露TCP端口为6379,cluster ip 10.0.0.11,kubelet将为该Service生成以下环境变量:

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

基于环境变量的服务发现对顺序有要求,Pod访问的任何Service都要在Pod被创建之前创建,否则将不会生成环境变量。基于DNS的服务发现没有这个限制。

DNS

尽管Kubernetes强烈推荐使用DNS,但是DNS仍然是集群的可选项。DNS服务器监听Kubernetes 创建新Service,然后给每个Service创建一组DNS记录。如果在整个集群中启用了DNS,则所有Pods应该能够自动对“Service”进行名称解析。

举个例子,在 "my-ns" 命名空间内有一个叫 "my-service"Service ,DNS服务器会创建一个 "my-service.my-ns" 记录。在 "my-ns" 命名空间内的Pods可以通过 "my-service" 寻找到服务,不在该命名空间内的Pod需要使用全限定名 "my-service.my-ns"寻找服务。根据名字查找到的结果是cluster IP。

Kubernetes还支持命名端口的DNS SRV(服务)记录。如果服务 "my-service.my-ns" 有一个使用TCP命名为"http"的端口,你可以使用 "_http._tcp.my-service.my-ns" 进行DNS SRV查询。

使用Kubernetes DNS服务器是唯一可以访问 ExternalName类型服务的方式。

Headless services


有时您不需要也不想要负载均衡和单个服务IP。 在这种情况下,您可以通过为cluster IP(.pecpec.clusterIP)指定“None”来创建“无头”服务。

此选项允许开发人员通过自己的方式进行服务发现来减少与Kubernetes系统的耦合。 应用程序仍然可以使用自注册模式,并且可以轻松地在此API上构建适用于其他发现系统的适配器。

对于这些服务,系统不会分配cluster IP,kube-proxy也不处理这些服务,系统也不会提供负载均衡和代理。如何自动配置DNS取决于服务是否已定义选择器。

With selectors


对于定义了选择器的“”无头“”服务,端点控制器会在API中创建“端点”记录,并修改DNS配置以返回直接指向支持“服务”的“Pods”的记录(地址)。

Without selectors


对于没有定义选择器的无头服务,端点控制器不会创建“端点”记录。但是,DNS系统会查找和配置:

  • ExternalName 类型的服务使用CNNAME记录。
  • 所有其他类型的与服务共享名称的“端点”的记录。

发布服务 - 服务类型

对于应用的某些部分(比如前端),你可能希望将Service暴露在一个外部(集群外)IP地址上。Kubernetes的ServiceTypes允许你指定创建的Service类型,默认Service的类型为ClusterIPType 取值及对应的行为如下:

  • ClusterIP: Service暴露在一个集群内部的IP地址上。Service只能在集群内访问。

  • NodePort: 服务暴露在每个节点的静态端口上。可以通过 : 在集群外访问NodePort Service。

  • LoadBalancer:使用云提供商的负载均衡器在外部公开服务。

  • ExternalName: 将Service映射到外部服务,不设置任何代理,需要kubernetes1.7以上版本的kube-dns支持。

NodePort

如果你把type 字段设置为NodePort ,Kubernetes会从--service-node-port-range 指定的范围内分配一个端口(范围默认30000-32767), 每个节点都会将该端口(每个节点上的端口与该端口相同)代理到你的ServiceService 中的.spec.ports[*].nodePort 字段的值就是对应的端口。

如果你想指定代理端口的IP地址,可以将kube-proxy中的 --nodeport-addresses 设置为特定的IP地址块(该功能从Kubernetes v1.10版本开始支持)。以逗号分隔的IP块列表(例如10.0.0.0/8,1.2.3.4/32)用于过滤此节点的本地地址。例如,如果使用标志--nodeport-addresses = 127.0.0.0 / 8启动kube-proxy,则kube-proxy将仅为NodePort服务选择环回接口。--nodeport-addresses默认为空([]),这意味着选择所有可用的接口并符合当前的NodePort行为。

如果需要特定的端口号,可以在nodePort字段中指定一个值,系统将会分配该端口,否则API事务将失败(用户需要自己处理可能的端口冲突)端口的值必须在配置的端口范围内。

这使开发人员可以自由地设置自己的负载均衡器,配置Kubernetes不完全支持的环境,甚至直接暴露一个或多个节点的IP。

注意NodePort 类型的服务可以通过 :spec.ports[*].nodePort.spec.clusterIP:spec.ports[*].port访问(如果设置了kube-proxy--nodeport-addresses ,将会过滤节点的IP地址)

LoadBalancer

在支持外部负载均衡器的云提供商上,将type字段设置为LoadBalancer将为Service配置一个负载均衡器。负载平衡器的实际创建是异步发生的,有关配置的f负载均衡器的信息将发布在Service.status.loadBalancer字段中。 例如:

apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
  - protocol: TCP
    port: 80
    targetPort: 9376
  clusterIP: 10.0.171.239
  loadBalancerIP: 78.11.24.19
  type: LoadBalancer
status:
  loadBalancer:
    ingress:
    - ip: 146.148.47.155

来自外部负载均衡器的流量将指向后端“Pods”,但具体如何工作取决于云提供商。某些云提供商支持设置 loadBalancerIP 。在这种条件下负载均衡器将以用户指定的 loadBalancerIP 创建。如果未指定loadBalancerIP字段,则会给负载均衡器分配一个短暂的IP地址。如果设置了 loadBalancerIP 但是云提供商不支持这个特性,该字段将会被忽略。

ExternalName

ExternalName 类型的Service把一个service映射到一个DNS名,而不是典型的选择器,比如 my-servicecassandra。你可以通过 spec.externalName 参数设置这些服务。

下面示例的服务定义,将prod 命名空间内的 my-service Service映射到my.database.example.com

apiVersion: v1
kind: Service
metadata:
  name: my-service
  namespace: prod
spec:
  type: ExternalName
  externalName: my.database.example.com

注意: ExternalName接受IPv4地址字符串,但是作为由数字组成的DNS名称,而不是IP地址。类似于IPv4地址的ExternalNames不会被CoreDNS或ingress-nginx解析,因为ExternalName旨在指定规范的DNS名称. 要对IP地址进行硬编码,请考虑无头服务。

查找主机my-service.prod.svc.cluster.local时,集群DNS服务将返回值为“my.database.example.com”的“CNAME”记录。访问“my-service”的工作方式与其他服务的工作方式相同,但重要的区别在于重定向发生在DNS级别,而不是通过代理或转发。如果您以后决定将数据库移动到群集中,则可以启动其pod,添加适当的选择器或端点,并更改服务的“type”。

External IPs

如果有一个外部的IP地址路由到集群内的一个或多个节点,kubernetes的service可以暴露在这些 externalIPs 上。在服务端口上使用外部IP(作为目的IP)进入群集的流量将路由到其中一个服务端点。 externalIPs 的管理是系统管理员的责任而不是kubernetes集群的责任。

externalIPs 可以和多种Service配合,在这面的例子中,客户端可以通过 “80.11.12.10:80” 访问“my-service” :

apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
  - name: http
    protocol: TCP
    port: 80
    targetPort: 9376
  externalIPs:
  - 80.11.12.10

虚拟IP的"血腥"细节

对于大多数想使用Service的用户来说,前面的信息已经足够了。然而,幕后有很多可能值得理解的事情。

避免碰撞

Kubernetes的主要哲学之一是用户不应该暴露于可能导致他们的行为失败的情况,而这不是用户自己的过错。在这种情况下,如果用户选择的端口可能和其他用户冲突,那么用户不应该自己选择端口。 这是隔离失败。

为了允许用户为自己的Service选择端口,kubernetes必须确保没有两个Service发生碰撞。kubernetes通过为每个Service分配自己的IP地址来支持该功能。

为了确保每个Service得到唯一的IP地址,在创建每个Service之前,内部分配器以原子方式更新etcd中的全局分配集合。为了Service能够获取到IP地址,映射对象必须存在注册表中,否在创建Service将失败并显示返回无法分配IP地址的错误。有一个后台控制器负责创建该映射表(从内存锁定中使用的旧版Kubernetes迁移)以及检查由于管理员干预而导致的无效分配,并清除已分配但当前没有Service使用的任何IP。

IPs and VIPs


与实际路由到固定目的 Pod IP地址不同,Service的IP地址实际上并不会由单个主机应答。相反,我们使用iptables(Linux中的数据包处理逻辑)来定义根据需要透明重定向的虚拟IP地址。当客户端连接到虚拟IP地址时,其流量会自动传输到适当的端点。Service的环境变量和DNS实际上时根据Service的虚拟IP地址和端口填充的。
kubernetes支持三种代理模式:userspace,iptables和工作方式稍微不同的ipvs。

Userspace


创建后端Service时,Kubernetes给Service分配一个虚拟IP地址,比如IP地址为10.0.0.1 。假设Service的端口为1234,集群中所有的kube-proxy 实例都会观察到Service 。当一个kube-proxy 实例观察到Service时,kube-proxy 打开一个新的随机端口,并创建一个iptabels将虚拟机IP的端口重定向到这个新的端口,并在新的端口上接受连接。

当一个客户端连接到虚拟IP,iptables规则启动,并将数据包重定向到Service代理自己的端口。Service代理选择一个后端,然后将数据包从客户端代理到选择的后端。

这意味着Service的所有者可以任意选择Service端口而不会有端口冲突问题。客户端只要连接到Service的IP和端口,而不用关心Pod的实际访问端口是多少。

Iptables


创建后端Service时,Kubernetes给Service分配一个虚拟IP地址,比如IP地址为10.0.0.1 。假设Service的端口为1234,集群中所有的kube-proxy 实例都会观察到Service 。当代理看到一个新的Service时,代理会安装一系列的iptables规则-从VIP重定向到每个Service的规则。每个Service规则和每个Endpoints规则关联,每个Endpoint规则重定向到后端。

当一个客户端连接到虚拟IP,iptables规则启动。会使用session亲和性或随机算法选择一个后端,并将数据包重定向到该后端。和userspace proxy不同,数据包不会拷贝到用户空间。kube-proxy不必为VIP运行而运行,并且客户端IP不会被更改。

当流量通过节点端口或通过负载均衡器进入时,执行相同的基本流程,但在这些情况下,客户端IP确实会被更改。

Ipvs


在大规模集群中(比如有10000个Service)iptables的操作会显著降低。IPVS旨在实现负载平衡并基于内核中的哈希表。 因此,我们可以从基于IPVS的kube-proxy实现大量Service下的性能一致性。于此同时,基于IPVS的kube-proxy拥有更多更复杂的负载均衡算法。

参考链接

  • kubernetes.io
  • Netfilter-iptabes报文过滤框架(一)

你可能感兴趣的:(Kubernetes Service)