Уймин - про разработку
190 subscribers
3 photos
1 file
40 links
Авторский канал про backend-разработку. Подробнее - в закрепллённом сообщении.

Личиный аккаунт: @maksimuimin
Download Telegram
Синхронные vs асинхронные операции

Любую операцию (вызов функции, запись в файл, сетевой поход, запрос к БД и т.д.) можно выполнить двумя способами: синхронно или асинхронно. Рассмотрим разницу между двумя подходами.

➡️ Синхронно - значит “с ожиданием завершения операции”. Обычно, это поведение по-умолчанию: программист пишет инструкции в программе, они выполняются процессором в том порядке, в котором написаны. Процессор не начинает выполнение следующей инструкции, пока не закончит предыдущую.

🔀 Асинхронно - значит “без ожидания завершения операции”. В этом случае работает подход “выстрелил и забыл” - ждать, когда прилетят наши “пули” часто бывает необязательно. В современных языках программирования есть множество механизмов, с помощью которых можно обеспечить асинхронное выполнение: дочерние процессы, треды, корутины, файберы, фьючеры и т.п. Результат операции при этом получить можно, но не сразу, а “когда-нибудь потом”.

🤔 Зачем вообще нужно асинхронное выполнение? Давайте сравним два подхода и определим, чем отличаются их свойства.

1️⃣ Асинхронное выполнение тесно связано с таким свойством ПО, как многозадачность. Это значит, что несколько задач должны выполняться параллельно. С точки зрения программиста, для этого нужно запустить несколько операций без ожидания их заверщения, то есть асинхронно. Так что, когда ОС одновременно запускает несколько программ или web-браузер скачивает параллельно несколько ресурсов, под капотом там работает асинхронный код.

2️⃣ Асинхронный код может эффективнее обрабатывать блокировки. Для быстрых операций вроде сложения или доступа к оперативной памяти асинхронное выполнение часто не имеет смысла. Но есть операции медленные: сетевые походы, работа с жёстким диском, работа с мьютексами. В этих операциях часто нужно подождать, пока система не будет готова к выполнению операции. При этом происходит блокировка выполнения - система простаивает во время ожидания. В случае синхронного выполнения простой неизбежен, а в случае асинхронного - из множества параллельных задач можно выполнить любую, готовую к выполнению, и избежать простоя.

3️⃣ Как следствие, асинхронного ПО использует аппаратное обеспечение более эффективно, т.к. снижается время простоя в ожидании блокировок.

4️⃣ При всех преимуществах асинхронного ПО, у него есть один существенный недостаток: код становится сложнее. Программисту надо решать:
- Какие задачи надо выполнять асинхронно?
- Какие механизмы ЯП и ОС для этого выбрать?
- Как обеспечить синхронизацию задач (переход от асинхронного обратно к синхронному выполнению)?
Всё это усложняет чтение асинхронного кода, делает его поддержку более дорогой и может приводить к появлению целых классов новых ошибок.

⚖️ Вот такой вот интересный трейдоф: производительность vs простота. Что надо запомнить:
- Синхронное и асинхронное выполнение - это фундаментальная концепция. С ней можно встретиться в разных областях IT и за пределами отрасли.
- Асинхронное выполнение имеет смысл для тяжёлых операций. Для них асинхронность может повысить эффективность использования ресурсов.
- Повышение производительности не бесплатное: за него мы платим сложностью системы.

Ставь огонёк, если интересна тема асинхронщины и параллельного выполнения кода 🔥

#theory #concurrency #pattern
Утилизация ресурсов сервера

Нет, речь не про переработку отходов 😊 У слова утилизация в контексте серверной разработки есть ещё одно значение: эффективность использования ресурсов.

👀  Утилизация считается по формуле: <утилизация (%)> = <потребление ресурса> / <кол-во ресурса в системе> * 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, про него имеет смысл говорить отдельно, т.к. грамотная утилизация процессора требует определённой квалификации от программиста.

🤔 Сколько процессора утилизирует этот код?
while (true) {}

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

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

📖 Доступом к процессору, как и ко всем другим аппаратным ресурсам, управляет операционная система. Современные процессоры - многоядерные, ядра процессора могут выполнять вычисления параллельно, независимо друг от друга. Чтобы приложения могли использовать эту возможность, ОС предоставляет асинхронный 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️⃣ Запустим в терминале бесконечный цикл:
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