Обзор инцидентов за третий квартал — выдержка из внутренней панели мониторинга надежности

Incidents per 30 Days (Customer-Visible)
Jan   ██████████████████ 18
Feb   ████████████████   16
Mar   ██████████████     14
Apr   ███████████        11
May   ████████            8
Jun   ██████              6

Та же команда. Тот же продукт. Немного уменьшенная скорость вывода функциональности, начиная с апреля.

Что изменилось? Не смена дежурств, не стек мониторинга и не стратегия развертывания.

Мы переписали два ключевых сервиса на Rust. Скорость разработки упала примерно на 18% в течение трех месяцев. Количество инцидентов в продакшен-среде, видимых клиентам, сократилось на 44%.

Этот компромисс не был очевиден, когда мы начинали работать.

Неделя, когда все выглядело нормально — пока не перестало

Наша предыдущая реализация была на Go.

Она показывала хорошие результаты в бенчмарках. Чисто проходила нагрузочное тестирование. Проходила ревью кода без проблем.

А потом трафик изменился.

Он не стал пиковым — изменился его характер. Соединения с более длительным временем жизни. Большая вариативность размера полезной нагрузки. Больше кросс-региональных рассылок.

Под такой нагрузкой система деградировала способами, которые дэшборды не показывали сразу:

  • Фрагментация памяти неуклонно росла в течение 36 часов.
  • Перцентили пауз сборщика мусора (GC) выросли с 3 мс до 28 мс (p99).
  • Ретриты усиливали частичные сбои.
  • Показатели задержек (SLO) смещались, но не скачкообразно.

Ни одного сбоя. Никакой паники. Просто код, который не выдержал испытания реальной энтропией и начал необратимо деградировать.

Мы исправили одну утечку. Потом другую. Настраивали GC. Агрессивно пулили ресурсы. Добавили противодавление.

Каждое исправление устраняло один вид сбоя и привносило другой.

Это был весь корректный код. Просто он не был структурно устойчив к граничным случаям.

Первое переписывание ощущалось как регресс

Мы не стали переносить все.

Выбрали два сервиса, наиболее коррелировавших с каскадными инцидентами:

  1. Граничный сервис приема данных (высокая конкурентность, пакетный ввод-вывод).
  2. Уровень агрегации рассылок (высокая нагрузка на CPU и память).

Остальную часть системы оставили без изменений.

Первые пул-реквесты на Rust были медленными:

  • меньше строк в день;
  • более длительные циклы ревью;
  • больше архитектурных обсуждений до написания кода;
  • ошибки компиляции, которые казались… излишне регулярными.

Правила владения замедляли нас.

Не в плане синтаксиса — в плане проектирования.

Функции, которые раньше принимали &interface{}, теперь требовали указания времен жизни и явной семантики заимствования. Общее изменяемое состояние требовало обоснования. Асинхронные потоки заставляли прояснять границы выполнения.

Это ощущалось как торможение. Это и было торможением.

Первое развертывание в продакшен-среде прошло без происшествий — чего раньше не было

Сначала мы развернули сервис приема.

Интересна была не задержка. Интересно было отсутствие проблем.

Никаких медленных утечек памяти. Никаких неожиданных штормов повторных попыток. Никакого каскада сбоев.

Тот же профиль трафика. Меньшая вариативность.

Memory Usage Over 72 Hours (Before vs After)
Before (Go):
Steady climb from 1.4GB → 3.2GB
Periodic GC plateaus
After (Rust):
Flat at ~1.6GB
No visible step patterns

Использование CPU упало примерно на 12%. Задержка p99 упала примерно на 18%.

Но эти цифры были не главным. Главным было то, что не разбудило нас в 2:17 ночи.

Где проявилась реальная разница

Через три месяца после миграции мы провели анализ классификации инцидентов.

Мы разделили производственные инциденты на категории:

Category                                    Pre-Rust (Q1)              Post-Rust (Q3)
==========================================================================
Memory / Leak / Fragmentation       7                           1
Concurrency / Race Conditions         5                           0    
Timeout Amplification                      3                           2
Logic / Business Bugs                      6                           6  
Infra / External Dependency            4                           4

Общее сокращение: 44%

Что не изменилось:

  • ошибки в бизнес-логике;
  • нестабильность внешних API;
  • ошибки конфигурации при развертывании.

Что изменилось:

  • Целые классы инцидентов, связанных с поведением во время выполнения, исчезли.

Их стало меньше. Они совсем исчезли.

Момент, когда мы поняли, что нас замедляло

Это была не система проверки заимствований.

Это была вынужденная ясность проектирования.

Вот небольшое, но показательное различие.

До (Go):

type Aggregator struct {
cache map[string]*Entry
mu    sync.RWMutex
}
func (a *Aggregator) Get(key string) *Entry {
a.mu.RLock()
defer a.mu.RUnlock()
return a.cache[key]
}

Корректно. Проверено. Запущено.

В итоге скрытый путь мутации обошел блокировку в другом участке кода.

Этот код прошел ревью. Прошел тесты. И вышел из строя при чередовании событий, которое мы не смоделировали.

После (Rust):

use std::sync::Arc;
use dashmap::DashMap;
pub struct Aggregator {
cache: Arc<DashMap<String, Entry>>,
}
impl Aggregator {
pub fn get(&self, key: &str) -> Option<Entry> {
self.cache.get(key).map(|e| e.clone())
}
}

Никакой явной дисциплины блокировок. Никаких случайных мутаций без права владения. Никаких вопросов на ревью вроде «Достаточно ли долгим было время удержания мьютекса?».

Rust не сделал нас умнее.

Он сделал определенные ошибки невозможными.

И именно навязывание этой дисциплины замедлило разработку.

Где Rust оказался болезненным

Мы заплатили за следующее.

1. Стоимость ввода в курс дела (онбординг)

Опытным инженерам, привыкшим к языкам со сборкой мусора, понадобились недели, а не дни.

Не синтаксис — ментальная модель.

2. Замедление на этапе компиляции

Рефакторинг каскадно проходил через ограничения времен жизни. Иногда мы боролись с компилятором. Иногда компилятор был прав. Различение этих ситуаций стоит времени.

3. Время сборки

Холодные сборки были заметно дольше. CI-пайплайны потребовали балансировки.

4. Меньше «быстрых исправлений»

На Go мы могли поставить защитную проверку за 20 минут. На Rust мы часто вместо этого реструктурировали поток данных.

Эта реструктуризация окупалась, но замедляла итерации.

Метрики скорости разработки снижались в течение двух кварталов.

Заинтересованные в функциональности стороны это заметили.

Что на самом деле сократило количество инцидентов

Дело не в том, что «Rust быстрее».

Дело в его свойствах:

  • отсутствии неявного разделяемого изменения;
  • проектировании API на основе владения;
  • исчерпывающем сопоставлении с образцом;
  • обеспечении обработки граничных случаев на этапе компиляции;
  • детерминированном поведении памяти под нагрузкой.

Под пиковым трафиком наши Rust-сервисы деградировали линейно.

Наши предыдущие сервисы деградировали нелинейно.

Нелинейная деградация — вот что будит разработчиков по ночам.

Конкретный артефакт бенчмарка

Мы воссоздали патологическую нагрузку, которая раньше вызывала каскадные повторные попытки.

Сценарий теста:

  • 15 000 RPS (запросов в секунду) постоянно;
  • 95% попаданий в кэш;
  • 5% рассылки к 12 нижестоящим сервисам;
  • искусственный джиттер в 300 мс на 2 нижестоящих узлах.

Результаты:

Metric              Go Version                Rust Version      
===============================================================
p99 Latency             842ms                    611ms
Max RSS                 3.4GB                     1.8GB
Error Amplification     3.2×                      1.4×
CPU Throttling          Yes                        No

Самая большая разница была не в базовой скорости.

А в том, как система вела себя при асимметричной нагрузке.

Неожиданный культурный эффект

Наши ревью кода изменились.

Вместо:

«Мы не забыли разблокировать здесь?»

Мы обсуждали:

«Корректна ли эта граница владения?»

Вместо:

«Будет ли здесь гонка?»

Задавались вопросом:

«Должны ли эти данные вообще быть изменяемыми?»

Обсуждения архитектуры сместились на более ранний этап.

Мы спорили больше до написания кода. Мы переписывали меньше после инцидентов.

Компилятор взял на себя часть когнитивной нагрузки, которая раньше сосредотачивалась в неписаных знаниях команды.

Что мы не стали изменять

Мы не переписывали:

  • инструменты администрирования;
  • трансформации в пайплайнах данных;
  • низкорисковые API без учета состояния.

Rust — не универсальный молоток.

Мы использовали его там, где:

  • конкурентность была сложной;
  • нагрузка на память была постоянной;
  • глубина рассылки усиливала сбои.

В остальных местах скорость разработки значила больше, чем структурные гарантии. Это разделение важно.

Шесть месяцев спустя

Частота инцидентов снизилась на 44%. Усталость от дежурств заметно уменьшилась. Время восстановления (MTTR) улучшилось, потому что сбои стали проще.

Скорость вывода фич? Восстановилась после второго квартала. Не стала выше — просто стабильной.

Мы не стали героями. Мы стали спокойнее.

А спокойствие в продакшен-системах неоценимо.

Что бы я сказал другой команде

Rust вас не спасет, если ваши инциденты в основном связаны с:

  • непониманием схемы данных;
  • ошибками в логике продукта;
  • сбоями внешних зависимостей.

Он может изменить «поверхность» ваших сбоев, если ваши инциденты связаны с:

  • «призраками» конкурентности;
  • утечками памяти;
  • усилением задержек при асимметричной нагрузке.

При этом ожидайте:

  • замедления скорости в первые кварталы;
  • более сложных ревью;
  • меньше коротких путей.

Компромисс здесь не в скорости ради скорости.

Это достижение структурной устойчивости за счет скорости итераций.

Заключение

Rust не сделал нас крутыми программистами.

Он просто сделал некоторые ошибки скучными. А когда система скучная — она работает тихо и не дергает по ночам.

Я не призываю все переписывать. Не каждой команде это нужно.

Но те части системы, которые годами сыпались и заставляли нас тушить пожары, — их переписать стоило. График сбоев теперь не скачет, а ползет ровно.

Правда, шума по этому поводу никто не поднимает.


Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи The CS Engineer: Rust Slowed Development. Production Incidents Fell by 44%.

Предыдущая статьяСовременное руководство по CSS для фронтенд-разработчиков