2.11 Service

2.11 Service #

2.11.1 什么是 Service #

Service 是集群内部的负载均衡机制,用来解决服务发现的关键问题。在 Kubernetes 集群里 Pod 的生命周期是比较 “短暂” 的,虽然 Deployment 和 DaemonSet 可以维持 Pod 总体数量的稳定,但在运行过程中,难免会有 Pod 销毁又重建,这就会导致 Pod 集合处于动态的变化之中。这种 “动态稳定” 对于现在流行的微服务架构来说是非常致命的,如果后台 Pod 的 IP 地址老是变来变去,客户端该怎么访问呢?

对于这种 “不稳定” 的后端服务问题,业内的解决方案是 “负载均衡”,典型的应用有 LVS、Nginx 等,它们在前端与后端之间加入了一个 “中间层”,屏蔽后端的变化,为前端提供一个稳定的服务。Service 的工作原理和 LVS、Nginx 差不多,Kubernetes 会给它分配一个静态 IP 地址,然后它再去自动管理、维护后面动态变化的 Pod 集合,当客户端访问 Service,它就根据某种策略,把流量转发给后面的某个 Pod。

LVS 即 Linux Virtual Server,是由章文嵩发起的一个开源项目,后来被集成进 Linux 内核。

Service 使用了 iptables 技术,每个节点上的 kube-proxy 组件自动维护 iptables 规则,客户不再关心 Pod 的具体地址,只要访问 Service 的固定 IP 地址,Service 就会根据 iptables 规则转发请求给它管理的多个 Pod,是典型的负载均衡架构。

iptables 基于 Linux 内核里的 netfilter 模块,用来处理网络数据包,实现修改、过滤、地址转换等功能。

Service 并不是只能使用 iptables 来实现负载均衡,它还有另外两种实现技术:性能更差的 userspace 和性能更好的 ipvs。

2.11.2 YAML 描述 Service #

Service YAML 描述样本可以使用命令 kubectl expose 进行创建。因为在 Kubernetes 里提供服务的是 Pod,而 Pod 又可以用 Deployment/DaemonSet 对象来部署,所以 kubectl expose 支持从多种对象创建服务,得先有对象,才有服务。Pod、Deployment、DaemonSet 都可以。

使用 kubectl expose 指令时还需要用参数 --port--target-port 分别指定映射端口和容器端口,而 Service 自己的 IP 地址和后端 Pod 的 IP 地址可以自动生成,用法上和 Docker 的命令行参数 -p 很类似。

比如,用以下的 YAML 描述生产 Deployment 对象:

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: ngx-dep
  name: ngx-dep

spec:
  replicas: 2
  selector:
    matchLabels:
      app: ngx-dep

  template:
    metadata:
      labels:
        app: ngx-dep
    spec:
      containers:
        - image: nginx:alpine
          name: nginx

用 ngx-dep 对象生成 Service,命令可以这么写:

export out="--dry-run=client -o yaml"
kubectl expose deploy ngx-dep --port=80 --target-port=80 $out

生成的 Service YAML 大致如下:

apiVersion: v1
kind: Service
metadata:
  name: ngx-svc

spec:
  selector:
    app: ngx-dep

  ports:
    - port: 80
      targetPort: 80
      protocol: TCP

selector 用来过滤出要代理的那些 Pod,因为已经指定了要代理 Deployment,所以 Kubernetes 就自动填上了 ngx-dep 的标签,会选择这个 Deployment 对象部署的所有 Pod。

ports 里面的三个字段分别表示外部端口、内部端口和使用的协议,在这里就是内外部都使用 80 端口,协议是 TCP。

2.11.3 使用 Service #

在使用 YAML 创建 Service 对象之前,可以先对 Deployment 做一点改造,方便观察 Service 的效果。

首先,创建一个 ConfigMap,定义一个 Nginx 的配置片段,它会输出服务器的地址、主机名、请求的 URI 等基本信息:

apiVersion: v1
kind: ConfigMap
metadata:
  name: ngx-conf

data:
  default.conf: |
    server {
      listen 80;
      location / {
        default_type text/plain;
        return 200
          'srv : $server_addr:$server_port\nhost: $hostname\nuri : $request_method $host $request_uri\ndate: $time_iso8601\n';
      }
    }    

然后在 Deployment 的 template.volumes 里定义存储卷,再用 volumeMounts 把配置文件加载进 Nginx 容器里:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ngx-dep

spec:
  replicas: 2
  selector:
    matchLabels:
      app: ngx-dep

  template:
    metadata:
      labels:
        app: ngx-dep
    spec:
      volumes:
        - name: ngx-conf-vol
          configMap:
            name: ngx-conf

      containers:
        - image: nginx:alpine
          name: nginx
          ports:
            - containerPort: 80

          volumeMounts:
            - mountPath: /etc/nginx/conf.d
              name: ngx-conf-vol

先 apply 部署 ConfigMap 和 改造过的 Deployment,然后通过以下的 Service YAML 部署 Service:

apiVersion: v1
kind: Service
metadata:
  name: ngx-svc

spec:
  selector:
    app: ngx-dep

  ports:
    - port: 80
      targetPort: 80
      protocol: TCP

可以看到,Kubernetes 为 Service 对象自动分配了一个 IP 地址 10.102.179.255。Service 对象的 IP 地址还有一个特点,它是一个 “虚地址”,不存在实体,只能用来转发流量。

如果想要看 Service 代理了哪些后端的 Pod,你可以用 kubectl describe 命令:

kubectl describe svc ngx-svc

可以看到 Service 对象管理了两个 endpoint,分别是 10.10.1.145:8010.10.1.146:80,如何知道这两个 IP 地址是不是 Nginx Pod 的实际地址呢?可以使用 kubectl get pod 来看一下,加上参数 -o wide:

kubectl get pod -o wide

把 Pod 的地址与 Service 的信息做个对比,就能够验证 Service 确实用一个静态 IP 地址代理了两个 Pod 的动态 IP 地址。

Service 负载均衡效果 #

因为 Service、 Pod 的 IP 地址都是 Kubernetes 集群的内部网段,所以需要用 kubectl exec 进入到 Pod 内部(或者 ssh 登录集群节点),再用 curl 等工具来访问 Service:

在 Pod 里,用 curl 访问 Service 的 IP 地址,就会看到它把数据转发给后端的 Pod,输出信息会显示具体是哪个 Pod 响应了请求,就表明 Service 确实完成了对 Pod 的负载均衡任务。

2.11.4 域名的方式使用 Service #

Service 对象的 IP 地址是静态的,保持稳定,这在微服务里确实很重要,不过数字形式的 IP 地址用起来不太方便。Kubernetes 的 DNS 插件可以为 Service 创建易写易记的域名,让 Service 更容易使用。

名字空间 #

namespace 名字空间用来在集群里实现对 API 对象的隔离和分组。namespace 的简写是 ns,可以使用命令 kubectl get ns 来查看当前集群里都有哪些名字空间,也就是说 API 对象有哪些分组:

Kubernetes 有一个默认的名字空间,叫 default,如果不显式指定,API 对象都会在这个 default 名字空间里。而其他的名字空间都有各自的用途,比如 kube-system 就包含了 apiserver、etcd 等核心组件的 Pod。

DNS 是一种层次结构,为了避免太多的域名导致冲突,Kubernetes 就把名字空间作为域名的一部分,减少了重名的可能性。Service 对象的域名完全形式是 “对象.名字空间.svc.cluster.local”,但很多时候也可以省略后面的部分,直接写 “对象.名字空间” 甚至 “对象名” 就足够了,默认会使用对象所在的名字空间。

可以试验一下 DNS 域名的用法,以 kubectl exec 命令进入 Pod,然后用 curl 访问 ngx-svc、ngx-svc.default 等域名:

可以看到,不再关心 Service 对象的 IP 地址,只需要知道它的名字,就可以用 DNS 的方式去访问后端服务。

其实 Kubernetes 也为每个 Pod 分配了域名,形式是 “IP 地址.名字空间.pod.cluster.local”,但需要把 IP 地址里的.改成-。比如地址 10.10.1.87,它对应的域名就是 10-10-1-87.default.pod。

2.11.5 对外暴露服务 #

Service 是一种负载均衡技术,它不仅能够管理 Kubernetes 集群内部的服务,还能够担任向集群外部暴露服务的重任。

Service 对象有一个关键字段 type,表示 Service 是哪种类型的负载均衡。前面我们看到的用法都是对集群内部 Pod 的负载均衡,默认为 ClusterIP,Service 的静态 IP 地址只能在集群内访问。除了 ClusterIP,Service 还支持其他三种类型,分别是 ExternalName,LoadBalancer,NodePort。前两种类型一般由云服务商提供。

在使用命令 kubectl expose 的时候如果加上参数 --type=NodePort,或者在 YAML 里添加字段 type:NodePort,那么 Service 除了会对后端的 Pod 做负载均衡之外,还会在集群里的每个节点上创建一个独立的端口,用这个端口对外提供服务,这也正是 NodePort 这个名字的由来。

加上 type 的 Service YAML 描述文件如下:

apiVersion: v1
kind: Service
metadata:
  name: ngx-svc

spec:
  type: NodePort
  selector:
    app: ngx-dep

  ports:
    - port: 80
      targetPort: 80
      protocol: TCP

apply 部署后查看 Service 的状态:

可以看到 TYPE 变成了 NodePort,而在 PORT 列里的端口信息也不一样,除了集群内部使用的 80 端口,还多出了一个 32096 端口,这就是 Kubernetes 在节点上为 Service 创建的专用映射端口。

因为这个端口号属于节点,外部能够直接访问,所以现在就可以不用登录集群节点或者进入 Pod 内部的情况下,直接在集群外使用任意一个节点的 IP 地址,就能够访问 Service 和它代理的后端服务了。

比如我现在所在的服务器是 192.168.14.70,在这台主机上用 curl 访问 Kubernetes 集群的两个节点 192.168.14.142,192.168.14.143,就可以得到 Nginx Pod 的响应数据:

NodePort 与 Service、Deployment 的工作原理大致如下:

其实 NodePort 类型的 Service 虽然方便,但是也有一些缺点。

  1. 端口数量有限,Kubernetes 为了避免端口冲突,默认只在 30000~32767 这个范围内随机分配,只有 2000 多个,而且都不是标准端口号,这对于具有大量业务应用的系统来说可能会不够用。

  2. 会在每个节点上都开端口,然后使用 kube-proxy 路由到真正的后端 Service,这对于有很多计算节点的大集群来说就带来了一些网络通信成本,不是特别经济。

  3. 要求向外界暴露节点的 IP 地址,这在很多时候是不可行的,为了安全还需要在集群外再搭一个反向代理,增加了方案的复杂度。