使用 mcrouter 在 kubernetes 中构建高可用 memcached

网友投稿 239 2022-11-02

使用 mcrouter 在 kubernetes 中构建高可用 memcached

使用 mcrouter 构建高可用 memcached

在其中一个项目中,我发现自己面临一个经典问题:由于高 RPS(每秒请求数)速率,关系数据库上的应用程序负载非常重。但是,从数据库中检索到的唯一数据的实际百分比相对较低。此外,缓慢的数据库响应迫使应用程序建立新的连接,进一步增加负载并造成滚雪球效应。

这个问题的解决方案很明显:数据缓存。我使用 memcached 作为缓存系统。数据检索请求首当其冲。但是,当我尝试将应用程序迁移到 Kubernetes 时,出现了一些问题。

问题

由于所选缓存方案易于扩展和透明,该应用程序受益于迁移到 K8s。但是,应用程序的平均响应时间增加了。使用 New Relic 平台进行的性能分析显示,迁移后 memcached 事务时间显着增加。

在调查了延迟增加的原因后,我意识到它们完全是由网络延迟引起的。问题是,在迁移之前,应用程序和 memcached 运行在同一个物理节点上,而在 K8s 集群中,应用程序和 memcached Pod 通常运行在不同的节点上。在这种情况下,网络延迟是不可避免的。

解决方案

显然,memcached 必须作为 DaemonSet 在运行应用程序的相同节点上运行。这意味着您必须配置​​节点亲和性​​。这是一个类似于生产的清单,带有探测和请求/限制:

apiVersion: apps/v1kind: DaemonSetmetadata: name: mc labels: app: mcspec: selector: matchLabels: app: mc template: metadata: labels: app: mc spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: node-role.kubernetes.io/node operator: Exists containers: - name: memcached image: memcached:1.6.9 command: - /bin/bash - -c - -- - memcached --port=30213 --memory-limit=2048 -o modern v --conn-limit=4096 -u memcache ports: - name: mc-production containerPort: 30213 livenessProbe: tcpSocket: port: mc-production initialDelaySeconds: 30 timeoutSeconds: 5 readinessProbe: tcpSocket: port: mc-production initialDelaySeconds: 5 timeoutSeconds: 1 resources: requests: cpu: 100m memory: 2560Mi limits: memory: 2560Mi---apiVersion: v1kind: Servicemetadata: name: mcspec: selector: app: mc clusterIP: None publishNotReadyAddresses: true ports: - name: mc-production port: 30213

就我而言,应用程序还需要​​缓存一致性​​​。换句话说,所有缓存实例中的数据必须与数据库中的数据相同。该应用程序具有用于更新 memcached 数据以及更新数据库中缓存数据的机制。因此,我们需要一种机制,将一个节点上的应用程序实例所做的缓存更新传播到所有其他节点。为此,我们将使用​​mcrouter​​,一个用于扩展 memcached 部署的 memcached 协议路由器。

将 mcrouter 添加到集群

Mcrouter 也应该作为 DaemonSet 运行,以加快读取缓存数据的速度。因此,我们可以保证 mcrouter 连接到最近的 memcached 实例(即,在同一个节点上运行)。基本方法是在 memcached Pod 中将 mcrouter 作为 sidecar 容器运行。在这种情况下,mcrouter 可以连接到最近的 memcached 实例 127.0.0.1。

但是,为了提高容错性,最好将 mcrouter 放在单独的 DaemonSet 中,同时启用​​hostNetwork​​memcached 和 mcrouter。此设置可确保任何 memcached 实例的任何问题都不会影响应用程序的缓存可用性。同时,您可以独立重新部署memcached和mcrouter,从而提高整个系统在此类操作过程中的容错能力。

让我们添加​​hostNetwork: true​​​到清单中以使 memcached 能够使用​​hostNetwork​​.

我们还向 memcached 容器添加一个环境变量,其中包含运行 Pod 的主机的 IP 地址:

env: - name: HOST_IP valueFrom: fieldRef: fieldPath: status.hostIP

同时修改memcached启动命令,使端口只监听到内部clusterIP::

command: - /bin/bash - -c - -- - memcached --listen=$HOST_IP --port=30213 --memory-limit=2048 -o modern v --conn-limit=4096 -u memcache

现在对 mcrouter 的 DaemonSet 做同样的事情(它的 Pod 也必须使用​​hostNetwork​​):

apiVersion: apps/v1kind: DaemonSetmetadata: name: mcrouter labels: app: mcrouterspec: selector: matchLabels: app: mcrouter template: metadata: labels: app: mcrouter spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: node-role.kubernetes.io/node operator: Exists hostNetwork: true imagePullSecrets: - name: "registrysecret" containers: - name: mcrouter image: {{ .Values.werf.image.mcrouter }} command: - /bin/bash - -c - -- - mcrouter --listen-addresses=$HOST_IP --port=31213 --config-file=/mnt/config/config.json --stats-root=/mnt/config/ volumeMounts: - name: config mountPath: /mnt/config ports: - name: mcr-production containerPort: 30213 livenessProbe: tcpSocket: port: mcr-production initialDelaySeconds: 30 timeoutSeconds: 5 readinessProbe: tcpSocket: port: mcr-production initialDelaySeconds: 5 timeoutSeconds: 1 resources: requests: cpu: 300m memory: 100Mi limits: memory: 100Mi env: - name: HOST_IP valueFrom: fieldRef: fieldPath: status.hostIP volumes: - configMap: name: mcrouter name: mcrouter - name: config emptyDir: {}

由于 mcrouter 使用​​hostNetwork​​,我们也将其限制为监听节点的内部 IP 地址。

下面是使用​​werf​​构建 mcrouter 镜像的配置文件,您可以轻松地将其转换为常规 Dockerfile:

image: mcrouterfrom: ubuntu:18.04mount:- from: tmp_dir to: /var/lib/apt/listsansible:beforeInstall:- name: Install prerequisites apt: name: - apt-transport- - apt-utils - dnsutils - gnupg - tzdata - locales update_cache: yes- name: Add mcrouter APT key apt_key: url: name: Add mcrouter Repo apt_repository: repo: deb bionic contrib filename: mcrouter update_cache: yes- name: Set timezone timezone: name: "Europe/London"- name: Ensure a locale exists locale_gen: name: en_US.UTF-8 state: presentinstall:- name: Install mcrouter apt: name: - mcrouter

现在让我们继续 mcrouter 配置。我们必须在特定节点上调度 Pod 后即时生成它,以将该节点的地址设置为主节点。为此,您需要在 mcrouter Pod 中运行一个 init 容器。它将生成配置文件并将其保存到共享​​emptyDir​​卷:

initContainers: - name: init image: {{ .Values.werf.image.mcrouter }} command: - /bin/bash - -c - /mnt/config/config_generator.sh /mnt/config/config.json volumeMounts: - name: mcrouter mountPath: /mnt/config/config_generator.sh subPath: config_generator.sh - name: config mountPath: /mnt/config env: - name: HOST_IP valueFrom: fieldRef: fieldPath: status.hostIP

这是在 init 容器中运行的配置生成器脚本的示例:

apiVersion: v1kind: ConfigMapmetadata: name: mcrouterdata: config_generator.sh: | #!/bin/bash set -e set -o pipefail config_path=$1; if [ -z "${config_path}" ]; then echo "config_path isn't specified"; exit 1; fi function join_by { local d=$1; shift; local f=$1; shift; printf %s "$f" "${@/#/$d}"; } mapfile -t ips < <( host mc.production.svc.cluster.local 10.222.0.10 | grep mc.production.svc.cluster.local | awk '{ print $4; }' | sort | grep -v $HOST_IP ) delimiter=':30213","' servers='"'$(join_by $delimiter $HOST_IP "${ips[@]}")':30213"' cat <<< '{ "pools": { "A": { "servers": [ '$servers' ] } }, "route": { "type": "OperationSelectorRoute", "operation_policies": { "add": "AllSyncRoute|Pool|A", "delete": "AllSyncRoute|Pool|A", "get": "FailoverRoute|Pool|A", "set": "AllSyncRoute|Pool|A" } } } ' > $config_path

该脚本向集群的内部 DNS 发送请求,获取 memcached Pod 的所有 IP 地址,并生成它们的列表。列表中的第一个是运行此特定 mcrouter 实例的节点的 IP 地址。

请注意,您必须在上面的 memcached 服务清单中进行设置​​clusterIP: None​​,才能在请求 DNS 记录时获取 Pod 地址。

下面是脚本生成的文件示例:

cat /mnt/config/config.json{ "pools": { "A": { "servers": ["192.168.100.33:30213","192.168.100.14:30213","192.168.100.15:30213","192.168.100.16:30213","192.168.100.21:30213","192.168.100.22:30213","192.168.100.23:30213","192.168.100.34:30213" ] } }, "route": { "type": "OperationSelectorRoute", "operation_policies": { "add": "AllSyncRoute|Pool|A", "delete": "AllSyncRoute|Pool|A", "get": "FailoverRoute|Pool|A", "set": "AllSyncRoute|Pool|A" } }}

这样,我们确保在“本地”节点上进行读取时,更改同步传播到所有 memcached 实例。

注意。如果没有严格的缓存一致性要求,我们建议使用​​AllMajorityRoute​​​甚至​​AllFastestRoute​​​路由句柄来代替,​​AllSyncRoute​​以获得更快的性能和对集群不稳定性的一般敏感性。

适应集群不断变化的性质

但是,确实存在一个麻烦:集群不是静态的,集群中工作节点的数量可能会发生变化。如果集群节点的数量增加,则无法保持缓存一致性,因为:

会有新的 memcached/mcrouter 实例;新的 mcrouter 实例将写入旧的memcached 实例;同时,旧的 mcrouter 实例将不知道有新的 memcached 实例可用。

同时,如果节点数量减少(前提是启用了 AllSyncRoute 策略),节点缓存本质上会变成只读的。

可能的解决方法是在 mcrouter Pod 中运行带有 cron 的 sidecar 容器,该容器将验证节点列表并应用更改。

下面是这样一个sidecar的配置:

- name: cron image: {{ .Values.werf.image.cron }} command: - /usr/local/bin/dumb-init - /bin/sh - -c - /usr/local/bin/supercronic -json /app/crontab volumeMounts: - name: mcrouter mountPath: /mnt/config/config_generator.sh subPath: config_generator.sh - name: mcrouter mountPath: /mnt/config/check_nodes.sh subPath: check_nodes.sh - name: mcrouter mountPath: /app/crontab subPath: crontab - name: config mountPath: /mnt/config resources: limits: memory: 64Mi requests: memory: 64Mi cpu: 5m env: - name: HOST_IP valueFrom: fieldRef: fieldPath: status.hostIP

在这个cron容器中运行的脚本调用了init容器中使用的config_generator.sh脚本:

crontab: | # Check nodes in cluster * * * * * * * /mnt/config/check_nodes.sh /mnt/config/config.json check_nodes.sh: | #!/usr/bin/env bash set -e config_path=$1; if [ -z "${config_path}" ]; then echo "config_path isn't specified"; exit 1; fi check_path="${config_path}.check" checksum1=$(md5sum $config_path | awk '{print $1;}') /mnt/config/config_generator.sh $check_path checksum2=$(md5sum $check_path | awk '{print $1;}') if [[ $checksum1 == $checksum2 ]]; then echo "No changes for nodes." exit 0; else echo "Node list was changed." mv $check_path $config_path echo "mcrouter is reconfigured." fi

每秒运行一次脚本,该脚本会为 mcrouter 生成配置文件。当配置文件的校验和发生变化时,更新的文件会保存到​​emptyDir​​​共享目录中,以便 mcrouter 可以使用它。您不必担心 mcrouter 会更新配置,因为它每秒会重新读取一次​​参数​​。

现在您所要做的就是通过包含 memcached 地址的环境变量将 Node 的 IP 地址传递给应用程序 Pod,同时指定 mcrouter 端口而不是 memcached 端口:

env: - name: MEMCACHED_HOST valueFrom: fieldRef: fieldPath: status.hostIP - name: MEMCACHED_PORT value: 31213

总结一下

最终目标已经实现:应用程序现在运行得更快。New Relic 数据显示,处理用户请求的 memcached 事务时间已从 70-80 毫秒降至约 20 毫秒。

优化前:

优化后:

该解决方案已投入生产约六个月;在那段时间没有发现任何坑。

文章中提到的文件(Helm 图表和​​werf.yaml​​​)可以在​​examples 存储库​​中找到。

参考

https://blog.flant.com/highly-available-memcached-with-mcrouter-in-kubernetes/

关注

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:学习Java中的List集合
下一篇:基于PCI Express总线的CamLink接口的高速图像采集系统
相关文章

 发表评论

暂时没有评论,来抢沙发吧~