четверг, 25 декабря 2014 г.

[prog.thoughts] В каких ситуациях полезны дедлайны доставки сообщений?

Думаю, что вопрос о том, в каких ситуациях полезно ограничивать время доставки сообщения от одного агента к другому, интересен не только пользователям конкретных фреймворков (будь то Akka, CAF или SObjectizer). Но и вообще разработчикам, которые используют механизм message-passing для разработки своих приложений. А этот механизм весьма актуален, как для утилизации современных многоядерных процессоров, так и для разработки распределенных приложений, компоненты которых располагаются на разных узлах сети, но должны общаться между собой.

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

Итак, сходу в памяти всплывает две ситуации, когда было бы желательно ограничивать время жизни сообщения m, отосланного агентом S агенту R.


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

Агент S не сам общается с биллингом, а использует некоторого рабочего агента R. Агент S отсылает сообщение-запрос агенту R и ждет ответа. Время ожидания должно быть ограничено, т.к. эта операция может быть частью онлайн-транзакции и, возможно, в настоящий момент кто-то ждет результата в Интернет-магазине или у банкомата. Если за отведенное время, скажем, за 5 секунд, агент S не получает от R ответа, то он должен отдать внешней системе отрицательный ответ со специальным кодом возврата.

Поведение агента S понятно. А вот что с агентом R? Если он получил m, сделал запрос в биллинг и ждал ответа от биллинга слишком долго, то это понятно. Ответ от биллинга будет получен, но он уже никого не будет интересовать. Поэтому будет выброшен.

Но что, если биллинг стал драматически притормаживать и тратить на каждую проверку не 1-2 секунды, а 3-4-5-... секунд? Такое, кстати говоря, встречается на практике и не так уж редко :( Если в очереди к R стоят несколько сообщений m (m1,...,mK), то только небольшая часть из них будет обработана биллингом. Все остальные просто "протухнут" еще до того, как R извлечет их из очереди.

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

Соответственно, для каждого сообщения m нужно устанавливать дедлайн. Если до дедлайна сообщение к агенту R не попало, то смысла в доставке больше нет и сообщение можно выбросить, ничего не передавая агенту R.

Вот получился и первый сценарий для дедлайнов: агент S при отсылке m устанавливает дедлайн доставки, если доставка до дедлайна не произошла, то сообщение m из очереди R выбрасывается и агент R об этом даже ничего не узнает.

Существует еще один вариант этого сценария. Когда агент S должен повторять сообщение m для агента R до тех пор, пока R не ответит что-нибудь внятное. Например, агент S получил прикладное сообщение (например, email), который нужно передать получателю, а R -- это промежуточное звено, отвечающее за дальнейшую доставку сообщения (вроде MTA, mail transfer agent). S шлет сообщение R и ждет какое-то время подтверждения. Если не дожидается, то повторяет доставку. И так до тех пор, пока R не ответит или пока не закончится отведенное для доставки время (или попытки доставки).

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

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


Ситуация вторая. Периодически обновляемая (как правило измерительная) информация.

Представьте себе датчик температуры/влажности/освещенности/давления и т.д., который опрашивается агентом S с заданным темпом и полученная информация отсылается агенту R (для сохранения/ретрансляции/обработки или чего-то другого). Если опросы идут достаточно часто, а агент R по какой-то причине притормозил и в его очереди скопилось несколько замеров от одного источника, то в ряде случаев агенту R старые замеры просто не нужны. Например, если агент R не пишет данные в БД для построения исторических трендов, а просто контролирует пороги и как-то реагирует на их нарушение, то такого агента будут интересовать только текущие показания, а отдаленное прошлое уже не имеет значения.

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

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


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

Другое дело, на каком именно уровне разработчик будет вынужден этим заниматься. Наличие каких-то базовых элементов overload control в фреймворке упрощает работу программиста, позволяет избежать необходимости изготавливать свои собственные низкоуровневые велосипеды и более плотно заниматься своей прикладной задачей.

Отсюда и интерес к добавлению дедлайнов для сообщений в SObjectizer. Это не панацея от всех болезней и не универсальный механизм overload control-я на все случаи жизни. Но в качестве одного из базовых блоков... Почему бы и нет?

PS. Кстати говоря, успешность эксплуатации в течении более 10 лет пары больших, missionbusiness-critical, прикладных систем, разработанных на SObjectizer-е в "Интервэйле", во многом была обусловлена как раз наличием в них большого количества источников мониторинговой информации и инструментами для ее получения и визуализации.

Комментариев нет: