Иногда по долгу службы приходится проводить деструктивные эксперименты. Делаем мы это лишь для того чтобы сделать нашу систему более стабильной и надёжной.

Недавно мы провели такой эксперимент, связанный с проверкой алгоритма балансировки внутри поисковой системы. Суть эксперимента заключалась в проверке того, что алгоритм в состоянии распределить запросы между репликам в соответствии с их текущей пропускной способностью. Если какая-то реплика стала медленней обрабатывать запросы, она должна получать пропорционально меньшее количество запросов.

После того как алгоритм был покрыт тестами и мы убедились что он работает как ожидается, встал вопрос о его проверке в “боевых условиях”. Как следствие, возникла потребность — замедлить работу одной из реплик. С одной стороны достаточно сильно, чтобы проверить, как поведёт себя алгоритм при сильной ассиметрии в пропускной способности между репликами. С другой стороны достаточно “мягко”, чтобы пользователи не испытывали проблем с использованием системы.

Вводные Link to heading

Дано: два экземпляра сервиса со средним временем ответа 10 милисекунд, и утилизацией значительно ниже 50%. То есть, каждый из экземпляров способен обрабатывать весь поток запросов самостоятельно.

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

Вариант в лоб Link to heading

Самый простой способ удвоить время ответа — засечь время обработки запроса, после чего подождать ещё столько же.

public Response processRequest(Request req) {
	Timed<Response> response = measureTime(() -> doProcessRequest(req));
	sleepUninterruptibly(response.getWallTime(), MILLISECONDS);
	return response.get();
}

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

Альтернативный способ Link to heading

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

Если среднее время ответа системы равно 10 милисекундам, достаточно следующего:

public Response processRequest(Request req) {
	Response response = doProcessRequest(req);
	sleepUninterruptibly(10, MILLISECONDS);
	return response;
}

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

Тот факт что такое небольшое замедление каждого запроса приведёт к замедлению системы в целом в два раза следует из свойств математического ожидания (среднее время ответа системы).

$$ 2 \times \operatorname{E}[T] = \operatorname{E}[T + \operatorname{E}[T]] $$

Это тождество говорит нам, что если к случайной величине прибавить её мат. ожидание мы получим случайную величину с удвоенным мат. ожиданием (средним). Что нам и требуется.

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

Выводы Link to heading

При достаточно низком среднем времени ответа системы, вполне возможно замедлить систему и в 3-4 раза без какого-либо негатива со стороны пользователей. “Что за чушь?! Зачем это может вообще понадобиться”, — скажете вы. И будете отчасти правы. Но давайте взглянем на ситуацию с другой стороны.

Задумайтесь… Если возможно замедлить систему (в терминах среднего времени ответа) в несколько раз и пользователи не заметят ничего плохого, то по симметрии верно и обратное. Вы можете снизить среднее время ответа, сделав систему в несколько раз быстрее, а пользователи этого попросту не заметят.

А это уже ставит уже другой, не менее интересный вопрос. Какие оптимизации следует выбирать и на какие метрики смотреть, чтобы наше ощущение ускорения системы не оказалось иллюзией?