乐趣区

关于java:Kubernetes-是怎么实现服务发现的

作者:fredalxin

地址:https://fredal.xin/kubertnete…

咱们来说说 kubernetes 的服务发现。那么首先这个大前提是同主机通信以及跨主机通信都是 ok 的,即同一 kubernetes 集群中各个 pod 都是互通的。这点是由更底层的计划实现,包含 docker0/CNI 网桥、flannel vxlan/host-gw 模式等,在此篇就不开展讲了。

在各 pod 都互通的前提下,咱们能够通过拜访 podIp 来调用 pod 上的资源,那么离服务发现还有多少间隔呢?首先 Pod 的 IP 不是固定的,另一方面咱们拜访一组 Pod 实例的时候往往会有负载平衡的需要,那么 service 对象就是用来解决此类问题的。

集群内通信

endPoints

service 首先解决的是集群内通信的需要,首先咱们编写一个一般的 deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hostnames
spec:
  selector:
    matchLabels:
      app: hostnames
  replicas: 3
  template:
    metadata:
      labels:
        app: hostnames
    spec:
      containers:
        - name: hostnames
          image: mirrorgooglecontainers/serve_hostname
          ports:
            - containerPort: 9376
              protocol: TCP

这个利用干的事儿就是拜访它是返回本人的 hostname,并且每个 pod 都带上了 app 为 hostnames 的标签。

那么咱们为这些 pod 编写一个一般的 service:

apiVersion: v1
kind: Service
metadata:
  name: hostnames
spec:
  selector:
    app: hostnames
  ports:
    - name: default
      protocol: TCP
      port: 80
      targetPort: 9376

能够看到 service 通过 selector 抉择 了带相应的标签 pod,而这些被选中的 pod,成为 endpoints,咱们能够试一下:

~/cloud/k8s kubectl get ep hostnames
NAME        ENDPOINTS
hostnames   172.28.21.66:9376,172.28.29.52:9376,172.28.70.13:9376

当某一个 pod 呈现问题,不处于 running 状态或者 readinessProbe 未通过时,endpoints 列表会将其摘除。

clusterIp

以上咱们有了 service 和 endpoints,而默认创立 service 的类型是 clusterIp 类型,咱们查看一下之前创立的 service:

~ kubectl get svc hostnames
NAME        TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
hostnames   ClusterIP   10.212.8.127   <none>        80/TCP    8m2s

咱们看到 cluster-ip 是 10.212.8.127,那么咱们此时能够在 kubernetes 集群内通过这个地址拜访到 endpoints 列表里的任意 pod:

sh-4.2# curl 10.212.8.127
hostnames-8548b869d7-9qk6b
sh-4.2# curl 10.212.8.127
hostnames-8548b869d7-wzksp
sh-4.2# curl 10.212.8.127
hostnames-8548b869d7-bvlw8

拜访了三次 clusterIp 地址,返回了三个不同的 hostname,咱们意识到 clusterIp 模式的 service 主动对申请做了 round robin 模式的负载平衡。

对于此时 clusterIp 模式 serivice 来说,它有一个 A 记录是service-name.namespace-name.svc.cluster.local,指向 clusterIp 地址:

sh-4.2# nslookup hostnames.coops-dev.svc.cluster.local
Server:        10.212.0.2
Address:    10.212.0.2#53

Name:    hostnames.coops-dev.svc.cluster.local
Address: 10.212.8.127

天经地义咱们通过此 A 记录去拜访失去的成果一样:

sh-4.2# curl hostnames.coops-dev.svc.cluster.local
hostnames-8548b869d7-wzksp

那对 pod 来说它的 A 记录是啥呢,咱们能够看一下:

sh-4.2# nslookup 172.28.21.66
66.21.28.172.in-addr.arpa    name = 172-28-21-66.hostnames.coops-dev.svc.cluster.local.

headless service

service 的 cluserIp 默认是 k8s 主动调配的,当然也能够本人设置,当咱们将 clusterIp 设置成 none 的时候,它就变成了 headless service。

headless service 个别配合 statefulSet 应用。statefulSet 是一种有状态利用的容器编排形式,其核心思想是给予 pod 指定的编号名称,从而让 pod 有一个不变的惟一网络标识码。那这么说来,应用 cluserIp 负载平衡拜访 pod 的形式显然是行不通了,因为咱们渴望通过某个标识间接拜访到 pod 自身,而不是一个虚构 vip。

这个时候咱们其实能够借助 dns,每个 pod 都会有一条 A 记录 pod-name.service-name.namespace-name.svc.cluster.local 指向 podIp,咱们能够通过这条 A 记录间接拜访到 pod。

咱们编写相应的 statefulSet 和 service 来看一下:

---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: hostnames
spec:
  serviceName: "hostnames"
  selector:
    matchLabels:
      app: hostnames
  replicas: 3
  template:
    metadata:
      labels:
        app: hostnames
    spec:
      containers:
        - name: hostnames
          image: mirrorgooglecontainers/serve_hostname
          ports:
            - containerPort: 9376
              protocol: TCP

---
apiVersion: v1
kind: Service
metadata:
  name: hostnames
spec:
  selector:
    app: hostnames
  clusterIP: None
  ports:
    - name: default
      protocol: TCP
      port: 80
      targetPort: 9376

如上,statefulSet 和 deployment 并没有什么不同,多了一个字段 spec.serviceName,这个字段的作用就是通知 statefuleSet controller,在逻辑解决时应用hostnames 这个 service 来保障 pod 的惟一可解析性。

当你执行 apply 之后,一会你就能够看到生成了对应的 pod:

~ kubectl get pods -w -l app=hostnames
NAME          READY   STATUS    RESTARTS   AGE
hostnames-0   1/1     Running   0          9m54s
hostnames-1   1/1     Running   0          9m28s
hostnames-2   1/1     Running   0          9m24s

如意料之中,这里对 pod 名称进行了递增编号,并不反复,同时这些 pod 的创立过程也是依照编号顺次串行进行的。咱们晓得,应用 deployment 部署的 pod 名称会加上 replicaSet 名称和随机数,重启后是一直变动的。而这边应用 statefulSet 部署的 pod,尽管 podIp 依然会变动,但名称是始终不会变的,基于此咱们得以通过固定的 Dns A 记录来拜访到每个 pod。

那么此时,咱们来看一下 pod 的 A 记录:

sh-4.2# nslookup hostnames-0.hostnames
Server:        10.212.0.2
Address:    10.212.0.2#53

Name:    hostnames-0.hostnames.coops-dev.svc.cluster.local
Address: 172.28.3.57

sh-4.2# nslookup hostnames-1.hostnames
Server:        10.212.0.2
Address:    10.212.0.2#53

Name:    hostnames-1.hostnames.coops-dev.svc.cluster.local
Address: 172.28.29.31

sh-4.2# nslookup hostnames-2.hostnames
Server:        10.212.0.2
Address:    10.212.0.2#53

Name:    hostnames-2.hostnames.coops-dev.svc.cluster.local
Address: 172.28.23.31

和之前的推论统一,咱们能够通过 pod-name.service-name.namespace-name.svc.cluster.local 这条 A 记录拜访到 podIp,在同一个 namespace 中,咱们能够简化为pod-name.service-name

而这个时候,service 的 A 记录是什么呢:

sh-4.2# nslookup hostnames
Server:        10.212.0.2
Address:    10.212.0.2#53

Name:    hostnames.coops-dev.svc.cluster.local
Address: 172.28.29.31
Name:    hostnames.coops-dev.svc.cluster.local
Address: 172.28.3.57
Name:    hostnames.coops-dev.svc.cluster.local
Address: 172.28.23.31

原来是 endpoints 列表里的一组 podIp,也就是说此时你仍然能够通过 service-name.namespace-name.svc.cluster.local 这条 A 记录来负载平衡地拜访到后端 pod。

iptables

或多或少咱们晓得 kubernetes 外面的 service 是基于 kube-proxy 和 iptables 工作的。service 创立之后能够被 kube-proxy 感知到,那么它会为此在宿主机上创立对应的 iptables 规定。

以 cluserIp 模式的 service 为例,首先它会创立一条 KUBE-SERVICES 规定作为入口:

-A KUBE-SERVICES -d 10.212.8.127/32 -p tcp -m comment --comment "default/hostnames: cluster IP" -m tcp --dport 80 -j KUBE-SVC-NWV5X2332I4OT4T3

这条记录的意思是: 所有目标地址是 10.212.8.127 这条 cluserIp 的,都将跳转到KUBE-SVC iptables 链解决。

那么咱们来看 KUBE-SVC链都是什么:

-A KUBE-SVC-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames:" -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-WNBA2IHDGP2BOBGZ
-A KUBE-SVC-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-X3P2623AGDH6CDF3
-A KUBE-SVC-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames:" -j KUBE-SEP-57KPRZ3JQVENLNBR

这组规定其实是用于负载平衡的,咱们看到了 –probability 顺次是 1/3、1/2、1,因为 iptables 规定是自上而下匹配的,所以设置这些值能保障每条链匹配到的几率一样。解决完负载平衡的逻辑后,又别离将申请转发到了另外三条规定,咱们来看一下:

-A KUBE-SEP-57KPRZ3JQVENLNBR -s 172.28.21.66/32 -m comment --comment "default/hostnames:" -j MARK --set-xmark 0x00004000/0x00004000
-A KUBE-SEP-57KPRZ3JQVENLNBR -p tcp -m comment --comment "default/hostnames:" -m tcp -j DNAT --to-destination 172.28.21.66:9376

-A KUBE-SEP-WNBA2IHDGP2BOBGZ -s 172.28.29.52/32 -m comment --comment "default/hostnames:" -j MARK --set-xmark 0x00004000/0x00004000
-A KUBE-SEP-WNBA2IHDGP2BOBGZ -p tcp -m comment --comment "default/hostnames:" -m tcp -j DNAT --to-destination 172.28.29.52:9376

-A KUBE-SEP-X3P2623AGDH6CDF3 -s 172.28.70.13/32 -m comment --comment "default/hostnames:" -j MARK --set-xmark 0x00004000/0x00004000
-A KUBE-SEP-X3P2623AGDH6CDF3 -p tcp -m comment --comment "default/hostnames:" -m tcp -j DNAT --to-destination 172.28.70.13:9376

能够看到 KUBE-SEP链 就是三条 DNAT 规定,并在 DNAT 之前设置了一个 0x00004000 的标记。DNAT 规定就是在 PREROUTING,即路由作用之前,将申请的目标地址和端口改为 –to-destination 指定的 podIp 和端口。这样一来,咱们起先拜访 10.212.8.127 这个 cluserIp 的申请,就会被负载平衡到各个 pod 上。

那么 pod 重启了,podIp 变了怎么办?天然是 kube-proxy 负责监听 pod 变动以及更新保护 iptables 规定了。

而对于 headless service 来说,咱们间接通过固定的 A 记录拜访到了 pod,天然不须要这些 iptables 规定了。

iptables 了解起来比较简单,但实际上性能并不好。能够设想,当咱们的 pod 十分多时,成千上万的 iptables 规定将被创立进去,并一直刷新,会占用宿主机大量的 cpu 资源。一个卓有成效的计划是基于 IPVS 模式的 service,IPVS 不须要为每个 pod 都设置 iptables 规定,而是将这些规定都放到了内核态,极大升高了保护这些规定的老本。

集群间通信

外界拜访 service

以上咱们讲了申请怎么在 kubernetes 集群内互通,次要基于 kube-dns 生成的 dns 记录以及 kube-proxy 保护的 iptables 规定。而这些信息都是作用在集群内的,那么天然咱们从集群外拜访不到一个具体的 service 或者 pod 了。

service 除了默认的 cluserIp 模式外,还提供了很多其余的模式,比方 nodePort 模式,就是用于解决该问题的。

apiVersion: v1
kind: Service
metadata:
  name: hostnames
spec:
  selector:
    app: hostnames
  type: NodePort
  ports:
    - nodePort: 8477
      protocol: TCP
      port: 80
      targetPort: 9376

咱们编写了一个 nodePort 模式的 service,并且设置 nodePort 为 8477,那么意味着咱们能够通过任意一台宿主机的 8477 端口拜访到 hostnames 这个 service。

sh-4.2# curl 10.1.6.25:8477
hostnames-8548b869d7-j5lj9
sh-4.2# curl 10.1.6.25:8477
hostnames-8548b869d7-66vnv
sh-4.2# curl 10.1.6.25:8477
hostnames-8548b869d7-szz4f

咱们轻易找了一台 node 地址去拜访,失去了雷同的返回配方。
那么这个时候它的 iptables 规定是怎么作用的呢:

-A KUBE-NODEPORTS -p tcp -m comment --comment "default/hostnames: nodePort" -m tcp --dport 8477 -j KUBE-SVC-67RL4FN6JRUPOJYM

kube-proxy 在每台宿主机上都生成了如上的 iptables 规定,通过 –dport 指定了端口,拜访该端口的申请都会跳转到 KUBE-SVC 链上,KUBE-SVC链和之前 cluserIp service 的配方一样,接下来就和拜访 cluserIp service 没什么区别了。

不过还须要留神的是,在申请来到以后宿主机发往其余 node 时会对其做一次 SNAT 操作:

-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark --mark 0x4000/0x4000 -j MASQUERADE

能够看到这条 postrouting 规定给行将来到主机的申请进行了一次 SNAT,判断条件为带有 0x4000 标记,这就是之前 DNAT 带的标记,从而判断申请是从 service 转发进去的,而不是一般申请。

须要做 SNAT 的起因很简略,首先这是一个内部的未经 k8s 解决的申请,
如果它拜访 node1,node1 的负载平衡将其转发给 node2 上的某个 pod,这没什么问题,而这个 pod 解决完后间接返回给内部 client,那么内部 client 就很纳闷,明明本人拜访的是 node1,给本人返回确实是 node2,这时往往会报错。

SNAT 的作用与 DNAT 相同,就是在申请从 node1 来到发往 node2 时,将源地址改为 node1 的地址,那么当 node2 上的 pod 返回时,会返回给 node1,而后再让 node1 返回给 client。

client
                | ^
                | |
                v |
   node 2 <--- node 1
    | ^   SNAT
    | |   --->
    v |
 endpoints

service 还有另外 2 种通过外界拜访的形式。实用于私有云的 LoadBalancer 模式的 service,私有云 k8s 会调用 CloudProvider 在私有云上为你创立一个负载平衡服务,并且把被代理的 Pod 的 IP 地址配置给负载平衡服务做后端。另外一种是 ExternalName 模式,能够通过在 spec.externalName 来指定你想要的内部拜访域名,例如 hostnames.example.com,那么你拜访该域名和拜访service-name.namespace-name.svc.cluser.local 成果是一样的,这时候你应该晓得,其实 kube-dns 为你增加了一条 CNAME 记录。

ingress

service 有一种类型叫作 loadBalancer,不过如果每个 service 对外都配置一个负载平衡服务,老本很高而且节约。一般来说咱们心愿有一个全局的负载均衡器,通过拜访不同 url,转发到不同 service 上,而这就是 ingress 的性能,ingress 能够看做是 service 的 service。

ingress 其实是对反向代理的一种形象,置信大家曾经感觉到,这玩意儿和 nginx 十分相似,实际上 ingress 是形象层,而其实现层其中之一就反对 nginx。

咱们能够部署一个 nginx ingress controller:

$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/mandatory.yaml

mandatory.yaml 是官网保护的 ingress controller,咱们看一下:

kind: ConfigMap
apiVersion: v1
metadata:
  name: nginx-configuration
  namespace: ingress-nginx
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: nginx-ingress-controller
  namespace: ingress-nginx
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: ingress-nginx
      app.kubernetes.io/part-of: ingress-nginx
  template:
    metadata:
      labels:
        app.kubernetes.io/name: ingress-nginx
        app.kubernetes.io/part-of: ingress-nginx
      annotations:
        ...
    spec:
      serviceAccountName: nginx-ingress-serviceaccount
      containers:
        - name: nginx-ingress-controller
          image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.20.0
          args:
            - /nginx-ingress-controller
            - --configmap=$(POD_NAMESPACE)/nginx-configuration
            - --publish-service=$(POD_NAMESPACE)/ingress-nginx
            - --annotations-prefix=nginx.ingress.kubernetes.io
          securityContext:
            capabilities:
              drop:
                - ALL
              add:
                - NET_BIND_SERVICE
            # www-data -> 33
            runAsUser: 33
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
            - name: http
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
          ports:
            - name: http
              containerPort: 80
            - name: https
              containerPort: 443

总的来说,咱们定义了一个基于 nginx-ingress-controller 镜像的 pod,
而这个 pod 本身,是一个监听 ingress 对象及其代理后端 service 变动的控制器。

当一个 ingress 对象被创立时,nginx-ingress-controller 就会依据 ingress 对象里的内容,生成一份 nginx 配置文件(nginx.conf),并依此启动一个 nginx 服务。

当 ingress 对象被更新时,nginx-ingress-controller 就会更新这个配置文件。nginx-ingress-controller 还通过 nginx lua 计划实现了 nginx upstream 的动静配置。

为了让外界能够拜访到这个 nginx,咱们还得给它创立一个 service 来把 nginx 裸露进来:

$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/provider/baremetal/service-nodeport.yaml

这外面的内容形容了一个 nodePort 类型的 service:

apiVersion: v1
kind: Service
metadata:
  name: ingress-nginx
  namespace: ingress-nginx
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
spec:
  type: NodePort
  ports:
    - name: http
      port: 80
      targetPort: 80
      protocol: TCP
    - name: https
      port: 443
      targetPort: 443
      protocol: TCP
  selector:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx

能够看到这个 service 仅仅是把 nginx pod 的 80/443 端口裸露进来,完了你就能够通过宿主机 Ip 和 nodePort 端口拜访到 nginx 了。

接下来咱们来看 ingress 对象个别是如何编写的,咱们能够参考一个例子

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: cafe-ingress
spec:
  tls:
  - hosts:
    - cafe.example.com
    secretName: cafe-secret
  rules:
  - host: cafe.example.com
    http:
      paths:
      - path: /tea
        backend:
          serviceName: tea-svc
          servicePort: 80
      - path: /coffee
        backend:
          serviceName: coffee-svc
          servicePort: 80

这个 ingress 表明咱们整体的域名是 cafe.example.com,心愿通过cafe.example.com/tea 拜访 tea-svc 这个 service,通过 cafe.example.com/coffee 拜访 coffee-svc 这个 service。这里咱们通过关键字段 spec.rules 来编写转发规定。

咱们能够查看到 ingress 对象的详细信息:

$ kubectl get ingress
NAME           HOSTS              ADDRESS   PORTS     AGE
cafe-ingress   cafe.example.com             80, 443   2h

$ kubectl describe ingress cafe-ingress
Name:             cafe-ingress
Namespace:        default
Address:
Default backend:  default-http-backend:80 (<none>)
TLS:
  cafe-secret terminates cafe.example.com
Rules:
  Host              Path  Backends
  ----              ----  --------
  cafe.example.com
                    /tea      tea-svc:80 (<none>)
                    /coffee   coffee-svc:80 (<none>)
Annotations:
Events:
  Type    Reason  Age   From                      Message
  ----    ------  ----  ----                      -------
  Normal  CREATE  4m    nginx-ingress-controller  Ingress default/cafe-ingress

咱们之前讲了咱们通过 nodePort 的形式将 nginx-ingress 裸露进来了,而这时候咱们 ingress 配置又心愿通过 cafe.example.com 来拜访到后端 pod,那么首先 cafe.example.com 这个域名得指到 任意一台宿主机 Ip:nodePort上,申请达到 nginx-ingress 之后再转发到各个后端 service 上。当然,裸露 nginx-ingress 的形式有很多种,除了 nodePort 外还包含 loadBalancer、hostNetWork 形式等等。

咱们最初来试一下申请:

$ curl cafe.example.com/coffee
Server name: coffee-7dbb5795f6-vglbv
$ curl cafe.example.com/tea
Server name: tea-7d57856c44-lwbnp

能够看到 nginx ingress controller 曾经为咱们胜利将申请转发到了对应的后端 service。而当申请没有匹配到任何一条 ingress rule 的时候,天经地义咱们会失去一个 404。

至此,kubernetes 的容器网络是怎么实现服务发现的曾经讲完了,而服务发现正是微服务架构中最外围的问题,解决了这个问题,那么应用 kubernetes 来实现微服务架构也就实现了一大半。

近期热文举荐:

1.600+ 道 Java 面试题及答案整顿(2021 最新版)

2. 终于靠开源我的项目弄到 IntelliJ IDEA 激活码了,真香!

3. 阿里 Mock 工具正式开源,干掉市面上所有 Mock 工具!

4.Spring Cloud 2020.0.0 正式公布,全新颠覆性版本!

5.《Java 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

退出移动版