При работе над пет проектами дело редко доходит до длительной эксплуатации. В ТЗ проектов, начиная с третьего, есть деплой на удаленный сервер, и студенты часто делятся своими задеплоенными проектами, но о длительной эксплуатации речь обычно не идёт.
В этой статье я хочу поделиться двумя историями из полугодовой жизни нашего первого командного проекта, который мы писали летом - сервис аналитики рынка труда it-jobs.zhukovsd.com:
Функционал проекта - парсить вакансии с hh.ru, уведомлять о новых вакансиях через телеграм бота, строить аналитику по количеству вакансий и средней зарплате
Проект задеплоен в конце августа. С тех пор он крутится на сервере провайдера Hetzner, успел импортировать 144 тысячи вакансий и разослать 2 миллиона телеграм уведомлений. Запущен на 4 CPU cores / 8 GB RAM машине через Docker Compose.
История #1 - взлом и удаление данных из БД
Костя - один из студентов, работавших над проектом, запускал свой собственный экземпляр приложения не локально, а на личном удалённом сервере. Такой экземпляр необходим для разработки и тестирования функционала. По возможности, такие экземпляры проекта разворачиваются локально, но у Кости была причина прибегнуть к удалённому серверу - недостаток памяти на его ноутбуке, что не давало возможности адекватно запускать проект и параллельно заниматься разработкой с запущенными IDEA и браузером.
В таких личных экземплярах приложения безопасность не является приоритетом, и для удобства все пароли максимально простые - password. Это упрощает конфигурацию и ручной доступ к базам данных. Обратная сторона такого удобства - уязвимость. Приложения, запущенные локально, атаковать некому, но Костя запустил экземпляр на сервере.
Результат - через некоторое время данные из MySQL Кости были удалены, вместо этого была создана таблица README_TO_RECOVER. Единственная запись в этой таблице описывала, на какой криптокошелек перевести выкуп и как связаться со злоумышленниками для получения бэкапа данных.
Хронология событий
Костя запускает свой экземпляр приложения, MySQL сервис открывает наружу порт 3306, доступ к которому защищен элементарным паролем password
У злоумышленников 24/7 работают системы, сканирующие публичные IP адреса на предмет открытых портов. Если порт найден - пробуют зайти в приложение по словарю самых частых пар логинов и паролей
Когда доступ получен - зловредное приложение делает бэкап данных, выгружает его, удаляет данные из хранилища
Чтобы жертва знала куда обращаться, злоумышленники оставляют свои контакты и адрес крипокошелька для выкупа
Под такую атаку попадают многие популярные приложения - MySQL, Postgres, Wordpress, Tomcat, и так далее. Если у вас есть запущенный публично доступный Tomcat - загляните в его access логи, увидите множественные попытки получить доступ к админской панели.
В нашем конкретном примере - потеря данных с тестового локального окружения не является проблемой. Но несколько человек из моих знакомых сталкивались с ситуацией, когда их проекту пришлось заплатить выкуп.
Выводы
Не использовать пустые или простые пароли для данных, которые не хочется потерять
По возможности, максимально закрывать внешний доступ к сервисам
В production конфигураци проекта внешний доступ ко всем БД закрыт. К БД могут обращаться только контейнеры, запущенные в том же Docker Compose стеке.
История #2 - частичное падение production окружения из-за нехватки памяти
Главный элемента функционала проекта - каждый час парсить новые вакансии с Headhunter. В январе я заметил, что уведомления от бота перестали приходить.
Пошёл разбираться и первым делом посмотрел 2 вещи:
Количество RAM (свободно было 200 мб из 8 гб)
Логи сервисов отправки телеграм уведомлений и импорта вакансий из headhunter api
В логах сервиса импорта обнаружилась ошибка:
Caused by: org.springframework.web.client.HttpServerErrorException$ServiceUnavailable: 503 Service Unavailable: "<!DOCTYPE html><html lang=en><meta charset=utf-8><meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width"><title>Error 503</title><style>*{margin:0;padding:0}html{font:15px/22px arial,sans-serif;background: #fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}p{margin:11px 0 22px;overflow :hidden}ins{color:#777;text-decoration :none;}</style><p><b>503 - Service Unavailable .</b> <ins>That’s an error.</ins><p>The server is not ready to handle the request. <ins>That’s all we know.</ins>"
Headhunter вернул 503, что похоже на разовую проблему, а не бан по айпи или ограничение на количество запросов.
Сервис работает следующим образом:
Принимает задачи на импорт вакансий от другого сервиса - планировщика
Задачи поступают из RabbitMQ очереди
В случае ошибки при обработке задачи, она возвращается в очередь
Unknown exception occurred, propagating it, so the message will be re-queued
Однако, этого не произошло, операция по возврату задачу в очередь зависла на полчаса и упала с таймаутом:
2024-01-08T10:29:20.096Z ERROR 1 --- [172.19.0.8:5672] o.s.a.r.c.CachingConnectionFactory : Shutdown Signal: channel error; protocol method: #method<channel.close>(reply-code=406, reply-text=PRECONDITION_FAILED - delivery acknowledgement on channel 1 timed out. Timeout value used: 1800000 ms. This timeout value can be configured, see consumers doc guide to learn more, class-id=0, method-id=0)
На этом сервис заглох намертво, перестал принимать новые задачи и обрабатывать уже поставленные. Полагаю, что проблема вызвана недостатком RAM.
Если посмотреть, какой из сервисов потребляет больше всего памяти, то это сервис, ответственный за построение аналитики:
Данный сервис выгружает из хранилища все вакансии, и строит по ним срез аналитики - средние зарплаты, количество вакансий. Графики на главной странице сайта отображают историю изменения таких срезов за последние 30 дней.
Сервис анализа вакансий стал потреблять больше 2 гб памяти
Сервис импорта вакансий получил ошибку 503 от api headhunter и попробовал вернуть задачу в очередь
RabbitMQ из-за недостатка RAM не смог обработать возврат задачи в очередь
Сервис импорта вакансий повис, ожидая обработки операции по возврату задачи в очередь, и перестал обрабатывать новые сообщения
Импорт остановился, в следствие чего остановилась рассылка уведомлений о новых вакансиях, а сервис аналитики строил каждый день одинаковые срезы, потому что коллекция вакансий не обновлялась
Как я боролся бы с такими проблемами в рабочем проекте
Прошло более двух недель, пока я заметил инцидент и устранил его. Первая проблема - отсутствие прозрачности о состоянии проекта. В реальных проектах такое неприемлемо и разработчики должны узнавать об инцидентах до того, как первый пользователь обратится в тех.поддержку. Вторая проблема - одна точка отказа. Память кончилась и всё посыпалось.
Достичь этого можно с помощью инструментов мониторинга и алертинга. Сервис должен быть развернут в инфраструктуре, которая позволяет прозрачно наблюдать за здоровьем всех компонентов системы.
В коммерческих проектах, при наличии требований по отказоустойчивости и стабильность, я стремлюсь к такой инфраструктуре:
Kubernetes кластер из 2 или более серверов. Kubernetes содержит встроенные механизмы обеспечения отказоустойчивости - упавшие сервисы автоматически перезапускаются, зависшие сервисы убиваются и поднимаются, потребление ресурсов балансируется между нодами кластера.
Мониторинг для автоматического сбора информации о текущем состоянии системы - количество свободных CPU/RAM ресурсов, статус контейнеров, бизнес метрики - количество импортированных вакансий, отправленных уведомлений.
Алертинг - если какая-то из метрик принимает нездоровое значение, автоматически создается инцидент, уведомление о котором может прийти ответственному за стабильность, например на почту или в Slack.
На практике я обычно использую kube-prometheus для мониторинга, alert-manager для алертов и отправки уведомлений в Slack.
Что касается конкретных технических проблем, которые привели к инциденту выше (большой таймаут RabbitMQ, большое потребление памяти со стороны analytics-builder-service), то такие моменты всегда будут, и по отдельности являются менее важными, чем грамотная и устойчивая инфрастуктура, позволяющая быстро выявлять, идентифицировать и исправлять такие проблемы. Это является одним из главных (но не единственным) подходов по обеспечению качества приложений.