• KEDA:基于事件驱动扩展K8S应用的深度实践

    为什么我们要自动扩展应用程序?

    作为 SRE,需要保证应用弹性和高可用性。因此,自动缩放是我们需要的必须功能。通过自动缩放,我们能确保工作负载能够高效的地处理业务流量。

    在本文中,我们将详细描述如何使用 KEDA 以事件驱动的方式自动扩展 Kubernetes 应用程序。

    什么是KEDA?

    KEDA 是一个轻量级的开源 Kubernetes 事件驱动的自动缩放器,DevOps、SRE 和 Ops 团队使用它来根据外部事件或触发器水平扩展 Pod。KEDA 有助于扩展本机 Kubernetes 自动缩放解决方案的功能,这些解决方案依赖于标准资源指标,如 CPU 或内存。我们可以将 KEDA 部署到 Kubernetes 集群中,并使用自定义资源定义 (CRD) 管理 Pod 的扩展。

    KEDA 基于 Kubernetes HPA 构建,根据来自 AWS SQS、Kafka、RabbitMQ 等事件源的信息扩展 Pod。这些事件源使用缩放程序进行监视,缩放程序根据为其设置的规则激活或停用部署。KEDA 缩放器还可以为特定事件源提供自定义指标,帮助 DevOps 团队观察与其相关的指标

    我们唯一要做的就是通过选择要用于自动扩展应用程序的缩放器以及一些参数来配置 ScaledObject (KEDA CRD),KEDA 将完成剩下的工作:

    • 监视事件源
    • 创建和管理 HPA 生命周期

    截至目前,有 62 个内置缩放器和 4 个外部缩放器可用。 KEDA 之所以好,是因为使用轻量级组件以及原生 Kubernetes 组件,例如 HorizontalPodAutoscaler ,更重要的是“即插即用”。

    部署KEDA

    那么,部署 KEDA 的最简单方法是使用官方 Helm,安装如下所示:

    helm repo add kedacore 
    helm repo update
    helm install keda kedacore/keda --namespace keda --create-namespace

     

    ⚠️ 如果使用 ArgoCD 部署 KEDA,您可能会遇到有关 CRD 注释长度的问题。我们可以使用选项 ServerSideApply=true 来解决 template.sped.syncPolicy.syncOptions 。此外,还可以在 helm中设置参数,从而 禁用 CRD 部署,当然,这种情况下也必须找到另一种方法来部署 KEDA CRD。

    基于本机Cron Scaler自动缩放Web应用

    下面让我们深度实践一下KEDA!

    部署我们的Web应用

    对于demo,将使用一个建的 Golang Web 应用程序。使用以下清单部署:
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      labels:
        app: go-helloworld
      name: go-helloworld
    spec:
      selector:
        matchLabels:
          app: go-helloworld
      template:
        metadata:
          labels:
            app: go-helloworld
        spec:
          containers:
            - image: rg.fr-par.scw.cloud/novigrad/go-helloworld:0.1.0
              name: go-helloworld
              resources:
                requests:
                  cpu: "50m"
                  memory: "64Mi"
                limits:
                  memory: "128Mi"
                  cpu: "100m"
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: go-helloworld
    spec:
      selector:
        app: go-helloworld
      ports:
        - protocol: TCP
          port: 8080
          name: http
    ---apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      annotations:
        kubernetes.io/ingress.class: nginx
        cert-manager.io/cluster-issuer: letsencrypt
      name: go-helloworld
    spec:
      rules:
      - host: helloworld.jourdain.io
        http:
          paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: go-helloworld
                port:
                  number: 8080
      tls: # < placing a host in the TLS config will indicate a certificate should be created
      - hosts:
        - helloworld.jourdain.io
        secretName: go-helloworld-tls-cert

    将KEDA配置为仅在工作时间自动扩展Web应用

    如果,我们希望我们的应用程序仅在工作时间可用。至于为何有如此要求,下面是一些场景。
    例如,在开发环境中,不一定需要保持应用程序24小时启动和运行。在云环境中,它可以为您节省大量成本,具体取决于用户实际环境的应用程序/计算实例的数量。 为了实现这一点,我们将使用KEDA的原生Cron scaler。由于 Cron scaler 支持 Linux 格式的 cron,它甚至允许我们在工作日扩展我们的应用程序 
    要配置 Cron scaler,我们将按如下方式使用 [ScaledObject] CRD:
    apiVersion: keda.sh/v1alpha1
    kind: ScaledObject
    metadata:
      name: go-helloworld
    spec:
      scaleTargetRef:
        name: go-helloworld
      triggers:
      - type: cron
        metadata:
          timezone: Europe/Paris
          start: 00 08 * * 1-5
          end: 00 18 * * 1-5
          desiredReplicas: "2"
    ⚠️ ScaledObject 必须与应用程序位于同一命名空间中!
    让我们深入了解一下这个配置:
      • spec.scaleTargetRef 是 Kubernetes Deployment/StatefulSet 或其他自定义资源的引用
    • name (必填): Kubernetes 资源的名称
    • kind (可选):Kubernetes 资源的种类,默认值为 Deployment
      • spec.triggers 是用于激活目标资源缩放的触发器列表
    • type (必填):缩放器名称
    • metadata (必需):Cron 缩放器所需的配置参数,使用此配置,我的应用程序将在周一到周五的一周中每天的 08:00 到 18:00 之间启动并运行两个副本。

    基于HTTP事件自动缩放Web应用

    (使用 KEDA HTTP 外部缩放器)

    借助所有 KEDA 的缩放器,我们可以通过多种方式自动扩展 Web 应用程序,例如,基于 AMQP 队列中的消息进行缩放应用。

    目前,我们了解了 KEDA 的工作原理。我下面们将探讨 KEDA 如何通过基于 HTTP 事件自动扩展应用程序来帮助我们处理流量高峰。为此,我们有两个选择:

    • 使用Prometheus scaler
    • 使用 KEDA HTTP 外部缩放器,它的工作方式类似于附加组件,由于演示集群上没有安装 Prometheus,因此我们将使用 KEDA HTTP 外部缩放器。

    ? KEDA HTTP插件目前处于测试阶段。它主要由KEDA团队维护。

    解决方案概述

    KEDA HTTP scaler是构建在 KEDA 核心之上的附加组件,它拥有自己的组件:operator, scaler和 interceptor。如果你想进一步了解它们的作用,请阅读官方文档。总之,为了帮助大家更好地理解它的工作原理,下面提供一个小案例:

    安装KEDA HTTP附加组件

    由于这个缩放器不是内置的,我们必须手工安装。根据官方文档的说明,我们可以使用 Helm 来进行安装:
    helm install http-add-on kedacore/keda-add-ons-http --namespace keda
    如果安装顺利,我们应该会看到以下pod:
    ❯ k get pods -l app=keda-add-ons-http -o name
    pod/keda-add-ons-http-controller-manager-5c8d895cff-7jsl8
    pod/keda-add-ons-http-external-scaler-57889786cf-r45lj
    pod/keda-add-ons-http-interceptor-5bf6756df9-wwff8
    pod/keda-add-ons-http-interceptor-5bf6756df9-x8l58
    pod/keda-add-ons-http-interceptor-5bf6756df9-zxvw

    为我们的Web应用配置’HTTPScaledObject’

    正如之前所说,KEDA HTTP 附加组件自带组件,包括操作符,这也意味着它自带 CRD。HTTPScaledObject 是由 KEDA HTTP 附加组件管理的 CRD。这就是我们在这里需要配置的。让我们为 Web 应用程序创建 HTTPScaledObject 资源: 
    ⚠️ HTTPScaleObject 必须在与 Web 应用相同的命名空间中创建资源!
    kind: HTTPScaledObject
    apiVersion: http.keda.sh/v1alpha1
    metadata:
        name: go-helloworld
    spec:
        host: "helloworld.jourdain.io"
        targetPendingRequests: 10
        scaledownPeriod: 300
        scaleTargetRef:
            deployment: go-helloworld
            service: go-helloworld
            port: 8080
        replicas:
            min: 0
            max: 10
    在这里,我们已经配置了我们的 HTTPScaledObject 应用程序,以便将我们的应用程序 Deployment 从 0 个副本扩展到 10 个副本。因为,如果拦截器上有 10 个请求处于挂起状态(应用程序尚未接收的请求),则 KEDA 将添加一个 pod。

    调整我们的Web应用程序的service和ingress

    仔细观察一下上面的图,可以看到我们的 Web 应用程序 ingress 需要引用 KEDA HTTP 附加组件的拦截器服务,而不是 Web 应用程序的拦截器服务。由于 ingress 无法引用另一个命名空间中的服务,因此我们将在与 Web 应用相同的命名空间 external 中创建类型服务,该服务引用来自 keda 命名空间的拦截器服务:
    kind: Service
    apiVersion: v1
    metadata:
      name: keda-add-ons-http-interceptor-proxy
    spec:
      type: ExternalName
      externalName: keda-add-ons-http-interceptor-proxy.keda.svc.cluster.local
    现在,我们需要重新配置 Web 应用的入口,使其引用新创建的服务:
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      annotations:
        kubernetes.io/ingress.class: nginx
        cert-manager.io/cluster-issuer: letsencrypt
      name: go-helloworld
    spec:
      rules:
      - host: helloworld.jourdain.io
        http:
          paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: keda-add-ons-http-interceptor-proxy
                port:
                  number: 8080
      tls: # < placing a host in the TLS config will indicate a certificate should be created
      - hosts:
        - helloworld.jourdain.io
        secretName: go-helloworld-tls-cert
    ⚠️ 需要输入新服务的名称,但请注意端口,该端口也被拦截器的服务所取代

    让我们测试一下

    为了保证我们的配置正常,本处将使用 [k6]工具 ,这是一个负载测试工具。如果想了解有关k6的更多信息,以下是Padok博客中的一些介绍:
    • [How to do distributed load testing using K6 & Kubernetes?] (https://www.padok.fr/en/blog/k6-load-testing)
    • [k6 description from the Padok tech radar] (https://www.padok.fr/en/tech-radar-resilient?category=resilient&rank=6)
    下面本次使用的 k6 脚本,后续将使用它进行测试:
    import { check } from 'k6';
    import http from 'k6/http';
    export const options = {
      scenarios: {
        constant_request_rate: {
          executor: 'constant-arrival-rate',
          rate: 100,
          timeUnit: '1s', // 100 iterations per second, i.e. 100 RPS
          duration: '30s',
          preAllocatedVUs: 50, // how large the initial pool of VUs would be
          maxVUs: 50, // if the preAllocatedVUs are not enough, we can initialize more
        },
      },
    };
    export function test(params) {
      const res = http.get('');
      check(res, {
        'is status 200': (r) => r.status === 200,
      });
    }
    export default function () {
      test();
    }
    首先,让我们看看 100 个 RPS 会发生什么:
    ❯ k6 run k6/script.js
              /\\      |‾‾| /‾‾/   /‾‾/
         /\\  /  \\     |  |/  /   /  /
        /  \\/    \\    |     (   /   ‾‾\\
       /          \\   |  |\\  \\ |  (‾)  |
      / __________ \\  |__| \\__\\ \\_____/ .io
      execution: local
         script: k6/script.js
         output: -
      scenarios: (100.00%) 1 scenario, 50 max VUs, 1m0s max duration (incl. graceful stop):
               * constant_request_rate: 100.00 iterations/s for 30s (maxVUs: 50, gracefulStop: 30s)
    
         ✓ is status 200
         checks.........................: 100.00% ✓ 3001      ✗ 0
         data_received..................: 845 kB  28 kB/s
         data_sent......................: 134 kB  4.5 kB/s
         http_req_blocked...............: avg=792.54µs min=0s     med=1µs     max=137.85ms p(90)=2µs     p(95)=2µs
         http_req_connecting............: avg=136.6µs  min=0s     med=0s      max=17.67ms  p(90)=0s      p(95)=0s
         http_req_duration..............: avg=11.38ms  min=7.68ms med=10.68ms max=100.96ms p(90)=12.78ms p(95)=14.33ms
           { expected_response:true }...: avg=11.38ms  min=7.68ms med=10.68ms max=100.96ms p(90)=12.78ms p(95)=14.33ms
         http_req_failed................: 0.00%   ✓ 0         ✗ 3001
         http_req_receiving.............: avg=89.68µs  min=8µs    med=64µs    max=6.35ms   p(90)=112µs   p(95)=134µs
         http_req_sending...............: avg=152.31µs min=14µs   med=137µs   max=2.57ms   p(90)=274µs   p(95)=313µs
         http_req_tls_handshaking.......: avg=587.62µs min=0s     med=0s      max=74.46ms  p(90)=0s      p(95)=0s
         http_req_waiting...............: avg=11.14ms  min=7.62ms med=10.48ms max=100.92ms p(90)=12.47ms p(95)=13.96ms
         http_reqs......................: 3001    99.983105/s
         iteration_duration.............: avg=12.37ms  min=7.73ms med=10.88ms max=194.89ms p(90)=13.07ms p(95)=14.99ms
         iterations.....................: 3001    99.983105/s
         vus............................: 1       min=1       max=1
         vus_max........................: 50      min=50      max=50
    
    running (0m30.0s), 00/50 VUs, 3001 complete and 0 interrupted iterations
    constant_request_rate ✓ [======================================] 00/50 VUs  30s  100.00 iters/s
    ? 如果您想实时查看拦截器队列中有多少请求,可以在两个终端窗格中启动以下命令:
    ❯ kubectl proxy
    
    Starting to serve on 127.0.0.1:8001
    以及:
    ❯ watch -n '1' curl --silent localhost:8001/api/v1/namespaces/keda/services/keda-add-ons-http-interceptor-admin:9090/proxy/queue
    
    {"default/go-helloworld":0}
    在 100 RPS 测试中,应用程序没有纵向扩展,因为拦截器队列中的挂起请求数不超过 1。提醒一下,我们配置为 targetPendingRequests 10 。所以一切都很正常 ?
    下面让我们将 RPS x10 ,看看会发生什么:
    ❯ k6 run k6/script.js
              /\\      |‾‾| /‾‾/   /‾‾/
         /\\  /  \\     |  |/  /   /  /
        /  \\/    \\    |     (   /   ‾‾\\
       /          \\   |  |\\  \\ |  (‾)  |
      / __________ \\  |__| \\__\\ \\_____/ .io
      execution: local
         script: k6/script.js
         output: -
      scenarios: (100.00%) 1 scenario, 50 max VUs, 1m0s max duration (incl. graceful stop):
               * constant_request_rate: 1000.00 iterations/s for 30s (maxVUs: 50, gracefulStop: 30s)
         ✗ is status 200
          ↳  99% — ✓ 11642 / ✗ 2
         checks.........................: 99.98% ✓ 11642      ✗ 2
         data_received..................: 2.6 MB 86 kB/s
         data_sent......................: 446 kB 15 kB/s
         dropped_iterations.............: 18356  611.028519/s
         http_req_blocked...............: avg=1.07ms   min=0s     med=0s      max=408.06ms p(90)=1µs      p(95)=1µs
         http_req_connecting............: avg=43.12µs  min=0s     med=0s      max=11.05ms  p(90)=0s       p(95)=0s
         http_req_duration..............: avg=120.09ms min=8.14ms med=74.77ms max=6.87s    p(90)=189.49ms p(95)=250.21ms
           { expected_response:true }...: avg=120.01ms min=8.14ms med=74.76ms max=6.87s    p(90)=189.41ms p(95)=249.97ms
         http_req_failed................: 0.01%  ✓ 2          ✗ 11642
         http_req_receiving.............: avg=377.61µs min=5µs    med=32µs    max=27.32ms  p(90)=758.1µs  p(95)=2.49ms
         http_req_sending...............: avg=61.57µs  min=9µs    med=45µs    max=9.99ms   p(90)=102µs    p(95)=141µs
         http_req_tls_handshaking.......: avg=626.79µs min=0s     med=0s      max=297.82ms p(90)=0s       p(95)=0s
         http_req_waiting...............: avg=119.65ms min=7.95ms med=74.32ms max=6.87s    p(90)=188.95ms p(95)=249.76ms
         http_reqs......................: 11644  387.60166/s
         iteration_duration.............: avg=121.26ms min=8.32ms med=74.87ms max=7.07s    p(90)=189.62ms p(95)=250.28ms
         iterations.....................: 11644  387.60166/s
         vus............................: 44     min=25       max=50
         vus_max........................: 50     min=50       max=50
    
    running (0m30.0s), 00/50 VUs, 11644 complete and 0 interrupted iterations
    constant_request_rate ✓ [======================================] 00/50 VUs  30s  1000.00 iters/s
    结果还不错,我们有两个请求 KO ,这是因为应用程序冷启动(从 0 开始),每个请求等待的时间不超过 1/2 秒。
    以下是部署历史记录:
    ❯ k get deployments.apps -w
    NAME            READY   UP-TO-DATE   AVAILABLE   AGE
    go-helloworld   0/0     0            0           36m
    go-helloworld   0/1     0            0           36m
    go-helloworld   1/1     1            1           36m
    go-helloworld   1/4     1            1           36m
    go-helloworld   2/4     4            2           36m
    go-helloworld   3/4     4            3           36m
    go-helloworld   4/4     4            4           36m
    go-helloworld   4/5     4            4           37m
    go-helloworld   5/5     5            5           37m
    应用程序从 0 个副本扩展到 5 个副本;直到 Web 应用程序的挂起请求数少于 10。

    缩放指令非常快,应用程序很快达到了 5 个副本。

    以下是 100 RPS 和 1k RPS 测试之间 http_req_duration k6 指标的一些对比:

    # 100 RPS
    http_req_duration: avg=11.38ms  min=7.68ms med=10.68ms max=100.96ms p(90)=12.78ms p(95)=14.33ms
    # 1k RPS
    http_req_duration: avg=120.09ms min=8.14ms med=74.77ms max=6.87s    p(90)=189.49ms p(95)=250.21ms
    根据我们的需求(SLO,SLA等),我们也许可以稍微调整一下 Web应用程序的 targetPendingRequestsHTTPScaledObject 参数 。

    缩放到零!

    通过本文介绍的两个案例,我们已经验证过了如何缩放到零的场景。但是,大家真的知道它是如何工作的吗?
    由于 KEDA 根据事件自动缩放应用程序,因此从收到事件的那一刻起,KEDA 会将应用程序缩放到其最小副本。例如,如果我们以 HTTP 附加组件为例,KEDA 将在第一次收到请求时扩展到最小副本。

    总结

    KEDA 提供了一个类似于 FaaS 的事件感知扩展模型,在这种模型中,Kubernetes 部署可以基于需求和基于智能动态地从零扩展,而不会丢失数据和上下文。在业务请求量上来后,应用程序将进行自动化的扩容,当业务低谷的时候,则会自动的缩容。这可以在缓解很多生产环境下的手动扩/缩容操作,以保障用户的服务体验。

    KEDA 还为 Kubernetes 带来了更多的事件源。随着未来更多触发器的加入,KEDA 有很大的潜力成为生产级 Kubernetes 部署的必需品,从而使应用程序自动缩放成为应用程序开发中的嵌入式组件。

     

    «
    »
以专业成就每一位客户,让企业IT只为效果和安全买单

以专业成就每一位客户,让企业IT只为效果和安全买单