Как повысить производительность: целесообразность тайм-аутов

1656666258 kak povysit proizvoditelnost czelesoobraznost tajm autov

Алекс Надолин

1*A6SFwHAQlwml4ff8HOA2jQ

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

Даже если вы действительно не используете микросервисы, есть вероятность, что вы уже зависите от внешних сервисов, таких как elasticsearch, Redis или платежный шлюз, и требуете интеграции с ними через какой-то API.

Что произойдет, если эти службы медленны или недоступны? Что ж, вы не можете обрабатывать поисковые запросы или платежи, но ваша программа будет все равно работать «хорошо» — да?

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

Наш случай

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

0*WvZHW7KYooyqCCRC

Наши клиенты будут подключаться к нашему главному интерфейсу, server1.js который затем сделает HTTP-запрос в другую службу, server2.js который ответит в ответ. Как только мы получим ответ от server2.js затем мы можем вернуть тело ответа нашему клиенту.

Несколько вещей, на которые следует обратить внимание:

  • Серверы работают на порту 3000 (основное приложение) и 3001 («бэкенд» сервер). Итак, как только клиент делает запрос к localhost:3000 будет отправлен новый запрос HTTP localhost:3001
  • Сервисная служба будет ждать 100 мс (это для имитации реальных случаев использования), прежде чем вернуть ответ
  • Я использую HTTP клиент unirest. Мне это очень нравится, хотя мы могли бы просто использовать встроенный http модуль, я уверен, что это даст нам лучшее ощущение с точки зрения реальных программ
  • Unirest любезно сообщит нам, если в нашем запросе есть ошибка, чтобы мы могли просто проверить response.error и решайте драму оттуда
  • Я собираюсь использовать докер для запуска этих тестов, а код доступен на GitHub.

Давайте проведем наши первые тесты

Давайте запустим наши серверы и начнем бомбить server1.js с просьбами. Мы будем использовать siege (я слишком хипстер для AB), который предоставляет полезную информацию после выполнения погрузочного теста:

siege -c 5 www.google.com** SIEGE 3.0.5** Preparing 5 concurrent users for battle.The server is now under siege...^CLifting the server siege...      done.
Transactions:                26 hitsAvailability:            100.00 %Elapsed time:              6.78 secsData transferred:          0.20 MBResponse time:             0.52 secsTransaction rate:          3.83 trans/secThroughput:                0.03 MB/secConcurrency:               2.01Successful transactions:          27Failed transactions:              0Longest transaction:           1.28Shortest transaction:          0.36

The -c параметр, в осаде, определяет, сколько одновременных запросов мы должны отправить на сервер, и вы даже можете указать, сколько повторений (-r) вы бы хотели бежать. Например, -c 10 -r 5 означает, что мы будем отправлять на сервер 50 запросов в группах по 10 одновременных запросов. Для целей нашего сравнительного теста я решил продлить тестирование в течение трех минут и проанализировать результаты после этого, не устанавливая максимальное количество повторений.

Кроме того, в следующих примерах я сокращу результаты до важнейших элементов, предоставленных siege:

  • Доступность: сколько наших запросов сервер мог обработать
  • Скорость транзакций: сколько запросов в секунду мы смогли сделать?
  • Успешные/неудачные транзакции: сколько запросов завершились успешными/неудачными кодами статуса (т.е. 2xx против 5xx)?

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

docker run --net host -v $(pwd):/src -d mhart/alpine-node:7.1 node /src/server1.jsdocker run --net host -v $(pwd):/src -d mhart/alpine-node:7.1 node /src/server2.js
siege -c 500 127.0.0.1:3000

Приблизительно через три минуты пора прекратить осаду (ctrl+c) и посмотрите, как выглядят результаты:

Availability:             100.00 %Transaction rate:       1156.89 trans/secSuccessful transactions:      205382Failed transactions:              0

Неплохо, поскольку мы смогли обслуживать 1156 транзакций в секунду. Даже лучше того, кажется, что у нас нет никакой ошибки, а это значит, что наш уровень успеха составляет 100%. Что, если бы мы улучшили нашу игру и перешли к 1 тысяче одновременных транзакций?

siege -c 1000 127.0.0.1:3000...
Availability:            100.00 %Transaction rate:       1283.61 trans/secSuccessful transactions:      232141Failed transactions:              0

молодец! Мы немного увеличили пропускную способность, поскольку теперь наше приложение может обрабатывать 1283 запроса в секунду. Поскольку программы делают очень мало (напечатайте строку и все), вероятно, чем больше одновременных запросов мы пришлем, тем выше пропускная способность.

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

Знакомство с провалом

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

К примеру, допустим, что наш бэкенд-сервис переживает тяжелую фазу и время от времени начинает отставать:

В этом примере 1 из 10 запросов будет обслуживаться по истечении времени ожидания в 10 с, в то время как остальные будут обработаны со «стандартной» задержкой в ​​100 мс. Этот вид имитирует сценарий, когда у нас есть несколько серверов по балансировщику нагрузки, и один из них начинает выдавать случайные ошибки или становится медленнее из-за чрезмерной нагрузки.

Давайте вернемся к нашему тесту и посмотрим, как наши server1.js выполняет сейчас, когда его зависимость начнет замедляться:

siege -c 1000 127.0.0.1:3000
Availability:            100.00 %Transaction rate:        853.93 trans/secSuccessful transactions:      154374Failed transactions:              0

Какой облом: наш уровень транзакций резко упал более чем на 30% только потому, что некоторые ответы задерживаются. Это означает, что server1.js нужно задержаться дольше, чтобы получить ответы от server2.jsтаким образом, используя больше ресурсов и имея возможность обслуживать меньше запросов, чем теоретически.

Ошибка сейчас лучше, чем ответ завтра

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

Через 1-2 секунды их внимание погаснет, и вероятность того, что они все еще будут привязаны к вашему содержимому, исчезнет, ​​как только вы пересечете порог 4/5 с. Это означает, что в общем лучше предоставить им немедленный отклик, даже если он отрицательный («»Произошла ошибка, попробуйте еще раз»), а не позволять им разочаровываться из-за того, насколько медленно работает ваше обслуживание.

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

...
require('unirest').get(' {
...

Давайте посмотрим, как выглядят числа с включенными тайм-аутами:

Availability:              90.14 %Transaction rate:       1125.26 trans/secSuccessful transactions:      209861Failed transactions:          22964

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

Конечно, одним из недостатков является то, что сейчас мы увеличили количество неудач (10% от общего количества запросов), а это означает, что некоторым пользователям будет представлена ​​страница ошибки. Однако это все равно лучше, чем обслуживать их после 10 секунд, поскольку большинство из них все равно отказались бы от нашей службы.

Теперь мы увидели, что в идеале, тайм-ауты помогают сохранить почти идеальные обороты (запросов в секунду), а как насчет потребления ресурсов? Смогут ли они лучше убедиться, что наши серверы не потребуют дополнительных ресурсов, если одна из их зависимостей станет менее отзывчивой?

Давайте разберемся.

Фактор оперативной памяти

Чтобы понять, сколько памяти у нас server1.js потребляет, нам нужно периодически измерять объем памяти, используемый сервером. В производстве мы использовали бы такие инструменты, как NewRelic или KeyMetrics, но для наших простых сценариев мы прибегнем к версии таких инструментов для бедняков. Мы будем печатать объем памяти с server1.js и мы используем другой сценарий, чтобы записать результат и распечатать простую статистику.

Давайте убедимся server1.js печатает количество использованной памяти каждые 100 мс:

...
setInterval(_ => {  console.log(process.memoryUsage().heapUsed / 1000000)}, 100)
...

Если мы запустим сервер, мы увидим что-то вроде:

3.9901764.0667524.0760244.0777844.0795444.0813044.0830644.084824

это объем памяти в МБ, используемой сервером. Чтобы сократить цифры, я написал простой сценарий, читающий входные данные из stdin и вычисляет статистику:

Модуль является общедоступным и доступным на NPM, поэтому мы можем просто установить его глобально и перенаправить исходные данные сервера на него:

docker run --net host -v $(pwd):/src -ti mhart/alpine-node:7.1 shnpm install -g number-aggregator-statsnode /src/server1.js | number-aggregator-stats
Meas: 18 Min: 3 Max: 4 Avg: 4 Cur: 4

Давайте снова запустим наш тест – 3 минуты, 1 тысяча одновременных запросов, без тайм-аутов:

node /src/server1.js | number-aggregator-stats
Meas: 1745 Min: 3 Max: 349 Avg: 194 Cur: 232

А теперь включите тайм-аут 3 с:

node /src/server1.js | number-aggregator-stats
Meas: 1429 Min: 3 Max: 411 Avg: 205 Cur: 172

Ого, на первый взгляд, так кажется в конце концов, тайм-ауты не помогают: наше использование памяти достигает высокого уровня, когда включены тайм-ауты, и в среднем также на 5% больше. Есть ли этому какое-нибудь разумное объяснение?

Есть, конечно, поскольку нам просто нужно вернуться к Siege и посмотреть на rps:

853.60 trans/sec --> without timeouts1134.48 trans/sec --> with timeouts

Вот мы сравниваем яблоки к апельсинам. Бесполезно смотреть на использование памяти двумя серверами, обслуживающими разное количество rps. В то же время мы должны убедиться, что они оба предлагают одинаковую пропускную способность, и измерять память только в этот момент. Иначе сервер, обслуживающий большее количество запросов, всегда будет запускаться с определенным недостатком!

Для этого нам нужен какой-нибудь инструмент, который облегчит генерацию загрузок на основе rps, а siege для этого не очень подходит. Пора назвать нашего друга vegeta, современный инструмент тестирования нагрузки, написанный на языке Golang.

Введите вегета

0*EUTGjti8owj7vE50

Vegeta очень проста в использовании, просто начните «атаковать» сервер и позвольте ему сообщить о результатах:

echo "GET  | vegeta attack --duration 1h -rate 1000 | tee results.bin | vegeta report

Здесь два очень интересных варианта:

  • --durationтак что вегета прекратится через определенное время
  • --rateкак у rps

Похоже, вегета – правильный инструмент для нас – тогда мы можем предоставить команду, адаптированную к нашему серверу, и увидеть результаты:

echo "GET  | vegeta attack --duration 3m --insecure -rate 1000 | tee results.bin | vegeta report

Вот что vegeta выводит без тайм-аутов:

Requests      [total, rate]            180000, 1000.01Duration      [total, attack, wait]    3m10.062132905s, 2m59.998999675s, 10.06313323sLatencies     [mean, 50, 95, 99, max]  1.172619756s, 170.947889ms, 10.062145485s, 10.134037994s, 10.766903205sBytes In      [total, mean]            1080000, 6.00Bytes Out     [total, mean]            0, 0.00Success       [ratio]                  100.00%Status Codes  [code:count]             200:180000Error Set:

и это то, что мы получаем, когда server1.js имеет тайм-аут 3 секунды:

Requests      [total, rate]            180000, 1000.01Duration      [total, attack, wait]    3m3.028009507s, 2m59.998999479s, 3.029010028sLatencies     [mean, 50, 95, 99, max]  455.780741ms, 162.876833ms, 3.047947339s, 3.070030628s, 3.669993753sBytes In      [total, mean]            1142472, 6.35Bytes Out     [total, mean]            0, 0.00Success       [ratio]                  90.00%Status Codes  [code:count]             500:18000  200:162000Error Set:500 Internal Server Error

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

Без тайм-аутов:

node /src/server1.js | number-aggregator-stats
Meas: 1818 Min: 3 Max: 372 Avg: 212 Cur: 274

и с тайм-аутами:

node /src/server1.js | number-aggregator-stats
Meas: 1886 Min: 3 Max: 299 Avg: 149 Cur: 292

Это больше похоже на это: тайм-ауты помогли нам сохранить использование памяти в среднем на 30% меньше.

Все это благодаря простому .timeout(3000) . Какая победа!

Избегание эффекта домино

Цитирую себя:

Что произойдет, если эти службы медленны или недоступны? Что ж, вы не можете обрабатывать поисковые запросы или платежи, но ваша программа будет все равно работать «хорошо» — да?

Смешной факт: отсутствие тайм-аута может искалечить всю вашу инфраструктуру!

0*5ewVUhjYhEJnJ4VR

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

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

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

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

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

Примечание по тайм-аутам

Если вы думали, что ждать опасно, давайте добавим в огонь:

  • Мы не говорим только о HTTP — каждый раз, когда мы полагаемся на внешнюю систему, мы должны использовать тайм-ауты.
  • Сервер может иметь открытый порт и сбрасывать каждый пакет, который вы отправляете, что приведет к тайм-ауту соединения TCP. Попробуйте это в своем терминале: time curl example.com:81. Удачи!
  • Сервер мог ответить мгновенно, но отправлять каждый пакет очень медленно (например, секунды между пакетами). Тогда вам нужно будет оградить себя от a время ожидания чтения.

…и многие другие крайние случаи, чтобы перечислить. Я знаю, что распределенные системы – это плохо.

К счастью, API высокого уровня (например, открывшего unirest) в целом полезны, поскольку они позаботятся обо всех проблемах, которые могут возникнуть на пути.

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

Что нужно сделать, если вы серьезно относитесь к тестам:

  • Не запускайте сравнимый код и инструмент, используемый для сравнения, на одной машине. Здесь я запустил все на своем XPS, который достаточно мощный, чтобы позволить мне провести эти тесты. Но запуск siege/vegeta на той же машине, на которой работают серверы, безусловно, влияет на результаты (я говорю ulimit а остальное вы поймете). Мой совет состоит в том, чтобы попытаться получить некоторое оборудование на AWS и сравнить оттуда – больше изоляции, меньше сомнений.
  • Не измеряйте память, выходя из нее с помощью a console.logвместо этого используйте такой инструмент, как NewRelic, который, по моему мнению, менее инвазивный.
  • Измеряйте больше данных: трехминутное сравнение нормально для этой публикации, но если мы хотим взглянуть на данные реального мира, чтобы лучше оценить, насколько полезны тайм-ауты, вам следует оставить тесты работать гораздо дольше.
  • Держите Gmail закрытым во время бега siege ...квартиранты, проживающие в /proc/cpuinfo будет благодарен.

И… Я закончил на сегодняшний день: я надеюсь, вам понравился этот пост, а если в противном случае, не стесняйтесь напишите в поле комментариев ниже!

Первоначально опубликовано на odino.org (19 января 2017).

Добавить комментарий

Ваш адрес email не будет опубликован.