Синхронные vs асинхронные операции
Любую операцию (вызов функции, запись в файл, сетевой поход, запрос к БД и т.д.) можно выполнить двумя способами: синхронно или асинхронно. Рассмотрим разницу между двумя подходами.
➡️ Синхронно - значит “с ожиданием завершения операции”. Обычно, это поведение по-умолчанию: программист пишет инструкции в программе, они выполняются процессором в том порядке, в котором написаны. Процессор не начинает выполнение следующей инструкции, пока не закончит предыдущую.
🔀 Асинхронно - значит “без ожидания завершения операции”. В этом случае работает подход “выстрелил и забыл” - ждать, когда прилетят наши “пули” часто бывает необязательно. В современных языках программирования есть множество механизмов, с помощью которых можно обеспечить асинхронное выполнение: дочерние процессы, треды, корутины, файберы, фьючеры и т.п. Результат операции при этом получить можно, но не сразу, а “когда-нибудь потом”.
🤔 Зачем вообще нужно асинхронное выполнение? Давайте сравним два подхода и определим, чем отличаются их свойства.
1️⃣ Асинхронное выполнение тесно связано с таким свойством ПО, как многозадачность. Это значит, что несколько задач должны выполняться параллельно. С точки зрения программиста, для этого нужно запустить несколько операций без ожидания их заверщения, то есть асинхронно. Так что, когда ОС одновременно запускает несколько программ или web-браузер скачивает параллельно несколько ресурсов, под капотом там работает асинхронный код.
2️⃣ Асинхронный код может эффективнее обрабатывать блокировки. Для быстрых операций вроде сложения или доступа к оперативной памяти асинхронное выполнение часто не имеет смысла. Но есть операции медленные: сетевые походы, работа с жёстким диском, работа с мьютексами. В этих операциях часто нужно подождать, пока система не будет готова к выполнению операции. При этом происходит блокировка выполнения - система простаивает во время ожидания. В случае синхронного выполнения простой неизбежен, а в случае асинхронного - из множества параллельных задач можно выполнить любую, готовую к выполнению, и избежать простоя.
3️⃣ Как следствие, асинхронного ПО использует аппаратное обеспечение более эффективно, т.к. снижается время простоя в ожидании блокировок.
4️⃣ При всех преимуществах асинхронного ПО, у него есть один существенный недостаток: код становится сложнее. Программисту надо решать:
- Какие задачи надо выполнять асинхронно?
- Какие механизмы ЯП и ОС для этого выбрать?
- Как обеспечить синхронизацию задач (переход от асинхронного обратно к синхронному выполнению)?
Всё это усложняет чтение асинхронного кода, делает его поддержку более дорогой и может приводить к появлению целых классов новых ошибок.
⚖️ Вот такой вот интересный трейдоф: производительность vs простота. Что надо запомнить:
- Синхронное и асинхронное выполнение - это фундаментальная концепция. С ней можно встретиться в разных областях IT и за пределами отрасли.
- Асинхронное выполнение имеет смысл для тяжёлых операций. Для них асинхронность может повысить эффективность использования ресурсов.
- Повышение производительности не бесплатное: за него мы платим сложностью системы.
Ставь огонёк, если интересна тема асинхронщины и параллельного выполнения кода 🔥
#theory #concurrency #pattern
Любую операцию (вызов функции, запись в файл, сетевой поход, запрос к БД и т.д.) можно выполнить двумя способами: синхронно или асинхронно. Рассмотрим разницу между двумя подходами.
➡️ Синхронно - значит “с ожиданием завершения операции”. Обычно, это поведение по-умолчанию: программист пишет инструкции в программе, они выполняются процессором в том порядке, в котором написаны. Процессор не начинает выполнение следующей инструкции, пока не закончит предыдущую.
🔀 Асинхронно - значит “без ожидания завершения операции”. В этом случае работает подход “выстрелил и забыл” - ждать, когда прилетят наши “пули” часто бывает необязательно. В современных языках программирования есть множество механизмов, с помощью которых можно обеспечить асинхронное выполнение: дочерние процессы, треды, корутины, файберы, фьючеры и т.п. Результат операции при этом получить можно, но не сразу, а “когда-нибудь потом”.
🤔 Зачем вообще нужно асинхронное выполнение? Давайте сравним два подхода и определим, чем отличаются их свойства.
1️⃣ Асинхронное выполнение тесно связано с таким свойством ПО, как многозадачность. Это значит, что несколько задач должны выполняться параллельно. С точки зрения программиста, для этого нужно запустить несколько операций без ожидания их заверщения, то есть асинхронно. Так что, когда ОС одновременно запускает несколько программ или web-браузер скачивает параллельно несколько ресурсов, под капотом там работает асинхронный код.
2️⃣ Асинхронный код может эффективнее обрабатывать блокировки. Для быстрых операций вроде сложения или доступа к оперативной памяти асинхронное выполнение часто не имеет смысла. Но есть операции медленные: сетевые походы, работа с жёстким диском, работа с мьютексами. В этих операциях часто нужно подождать, пока система не будет готова к выполнению операции. При этом происходит блокировка выполнения - система простаивает во время ожидания. В случае синхронного выполнения простой неизбежен, а в случае асинхронного - из множества параллельных задач можно выполнить любую, готовую к выполнению, и избежать простоя.
3️⃣ Как следствие, асинхронного ПО использует аппаратное обеспечение более эффективно, т.к. снижается время простоя в ожидании блокировок.
4️⃣ При всех преимуществах асинхронного ПО, у него есть один существенный недостаток: код становится сложнее. Программисту надо решать:
- Какие задачи надо выполнять асинхронно?
- Какие механизмы ЯП и ОС для этого выбрать?
- Как обеспечить синхронизацию задач (переход от асинхронного обратно к синхронному выполнению)?
Всё это усложняет чтение асинхронного кода, делает его поддержку более дорогой и может приводить к появлению целых классов новых ошибок.
⚖️ Вот такой вот интересный трейдоф: производительность vs простота. Что надо запомнить:
- Синхронное и асинхронное выполнение - это фундаментальная концепция. С ней можно встретиться в разных областях IT и за пределами отрасли.
- Асинхронное выполнение имеет смысл для тяжёлых операций. Для них асинхронность может повысить эффективность использования ресурсов.
- Повышение производительности не бесплатное: за него мы платим сложностью системы.
Ставь огонёк, если интересна тема асинхронщины и параллельного выполнения кода 🔥
#theory #concurrency #pattern
Утилизация ресурсов сервера
Нет, речь не про переработку отходов 😊 У слова утилизация в контексте серверной разработки есть ещё одно значение: эффективность использования ресурсов.
👀 Утилизация считается по формуле:
🤔 Почему это важно? Во-первых, утилизация тесно связана с экономической эффективностью. Если в вашей системе тонна ресурсов, которыми никто не пользуется, то говорят, что система недоутилизирована. В контексте бэкенда это значит, что мы купили кучу дорогущих серверов и не пользуемся ими, можно было бы и не покупать 😊
😮 Так значит надо максимизировать утилизацию! Нет, не значит 🙅 Когда утилизация приближается к 100%, в системе может наступить дефицит ресурсов. Это может привести к снижению качества предоставляемого сервиса или полному отказу в обслуживании. На этом эффекте, например, основаны широко известные DoS-атаки.
❗️ За утилизацией аппаратных ресурсов в системе надо следить. При этом за каждым ресурсом нужно следить по-отдельности, т.к. дефицит любого ресурса может привести к сбоям. Какие ресурсы есть на сервере?
1️⃣ Оперативная память - следим за объёмом доступной памяти. Не забываем, что современные ОС используют свободную оперативку под файловые кеши. Память, занятую этими кешами, часто тоже можно считать “свободной”.
2️⃣ Диски - тут чуть сложнее, надо следить не только за размером свободного места, но и за iops’ами. Диски могут выполнить ограниченное количество операций ввода/вывода за единицу времени, поэтому важно не только количество хранимых данных, но и частота их обновления на диске.
3️⃣ Сеть - следим за пропускной способностью, сколько данных мы можем прокачать через сеть за единицу времени. Не забываем, что на сети пропускная способность измеряется в гигабитах/сек - не в гигабайтах/сек. В 8 раз ошибаться не надо 😊
4️⃣ CPU (и другие вычислители при наличии: GPU, APU, аппаратные ускорители) - тут всё сложнее. Потребление процессора обычно измеряется в процентах: какую долю времени процессор занят вычислениями (т.е. не простаивает). Расчёт утилизации усложняется коэффициентами. Современные процессоры многоядерные - каждое ядро может вести вычисления независимо от других ядер (утилизация у него тоже независимая). Современные ядра поддерживают гипертрединг - это когда 1 ядро может выполнять несколько (обычно 2) независимых потока вычислений. Лимит вычислительных ресурсов можно посчитать так:
💎 Мониторинг аппаратных ресурсов нужен всем, поэтому в опенсорсе для этого есть готовые инструменты. Свои тулзы изобретать ну нужно, лучше поставьте себе Node Exporter.
👨💻 Для программиста мониторинг утилизации хардварных ресурсов не менее важен, чем для сисадмина. Там можно увидеть 2 класса проблем с производительностью:
1️⃣ Система не может принимать новых клиентов, но хардвар недоутилизирован - программист написал немасштабируемый код.
2️⃣ Поделите утилизацию ресурса на кол-во полезных операций. Если числа слишком большие - программист написал неэффективный код.
🔥 В высоконагруженных системах обычно стараются поддерживать такую утилизацию, чтобы система могла пережить рост нагрузки x2. Это число не выбито в камне и может меняться в зависимости от требований бизнеса. Однако, вне зависимости от желаемого режима работы вашей системы, мониторинг и контроль утилизации аппаратных ресурсов обязателен для всех серверных систем.
#theory #concurrency #highload #bestpractice
Нет, речь не про переработку отходов 😊 У слова утилизация в контексте серверной разработки есть ещё одно значение: эффективность использования ресурсов.
👀 Утилизация считается по формуле:
<утилизация (%)> = <потребление ресурса> / <кол-во ресурса в системе> * 100
. Давайте на примере. Пусть в вашем сервере 32 GB
оперативной памяти. На нём запущено 1 приложение, которое потребляет 4 GB
. Тогда утилизация памяти вашего сервера 4 / 32 * 100 = 12.5%
.🤔 Почему это важно? Во-первых, утилизация тесно связана с экономической эффективностью. Если в вашей системе тонна ресурсов, которыми никто не пользуется, то говорят, что система недоутилизирована. В контексте бэкенда это значит, что мы купили кучу дорогущих серверов и не пользуемся ими, можно было бы и не покупать 😊
😮 Так значит надо максимизировать утилизацию! Нет, не значит 🙅 Когда утилизация приближается к 100%, в системе может наступить дефицит ресурсов. Это может привести к снижению качества предоставляемого сервиса или полному отказу в обслуживании. На этом эффекте, например, основаны широко известные DoS-атаки.
❗️ За утилизацией аппаратных ресурсов в системе надо следить. При этом за каждым ресурсом нужно следить по-отдельности, т.к. дефицит любого ресурса может привести к сбоям. Какие ресурсы есть на сервере?
1️⃣ Оперативная память - следим за объёмом доступной памяти. Не забываем, что современные ОС используют свободную оперативку под файловые кеши. Память, занятую этими кешами, часто тоже можно считать “свободной”.
2️⃣ Диски - тут чуть сложнее, надо следить не только за размером свободного места, но и за iops’ами. Диски могут выполнить ограниченное количество операций ввода/вывода за единицу времени, поэтому важно не только количество хранимых данных, но и частота их обновления на диске.
3️⃣ Сеть - следим за пропускной способностью, сколько данных мы можем прокачать через сеть за единицу времени. Не забываем, что на сети пропускная способность измеряется в гигабитах/сек - не в гигабайтах/сек. В 8 раз ошибаться не надо 😊
4️⃣ CPU (и другие вычислители при наличии: GPU, APU, аппаратные ускорители) - тут всё сложнее. Потребление процессора обычно измеряется в процентах: какую долю времени процессор занят вычислениями (т.е. не простаивает). Расчёт утилизации усложняется коэффициентами. Современные процессоры многоядерные - каждое ядро может вести вычисления независимо от других ядер (утилизация у него тоже независимая). Современные ядра поддерживают гипертрединг - это когда 1 ядро может выполнять несколько (обычно 2) независимых потока вычислений. Лимит вычислительных ресурсов можно посчитать так:
<кол-во процессоров> * <кол-во ядер> * <коэффициент гипертрединга> * 100%
Так что если вы увидите, что сервер жрёт 1000% СPU, не пугайтесь - это значит, что вы утилизировали 10 ядер.💎 Мониторинг аппаратных ресурсов нужен всем, поэтому в опенсорсе для этого есть готовые инструменты. Свои тулзы изобретать ну нужно, лучше поставьте себе Node Exporter.
👨💻 Для программиста мониторинг утилизации хардварных ресурсов не менее важен, чем для сисадмина. Там можно увидеть 2 класса проблем с производительностью:
1️⃣ Система не может принимать новых клиентов, но хардвар недоутилизирован - программист написал немасштабируемый код.
2️⃣ Поделите утилизацию ресурса на кол-во полезных операций. Если числа слишком большие - программист написал неэффективный код.
🔥 В высоконагруженных системах обычно стараются поддерживать такую утилизацию, чтобы система могла пережить рост нагрузки x2. Это число не выбито в камне и может меняться в зависимости от требований бизнеса. Однако, вне зависимости от желаемого режима работы вашей системы, мониторинг и контроль утилизации аппаратных ресурсов обязателен для всех серверных систем.
#theory #concurrency #highload #bestpractice
Потоки vs процессы: масштабирование по ядрам CPU
Это база - спрашиваю на каждом собеседовании 😉 С ростом нагрузки на систему становится критически важно грамотно утилизировать аппаратные ресурсы. Сегодня обсудим CPU, про него имеет смысл говорить отдельно, т.к. грамотная утилизация процессора требует определённой квалификации от программиста.
🤔 Сколько процессора утилизирует этот код?
В абсолютных числах - 1 поток. В процентах - зависит от машины, на которой код запущен. Надо поделить 1 на количество ядер процессора, помноженное на коэффициент гипертрединга. Подробнее писал в посте про утилизацию.
😮 Даже самый неоптимальный код по-умолчанию не может утилизировать все вычислительные ресурсы сервера, об этом должен позаботиться программист.
📖 Доступом к процессору, как и ко всем другим аппаратным ресурсам, управляет операционная система. Современные процессоры - многоядерные, ядра процессора могут выполнять вычисления параллельно, независимо друг от друга. Чтобы приложения могли использовать эту возможность, ОС предоставляет асинхронный API для параллельных вычислений, в основе которого лежит 2 концепции: потоки и процессы.
▶️ Процесс (process) - это запущенное приложение. У каждого процесса есть эксклюзивный доступ к ресурсам: аллоцированной памяти, файловым дескрипторам и т.д. Процессы максимально изолированы: они не имеют доступа к ресурсам друг друга и обладают независимым жизненным циклом. Любое взаимодействие между процессами требует написания кода с использованием специальных механизмов IPC (inter-process communication) - файлов, пайпов, сокетов, сигналов, и других.
➡️ Поток (thread) - это последовательность вычислений. У каждого процесса под капотом по-умолчанию есть 1 поток, все вычисления выполняются в нём последовательно. Ресурсы внутри процесса общие для всех потоков. Если кто-то открыл файл или аллоцировал память, все потоки могут этот ресурс прочитать и записать без необходимости использовать какие-то дополнительные механизмы.
🔀 По-настоящему параллельные вычисления, с утилизацией нескольких ядер CPU, можно организовать двумя способами:
- запустив дополнительные потоки
- создав дочерние процессы
☝️ Ключевая разница между двумя способами в разграничении доступа к ресурсам: у процессов эксклюзивные ресурсы, у потоков общие.
📈 Максимальной производительности можно достичь, когда кол-во запущенных потоков в системе равняется кол-ву потоков в архитектуре CPU. В этом случае каждый поток может выполняться непрерывно.
🤔 Зачем знать низкоуровневые API в 2024? У нас же есть языки программирования высокого уровня со всевозможными
⚠️ Дело в том, что ОС кроме процессов и тредов других асинхронных моделей не знает. Поэтому, если вам нужно больше 1 ядра CPU, понимание и использование низкоуровневых API необходимо. Например:
- В
- В
- В
- А в
🔥 Не забывайте про процессы и потоки - это фундамент, на котором стоят все параллельные вычисления. А параллельные вычисления - ключ к высокой производительности 😉
#theory #Linux #concurrency #tools #coding #pattern #highload
Это база - спрашиваю на каждом собеседовании 😉 С ростом нагрузки на систему становится критически важно грамотно утилизировать аппаратные ресурсы. Сегодня обсудим CPU, про него имеет смысл говорить отдельно, т.к. грамотная утилизация процессора требует определённой квалификации от программиста.
🤔 Сколько процессора утилизирует этот код?
while (true) {}
😮 Даже самый неоптимальный код по-умолчанию не может утилизировать все вычислительные ресурсы сервера, об этом должен позаботиться программист.
📖 Доступом к процессору, как и ко всем другим аппаратным ресурсам, управляет операционная система. Современные процессоры - многоядерные, ядра процессора могут выполнять вычисления параллельно, независимо друг от друга. Чтобы приложения могли использовать эту возможность, ОС предоставляет асинхронный API для параллельных вычислений, в основе которого лежит 2 концепции: потоки и процессы.
▶️ Процесс (process) - это запущенное приложение. У каждого процесса есть эксклюзивный доступ к ресурсам: аллоцированной памяти, файловым дескрипторам и т.д. Процессы максимально изолированы: они не имеют доступа к ресурсам друг друга и обладают независимым жизненным циклом. Любое взаимодействие между процессами требует написания кода с использованием специальных механизмов IPC (inter-process communication) - файлов, пайпов, сокетов, сигналов, и других.
➡️ Поток (thread) - это последовательность вычислений. У каждого процесса под капотом по-умолчанию есть 1 поток, все вычисления выполняются в нём последовательно. Ресурсы внутри процесса общие для всех потоков. Если кто-то открыл файл или аллоцировал память, все потоки могут этот ресурс прочитать и записать без необходимости использовать какие-то дополнительные механизмы.
🔀 По-настоящему параллельные вычисления, с утилизацией нескольких ядер CPU, можно организовать двумя способами:
- запустив дополнительные потоки
- создав дочерние процессы
☝️ Ключевая разница между двумя способами в разграничении доступа к ресурсам: у процессов эксклюзивные ресурсы, у потоков общие.
📈 Максимальной производительности можно достичь, когда кол-во запущенных потоков в системе равняется кол-ву потоков в архитектуре CPU. В этом случае каждый поток может выполняться непрерывно.
🤔 Зачем знать низкоуровневые API в 2024? У нас же есть языки программирования высокого уровня со всевозможными
async/await
, go func () { }
и другими высокоуровневыми асинхронными API.⚠️ Дело в том, что ОС кроме процессов и тредов других асинхронных моделей не знает. Поэтому, если вам нужно больше 1 ядра CPU, понимание и использование низкоуровневых API необходимо. Например:
- В
go
есть GOMAXPROCS- В
Node.js
есть --v8-pool-size- В
Python
есть threading и multiprocessing- А в
C/C++
можно использовать fork(2) и pthread_create(3) напрямую🔥 Не забывайте про процессы и потоки - это фундамент, на котором стоят все параллельные вычисления. А параллельные вычисления - ключ к высокой производительности 😉
#theory #Linux #concurrency #tools #coding #pattern #highload
UNIX - сигналы
Рассмотрим UNIX сигналы - механизм ОС для управления процессами. Сигналы используются для:
- системного администрирования;
- обработки исключений;
- межпроцессного взаимодействия.
Я расскажу, что такое сигналы, как написать свой обработчик и как применить их на практике.
Сигнал уведомляет процесс о наступлении события. При получении сигнала процесс вызывает обработчик сигнала, прервав нормальный поток выполнения. Обычно, нормальный поток выполнения продолжается после вызова обработчика с того места, где был прерван. Но бывают исключения. Например:
1️⃣ Запустим в терминале бесконечный цикл:
2️⃣ Нажмём
Послать сигнал можно несколькими способами:
- процессу - с помощью программы или сисколла
- группе процессов - с помощью библиотечного вызова
- отдельному потоку - с помощью сисколла
Обработчик сигнала выполняется вне очереди, даже в однопоточном приложении. Сигналы - самый простой способ выполнить задачу вне очереди. Список стандартных сигналов и обработчиков по умолчанию ищи в мануале.
Программист может определить свои обработчики. Для управления обработчиками используй вызов sigaction. Он может установить кастомный обработчик, вернуть обработчик по умолчанию, включить игнор сигнала.
⚠️ Будь осторожен при написании кастомных обработчиков - можно легко получить неопределённое поведение. Чтобы этого избежать, используй в обработчиках только
В заключение, рассмотрим кейсы использования сигналов:
- Nginx:
- Tarantool:
- Kubernetes: при завершении пода посылает
Сигналы незаменимы в разработке под Linux. Надеюсь, в твоём тулсете стало на 1 инструмент больше 🔥
#theory #coding #tools #Linux #concurrency
===
Мои любимые посты в канале t.me/uimindev/37
Рассмотрим UNIX сигналы - механизм ОС для управления процессами. Сигналы используются для:
- системного администрирования;
- обработки исключений;
- межпроцессного взаимодействия.
Я расскажу, что такое сигналы, как написать свой обработчик и как применить их на практике.
Сигнал уведомляет процесс о наступлении события. При получении сигнала процесс вызывает обработчик сигнала, прервав нормальный поток выполнения. Обычно, нормальный поток выполнения продолжается после вызова обработчика с того места, где был прерван. Но бывают исключения. Например:
1️⃣ Запустим в терминале бесконечный цикл:
while true; do echo "Looping..."; done
2️⃣ Нажмём
Ctrl + C
- цикл завершится. Эта комбинация клавиш отправляет сигнал SIGINT
. Обработчик по умолчанию для этого сигнала - завершить процесс, поэтому нормальный поток в этом случае не был продолжен.Послать сигнал можно несколькими способами:
- процессу - с помощью программы или сисколла
kill
;- группе процессов - с помощью библиотечного вызова
killpg
;- отдельному потоку - с помощью сисколла
tkill
или библиотечного вызова pthread_kill
.Обработчик сигнала выполняется вне очереди, даже в однопоточном приложении. Сигналы - самый простой способ выполнить задачу вне очереди. Список стандартных сигналов и обработчиков по умолчанию ищи в мануале.
Программист может определить свои обработчики. Для управления обработчиками используй вызов sigaction. Он может установить кастомный обработчик, вернуть обработчик по умолчанию, включить игнор сигнала.
⚠️ Будь осторожен при написании кастомных обработчиков - можно легко получить неопределённое поведение. Чтобы этого избежать, используй в обработчиках только
async-signal-safe
функции стандартной библиотеки. Список безопасных функций можно найти здесь.В заключение, рассмотрим кейсы использования сигналов:
- Nginx:
SIGUSR1
ротирует логи, SIGUSR2
запускает graceful restart, SIGQUIT
запускает graceful shutdown, SIGHUP
перечитывает конфиги - см. тут;- Tarantool:
SIGUSR1
откладывает снапшот базы данных - см. тут;- Kubernetes: при завершении пода посылает
SIGTERM
для запуска graceful shutdown и SIGKILL
для принудительного завершения - см. тут.Сигналы незаменимы в разработке под Linux. Надеюсь, в твоём тулсете стало на 1 инструмент больше 🔥
#theory #coding #tools #Linux #concurrency
===
Мои любимые посты в канале t.me/uimindev/37