实用指南 | 基于Kubernetes, GRPC 和 Linkerd 构建可扩展微服务

作者:Rik Nauta
翻译:周巍
原文:Building scalable micro-services with Kubernetes, GRPC & Linkerd
地址:https://medium.com/%40riknauta ... 79599


微信图片_20180305170110.jpg


这是一份关于如何在 K8S(注1)上实现 Docker(注2)微服务自动伸缩的实用指南,其中服务间通过 GRPC(注3)通信,通过 Linkerd(注4)实现负载均衡和服务发现,直到谷歌支持开箱即用的功能。

Warning:这是一篇高技术含量的博文。它不适合所有人但希望对一些人有用,因此如果你不理解标题,建议看这里(注5)或者给 Wikipedia(注6)捐点钱~

Update:关于这个问题 Google 答复了我,似乎可以用 Ingress 控制器代替 LB 来解决。我正在研究是否可以通过那种方法解决,如果可以,我将再写一片博文。




1、介绍

在这篇文章中,我将简要介绍下我们最近如何修改我们的数据处理 Pipeline,来加入一个新的自然语言处理(Natural Language Processing,NLP)微服务。Kubernetes、Docker 和 GRPC 本已经开箱即用支持我们的这种场景,但还是遇到了一些烦人的扩展伸缩问题。我们花了很长的时间来找到一个“production ready”的临时解决方案。为了让其他人避免类似问题,本文将介绍我们是如何做到的。同时,如果你有任何建议,请作评论(或者将你的 CV 发给我们)。


本文首先解释我们的设计方案,然后将会介绍我们当前的实现,最后描述一些未来改进的想法。


2、背景


1为什么选择Docker微服务?

如果你完全不了解 Docker 或者微服务,你应该快速阅读这篇文章(注7)。我们将巨无霸“MapReduce”修改成微服务 pipeline 有三大好处:


1. 可以用任何语言来实现 NLP 模型。将它抽象为 Docker 容器并对外提供 API,则不再需要关心实现的细节,只需要符合 API 约定,这就引入了 GRPC,稍后详细描述。

2. 可以轻松地对模型进行修改、升级和 A/B 测试。我们只需要用新版本替换 Docker 容器,甚至可以实时操作,这在流式数据处理和在线机器学习模型中非常方便。

3. 更好的伸缩性。这一优点并不总是,但至少在本例中能够体现。在我们的巨无霸中,我们不得不将100个模型加载到内存中,尽管这样能够解决问题,但它会导致很多难以加载/卸载的代码,这些代码并是我们核心目的的一部分。相反,针对不同的语言,使用100个不同的 NLP 微服务按需进行扩展要更加高效。

理论上来说,用 RPC 调用来“阻止(clog down)”高效的 MapReduce 任务并不是个好的方法,但实际中使用 Apache Beam 效果很好,它使得我们能够用 Beam 作为数据编排器从而能够执行 ETL 操作,而不是简单的 MapReduce 实现。


2为什么选择K8S?

为什么要选择 kubernetes ?因为它真的很棒!我仍然记得尝试 Docker Swarm 甚至 Amazon 的容器编排器的黑暗时光,这并不有趣。但 Kubernetes 却提供了我们部署和伸缩那些微服务所需的所有功能(甚至更多)。

我们并没有搭建自己的 Kubernetes 集群,而是使用 Google Container Engine(GCE) (注8)。当然,如果你觉得管理自己的 Kubernetes 集群更好的话,你可以使用 AWS 或者其他类似平台。但对我们来说,还有更重要的事情做,所以就选择了 GCE。

不幸的是,尽管 GCE 中已经集成了 K8S,我们还是发现不得不绞尽脑汁地找到好的办法来实现微服务的负载均衡,稍后详细解释。


3为什么选择GRPC?

对于微服务来说,相对于普通的 REST/JSON API,使用像 GRPC 一样的框架将会有许多优势。首先 GRPC 是一个非常高效的二进制协议,同时,你仅仅需要指定你的“服务API”就可以生成任何你所需要的语言绑定,例如 Java, Python, Go, Rust 等,与发送 JSON 并期望每个人都遵从最新的约定相比,GRPC 更加友好且更不容易出错,因为定义和实现是分离的。

当然,GRPC 不是唯一有这些特性的框架,Apache Thrift 是另一个常见的竞争者。也许你不认为,但我觉得 GRPC 将是未来的趋势。


4为什么选择Linkerd?

正如我之前提到的那样,我们在扩展伸缩微服务时遇到了一些问题,因为 GRPC 使用的新的 HTTP2 作为通信协议,虽然 HTTP2 相对 HTTP1.1 来说更快且更加高效,但几乎所有我们见过的与 Kubernetes 一起使用的“标准”负载均衡均是4层负载均衡(包括谷歌和亚马逊的“硬件”负载均衡器),而我们需要一个7层负载均衡器来实现 HTTP2 通信的负载均衡。

简单地说:客户端连接一个4层负载均衡器,之后再连一个后端服务,客户端的请求会不断重用与负载均衡的连接,这就意味着通信根本没有实现负载均衡。而7层负载均衡器则更加智能,能将请求发送到不同的服务器。


因此我们找了一些支持7层的负载均衡器。尽管 Lyft 的 Envoy 是个很不错的方案,但我们还是选择了 Linkerd,因为它看起来经过了更多的测试,而且至少还有一些关于 Kubernetes 和 GRPC 通信的文档作为参考。


3、实现

既然你了解了那些组件,并且知道我们为什么选择它们。以下是如何将它们组合起来的一个例子:在 Kubernetes 集群中部署自动伸缩,负载均衡的 GRPC 微服务。如果有必要,我可以将它放到 GitHub repo 中,但现在的话,先看看吧。

--

apiVersion: v1

kind: ConfigMap

metadata:

  name: linkerd-config

  namespace: app

data:

  config.yaml: |-

    admin:

      port: 9990

    namers:

      - kind: io.l5d.k8s

        experimental: true

        host: 127.0.0.1

        port: 8001

    routers:

    - protocol: h2

      experimental: true

      label: grpc

      client:

        loadBalancer:

          kind: ewma

          maxEffort: 10

          decayTimeMs: 15000

      servers:

        - port: 8080

          ip: 0.0.0.0

      baseDtab: |

      # this directs http2 traffic straight to the specified service

      # this can be changed to read the service name header and redirect

      # traffic to different services based on that:

      # /srv => /#/io.l5d.k8s/<namespace>/service;

      # /h2 => /srv ;

        /h2 => /#/io.l5d.k8s/<namespace>/<port-name>/<service-name>;

```







---

apiVersion: extensions/v1beta1

kind: Deployment

metadata:

  name: app

  namespace: app

  labels:

    type: app

spec:

  replicas: 3

  template:

    metadata:

      labels:

        type: app

      name: app

      annotations:

        #this annotation makes sure the containers get scheduled on a nodepool that

        #autoscales based on container requirements. This feature might not be available

        #outside of GCE...but why would you use anything else!

        scheduler.alpha.kubernetes.io/affinity: >

          {

            "nodeAffinity": {

              "requiredDuringSchedulingIgnoredDuringExecution": {

                "nodeSelectorTerms": [

                  {

                    "matchExpressions": [

                      {

                        "key": "cloud.google.com/gke-nodepool",

                        "operator": "In",

                        "values": ["autoscaling-nodepool"]

                      }

                    ]

                  }

                ]

              }

            }

          }

    spec:

      containers:

        - name: app

          image: company/microservice:0.0.1

          imagePullPolicy: Always

          ports: 

          resources:

            requests:

              memory: "1Gi"

            limits:

              memory: "1Gi"

          ports:

          - name: grpc

            containerPort: 50051

            

        - name: linkerd

          image: buoyantio/linkerd:latest

          args:

          - "/io.buoyant/linkerd/config/config.yaml"

          ports:

          - name: ext

            containerPort: 8080

          - name: admin

            containerPort: 9990

          volumeMounts:

          - name: "linkerd-config"

            mountPath: "/io.buoyant/linkerd/config"

            readOnly: true

        

        #This container is used by linkerd to resolve the service name

        #to an actual local IP

        - name: kubectl

          image: buoyantio/kubectl:1.2.3

          args:

          - "proxy"

          - "-p"

          - "8001"

      dnsPolicy: ClusterFirst

      volumes:

        - name: linkerd-config

          configMap:

            name: "linkerd-config"

  ```




---

kind: Service

apiVersion: v1

metadata:

  namespace: app

  name: app

spec:

  selector:

    type: app

  type: LoadBalancer

loadBalancerSourceRanges:

  #You can easily turn this into an internal load balancer so that any service (inside and outside of your K8S network)

  #can reach it. This again won't work outisde Google's super smart router which will optimize traffic to take the shortest route.

  #So even if you give an internal service the public IP of this loadbalancer the traffic will be optimized to use the internal route.

  #In contrast...on AWS you're f#ck'd, they just route you outside your private network and then back which means your public IP isn't accepted

  #and you have to fiddle with resolving private IP's manually...ugh

  - "0.0.0.0/0" 

  ports:

  - name: ext

    port: 80

    targetPort: 8080

  - name: grpc

    port: 50051

    targetPort: grpc

```




# The admin shouldn't be reachable outside of Kubernetes. Just use `kubectl proxy` or `kubectl port-forward` to access it securely!

---

kind: Service

apiVersion: v1

metadata:

  namespace: app

  name: admin

spec:

  selector:

    type: app

  ports:

    - name: admin

      port: 9990

```




---

apiVersion: autoscaling/v1

kind: HorizontalPodAutoscaler

metadata:

  name: app

  namespace: app

spec:

  scaleTargetRef:

    apiVersion: extensions/v1beta1

    kind: Deployment

    name: app

  minReplicas: 1

  maxReplicas: 100

  targetCPUUtilizationPercentage: 30

  #Custom metrics will soon be supported!

```




4、结论

首先,我不是100%肯定,但它完全可以正常工作。最初的一些测试结果显示流量确实路由到了所有 pod,这对我来说有些神奇。当然,我只看了 Linkerd 的文档十分钟,还有一大堆我们没有涉及的功能,所以我相信还有提升的空间。

我的一个想法是不将 Linkerd 作为的 side-carts 部署,而是部署为 DaemonSets,这意味着每个节点仅需要运行一个实例。为了实现这一点,我们需要从 HTTP2 请求的 header 中获取服务名,并相应地路由请求。然而,根据我们当前的命名/命名空间设置,这很难做到,因此我们先进行测试。

另一个想法是让 Linkerd 根据需要向 K8S 发送伸缩命令,而不是利用 Horizontal Pod Autoscaler,因为它只支持测量 CPU 负载。

最后,这不是最终的解决方案。Kubernetes 和 GRPC 正在快速发展,我相信 GRPC 微服务的部署、伸缩和负载均衡将会很快实现开箱即用,到时候我将第一个切换。




1、https://kubernetes.io/

2、https://www.docker.com/

3、https://grpc.io/

4、https://linkerd.io/

5、https://www.youtube.com/watch?v=xb8u2s7cxzg

6、https://wikimediafoundation.org/wiki/Ways_to_Give

7、http://www.simplicityitself.io ... w.html)

8、http://%2522https//cloud.googl ... gine/

ServiceMesh微信交流群:

添加微信xiaoshu062,备注:服务网格,即可加入Service Mesh微信交流群。
 

1 个评论

您好如果使用 service mesh , 服务间的互相调用在业务层代码上是怎样的呢? 如果 service mesh 可以跨语言又该在代码中如何调用其他服务呢?

要回复文章请先登录注册