使用任何语言开发高负载项目都需要特殊的方法和使用特殊的工具,但是当涉及到 PHP 中的应用程序时,情况可能会变得如此严重,以至于您必须开发,例如,
这次的主角是基于 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' ]
...并绘制草图 舵图。 有趣的是,只有一个基于副本数量的配置生成器 (如果有人有更简洁优雅的选择,请在评论中分享):
{{- $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 }}
}
}
}
}
我们将其部署到测试环境中并检查:
# 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 >
搜索错误的文本没有给出任何结果,但是对于查询“
NB:memcached 中的 ASCII 协议比二进制协议慢,并且一致性密钥散列的标准方法仅适用于二进制协议。 但这不会对特定情况造成问题。
诀窍就在眼前:您所要做的就是切换到 ASCII 协议,一切都会正常工作...... 然而,在这种情况下,寻找答案的习惯
是的,正确的选项名称是 memcached.sess_binary_protocol
。 必须将其禁用,之后会话才会开始工作。 剩下的就是将带有 mcrouter 的容器放入带有 PHP 的 pod 中!
结论
因此,通过基础设施的改变,我们就能够解决这个问题:memcached的容错问题已经解决,并且缓存存储的可靠性也得到了提高。 除了为应用程序带来明显的优势之外,这还为在平台上工作时提供了回旋余地:当所有组件都有储备时,管理员的工作就大大简化了。 是的,这种方法也有其缺点,它可能看起来像“拐杖”,但如果它省钱,埋葬问题并且不会引起新的问题 - 为什么不呢?
PS
另请阅读我们的博客:
- “使用 dapp 进行练习” (以 symfony-demo 为例):
第 1 部分(构建简单的应用程序) и第 2 部分(使用 Helm 将 Docker 映像部署到 Kubernetes) ; - «
来自 Kubernetes 的生活:HTTP 服务器为何不利于西班牙人 “。
来源: habr.com