最终用户客座文章作者:Ratnadeep Debnath,Zapier 网站可靠性工程师
在Zapier[1],RabbitMQ[2]是 Zap 处理的核心。在 Zap 中,每一步我们都会将消息队列发送到 RabbitMQ。这些消息被运行在 Kubernetes 上的后端工作器(worker)使用。为了跟上 Zapier 中不断变化的任务负载,我们需要用消息积压(backlog)来扩展工作器。
很长一段时间以来,我们使用 Kubernetes 原生 Horizontal Pod Autoscaling(HPA)来实现基于 CPU 的自动伸缩。一般来说,更多的任务会导致更多的处理,产生更多的 CPU 使用量,最终触发我们的工作器的自动伸缩。它似乎工作得很好,除了某些边缘情况。
我们在 Python 中做了大量的阻塞 I/O[3](我们在用 Python 编写的 worker 中不使用基于事件的循环)。这意味着我们可以有一群工作器闲置在阻塞 I/O 时使用低 CPU 配置文件,而队列不断增长无限,因为低 CPU 使用率会阻止自动缩放的启动。如果工作器在等待 I/O 时处于空闲状态,那么我们可能会有越来越多的消息积压,而基于 CPU 的自动标度器可能会错过这些消息。这种情况会导致通信阻塞,并在处理 Zap 任务时引入延迟。
理想情况下,我们希望在 CPU 和 RabbitMQ 中扩展我们的 worker。不幸的是,Kubernetes 的原生 HPA 不支持基于 RabbitMQ 队列长度的即时扩展。有一个潜在的解决方案,在 Prometheus 中收集 RabbitMQ 指标,创建一个自定义指标服务器,并配置 HPA 来使用这些指标。然而,这是一项大量的工作,当有KEDA[4]的时候,为什么要另起炉灶呢?
KEDA 是什么?
KEDA 是一个基于 Kubernetes 的事件驱动自动伸缩器,旨在使自动伸缩变得非常简单。使用 KEDA,你可以通过使用 40 多个可用的 scaler 来驱动 Kubernetes 中任何容器的扩展,包括基于 RabbitMQ 中的未决消息。
KEDA 是一个单一用途的轻量级组件,可以添加到任何 Kubernetes 集群中。KEDA 通过扩展Horizontal Pod Autoscaler[5],并根据使用的 scaler 提供外部指标,在 Kubernetes 中完成了所有的自动伸缩重载。有了 KEDA,你可以很容易地定义你想要扩展的应用程序,而不改变应用程序本身,其他应用程序继续运行。这使得 KEDA 成为一个灵活和安全的选择,可以与任意数量的其他 Kubernetes 应用程序或框架一起运行。
我们如何使用 KEDA?
我们已经在 Kubernetes 集群中安装了 KEDA,并开始选择使用 KEDA 进行自动伸缩。我们的目标是,不仅要根据 CPU 使用率,还要根据 RabbitMQ 队列中 ready 消息的数量来自动伸缩 worker。
代码语言:javascript复制priorityClassName: system-cluster-critical
prometheus:
enabled: true
metricServer:
enabled: true
podMonitor:
enabled: true
namespace: keda
operator:
enabled: true
podMonitor:
enabled: true
namespace: keda
prometheusRules:
enabled: true
alerts:
- alert: KedaMetricsServerDown
expr: absent(up{job="keda-operator-metrics-apiserver"})
for: 1m
labels:
severity: critical
annotations:
summary: "Keda metrics server is down"
- alert: KedaScalerErrorsHigh
expr: rate(keda_metrics_adapter_scaler_errors{}[5m]) > 0
for: 1m
labels:
severity: critical
annotations:
summary: "Keda scaler errors is high for {{ $labels.namespace }}/{{ $labels.scaledObject }} {{ $labels.scaler }}"
- alert: KedaScaledObjectErrorsHigh
expr: rate(keda_metrics_adapter_scaled_object_errors{}[5m]) > 0
for: 1m
labels:
severity: critical
annotations:
summary: "Keda scaledObject errors is high for {{ $labels.namespace }}/{{ $labels.scaledObject }}"
service:
annotations:
prometheus.io/path: /metrics
prometheus.io/port: "9022"
prometheus.io/scrape: "true"
resources:
requests:
cpu: "100m"
memory: "100Mi"
limits:
cpu: "1"
memory: "1000Mi"
部署
我们使用Helm[6]在 Kubernetes 集群中部署了 KEDA。
为 KEDA 贡献特性
因为我们的 worker 从多个 RabbitMQ 主机读取队列的消息,所以我们需要根据多个 RabbitMQ 主机上队列的就绪消息进行扩展。但是 KEDA 2.3.0 版本的 KEDA RabbitMQ scaler 不支持为多个 RabbitMQ 主机定义同一个队列的触发器,因为 KEDA RabbitMQ scaler 会使用队列名自动生成指标名称。
KEDA 是一个开源项目,所以我们可以自己添加功能来支持我们的设置。使用与其他伸缩器相同的模式(如 Kafka),我们修改了指标名称的生成方式,并将更改提交给 KEDA 维护者[7]。他们能够审查、合并这些变更,并将其作为2.4.0 版本[8]的一部分发布。感谢 KEDA 团队的可爱和乐于助人。
使用 KEDA 配置自动伸缩
在我们用于在 Kubernetes 上启动服务的私有 Helm chart 中,我们增加了对基于 KEDA 的自动缩放的支持。我们现在需要做的就是在服务的 Helm 值中配置自动伸缩部分。
代码语言:javascript复制autoscaling:
minReplicas: 3
maxReplicas: 10
keda:
enabled: true
rabbitmq:
auth:
vaultSecret: zapier/data/keda/rabbitmq
servers:
- "rabbitmq-1"
- "rabbitmq-2"
metrics:
- type: cpu
metadata:
type: Utilization
value: "82"
- type: rabbitmq
metadata:
protocol: amqp
queueName: celery
mode: QueueLength
value: "180"
这将生成必要的工件:ExternalSecret(external-secrets 控制器,https://github.com/external-secrets/kubernetes-external-secrets 通过从Vault[9]中获取秘密,从一个 ExternalSecret 对象中生成 Kubernetes Secret)、TriggerAuthentications、ScaledObject,用于为服务配置 KEDA 的自动伸缩。在这个例子中,ScaledObject zapier-worker-celery 告诉 KEDA 在以下触发器上扩展 zapier-worker-celery 的部署:
- 当 pod 的 CPU 利用率为 82%
- 当 rabbitmq-1 主机中 celery 队列的 Ready 消息数为 180 条时
- 当 rabbitmq-2 主机中 celery 队列的 Ready 消息数为 180 条时
上述 ScaledObject 中的 rabbitmq 触发器使用触发器的 authenticationRef 中提到的 TriggerAuthentication 来认证 rabbitmq 主机,为 scaler 收集 rabbitmq 的指标。
代码语言:javascript复制# Secret containing RabbitMQ host connection strings for use in KEDA TriggerAuthentication. This is autogenerated by ExternalSecrets for us
---
apiVersion: v1
kind: Secret
metadata:
name: zapier-worker-celery
data:
rabbitmq-1: <AMQP URI connection string> # base64 encoded value of format amqp://guest:password@localhost:5672/vhost
rabbitmq-2: <AMQP URI connection string> # base64 encoded value of format amqp://guest:password@localhost:5672/vhost
---
# KEDA TriggerAuthentications for use in KEDA’s RabbitMQ triggers in ScaledObject
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
name: worker-celery-rabbitmq-1
namespace: zapier
spec:
secretTargetRef:
- key: rabbitmq-1
name: zapier-worker-celery
parameter: host
---
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
name: worker-celery-rabbitmq-2
namespace: zapier
spec:
secretTargetRef:
- key: rabbitmq-2
name: zapier-worker-celery
parameter: host
---
# KEDA ScaledObject to define how to scale a deployment
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
finalizers:
- finalizer.keda.sh
name: zapier-worker-celery
namespace: zapier
spec:
cooldownPeriod: 300
maxReplicaCount: 10
minReplicaCount: 3
pollingInterval: 15
scaleTargetRef:
name: zapier-worker-celery # Name of target deployment to scale
triggers:
# CPU trigger
- metadata:
type: Utilization
value: "82"
type: cpu
# RabbitMQ trigger for “celery” queue length on rabbitmq-1 host
- authenticationRef:
name: worker-celery-rabbitmq-1
metadata:
metricName: celery-rabbitmq-1-worker-celery
mode: QueueLength
protocol: amqp
queueName: celery
value: "180"
type: rabbitmq
# RabbitMQ trigger for “celery” queue length on rabbitmq-2 host
- authenticationRef:
name: worker-celery-rabbitmq-2
metadata:
metricName: celery-rabbitmq-2-worker-celery
mode: QueueLength
protocol: amqp
queueName: celery
value: "180"
type: rabbitmq
监控和警报
在 Zapier,我们使用Grafana[10]来可视化Thanos[11](具有长期存储能力的高可用 Prometheus 设置)的指标。为了确保系统按预期运行,我们使用自定义的 Prometheus 规则在出现问题时发出警报。
为此,我们依赖 KEDA 提供的内置指标,允许我们定义以下规则(见上文 helm values 文件):
- KedaMetricsServerDown:当 keda-operator-metrics-server 宕机 1m 时被引发
- KedaScalerErrorsHigh:当 keda_metrics_adapter_scaler_errors_total > 0 为 1m 时被触发
- KedaScaledObjectErrorsHigh:当 keda_metrics_adapter_scaled_object_errors > 0 为 1m 时被触发
我们还通过跟踪以下指标来使用它们进行监控:
- up{job=”keda-operator-metrics-apiserver”}:这表示 KEDA 服务是否已启动并正在运行
- keda_metrics_adapter_scaled_object_errors:ScaledObject 错误,例如,scaler 错误,TriggerAuthentication 缺少秘密,需要扩展到最大副本等等。
- keda_metrics_adapter_scaler_errors_total:集群中所有伸缩器的错误总数
- keda_metrics_adapter_scaler_errors:ScaledObject 中按每个触发器分组的伸缩器错误
- keda_metrics_adapter_scaler_metrics_value:由 ScaledObject 中的每个触发器分组的 KEDA 伸缩器的指标值
这种监控和警报设置帮助我们掌握来自 KEDA 控制器和伸缩器的任何错误。
结果
KEDA 在基于 CPU 的扩展和基于 RabbitMQ ready 消息的无缝自动扩展方面的性能与 Kubernetes 原生 HPA 相当。
使用 KEDA 来自动伸缩我们的工作器,可以显著避免由于阻塞的 I/O 调用而导致的 Zap 处理延迟。我们正在慢慢更新 Zapier 的应用程序以使用 KEDA。
总结
我们很高兴 KEDA 能够帮助扩展我们的工作负载。它使用简单,有良好的文档记录,并且有大量现成的伸缩器。
除此之外,KEDA 还有一个非常活跃和乐于助人的贡献者社区。我们刚刚开始使用 KEDA,但我们已经看到了将 KEDA 推向极限的新用例。我们期待与 KEDA 社区在未来的合作。
关于作者
Ratnadeep Debnath 是 Zapier 的一名网站可靠性工程师。他是整个组织中开源技术的热情倡导者。他花了大量时间研究 Kubernetes 和其他 CNCF 项目,并积极参与其中的一些项目。他喜欢在闲暇时间与朋友和家人在一起,做冥想、阅读和种植食物。
参考资料
[1]Zapier: https://zapier.com/
[2]RabbitMQ: https://www.rabbitmq.com/
[3]阻塞 I/O: https://medium.com/coderscorner/tale-of-client-server-and-socket-a6ef54a74763
[4]KEDA: https://keda.sh/
[5]Horizontal Pod Autoscaler: https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/
[6]Helm: https://keda.sh/docs/2.5/deploy/#helm
[7]将更改提交给 KEDA 维护者: https://github.com/kedacore/keda/pull/1976
[8]2.4.0 版本: https://github.com/kedacore/keda/releases/tag/v2.4.0
[9]Vault: https://www.vaultproject.io/
[10]Grafana: https://grafana.com/
[11]Thanos: https://thanos.io/