使用 mcrouter 水平扩展 memcached

使用 mcrouter 水平扩展 memcached

使用任何语言开发高负载项目都需要特殊的方法和使用特殊的工具,但是当涉及到 PHP 中的应用程序时,情况可能会变得如此严重,以至于您必须开发,例如, 自己的应用服务器。 在这篇文章中,我们将讨论 memcached 中分布式会话存储和数据缓存所带来的常见问题,以及我们如何在一个“ward”项目中解决这些问题。

这次的主角是基于 symfony 2.3 框架的 PHP 应用程序,该应用程序根本没有包含在更新的业务计划中。 除了相当标准的会话存储之外,该项目还充分利用了 “缓存一切”政策 在 memcached 中:响应对数据库和 API 服务器的请求、各种标志、用于同步代码执行的锁等等。 在这种情况下,memcached 的故障对于应用程序的运行来说是致命的。 此外,缓存丢失会导致严重后果:DBMS 开始爆裂、API 服务开始禁止请求等。 稳定局势可能需要数十分钟,在此期间服务将非常慢或完全不可用。

我们需要提供 轻松水平扩展应用程序的能力, IE。 对源代码进行最少的更改并保留全部功能。 使缓存不仅能够抵抗故障,而且还要尽量减少由此造成的数据丢失。

memcached 本身有什么问题?

一般来说,PHP 的 memcached 扩展支持开箱即用的分布式数据和会话存储。 一致的密钥散列机制允许您将数据均匀地放置在许多服务器上,将每个特定密钥唯一地寻址到组中的特定服务器,并且内置的故障转移工具确保了缓存服务的高可用性(但不幸的是, 没有数据).

会话存储的情况要好一些:您可以配置 memcached.sess_number_of_replicas,这样一来,数据将同时存储在多台服务器上,并且当其中一个 memcached 实例出现故障时,数据将从其他服务器转移。 但是,如果服务器在没有数据的情况下重新上线(通常在重新启动后发生),则某些密钥将按照有利于服务器的方式重新分配。 事实上这意味着 会话数据丢失,因为如果发生丢失,就无法“转到”另一个副本。

标准库工具主要针对 水平的 缩放:它们允许您将缓存增加到巨大的大小,并提供从不同服务器上托管的代码对其的访问。 不过,在我们的情况下,存储的数据量不超过几GB,一两个节点的性能就已经足够了。 因此,唯一有用的标准工具可能是确保 memcached 的可用性,同时维持至少一个缓存实例处于工作状态。 然而,甚至不可能利用这个机会......这里值得回顾一下项目中使用的框架的古老性,这就是为什么不可能让应用程序与服务器池一起工作的原因。 我们也不要忘记会话数据的丢失:客户的眼睛因用户的大量注销而抽搐。

理想情况下是必需的 复制 memcached 中的记录并绕过副本 如果出现错误或错误。 帮助我们实施这一战略 微路由器.

微路由器

这是Facebook为了解决其问题而开发的memcached路由器。 它支持memcached文本协议,允许 扩展 memcached 安装 达到疯狂的程度。 mcrouter的详细描述可以在 本公告。 除其他事项外 广泛的功能 它可以做我们需要的事情:

  • 复制记录;
  • 如果发生错误,则回退到组中的其他服务器。

为了事业!

微路由器配置

我直接进入配置:

{
 "pools": {
   "pool00": {
     "servers": [
       "mc-0.mc:11211",
       "mc-1.mc:11211",
       "mc-2.mc:11211"
   },
   "pool01": {
     "servers": [
       "mc-1.mc:11211",
       "mc-2.mc:11211",
       "mc-0.mc:11211"
   },
   "pool02": {
     "servers": [
       "mc-2.mc:11211",
       "mc-0.mc:11211",
       "mc-1.mc:11211"
 },
 "route": {
   "type": "OperationSelectorRoute",
   "default_policy": "AllMajorityRoute|Pool|pool00",
   "operation_policies": {
     "get": {
       "type": "RandomRoute",
       "children": [
         "MissFailoverRoute|Pool|pool02",
         "MissFailoverRoute|Pool|pool00",
         "MissFailoverRoute|Pool|pool01"
       ]
     }
   }
 }
}

为什么要三个池? 为什么服务器会重复? 让我们弄清楚它是如何工作的。

  • 在此配置中,mcrouter 根据请求命令选择请求将发送到的路径。 那家伙告诉他这件事 OperationSelectorRoute.
  • GET 请求转到处理程序 RandomRoute它在数组对象中随机选择一个池或路由 children。 该数组的每个元素又是一个处理程序 MissFailoverRoute,它将遍历池中的每个服务器,直到收到带有数据的响应,该数据将返回给客户端。
  • 如果我们专门使用 MissFailoverRoute 如果池由三台服务器组成,那么所有请求将首先到达第一个 memcached 实例,其余的将在没有数据时剩余地接收请求。 这种方法将导致 列表中第一台服务器负载过大,因此决定生成三个地址顺序不同的池并随机选择。
  • 所有其他请求(这是一条记录)均使用 AllMajorityRoute。 该处理程序向池中的所有服务器发送请求,并等待至少 N/2 + 1 个服务器的响应。 从使用来看 AllSyncRoute 对于写操作必须被放弃,因为这种方法需要来自 所有 组中的服务器 - 否则将返回 SERVER_ERROR。 虽然 mcrouter 会将数据添加到可用缓存中,但调用 PHP 函数 会返回一个错误 并会产生通知。 AllMajorityRoute 并不那么严格,允许多达一半的设备停止使用而不会出现上述问题。

主要减去 这种方案是,如果缓存中确实没有数据,那么对于客户端的每个请求,实际上都会执行 N 个对 memcached 的请求 - 到 大家 池中的服务器。 例如,我们可以将池中的服务器数量减少到两个:牺牲存储可靠性,我们得到о更高的速度和更少的请求到丢失密钥的负载。

NB:您还可能找到学习 mcrouter 的有用链接 维基百科上的文档 и 项目问题 (包括封闭的),代表了一个各种配置的整个仓库。

构建并运行微处理器

我们的应用程序(以及 memcached 本身)在 Kubernetes 中运行 - 因此,mcrouter 也位于那里。 为了 集装箱组装 我们用 韦尔夫,其配置如下所示:

NB:文章中给出的列表已发布在存储库中 平面/微路由器.

configVersion: 1
project: mcrouter
deploy:
 namespace: '[[ env ]]'
 helmRelease: '[[ project ]]-[[ env ]]'
---
image: mcrouter
from: ubuntu:16.04
mount:
- from: tmp_dir
 to: /var/lib/apt/lists
- from: build_dir
 to: /var/cache/apt
ansible:
 beforeInstall:
 - name: Install prerequisites
   apt:
     name: [ 'apt-transport-https', 'tzdata', 'locales' ]
     update_cache: yes
 - name: Add mcrouter APT key
   apt_key:
     url: https://facebook.github.io/mcrouter/debrepo/xenial/PUBLIC.KEY
 - name: Add mcrouter Repo
   apt_repository:
     repo: deb https://facebook.github.io/mcrouter/debrepo/xenial xenial contrib
     filename: mcrouter
     update_cache: yes
 - name: Set timezone
   timezone:
     name: "Europe/Moscow"
 - name: Ensure a locale exists
   locale_gen:
     name: en_US.UTF-8
     state: present
 install:
 - name: Install mcrouter
   apt:
     name: [ 'mcrouter' ]

(werf.yaml)

...并绘制草图 舵图。 有趣的是,只有一个基于副本数量的配置生成器 (如果有人有更简洁优雅的选择,请在评论中分享):

{{- $count := (pluck .Values.global.env .Values.memcached.replicas | first | default .Values.memcached.replicas._default | int) -}}
{{- $pools := dict -}}
{{- $servers := list -}}
{{- /* Заполняем  массив двумя копиями серверов: "0 1 2 0 1 2" */ -}}
{{- range until 2 -}}
 {{- range $i, $_ := until $count -}}
   {{- $servers = append $servers (printf "mc-%d.mc:11211" $i) -}}
 {{- end -}}
{{- end -}}
{{- /* Смещаясь по массиву, получаем N срезов: "[0 1 2] [1 2 0] [2 0 1]" */ -}}
{{- range $i, $_ := until $count -}}
 {{- $pool := dict "servers" (slice $servers $i (add $i $count)) -}}
 {{- $_ := set $pools (printf "MissFailoverRoute|Pool|pool%02d" $i) $pool -}}
{{- end -}}
---
apiVersion: v1
kind: ConfigMap
metadata:
 name: mcrouter
data:
 config.json: |
   {
     "pools": {{- $pools | toJson | replace "MissFailoverRoute|Pool|" "" -}},
     "route": {
       "type": "OperationSelectorRoute",
       "default_policy": "AllMajorityRoute|Pool|pool00",
       "operation_policies": {
         "get": {
           "type": "RandomRoute",
           "children": {{- keys $pools | toJson }}
         }
       }
     }
   }

(10-mcrouter.yaml)

我们将其部署到测试环境中并检查:

# php -a
Interactive mode enabled

php > # Проверяем запись и чтение
php > $m = new Memcached();
php > $m->addServer('mcrouter', 11211);
php > var_dump($m->set('test', 'value'));
bool(true)
php > var_dump($m->get('test'));
string(5) "value"
php > # Работает! Тестируем работу сессий:
php > ini_set('session.save_handler', 'memcached');
php > ini_set('session.save_path', 'mcrouter:11211');
php > var_dump(session_start());
PHP Warning:  Uncaught Error: Failed to create session ID: memcached (path: mcrouter:11211) in php shell code:1
Stack trace:
#0 php shell code(1): session_start()
#1 {main}
  thrown in php shell code on line 1
php > # Не заводится… Попробуем задать session_id:
php > session_id("zzz");
php > var_dump(session_start());
PHP Warning:  session_start(): Cannot send session cookie - headers already sent by (output started at php shell code:1) in php shell code on line 1
PHP Warning:  session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning:  session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning:  session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning:  session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning:  session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning:  session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning:  session_start(): Unable to clear session lock record in php shell code on line 1
PHP Warning:  session_start(): Failed to read session data: memcached (path: mcrouter:11211) in php shell code on line 1
bool(false)
php >

搜索错误的文本没有给出任何结果,但是对于查询“微路由器 PHP“最重要的是该项目最古老的未解决问题 - 缺乏支持 memcached 二进制协议。

NB:memcached 中的 ASCII 协议比二进制协议慢,并且一致性密钥散列的标准方法仅适用于二进制协议。 但这不会对特定情况造成问题。

诀窍就在眼前:您所要做的就是切换到 ASCII 协议,一切都会正常工作...... 然而,在这种情况下,寻找答案的习惯 php.net 上的文档 开了一个残酷的玩笑。 你不会在那里找到正确的答案......当然,除非你滚动到最后,在该部分的位置 “用户贡献的笔记” 将会忠诚并且 不公平地被否决的答案.

是的,正确的选项名称是 memcached.sess_binary_protocol。 必须将其禁用,之后会话才会开始工作。 剩下的就是将带有 mcrouter 的容器放入带有 PHP 的 pod 中!

结论

因此,通过基础设施的改变,我们就能够解决这个问题:memcached的容错问题已经解决,并且缓存存储的可靠性也得到了提高。 除了为应用程序带来明显的优势之外,这还为在平台上工作时提供了回旋余地:当所有组件都有储备时,管理员的工作就大大简化了。 是的,这种方法也有其缺点,它可能看起来像“拐杖”,但如果它省钱,埋葬问题并且不会引起新的问题 - 为什么不呢?

PS

另请阅读我们的博客:

来源: habr.com

添加评论