Хабрахабр

Elixir как цель развития для python async

В книге «Python. К вершинам мастерства» Лучано Рамальо описывает одну историю. В 2000 году Лучано проходил курсы, и однажды в аудиторию заглянул Гвидо ван Россум. Раз подвернулся такой случай, все стали задавать ему вопросы. На вопрос о том, какие функции Python заимствовал из других языков, Гвидо ответил: «Все, что есть хорошего в Python, украдено из других языков».

Python давно живет в контексте других языков программирования и впитывает концепции из окружения: asyncio позаимствован, благодаря Lisp появились лямбда-выражения, а Tornado скопировали с libevent. Это действительно так. Он создан 30 лет назад, и все концепции в Python, которые сейчас реализуются или только намечаются, в Erlang давно работают: многоядерность, сообщения как основа коммуникации, вызовы методов и интроспекция внутри живой системы на продакшн. Но если у кого и стоит заимствовать идеи, так это у Erlang. Эти идеи в том или в ином виде находят своё проявление в системах вроде Seastar.io.

Если не брать во внимание Data Science, в котором Python сейчас вне конкуренции, то все остальное уже реализовано в Erlang: работа с сетью, обработка HTTP и веб-сокетов, работа с базами данных. Поэтому Python-разработчикам важно понимать, куда будет двигаться язык: по дороге, которую уже прошли 30 лет назад.

Чтобы разобраться в истории развития других языков и понять, куда двигается прогресс, мы пригласили на Moscow Python Conf++ Максима Лапшина (erlyvideo) — автора проекта Erlyvideo.ru.

Отдельное внимание уделим тому, как на разных языках программирования и платформах управлять сокетами, потоками исполнения и данными.
Под катом текстовая версия этого доклада, а именно: в каком направлении вынуждена развиваться система, которая продолжает мигрировать от простого линейного кода к libevent и дальше, что общего и в чем отличия между Elixir и Python.

У Erlyvideo.ru есть система видеонаблюдения, в которой управление доступом к камерам написано на Python. Это классическая задача для этого языка. Есть пользователи и камеры, видео с которых они могут смотреть: кто-то видит одни камеры, кто-то другие — обычный сайт.

Разрабатываемый софт пакуется и продается пользователям. Python был выбран, потому что на нём удобно писать такой сервис: есть фреймворки, ORM, программисты, в конце концов. Erlyvideo.ru та компания, которая продает софт, а не только дает сервис.

Какие проблемы с Python хочется решить.

Но у Python с этим сложности: почему он до сих пор не использует все 80 ядер наших серверов для работы? Почему такие проблемы с многоядерностью? Мы запускали Flussonic на стоядерных компьютерах еще до того, как это делал Intel.

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

Есть ли решение у забытых глобальных переменных? Утечка глобальных переменных — это ад для любого языка со сборкой мусора, как то Java или C#.

Как использовать железо, не сжирая впустую ресурсы? Как обойтись без запуска 40 джанговских воркеров и 64 Гбайт RAM, если мы хотим использовать серверы эффективно, а не выбрасывать сотни тысяч долларов в месяц на ненужное железо?

Зачем нужна многоядерность

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

Это мы еще пишем на Python, а не на Ruby on Rails, который может потреблять в несколько раз больше и 40 Гбайт RAM легко и непринужденно вылетят впустую. Один воркер может потреблять 300-400 Мбайт. Это не сильно дорого, но зачем покупать память там, где можно не покупать.

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

Управление сокетами

По веб-сокету опрашиваем runtime-данные видеокамер с бэкенда. Софт на Python подсоединяется к Flussonic и опрашивает данные состояния видеокамер: работают или нет, есть ли новые события.

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

Запустили reload, что-то сделали, опять эта проблема — остались два сокета. Но, например, произошла какая-то проблема: база данных не ответила на запрос, весь код упал, осталось два открытых сокета. Через какое-то время это приводит к утечкам сокетов. Неправильно обработали ошибку БД и повисло два открытых соединения.

Забытые глобальные переменные

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

Сработало исключение, забыли удалить ссылку и данные повисли. Например, записали в dict ссылку на подключение, чтобы рассылать данные. Это не решение, потому что все равно данные будут утекать.
Так через какое-то время начинает не хватать уже и 64 Гбайт, и хочется удвоить память на сервере.

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

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

Исторический экскурс

Чтобы подойти к основной теме, углубимся в историю. Все, о чем мы сейчас говорим о Python, Go и Erlang, — весь этот путь другие люди прошли лет 30 назад. Мы в Python проходим путь и набиваем шишки, которые уже пройдены десятилетия назад. Путь повторяется просто удивительным образом.

DOS

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

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

Кооперативная многозадачность

Поскольку с DOS было совсем больно, появлялись новые вызовы, компьютеры становились мощнее. Десятилетия назад разработали концепцию кооперативной многозадачности, еще до Windows 3.11.

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

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

Вытесняющая многозадачность

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

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

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

Потоки

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

Потоки — это легковесные процессы, которые объединены общей памятью.

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

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

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

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

Примеры на Python

Рассмотрим простой пример «Сервис в помощь покупателю». Он подбирает лучшую цену товара на нескольких площадках: вбиваем название товара и ищем торговые площадки с минимальной ценой.

Он сегодня не очень популярен, мало кто на нем начинает проекты. Это код на старом Django, Python 2.

@api_view(['GET'])
def best_price(request): name = request.GET['name'] price1 = http_fetch_price('market.yandex.ru', name) price2 = http_fetch_price('ebay.com', name) price3 = http_fetch_price('taobao.com', name) return Response(min([price1,price2,price3]))

Приходит запрос, мы идем к одному бэкенду, потом к другому. В местах, где вызывается http_fetch_price, потоки блокируются. В этот момент весь воркер встает на поход к Яндекс.Маркету, потом к eBay, потом до таймаута на Taobao, а в конце выдает ответ. Все это время весь воркер стоит.

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

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

Например, чтобы пойти к dict и ничего не сломать, надо ставить перед ним мьютексы. Если пойти к dict с разных потоков, то доступ к данным можно написать так: склеить в памяти два экземпляра Python, чтобы они пошарили данные — они их просто сломают. Это сложно протаскивать в мультикор. Если перед каждым dict будет мьютекс, тогда система замедлится примерно в 1000 раз — будет просто неудобно.

Фактически, мы переизобрели DOS внутри процесса — скриптовый язык образца 2010 года. У нас есть только один поток исполнения и масштабироваться возможно только процессами. Огромный перерасход ресурсов и медленный ответ никому не нравился. Внутри процесса есть штука, которая напоминает DOS: пока мы что-то делаем, все другие процессы не работают.

Появилась возможность ожидать готовности сразу нескольких сокетов. Какое-то время назад в Python появился реактор сокетов, хотя сама концепция родилась давно.

В том числе благодаря правильному использованию этой технологии, он и стал популярен. Сначала реактор стал востребован на серверах типа nginx. Потом концепция переползла и в скриптовые языки вроде Python и Ruby.

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

Событийно-ориентированное программирование

Один контекст выполнения производит запрос. Пока ждем ответ, выполняется другой контекст. Примечательно, что мы практически прошли тот же этап эволюции, как переход от DOS к Windows 3.11. Только люди это сделали на 20 лет раньше, а в Python и в Ruby это появилось лет 10 назад.

Twisted

Это событийно-ориентированный сетевой фреймворк. Он появился в 2002 году и написан на Python. Я взял пример выше и переписал его на Twisted.

def render_GET(self, request): price1 = deferred_fetch_price('market.yandex.ru', name) price2 = deferred_fetch_price('ebay.com', name) price3 = deferred_fetch_price('taobao.com', name) dl = defer.DeferredList([price1,price2,price3]) def reply(prices): request.write('%d'.format(min(prices))) request.finish() dl.addCallback(reply) return server.NOT_DONE_YET

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

В первой части то, что было до запроса, а во второй то, что после.
Фактически, код состоит из двух частей.

Вся история событийно-ориентированного программирования пропитана болью от разрыва линейного кода на «до запроса» и «после запроса».

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

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

Идея Twisted

Объект может быть активирован при готовности сокета.

Мы берем объекты, в которые собираем необходимые данные от контекста и привязываем их активацию к сокету. Теперь готовность сокетов — один из самых важных элементов управления всей системой. Объекты будут нашими контекстами.

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

За что, например, «любят» Node.js — до недавнего времени не было вообще никаких других способов, а в Python уже все-таки появилось. Все это приводит к классическому callback hell. Беда в том, что есть разрывы кода в точках внешнего IO, которые приводят к callback.

Можно ли «склеить» края разрыва в коде? Вопросов много. Что делать, если логический объект работает с двумя сокетами и один из них закрывается? Можно ли вернуться обратно к нормальному человеческому коду? Можно ли как-то использовать все ядра? Как не забыть закрыть второй?

Async IO

Хороший ответ на эти вопросы — Async IO. Это крутой шаг вперед, хотя и непростой. Async IO сложная штука, под капотом которой много болезненных нюансов.

async def best_price(request): name = request.GET['name'] price1 = async_http_fetch_price('market.yandex.ru', name) price2 = async_http_fetch_price('ebay.com', name) price3 = async_http_fetch_price('taobao.com', name) prices = await asyncio.wait([price1,price2,price3]) return min(prices)

Разрыв кода скрыт под синтаксическим сахаром async/await. Мы взяли, все что было раньше, но не пошли к сети в этом коде. Мы убрали Callback(reply), который был в предыдущем примере и скрыли его за await — местом, где код будет разрезан ножницами. Он будет разделен на две части: вызывающую и callback-часть, которая обрабатывает результаты.

Есть методы для склейки нескольких ожиданий в одно. Это прекрасный синтаксический сахар. В Python до сих пор огромное количество библиотек, которые пойдут к сокету синхронно, сделают timer libraryи все вам испортят. Это классно, но есть нюанс: все можно сломать «классическим» сокетом. Как это отладить, я не знаю.

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

У нас остались все проблемы, о которых мы говорили в начале:

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

Что делать

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

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

Существуют аналоги, это не что-то свежее. Смена парадигмы объектов. Давайте соединим контекст с потоком выполнения. Между Apache pool’s запрещены какие-либо ссылки. Если кто-то пытался править исходники Apache и писать к ним модули, то знает, что там есть Apache pool. Данные от одного Apache pool — пула, связанного с запросами, находятся внутри него, и нельзя оттуда ничего выносить.

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

Необходимы не маленькие монады, которые внутри себя закрыты и никак друг с другом не общаются. Как обмениваться активностью? Один из подходов — это обмен сообщениями. Нам надо, чтобы они общались. В обычной ОС нельзя дать ссылку на память другого процесса, но можно сигнализировать через сеть, как в UNIX, или через сообщения, как в Windows. Это примерно тот путь, по которому пошли в Windows, обмениваясь сообщениями между процессами.

Мы склеили вместе: Все ресурсы внутри процесса и контекст становятся потоком исполнения.

  • runtime-данные в виртуальной машине, в которых возникают исключения;
  • поток исполнения, как то, что исполняется на процессоре;
  • объект, в котором логически собираются все данные.

Поздравляю — мы изобрели UNIX внутри языка программирования! Эту идею придумали примерно в 1969 году. Пока что в Python его еще нет, но Python, скорее всего, к этому придет. А, возможно, и не придет — не знаю.

Что это дает

Прежде всего, автоматический контроль за ресурсами. На Moscow Python Conf++ 2019 рассказывали, что можно на Go написать программу и обработать все ошибки. Программа будет стоять как влитая и работать месяцами. Это действительно так, но мы не обрабатываем все ошибки.

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

Будем честны: только когда нужно что-то обрабатывать, тогда и начинаем обрабатывать. Поэтому мы все пишем «happy path», а дальше на продакшн разберемся. Defensive programming — это чуть-чуть другое, и это не коммерческая разработка.

Но операционные системы его придумали 50 лет назад: если какой-то процесс умирает, то все, что он открыл, закроется автоматически. Поэтому, когда у нас есть автоконтроль за ошибками — это прекрасно. Этого нет уже 50 лет ни в одной ОС, а в Python все еще надо за этим всем внимательно и аккуратно следить руками. Никому сегодня не надо писать код, который будет подчищать файлы за убитым процессом. Это странно.

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

Дальше небольшая помощь от виртуальной машины, от runtime. Реализация asyncio без слов «async/await». Это то, о чем мы говорили с async/await: можно переделать также на сообщения, убрать async/await и получить это на уровне виртуальной машины.

Процессы Erlang

Erlang придумали 30 лет назад. Бородатые ребята, которые тогда были не очень бородатые, посмотрели на UNIX и перенесли все концепции в язык программирования. Они решили, что у них теперь будет своя штука, чтобы спать по ночам и спокойно ездить на рыбалку без компьютера. Тогда еще не было ноутбуков, но бородатые ребята уже догадывались, что об этом нужно думать заранее.

Дальше мой пример на Erlang. Мы получили Erlang (Elixir) — активные контексты, которые выполняются сами. На Elixir он выглядит примерно так же, с некоторыми вариациями.

best_price(Name) -> Price1 = spawn_price_fetcher('market.yandex.ru', Name), Price2 = spawn_price_fetcher('ebay.com', Name), Price3 = spawn_price_fetcher('taobao.com', Name), lists:min(wait4([Price1,Price2,Price3])).

Запускаем несколько fetcher'ов — это несколько отдельных новых контекстов, которые мы ждем. Дождались, собрали данные и результат вернули как минимальную цену. Все это похоже на async/await, только без слов «async/await».

Особенности Elixir

Elixir находится в базе у Erlang, и все концепции языка спокойно переносятся на Elixir. Какие у него особенности?

Упрощенно, если перенести на Python, в Erlang запрещены ссылки на данные внутри другого объекта. Запрет на кросс-процессорные ссылки. Под словом процесс я подразумеваю уже легковесный процесс внутри виртуальной машины — контекст. Нельзя даже синтаксически получить указатель на данные, которые находятся внутри другого объекта. Можно иметь ссылку на весь объект целиком, как на закрытую коробочку, но на данные внутри него ссылаться нельзя. Можно только знать о самом объекте.

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

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

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

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

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

Через сообщения вызываются методы на процессах. Вызовы методов. У нас есть объекты, которые мы называем процессы.

Если что-то нам отвечает медленно, вызываем метод на другом объекте. Вызов методов — это тоже посылка сообщения. Здорово, что теперь его можно сделать с таймаутом. Мне нужно будет пойти и сказать ему «503» — приходи завтра, сейчас тебя не ждут. Но при этом говорим, что готовы ждать не больше 60 с, потому что у меня клиент с таймаутом в 70 с.

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

Как работать с сетью?

Можно писать линейный код, callback’ами или в стиле asyncio.gather. Пример, как это будет выглядеть.

wait4([ ]) -> [ ]; wait4(List) -> receive -> [Price] ++ wait4(List -- [Pid]) after 60000 -> [] end.

В функцииwait4 из предыдущего примера мы перебираем список тех, от кого еще ждем ответы. Если с помощью метода receive получаем сообщение от того процесса — записываем в список. Если список закончился, мы возвращаем все, что было и накапливаем список. Мы попросили одновременно три объекта пригнать нам данные. Если они не справились все вместе за 60 с, и хотя бы один из них не ответил ОК, у нас будет пустой список. Но важно то, что мы сделали общий таймаут на запрос сразу к целой пачке объектов.

Но здесь важно то, что с той стороны может быть не только поход по HTTP, но и поход к БД, а еще какие-то вычисления, например, подсчет какой-то оптимальной циферки для клиента. Кто-то может сказать: «Подумаешь, в libcurlесть все то же самое».

Обработка ошибок

Ошибки перешли из потока в объект, которые теперь одно и то же. Теперь сама ошибка становится привязана не к потоку, а к объекту, где это выполнялось.

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

Интроспекция или отладка в продакшн

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

— Давайте, я сейчас рестартну!
— Иди за дверь и там рестартни у кого-нибудь другого!

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

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

Бонусы

Код сверхнадежен. Например, у Python есть хрупкость с old vs async, и она еще сохранится лет пять, не меньше. Учитывая, с какой скоростью внедрялся Python 3, не стоит надеяться, что это будет быстро.

Это важно. Читать и трейсить сообщения проще, чем отлаживать callback’и. Тем, что сообщения — это кусочек данных в памяти. Казалось бы, если у нас все равно есть callback’и для обработки сообщений, которые мы можем увидеть, то чем это лучше? Его можно добавить в трейсер, получить в текстовом файле список сообщений. Его можно посмотреть глазками и понять, что сюда пришло. Это удобнее, чем callback’и.

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

Проблемы

Естественно, проблемы у Erlang тоже есть.

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

В Erlang так нельзя: надо аккуратно распилить данные, распределить по пачке процессов, уследить за всем. Накладные расходы на копирование данных между процессами. Мы можем написать программу на C, которая будет запускаться на всех 80 ядрах и обрабатывать один массив данных, и будем считать, что она это делает правильно и корректно. Это коммуникация стоит ресурсов — тактов процессора.

Единственный конкурент, который выжил за эти 10 лет, написан на Java. Насколько это быстро или медленно? Мы пишем код на Erlang уже 10 лет. Но у них Java со всеми ее заморочками, начиная с JIT. С ним у нас практически полный паритет по производительности: кто-то говорит, что мы хуже, кто-то, что они.

Внезапно выясняется, что в этом случае правильность алгоритмов и умение все это отлаживать в продакшн оказывается важнее, чем потенциальные плюшки от Java. Мы пишем программу, которая обслуживает одновременно десятки тысяч сокетов и прокачивает через себя десятки Гб данных. В нее вложили миллиарды долларов, но это не дает Java JIT каких-то магических преимуществ.

Но если мы хотим померяться дурацкими и бессмысленным бенчмарками, вроде «посчитать числа Фибоначчи», то здесь Erlang будет, наверное, даже хуже Python или сравним.

Например, у нас в коде есть некоторые кусочки на C, и в этих местах совсем не получалось с Erlang. Накладные расходы на аллокацию сообщений. Иногдаэто больно. Но таких мест очень мало, мы почти все выпилили из того, что оказалось лишним.

Это функция, которая вращается по кругу, делает методы receive и send receive. Под капотом в Erlang нет даже синтаксиса для изменения переменных, есть только данные, которые передаются в саморекурсивную функцию. Там даже нет объектов, это просто функция, которая работает с данными. Это и есть процесс — эмуляция состояния объекта, которая инспектируется снаружи.

Зачем это всё программисту на Python

Важно понимать траекторию развития. Я не просто так начал с исторического контекста. Хотел показать стадии разработки у системных программистов, и что мы с вами и Python находимся где-то в середине того пути развития.

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

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

Здесь можете посмотреть, что у программного комитета в работе и какие 6 тем приняты в программу за 4 месяца до конференции. Сейчас мы работаем над программой следующей Moscow Python Conf++. Call for Papers открыт до 13 января, а сама конференция состоится 27 марта. Если знаете, что нужно добавить, то а) напишите в комментариях или б) подайте заявку на доклад.

Теги
Показать больше

Похожие статьи

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

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть