使用mcrouter来水平扩展memcached



使用任何语言开发高负荷项目都需要一种特殊的方法并使用特殊的工具,但是当涉及到PHP中的应用程序时,情况可能会恶化得如此之多,以至于您必须开发自己的应用程序服务器 。 在本文中,我们将讨论每个人都对分布式存储会话和在memcached中缓存数据的痛苦,以及我们如何在一个“病房”项目中解决这些问题。

庆典的罪魁祸首是一个基于symfony 2.3框架的PHP应用程序,它根本没有包含在业务计划中。 除了完全标准的会话存储外,该项目还充分利用 memcached 中所有内容的缓存:对数据库和API服务器查询的响应,各种标志,用于同步代码执行的锁等等。 在这种情况下,memcached故障对于应用程序的正常运行至关重要。 此外,高速缓存的丢失会导致严重的后果:DBMS开始在接缝处破裂,API服务-禁止请求等。 稳定局势可能需要数十分钟,并且此时服务将非常缓慢或完全无法访问。

我们需要提供使用少量血液的应用程序水平缩放可能性 ,即 只需对源代码进行最少的更改,并完整保留功能。 使高速缓存不仅具有容错能力,还应尽量减少高速缓存中的数据丢失。

memcached本身有什么问题?


通常,即用型的PHP memcached扩展支持数据和会话的分布式存储。 一致的密钥哈希机制使您可以在许多服务器上平均分配数据,明确地将每个特定密钥分配给组中的特定服务器,并且故障转移的内置工具可提供高速缓存服务的可用性(但不幸的是, 不是数据 )。

使用存储会话,情况会更好一些:您可以配置memcached.sess_number_of_replicas ,这样数据将立即保存到多台服务器,并且如果一个Memcached实例发生故障,则数据将从其他实例转移。 但是,如果服务器在没有数据的情况下恢复服务(通常是在重新启动后),则部分密钥将重新分配给它。 实际上,这将意味着会话数据丢失 ,因为如果丢失,则无法“转到”另一个副本。

标准库工具主要针对水平扩展:它们允许您将缓存增加到巨大的大小,并提供位于不同服务器上的代码对其的访问。 但是,在我们的情况下,存储的数据量不会超过几GB,一个或两个节点的性能就足够了。 因此,从一种有用的常规方式来看,它们只能确保memcached的可用性,同时将至少一个缓存实例保持在工作状态。 但是,即使利用这个机会,我也没有成功……在这里,我们应该回顾一下项目中使用的框架的上古时代,这使得应用程序无法与服务器池一起使用。 我们也不会忘记会话数据的丢失:大批用户退出客户的视线引起了人们的注意。

理想情况下,如果丢失或出错,则需要在内存缓存和爬网副本复制记录Mcrouter帮助我们实施了这一策略。

微电脑


这是Facebook开发的用于解决其问题的Memcached路由器。 它支持内存缓存文本协议,该协议允许您将内存缓存安装扩展到疯狂的大小。 有关微计算机的详细说明,请参见本公告 。 在其他广泛功能中,它可以满足我们的需求:

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

为了事业!

微电脑配置


我将直接进入配置:

 { "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的请求-到池中的所有服务器。 例如,您可以将池中的服务器数量减少到两个:牺牲存储可靠性,我们将获得更快的速度和更少的请求(从丢失密钥的请求)。

注意Wiki中的文档项目问题 (包括已关闭的文档 )代表了各种配置的整个仓库,对于学习微型计算机也是有用的链接。

构建并运行微计算机


该应用程序(和memcached本身)在Kubernetes中为我们工作-分别在同一地点和mcrouter。 要构建容器,我们使用werf ,其配置如下所示:

注意 :本文清单是在flant / mcrouter存储库中发布的

 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.imtqy.com/mcrouter/debrepo/xenial/PUBLIC.KEY - name: Add mcrouter Repo apt_repository: repo: deb https://facebook.imtqy.com/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

...然后扔一张Helm图表 。 有趣的是-副本数量上只有一个配置生成器(如果有人有一个更简洁和优雅的选项-请在注释中共享)

 {{- $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 > 

文本中的搜索没有给出错误,但是在“ mcrouter php ”的请求下,最老的未关闭项目问题出现在最前沿- 缺少对内存缓存二进制协议的支持

注意 :memcached中的ASCII协议比二进制要慢,并且常规的一致性密钥哈希方法仅适用于二进制协议。 但这不会在特定情况下产生问题。

问题出在帽子上:仅保留切换到ASCII协议,它将起作用.. 但是,在这种情况下, 在php.net上文档中寻找答案的习惯开了一个残酷的玩笑。 您将不会在这里找到正确的答案...当然,除非您一直翻到最后,在“用户提供的注释”部分中,将提供正确且不受欢迎的答案

是的,正确的选项名称是memcached.sess_binary_protocol 。 必须禁用它,然后会话才能开始工作。 剩下的只是将带有mcrouter的容器放在PHP的容器中!

结论


因此,仅借助基础架构更改,我们就能够解决所提出的问题:解决了内存缓存容错的问题,提高了缓存存储的可靠性。 除了为应用程序带来明显优势之外,这还为在平台上工作提供了回旋余地:当所有组件都有储备时,极大地简化了管理员的工作。 是的,这种方法也有其缺点,它看起来可能像“拐杖”,但是如果它可以省钱,可以掩盖问题并且不会引起新问题-为什么不这样做?

聚苯乙烯


另请参阅我们的博客:

Source: https://habr.com/ru/post/zh-CN457510/


All Articles