Contents

Способ тестирования изменений сервиса через shadow-трафик

Note
Иногда канареечных релизов недостаточно для обеспечения полностью безопасного релиза.

Проблема

Пример из реальной жизни - я работаю над одним из высоко нагруженных компонентов системы, который обрабатывает 1,5-2 млн запросов в минуту (33 k RPS).

Наш сервис уже стал сложным для внесения изменений, а внутренний механизм устарел и не соответствует новым требованиям бизнеса. Он также уже почти не может быть ускорен и имеет технические проблемы. Мы спланировали и разработали новый механизм работы и обработки данных (изменено около 40% кодовой базы), который нужно интегрировать и перенаправить на него трафик. Все старые тесты были пройдены и показали, что все работает хорошо. Несмотря на это, нет уверенности в качестве наших тестов, и не все сценарии были покрыты.

Решение

У нас были разные подходы и идеи для проверки работы на реальных данных. Однако, мы выбрали идею повторного запроса для части трафика. Нам помог shadow-трафик - подход, при котором часть трафика (запросов) обрабатывается повторно другим механизмом и ответы сравниваются.

Как идет запрос

Как выглядит на практике

Мы написали утилиту, в которую можно скормить 2 json ответа и получить:

  • дифф объектов
  • список полей, которые различаются
  • метрика сколько ответов совпало, а сколько различается

Дифф и запрос логировали, а по кажлому полю записывали отдельную метрику. Метрики по совпадению ответов превратили в график, который показывал сходимость механизма (совпадаюющие запросы / общее количество).

Как анализировать трафик

Результаты

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

Из забавного, обнаружили проблемы в старом механизме, который все еще обрабатывает часть трафика. Сходимость старого механизма со старым не была 100% xD

Как добивали сходимость

Проблемы подхода

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

Выводы

В итоге, использование shadow-трафика оказалось эффективным подходом для проверки работы системы на реальных данных. Это помогло нам улучшить качество системы и снизить риски при переводе трафика на новый механизм. С помощью этого способа получилось обнаружить проблемы, которые не нашли тестами и дополнительно покрыли. Мы добивали сходимость до 99.99% чтобы разблокировать раскатку и все получилось.

Реализация

Реализовать на практике достаточно просто, приведу простой пример кода на golang. По комментариям общую логику возможно воспроизвести как угодно. В идеале, реализовать как дополнительную middleware, чтобы не дублировать код во всех обработчиках, но в примере показан базовый сценарий. Процентом трафика также возможно гибко управлять например через свои кастомные переключатели, либо просто релизами. Советую не ставить процент больше 10, т.к это дополнительные 10% запросов на сервис, может сильно вырасти нагрузка.

Логика в обработчике

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import "math/rand"

const (
  // процент трафика на котором запустились 
	trafficPercent = 1
)

func (h *handler) handle(req Request) {
	// получаем первый ответ
	resp1 := h.v1(req)

	// простейший способ отправить часть трафика, если число попадает в интервал, то отправляем
	// дополнительно можно сделать реальный переключатель, чтобы процент увеличить/уменьшить
	if rand.Intn(100) < trafficPercent {
		go func(req Request, resp1 Response) {
			// получаем второй ответ
			resp2 := h.v2(req)
			// запустить сравнение
			h.cmp(req, resp1, resp2)
		}(req, resp1)
	}

	return resp1
}

// сравнение ответов
func (h *handler) cmp(req Request, resp1, resp2 Response) {
	// получаем дифф и поля которые различаются
	// поля возвращаем списком
	diff, filds := lib.compare(resp1, resp2)

	// есть различия
	// логируем запрос + разлчия, чтобы повторить
	// записываем метрику по полям
	if diff != null {
		h.log(req, diff)
		h.metric.Diff.Inc()
		for _, field := range fields {
			h.metric.Diff(field).Inc()
		}
	} else {
		// ответ сопадает
		h.metric.Same.Inc()
	}
}

Презентация

    / [pdf]

Дополнительные источники для изучения

Note

Весь исходный код доступен на github

Видео на youtube