При работе над пет проектами дело редко доходит до длительной эксплуатации. В ТЗ проектов, начиная с третьего, есть деплой на удаленный сервер, и студенты часто делятся своими задеплоенными проектами, но о длительной эксплуатации речь обычно не идёт.

В этой статье я хочу поделиться двумя историями из полугодовой жизни нашего первого командного проекта, который мы писали летом - сервис аналитики рынка труда 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.

Если посмотреть, какой из сервисов потребляет больше всего памяти, то это сервис, ответственный за построение аналитики:

11e5a9d2c30f  compose-stack-analytics-builder-service-1    0.21%   2.138GiB / 7.57GiB

Данный сервис выгружает из хранилища все вакансии, и строит по ним срез аналитики - средние зарплаты, количество вакансий. Графики на главной странице сайта отображают историю изменения таких срезов за последние 30 дней.

Тонкое место данного сервиса - выгрузка вакансий из MongoDB для анализа. Вакансии выгружаются (https://github.com/IT-job-market-analytics/analytics-builder-service/blob/dev/src/main/java/com/example/analyticsbuilderservice/service/Consumer.java#L33) из хранилища разом. Лучшим решением было бы выгружать их постранично. Благодаря git blame можно увидеть виновника данного недосмотра:

TODO картинка

Предполагаемая хронология событий

  • Сервис анализа вакансий стал потреблять больше 2 гб памяти
  • Сервис импорта вакансий получил ошибку 503 от api headhunter и попробовал вернуть задачу в очередь
  • RabbitMQ из-за недостатка RAM не смог обработать возврат задачи в очередь
  • Сервис импорта вакансий повис, ожидая обработки операции по возврату задачи в очередь, и перестал обрабатывать новые сообщения
  • Импорт остановился, в следствие чего остановилась рассылка уведомлений о новых вакансиях, а сервис аналитики строил каждый день одинаковые срезы, потому что коллекция вакансий не обновлялась

Как я боролся бы с такими проблемами в рабочем проекте

Прошло более двух недель, пока я заметил инцидент и устранил его. Первая проблема - отсутствие прозрачности о состоянии проекта. В реальных проектах такое неприемлемо и разработчики должны узнавать об инцидентах до того, как первый пользователь обратится в тех.поддержку. Вторая проблема - одна точка отказа. Память кончилась и всё посыпалось.

Достичь этого можно с помощью инструментов мониторинга и алертинга. Сервис должен быть развернут в инфраструктуре, которая позволяет прозрачно наблюдать за здоровьем всех компонентов системы.

Для упрощения, http://it-jobs.zhukovsd.com/ развернут через Docker Compose на одном виртуальном сервере.

В коммерческих проектах, при наличии требований по отказоустойчивости и стабильность, я стремлюсь к такой инфраструктуре:

  • Kubernetes кластер из 2 или более серверов. Kubernetes содержит встроенные механизмы обеспечения отказоустойчивости - упавшие сервисы автоматически перезапускаются, зависшие сервисы убиваются и поднимаются, потребление ресурсов балансируется между нодами кластера.
  • Мониторинг для автоматического сбора информации о текущем состоянии системы - количество свободных CPU/RAM ресурсов, статус контейнеров, бизнес метрики - количество импортированных вакансий, отправленных уведомлений.
  • Алертинг - если какая-то из метрик принимает нездоровое значение, автоматически создается инцидент, уведомление о котором может прийти ответственному за стабильность, например на почту или в Slack.

На практике я обычно использую kube-prometheus для мониторинга, alert-manager для алертов и отправки уведомлений в Slack.

Что касается конкретных технических проблем, которые привели к инциденту выше (большой таймаут RabbitMQ, большое потребление памяти со стороны analytics-builder-service), то такие моменты всегда будут, и по отдельности являются менее важными, чем грамотная и устойчивая инфрастуктура, позволяющая быстро выявлять, идентифицировать и исправлять такие проблемы. Это является одним из главных (но не единственным) подходов по обеспечению качества приложений.