Service 的作用

参考链接

虽然每个Pod都有自己的IP地址,但即使这些IP地址不能长期保持稳定。这导致了一个问题:如果一些Pod(称为它们的后端)为Kubernetes集群内的其他Pod(我们称之为前端)提供了功能,那么这些前端如何发现并跟踪哪些后端位于该集合中?

通过Service。

Kubernetes的 service是一个抽象概念,它定义了Pod的逻辑集合以及访问它们的策略 - 有时称为微服务。service所针对的Pod集(通常)由标签选择器决定(请参阅下面为什么您可能需要没有选择器的服务)。

举一个例子,考虑一个运行3个副本的应用处理后端。这些副本是可替代的 - 前端不关心他们使用的后端。虽然构成后端集合的实际Pod可能会发生变化,但前端客户端不需要知道该事件,也不需要跟踪后端列表本身。服务抽象使这种解耦成为可能。

对于Kubernetes原生应用程序,Kubernetes提供了一个简单的Endpoints API,只要服务中的Pod集合发生更改,它就会更新。对于非本机应用程序,Kubernetes提供了一个基于虚拟IP的网桥,用于重定向到后端Pod的服务。

定义Service

Serive 可以通过两种方式定义,yaml 文件方式和使用命令行创建的方式。

使用yaml方式

使用yaml文件定义个service:

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

此规范将创建一个名为“my-service”的新服务对象,该服务对象将任何Pod上的TCP端口9376以“app = MyApp”标签作为目标。 该服务还将被分配一个IP地址(有时称为“群集IP”),服务代理使用该地址(见下文)。 服务的选择器将被连续评估,结果将被发送到一个名为“my-service”的端点对象。

请注意,服务可以将传入端口映射到任何目标端口。 默认情况下,targetPort将设置为与端口字段相同的值。 也许更有趣的是,targetPort可以是一个字符串,指的是后端Pod中端口的名称。 分配给该名称的实际端口号可以在每个后端Pod中不同。 这为部署和发展您的服务提供了很大的灵活性。 例如,您可以在不中断客户端的情况下,更改后续版本后端软件中的端口号。 Kubernetes 的 Services 支持TCP和UDP协议,默认支持TCP。

使用命令方式

使用kubectl expose命令也可以创建一个Services:

 kubectl  expose deployment php-apache

创建不带标签选择器的service

我们有时候也可以定义一个不带标签选择器的Service,即无法选择后端的Pod,系统不会自动创建Endpoint,当要正式使用的时候再手动创建一个和该Service同名的Endpoint,用于指向实际的后端访问地址。 这种方式一般有如下的应用场景:

  • 在生产环境中,需要连接一个外部的数据库,但是在测试的阶段,我们会使用自己的测试数据库。
  • 需要将service指向其他namespace的服务或外部集群中。
  • 您正在将工作负载迁移到Kubernetes,而您的一些后端运行在Kubernetes之外。

这样定义一个不含选择器标签的service:

kind: Service
apiVersion: v1
metadata:
  name: my-service
spec:
  ports:
  - protocol: TCP
    port: 80                       # services 的端口
    targetPort: 9376               # endpoint 的端口

在使用的时候还需要手动创建一个同名的Endpoint:

kind: Endpoints
apiVersion: v1
metadata:
  name: my-service
subsets:
  - addresses:
      - ip: 1.2.3.4       # 外部服务的IP和端口,这里相当于Pod 的IP,只不过是外部的一个不变的地址
    ports:
      - port: 9376

虚拟IP和服务代理

Kubernetes群集中的每个节点都运行一个kube-proxy。 kube-proxy负责为ExternalName以外的其他类型的服务提供一个虚拟IP。 在Kubernetes v1.0中,服务是“第4层”(TCP / UDP over IP)构造,代理纯粹在用户空间中。 在Kubernetes v1.1中,添加了(测试版)Ingress API以表示“第7层”(HTTP)服务,还添加了iptables代理,并成为自Kubernetes v1.2以来的默认操作模式。 在Kubernetes v1.8.0-beta.0中,添加了ipvs代理。

用户空间代理模式

在此模式下,kube-proxy监视Kubernetes主服务器以添加和删除Service和Endpoints对象。 对于每个服务,它在本地节点上打开一个端口(随机选择)。 与此“代理端口”的任何连接都将代理到Service的后端Pod之一(如端点中所报告)。 根据服务的SessionAffinity决定使用哪个后端Pod。 最后,它安装iptables规则,捕获流量到服务的clusterIP(虚拟)和端口,并将该流量重定向到代理后端Pod的代理端口。 默认情况下,后端的选择是轮询模式。

image

iptables 代理模式

在此模式下,kube-proxy监视Kubernetes主服务器以添加和删除Service和Endpoints对象。 对于每个服务,它都会安装iptables规则,这些规则将流量捕获到服务的clusterIP(这是虚拟的)和端口,并将该流量重定向到服务的后端集合之一。 对于每个Endpoints对象,它都会安装选择后端Pod的iptables规则。 默认情况下,后端的选择是随机的。

显然,iptables不需要在用户空间和内核空间之间切换,它应该比用户空间代理更快,更可靠。 但是,与用户空间连接器不同,如果iptables连接器最初选择的连接器不响应,iptables连接器不能自动重试另一个连接,因此它依赖于正在工作的准备就绪探测器。

image

IPVS代理模式

在这种模式下,Kubernetes Services和Endpoints调用netlink接口来相应地创建ipvs规则,并定期与Kubernetes Services和Endpoints同步ipvs规则,以确保ipvs状态与预期一致。 当访问服务时,流量将被重定向到其中一个后端Pod。

与iptables类似,Ipvs基于netfilter钩子函数,但使用散列表作为基础数据结构并在内核空间中工作。 这意味着ipvs可以更快地重定向流量,并且在同步代理规则时具有更好的性能。 此外,ipvs为负载均衡算法提供了更多选项,例如:

  • rr:轮询
  • lc:最少连接
  • dh:目标哈希
  • sh:源哈希
  • sed:预计的最短延迟
  • nq:从不排队

在运行kube-proxy之前,ipvs模式假定在节点上安装了IPVS内核模块。 当kube-proxy以ipvs代理模式启动时,kube-proxy会验证节点上是否安装了IPVS模块,如果未安装,kube-proxy将回退到iptables代理模式。

image

在任何这些代理模型中,为服务的IP:端口绑定的任何流量都会代理到适当的后端,而客户端不知道任何关于Kubernetes或服务或Pod的信息。 通过将service.spec.sessionAffinity设置为“ClientIP”(缺省值为“None”),可以选择基于Client-IP的会话关联,并且可以通过设置字段service.spec.sessionAffinityConfig.clientIP来设置最大会话粘滞时间。 timeoutSeconds如果您已经将service.spec.sessionAffinity设置为“ClientIP”(默认值为“10800”)

多端口service

有时一个容器也可以映射多个端口服务,在service的定义中也可以相应的设置多端口的对应到多个应用服务器上:

kind: Service
apiVersion: v1
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

在映射多个端口的时候,需要给每一个端口指定名称。

服务发现机制

在Kubernetes 集群中是如何进行服务发现的呢? Kubernetes为我们提供了两种方式:

  • 环境变量
  • DNS

环境变量

当 Pod 运行在 Node 上,kubelet 会为每个活跃的 Service 添加一组环境变量。 它同时支持 Docker links兼容 变量(查看 makeLinkVariables)、简单的 {SVCNAME}_SERVICE_HOST 和 {SVCNAME}_SERVICE_PORT 变量,这里 Service 的名称需大写,横线被转换成下划线。

举个例子,一个名称为 "redis-master" 的 Service 暴露了 TCP 端口 6379,同时给它分配了 Cluster IP 地址 10.0.0.11,这个 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

一个可选(尽管强烈推荐)集群插件 是 DNS 服务器。 DNS 服务器监视着创建新 Service 的 Kubernetes API,从而为每一个 Service 创建一组 DNS 记录。 如果整个集群的 DNS 一直被启用,那么所有的 Pod 应该能够自动对 Service 进行名称解析。

例如,有一个名称为 "my-service" 的 Service,它在 Kubernetes 集群中名为 "my-ns" 的 Namespace 中,为 "my-service.my-ns" 创建了一条 DNS 记录。 在名称为 "my-ns" 的 Namespace 中的 Pod 应该能够简单地通过名称查询找到 "my-service"。 在另一个 Namespace 中的 Pod 必须限定名称为 "my-service.my-ns"。 这些名称查询的结果是 Cluster IP。

Kubernetes 也支持对端口名称的 DNS SRV(Service)记录。 如果名称为 "my-service.my-ns" 的 Service 有一个名为 "http" 的 TCP 端口,可以对 "_http._tcp.my-service.my-ns" 执行 DNS SRV 查询,得到 "http" 的端口号。

Kubernetes DNS 服务器是唯一的一种能够访问 ExternalName 类型的 Service 的方式。 更多信息可以查看DNS Pod 和 Service。

Headless Service

有时不需要或不想要负载均衡,以及单独的 Service IP。 遇到这种情况,可以通过指定 Cluster IP(spec.clusterIP)的值为 "None" 来创建 Headless Service。

这个选项允许开发人员自由寻找他们自己的方式,从而降低与 Kubernetes 系统的耦合性。 应用仍然可以使用一种自注册的模式和适配器,对其它需要发现机制的系统能够很容易地基于这个 API 来构建。

对这类 Service 并不会分配 Cluster IP,kube-proxy 不会处理它们,而且平台也不会为它们进行负载均衡和路由。 DNS是否能够自动配置,依赖于 Service 是否定义了 selector。

带Selectors 的Headless Service

如果在Services定义了一个Selectors, K8S将会为每个Pod创建一个Endpoint,并配置到DNS,访问此Service时,会将Pod对应的所有A记录地址返回。 这对于分布式的集群创建非常有用,可以通过使用这种方式获得集群的列表。

定义:

cat nginx-headless-service.yaml 

kind: Service
apiVersion: v1
metadata:
  name: nginx-service
  labels:
    app: nginx-service
spec:
  selector:
    app: nginx
  clusterIP: None
  ports:
  - protocol: TCP
    port: 80

创建一个nginx 的deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.10.3
        ports:
        - containerPort: 80

查看对应的serivce和pod 信息:

# kubectl  get svc nginx-service
NAME            TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
nginx-service   ClusterIP   None         <none>        80/TCP    27m

# kubectl  get pod -o wide |grep nginx
nginx-deployment-75d56bb955-pw7mm   1/1       Running   0          40m       10.2.61.9   10.0.0.2
nginx-deployment-75d56bb955-qprjw   1/1       Running   0          40m       10.2.39.3   10.0.0.3
nginx-deployment-75d56bb955-xfgbk   1/1       Running   0          40m       10.2.39.2   10.0.0.3


在pod中访问此服务,可以发现DNS上直接绑定了Pod的IP列表,不再绑定Cluster IP:

# kubectl  run  busybox --image=busybox -it sh  --rm

/ # nslookup nginx-service
Server:    10.1.0.100
Address 1: 10.1.0.100 coredns.kube-system.svc.cluster.local

Name:      nginx-service
Address 1: 10.2.39.2 10-2-39-2.nginx-service.default.svc.cluster.local
Address 2: 10.2.39.3 10-2-39-3.nginx-service.default.svc.cluster.local
Address 3: 10.2.61.9 10-2-61-9.nginx-service.default.svc.cluster.local

使用headless service 和 Stateful Sets 部署Cassandra分布式集群实例参考此链接:https://kubernetes.io/docs/tutorials/stateful-application/cassandra/

发布服务-集群外部访问Pod或Service

在kubernetes中,发布服务有有如下几种方式:

  • ClusterIP:通过集群的内部 IP 暴露服务,选择该值,服务只能够在集群内部可以访问,这也是默认的 ServiceType。
  • NodePort:通过每个 Node 上的 IP 和静态端口(NodePort)暴露服务。NodePort 服务会路由到 ClusterIP 服务,这个 ClusterIP 服务会自动创建。通过请求 <NodeIP>:<NodePort>,可以从集群的外部访问一个 NodePort 服务。
  • LoadBalancer:使用云提供商的负载局衡器,可以向外部暴露服务。外部的负载均衡器可以路由到 NodePort 服务和 ClusterIP 服务。
  • ExternalName:通过返回 CNAME 和它的值,可以将服务映射到 externalName 字段的内容(例如, foo.bar.example.com)。 没有任何类型代理被创建,这只有 Kubernetes 1.7 或更高版本的 kube-dns 才支持。
  • 如果是实现外部访问内部服务,还可以将容器的端口映射到宿主机上。

将容器端口映射到宿主机

通过设置hostPort来设置容器端口到物理机:

apiVersion: v1
kind: Pod
metadata: 
  name: redis-php
  labels:
    name: redis-php
spec:
  hostNetwork: true                        # 指定可以通过宿主机访问pod中的服务
  containers:
  - name: frontend
    image: kubeguide/guestbook-php-frontend:localredis
    ports:
    - containerPort: 80
    # 指定宿主机映射端口。在不与hostNetwork: true 同时使用时可以指定任意端口,但是在某些使用CNI插件的情况下可能不会生效。
    # 与hostNetwork使用的时候,只能与容器端口一致,且可以省略,一般只在测试时使用。
      hostPort: 80                          
  - name: redis
    image: kubeguide/redis-master
    ports:
    - containerPort: 6379
      hostPort: 6379


使用nodePort将Service的端口映射到物理机

如果设置 type 的值为 "NodePort",Kubernetes master 将从给定的配置范围内(默认:30000-32767)分配端口,每个 Node 将从该端口(每个 Node 上的同一端口)代理到 Service。配置如下参数指定端口范围:

# grep -ir "20000-40000" /usr/lib/systemd/system/

/usr/lib/systemd/system/kube-apiserver.service:  --service-node-port-range=20000-40000 \

该端口将通过 Service 的 spec.ports[*].nodePort 字段被指定。

如果需要指定的端口号,可以配置 nodePort 的值,系统将分配这个端口,否则调用 API 将会失败(比如,需要关心端口冲突的可能性)。并且指定的端口要在配置文件指定的端口范围内。 从kubernetes 1.10开始,支持指定IP, 通过如下参数:

--nodeport-addresses=127.0.0.0/8

也可以支持指定多个IP段,用逗号分隔的IP块列表(例如10.0.0.0/8,1.2.3.4/32)用于过滤本节点的地址。例如,如果您使用标志--nodeport-addresses = 127.0.0.0 / 8启动kube-proxy,则kube-proxy将仅为NodePort服务选择环回接口。 --nodeport地址默认为空([]),这意味着选择所有可用的接口并符合当前的NodePort行为。

这可以让开发人员自由地安装他们自己的负载均衡器,并配置 Kubernetes 不能完全支持的环境参数,或者直接暴露一个或多个 Node 的 IP 地址。

需要注意的是,Service 将能够通过 <NodeIP>:spec.ports[].nodePort 和 spec.clusterIp:spec.ports[].port 而对外可见。

定义示例:

apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  type: NodePort
  selector:
    app: nginx
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
    nodePort: 28888

创建此service后,所有安装有kube-proxy的节点上都会映射28888的端口,供外部访问。