К нам пришёл клиент с формулировкой, которую слышал каждый: «сервис ложится под нагрузкой, нужно переписать на чём-нибудь быстром». Переписывание — самая дорогая из возможных гипотез, поэтому первое, что мы сделали, — отказались её проверять, пока не появятся измерения.
Договорились так: неделя на диагностику, и только потом разговор о том, что делать. Если после недели окажется, что архитектура действительно тупиковая, — перепишем осознанно, а не из ощущения.
Начали с самого скучного: сняли метрики. Не «сервер тормозит», а конкретные числа — сколько запросов в секунду приходит, как распределяется время ответа по перцентилям, сколько времени занимает каждый внешний вызов. Оказалось, что средний ответ был 180 мс и выглядел прилично, а p99 уходил за восемь секунд. Именно эти восемь секунд пользователи и называли «сервис лежит».
Среднее вообще плохо описывает боль пользователя. Если девяносто запросов отвечают мгновенно, а десять висят по десять секунд, среднее будет выглядеть здоровым, а сервисом будет невозможно пользоваться. Мы почти никогда не смотрим на среднее.
Дальше сняли профиль. В Go для этого есть pprof, и это, наверное, лучший аргумент в пользу языка: подключается парой строк, снимается на живом сервисе, показывает, где реально сгорает процессорное время и что держит память.
Профиль показал неожиданное. Узким местом была не база данных, на которую все грешили, а генерация PDF-документов, которая происходила прямо внутри HTTP-обработчика. Пользователь нажимал кнопку и ждал, пока сервер соберёт документ, отправит его во внешний сервис подписи, дождётся ответа и только потом вернёт результат. Всё это время горутина была занята, а соединение — открыто.
Дальше цепочка становилась хуже. Когда таких запросов приходило много, они забивали пул соединений к базе — не потому, что база не справлялась, а потому что соединения удерживались всё время долгой операции. Быстрые запросы вставали в очередь за медленными. Так один тяжёлый эндпоинт положил весь сервис.
Лечение оказалось архитектурным, но не требовало переписывания. Мы вынесли генерацию документов в очередь: HTTP-обработчик теперь только кладёт задачу и сразу отдаёт клиенту идентификатор. Отдельные воркеры разбирают очередь в своём темпе. Интерфейс показывает статус и обновляет его, когда документ готов.
Это изменение стоило примерно трёх дней. p99 упал с восьми секунд до четырёхсот миллисекунд, потому что медленные операции перестали делить пул с быстрыми.
Второй выигрыш дал кэш. Профиль показывал, что один и тот же справочник — список складов, штук двести записей, меняется раз в месяц — читался из базы на каждый запрос. Мы положили его в Redis с временем жизни в пять минут. Минус тридцать процентов запросов к базе одной правкой.
Третье, менее очевидное: мы настроили пул соединений. По умолчанию драйвер открывает соединений столько, сколько попросят, и под нагрузкой сервис устраивал базе отказ в обслуживании силами самого себя. Ограничили пул числом, которое база выдерживает, и добавили таймауты. Под пиком сервис стал деградировать плавно вместо того, чтобы падать.
Отдельно про таймауты. Любой внешний вызов без таймаута — это отложенная авария. Рано или поздно внешний сервис начнёт отвечать не за сто миллисекунд, а за две минуты, и ваши горутины закончатся. Мы ставим таймаут на каждый вызов наружу, всегда, даже когда «этот сервис всегда быстрый».
Итог: 200 rps превратились в 4000 rps на том же железе и том же языке. Переписывать не пришлось.
Мы не рассказываем эту историю как «смотрите, какие мы молодцы». Все четыре примененных приёма — очередь, кэш, пул, таймауты — описаны в любом учебнике. Ценность была не в них, а в том, что мы неделю не трогали код, пока не поняли, где именно теряется время.
Правило, которое мы из этого носим по всем проектам: сначала измерение, потом гипотеза, потом правка. Оптимизация без профиля — это угадывание, и обычно вы угадываете не то. В нашем случае вся команда была уверена, что виновата база.
Понравилась статья?
У нас много таких историй — и ещё больше идей для вашего продукта.
Обсудить проектЧитать дальше
Как мы пришли к ScanClick
Продукт вырос из чужой боли, которую мы увидели на клиентском проекте: склад, тетрадь и три часа сверки каждый вечер.
Архитектура API с нуля: наш подход
Контракты, версионирование и ошибки, которые не стыдно отдавать наружу. Делимся структурой, которая работает.
Очереди и вебхуки: как не потерять сообщения
Идемпотентность, ретраи и dead-letter. Рассказываем, как строим надёжную обработку событий.