Что такое пулы ликвидности в криптовалюте и как на них заработать
Ликвидность важна для всех торгуемых активов, включая криптовалюты. Низкий ее уровень означает, что на рынке присутствует волатильность, вызывающая скачки цен. С другой стороны, высокий уровень означает, что существует стабильный рынок с небольшими ценовыми колебаниями.
Таким образом, легче покупать или продавать криптоактивы на ликвидном рынке, поскольку заказы на покупку или продажу будут исполняться быстрее из-за большего количества участников рынка. По сути, это означает, что можно войти в сделку или выйти из нее в любой момент.
Что такое пулы ликвидности в криптовалюте простыми словами
В криптовалютном мире существует огромное количество централизованных сервисов, хранящих огромные резервы биткоинов и других криптовалют. Прежде всего, это централизованные биржи (CEX).
Чтобы начать на них торговлю, пользователю необходимо перевести средства в свой аккаунт. Взамен он получает удобство использования торгового терминала и другие преимущества. Однако средства пользователя находятся на счетах и под контролем биржи.
Для решения данной проблемы отдельные команды и разработчики начали активно создавать децентрализованные биржи (DEX). Такие площадки позволяют трейдерам напрямую осуществлять торговые операции без посредников и брокеров посредством смарт-контрактов. При этом приватные ключи остаются у пользователя, а не хранятся на бирже.
Разработчики полностью скопировали интерфейс и прототип торговых операций с централизованных бирж. Но, как оказалось, этого было недостаточно.
Основной проблемой DEX была нехватка ликвидности. Пользователи не понимали, как это работает, не хотели торговать на таких биржах, поэтому не вносили депозит. В результате спреды в книге заказов (разница между ближайшими ценами покупки и продажи) могли достигать десятки процентов. Особенно это было заметно в паре с непопулярными активами.
Для решения этой проблемы разработчики решили прибегнуть ко второй особенности централизованных бирж — использовать маркет-мейкеров для создания ликвидности. Создавая заказы на покупку и продажу, маркет-мейкеры тем самым уменьшают размер спреда и от этого получают прибыль.
Однако эта идея также не прижилась из-за того, что пропускная способность блокчейнов по-прежнему невелика, а сетевые комиссии превышают прибыль маркет-мейкера. Это была вторая ошибка DEX.
Со временем разработчики начали понимать, что стандартные DEX никогда не смогут конкурировать с централизованными биржами. Децентрализованные биржи были мало привлекательны, на них никто не торговал. Поэтому команды начали думать о том, какими должны быть DEX следующего поколения. Вскоре ответ был найден.
Они придумали пулы — хранилище, куда пользователи могут добавлять свои активы для увеличения рыночной ликвидности. Эти средства замораживаются на специальном смарт-контракте, по которому проводятся обменные операции.
Из теории экономики:
«Пул — объединение компаний, при котором прибыль поступает в общий фонд, а затем распределяется между участниками согласно заранее установленной пропорции.»
Как работают пулы ликвидности (пример)
Провайдер ликвидности создает пул, в котором размещает два актива для обмена и устанавливает начальный обменный курс. Теперь любой желающий (поставщик) может добавить в него свои активы. За это он будет получать комиссию от каждой сделки пользователей с данным хранилищем.
Каждый обмен в пуле приводит к изменению обменного курса. Этот механизм называется автоматическим маркет-мейкером (AMM). При заключении сделки объем монет первого актива увеличивается, а объем второго — уменьшается. В результате курс меняется.
Соотношение активов рассчитывается как 400 / 0,25 = 1600. Это означает, что на один ETH приходится 1600 BAT. Курс 1/1600.
Затем приходит пользователь и хочет купить 400 BAT.
По текущему курсу он должен заплатить 0,25 ETH ($100).
После покупки соотношение становится 1,25 ETH / 1200 BAT, потому что пользователь добавляет 0,25 ETH и получает 400 BAT из пула. Соотношение станет 1/960.
В результате курс вырос на 67%.
Это их основная проблема — при крупных покупках относительно объема монет в хранилище курс может существенно измениться.
Однако, если объем покупок невелик или размер общего фонда достаточно крупный, то разница не превысит 0,1%. А это уже очень близко к спредам централизованных бирж.
Uniswap была первой биржей, реализовавшей этот алгоритм. Именно из-за этого он стал настолько привлекательным сейчас и обогнал Coinbase по объему торгов.
Однако были и конкуренты со своими особенностями. Например, маркет-мейкер Balancer увеличил количество возможных активов в пуле с 2 до 8. Это привлекло поставщиков ликвидности более высоким размером вознаграждения.
Другой проект — Curve. Разработчики сразу поняли, что торговля такими активами сопряжена с высокими рисками из-за проскальзывания и волатильности цен на криптоактивы. Поэтому они реализовали пулы со стейблкоинами, в результате чего курс обмена в хранилищах практически не меняется.
Децентрализованные биржи имеют долгую историю и не сразу получили признание. Прошло много времени, прежде чем они пришли к решению своей основной проблемы — низкой ликвидности. Пулы были нововведением, которое вдохнуло жизнь в DEX и помогло им конкурировать с централизованными площадками.
В ближайшем будущем появится много DEX нового поколения, которые решат проблемы проскальзывания и станут полноценной заменой традиционных бирж.
Недостатки пулов ликвидности и что такое непостоянная потеря
Предоставление ликвидности пулу помогает функции DeFi и может иметь экономическую отдачу, но также сопряжено с некоторыми значительными рисками.
Во время высокой волатильности рынка предложение токенов в хранилище может привести к потерям из-за проскальзывания. Такие потери называют непостоянными убытками. Может случиться так, что стоимость ваших активов в общем фонде возрастет, но гораздо меньше, чем если бы она лежала в вашем кошельке.
Пример непостоянной потери:
Через некоторое время количество активов в пуле меняется: теперь в хранилище 5 ETH и 20000 DAI. Следовательно, цена 1 ETH = 4000 DAI.
Это то, что называется непостоянной потерей.
Кроме этого, существуют другие риски при работе с пулами:
Как заработать на пулах на бирже Binance
Вы можете получать пассивный доход, став поставщиком ликвидности для других трейдеров. Для этого нужно добавить собственные активы в любой из пулов, представленных на бирже Binance.
У поставщиков активов есть три источника дохода:
Размер вашего дохода будет зависеть от нескольких показателей:
Самый доходный пул — с небольшим объемом, но с высокими показателями годовой процентной доходности и большим количеством транзакций.
Binance Liquid Swap позволяет забрать ваш доход в любое время.
Как добавить активы в пул
1. После авторизации на бирже Бинанс перейдите на вкладку Финансы — Liquid Swap в верхнем меню.
В этом разделе вы увидите 4 вкладки:
2. Перейдите на первую вкладку Обзор (Overview).
Вы увидите все предлагаемые пары для пополнения. Они представлены в двух видах:
Стабильные (Stable) — пары долларовых стейблкоинов. Колебания обменного курса в этих парах практически незаметны, поэтому доход более надежный и стабильный. Но потенциальная прибыль гораздо меньше.
Инновационные (Innovation) — эта модель предполагает повышенные колебания обменного курса. Это является причиной повышенного риска волатильности, но обладает преимуществами повышенного размера потенциальной прибыли.
Нужные можно отобрать в выпадающем списке.
3. Кликните по выбранному пулу (например, стабильный BUSD/DAI).
Теперь нажмите на кнопку Ликвидность (Liquidity).
Вам предложат пройти тест из 10 вопросов, на которые нужно правильно ответить. Эта мера предосторожности необходима для того, чтобы вы осознавали свои риски.
После успешного прохождения теста вы увидите такую форму:
Отметьте точкой активы, которые будете вносить в пул (они должны быть у вас на балансе). На выбор дается три варианта — вы можете выбрать любой в зависимости от того, какие монеты у вас есть в наличии.
Укажите сумму пополнения.
Поставьте галочку напротив Условий использования Binance Liquid Swap.
Нажмите кнопку Добавить ликвидность (Add Liquidity).
Теперь загляните на вкладку Моя доля (MyShare). Здесь будет указан ее размер в USD, а также фиксироваться все начисленные вам вознаграждения.
Как вывести активы и вознаграждение на баланс
Чтобы вывести свою долю на баланс Бинанса, перейдите в свой пул.
Нажмите на кнопку Ликвидность (Liquidity), на закладку Удалить (Redeem).
В форме укажите сумму доли, которую хотите вывести.
Точкой отметьте нужную валюту из предлагаемых трех вариантов.
Нажмите кнопку Удалить (Redeem).
Теперь проверьте ваш баланс на Binance — на нем должны появиться выведенные из пула монеты с вознаграждением.
Децентрализованные биржи с использованием пулов ликвидности
Ниже представлены пять наиболее популярных среди пользователей обменных сервисов, использующих протоколы ликвидности для определения цен на активы.
Протокол кредитования и заимствования стейблкоинов и альткоинов. Пользователям предлагаются плавающие и фиксированные процентные ставки по кредитам.
Самая известная DEX в настоящее время, где пользователи могут обменивать любой токен ERC-20 с сотнями пулов.
Децентрализованная биржа, где пользователи могут создавать пулы до 8 криптовалют вместо стандартных 2. Поставщики ликвидности (LP) также могут устанавливать комиссию за транзакции при обмене с определенным хранилищем.
Данная площадка ориентирована на торговлю стейблкоинами USDT и USDC. Ориентация на стабильные монеты снижает комиссии, а также минимизирует проскальзывание при обмене.
Популярный сервис позиционирует себя, как управляемая сообществом децентрализованная биржа. Данный протокол предлагает пользователям до 3 уровней потенциального дохода.
У каждой вышеперечисленной биржи есть свой токен (тикеры указаны в скобках), который можно купить на бирже Binance. Так как тема ликвидности очень важная и перспективная, инвестиции в данные активы могут принести дополнительную прибыль.
Как работает майнинг ПУЛ на пальцах
В последние годы волна майнинга накрыла всех. На фоне этого в нишу пришло много новичков, которые зачастую не разбираются в области криптовалют или программирования. Но это не повод оставаться в стороне прогресса. Давайте разберёмся, что такое майнинг и как работает майнинг пул. Объясняем очень-очень простым языком.
Что такое блокчейн
Для работы большинства криптовалют необходимо создание цепочки блоков — блокчейна. Каждый новый блок содержит транзакции, которые появились в сети с момента создания предыдущего блока. Мы любим сравнивать блокчейн с бухгалтерской книгой. Каждый новый блок — страница книги, на которой записаны транзакции.
Как создать новый блок, то есть перевернуть страницу бухгалтерской книги? Для создания нового блока майнеры решают математическую задачу. Важный момент: эта задача основана на информации, которую содержит предыдущий блок. Особенность неслучайная, ведь таким образом блоки формируют непрерывную цепочку — блокчейн, в которой все блоки связаны друг с другом. Соответственно, хакеры или другие плохие люди не могут удалить или заменить блок в цепочке.
Зачем удалять или заменять блок? Например, в блок под номером 324670 попала важная транзакция, в которой вы перевели Васе 10 долларов в биткоинах, а Вася дал вам взамен свои Жигули. Если вы впоследствии удалите блок 324670, то и транзакция с переводом Васе пропадет. Это значит, что у вас останутся и деньги и Жигули Васи, который теперь разве что будет плакать. В соответствии с основным принципом работы блокчейна вынуть блок не получится. В этом основной принцип его работы.
Как работает майнинг
Математические задачи для создания нового блока решаются майнерами с помощью различного вычислительного оборудования: процессоров, видеокарт, FPGA или ASIC-устройств. На заре майнинга любой процессор или видеокарта обладали достаточной мощностью для нахождения массы решений в день. За это они получали своё вознаграждение за созданный блок, причём делали это ежедневно. По мере роста интереса к криптовалютам сложность задач возрастала, поэтому одиночный компьютер уже не мог находить блоки так же часто. Со сложностью возрастала и награда за найденный блок в долларовом эквиваленте, ведь курс криптовалют увеличивался.
Со временем майнеры стали создавать фермы из нескольких майнинг-устройств — например, видеокарт. Это нужно для увеличения вычислительной способности своего оборудования, которую майнеры называют хешрейтом. На фотографии ниже — ферма, в которой одновременно работают сразу восемь видеокарт.
Рекомендуем прочитать статью «Что такое майнинг? Удача в майнинге», если хотите глубже изучить вопрос. Не переживайте, там нет сложных формул. Статья написана очень понятным языком.
Курсы криптовалют росли, а желающих обогатиться становилось всё больше. В результате даже майнинг-фермы в одиночку были уже не способны найти решение блока. Тогда майнеры решили объединить усилия — создать пулы для совместного майнинга.
Как работает майнинг-пул
К примеру, в сети Биткоина в среднем появляется 144 новых блока в день. Конкуренция просто сумасшедшая. Майнеров намного больше, чем 144 человека, а получить свой кусок пирога хочет каждый. Одному майнеру в добыче Биткоина делать нечего, поэтому все одиночные майнеры собрались в майнинг-пулы. Майнинг пул — это сервер, который объединяет майнеров. Майнеры совместно находят решения блоков и делят между собой награду.
Как только в сети криптовалюты находится блок — неважно, кто именно его нашёл — информация о нём распространяется по всей сети. После этого сеть сразу же предлагает решить новую задачу для следующего блока. Майнинг-пул получает эту задачу и сразу же отправляет её всем своим майнерам. С каждым новым блоком это повторяется, то есть пул отправляет майнерам новую задачу.
Если один из майнеров пула находит решение задачи, он отправляет решение на пул, пул его проверяет и отправляет дальше в сеть криптовалюты. Сеть никогда не узнает, какой именно майнер нашёл блок: для неё это сделал майнинг пул. Таким образом вся слава достаётся майнинг-пулу, и именно ему будут петь дифирамбы, а майнерам нет. Однако цель майнера это не известность, а заработок. Поэтому майнер прежде всего хочет, чтобы пул честно распределил вознаграждение за блок и отдал ему его долю.
Источник: блокчейн-эксплорер Zcoin
Допустим, сто майнеров работают через один майнинг-пул. Как только в сети находится новый блок, пул шлёт майнерам новую задачу. Они трудятся целые сутки не покладая рук, и вот в конце дня майнер Вася — один из ста майнеров пула — нашёл решение блока. Пул проверил решение, оно оказалось верным. Пул отправил решение в сеть и получил за него вознаграждение.
Как поделить вознаграждение среди майнеров? Получается, что Вася нашёл решение одного блока за сутки, а все остальные нашли ноль решений блоков. Отдать Васе всё — нечестно, ведь работали все. Поделить поровну — тоже нечестно, поскольку у Васи работала одна ферма из восьми видеокарт, а у Пети — пять ферм из восьми видеокарт. Петя потратил больше денег на электроэнергию, закупку оборудования и так далее, просто сегодня ему не повезло и он не нашёл ни одного решения. Как быть?
Майнинг-пулы решили усложнить тактику работы. Они стали отправлять майнерам не задачу, которую нужно решить для создания нового блока, а более упрощённый вариант этой задачи в надежде, что решение упрощённой задачи совпадает с решением полной задачи. Вспомните фильм “Эпидемия” 1995 года, в котором все хотели найти одну-единственную обезьянку по имени Бетси, зараженную редким вирусом, чтобы сделать вакцину и спасти человечество. Эта обезьянка — своего рода решение блока.
Так вот, майнинг-пул начал говорить майнерам: “тащите сюда всех обезьян, я сам проверю, какая из них правильная”. Майнеры решили отправлять пулу всех обезьян, которых они находят. У Васи одна ферма — то есть один сотрудник, который ищет обезьян, у Пети пять ферм или пять сотрудников, ищущие обезьян. Вася за день отправил на пул 100 обезьян, и одна из них оказалась нужной всему миру Бетси, а Петя 500 обезьян, но Бетси так и не нашёл. В майнинг-терминологии обезьянка — это шара (от английского “share”). Подробно про шары в майнинге читайте в этой статье.
Такой подход решает сразу несколько задач. Во-первых, у пула есть возможность честно распределить награду среди майнеров в соответствии с количеством обезьянок (шар), которые они отправляют на пул. Петя, нашедший 500 обезьян (шар), получит в 5 раз больше, чем Вася, нашедший 100 обезьян. И не важно, что заветную обезьянку нашёл Вася. Во-вторых, на основании полученных шар пул может предоставить майнеру красивую и понятную статистику его работы.
Майнинг на одной видеокарте
У меня всего одна видеокарта. Куда мне тягаться с грозными криптовалютными фермерами?
Майнинг-пулы существуют именно для вас. Пусть ваши мощности небольшие, но только благодаря пулам вы можете получать регулярное вознаграждение за майнинг. В одиночку вы никогда не найдёте блок, а пул находит блоки регулярно. Как только очередной блок будет найден, пул распределит вознаграждение за него в соответствии с майнинг-мощностями майнеров пула. Если вы принимали участие в нахождении блока, вы тоже получите свою “дольку”.
Майнинг без пула?
Мы немного лукавим, говоря, что один майнер в современном мире больше не может найти решение блока. В принципе, это возможно, просто очень маловероятно — особенно в случае с Биткоином. Найти решение блока любой другой криптовалюты намного проще. Если майнеры не хотят объединяться в пулы, а собственных вычислительных мощностей им недостаточно, они могут арендовать майнинг-мощности на сайтах по типу Nicehash и Miningrigrentals.
Получается, один в поле всё еще может быть воином. Подробнее о самостоятельном майнинге читайте по этой ссылке.
Что такое пулы для майнинга и как они работают
Содержание
Введение
Очевидно, что компьютеры, способные вычислить большее количество хешей в секунду, найдут больше блоков. Это является причиной огромного сдвига в экосистеме. Майнеры стали наращивать вычислительные мощности в стремлении получить конкурентное преимущество.
Как следует из названия, ASIC были созданы для выполнения единственной задачи – вычисления хешей. И поскольку они разработаны специально для этого, их производительность крайне велика. Настолько велика, что использование других типов оборудования для майнинга биткоинов стало довольно редким.
Что такое майнинг-пул?
Допустим, вы и девять других участников владеете каждый по 0.1% общей хеш-мощности сети. Это означает, что в среднем вероятность нахождения блока для вас равна 1:1000. При прогнозируемой добыче в 144 блока в день, вероятнее всего, вы будете находить один блок в неделю. В зависимости от денежных ресурсов, инвестиций в аппаратное обеспечение и электроэнергию, такой подход одиночного майнинга может быть приемлемой стратегией.
Так в двух словах можно описать майнинг-пул. В настоящее время майнинг-пулы широко распространены, поскольку гарантируют более устойчивый доход своим участникам.
Как работает майнинг-пул?
Майнинг-пулы системы Pay-Per-Share (PPS)
Доля – это хеш, используемый для отслеживания работы каждого майнера. Сумма, выплачиваемая за каждую долю, является номинальной, но со временем увеличивается. Обратите внимание: доля не является валидным хешем сети. Это лишь хеш, соответствующий условиям конкретного майнинг-пула.
По схеме PPS вы получаете вознаграждение независимо от того, нашли ли блок. Менеджер пула берет на себя риски, поэтому он, вероятнее всего, будет взимать дополнительную плату – либо с пользователей, либо с вознаграждения за блок.
Майнинг-пулы системы Pay-Per-Last-N-Shares (PPLNS)
Рассмотрим пример. Если текущая награда за блок составляет 12,5 BTC (допустим, комиссии за транзакцию нет), а комиссия оператора – 20%, то все майнеры получат 10 BTC. Если N равно 1 000 000 и вы отправили 50 000 долей, вы получите 5% от награды майнеров (или 0,5 BTC).
Вы можете найти различные вариации подобных схем, но чаще всего будете слышать именно об этих двух. Обратите внимание: хотя мы говорим здесь о биткоине, большинство популярных криптовалют Proof of Work также имеют пулы для майнинга, например, Zcash, Monero, Grin и Ravencoin.
Угрожают ли майнинг-пулы децентрализации?
Увеличивают ли майнинг-пулы риск атаки 51%? Возможно, но маловероятно.
В теории четыре верхних пула могут вступить в сговор с целью захвата сети, хотя это не имеет большого смысла. Даже если им удастся осуществить атаку, цена биткоина, вероятно, упадет, поскольку их действия подорвут систему. В результате все полученные ими монеты потеряют ценность.
Более того, пулы не обязательно владеют оборудованием для майнинга. Майнеры предоставляют информацию о своем оборудовании серверу координатора, но могут свободно переходить в другие пулы. В интересах участников и операторов пула сохранить децентрализованную экосистему. В конце концов, они зарабатывают деньги лишь при условии, что майнинг остается прибыльным.
Известно случаев, когда пулы росли до размеров, которые можно было бы считать угрожающими. Обычно пул (и его майнеры) предпринимают шаги по уменьшению хешрейта.
Понимаем соединения и пулы
Прим. перев.: автор этой статьи — технический архитектор Sudhir Jonathan — рассказывает об одном из тех базовых механизмов, с которым сталкивается каждый пользователь, разработчик и системный администратор. Однако до возникновения определённых (и иногда довольно специфичных) проблем многие не задумываются о том, как всё работает «под капотом». Автор устраняет этот пробел, используя популярные фреймворки, серверы БД и приложений в качестве понятных примеров.
Соединения — это скрытый механизм, который компьютерные системы используют для общения друг с другом. Они стали настолько неотъемлемой частью нашей жизни, что мы часто забываем, насколько они важны, не замечаем, как они работают и терпят неудачу. Часто мы забываем о них до тех пор, пока не возникает проблема. При этом обычно она проявляется массовым отказом именно в то время, когда системы загружены сильнее всего. Поскольку соединения встречаются повсюду и они важны практически для каждой системы, стоит потратить немного времени на их изучение.
Соединения — что это?
Соединение — это связующее звено между двумя системами, позволяющее им обмениваться информацией в виде последовательности нулей и единиц: посылать и принимать байты.
В зависимости от того, как расположены системы по отношению друг к другу, комбинация нижележащего программного и аппаратного обеспечения активно работает, чтобы обеспечить физическое перемещение информации, абстрагируя ее. Например, при взаимодействии двух Unix-процессов за выделение памяти для обмена данными и приём/доставку байтов с обеих сторон отвечает система межпроцессного взаимодействия (IPC). Если системы расположены на разных компьютерах, они скорее всего будут взаимодействовать по протоколу TCP, который и обеспечит перемещение данных по проводной или беспроводной системе связи между компьютерами. Детали совместной работы компьютеров для надёжной обработки, передачи и приёма данных скорее относятся к проблеме стандартизации, и большинство систем используют базовые блоки, предоставляемые протоколами UDP и TCP. То, как эти соединения обрабатываются на каждом из концов, является более актуальной проблемой для разработки приложений. О ней мы и поговорим сейчас.
Где используются соединения?
Соединения используются прямо сейчас. Ваш браузер установил соединение с веб-сервером, на котором размещен этот блог, и по нему получил байты, составляющие HTML, CSS, JavaScript и изображения, на которые вы сейчас смотрите. При работе по протоколу HTTP/1.1 браузер устанавливал множество соединений с сервером — по одному для каждого файла. Протокол HTTP/2 позволил получить все файлы по одному соединению (с помощью мультиплексирования). Во всех этих случаях браузер выступал клиентом, а сервер блога, собственно, был сервером.
Но сервер, в свою очередь, также устанавливал соединения, чтобы передать эту страницу. Так, он подключился к базе данных и отправил ей запрос, содержащий URL страницы. В ответ он получил её содержимое. В данном сценарии сервер приложений выступал клиентом, а база данных — сервером. Кроме того, сервер приложений мог устанавливать соединения с различными сторонними сервисами, такими как сервис подписки или оплаты, а также сервис определения местоположения.
Организовать «отгрузку» статических файлов, таких как JS, CSS и изображения, помогает CDN-система, расположенная между браузером и сервером блога. Браузер (клиент) установил соединение с ближайшим сервером CDN, и, если нужных файлов не оказалось в кэше CDN-сервера, тот (выступая как клиент) связался с сервером блога (сервер).
Если внимательно посмотреть на системы, которыми мы пользуемся или которые создаём, можно увидеть множество всевозможных соединений. Часто они скрыты от глаз, и, забывая об их невидимом существовании и ограничениях, можно столкнуться с проблемами в моменты, когда меньше всего этого ожидаешь.
Почему важна обработка соединений?
Понимание того, как именно ведется работа с соединениями, важна, поскольку их стоимость асимметрична: издержки, связанные с созданием соединения, отличаются на стороне клиента и сервера. В одноранговой (P2P) системе это не так, и соединения имеют одинаковую «стоимость» на обоих концах, но такое бывает редко. Типичное использование соединений всегда предполагает наличие клиента и сервера, при этом издержки, связанные с созданием соединения, различны на стороне клиента и сервера.
Прежде чем перейти к различным механизмам обработки соединений, необходимо освежить знания о способах запуска программ на компьютерах и об их параллельной работе.
При старте программы операционная система запускает код как один экземпляр процесса. Во время работы процесс занимает одно ядро CPU и некоторый объём памяти, и не делится своей памятью ни с какими другими процессами.
Процесс может запускать так называемые threads (потоки выполнения) — дочерние элементы процесса, способные работать параллельно. Потоки используют память совместно с процессом, который их породил (тот может выделять больше памяти для их использования).
Также процесс может использовать event loop (цикл событий), который выглядит как система с одним процессом, который отслеживает имеющиеся задания, непрерывно и бесконечно перебирая их. При этом он выполняет активные задания и пропускает заблокированные.
Другой способ предполагает использование внутренних конструкций, таких как fibers (файберы), green-threads (зелёные потоки), coroutines (сопрограммы) или actors (акторы). Каждая из этих конструкций чуть отличается от остальных (в том числе, в смысле издержек), но все они внутренне управляются процессом и его потоками.
Возвращаясь к обработке соединений, давайте сначала рассмотрим подключения к базе данных. Для сервера приложений (клиента в данном случае) установка TCP-соединения связана с выделением небольшого объёма памяти для буфера и одного порта.
Если используется PostgreSQL, на стороне сервера каждое соединение обрабатывается путём создания нового процесса, который занимается всеми запросами, поступающими по данному соединению. Такой процесс занимает ядро процессора и около 10 Мб памяти (или больше).
MySQL для обработки каждого подключения создаёт поток внутри процесса. Требования к памяти гораздо ниже в потоковой модели, но платить за это приходится постоянным переключением контекстов.
Redis обрабатывает каждое соединение как итерацию в цикле событий, что снижает требования к ресурсам, однако платить за это приходится тем, что каждое соединение дожидается своей очереди, при этом Redis обрабатывает запросы строго по одному.
Представьте себе запрос к серверу приложений. Браузер инициирует TCP-соединение в качестве клиента (это ему обходится дёшево — небольшой объём памяти для буфера и один порт). На стороне сервера ситуация совершенно иная:
Если сервер использует Ruby on Rails, каждое соединение обрабатывается одним потоком, порождённым внутри фиксированного числа запущенных процессов (в случае веб-сервера Puma), или одним процессом (Unicorn).
Если используется PHP, система CGI запускает новый PHP-процесс для каждого соединения, а более популярная реализация FastCGI поддерживает несколько активных процессов, чтобы ускорить обработку новых соединений.
В случае Go для обработки каждого соединения создается goroutine (дешёвая и легковесная потокоподобная структура, управляемая & планируемая исполняемой средой Go).
В Node.js/Deno входящие соединения обрабатываются в цикле событий путём последовательного перебора и ответа на запросы по одному за раз.
В системах вроде Erlang/Elixir каждое соединение обрабатывается актором — ещё одной легковесной и внутренне-планируемой потокоподобной конструкцией.
Архитектуры обработки соединений
Из примеров выше видно, что существуют несколько типичных стратегий обработки соединений:
Процессы. Каждое соединение обрабатывается отдельным процессом, который либо создается специально для этого соединения (CGI, PostgreSQL), либо входит в некую группу доступных процессов (Unicorn, FastCGI).
Потоки. Каждое соединение обрабатывается отдельным потоком, который либо специально создаётся, либо берётся из специального резерва. Потоки могут быть распределены по нескольким процессам, при этом все потоки эквивалентны между собой (Puma/Ruby, Tomcat/Java, MySQL).
Цикл событий. Каждое соединение включается в цикл событий в виде задачи, и соединения с данными для чтения обрабатываются последовательно (Node, Redis). Обычно подобные системы — однопроцессные и однопотоковые, но в некоторых случаях бывают многопроцессными, когда каждый процесс действует как полунезависимая система с отдельными циклами событий.
Coroutines / Green-Threads / Fibers / Actors. Каждое соединение обрабатывается легковесной конструкцией с внутренним управлением (Go, Erlang, Scala/Akka).
Представление о том, как сервер обрабатывает соединения, имеет решающее значение для понимания его ограничений и моделей масштабирования. Даже базовое использование или настройка требуют понимания того, как обрабатываются соединения: Redis и PostgreSQL, например, предлагают различную семантику транзакций и блокировок, на которую влияют их соответствующие механизмы обработки соединений. Серверы, основанные на процессах и потоках, могут «упасть» из-за исчерпания ресурсов, если их максимальное количество не задано в разумных пределах. С другой стороны, установка пределов может привести к масштабному недоиспользованию серверов из-за слишком низких лимитов. Системы, основанные на циклах событий, ничего не выигрывают от работы на 64-ядерных CPU (если, конечно, их 64 копии не настроены на совместную работу — что отлично работает в случае веб-серверов, но плохо подходит для баз данных).
Каждый из этих способов обработки соединений по-разному проявляет себя при использовании в серверах приложений и базах данных из-за распределённой или централизованной природы каждой системы. Например, серверы приложений, как правило, хорошо подходят для горизонтального масштабирования — они работают нормально и одинаково независимо от того, один ли у вас сервер, 10 или 10000. В этих случаях отказ от модели процессов/потоков обычно приводит к росту производительности, поскольку мы хотим выполнить как можно больше работы с минимальным использованием памяти и переключением контекста процессором.
Подходы на циклах событий вроде Node отлично зарекомендовали себя на одноядерных серверах, и для использования на многоядерных серверах их необходимо кластеризовать правильным образом. Системы, основанные на сопрограммах/акторах, такие как Go или Erlang, гораздо легче задействуют ядра процессора, так как разработаны специально для этого: на одной машине параллельно могут работать многие тысячи горутин и акторов.
С другой стороны, централизованные базы данных выигрывают от обработки на основе процессов/потоков/циклов событий, поскольку из-за транзакционных гарантий системы нежелательно, чтобы множество соединений работало с одними и теми же данными в одно и то же время. В случае операций, происходящих на множестве соединений, придётся использовать блокировки во время чувствительных к транзакциям этапов их работы, или использовать стратегии вроде MVCC, поэтому чем меньше число возможных обработчиков соединений, тем лучше. Эти системы поддерживают малое число соединений на одной машине.
На крупном сервере PostgreSQL может управлять несколькими сотнями соединений, в то время как MySQL способен обрабатывать пару тысяч. Redis способен обрабатывать наибольшее количество соединений (возможно, десятки тысяч), поскольку цикл событий помогает ему поддерживать согласованность данных, но платить за это приходится тем, что одновременно выполняется только одна операция.
Распределённые базы данных могут и будут пытаться отойти от модели, основанной на процессах и потоках. Поскольку данные распределяются по нескольким машинам, от блокировок обычно отказываются в пользу секционирования (partitioning). Такие базы данных способны поддерживать множество соединений между большим числом серверов. Например, AWS DynamoDB или Google Datastore, а также распределенные БД, написанные на Go, с готовностью примут миллионы или даже миллиарды одновременных подключений.
Однако все эти решения имеют последствия — они жертвуют многими операциями (join’ы, ad-hoc-запросы) и гарантиями согласованности, предоставляемыми централизованными/односерверными базами данных. Но, идя на эту жертву, они получают возможность обрабатывать соединения секционированным, горизонтально масштабированным, практически неограниченным способом, позволяя выбирать конструкцию, которая поддерживает множество соединений на множестве машин. Проблема соединений в данном случае снимается: каждый отдельный сервер должен сам заботиться об их обработке, но в совокупности, с тысячами и миллионами машин с умной маршрутизацией подключений, данные системы часто ведут себя так, словно они бесконечно масштабируемы.
Что такое пул и зачем он нужен?
«Дороговизна» соединений требует их эффективного и экономного использования. Часто бывает сложно понять, насколько дорого они обходятся. Связано это с асимметрией: с точки зрения клиента соединение стоит дёшево, и обычно от их чрезмерного числа страдает именно сервер.
В процессе работы клиент не может позволить себе роскошь использовать отдельное соединение для каждой операции. Например, сервер приложений как клиент подключается к базе данных. Устанавливая для каждого запроса новое соединение, он искусственно ограничил бы себя возможностями БД (к которой подключается) по обработке соединений. В некоторых случаях подобный способ работы абсолютно эффективен — например, если сервер приложений является прокси-сервером для базы данных. В реальности серверы приложений выполняют кучу другой работы: ожидают поступления запрашиваемых данных на сервер, анализируют его, формулируют запрос, пересылают его в БД по соответствующему соединению, ждут результатов, считывают и обрабатывают их, преобразуют выходные данные в формат HTML/JSON/RPC, посылают сетевые запросы другим сервисам, и т.д. Основную часть времени соединение простаивает — другими словами, дорогостоящий ресурс используется неэффективно. И это мы ещё не учли издержки, связанные с созданием соединения (запуск процесса, аутентификация) и завершением его работы на стороне сервера.
Чтобы повысить эффективность использования соединений, многие клиенты баз данных используют так называемые connection pools (пулы соединений). Пул — это объект, самостоятельно обслуживающий некоторый набор соединений, не предусматривающий прямого доступа или использования. Пул выдает соединения, когда необходимо связаться с базой данных. Соединения возвращаются в пул после завершения работы. Пул может быть инициализирован с заданным числом соединений, или может наполняться по необходимости. Идеальное применение пула соединений выглядит следующим образом: код запрашивает соединение у пула (checkout), когда оно ему необходимо, использует его и сразу возвращает в пул (release). Таким образом, код не удерживает соединение, пока выполняется работа, никак с ним не связанная, что существенно повышает эффективность. Это позволяет выполнять множество различных задач с использованием одного или нескольких соединений. Если все соединения в пуле заняты, когда происходит очередной запрос (checkout), запрашивающей стороне обычно приходится ждать (block), пока соединение не освободится.
Механизм пулов по-разному реализован в различных языках и фреймворках:
Ruby on Rails, например, автоматически выделяет и возвращает соединения в пул, при этом непонимание нюансов данного процесса приводит к неэффективному коду. Представьте, что совершается запрос к базе данных, за которым следует длинный сетевой запрос к другому сервису, и еще один запрос к БД. В этом случае соединение простаивает во время выполнения сетевого запроса (автоматическое управление Rails должно быть консервативным и осторожным, а потому — неэффективным).
В Go есть драйвер для работы с БД, входящий в стандартную библиотеку, который поддерживает автоматическую работу с пулом соединений. Однако неучет того, что соединения возвращаются в пул между обращениями к БД, приводит к неожиданным и трудно воспроизводимым багам. Иногда разработчики исходят из предположения, что последовательные операции в одном и том же запросе идут по тому же соединению, однако автоматический менеджер случайным образом выбирает соединения из пула (гейзенбаг в Go, связанный с Postgres advisory locks).
Транзакции усугубляют данную проблему: базы данных часто привязывают функционал транзакции к соединению (иногда это называют session — сессией). Начав транзакцию, вы можете её зафиксировать (commit) или откатить (roll back) только по тому же соединению, на котором она изначально запускалась. Автоматическое управление пулом должно учитывать этот фактор с тем, чтобы не вернуть соединение в пул во время выполнения транзакции. В зависимости от базы данных другие функции, такие как блокировки и подготовленные выражения (prepared statements), также могут иметь привязку к соединениям.
Так что, если мы хотим писать эффективный код, нам необходимо знать, как работает пулинг соединений в используемом фреймворке, какие действия выполняются автоматически, и когда автоуправление не работает или его применение контрпродуктивно. Pooling-прокси (вроде pgBouncer, Odyssey или AWS RDS Proxy) — один из инструментов, помогающий забыть об этих нюансах. Эти системы позволяют создавать столько соединений с базой данных, сколько нужно, не заботясь об управлении ими, поскольку выделяемые соединения не настоящие, а их «дешёвая» имитация, не требующая значительных ресурсов. Когда клиент пытается воспользоваться одной из имитаций, pooling-прокси извлекает настоящее соединение из внутреннего пула и сопоставляет имитацию с реальным соединением. Когда прокси замечает, что соединение больше не используется, он оставляет открытым соединение с клиентом, но агрессивно освобождает и повторно использует соединение с базой данных. Число соединений и уровень «агрессивности» можно настроить, в том числе с учётом таких нюансов, как транзакции, подготовленные выражения и блокировки.
Решите ли вы оптимизировать свой код и включить в него эффективное управление соединениями, или выберете инструмент вроде pgBouncer, в конечном счёте будет определяться требуемым компромиссом между производительностью и сложностью развертывания. Любой вариант подойдёт в зависимости от того, сколько кода вы готовы написать, насколько удобно реализовано управление соединениями в используемом языке программирования и насколько эффективным должен быть проект.
Pooling не ограничивается клиентами баз данных. Мы называли соединения на стороне клиента «дешёвыми», но — увы — они не бесплатны. Они тоже используют память, порты и файловые дескрипторы на стороне клиента — ресурсы, которые ограничены. По этой причине многие языки/библиотеки имеют пулы для HTTP-соединений с одним и тем же сервером, а также используют пулы для других ограниченных ресурсов. Как правило, эти пулы скрыты от глаз до тех пор, пока система не исчерпает ограниченный ресурс, и в этот момент она обычно падает. Знание о подобной специфике сильно помогает при отладке — помимо соединений, виновниками проблем часто выступают файловые дескрипторы.
Настройка общих пулов
Теперь, когда мы увидели, как обычно обрабатываются соединения, можно поговорить о различных комбинациях сервер приложений + база данных и подумать о том, как максимизировать количество запросов, обрабатываемых на сервере приложений, минимизируя количество подключений к базе данных. Хотя эта статья покрывает не все возможные комбинации, большинство из них похожи по крайней мере на один из приведенных ниже примеров, так что понимание их принципов работы поможет разобраться с другими возможными комбинациями. Дайте мне знать, если хотите, чтобы я добавил больше комбинаций/систем.
1. Обработка на основе процессов и потоков
Puma, популярный сервер для запуска Ruby-приложений, предлагает пару механизмов для управления обработчиками входящих HTTP-запросов. Первый механизм — это число запускаемых процессов, представленное в конфигурации директивой workers. Каждый процесс сервера независим и загружает полный стек приложения в память. Таким образом, если приложение занимает N Мб памяти, необходимо убедиться, что на машине по крайней мере доступно workers×N Мб памяти для запуска всех копий. Есть способ смягчить эту проблему: Ruby 2+ поддерживает механизм копирования при записи (copy-on-write), позволяющий запускать множество процессов как один и потом разветвлять его на нужное число без необходимости копировать всю память — общие области памяти будут использоваться совместно до тех пор, пока в них не внесут изменения. Активация функции copy-on-write с помощью директивы preload_app! может помочь снизить потребление памяти по сравнению с приведенной выше формулой (workers×N). Однако не стоит возлагать на неё слишком большие надежды, не протестировав предварительно поведение при длительных нагрузках.
Серверы, основанные исключительно на процессах, такие как Unicorn, останавливаются на этом уровне конфигурации — как и популярные серверы для Python, PHP и других языков, использующие глобальную блокировку или построенные по схеме один поток/один процесс. Каждый процесс может обрабатывать один запрос за раз, но это не гарантирует полную загрузку: пока процесс дожидается выполнения запроса к БД или сетевого запроса к другому сервису, он не будет обрабатывать другие запросы, и соответствующее ядро процессора будет простаивать. Для устранения этой проблемы можно запустить больше процессов, чем имеется ядер CPU (что приведёт к издержкам, связанным с переключением контекста), или задействовать потоки.
Что приводит нас ко второму механизму, который предлагает Puma — числу потоков для запуска в каждом процессе/worker’e. Директива threads позволяет настроить минимальное и максимальное число потоков в пуле потоков каждого worker’а. Использование двух этих директив позволяет контролировать общее число потоков, которые будут действовать как параллельные обработчики запросов для приложения. Очевидно, оно равно произведению числа worker’ов на число потоков.
Эмпирическое правило состоит в том, что обычно на каждое доступное ядро CPU приходится по одному worker’у — при условии, конечно, что для этого достаточно памяти. Это позволяет эффективно использовать память, так что можно прикинуть, сколько RAM нужно, проведя несколько тестов с этим номером. Теперь желательно полностью задействовать CPU — сделать это можно, увеличивая максимальное количество потоков. Как помните, потоки используют память совместно с процессом, так что увеличение их числа не слишком отражается на потреблении памяти. Вместо этого большее число потоков будет всё сильнее загружать процессор, попутно позволяя обрабатывать больше запросов одновременно. Это разумно, поскольку, пока один поток спит в ожидании результатов запроса к БД или сетевого запроса, ядро CPU может переключиться на другой поток из того же процесса. Но помните, что большое число потоков приведёт к конкуренции за блокировки процессов, так что вам придется опытным путем установить, сколько потоков можно добавить для значимого увеличения производительности.
Но как все эта конфигурация влияет на количество соединений с базой данных? Rails использует автоматическое управление подключениями к базе данных, так что каждому запущенному потоку потребуется собственное подключение к базе данных для эффективной работы без ожидания, когда работу завершат другие потоки. Он поддерживает пул соединений (его настройки хранятся в файле database.yml и применяются на уровне процесса/worker’а). Таким образом, если оставить значение по умолчанию, равное 5, Rails будет поддерживать максимум 5 соединений для каждого worker’а. Такой лимит не слишком хорошо сработает, если изменить максимальное число потоков — все они будут бороться за эти пять соединений в пуле. Эмпирическое правило состоит в том, чтобы сделать число соединений в пуле равным максимальному количеству процессов, как рекомендовано в руководстве по развертыванию Heroku Puma.
Но тут возникает другая проблема: количество соединений, равное произведению числа worker’ов на число потоков (workers × threads), благотворно влияет на производительность сервера приложений, но совершенно не подходит для базы данных вроде PostgreSQL и, в некоторых случаях, для MySQL. В зависимости от того, сколько у вас оперативной памяти (в случае Postgres) и сколько CPU (в случае MySQL), данная конфигурация может не сработать. Можно уменьшить объём пула (pool) или значение threads, чтобы сократить число соединений, или число worker’ов, или оба этих параметра. В зависимости от конкретного приложения, все эти решения, вероятно, будут иметь один и тот же эффект: если каждый запрос требует обращений к базе данных, количество подключений к БД всегда будет выступать узким местом, ограничивая число запросов, которые реально обработать. Но если некоторые запросы могут обходиться без обращений к БД, то число worker’ов и потоков можно оставить высоким, а объём пула сделать относительно низким — в результате множество потоков будет обрабатывать запросы, но только небольшая их часть станет конкурировать за подключения к БД в пуле по мере необходимости.
Эту идею можно реализовать другим способом: перевести управление соединениями в код и убедиться, что они эффективно резервируются (checkout) и освобождаются (release). Это особенно важно, если между обращениями к БД делаются сетевые запросы.
Если станет понятно, что управлять соединениями должным образом или настраивать эти цифры затруднительно, и сервер приложений искусственно ограничен пределом на количество подключений к БД, можно будет воспользоваться инструментом вроде pgBouncer, Odyssey, AWS RDS Proxy. Запуск pooling-прокси позволит сделать размер пула равным максимальному числу потоков и попутно обеспечит уверенность в том, что прокси-сервер сделает все максимально эффективно.
Что касается баз данных, PostgreSQL использует обработчики на основе процессов, поэтому приходится проявлять сдержанность в отношении числа соединений при работе с этой БД. MySQL использует потоки, так что количество соединений можно увеличить — хотя это и способно привести к снижению производительности из-за переключения контекста и блокировок.
2. Обработка на основе цикла событий
Node / Deno — первый сервер на основе цикла событий (event loop), который мы рассмотрим. Подобный сервер с конфигурациями по умолчанию способен весьма эффективно использовать ядро процессора, на котором работает, но будет практически полностью игнорировать другие. Да, его внутренние подсистемы и библиотеки вполне могут задействовать и другие ядра, но сейчас мы больше заинтересованы в полезной нагрузке, и поможет в этом кластеризация (clustering). Она достигается путем запуска одного процесса, который принимает все входящие соединения, а затем действует как прокси-сервер и распределяет соединения по другим процессам, работающим на той же машине. У Node имеется модуль кластеризации, входящий в стандартную библиотеку, и популярные серверы вроде PM2 его активно используют. Основное правило здесь состоит в том, чтобы запускать столько же процессов, сколько ядер CPU доступно (конечно, при условии, что памяти достаточно).
Stripe выпустил интересный проект Einhorn, который представляет собой менеджер соединений, существующий вне стека, в котором пишется код. Он запускает собственный процесс, которые принимает соединения, и распределяет их по экземплярам приложения, которые запускает и которыми управляет как дочерними процессами. Инструмент вроде этого очень полезен в системах, основанных на циклах событий, поскольку при возможности всегда будет максимально полно задействовать ядро процессора. Однако сам по себе он не так полезен с Ruby/Python, поскольку, хотя и позволяет запускать несколько процессов, отсутствие потоков означает, что каждый из процессов может обслуживать только один запрос за раз.
Подход на основе кластеризации цикла событий используется системами, которые меняют поведение по умолчанию для языка, основанного на процессах. Сервер Tornado для Python, например, преобразовывает обработчик запросов Python в систему на основе цикла событий, также известную как non-blocking I/O. Также можно настроить его кластеризацию на все доступные ядра CPU (при условии достаточного количества памяти).
Аналогичный подход используется в веб-сервере Falcon для Ruby. В новых версиях Ruby имеется аналог зеленых потоков (green-thread), называемый файбером (fiber), и каждый входящий запрос Falcon обрабатывает с помощью одного такого Ruby-примитива. Файберы не могут автоматически использовать все ядра CPU, поэтому Falcon запускает копию приложения в каждом доступном ядре процессора (опять же, при условии, что памяти достаточно).
Во всех этих случаях нужно настроить пулы соединений в адаптерах БД с целью ограничить число подключений, доступное для каждого процесса. Также можно использовать pooling-прокси, если серверы приложений ограничены лимитом базы данных на соединения, или если управление выделением и высвобождением соединений становится затруднительным.
Redis обрабатывает соединения с помощью цикла событий. То есть он может поддерживать столько соединений, сколько имеется доступных портов, файловых дескрипторов и памяти, при этом каждая операция от каждого соединения обрабатывается по очереди.
3. Внутреннее управление / Кастомная обработка
Образцом в данном случае выступает Go, который, в отличие от всех других примеров, полностью свободен от каких-либо ограничений при обработке запросов — он может по-максимуму использовать доступные ресурсы CPU и памяти. Каждый входящий запрос обрабатывается новой горутиной (goroutine) — легковесной потокоподобной конструкцией. Исполняемая среда Go управляет ей, при этом планирование значительно опережает по эффективности аналогичные механизмы для потоков или процессов. Go автоматически «раскидывает» горутины по всем доступным ядрам CPU, хотя его аппетиты можно несколько обуздать, задав параметр runtime.GOMAXPROCS. Поскольку все происходит в рамках исполняемой среды, память не копируется. Go работает на всех ядрах CPU и не требует запуска отдельной копии приложения на каждом ядре.
Поскольку Go оптимизирован для работы с десятками тысяч параллельных запросов даже на совсем маленьких серверах, его сопряжение с базой данных на основе процессов, такой как PosgreSQL, можно сравнить с картинкой, на которой гоночный автомобиль на полной скорости врезается в кирпичную стену. Если каждая горутина использует SQL-пакет из стандартной библиотеки, будет создано столько соединений, сколько насчитывается горутин — ведь по умолчанию размер пула не ограничен.
Первое, что необходимо сделать с любым приложением на Go, работающем с SQL-базой, — задать предел соединений с помощью SetMaxOpenConns и связанных опций. Go использует внутренний пул с пакетом sql, поэтому каждый запрос будет получать соединение из пула, и сразу после завершения запроса оно будет возвращаться в пул. Другими словами, чтобы провести транзакцию, придется воспользоваться специальными методами, предоставляющими объект, который «обёртывает» открытое соединение, в котором транзакция была запущена — это единственный способ, позволяющий в дальнейшем зафиксировать данные или откатить транзакцию. Это имеет большое значение и при использовании других завязанных на соединение функций, таких как подготовленные выражения или рекомендательные блокировки (advisory locks).
Подобный автоматический подход даёт неожиданное преимущество: он устраняет необходимость в pooling-прокси, поскольку управление соединениями уже осуществляется крайне агрессивно. В вызовы БД по умолчанию встроено эффективное управление соединениями, и соединение используется так же эффективно, как и в pooling-прокси. Системы, устроенные подобным образом, по умолчанию работают эффективно, однако пользователю приходится разбираться с пограничными случаями, когда ручное управление действительно необходимо.
Помимо привычных нюансов, связанных с подготовленными выражениями и рекомендательными блокировками (advisory locks), подобное ручное управление соединениями также потенциально может приводить к проблемам, связанным со взаимной блокировкой. Если задано максимальное число соединений, и некоторым запросам для работы требуется более одного соединения, существует вероятность попадания в порочный круг, в котором запросы бесконечно дожидаются, пока другие запросы освободят соединения. В заметках об определении размера пула для HikariCP приводятся формулы, которые помогают справится с проблемами вроде этой.
Другие языки на основе VM, такие как Java, Scala/Akka, Clojure, Kotlin (все на JVM) и Elixir/Erlang (на BEAM VM) работают аналогичным образом: задействование всех доступных ядер CPU возможно без запуска отдельной копии приложения для каждого ядра. Реализация каждой конкретной системы или библиотеки подключений к БД обычно имеет свои тонкости, однако с ними легко разобраться, используя одну или несколько концепций, приведённых выше.
Свяжитесь со мной в Twitter, если хотите добавить примеры работы других систем, у вас есть какие-либо вопросы/замечания/пожелания, или вы обнаружили ошибку.











