Как мы переписали матчинг студентов и менторов на Go
Старый матчер на Python падал под нагрузкой каждый набор. Рассказываем, как очередь, горутины и честные метрики свели среднее время ответа ментора с 18 до 4 часов.
Каждый новый набор наш старый матчер на Python падал одинаково: очередь заявок росла быстрее, чем воркеры успевали её разгребать, а менторы получали студентов через сутки вместо нескольких часов. Мы переписали сервис на Go — рассказываем, что изменилось и почему.
Проблема с оригинальным сервисом
Матчинг — это по сути распределение заявок по очереди с учётом загрузки ментора, направления и опыта. На Python это был Celery-воркер с одним общим пулом задач: как только заявок становилось много, воркеры начинали конкурировать за одну и ту же блокировку в базе, и всё вставало.
Блокировка на уровне строки в PostgreSQL держалась дольше, чем ожидалось, из-за долгих транзакций — воркеры буквально стояли в очереди друг за другом.
Что изменили
Мы вынесли очередь в отдельный процесс на Go с явным пулом горутин и каналом заданий вместо разделяемой блокировки:
func (m *Matcher) Run(ctx context.Context) {
for i := 0; i < m.workers; i++ {
go m.worker(ctx)
}
}
func (m *Matcher) worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case req := <-m.queue:
m.assign(req)
}
}
}Каждый воркер обрабатывает заявки независимо, а очередь — простой буферизированный канал. Никаких общих блокировок на уровне строк: назначение ментора теперь атомарная операция внутри одной горутины.
Результат
Среднее время ответа ментора упало с 18 до 4 часов, а p99 — с трёх суток до 22 часов. Самое приятное: сервис ни разу не упал за следующий набор, хотя заявок пришло на 30% больше, чем в прошлый раз.
| Метрика | До | После |
|---|---|---|
| Среднее время | 18 ч | 4 ч |
| p99 | 3 суток | 22 ч |
| Падения за набор | 2–3 | 0 |
Если интересны детали реализации очереди — заходите в блог за следующими разборами, будем публиковать код.