Service是一种抽象的对象,它定义了一组Pod的逻辑集合和一个用于访问它们的策略,其实这个概念和微服务非常类似。一个Service下面包含的Pod集合是由Label Selector来决定的。

假如后端运行了三个副本,这些副本都是可以替代的,因为前端并不关心使用的是哪一个后端服务,尽管由于各种原因后端的Pod集合会发生变化,但是前端却不需要知道这种变化,也不需要用一个列表来记录这些后端的服务,Service的这种抽象就可以达到这种解耦的目的。

三种IP

先弄明白k8s系统中的三种IP,

  • Node IP:Node节点的IP地址
  • Pod IP:Pod的IP地址
  • Cluster IP:Service的IP地址

首先,Node IP是k8s集群中节点的物理网卡地址(一般为内网),所有属于这个网络的服务器之间都是可以直接通信,所以k8s集群外要想访问k8s集群内部的某个节点或者服务,肯定得通过Node IP进行通信(这时候一般是通过外网IP)

Pod IP是每个Pod的IP地址,它是网络插件进行分配的

Cluster IP是一个虚拟的IP,仅仅作用与k8s Service对象,由k8s自己来进行管理和地址分配

定义Service

定义Service的方式和之前定义的各种资源对象的方式类似,例如,假定有一组Pod服务,对外暴露了8080端口,同时都被打上了app=myapp这样的标签,就可以定义这么一个Service对象:

  1. apiVerison: v1
  2. kind: Service
  3. metadata:
  4. name: myservice
  5. spec:
  6. selector:
  7. app: myapp
  8. ports:
  9. - protocol: TCP
  10. port: 80
  11. targetPort: 8080
  12. name: myapp-http

然后通过kubectl create就可以创建一个名为myservice的Service对象,他会将请求代理到使用TCP端口为8080,具有标签app=myapp的Pod上,这个Service会被系统分配一个Cluster IP,该Service还会持续的监听selector下面的Pod,会把这些Pod信息更新到一个名为myservice的Endpoints对象上去,这个对象就类似于上面说的Pod集合。

需要注意的是,Service能够将一个接收端口映射到任意的targetPort。默认情况下,targetPort将被设置为与port字段相同的值。targetPort可以是一个字符串,引用了backend pod的一个端口的名称,因实际指派给该端口名称的端口号,在每个backend Pod中可能并不相同,所以对于部署和设计Service,这种方式提供更大的灵活性。

另外Service能够支持TCP和UDP协议,默认是TCP。

kube-proxy

在k8s集群中,每个Node会运行一个kube-proxy进程,负责为Service实现一种VIP(虚拟IP,即Cluster IP)的代理形式,现在的k8s中默认是使用iptables这种模式来代理。

iptables

这种模式,kube-proxy会watch apiserver对Service对象和Endpoints对象的添加和移除。对每个Service,它会添加上iptables规则,从而捕获达到该clusterIP和端口的请求,进而将请求重定向到Service的一组backend中的某一个Pod上。还可以使用readiness探针验证后端Pod是否正常工作,以便iptables模式下kube-proxy仅看到测试正常的后端,这样意味着可以避免将流量通过kube-proxy发送到已知失败的Pod中,所以对于线上的应用来说一定要做readiness探针。
image.png
iptables模式下的kube-proxy默认的策略是随机选择一个后端的Pod。
比如当创建backend Service时,k8s会给它指派一个虚拟IP地址,10.0.0.1。假设Service的端口是1234,该Service会被集群中所有kube-proxy实例观察到。当kube-proxy看到一个新的Service,他会安装一系列的iptables规则,从VIP重定向per-Service规则。该per-Service规则连接到per-Endpoint规则,该per-Endpoint规则会重定向(目标NAT)到后端的Pod。

ipvs

除了iptables模式外,k8s也支持ipvs模式,在该模式下,kube-proxy watch k8s服务和端点,调用netlink接口相应的创建IPVS规则,并定期将IPVS规则与k8s服务和端点同步。该控制循环可确保IPVS状态与所需状态匹配。访问服务时,IPVS将流量定向到后端Pod之一。

IPVS代理模式基于类似于iptables模式的netfilter钩子函数,但是使用哈希表作为基础数据结构,并且在内核空间中工作。所以与iptables模式下的kube-proxy相比,IPVS模式下的kube-proxy重定向通信延迟要低,并且在同步代理规则时具有更好的性能。与其他代理模式相比,IPVS模式还支持更高的网络流量吞吐量。所以对于较大规模的集群会使用ipvs模式的kube-proxy,只需要满足节点上运行ipvs的条件,然后就可以直接将kube-proxy的模式修改为ipvs,如果不满足运行条件会自动降级为iptables模式,现在都推荐使用ipvs模式,可以大幅度提高Service性能。

IPVS提供了更多选项来平衡后端Pod的流量,默认是rr,有以下策略:

  • rr:round-robin
  • lc:least-connection
  • dh:destination hashing
  • sh:source hashing
  • sed:shortest expected delay
  • nq:never queue

现在只能整体修改策略,可以通过kube-proxy中配置-ipvs-scheduler参数来实现,暂时不支持特定的Service进行配置。
image.png
也可以实现基于客户端IP的会话亲和性,可以将service.spec.sessionAffinity的值设置为”ClientIP”(默认值为”None”)即可,此外还可以通过service.spec.sessionAffinityConfig.clientIP.timeoutSeconds来设置最大会话停留时间(默认为10800s,3小时):

apiVersion: v1
kind: Service
spec:
  sessionAffinity: ClientIP
  ...

Service 只支持两种形式的会话亲和性服务:None 和 ClientIP,不支持基于 cookie 的会话亲和性,这是因为 Service 不是在 HTTP 层面上工作的,处理的是 TCP 和 UDP 包,并不关心其中的载荷内容,因为 cookie 是 HTTP 协议的一部分,Service 并不知道它们,所有会话亲和性不能基于 Cookie。

Service

定义Service的时候可以指定类型,默认为Cluster IP。有如下的服务类型:

  • Cluster IP:通过集群的内部IP暴露服务,选择该值,服务只能在集群内部访问。
  • NodePort:通过每个Node节点上的IP和静态端口(NodePort)暴露服务。NodePort服务会路由到Cluster IP服务,这个Cluster IP服务会自动创建。通过请求NodeIP:NodePort,可以从集群的外部访问一个NodePort服务。
  • LoadBalancer:使用云厂商的负载均衡器,可以向外暴富服务。外部的负载均衡器可以路由到NodePort服务和Cluster服务,这个需要结合具体的云厂商进行操作。
  • ExternalName:通过返回CNAME和它的值,可以将服务映射到externalName字段的内容。

NodePort类型

如果设置type的值为”NodePort”,k8s master将从给定的配置范围内(默认:30000-32767)分配端口,每个Node将从该端口(每个Node上的同一端口)代理到Service。该端口将通过Service的spec.ports[*].nodePort字段被指定,如果不指定的话会自动生成一个端口。

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

创建一个NodePort类型的Service(service-nodeport-demo.yaml):

apiVersion: v1
kind: Service
metadata:
  name: myservice
spec:
  selector:
    app: myapp
  type: NodePort
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
    name: myapp-http

创建该Service:

$ kubectl apply -f service-demo.yaml

然后查看Service对象信息:

$ kubectl get svc
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP        27d
myservice    NodePort    10.104.57.198   <none>        80:32560/TCP   14h

可以看到myservice的TYPE类型已经是NodePort,后面的port(s)部分也多了一个32560的映射端口。

ExternalName

ExternalName是Service的特例,他没有selector,也没有定义任何端口的Endpoint。对于运行在集群外部的服务,它通过返回该外部服务的别名这种方式来提供服务。

apiVersion: v1
kind: Service
metadata:
  name: myservice
spce:
  type: ExternalName
  externalName: my.database.ex.com

当访问地址my-service.prod.svc.cluster.local时,集群的DNS服务将返回一个值为my.database.ex.com的CNAME记录。访问这个服务的工作方式与其他的相同,唯一不同的是重定向发生在DNS层,而且不会进行代理或转发。如果后续决定要将数据库迁移到k8s集群中,可以启动对应的Pod,增加合适的Selector或者Endpoint,修改Service的type,完全不需要修改调用的代码,这样就完全解耦了。

除了可以直接通过externalName指定外部服务的域名外,还可以通过自定义Endpoints来创建Service,前提是clusterIP=None。名称要和Service保持一致,如下:

apiVersion: v1
kind: Service
metadata:
  name: etcd-k8s
  namespace: kube-system
  labels:
    k8s-app: etcd
spec:
  type: ClusterIP
  clusterIP: None
  ports:
  - name: port
    port: 2379

---

apiVersion: v1
kind: Endpoints
metadata:
  name: etcd-k8s # 名称必须和service一致
  namespace: kube-system
  labels:
    k8s-app: etcd
subsets:
- addresses:
  - ip: 10.151.30.57 # Service 将连接重定向到endpoint
  ports:
  - name: port
    port: 2379 # endpoint的目标端口

上面这个服务就是将外部的etcd服务引入到k8s集群中。

获取客户端IP

通常,当集群内的客户端连接到服务的时候,是支持服务的Pod可以获取到客户端的IP地址,但是,当通过节点端口接收到连接时,由于对数据包执行了源地址转换(SNAT),因此数据包的源IP地址会发生变化,后端的Pod无法看到实际的客户端IP,对于某些应用来说是个问题,比如nginx的请求日志就无法获取准确的客户端访问IP了。如下方:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  selector:
    app: nginx
  type: NodePort
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80

创建后可以查看nginx服务被自动分配了一个32761的NodePort端口:

$ kubectl get svc
NAME         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
kubernetes   ClusterIP   10.96.0.1        <none>        443/TCP        28d
nginx        NodePort    10.106.190.194   <none>        80:32761/TCP   48m
$ kubectl get pods -o wide
NAME                              READY   STATUS    RESTARTS   AGE     IP             NODE         NOMINATED NODE   READINESS GATES
nginx-54f57cf6bf-nwtjp            1/1     Running   0          3m      10.244.3.15    ydzs-node3   <none>           <none>
nginx-54f57cf6bf-ptvgs            1/1     Running   0          2m59s   10.244.2.13    ydzs-node2   <none>           <none>
nginx-54f57cf6bf-xhs8g            1/1     Running   0          2m59s   10.244.1.16    ydzs-node1   <none>           <none>

可以看到这3个Pod被分配到了3个不同的节点,这时候通过master节点的NodePort端口来访问服务,查看nginx的Pod日志可以看到获取到的clientIP是10.151.30.11,其实就是master节点的内网IP,并不是期望的真正的浏览器访问的IP地址:

$ kubectl logs -f nginx-54f57cf6bf-xhs8g
10.151.30.11 - - [07/Dec/2019:16:44:38 +0800] "GET / HTTP/1.1" 200 612 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36" "-"

这是因为master节点上并没有对应的Pod,所以通过master节点去访问应用的时候必然需要额外的网络跳转才能到达其他节点上的Pod,在跳转过程中由于数据包进行了SNAT,所以看到的是master节点的IP。这个时候可以在Service设置externalTrafficPolicy来减少网络跳数:

spec:
  externalTrafficPolicy: Local

如果Service中配置了externalTrafficPolicy=Local,并且通过服务的节点端口来打开外部连接,则Service会代理到本地运行的Pod,如果本地没有Pod存在,则连接将挂起。如果设置该字段后更新,这时候通过master节点的NodePort访问应用是访问不到的,因为master节点上并没有对应的Pod运行,所以需要确保负载均衡器将连接转发给至少具有一个Pod的节点。

但是需要注意的是使用这个参数有一个缺点,通常情况下,请求都是均衡到所有的Pod上的,但是使用了这个配置的话,情况可能不一样了。比如有两个节点上运行了3个Pod,假如节点A运行一个Pod,节点B运行两个Pod,如果负载在两个节点简均衡分布连接,则节点A上的Pod将接收50%的请求,但节点B上的每个Pod只接收25%的请求。

由于增加了externalTrafficPolicy: Local这个配置后,接收请求的节点和目标Pod都在一个节点上,所有没有额外的网络跳转(不执行SNAT),所以就可以拿到正确的客户端IP,如下所示,把Pod都固定到master节点:

apiVersion: apps/v1
kind: Deployment
metadata:
  name:  nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      tolerations:
      - operator: "Exists"
      nodeSelector:
        kubernetes.io/hostname: ydzs-master
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  externalTrafficPolicy: Local
  selector:
    app: nginx
  type: NodePort
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80

更新服务后,然后在通过NodePort访问服务可以看到正确的客户端IP地址:

$ kubectl logs -f nginx-ddc8f997b-ptb7b
182.149.166.11 - - [07/Dec/2019:17:03:43 +0800] "GET / HTTP/1.1" 200 612 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36" "-"

服务发现

上面我们讲解了 Service 的用法,我们可以通过 Service 生成的 ClusterIP(VIP) 来访问 Pod 提供的服务,但是在使用的时候还有一个问题:我们怎么知道某个应用的 VIP 呢?比如我们有两个应用,一个是 api 应用,一个是 db 应用,两个应用都是通过 Deployment 进行管理的,并且都通过 Service 暴露出了端口提供服务。api 需要连接到 db 这个应用,我们只知道 db 应用的名称和 db 对应的 Service 的名称,但是并不知道它的 VIP 地址,我们前面的 Service 课程中是不是学习到我们通过 ClusterIP 就可以访问到后面的 Pod 服务,如果我们知道了 VIP 的地址是不是就行了?

环境变量

为了解决上面的问题,在之前的版本中,k8s才用了环境变量的方法,每个Pod启动的时候,会通过环境变量设置所有服务的IP和Port信息,这样Pod中的应用就可以通过读取环境变量来获取依赖服务的地址信息,这种方法使用起来简单,但是有一个很大的问题就是依赖的服务必须在Pod启动之前就存在,不然是不会被注入到环境变量中的。Nginx服务(test-nginx.yaml):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deploy
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-service
  labels:
    name: nginx-service
spec:
  ports:
  - port: 5000
    targetPort: 80
  selector:
    app: nginx

创建上面的服务:

$ kubectl apply -f test-nginx.yaml
deployment.apps "nginx-deploy" created
service "nginx-service" created
$ kubectl get pods
NAME                                      READY     STATUS    RESTARTS   AGE
...
nginx-deploy-75675f5897-47h4t             1/1       Running   0          53s
nginx-deploy-75675f5897-mmm8w             1/1       Running   0          53s
...
$ kubectl get svc
NAME            TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
...
nginx-service   ClusterIP   10.107.225.42    <none>        5000/TCP         1m
...

可以看到两个Pod和一个名为nginx-service的服务创建成功,该Service监听的端口是5000,同时它会把流量转发给它代理的所有Pod(这里就是拥有app: nginx标签的两个Pod)。

现在再创建一个普通的Pod,观察下该Pod中的环境变量是否包含上面的nginx-service的服务信息(test-pod.yaml)

apiVersion: v1
kind: Pod
metadata:
  name: test-pod
spec:
  containers:
  - name: test-service-pod
    image: busybox
    command: ["/bin/sh", "-c", "env"]

创建该Pod:

$ kubectl apply -f test-pod.yaml
pod "test-pod" created

等Pod创建完成后,查看日志信息:

$ kubectl logs test-pod
...
KUBERNETES_PORT=tcp://10.96.0.1:443
KUBERNETES_SERVICE_PORT=443
HOSTNAME=test-pod
HOME=/root
NGINX_SERVICE_PORT_5000_TCP_ADDR=10.107.225.42
NGINX_SERVICE_PORT_5000_TCP_PORT=5000
NGINX_SERVICE_PORT_5000_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
NGINX_SERVICE_SERVICE_HOST=10.107.225.42
NGINX_SERVICE_PORT_5000_TCP=tcp://10.107.225.42:5000
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP_PROTO=tcp
NGINX_SERVICE_SERVICE_PORT=5000
NGINX_SERVICE_PORT=tcp://10.107.225.42:5000
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
KUBERNETES_SERVICE_HOST=10.96.0.1
PWD=/
...

打印的环境变量中就包括刚创建的nginx-service这个服务,有HOST、PORT、PROTO、ADDR等,也包括其他已经存在的Service的环境变量,现在如果需要在这个Pod里面访问nginx-service的服务,是不是可以直接通过NGINX_SERVICE_SERVICE_HOST和NGINX_SERVICE_SERVICE_PORT就可以了?但是如果这个Pod启动起来的时候nginx-service服务还没起来,那么在环境变量中就无法获取这些信息,当然也可以通过initContainer之类的方法来确保nginx-service启动后再启动Pod,但是这种方法增加了Pod的启动的复杂性,所以这不是最优的方法,局限性太多。

DNS

由于上面环境变量这种方式的局限性,需要一种更加智能的方案,其实自己可以思考一种比较理想的方案:直接使用Service的名称,因为Service的名称不会变化,不需要去关心分配的ClusterIP的地址,因为这个地址并不是固定不变的,所以如果我们直接使用Service的名字,然后对应的ClusterIP地址的转换能够自动完成就很好了。他们之间的转换功能通过DNS就可以解决,同样,k8s也提供了DNS的方案来解决上面的服务发现的问题。

DNS服务不是一个独立的系统服务,而是作为一种addon插件而存在,现在比较推荐的两个插件:kube-dns和CoreDNS,实际上在比较新点的版本中已经默认是CoreDNS了,因为kube-dns默认一个Pod中需要3个容器配合使用,CoreDNS只需要一个容器即可,kubeadm搭建集群的时候直接安装的就是CoreDNS插件:

$ kubectl get pods -n kube-system -l k8s-app=kube-dns
NAME                       READY   STATUS    RESTARTS   AGE
coredns-667f964f9b-sthqq   1/1     Running   0          32m
coredns-667f964f9b-zj4r4   1/1     Running   0          33m

CoreDNS是用GO写的高性能,高可扩展性的DNS服务,基于HTTP/2 Web服务Caddy进行编写的。CoreDns内部采用插件机制,所有功能都是插件形式编写,用户也可以扩展自己的插件,以下是k8s部署CoreDNS时的默认配置:

$ kubectl get cm coredns -n kube-system -o yaml
apiVersion: v1
data:
  Corefile: |
    .:53 {
        errors  # 启用错误记录
        health  # 启用健康检查检查端点,8080:health
        ready
        kubernetes cluster.local in-addr.arpa ip6.arpa {  # 处理 k8s 域名解析
           pods insecure
           fallthrough in-addr.arpa ip6.arpa
           ttl 30
        }
        prometheus :9153  # 启用 metrics 指标,9153:metrics
        forward . /etc/resolv.conf  # 通过 resolv.conf 内的 nameservers 解析
        cache 30  # 启用缓存,所有内容限制为 30s 的TTL
        loop  # 检查简单的转发循环并停止服务
        reload  # 运行自动重新加载 corefile,热更新
        loadbalance  # 负载均衡,默认 round_robin
    }
kind: ConfigMap
metadata:
  creationTimestamp: "2019-11-08T11:59:49Z"
  name: coredns
  namespace: kube-system
  resourceVersion: "188"
  selfLink: /api/v1/namespaces/kube-system/configmaps/coredns
  uid: 21966186-c2d9-467a-b87f-d061c5c9e4d7
  • 每个{}代表一个zone,格式是”Zone:port{}“,其中"."代表默认zone
  • {}内的每个名称代表插件的名称,只有配置的插件才会启用,当解析域名时,会先匹配zone(都未匹配会执行默认zone),然后zone内的插件从上到下依次执行(这个顺序并不是配置文件内谁在前面的顺序,而是core/dnsserver/zdirectives.go内的顺序),匹配后返回处理(执行过的插件从下到上依次处理返回逻辑),不再执行下一个插件。

CoreDNS的Service地址一般情况下是固定的,类似于k8s这个Service地址一般就是第一个IP地址10.96.0.1,CoreDNS的Service地址就是10.96.0.10,该IP被分配后,kubelet会将使用--cluster-dns=<service-ip>参数配置的DNS传递给每个容器。DNS名称也需要域名,本地域可以使用参数--cluster-domain=<default-local-domain>在kubelet中配置:

$ cat /var/lib/kubelet/config.yaml
......
clusterDNS:
- 10.96.0.10
clusterDomain: cluster.local
......

前面说了如果我们建立的Service如果支持域名形式进行解析,就可以解决服务发现的功能,那么利用kubedns就可以将Service生成怎样的dns记录呢?

  • 普通的Service:会生成servicename.namespace.svc.cluster.local的域名,会解析到Service对应的ClusterIP上,在Pod之间的调用可以简写成servicename.namespace,如果处于同一个命名空间下面,甚至可以只写servicename即可访问
  • Headless Service:无头服务,就是把ClusterIP设置为None的,会被解析为指定Pod的IP列表,同样还可以通过podname.servicename.namespace.svc.cluster.local访问到具体的某一个Pod

使用一个pod来测试Service的域名访问:

$ kubectl run -it --image busybox:1.28.3 test-dns --restart=Never --rm /bin/sh
If you don't see a command prompt, try pressing enter.
/ # cat /etc/resolv.conf
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
/ #

进入Pod中,查看/etc/resolv.conf的内容,可以看到nameserver的地址10.96.0.10,该IP地址即是在安装CoreDNS插件的时候分配的一个固定的静态IP地址

$ kubectl get svc -n kube-system
NAME                      TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                  AGE
kube-dns                  ClusterIP   10.96.0.10       <none>        53/UDP,53/TCP,9153/TCP   28d

也就是说这个Pod现在默认的nameserver就是kube-dns的地址,访问前面创建的nginx-service服务:

/ # wget -q -O- nginx-service.default.svc.cluster.local:5000
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

给Pod添加DNS记录

StatefulSet中的Pod是拥有单独的DNS记录,比如一个StatefulSet名称为etcd,而它关联的Headless SVC名称为etcd-headless,那么CoreDns就会为它的每个Pod解析如下的记录:

etcd-0.etcd-headless.default.svc.cluster.local
etcd-1.etcd-headless.default.svc.cluster.local
......

那么除了StatefulSet管理的Pod之外,其他的Pod是否也可以生成DNS记录呢?

如下所示,这里只有一个Headless 的SVC,并没有StatefulSet管理的Pod,而是ReplicaSet管理的Pod,可以看到貌似也生成了类似于StatefulSet中的解析记录。
image.png
按照常规的理解会认为这是一个StatefulSet管理的Pod,但其实这里是不同的ReplicaSet而已,这里的实现其实是因为Pod自己本身也是可以有自己的DNS记录的,所以是可以去实现一个类似于StatefulSet的Pod那样的解析记录的。

首先部署一个Deployment管理的普通应用,其定义如下:

# nginx.yaml
apiVersion: apps/v1 
kind: Deployment
metadata:
  name: nginx
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80

部署后创建了两个Pod:

$ kubectl apply -f nginx.yaml
deployment.apps/nginx created
$ kubectl get pod -l app=nginx -o wide
NAME                     READY   STATUS    RESTARTS   AGE   IP             NODE    NOMINATED NODE   READINESS GATES
nginx-5d59d67564-2cwdz   1/1     Running   0          19s   10.244.1.68    node1   <none>           <none>
nginx-5d59d67564-bp5br   1/1     Running   0          19s   10.244.2.209   node2   <none>           <none>

然后定义如下的Headless Service:

# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  clusterIP: None
  ports:
  - name: http
    port: 80
    protocol: TCP
  selector:
    app: nginx
  type: ClusterIP

创建该Service,并尝试解析Service DNS:

$ kubectl apply -f service.yaml
service/nginx created
$ kubectl get svc
NAME                       TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                      AGE
kubernetes                 ClusterIP   10.96.0.1        <none>        443/TCP                      38d
nginx                      ClusterIP   None             <none>        80/TCP                       7s
$ dig @10.96.0.10 nginx.default.svc.cluster.local

; <<>> DiG 9.9.4-RedHat-9.9.4-73.el7_6 <<>> @10.96.0.10 nginx.default.svc.cluster.local
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 2573
;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;nginx.default.svc.cluster.local. IN A

;; ANSWER SECTION:
nginx.default.svc.cluster.local. 30 IN A 10.244.2.209
nginx.default.svc.cluster.local. 30 IN A 10.244.1.68

;; Query time: 19 msec
;; SERVER: 10.96.0.10#53(10.96.0.10)
;; WHEN: Wed Nov 25 11:44:41 CST 2020
;; MSG SIZE  rcvd: 154

然后对 nginx 的 FQDN 域名进行 dig 操作,可以看到返回了多条 A 记录,每一条对应一个 Pod。上面 dig 命令中使用的 10.96.0.10 就是 kube-dns 的 cluster IP,可以在 kube-system namespace 中查看:

$ kubectl -n kube-system get svc
NAME                   TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)         AGE
kube-dns               ClusterIP   10.96.0.10     <none>        53/UDP,53/TCP   52m

接下来试试在service名字前面加上Pod名字交给kube-dns做解析:

$ dig @10.96.0.10 nginx-5d59d67564-bp5br.nginx.default.svc.cluster.local

; <<>> DiG 9.9.4-RedHat-9.9.4-73.el7_6 <<>> @10.96.0.10 nginx-5d59d67564-bp5br.nginx.default.svc.cluster.local
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 10485
;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;nginx-5d59d67564-bp5br.nginx.default.svc.cluster.local. IN A

;; AUTHORITY SECTION:
cluster.local.  30 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1606275807 7200 1800 86400 30

;; Query time: 4 msec
;; SERVER: 10.96.0.10#53(10.96.0.10)
;; WHEN: Wed Nov 25 11:47:31 CST 2020
;; MSG SIZE  rcvd: 176

可以看到并没有得到解析结果。官方文档中有一段Pod’s hostname and subdomain fields说明:

Pod 规范中包含一个可选的 hostname 字段,可以用来指定 Pod 的主机名。当这个字段被设置时,它将优先于 Pod 的名字成为该 Pod 的主机名。举个例子,给定一个 hostname 设置为 “my-host” 的 Pod, 该 Pod 的主机名将被设置为 “my-host”。Pod 规约还有一个可选的 subdomain 字段,可以用来指定 Pod 的子域名。举个例子,某 Pod 的 hostname 设置为 “foo”,subdomain 设置为 “bar”, 在名字空间 “my-namespace” 中对应的完全限定域名为 “foo.bar.my-namespace.svc.cluster-domain.example”。

给上面的nginx.yaml加上subdomain测试下:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      subdomain: nginx
      containers:
      - name: nginx:1.7.9
        ports:
        - containerPort: 80

更新部署在重新解析:

$ kubectl apply -f nginx.yaml
$ kubectl get pod -l app=nginx -o wide
NAME                     READY   STATUS        RESTARTS   AGE     IP             NODE    NOMINATED NODE   READINESS GATES
nginx-78f58d8bcb-6kctm   1/1     Running       0          8s      10.244.2.210   node2   <none>           <none>
nginx-78f58d8bcb-6tbnv   1/1     Running       0          15s     10.244.1.69    node1   <none>           <none>
$ dig @10.96.0.10 nginx-78f58d8bcb-6kctm.nginx.default.svc.cluster.local

; <<>> DiG 9.9.4-RedHat-9.9.4-73.el7_6 <<>> @10.96.0.10 nginx-78f58d8bcb-6kctm.nginx.default.svc.cluster.local
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 34172
;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;nginx-78f58d8bcb-6kctm.nginx.default.svc.cluster.local. IN A

;; AUTHORITY SECTION:
cluster.local.  30 IN SOA ns.dns.cluster.local. hostmaster.cluster.local. 1606276303 7200 1800 86400 30

;; Query time: 2 msec
;; SERVER: 10.96.0.10#53(10.96.0.10)
;; WHEN: Wed Nov 25 11:52:18 CST 2020
;; MSG SIZE  rcvd: 176

可以看到依然不能解析,那就试试官方文档中的例子 ,不用 Deployment 直接创建 Pod 吧。第一步先将 hostname 和 subdomain 注释掉:

# individual-pods-example.yaml
apiVersion: v1
kind: Service
metadata:
  name: default-subdomain
spec:
  selector:
    name: busybox
  clusterIP: None
  ports:
  - name: foo # Actually, no port is needed.
    port: 1234
    targetPort: 1234
---
apiVersion: v1
kind: Pod
metadata:
  name: busybox1
  labels:
    name: busybox
spec:
  hostname: busybox-1
  subdomain: default-subdomain
  containers:
  - image: busybox:1.28
    command:
      - sleep
      - "3600"
    name: busybox
---
apiVersion: v1
kind: Pod
metadata:
  name: busybox2
  labels:
    name: busybox
spec:
  hostname: busybox-2
  subdomain: default-subdomain
  containers:
  - image: busybox:1.28
    command:
      - sleep
      - "3600"
    name: busybox

部署然后尝试解析 Pod DNS (注意这里 hostname 和 pod 的名字有区别,中间多了减号):

$ kubectl apply -f individual-pods-example.yaml
$ $ dig @10.96.0.10 busybox-1.default-subdomain.default.svc.cluster.local

; <<>> DiG 9.11.3-1ubuntu1.5-Ubuntu <<>> @10.96.0.10 busybox-1.default-subdomain.default.svc.cluster.local
; (1 server found)
;; global options: +cmd
;; Got answer:
;; WARNING: .local is reserved for Multicast DNS
;; You are currently testing what happens when an mDNS query is leaked to DNS
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12636
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
; COOKIE: 5499ded915cf1ff2 (echoed)
;; QUESTION SECTION:
;busybox-1.default-subdomain.default.svc.cluster.local. IN A

;; ANSWER SECTION:
busybox-1.default-subdomain.default.svc.cluster.local. 5 IN A 10.44.0.6

;; Query time: 0 msec
;; SERVER: 10.96.0.10#53(10.96.0.10)
;; WHEN: Fri Apr 19 15:27:38 CST 2019
;; MSG SIZE  rcvd: 163

ANSWER 记录回来了,hostname 和 subdomain 二者都必须显式指定,缺一不可。

修改之前的nginx deploymen加上hostname,重新解析:

$ dig @10.96.0.10 nginx.nginx.default.svc.cluster.local

; <<>> DiG 9.9.4-RedHat-9.9.4-73.el7_6 <<>> @10.96.0.10 nginx.nginx.default.svc.cluster.local
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 21127
;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;nginx.nginx.default.svc.cluster.local. IN A

;; ANSWER SECTION:
nginx.nginx.default.svc.cluster.local. 30 IN A 10.244.2.211
nginx.nginx.default.svc.cluster.local. 30 IN A 10.244.1.70

;; Query time: 1 msec
;; SERVER: 10.96.0.10#53(10.96.0.10)
;; WHEN: Wed Nov 25 11:55:37 CST 2020
;; MSG SIZE  rcvd: 172

可以看到解析成功了,但是因为 Deployment 中无法给每个 Pod 指定不同的 hostname,所以两个 Pod 有同样的 hostname,解析出来两个 IP,跟我们的本意就不符合了。不过知道了这种方式过后我们就可以自己去写一个 Operator 去直接管理 Pod 了,给每个 Pod 设置不同的 hostname 和一个 Headless SVC 名称的 subdomain,这样就相当于实现了 StatefulSet 中的 Pod 解析。

Pod的DNS策略

DNS策略可以单独对Pod进行设定,目前k8s支持以下特定的DNS策略。这些策略可以在Pod规范中的dnsPolicy字段设置:

  • Default:是让kubelet来决定使用何种DNS策略。而kubelet默认的方式是使用宿主机的/etc/resolv.conf,但是kubelet是可以灵活来配置使用什么文件来进行DNS策略的,使用kubelet的参数-resolv-conf=/etc/resolv.conf来决定DNS解析的文件地址。
  • ClusterFirst:表示Pod内的DNS使用集群中配置的DNS服务,简单的来说,就是使用k8s中kubedns或coredns服务进行域名解析。如果解析不成功,才会使用宿主机的DNS配置进行解析。
  • ClusterFirstWithHostNet:在某些场景下,Pod是用HostNetwork模式启动的,一旦用HostNetwork模式,表示这个Pod中的所有容器,都要使用宿主机的/etc/resolv.conf配置进行DNS查询,但如果想继续使用k8s的DNS服务,那就将dnsPolicy设置为该模式。
  • None:表示空的DNS设置,这种方式一般用于想要自定义DNS配置的场景,需要和dnsConfig配合一起使用达到自定义DNS的目的。

如下资源清单,其DNS策略设置为ClusterFirstWithHostNet

apiVersion: v1
kind: Pod
metadata:
  name: busybox
  namespace: default
spec:
  containers:
  - image: busybox:1.28
    command:
      - "sleep"
      - "3600"
    imagePullPolicy: IfNotPresent
    name: busybox
  restartPolicy: Always
  hostNetwork: true
  dnsPolicy: ClusterFirstWithHostNet

Pod的DNS配置

Pod的DNS配置可让用户对Pod的DNS设置进行更多控制。dnsConfig字段是可选的,他可以与任何dnsPolicy设置一起使用。但是当Pod的dnsPolicy设置为”None”时,必须指定dnsConfig字段。

用户可以在dnsConfig字段中指定以下属性:

  • nameservers:将用作于Pod的DNS服务器的IP地址列表。最多可以指定3个IP地址。当Pod的dnsPolicy设置为None时,列表必须至少包含一个IP地址,否则此属性是可选的。所列出的服务器将合并到从指定的DNS策略生成的基本名称服务器,并删除重复的地址。
  • searches:用于在Pod中查找主机名的DNS搜索域的列表。此属性可选。指定此属性时,所提供的列表将合并到根据所选的DNS策略生成的基本搜索域名中。重复的域名将被删除,k8s最多允许6个搜索域。
  • options:可选的对象列表,其中每个对象可能具有name属性(必需)和value属性(可选)。此属性中的内容将合并到从指定的DNS策略生成的选项。重复的条目将被删除。

以下是具有自定义DNS设置的Pod示例:

apiVersion: v1
kind: Pod
metadata:
  name: dns-example
  namespace: default
spec:
  containers:
    - name: test
      image: nginx
  dnsPolicy: "None"
  dnsConfig:
    nameservers:
      - 1.2.3.4
    searches:
      - ns1.svc.cluster-domain.example
      - my.dns.search.suffix
    options:
      - name: dots
        value: "2"
      - name: edns0

创建上面的Pod后,容器test会在其/etc/resolv.conf文件中获取以下内容:

nameserver 1.2.3.4
search ns1.svc.cluster-domain.example my.dns.search.suffix
options ndots:2 edns0