День, Когда Додо Остановился. Синхронный Сценарий

Dodo IS — глобальная система, которая помогает эффективно управлять бизнесом в «Додо Пицца».

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

Последнее для нас самое худшее.

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

Но теперь мы спим лучше.

Мы научились распознавать сценарии системного апокалипсиса и обрабатывать их.

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



День, когда Додо остановился.
</p><p>
 Синхронный сценарий

Серия статей о крахе системы Dodo IS* : 1. День, когда Додо остановился.

Синхронный сценарий.

2. День, когда Додо остановился.

Асинхронный скрипт. * Материалы написаны на основе мое выступление на DotNext 2018 в Москве .



Додо IS

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

Это ERP, HRM и CRM, все в одном.

Система появилась через пару месяцев после открытия первой пиццерии.

Его используют менеджеры, клиенты, кассиры, повара, тайные покупатели, сотрудники колл-центров – все.

Условно Dodo IS делится на две части.

Первый предназначен для клиентов.

Это включает в себя веб-сайт, мобильное приложение и контакт-центр.

Второй — для партнеров-франчайзи, помогает управлять пиццериями.

В системе обрабатываются счета от поставщиков, управление персоналом, перевод людей на смены, автоматический расчет заработной платы, онлайн-обучение персонала, аттестация менеджеров, система контроля качества и тайные покупатели.



Производительность системы

Производительность системы Dodo IS = Надежность = Отказоустойчивость/Восстановление.

Давайте более подробно рассмотрим каждый из пунктов.



Надежность

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

Количество клиентов не сильно варьируется.

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

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



Отказоустойчивость

Один компонент может зависеть от другого компонента.

Если одна система выйдет из строя, другая подсистема не должна выйти из строя.



Устойчивость

Отказы отдельных компонентов происходят каждый день.

Это отлично.

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



Сценарий синхронного отказа системы



Что это?

Инстинкт крупного бизнеса — обслуживать множество клиентов одновременно.

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

Существует принципиальная разница между выполнением алгоритма в единственном экземпляре и выполнением того же алгоритма сервером в составе службы массового обслуживания.

Посмотрите на картинку ниже.

Слева мы видим, как происходят запросы между двумя сервисами.

Это вызовы RPC. Следующий запрос завершается после предыдущего.

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

Для обслуживания большого количества заказов нам нужен правильный вариант:

День, когда Додо остановился.
</p><p>
 Синхронный сценарий

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

Это само по себе может привести к неудаче.

Упрощенную вытесняющую многозадачность можно проиллюстрировать следующим образом:

День, когда Додо остановился.
</p><p>
 Синхронный сценарий

Цветные блоки — это фактическая работа, которую выполняет ЦП, и мы видим, что полезная работа, обозначенная на диаграмме зеленым цветом, довольно мала по сравнению с общим фоном.

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

Такой сон/пробуждение происходит во время синхронизации на любых примитивах синхронизации.

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

Насколько вытесняющая многозадачность может повлиять на эффективность? Посмотрим на результаты синтетического теста:

День, когда Додо остановился.
</p><p>
 Синхронный сценарий

Если интервал потоков между синхронизациями составляет около 1000 наносекунд, эффективность довольно мала, даже если количество потоков равно количеству ядер.

В этом случае КПД составляет около 25%.

Если количество потоков в 4 раза больше, эффективность резко падает — до 0,5%.

Подумайте, вы заказали виртуальную машину в облаке с 72 ядрами.

Это стоит денег, а вы используете меньше половины одного ядра.

Именно это и может произойти в многопоточном приложении.

Если задач меньше, но их продолжительность больше, эффективность возрастает. Видим, что при 5000 операций в секунду в обоих случаях эффективность составляет 80-90%.

Это очень хорошо для многопроцессорной системы.



День, когда Додо остановился.
</p><p>
 Синхронный сценарий

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



Что происходит?

Обратите внимание на результат нагрузочного тестирования.

В данном случае это был так называемый «тест на экструзию».



День, когда Додо остановился.
</p><p>
 Синхронный сценарий

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

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

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

Именно это и произошло бы в реальной жизни, например, при обслуживании в ресторане, переполненном посетителями.

Но происходит нечто другое.

Клиенты сделали больше заказов, а система стала обслуживать меньше.

Система стала обслуживать так мало заказов, что ее можно считать полным провалом, поломкой.

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

Запросы, поступившие раньше, обслуживаются значительно позже.



День, когда Додо остановился.
</p><p>
 Синхронный сценарий

Почему приложение останавливается? Был алгоритм, он сработал.

Запускаем с локальной машины, работает очень быстро.

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

Получается, что запросы от разных клиентов конфликтуют друг с другом.

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

Отдельные запросы конкурируют за ресурсы.



Способы найти проблему

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

Существует еще целый класс проблем сетевого взаимодействия, но мы пока ограничимся этими тремя; этого достаточно, чтобы научиться распознавать подобные проблемы, а нас в первую очередь интересуют проблемы, порождающие Раздор – борьбу за ресурсы.



Блокировки в процессе

Вот типичный запрос в блокирующем приложении.



День, когда Додо остановился.
</p><p>
 Синхронный сценарий

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

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

Затем делается еще один запрос.

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

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

Мы работаем с использованием транзакций для обеспечения единообразия; два запроса имеют конфликт по ключу общего объекта.

Давайте посмотрим, как это масштабируется.



День, когда Додо остановился.
</p><p>
 Синхронный сценарий

Большую часть времени тред спит. Он, по сути, ничего не делает. У нас есть блокировка, которая мешает другим процессам.

Самое обидное, что наименее полезная операция в транзакции, блокирующей ключ, происходит в самом начале.

Это удлиняет объем транзакции во времени.

Вот как мы будем с этим бороться.

   

var fallback = FallbackPolicy<OptionalData> .

Handle<OperationCancelledException>() .

FallbackAsync<OptionalData>(OptionalData.Default); var optionalDataTask = fallback .

ExecuteAsync(async () => await CalculateOptionalDataAsync()); //… var required = await CalculateRequiredData(); var optional = await optionalDataTask; var price = CalculatePriceAsync(optional, required);

Это конечная согласованность.

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

Для этого нам нужно по-другому работать с кодом.

Надо признать, что данные разного качества.

Не будем рассматривать, что было раньше – менеджер что-то изменил в меню или клиент нажал кнопку «оформить заказ».

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

А для бизнеса разницы нет. Поскольку разницы нет, мы можем сделать что-то вроде этого.

Назовем это необязательными данными.

То есть некий смысл, без которого мы можем обойтись.

У нас есть запасной вариант — значение, которое мы берем из кеша или передаем какое-то значение по умолчанию.

А для самой важной операции (нужной переменной) сделаем await. Будем строго его ждать, и только потом будем ждать ответа на запросы дополнительных данных.

Это позволит нам ускорить нашу работу.

Есть еще один существенный момент – данная операция по каким-то причинам может вообще не выполниться.

Предположим, что код этой операции не оптимален и на данный момент есть ошибка.

Если операцию завершить не удалось, делаем откат. И дальше работаем с этим как с обычным значением.



Блокировки БД

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



День, когда Додо остановился.
</p><p>
 Синхронный сценарий

Здесь важно не то, что запрос стал быстрее по времени.

Важно то, что у нас нет Разногласий.

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



День, когда Додо остановился.
</p><p>
 Синхронный сценарий

Это запрос на блокировку.

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

Правый корпус может работать в таком режиме бесконечно долго.

Левый приведет к сбою сервера.



Синхронизировать ввод-вывод

Иногда нам нужны файлы журналов.

Удивительно, но система логирования может выдавать столь неприятные сбои.

Задержка на диске в Azure составляет 5 миллисекунд. Если писать файл подряд, это всего 200 запросов в секунду.

Всё, приложение остановлено.



День, когда Додо остановился.
</p><p>
 Синхронный сценарий

Просто волосы встают дыбом, когда видишь это - в приложении размножилось более 2000 Тем.

78% всех потоков представляют собой один и тот же стек вызовов.

Они остановились на том же месте и пытаются войти в монитор.

Этот монитор ограничивает доступ к файлу, куда мы все записываем.

Конечно, это нужно исключить.



День, когда Додо остановился.
</p><p>
 Синхронный сценарий

Вот что вам нужно сделать в NLog, чтобы настроить его.

Создаем асинхронную цель и пишем в нее.

А асинхронная цель записывает в реальный файл.

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

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



Все очень плохо

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

Источники разногласий должны быть идентифицированы и устранены.

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

Мне пришлось переписать большую часть наследия с блокировки вызовов на асинхронность; Я сам часто был инициатором такой модернизации.

Довольно часто кто-то подходит и спрашивает: «Слушай, мы уже две недели переписываем, почти все асинхронно.

Насколько быстрее это будет работатьЭ» Ребята, я вас разочарую - быстрее не получится.

Он станет еще медленнее.

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

В одном из наших проектов — примерно +5% к загрузке ЦП и GC. Есть и еще плохие новости — приложение может работать значительно хуже, если просто переписать его на асинхронный режим без понимания специфики параллельной модели.

Об этих особенностях я расскажу очень подробно в следующей статье.

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

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

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

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

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

Теги: #программирование #Высокая производительность #Распределенные системы #нагрузочное тестирование #dodo is #dodo Pizza #многопоточность #многопоточное программирование #инжиниринг dodo Pizza #синхронный скрипт

Вместе с данным постом часто просматривают: