我们知道Kubernetes Pod不总是健壮的,而是要假设Pod中的容器很可能因为各种原因发生故障而死掉。Deployment等Controller会通过动态创建和销毁Pod来保证应用整体的健壮性。换句话说,Pod是脆弱的,但应用应该是要健壮的。

每个Pod都有自己的IP地址。当Controller用新Pod替代发生故障的Pod时,新Pod会分配到新的IP地址。这样就产生了一个问题:

如果一组Pod对外提供服务(比如HTTP),它们的IP很有可能发生变化,那么客户端如何找到并访问这个服务呢?

Kubernetes给出的解决方案是Service。

创建Service

Kubernetes Service从逻辑上代表了一组Pod,具体是哪些Pod则是由label来挑选的。Service有自己的IP,而且这个IP是不变的。客户端只需要访问Service的IP,Kubernetes则负责建立和维护Service与Pod的映射关系。无论后端Pod如何变化,对客户端不会有任何影响,因为Service没有变。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# API 版本号
apiVersion: apps/v1
# 类型,如:Pod/ReplicationController/Deployment/Service/Ingress
kind: Deployment
metadata:
  # Kind 的名称
  name: httpd
spec:
  selector:
    matchLabels:
      # 容器标签的名字,发布 Service 时,selector 需要和这里对应
      app: httpd
  # 部署的实例数量
  replicas: 3
  template:
    metadata:
      labels:
        app: httpd
    spec:
      # 配置容器,数组类型,说明可以配置多个容器
      containers:
      # 容器名称
      - name: httpd
        # 容器镜像
        image: httpd
        # 只有镜像不存在时,才会进行镜像拉取
        imagePullPolicy: IfNotPresent
        ports:
        # Pod 端口
        - containerPort: 80

image-20230304185853617

来启动了三个Pod,运行httpd镜像,label是app: httpd,Service将会用这个label来挑选Pod,

Pod分配了各自的IP,这些IP只能被Kubernetes Cluster中的容器和节点访问,

image-20230304190145430

接下来创建Service:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
apiVersion: v1  #service API版本, service.apiVersion  // 1
kind: Service #类型为service     // 2
metadata: #定义service元数据,service.metadata  
  name: httpd-svc  # 定义Service名称   // 3
spec:  #定义service的详细信息,service.spec
  selector:  #service的标签选择器,定义要访问的目标pod
    app: httpd  #将流量路到选择的pod上,须等于     Deployment.spec.selector.matchLabels    // 4
  ports:
    - protocol: TCP       # 定义协议 // 5
      port: 8080         # service端口
      targetPort: 80   # 目标pod端口

①v1是Service的apiVersion。

②指明当前资源的类型为Service。

③Service的名字为httpd-svc。

④selector指明挑选那些label为app: httpd的Pod作为Service的后端。

⑤将Service的8080端口映射到Pod的80端口,使用TCP协议。

image-20230304192908696

可以看到http-svc分配到一个ClusterIP为10.108.236.53, 可以通过该IP访问后端httpd pod,

image-20230304193142633

根据前面的端口映射,这里要使用8080端口。另外,除了我们创建的httpd-svc,还有一个Service kubernetes,Cluster内部通过这个Service访问Kubernetes API Server。

通过kubectl describe可以查看httpd-svc与Pod的对应关系,

image-20230304193412806

Endpoints罗列了三个Pod的IP和端口。我们知道Pod的IP是在容器中配置的,那么Service的Cluster IP又是配置在哪里的呢?CLUSTER-IP又是如何映射到Pod IP的呢?

其实是iptables.

ClusterIP底层实现

ClusterIP是一个虚拟IP,是由Kubernetes节点上的iptables规则管理的。

可以通过iptables-save命令打印出当前节点的iptables规则,因为输出较多,这里只截取与httpd-svc Cluster IP 10.108.236.53相关的信息,

image-20230304194257568

这两条规则的含义是:

(1)如果Cluster内的Pod(源地址来自10.244.0.0/16)要访问httpd-svc,则允许。

(2)其他源地址访问httpd-svc,跳转到规则KUBE-SVC-IYRDZZKXS5EOQ6Q6。

KUBE-SVC-IYRDZZKXS5EOQ6Q6的规则如下:

image-20230304195029213

(1)1/3的概率跳转到规则KUBE-SEP-N4HQP4BS4JJGDYHG

(2)1/3的概率(剩下2/3的一半)跳转到规则KUBE-SEP-KS6SRLT6RA7U756F

(3)1/3的概率跳转到规则KUBE-SEP-TZUHJA6PDPFAF5LY

上面三个跳转的规则如下:

image-20230304195930573

即将请求分别转发到后端的三个Pod。通过上面的分析,我们得到结论:iptables将访问Service的流量转发到后端Pod,而且使用类似轮询的负载均衡策略。

另外补充的一点:Cluster的每一个节点都配置了相同的iptables规则,这样就确保了整个Cluster都能够通过Service的Cluster IP访问Service.

DNS访问Service

在Cluster中,除了可以通过Cluster IP访问Service,Kubernetes还提供了更为方便的DNS访问。

kubeadm部署时会默认安装dns相关组件,

image-20230304201727122

可以把kube-dns认为是一个dns服务器,每当有新的Service被创建,kube-dns会添加该Service的DNS记录。Cluster中的Pod可以通过<SERVICE_NAME>.<NAMESPACE_NAME>访问Service。

比如我们可以用httpd-svc.default访问Service httpd-svc,

以下面的yaml创建一个busybox的pod:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
apiVersion: v1
kind: Pod
metadata:
  name: busybox
  labels:
    app: busybox
spec:
  containers:
  - name: busybox
    image: busybox
    command: ['sh', '-c', 'echo The app is running! && sleep 3600']

image-20230304214650063

我们希望在一个临时的busybox Pod中验证了DNS的有效性。但是此时不通。

pod不能访问域名问题排查

一时间怀疑了k8s cluster搭建的是否有问题、coredns是不是正常、服务发现等哪里出了纰漏, 冷静分析下可能还是搭建环节少了一些东西, 排查步骤如下:

  • 可以确定进入pod, 可以访问IP但不能访问域名

image-20230304215056842

  • 进入目标pod容器, 查看/etc/resolv.conf

image-20230304222116299

可以看到dns服务器的IP为10.96.0.10, 然后查看k8s的core dns容器的信息

image-20230304222340970

image-20230304222440750

可以看到两个coredns pod位状态是running,日志业务异常情况,应该正常

  • 进一步查看dns service的信息

image-20230304222646866

比较奇怪, 这里并没有kube-dns的service, 我们知道kube-dns服务的IP正是10.96.0.10,而且pod是通过kube-dns 服务来解析域名的,现在的问题是POD无法与kube-dns通信呢?还是coredns本身域名解析有问题呢,

先恢复一下kube-dns,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: Service
metadata:
  name: kube-dns
  namespace: kube-system
  labels:
    k8s-app: kube-dns
    kubernetes.io/cluster-service: "true"
    kubernetes.io/name: "KubeDNS"
spec:
  selector:
    k8s-app: kube-dns
  clusterIP: 10.96.0.10
  ports:
  - name: dns
    port: 53
    protocol: UDP
  - name: dns-tcp
    port: 53
    protocol: TCP

创建kube-dns service之后,发现pod可以和kube-dns通信, 同时也恢复了pod里面以域名形式访问svc;

仍然来看下kube-dns服务后端是否正确绑定了coredns容器, 查看endpoint来确认:

image-20230304223446817

可以看到kube-dns后端正确的绑定了两个coredns pod的IP。

此时再执行ping也正常。

image-20230304223640057

到此, 我们在一个临时的busybox Pod中验证了DNS的有效性。

另外,由于这个Pod与httpd-svc同属于default namespace,因此也可以省略default直接用httpd-svc访问Service;

image-20230304224100225

这里应该还是有点问题, 正确应该返回:

image-20230304224153307

DNS服务器是kube-dns.kube-system.svc.cluster.local,这实际上就是kube-dns组件,它本身是部署在kube-system namespace中的一个Service

httpd-svc.default.svc.cluster.localhttpd-svc的完整域名。如果要访问其他namespace中的Service,就必须带上namesapce了。kubectl get namespace查看已有的namespace

image-20230304224437319

我们尝试在kube-public中再部署一个Service httpd2-svc

image-20230304225343784

在busybox Pod中访问httpd2-svc, 如下所示:

image-20230304225546656

因为不属于同一个namespace,所以必须使用httpd2-svc.kube-public才能访问到。

外网如何访问Service

除了Cluster内部可以访问Service,很多情况下我们也希望应用的Service能够暴露给Cluster外部。Kubernetes提供了多种类型的Service,默认是ClusterIP。

(1)ClusterIPService通过Cluster内部的IP对外提供服务,只有Cluster内的节点和Pod可访问,这是默认的Service类型,前面实验中的Service都是ClusterIP。

(2)NodePortService通过Cluster节点的静态端口对外提供服务。Cluster外部可以通过:访问Service。

(3)LoadBalancerService利用cloud provider特有的load balancer对外提供服务,cloud provider负责将load balancer的流量导向Service。

目前支持的cloud provider有GCP、AWS、Azure等。

下面我们先来实践NodePort, Service httpd-svc的配置文件修改如下:

image-20230305002937765

image-20230305003641168

Kubernetes依然会为httpd-svc分配一个ClusterIP,不同的是:

(1)EXTERNAL-IP为nodes,表示可通过Cluster每个节点自身的IP访问Service。

(2)PORT(S)为8080:31954。8080是ClusterIP监听的端口,32312则是节点上监听的端口。Kubernetes会从30000~32767中分配一个可用的端口,每个节点都会监听此端口并将请求转发给Service,

image-20230305003738867

image-20230305003942092

通过三个节点IP + 32312端口都能够访问httpd-svc。

接下来我们深入探讨一个问题:Kubernetes是如何将:映射到Pod的呢?

与ClusterIP一样,也是借助了iptables。

与ClusterIP相比,每个节点的iptables中都增加了下面两条规则,

1
2
-A KUBE-SVC-IYRDZZKXS5EOQ6Q6 -p tcp -m comment --comment "default/httpd-svc" -m tcp --dport 31954 -j KUBE-MARK-MASQ
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/httpd-svc" -m tcp --dport 31954 -j KUBE-SVC-IYRDZZKXS5EOQ6Q6

规则的含义是:访问当前节点31954端口的请求会应用规则KUBE-SVC-IYRDZZKXS5EOQ6Q6,

image-20230305004558721

其作用就是负载均衡到每一个Pod。NodePort默认的是随机选择,不过我们可以用nodePort指定某个特定端口。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
apiVersion: v1  #service API版本, service.apiVersion  
kind: Service #类型为service  
metadata: #定义service元数据,service.metadata  
  name: httpd-svc  # 定义Service名称
spec:  #定义service的详细信息,service.spec
  type: NodePort 
  selector:  #service的标签选择器,定义要访问的目标pod
    app: httpd  #将流量路到选择的pod上,须等于Deployment.spec.selector.matchLabels
  ports:
    - protocol: TCP       # 定义协议
      nodePort: 30000
      port: 8080         # service端口
      targetPort: 80   # 目标pod端口

现在配置文件中就有三个Port了:

  • nodePort是节点上监听的端口。
  • port是ClusterIP上监听的端口。
  • targetPort是Pod监听的端口。

最终,Node和ClusterIP在各自端口上接收到的请求都会通过iptables转发到Pod的targetPort。

应用到新的nodePort并验证:

image-20230305005126675

可以看到,nodePort:30000已经生效了;