В книге «Python. К вершинам мастерства» Лучано Рамальо описывает одну историю. В 2000 году Лучано проходил курсы, и однажды в аудиторию заглянул Гвидо ван Россум. Раз подвернулся такой случай, все стали задавать ему вопросы. На вопрос о том, какие функции Python заимствовал из других языков, Гвидо ответил: «Все, что есть хорошего в Python, украдено из других языков».
Это действительно так. Python давно живет в контексте других языков программирования и впитывает концепции из окружения: asyncio позаимствован, благодаря Lisp появились лямбда-выражения, а Tornado скопировали с libevent. Но если у кого и стоит заимствовать идеи, так это у Erlang. Он создан 30 лет назад, и все концепции в Python, которые сейчас реализуются или только намечаются, в 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 хочется решить.
Почему такие проблемы с многоядерностью? Мы запускали Flussonic на стоядерных компьютерах еще до того, как это делал Intel. Но у Python с этим сложности: почему он до сих пор не использует все 80 ядер наших серверов для работы?
Как не страдать от незакрытых сокетов? Мониторить количество открытых сокетов это большая проблема. Когда оно достигает предела, закрывать и не допускать утечек тоже.
Есть ли решение у забытых глобальных переменных? Утечка глобальных переменных — это ад для любого языка со сборкой мусора, как то Java или C#.
Как использовать железо, не сжирая впустую ресурсы? Как обойтись без запуска 40 джанговских воркеров и 64 Гбайт RAM, если мы хотим использовать серверы эффективно, а не выбрасывать сотни тысяч долларов в месяц на ненужное железо?
Зачем нужна многоядерность
Чтобы все ядра использовались полностью, требуется гораздо больше воркеров, чем ядер. Например, на 40 ядер процессора нужно от 100 воркеров: один воркер пошел к базе данных, другой занят чем-то еще.
Один воркер может потреблять 300-400 Мбайт. Это мы еще пишем на Python, а не на Ruby on Rails, который может потреблять в несколько раз больше и 40 Гбайт RAM легко и непринужденно вылетят впустую. Это не сильно дорого, но зачем покупать память там, где можно не покупать.
Многоядерность помогает шарить общие данные и снижать расход памяти, удобно и безопасно запускать много независимых друг от друга процессов. Это гораздо проще программировать, но дороже по памяти.
Управление сокетами
По веб-сокету опрашиваем 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 с разных потоков, то доступ к данным можно написать так: склеить в памяти два экземпляра Python, чтобы они пошарили данные — они их просто сломают. Например, чтобы пойти к dict и ничего не сломать, надо ставить перед ним мьютексы. Если перед каждым 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
Объект может быть активирован при готовности сокета.
Мы берем объекты, в которые собираем необходимые данные от контекста и привязываем их активацию к сокету. Теперь готовность сокетов — один из самых важных элементов управления всей системой. Объекты будут нашими контекстами.
Но при этом язык все еще отделяет само понятие контекста исполнения, в котором живут исключения. Контекст исполнения живет отдельно от объектов и слабо связан с ними. Здесь возникает проблема с тем, что мы стараемся собирать данные внутри объектов: без них никак, а язык это не поддерживает.
Все это приводит к классическому callback hell. За что, например, «любят» Node.js — до недавнего времени не было вообще никаких других способов, а в Python уже все-таки появилось. Беда в том, что есть разрывы кода в точках внешнего 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 и писать к ним модули, то знает, что там есть Apache pool. Между Apache pool’s запрещены какие-либо ссылки. Данные от одного Apache pool — пула, связанного с запросами, находятся внутри него, и нельзя оттуда ничего выносить.
Теоретически можно, но если так делать, то либо кто-то наругает, либо патч не примут, либо ждет долгая и мучительная отладка на продакшн. После этого никто не будет так поступать и разрешать делать такие вещи другим. На данные между контекстами ссылаться уже просто так нельзя, нужна полная изоляция.
Как обмениваться активностью? Необходимы не маленькие монады, которые внутри себя закрыты и никак друг с другом не общаются. Нам надо, чтобы они общались. Один из подходов — это обмен сообщениями. Это примерно тот путь, по которому пошли в Windows, обмениваясь сообщениями между процессами. В обычной ОС нельзя дать ссылку на память другого процесса, но можно сигнализировать через сеть, как в UNIX, или через сообщения, как в Windows.
Все ресурсы внутри процесса и контекст становятся потоком исполнения. Мы склеили вместе:
- runtime-данные в виртуальной машине, в которых возникают исключения;
- поток исполнения, как то, что исполняется на процессоре;
- объект, в котором логически собираются все данные.
Поздравляю — мы изобрели UNIX внутри языка программирования! Эту идею придумали примерно в 1969 году. Пока что в Python его еще нет, но Python, скорее всего, к этому придет. А, возможно, и не придет — не знаю.
Что это дает
Прежде всего, автоматический контроль за ресурсами. На Moscow Python Conf++ 2019 рассказывали, что можно на Go написать программу и обработать все ошибки. Программа будет стоять как влитая и работать месяцами. Это действительно так, но мы не обрабатываем все ошибки.
Мы — живые люди, у нас всегда есть сроки, желание сделать что-то полезное, а не обрабатывать 535-ю ошибку за сегодня. Код, который обсыпан обработкой ошибок, ни у кого никогда не вызывает теплых чувств.
Поэтому мы все пишем «happy path», а дальше на продакшн разберемся. Будем честны: только когда нужно что-то обрабатывать, тогда и начинаем обрабатывать. Defensive programming — это чуть-чуть другое, и это не коммерческая разработка.
Поэтому, когда у нас есть автоконтроль за ошибками — это прекрасно. Но операционные системы его придумали 50 лет назад: если какой-то процесс умирает, то все, что он открыл, закроется автоматически. Никому сегодня не надо писать код, который будет подчищать файлы за убитым процессом. Этого нет уже 50 лет ни в одной ОС, а в Python все еще надо за этим всем внимательно и аккуратно следить руками. Это странно.
Можно вынести тяжелые вычисления в другой контекст, а он уже может уйти на другое ядро. Мы разделили данные, нам больше не нужны мьютексы. Можно отправить данные в другой контекст, сказать: «Ты где-нибудь там выполнись, а потом сообщи мне, что ты закончил и что-то сделал».
Реализация asyncio без слов «async/await». Дальше небольшая помощь от виртуальной машины, от runtime. Это то, о чем мы говорили с async/await
: можно переделать также на сообщения, убрать async/await
и получить это на уровне виртуальной машины.
Процессы Erlang
Erlang придумали 30 лет назад. Бородатые ребята, которые тогда были не очень бородатые, посмотрели на UNIX и перенесли все концепции в язык программирования. Они решили, что у них теперь будет своя штука, чтобы спать по ночам и спокойно ездить на рыбалку без компьютера. Тогда еще не было ноутбуков, но бородатые ребята уже догадывались, что об этом нужно думать заранее.
Мы получили Erlang (Elixir) — активные контексты, которые выполняются сами. Дальше мой пример на Erlang. На 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, потому что не прижилось.
Вызовы методов. У нас есть объекты, которые мы называем процессы. Через сообщения вызываются методы на процессах.
Вызов методов — это тоже посылка сообщения. Здорово, что теперь его можно сделать с таймаутом. Если что-то нам отвечает медленно, вызываем метод на другом объекте. Но при этом говорим, что готовы ждать не больше 60 с, потому что у меня клиент с таймаутом в 70 с. Мне нужно будет пойти и сказать ему «503» — приходи завтра, сейчас тебя не ждут.
Больше того, ответ на вызов можно отложить. Внутри объекта можно принять запрос на вызов метода, и сказать: «Да-да, я тебя сейчас положу, приходи через полчаса, я тебе отвечу». Можно и не говорить, а молча отложить в сторонку. Мы этим иногда пользуемся.
Как работать с сетью?
Можно писать линейный код, callback’ами или в стиле asyncio.gather
. Пример, как это будет выглядеть.
wait4([ ]) ->
[ ];
wait4(List) ->
receive
{reply, Pid, Price} -> [Price] ++ wait4(List -- [Pid])
after
60000 ->
[]
end.
В функцииwait4
из предыдущего примера мы перебираем список тех, от кого еще ждем ответы. Если с помощью метода receive
получаем сообщение от того процесса — записываем в список. Если список закончился, мы возвращаем все, что было и накапливаем список. Мы попросили одновременно три объекта пригнать нам данные. Если они не справились все вместе за 60 с, и хотя бы один из них не ответил ОК, у нас будет пустой список. Но важно то, что мы сделали общий таймаут на запрос сразу к целой пачке объектов.
Кто-то может сказать: «Подумаешь, в libcurlесть все то же самое». Но здесь важно то, что с той стороны может быть не только поход по HTTP, но и поход к БД, а еще какие-то вычисления, например, подсчет какой-то оптимальной циферки для клиента.
Обработка ошибок
Ошибки перешли из потока в объект, которые теперь одно и то же. Теперь сама ошибка становится привязана не к потоку, а к объекту, где это выполнялось.
Это гораздо логичнее. Обычно, когда мы рисуем на доске всякие квадратики и кружочки в надежде, что они оживут и начнут приносить нам результат и деньги, мы рисуем, как правило, объекты, а не потоки, в которых эти объекты будут выполняться. Например, на сдачу мы можем получить автоматическое сообщение о смерти другого объекта.
Интроспекция или отладка в продакшн
Что может быть приятнее, чем пойти на прод и дебажить, особенно, если ошибка возникает только под нагрузкой в часы пик. В час пик мы говорим:
— Давайте, я сейчас рестартну!
— Иди за дверь и там рестартни у кого-нибудь другого!
Здесь мы можем зайти внутрь живой системы, которая запущена прямо сейчас и специально к этому не подготовлена. Для этого не требуется перезапускать её с профилировщиком, с отладчиком, пересобирать.
Без какой-либо потери производительности в живой продакшн-системе мы можем посмотреть список процессов: что у них внутри, как это все это работает, потрейсить их, проверить, что у них происходит. Все это бесплатно из коробки.
Бонусы
Код сверхнадежен. Например, у Python есть хрупкость с old vs async
, и она еще сохранится лет пять, не меньше. Учитывая, с какой скоростью внедрялся Python 3, не стоит надеяться, что это будет быстро.
Читать и трейсить сообщения проще, чем отлаживать callback’и. Это важно. Казалось бы, если у нас все равно есть callback’и для обработки сообщений, которые мы можем увидеть, то чем это лучше? Тем, что сообщения — это кусочек данных в памяти. Его можно посмотреть глазками и понять, что сюда пришло. Его можно добавить в трейсер, получить в текстовом файле список сообщений. Это удобнее, чем callback’и.
Шикарная многоядерность, управление памятью и интроспекция внутри живой системы на продакшн.
Проблемы
Естественно, проблемы у Erlang тоже есть.
Потеря максимальной производительности из-за того, что больше не можем ссылаться на данные в другом процессе или объекте. Приходится их перемещать, а это не бесплатно.
Накладные расходы на копирование данных между процессами. Мы можем написать программу на C, которая будет запускаться на всех 80 ядрах и обрабатывать один массив данных, и будем считать, что она это делает правильно и корректно. В Erlang так нельзя: надо аккуратно распилить данные, распределить по пачке процессов, уследить за всем. Это коммуникация стоит ресурсов — тактов процессора.
Насколько это быстро или медленно? Мы пишем код на Erlang уже 10 лет. Единственный конкурент, который выжил за эти 10 лет, написан на Java. С ним у нас практически полный паритет по производительности: кто-то говорит, что мы хуже, кто-то, что они. Но у них Java со всеми ее заморочками, начиная с JIT.
Мы пишем программу, которая обслуживает одновременно десятки тысяч сокетов и прокачивает через себя десятки Гб данных. Внезапно выясняется, что в этом случае правильность алгоритмов и умение все это отлаживать в продакшн оказывается важнее, чем потенциальные плюшки от Java. В нее вложили миллиарды долларов, но это не дает Java JIT каких-то магических преимуществ.
Но если мы хотим померяться дурацкими и бессмысленным бенчмарками, вроде «посчитать числа Фибоначчи», то здесь Erlang будет, наверное, даже хуже Python или сравним.
Накладные расходы на аллокацию сообщений. Иногдаэто больно. Например, у нас в коде есть некоторые кусочки на C, и в этих местах совсем не получалось с Erlang. Но таких мест очень мало, мы почти все выпилили из того, что оказалось лишним.
Под капотом в Erlang нет даже синтаксиса для изменения переменных, есть только данные, которые передаются в саморекурсивную функцию. Это функция, которая вращается по кругу, делает методы receive
и send receive
. Это и есть процесс — эмуляция состояния объекта, которая инспектируется снаружи. Там даже нет объектов, это просто функция, которая работает с данными.
Зачем это всё программисту на Python
Важно понимать траекторию развития. Я не просто так начал с исторического контекста. Хотел показать стадии разработки у системных программистов, и что мы с вами и Python находимся где-то в середине того пути развития.
Возможно, это позволит понять дальнейшее развитие. Вдруг кто-то из вас решит менять Python, чтобы он наконец приобрел фичи современнее, которым хотя бы 20 лет, а не 40.
Естественно, кругозор и знание альтернатив тоже полезны. Какие-то вещи, возможно, вы решите переписать на Elixir, посмотрев на примеры, но это в качестве дополнительного бонуса.
Сейчас мы работаем над программой следующей Moscow Python Conf++. Здесь можете посмотреть, что у программного комитета в работе и какие 6 тем приняты в программу за 4 месяца до конференции. Если знаете, что нужно добавить, то а) напишите в комментариях или б) подайте заявку на доклад. Call for Papers открыт до 13 января, а сама конференция состоится 27 марта.
Автор: Григорий Петров