问题起源于我用 envoy 对 grpc 做 Layer 7 负载均衡的时候,发现 traffic 永远被转发到了一个特定的 Pod,显然是配置出错了。

环境如下:

同一个 namespace 下部署了 2 个 grpc server,一个 envoy

$ kubectl get pod
NAME                             READY   STATUS    RESTARTS   AGE
grpc-envoy-7684f49cb-9fv4h     1/1     Running   0          4h49m
grpc-server-668bdd6576-2bvkz   1/1     Running   0          4h51m
grpc-server-668bdd6576-tqzj4   1/1     Running   0          4h51m

envoy 的 service 配置如下

apiVersion: v1
kind: Service
metadata:
  name: grpc-envoy
  namespace: default
  labels:
    app: grpc-envoy
spec:
  type: NodePort
  ports:
    - name: grpc
      port: 8080
      targetPort: grpc
      nodePort: 30061
      protocol: TCP
  selector:
    app: grpc-envoy

grpc-server 的 service 配置如下

apiVersion: v1
kind: Service
metadata:
  name: grpc-server
  namespace: default
spec:
  type: ClusterIP
  ports:
    - port: 8080
      targetPort: 8080
      protocol: TCP
  selector:
    app: grpc-server

其他的 deployment YAML file 省略。

这些都是非常简单的配置,看上去也不应该有问题,但是无论怎样,traffic 总是转发到一个 Pod,而无法实现负载均衡。

再一次又一次的检查 YAML 配置之后,发现原来需要明确的把 grpc server 的 clusterIP 设置为 None,这个叫 Headless Service

Headless Service

Headless Service 也是一种 Service,需要显式的在 YAML 中指定 spec:clusterIP: None,也就是不需要 Cluster IP 的 Service。

我们知道,ClusterIP 的作用是在 kubernetes 内部提供一组 Pod 对外服务的统一 IP地址,并且配合 CoreDNS, 让我们在 Pod 内部能用 SVC-NAME.NAMESPACE.svc.cluster.local 形式的 FQDN 访问其他服务。

而如果把 clusterIP 设置为 None,则查询 FQDN 的时候, DNS 会返回所有符合条件的 Pod 的地址。

下面就来做个试验,

ClusterIP 不为 None

先把 grpc server 的 Service 配置改为默认的,也就是自带 ClusterIP

$ kubectl get svc
NAME            TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
grpc-envoy    NodePort    10.97.207.187    <none>        8080:30061/TCP   4h33m
grpc-worker   ClusterIP   10.108.192.173   <none>        8080/TCP         4s

然后随便进入 k8s 集群中其他某一个 Pod,用 nslookup 查询 DNS 记录

root@flask-server-786f96dd84-j4xqk:/home# nslookup  grpc-server.default.svc.cluster.local
Server:		10.96.0.10
Address:	10.96.0.10#53

Name:	grpc-server.default.svc.cluster.local
Address: 10.108.192.173

可以看到,FQDN grpc-server.default.svc.cluster.local 返回的是 ClusterIP。

ClusterIP 为 None

接着,删除旧的 grpc server Service, apply 下面这个 YAML。

注意: 无法在旧的 YAML 添加 clusterIP:None 然后 apply,必须删掉 k8s 中旧的 svc,然后 apply 新的。

apiVersion: v1
kind: Service
metadata:
  name: grpc-worker
  namespace: default
spec:
  type: ClusterIP
  clusterIP: None
  ports:
    - port: 8080
      targetPort: 8080
      protocol: TCP
  selector:
    app: grpc-worker

之后,我们看到 svc 里面的 clusterIP 为空

$ kubectl get svc
NAME            TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
grpc-envoy    NodePort    10.97.207.187    <none>        8080:30061/TCP   4h42m
grpc-server   ClusterIP   None             <none>        8080/TCP         3m32s

再次进入到某个 Pod 内,进行 DNS 查询,可以看到,这次 DNS 返回来所有的 Pod IP。

root@flask-server-786f96dd84-j4xqk:/home# nslookup grpc-server.default.svc.cluster.local
Server:		10.96.0.10
Address:	10.96.0.10#53

Name:	grpc-server.default.svc.cluster.local
Address: 10.40.0.1
Name:	grpc-server.default.svc.cluster.local
Address: 10.44.0.2

这样就可以由外部的 envoy 决定把 traffic 转发到哪个 Pod。

我们用 client 在外部调用 grpc,从返回结果来看确实实现了负载均衡。

$ ./client -s 192.168.31.54:30061
2020/09/21 04:43:43 resp from grpc-server-668bdd6576-2bvkz
2020/09/21 04:43:43 resp from grpc-server-668bdd6576-tqzj4
2020/09/21 04:43:43 resp from grpc-server-668bdd6576-tqzj4
2020/09/21 04:43:43 resp from grpc-server-668bdd6576-2bvkz
2020/09/21 04:43:43 resp from grpc-server-668bdd6576-2bvkz
2020/09/21 04:43:43 resp from grpc-server-668bdd6576-tqzj4
2020/09/21 04:43:43 resp from grpc-server-668bdd6576-2bvkz
2020/09/21 04:43:43 resp from grpc-server-668bdd6576-tqzj4
2020/09/21 04:43:43 resp from grpc-server-668bdd6576-2bvkz
2020/09/21 04:43:43 resp from grpc-server-668bdd6576-tqzj4

参考资料