Таможенное оформление
Классификация кодов ТН ВЭД
- Год
- 2024
- Роль
- Бэкенд-инженер, ответственный за прод
- Результаты
- 1 400 деклараций в минуту
Контекст
Таможенный оператор оформляет декларации для импортёров. Классификацию товара по номенклатуре делал человек, сверяясь со справочником ведомства; ошибка означала задержку груза на границе и разбирательство. Бизнес-задача — снять с декларанта поиск кода и оставить ему только подтверждение.
Ограничения
- Внешний реестр падает примерно раз в сутки и не предупреждает об этом
- Справочник обновляется без версионирования и без уведомлений
- Ошибка классификации стоит клиенту задержки груза на границе
Решение
Реестр закрыт circuit breaker'ом, а разбор идёт через воркер-пул с ограниченным параллелизмом: тридцать две горутины подобраны замером, а не на глаз — выше пропускная способность не росла, а p99 начинал плыть.
func (p *Pool) Run(ctx context.Context, in <-chan Declaration) <-chan Result {
out := make(chan Result, p.size)
var wg sync.WaitGroup
for i := 0; i < p.size; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for decl := range in {
res, err := p.classify(ctx, decl)
if err != nil {
p.metrics.Failures.Inc()
res = Result{Declaration: decl, Fallback: true}
}
out <- res
}
}()
}
go func() { wg.Wait(); close(out) }()
return out
}Порог уверенности классификатора я зашил константой. Через квартал стало понятно, что для разных товарных групп он должен быть разным, и константа превратилась в карту порогов — это надо было заложить сразу.
Архитектура
Ключевые решения
Поведение при недоступном реестре
Что выбрал
Circuit breaker и кэш последнего успешного снимка справочника. При обрыве система отвечает данными вчерашней версии и помечает ответ как устаревший.
Что отверг
Ретраи с экспоненциальной задержкой без размыкателя: они превращают часовую недоступность реестра в часовую недоступность нашего API.
Почему
Декларанту нужен ответ сейчас, пусть и с оговоркой. Отказ без данных хуже, чем данные с пометкой «снимок от вчера» — во втором случае человек хотя бы может принять решение сам.
Размер воркер-пула
Что выбрал
Тридцать две горутины — подобрано замером на боевом профиле нагрузки.
Что отверг
Пул по числу ядер или неограниченный запуск горутин на каждую декларацию.
Почему
Выше тридцати двух пропускная способность переставала расти, а p99 начинал плыть: узкое место переезжало в пул соединений к базе. Число из замера, а не из формулы.
Что было сложно
Проблема
Справочник менялся без версионирования: ведомство молча правило позиции, и кэш отдавал устаревшие коды как актуальные.
Решение
Сборщик считает хэш выгрузки и сравнивает с прошлым. Расхождение помечает затронутые позиции и снимает их с кэша точечно, а не сбрасывает справочник целиком.
Проблема
Первый инцидент разбирали по логам: метрик не было, и шесть минут деградации восстанавливали по памяти дежурного.
Решение
Prometheus и дашборд поставил сразу после. Следующая деградация читалась с графика за полминуты.
Результаты
Под NDA
Кейс под NDA: абсолютные значения заменены относительными.
- пропускная способность
- ≈ / мин
- p95
- ≈ ms
- ошибок классификации
- ≈ %
- деградаций за 30 дней
- ≈ · 6 мин