Горизонтальное масштабирование PHP-приложений, часть 1
Представим, что мы сделали сайт. Процесс был увлекательным и очень приятно наблюдать, как увеличивается число посетителей.
Но в какой-то момент, траффик начинает расти очень медленно, кто-то опубликовал ссылку на ваше приложение в Reddit или Hacker News, что-то случилось с исходниками проекта на GitHub и вообще, все стало как будто против вас.
Ко всему прочему, ваш сервер упал и не выдерживает постоянно растущей нагрузки. Вместо приобретения новых клиентов и/или постоянных посетителей, вы остались у разбитого корыта и, к тому же, с пустой страничкой.
Все ваши усилия по возобновлению работы безрезультатны – даже после перезагрузки, сервер не может справиться с потоком посетителей. Вы теряете трафик!
Никто не может предвидеть проблемы с трафиком. Очень немногие занимаются долгосрочным планированием, когда работают над потенциально высокодоходным проектом, чтобы уложиться в фиксированные сроки.
Как же тогда избежать всех этих проблем? Для этого нужно решить два вопроса: оптимизация и масштабирование.
Оптимизация
Первым делом, стоит провести обновление до последней версии PHP (текущая версия 5.5, использует OpCache), проиндексировать базу данных и закэшировать статический контент (редко изменяющиеся страницы вроде About, FAQ и так далее).
Оптимизация затрагивает не только кэширование статических ресурсов. Также, есть возможность установить дополнительный не-Apache-сервер (например, Nginx), специально предназначенный для обработки статического контента.
Идея заключается в следующем: вы помещаете Nginx перед вашим Apache-сервером (Ngiz будет frontend-сервером, а Apache - backend), и поручаете ему, перехват запросов на статические ресурсы (т.е. *.jpg, *.png, *.mp4, *.html…) и их обслуживание БЕЗ ОТПРАВЛЕНИЯ запроса на Apache.
Такая схема называется reverse proxy (её часто упоминают вместе с техникой балансировки нагрузки, о которой рассказано ниже).
Масштабирование
Существует два типа масштабирования – горизонтальное и вертикальное.
Мы говорим, что сайт масштабируем, когда он может выдерживать увеличение нагрузки без необходимости внесения изменений в программное обеспечение.
Вертикальное масштабирование
Представьте, что у вас имеется веб-сервер, обслуживающий веб-приложение. Этот сервер имеет следующие характеристики 4GB RAM, i5 CPU и 1TB HDD.
Он хорошо выполняет возложенные на него задачи, но чтобы лучше справляться с нарастающим трафиком, вы решаете заменить 4GB RAM на 16GB, устанавливаете новый i7 CPU и добавляете гибридный носитель PCIe SSD/HDD.
Сервер теперь стал более мощным и может выдерживать увеличенные нагрузки. Именно это и называется вертикальным масштабированием или «масштабированием вглубь» – вы улучшаете характеристики машины, чтобы сделать её более мощной.
Это хорошо проиллюстрировано на изображении ниже:

Горизонтальное масштабирование
С другой стороны, мы имеем возможность произвести горизонтальное масштабирование. В примере, приведенном выше, стоимость обновления железа едва ли будет меньше стоимости первоначальных затрат на приобретение серверного компьютера.
Это очень финансово затратно и часто не дает того эффекта, который мы ожидаем – большинство проблем масштабирования относятся к параллельному выполнению задач.
Если количества ядер процессора недостаточно для выполнения имеющихся потоков, то не имеет значения, насколько мощный установлен CPU – сервер все равно будет работать медленно, и заставит посетителей ждать.
Горизонтальное масштабирование подразумевает построение кластеров из машин (часто достаточно маломощных), связанных вместе для обслуживания веб-сайта.
В данном случае, используется балансировщик нагрузки (load balancer) – машина или программа, которая занимается тем, что определяет, какому кластеру следует отправить очередной поступивший запрос.
А машины в кластере автоматически разделяют задачу между собой. В этом случае, пропускная способность вашего сайта возрастает на порядок по сравнению с вертикальным масштабированием. Это также известно как «масштабирование вширь».
Есть два типа балансировщиков нагрузки – аппаратные и программные. Программный балансировщик устанавливается на обычную машину и принимает весь входящий трафик, перенаправляя его в соответствующий обработчик. В качестве программного балансировщика нагрузки, может выступить, например, Nginx.
Он принимает запросы на статические файлы и самостоятельно их обслуживает, не обременяя этим Apache. Другим популярным программным обеспечением для программной балансировки является Squid, который я использую в своей компании. Он предоставляет полный контроль над всеми возможными вопросами посредством очень дружественного интерфейса.
Аппаратные балансировщики представляет собой отдельную специальную машину, которая выполняет исключительно задачу балансировки и на которой, как правило, не установленного другого программного обеспечения. Наиболее популярные модели разработаны для обработки огромного количества трафика.
При горизонтальном масштабировании происходит следующее:

Заметьте, что два описанных способа масштабирования не являются взаимоисключающими – вы можете улучшать аппаратные характеристики машин (также называемых нодами - node), используемых в масштабированной вширь кластерной системе.
В данной статье мы сфокусируемся на горизонтальном масштабировании, так как в большинстве случаев оно предпочтительнее (дешевле и эффективнее), хотя его и труднее реализовать с технической точки зрения.
Сложности с разделением данных
Имеется несколько скользких моментов, возникающих при масштабировании PHP-приложений. Узким местом здесь является база данных (мы еще поговорим об этом во второй части данного цикла).
Также, проблемы возникают с управлением данными сессий, так как залогинившись на одной машине, вы окажетесь неавторизованным, если балансировщик при следующем вашем запросе перебросит вас на другой компьютер. Есть несколько способов решения данной проблемы – можно передавать локальные данные между машинами, либо использовать постоянный балансировщик нагрузки.
Постоянный балансировщик нагрузки
Постоянный балансировщик нагрузки запоминает, где обрабатывался предыдущий запрос того или иного клиента и, при следующем запросе, отправляет запрос туда же.
Например, если я посещал наш сайт и залогинился там, то балансировщик нагрузки перенаправляет меня, скажем, на Server1, запоминает меня там, и при следующем клике, я вновь буду перенаправлен на Server1. Все это происходит для меня совершенно прозрачно.
Но что, если Server1 упал? Естественно, все данные сессии будут утеряны, а мне придется логиниться заново уже на новом сервере. Это очень неприятно для пользователя. Более того, это лишняя нагрузка на балансировщик нагрузки: ему нужно будет не только перенаправить тысячи людей на другие сервера, но и запомнить, куда он их перенаправил.
Это становится еще одним узким местом. А что, если единственный балансировщик нагрузки сам выйдет из строя и вся информации о расположении клиентов на серверах будет утеряна? Кто будет управлять балансировкой? Замысловатая ситуация, не правда ли?
Разделение локальных данных
Разделение данных о сессиях внутри кластера определенно кажется неплохим решением, но требует изменений в архитектуре приложения, хотя это того стоит, потому что узкое место становится широким. Падение одного сервера перестает фатально влиять на всю систему.
Известно, что данные сессии хранятся в суперглобальном PHP-массиве $_SESSION. Также, ни для кого не секрет, что этот массив $_SESSION хранится на жестком диске.
Соответственно, так как диск принадлежит той или иной машине, то другие к нему доступа не имеют. Тогда как же организовать к нему общий доступ для нескольких компьютеров?
Замечу, что обработчики сессий в PHP могут быть переопределены – вы можете определить свой собственный класс/функцию для управления сессиями.
Использование базы данных
Используя собственный обработчик сессий, мы можем быть уверены, что вся информация о сессиях хранится в базе данных. База данных должна находиться на отдельном сервере (или в собственном кластере). В таком случае, равномерно нагруженные сервера, будут заниматься только обработкой бизнес-логики.
Хотя данный подход работает достаточно хорошо, в случае большого трафика, база данных становится не просто уязвимым местом (потеряв её, вы потеряете все), к ней будет много обращений из-за необходимости записывать и считывать данные сессий.
Это становится очередным узким местом в нашей системе. В этом случае, можно применить масштабирование вширь, что проблематично при использовании традиционных баз данных типа MySQL, Postgre и тому подобных (эта проблема будет раскрыта во второй части цикла).
Использование общей файловой системы
Можно настроить сетевую файловую систему, к которой будут обращаться все серверы, и работать с данными сессий. Так делать не стоит. Это совершенно неэффективный подход, при котором велика вероятность потери данных, к тому же, все это работает очень медленно.
Это еще одна потенциальная опасность, даже более опасная, чем в случае с базой данных, описанном выше. Активация общей файловой системы очень проста: смените значение session.save_path в файле php.ini, но категорически рекомендуется использовать другой способ.
Если вы все-таки хотите реализовать вариант с общей файловой системой, то есть гораздо более лучшее решение - GlusterFS.
Memcached
Вы можете использовать memcached для хранения данных сессий в оперативной памяти. Это очень небезопасный способ, так как данные сессий будут перезаписаны, как только закончится свободное дисковое пространство.
Какое-либо постоянство отсутствует – данные о входе будут храниться до тех пор, пока memcached-сервер запущен и имеется свободное пространство для хранения этих данных.
Вы можете быть удивлены – разве оперативная память не отдельна для каждой машины? Как применить данный способ к кластеру? Memcached имеет возможность виртуально объединять всю доступную RAM нескольких машин в единое хранилище:

Чем больше машин у вас в наличии, тем больше будет размер созданного общего хранилища. Вам не нужно вручную распределять память внутри хранилища, однако вы можете управлять этим процессом, указывая, какое количество памяти можно выделить от каждой машины для создания общего пространства.
Таким образом, необходимое количество памяти остается в распоряжении компьютеров для собственных нужд. Остальная же часть используется для хранения данных сессий всего кластера.
В кэш, помимо сессий могут попадать и любые другие данные по вашему желанию, главное чтобы хватило свободного места. Memcached это прекрасное решение, которое получило широкое распространение.
Использовать этот способ в PHP-приложениях очень легко: нужно изменить значение в файле php.ini:
session.save_handler = memcache session.save_path = "tcp://path.to.memcached.server:port"
Redis Cluster
Redis это не SQL хранилище данных, расположенное в оперативной памяти, подобно Memcached, однако оно имеет постоянство и поддерживает более сложные типы данных, чем просто строки PHP-массива в форме пар «key => value».
Это решение не имеет поддержки кластеров, поэтому реализация его в горизонтальной системе масштабирования не так проста, как может показаться на первый взгляд, но вполне выполняема. На самом деле, альфа-версия кластерной версии уже вышла и можно её использовать.
Если сравнивать Redis с решениями вроде Memcached, то он представляет собой нечто среднее между обычной базой данных и Memcached.
Другие решения:
- ZSCM от Zend – хорошая альтернатива, но требует установки Zend Server на каждый нод в кластере;
- Прочие не SQL хранилища и системы кэширования также вполне работоспособны – ознакомьтесь с Scache, Cassandra или Couchbase, все они работают быстро и надежно.
Заключение
Как вы могли понять из написанного выше, горизонтальное масштабирование PHP-приложений это не пикник на выходных.
Возникает множество препятствий, способы устранения которых достаточно нетривиальны и имеют свои преимущества и недостатки. К тому же, к моменту, когда трафик возрастет до критической отметки, плавный переход осуществить уже, как правило, невозможно.
Надеюсь, эта короткая статья поможет вам сделать лучший выбор для своей компании. Во второй части данного цикла, мы обсудим масштабирование базы данных.