понедельник, 15 июня 2015 г.

[prog.actors] Ув.тов.netch80 о принципах построения систем на акторах (Erlang)

Чтение форумных разборок -- это поиск редких жемчужин в огромных кучах говна. Но иногда эти поиски приносят уникальные результаты. Вот, например, соображения одного из самых толковых RSDN-неров, netch80, о том, как нужно и как не нужно делать приложения с большим количеством акторов внутри. Тов.netch80 писал об Erlang-е, поэтому в его рассказе так же подчеркиваются достоинства Erlang-а. Но к похожим выводам пришли и мы на основании опыта использования SObjectizer, например, в плане количества ждущих обработке сообщений и соотношения количества текущих активностей к количеству доступных ядер. Утащу текст целиком к себе, дабы потом было проще искать:

В общем, можно сформулировать следующие принципы. Сначала с "так не делайте":

1. Если дизайн предполагает штатной ситуацией наличие больше 10 (условно) сообщений во входной очереди процесса, архитектура построена неправильно. Все асинхронные (то есть следующее шлётся не дожидаясь ответа на предыдущее) потоки должны быть перенесены внутри ноды в специальные драйвера, которые поставляют процессу данные по методу, аналогичному {active,false|once|N} для gen_tcp. Все асинхронные потоки между нодами в кластере должны идти вне штатных межнодовых коннекторов.

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

Дидактически отличный пример, где это реализуется само без участия программиста — отдача по tcp; пока драйвер не сложит порцию в сокетный буфер, gen_tcp:send не получит подтверждения, то есть OTP здесь принудительно обеспечивает синхронность. Именно поэтому Лапшин почти не боролся с тем, что нам убивало системы.

2. Если долгоживущих процессов заметно больше количества ядер, архитектура построена неправильно. Допускается постоянно живущих в объёме M*Ncores, где M — достаточно малая константа. Допускается огромное количество (грубо говоря, миллионы) процессов, но чтобы каждый из них жил одну операцию (самое крупное — ответ на http запрос, или транзакция), не постоянно, и чем короче — тем лучше (нижняя граница срока жизни — определяется моментом, когда переброс данных между такими эфемерами становится слишком дорогим). При требовании долгоживущего состояния — делать пулы рабочих процессов, каждый из которых держит комплект состояний.

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

3. Если действия, неэффективные для рантайма с динамическими типами и постоянной их проверкой (грубо говоря, математика). Выносить в нативный код или пользоваться новомодными фишками типа транслятора через LLVM (cloudozer сейчас такое творит).

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

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

1. Разделение активности по процессам, которые видны рантайму и поэтому контролируемы. Для каждого процесса можно узнать подробно, чем занимается (получить stacktrace, который не будет съеден оптимизациями; получить словарь процесса, куда часто экспортируется статистика и прочие подробности состояния; узнать объём очереди сообщений; и т.п.)

2. За счёт обмена сообщениями вместо блокируемого состояния — нет проблемы от грубого вмешательства, такого, как посылка фиктивного ответа; убийство процесса-исполнителя целиком (для синхронного запроса на манер gen_call, за счёт монитора таргета со стороны клиента, это немедленно приводит к ответу). Средства контроля и такого защитного управления могут быть как автоматическими, так и ручными, без необходимости глубинного знания потрохов. Там, где в C++/Java/etc. ты не можешь подобраться так, чтобы снять мьютекс, исправив данные, тем самым разрулив заклин на ходу — тут всё это из коробки.

3. Опять же, из коробки кластеризация позволяет, даже не распределяя реальную работу, забраться в живую систему (имея права) через remote shell и починить на ходу. Для автоматов вместо remote shell используется штатный RPC. Заведение этих средств в работу стоит единицы минут времени.

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

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