GRPC动态负载平衡
#go #kubernetes #grpc #loadbalancing

grpc

GRPC有很多好处,例如:

  1. 使用相同连接的许多请求多路复用。
  2. 支持典型的客户服务器请求响应以及双工流。
  3. 使用一个快速,非常轻的二进制协议,其结构化数据作为服务之间的通信媒介。

More about gRPC

上面的所有内容都使GRPC成为非常吸引人的交易,但是GRPC特别考虑负载平衡。

问题

让我们深入研究这个问题。

为此,我们需要设置。设置包括以下:

  • GRPC服务器,我们称其为Greet Server
  • 一个充当REST网关的客户,在内部也是GRPC客户端。我们称其为Greet Client

我们还在使用kubernetes进行演示,因此有大量的yaml清单文件。让我在下面解释一下:

招呼服务员deploy.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: greetserver-deploy
spec:
  replicas: 3
  selector:
    matchLabels:
      run: greetserver
  template:
    metadata:
      labels:
        run: greetserver
    spec:
      containers:
        - image: hiteshpattanayak/greet-server:1.0
          imagePullPolicy: IfNotPresent
          name: greetserver
          ports:
            - containerPort: 50051
          env:
            - name: POD_IP
              valueFrom:
                fieldRef:
                  fieldPath: status.podIP
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name

以上是Greet Server的部署主fest,它旋转了3个Greet Server的复制品。
Greet Server使用hiteshpattanayak/greet-server:1.0图像。
另外,部署的每个豆荚都暴露了50051端口。
环境变量:pod_ip和pod_name被注入到吊舱中。

上面服务器中的每个吊舱都做什么?

他们暴露了期望first_namelast_namerpc或服务,作为回应,他们以这种格式返回一条消息:
reponse from Greet rpc: Hello, <first_name> <last_name> from pod: name(<pod_name>), ip(<pod_ip>).

从回应中,我们可以推断出哪个POD完成了我们的请求。

etch.svc.yaml

apiVersion: v1
kind: Service
metadata:
  labels:
    run: greetserver
  name: greetserver
  namespace: default
spec:
  ports:
    - name: grpc
      port: 50051
      protocol: TCP
      targetPort: 50051
  selector:
    run: greetserver

以上是Greet server service的服务清单。这基本上充当了Greet Server Pods上方的代理。

服务的selector部分与每个吊舱的labels部分匹配。

engerclient-deploy.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: greetclient-deploy
spec:
  replicas: 1
  selector:
    matchLabels:
      run: greetclient
  template:
    metadata:
      labels:
        run: greetclient
    spec:
      containers:
        - image: hiteshpattanayak/greet-client:4.0
          name: greetclient
          ports:
            - containerPort: 9091
          env:
            - name: GRPC_SERVER_HOST
              value: greetserver.default.svc.cluster.local
            - name: GRPC_SVC
              value: greetserver
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace

以上是Greet Client的部署主fest,它旋转了1个Greet Client的复制品。

如上所述,POD运行了充当REST网关的应用程序,并与Greet Server联系以处理请求。

此部署使用hiteshpattanayak/greet-client:4.0图像。

4.0标记的图像具有负载平衡问题。

还pod(s)暴露端口9091

engerclient-svc.yaml

apiVersion: v1
kind: Service
metadata:
  labels:
    run: greetclient
  name: greetclient
  namespace: default
spec:
  ports:
    - name: restgateway
      port: 9091
      protocol: TCP
      targetPort: 9091
  selector:
    run: greetclient
  type: LoadBalancer

上面的服务只是将流量重定向到Greet Client pods。

问候 - ingress.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: greet-ingress
  namespace: default
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "false"
spec:
  rules:
    - host: greet.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: greetclient
                port:
                  name: restgateway

上面的入口是将Greet Client Service暴露于集群外部。

注意:
默认情况下,minikube没有启用入口

  • 启用是否检查:minikube addons list
  • 启用入口插件:minikube addons enable ingress

etch-clusterrole.yaml

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: default
  name: service-reader
rules:
  - apiGroups: [""]
    resources: ["services"]
    verbs: ["get", "watch", "list"]

entry-clusterrolebinding.yaml

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: service-reader-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: service-reader
subjects:
  - kind: ServiceAccount
    name: default
    namespace: default

需要群集角色和群集角色绑定,因为default服务帐户无权获取服务详细信息。
在内部试图获取服务详细信息,因此需要绑定。

在下面的序列中创建设置:


kubectl create -f greet-clusterrole.yaml

kubectl create -f greet-clusterrolebinding.yaml

kubectl create -f greetserver-deploy.yaml

kubectl get po -l 'run=greetserver' -o wide
<<com
NAME                                  READY   STATUS    RESTARTS   AGE   IP           NODE       NOMINATED NODE   READINESS GATES
greetserver-deploy-7595ccbdd5-67bmd   1/1     Running   0          91s   172.17.0.4   minikube   <none>           <none>
greetserver-deploy-7595ccbdd5-k6zbl   1/1     Running   0          91s   172.17.0.3   minikube   <none>           <none>
greetserver-deploy-7595ccbdd5-l8kmv   1/1     Running   0          91s   172.17.0.2   minikube   <none>           <none>
com

kubectl create -f greet.svc.yaml
kubectl get svc
<<com
NAME          TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)     AGE
greetserver   ClusterIP   None         <none>        50051/TCP   77s
com

kubectl create -f greetclient-deploy.yaml
kubectl get po -l 'run=greetclient' -o wide
<<com
NAME                                 READY   STATUS    RESTARTS   AGE   IP           NODE       NOMINATED NODE   READINESS GATES
greetclient-deploy-6bddb94df-jwr25   1/1     Running   0          35s   172.17.0.6   minikube   <none>           <none>
com

kubectl create -f greet-client.svc.yaml
kubectl get svc
<<com
NAME          TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
greetclient   LoadBalancer   10.110.255.115   <pending>     9091:32713/TCP   22s
greetserver   ClusterIP      None             <none>        50051/TCP        5m14s
com

kubectl create -f greet-ingress.yaml
kubectl get ingress
<<com
NAME            CLASS   HOSTS       ADDRESS        PORTS   AGE
greet-ingress   nginx   greet.com   192.168.49.2   80      32s
com

由于我们通过greet-ingressGreet Client暴露于群集外部,因此可以通过以下方式访问端点。
因此,当我们提出卷曲请求时:

请求#1

curl --request POST \
  --url http://greet.com/greet \
  --header 'Content-Type: application/json' \
  --data '{
    "first_name": "Hitesh",
    "last_name": "Pattanayak"
}'

<<com
Response

reponse from Greet rpc: Hello, Hitesh Pattanayak from pod: name(greetserver-deploy-7595ccbdd5-l8kmv), ip(172.17.0.2).
com

请求#2

curl --request POST \
  --url http://greet.com/greet \
  --header 'Content-Type: application/json' \
  --data '{
    "first_name": "Hitesh",
    "last_name": "Pattanayak"
}'

<<com
Response

reponse from Greet rpc: Hello, Hitesh Pattanayak from pod: name(greetserver-deploy-7595ccbdd5-l8kmv), ip(172.17.0.2).
com

请求#3

curl --request POST \
  --url http://greet.com/greet \
  --header 'Content-Type: application/json' \
  --data '{
    "first_name": "Hitesh",
    "last_name": "Pattanayak"
}'

<<com
Response

reponse from Greet rpc: Hello, Hitesh Pattanayak from pod: name(greetserver-deploy-7595ccbdd5-l8kmv), ip(172.17.0.2).
com

因此,无论我提出的许多要求,问题都在同一服务器中。这是因为http/2的粘性性质。
GRPC的优势成为自己的危险。

可以找到复制问题的代码库。

GRPC客户侧负载平衡

我们之前已经讨论了与负载平衡的GRPC挑战之一。

由于GRPC连接的粘性性质而发生的。

现在我们将讨论如何解决该问题。

这个特殊的解决方案很简单。

加载余额的责任落在客户本身上。

特别是,客户端并不意味着最终用户。所有GRPC服务器都有最终用户使用的REST网关。

这是因为GRPC使用的协议HTTP2尚未具有浏览器支持。

因此,休息网关充当GRPC服务器的GRPC客户端。这就是为什么GRPC主要用于内部通信的原因。

早些时 可以将代码转介给here

变化

代码更改

对于此解决方案,我们使用hiteshpattanayak/greet-client:11.0图像。 codebase的变化以下:

更新的客户部署清单:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: greetclient-deploy
spec:
  replicas: 1
  selector:
    matchLabels:
      run: greetclient
  template:
    metadata:
      labels:
        run: greetclient
    spec:
      containers:
        - image: hiteshpattanayak/greet-client:11.0
          name: greetclient
          ports:
            - containerPort: 9091
          env:
            - name: GRPC_SVC
              value: greetserver
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
  • 在拨打服务器时配置负载平衡策略。
  • 在拨打服务器时配置以终止连接。
a.conn, err = grpc.Dial(
        servAddr,
        grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
        grpc.WithBlock(),
        opts,
    )
  • 拨号时使用的服务器地址需要服务器的DNS地址。
var serverHost string
if host := kubernetes.GetServiceDnsName(client, os.Getenv("GRPC_SVC"), os.Getenv("POD_NAMESPACE")); len(host) > 0 {
        serverHost = host
    }

servAddr := fmt.Sprintf("%s:%s", serverHost, serverPort)

无头服务

  • 同样早些时候复制我们为Greet server pods创建的服务(engerserver)的问题时,也是普通的ClusterIP类型。此解决方案需要无头聚类服务。
apiVersion: v1
kind: Service
metadata:
  labels:
    run: greetserver
  name: greetserver
  namespace: default
spec:
  ports:
    - name: grpc
      port: 50051
      protocol: TCP
      targetPort: 50051
  selector:
    run: greetserver
  clusterIP: None

这里要注意的一件重要的事情是,这是一种特殊类型的ClusterIP服务,称为Headless服务。

在这种service中,未指定服务类型。默认情况下,类型变为ClusterIP。这意味着该服务在集群中可用。

如果您已经有要重复使用的现有DNS条目,则可以设置.spec.clusterIP

如果将.spec.clusterIP设置为None,它可以制造服务headless,这意味着当客户端将请求发送到无头服务时,它将恢复该服务代表的所有POD的列表(在这种情况下,带有标签run: greetserver)。

kubernetes允许客户通过DNS查找发现POD IP。通常,当您执行DNS查找服务时,DNS服务器会返回单个IP'服务群集IP。但是,如果您告诉kubernetes,您不需要一个群集IP来服务(您可以通过将clusterip字段设置为服务规范中的non),而DNS服务器将返回POD IPS而不是单个服务IP。 DNS服务器不会返回单个DNS记录,而是将返回该服务的多个记录,每个指向当时的单个POD的IP。因此,客户可以进行简单的DNS查找,并获取服务中所有POD的IPS。然后,客户可以使用该信息连接到一个,许多或全部。

基本上,该服务现在让客户决定如何连接到Pods。

验证无头服务DNS查找

创建无头服务:

kubectl create -f greet.svc.yaml

创建一个实用程序吊舱:

kubectl run dnsutils --image=tutum/dnsutils --command -- sleep infinity

通过在POD上运行nslookup命令来验证

kubectl exec dnsutils --  nslookup greetserver

<<com
Result

Server:         10.96.0.10
Address:        10.96.0.10#53
Name:   greetserver.default.svc.cluster.local
Address: 172.17.0.4
Name:   greetserver.default.svc.cluster.local
Address: 172.17.0.3
Name:   greetserver.default.svc.cluster.local
Address: 172.17.0.2

您可以看到无头服务解决到通过服务连接的所有POD的IP地址。

将其与返回的无头服务的输出对比。

kubectl exec dnsutils --  nslookup greetclient

<<com
Server:     10.96.0.10
Address:    10.96.0.10#53

Name:   greetclient.default.svc.cluster.local
Address: 10.110.255.115
com

现在让我们通过向裸露的入口提出卷曲请求来测试更改。

请求#1

curl --request POST \
  --url http://greet.com/greet \
  --header 'Content-Type: application/json' \
  --data '{
    "first_name": "Hitesh",
    "last_name": "Pattanayak"
}'

<<com
Response

reponse from Greet rpc: Hello, Hitesh Pattanayak from pod: name(greetserver-deploy-7595ccbdd5-k6zbl), ip(172.17.0.3).
com

请求#2

curl --request POST \
  --url http://greet.com/greet \
  --header 'Content-Type: application/json' \
  --data '{
    "first_name": "Hitesh",
    "last_name": "Pattanayak"
}'

<<com
Response

reponse from Greet rpc: Hello, Hitesh Pattanayak from pod: name(greetserver-deploy-7595ccbdd5-67bmd), ip(172.17.0.4).
com

请求#3

curl --request POST \
  --url http://greet.com/greet \
  --header 'Content-Type: application/json' \
  --data '{
    "first_name": "Hitesh",
    "last_name": "Pattanayak"
}'

<<com
Response

reponse from Greet rpc: Hello, Hitesh Pattanayak from pod: name(greetserver-deploy-7595ccbdd5-l8kmv), ip(172.17.0.2).
com

问题不再存在。

但是,我们在这里失去的是GRPC保留连接更长的能力,并通过它们多重多个请求,从而减少了延迟。

GRPC LookAside负载平衡

之前我们讨论了:

  • 与GRPC的负载平衡挑战
  • 如何通过客户侧负载平衡来应对上述挑战

即使我们能够解决负载平衡问题,但我们交易了GRPC的主要优势之一,这是长期连接的。

因此,在这篇文章中,我们希望达到负载平衡(仍然是客户端),但我们不会权衡以上提及的GRPC的优势。

当我说加载余额的责任落在client side上时,我想重新求助,客户端并不意味着最终用户。所有GRPC服务器都有最终用户使用的REST网关。由于缺乏浏览器支持,GRPC服务没有直接暴露。

看起来平衡

此负载平衡器的目的是解决要连接的GRPC服务器。

目前,此负载平衡器以两种方式工作:循环和随机。

负载平衡器本身是基于GRPC的,并且由于负载不会太多,只有一个POD就足够了。

它揭示了一个名为lookaside的服务和一个称为Resolve的RPC,该服务期望路由类型以及有关GRPC服务器(例如Kubernetes服务名称和名称空间)的一些详细信息。

使用服务名称和名称空间,它将获取与之关联的Kubernetes端点对象。从端点对象服务器IP可以找到。
这些IP将存储在内存中。这些IP时不时会根据间隔集进行刷新。对于解决IP的每个请求,它将根据请求中的路由类型旋转IPS。

可以找到lookaside负载平衡器的代码here

我们正在使用image hiteshpattanayak/lookaside:9.0用于lookaside pod。

豆荚清单就是这样:

apiVersion: v1
kind: Pod
metadata:
  labels:
    run: lookaside
  name: lookaside
  namespace: default
spec:
  containers:
    - image: hiteshpattanayak/lookaside:9.0
      name: lookaside
      ports:
        - containerPort: 50055
      env:
        - name: LB_PORT
          value: "50055"

由于它是GRPC服务器,因此暴露的端口为50055

公开POD的服务清单如下:

apiVersion: v1
kind: Service
metadata:
  labels:
    run: lookaside-svc
  name: lookaside-svc
  namespace: default
spec:
  ports:
    - port: 50055
      protocol: TCP
      targetPort: 50055
  selector:
    run: lookaside
  clusterIP: None

我也为此选择了headless服务,但对此没有这种需求。

更新了ClusterRole,以包括获取endpointspod详细信息的能力

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: default
  name: service-reader
rules:
  - apiGroups: [""]
    resources: ["services", "pods", "endpoints"]
    verbs: ["get", "watch", "list"]

招待客户的变化

Greet Client现在是integrated,带有LookAside LoadBalancer。

客户端是set使用RoundRobin路由类型,但可以通过configmap或环境变量进行配置。

删除设置load-balancing策略,并通过拨号时设置WithBlock选项强制终止连接。

来自

conn, err := grpc.Dial(
    servAddr,
    grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
    grpc.WithBlock(),
    opts,
)


conn, err := grpc.Dial(
    servAddr,
    opts,
)

因此,它如何解决较早的负载平衡问题,我们进行了终止长时间连接以实现负载平衡的交易。

我们所做的是将以前的连接存储到服务器并重复使用,但要为每个请求旋转。

if c, ok := a.greetClients[host]; !ok {
    servAddr := fmt.Sprintf("%s:%s", host, serverPort)

    fmt.Println("dialing greet server", servAddr)

    conn, err := grpc.Dial(
        servAddr,
        opts,
    )
    if err != nil {
        log.Printf("could not connect greet server: %v", err)
        return err
    }

    a.conn[host] = conn

    a.currentGreetClient = proto.NewGreetServiceClient(conn)
    a.greetClients[host] = a.currentGreetClient
} else {
    a.currentGreetClient = c
}

结论

GRPC是由于效率,速度和奇偶校验而用于微服务内部通信的绝佳解决方案。但是,持续时间连接虽然有优势,但持续的连接会导致棘手的负载平衡。在本文的帮助下,我们找到了处理它的方法。

有多种方法可以通过Linkerd和Istio等服务网格处理。但是,在不设置服务网格的情况下,使用解决方案是很方便的。

,如果您喜欢我的内容,您会考虑在LinkedIn上关注我。