Привет всем! Я Руслан Абдуллаев, DevOps-инженер Технократии. Хочу рассказать про наш проект из 2020. В тексте будет немного моей боли, признание ошибок архитектуры, переход к ansible и minio, и финальная форма покемона без единого даунтайма
«Все упало – нужно поднять!»
На старте со стороны архитектуры все было несложно: два сервера для дев и тест окружения, плюс прод, состоящий из двух серверов средней мощности: под приложение и базу.
Но вслед за простотой пришли проблемы. Мы стали замечать слабые стороны такой конфигурации. Основная беда — упор в ресурсы. Под нашим контролем было только вертикальное масштабирование. То есть на все вызовы мы могли отвечать банальным увеличением ресурсов прод-серверов. Но и это спасало лишь на время. В итоге — частые падения и понимание, что у всего этого есть потолок.
Будильник в те времена был не нужен: в 6 утра тебя все равно разбудит менеджер, с пеной у рта кричащий: «все упало – нужно поднять!».
Ситуация не устраивала всех, начиная от пользователей, которые не могли пользоваться платформой, заканчивая мной, который хотел спать. И тут хочется сказать, что в какой-то момент мы встали и со словами “хватит это терпеть” начали все резко менять, но нет. Изменения, скорее, были естественным продолжением проекта.
Мы поняли, что горизонтальное масштабирование должно решить большую часть проблем. Мы приступили. Нужно было разнести компоненты проекта по серверам и сделать дублирование для отказоустойчивости.
Закупились серверами, в течение пары месяцев перерабатывали все, что можно переработать, собрали новый прод и после зимних каникул переключили весь трафик со старого на новый.
Теперь детали
Пришлось погрузиться в тему кластеров баз данных: так как мы строили отказоустойчивую инфраструктуру, нельзя было обойтись всего одним сервером, это стало бы точкой отказа.
Было больно поднимать этот кластер из-за опасности испортить все данные и положить платформу на долгое время. Но и от этого можно спастись. Нужно всего лишь каждый день перед сном…. держать в голове, что реплицирование базы всегда занимает какое-то время. Если мы хотим читать данные с реплики сразу после того, как записали в мастер, то может задуматься о кешировании?
Еще на данном этапе мы не настраивали автопереключение мастера, но не факт, что это нужно. Главной задачей было не допустить, чтобы тяжеловесные запросы на чтение мешали другим операциям с данными. Также помним о возможном сплитбрейне при автопереключении. Вот такой народный рецепт.
С технической стороны кластер выглядит так:
3 сервера по 8 CPU 16 RAM и 1tb ssd. Реплицирование на основе транслирования журнала WAL.
В итоге у нас кластер из трех серверов, которые могут проглотить довольно большую нагрузку. Основной же удар примут четыре новых сервера, поэтому пришлось переработать хранение файлов. Уже нельзя использовать для этого тот же сервер, где работал бекенд – это приведет к рассинхрону: один файл запишется на первый сервер, а другой — на второй.
Мы с нашим бекенд разработчиком принялись искать возможные решения и остановились на minio, так как это s3 совместимое хранилище. При этом его можно развернуть на наших серверах и довольно тонко настроить. Например, обеспечить запись файлов только по внутренней сети, чтобы какие-то данные мог загрузить только бекенд, а наружу выкинуть только раздачу файлов.
Кстати, недавно столкнулся с обновлением minio, которое сломало нам дев среду. С обновлением произошло разделение админки с апи на разные порты. На первый взгляд ничего страшного, я добавил еще один домен и повесил на него админку, но почему-то часть нужного нам функционала сломалась. Например, шер файлов по ссылке. Пришлось откатываться до версии, которая была до этого.
Идем дальше. Я укрепил свои знания в деплое с помощью ansible. Это очень удобно. И почему я раньше так не делал? До ansible мы использовали обычный скрипт, который выкатывал нужные изменения на сервер из gitlab ci/cd. С приходом нескольких серверов, которые нужно обновлять, такой подход уже не работал. Зато ansible идеально подошел для этой задачи. Можно сказать, ему что делать, и он выполнит это параллельно на всех серверах.
Работаем с нагрузкой
Следующий пункт — балансировка нагрузки. Тут по классике, два сервера с nginx, выступающие входной точкой в платформу и размазывающие нагрузку по серверам.
Важно уточнить, что нагрузка, генерируемая во время нагрузочного тестирования, отличается от реальной пользовательской нагрузки. Как бы хорошо не были написаны тесты, они не смогут предугадать реакцию системы на настоящих пользователей.
Пример:
Перед переключением трафика на новый прод мы в течение нескольких дней активно нагружали его. Все просто летало. И вот мы запустили настоящих пользователей, но уже через несколько дней столкнулись с проблемой нехватки оперативной памяти на бекенд серверах. Все стало тормозить даже хуже, чем раньше.
Оказалось, мы неверно настроили количество gunicorn воркеров. С настоящими пользователями они начали отъедать много памяти. Тут стоит громко сказать: «не верьте официальной доке gunicorn!». Количество воркеров нужно ставить в зависимости от вашего приложения и проведенных тестов, а не на основе формулы которая дана в доке.
Но это не конец. Когда поняли проблему, решили снизить количество воркеров. Но у нас резко перестал собираться билд, мы не могли выкатить обновление. В течение следующих нескольких дней мы возились со сборкой и каждую ночь ребутали прод, чтобы он не лагал от нехватки памяти. В итоге пришлось сменить базовый докер-образ приложения и переписать его сборку. Это помогло выкатиться и решить проблему. После этого иногда все же приходилось ребутаться, но уже очень редко.
Пара слов о мониторинге
Мы настроили мониторинг всего железа, докер контейнеров и аптайма самой платформы на основе zabbix. Плюс никуда без алертов в телеграм в случае проблем. В конечном итоге это помогло предугадывать проблемы и решать их заранее, а не как обычно, когда все ломается в 6 утра. И вот мы уже более 6 месяцев без единого даунтайма.
Это мой первый текст, поэтому буду особенно рад вопросам в комментариях. Всем, кто добрался досюда спасибо! Как и обещал, в конце нечто особенное: пак девопсерских мемов.
Кстати, подписывайтесь на наш телеграм-канал «Голос Технократии». Каждое утро мы публикуем новостной дайджест из мира ИТ, а по вечерам делимся интересными и полезными мастридами.
Комментарии (9)
amarao
11.08.2021 15:41+1Для "поделиться опытом" мало обобщений. Вы рассказали, как конкретное приложение конкретно пилили. Для личного опыта может быть хорошо, для помощи другим - 0.
И очень, очень много велосипедов (сделай сам). Я понимаю, что могут быть причины, но тогда их хотя бы надо декларировать.
Scank
11.08.2021 16:02Для меня сервер это как минимум xeon'чик и 128гб озу, у вас древние сервера или платформа с миллионом пользователей ? что за система которая постоянно ложится от нехватки ?
ps. Картинок много, но про пингвина поржал =)technokratiya Автор
11.08.2021 16:28Отвечает автор: У нас речь идет не о миллионах, а скорее о сотнях тысяч клиентов и довольно тяжелых запросах. И по опыту лучше вместо одного очень мощного сервера взять несколько средней мощности и распределить нагрузку на них :)
Alexey_Shalin
12.08.2021 06:07Добрый день, Автор !
Статья безусловно интересная, но хотелось бы например узнать, как вы построили кластер Postgres (сколько нод, какое ПО) ? почему именно выбрали эту реализацию ?
technokratiya Автор
12.08.2021 11:26Как было описано выше, у нас 3 ноды и трансирование журнала WAL. Выбрали этот метод из за большой надежности и простоты в эксплуатации) Подробнее написали ребята из Postgres Pro, https://postgrespro.ru/docs/postgrespro/10/warm-standby
beezy92
12.08.2021 08:47С настоящими пользователями они начали отъедать много памяти. Тут стоит громко сказать: «не верьте официальной доке gunicorn!». Количество воркеров нужно ставить в зависимости от вашего приложения и проведенных тестов, а не на основе формулы которая дана в доке.
А теперь заходим в документацию (https://docs.gunicorn.org/en/stable/design.html) и читаем:
DO NOT scale the number of workers to the number of clients you expect to have. Gunicorn should only need 4-12 worker processes to handle hundreds or thousands of requests per second.
Gunicorn relies on the operating system to provide all of the load balancing when handling requests. Generally we recommend
(2 x $num_cores) + 1
as the number of workers to start off with. While not overly scientific, the formula is based on the assumption that for a given core, one worker will be reading or writing from the socket while the other worker is processing a request.Obviously, your particular hardware and application are going to affect the optimal number of workers. Our recommendation is to start with the above guess and tune using TTIN and TTOU signals while the application is under load.
Always remember, there is such a thing as too many workers. After a point your worker processes will start thrashing system resources decreasing the throughput of the entire system.
Что мы видим? В документации, ясно указывается, что формулу надо использовать как отправную точку, но в зависимости от типа нагрузки и профиля использования (CPU Bound vs IO Bound, long-polling и т.д.) нужно выставлять данные параметры самому. Нету универсального способа. И в самом начале указывается, что в целом достаточно иметь 4-12 воркеров, и что ненужно иметь их много.
iron_udjin
04.09.2021 08:26До ansible мы использовали обычный скрипт, который выкатывал нужные изменения на сервер из gitlab ci/cd. С приходом нескольких серверов, которые нужно обновлять, такой подход уже не работал.
Что-то не совсем понятно. С появлением доп. серверов сложно было в в скрипте перед блоком деплоя написать: for srv in {1..n};do ... done и деплоить не на один а на все сервера?
lexore
10.09.2021 15:06В какой-то момент вы решите переезжать в kubernetes. Вот тогда у вас будет весело.
bezarius
Мне кажется с пикчами перебор :\ Может стоило остановиться на какой то одной?