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

Личиный аккаунт: @maksimuimin
Download Telegram
Анализируем соединения netstat

Работа с сетевыми соединениями лежит в основе серверной разработки. Важность этой темы сложно переоценить, однако ни в универе, ни на курсах дополнительного образования никто толком не рассказывал, как отлаживать работу с сетью 😅

Для меня открытием стала утилита netstat, она позволяет ответить на вопросы:
- Какая программа куда ходит по сети?
- Какая программа какой порт слушает?

Дисклеймер 1: приведённые команды работают на Linux и основаны на GNU-версии утилиты. На MacOS стоит BSD-версия netstat, он сильно отличается от GNU, поэтому на маке мои команды работать не будут 😞 Вместо netstat можно использовать lsof.

Дисклеймер 2: чтобы видеть PID’ы и имена программ в выхлопе netstat, нужно чтобы юзер, выполняющий команду, был овнером сокета или суперюзером. В общем, для лучшей наглядности выполняйте команду от рута 😉

1️⃣ Вывести список всех активных TCP-соединений:
netstat -natp


2️⃣ Вывести список процессов, которые слушают порт (готовы принимать входящие TCP-соединения):
netstat -natp | fgrep LISTEN


или то же самое, но дополнительно вывести имена столбцов в табличке:
netstat -natp | head -2 && netstat -natp | fgrep LISTEN


3️⃣ Вывести список установленных соединений по процессу (как входящих, так и исходящих):
netstat -natp | fgrep ESTABLISHED | fgrep <pid | program name>


4️⃣ Какой на машине есть UDP-трафик:
netstat -naup


А как вы дебажите работу с сетью? Напишите в комменты!
Ставьте огонёк, если было полезно 🔥

#hardskills #tools #Linux #networks #debug
🔥3👍1
Что такое контейнеры и зачем они нужны? (часть 1)

Есть 2 группы бэкендеров: те, кто ещё ничего не знает о контейнерах, и те, кто применяет их везде, где только можно. Что это за технология и почему стала такой популярной? 🧐

Контейнер - это изолированный процесс ОС.

Это простое для запоминания определение, но оно мало что говорит о сути технологии. Предлагаю рассмотреть основные свойства контейнеров, чтобы семантика слова “изолированный” стала понятнее:

1️⃣ Контейнер видит только своё поддерево файлов, см. chroot

2️⃣ Все файлы, с которыми работает контейнер, находятся во временной директории, которая автоматически удалится после завершения контейнеризованного процесса, см. mktemp

3️⃣ У контейнера свой набор переменных окружения, см. execve

4️⃣ Контейнер можно (и нужно!) запускать от имени непривилегированного пользователя, см. setuid/setgid

5️⃣ Даже если контейнер запущен от рута, у него часто ограничен набор разрешённых системных вызовов, см. capabilities

6️⃣ PID контейнеризованного процесса всегда равен 1, при этом изнутри контейнера не видны другие процессы - только дочерние процессы контейнера, см. pid_namespaces

7️⃣ На работу с сетью тоже накладываются ограничения - контейнер может иметь свои фаерволы, выделенный IP, доступ к сетевым девайсам сервера может быть ограничен белым списком, hostname контейнера может отличаться от хостовой машины, см. network_namespaces, uts_namespaces

8️⃣ Ещё на контейнер могут быть наложены ограничения на потребление основных хардварных ресурсов сервера: оперативную память, и процессорное время, см. cgroups

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

Контейнер - довольно элегантная абстракция, наверное именно поэтому они стали так популярны

Однако, когда начинаешь рассматривать свойства контейнеров внимательно, выясняется, что это довольно сложная технология, обеспеченная множеством хитрых механизмов ОС ⚙️

В части 2 рассмотрим преимущества и недостатки технологии и обсудим где её можно применять. Подпишитесь на канал, чтобы не пропустить!

А вы используете контейнеры? Напишите об этом в комментарии 👇
Ставьте огонёк, если узнали что-то новое 🔥

#theory #containers #tools #Linux
🔥41
Что такое контейнеры и зачем они нужны? (часть 2)

В части 1 мы рассмотрели, что такое контейнеры и какими свойствами они обладают. Вторую часть предлагаю начать с того, чтобы кратко рассмотреть жизненный цикл контейнера. Допустим, мы решили запустить какое-то свое ПО в контейнере, с чего нам начать? 🧐

1️⃣ Нам нужен имейдж. Имейдж - это архив, в который мы запакуем информацию об окружении (файлы, переменные окружения и т.п.), в котором будет запущено наше ПО. Сборка имейджа - первый этап жизненного цикла контейнера.

2️⃣ Имейдж надо отправить в реджистри. Реджистри - это файловый сервер, который хранит библиотеку собранных имейджей.

3️⃣ На машине, на которой мы хотим запустить контейнер, надо скачать имейдж из реджистри.

4️⃣ Теперь контейнер можно запустить. Как мы узнали из первой части, чтобы это сделать, надо выполнить десяток разных системных вызовов. Было бы чрезвычайно утомительно делать это каждый раз вручную, поэтому в нашей истории появляется специальная программа, которая сделает это за нас - контейнерный рантайм. Даём ему на вход имейдж, получаем на выходе запущенный контейнер. О, и ещё хорошая новость: имейджи и разные контейнерные рантаймы совместимы между собой благодаря стандартам OCI, давайте скажем коллегам спасибо 🤝

Плюсы контейнеров:

Поставляем ПО на продакшен сразу вместе с окружением.

↳ Деньги - единый подход к поставке ПО должен снижать стоимость эксплуатации.

Контейнеры изолированы, поэтому не конфликтуют между собой;

↳ Снова деньги - изолированные контейнеры можно “плотнее” набивать на сервера, так повышается утилизация железа ⇒ снова снижается стоимость эксплуатации ПО.

Безопасность - повышение изоляции компонентов усложняет проникновение злоумышленника в систему.

💡 Именно из-за экономической эффективности, как мне кажется, IT-бизнес по всему миру драйвит переход на контейнерные и облачные технологии.

Минусы контейнеров:

Не все приложения готовы к контейнерам - если вам нужно хранить состояние на диске (например, вы хотите запустить СУБД), или нужен какой-то специфичный девайс (например, GPU), или вы хотите выполнять экзотические системные вызовы - заехать в контейнеры может быть проблематично. Btw, вот тут есть список лучших практик, чтобы с контейнерами не было проблем.

Сложность системы сильно возрастает. Вся та изоляция, накрученная вокруг ПО, усложняет жизнь не только “злоумышленнику”, но и разработчику с админом. Когда что-то ломается, разобраться в этом - отдельный квест.

Необходимость в оркестрации - чтобы максимизировать экономический эффект от внедрения контейнеров, надо вместе с ними внедрять системы оркестрации, такие как Kubernetes. Это ещё сильнее усложняет систему.

В заключение, давайте рассмотрим варианты использования контейнеров:

☁️ Облачные инсталляции ПО

💻 Разработка ПО на локальном компьютере - контейнеры относительно удобно можно тестировать локально. Это работает как под Linux, так и под MacOS, и даже на Windows можно попробовать.

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

Как вы думаете, какое будущее ждёт контейнерные технологии? Напишите об этом в комментарии 👇
Ставьте огонёк, если контейнеры топ 🔥

#theory #containers #tools #Linux
🔥2👍1
Всё есть файл

Эта широко известная концепция лежит в основе UNIX-подобных операционных систем. Но что это значит? 🤔

📍 Начнём с того, что такое файл. Каноническое определение такое:
Файл - это именованная последовательность байт

☝️ Другими словами, файл - это фрагмент информации, который можно идентифицировать.

ℹ️ Под “всё есть файл” имеется в виду, что разные по своей сути фрагменты информации обладают одинаковым интерфейсом доступа и сходными свойствами.

🤔 Что за “разные фрагменты информации”?

1️⃣ Данные на жёстком диске - то, что пользователям привычнее всего называть словом “файл”.

2️⃣ Директории (каталоги, папки) - тоже разновидность файлов.

3️⃣ Сетевые соединения - обладают теми же свойствами, что и открытые файлы.

4️⃣ Стандартные потоки ввода/вывода

5️⃣ Интерфейсы межпроцессного взаимодействия - пайпы, сокеты

6️⃣ Интерфейсы взаимодействия с железом - девайсы

7️⃣ Даже с сигналами можно работать как с файлами 0_o

🤔 Окей, а чего между всеми этими сущностями общего, что за “одинаковый интерфейс и сходные свойства”?

1️⃣ Общий жизненный цикл:
- Cначала надо открыть файл (open, opendir, connect, pipe и т.п.)
- Потом с ним можно будет работать
- В конце его надо закрыть (close)

2️⃣ Каждый открытый файл идентифицируется числом - файловым дескриптором. Вне зависимости от типа файла, в ядре ОС все файловые дескрипторы процесса хранятся в одной таблице.

3️⃣ В процессе работы с открытым файлом для ввода/вывода информации мы используем одни и те же инструменты:
- read/write - прочитать/записать данные
- fcntl - настройка свойств ФД (блокировки, подписка на события, установка флагов и т.п.)
- dup - дублирование ФД
- mmap/munmap - отображение файла в оперативную память

4️⃣ Свойства операций ввода-вывода тоже похожи. Ввод/вывод может быть:
- Синхронным/асинхронным
- Буферизированным/прямым (direct)
- Блокирующим/неблокирующим

💡 Подсистема ввода/вывода - это непросто, но важно. С её инструментами и свойствами нужно разбираться, в работе мы с ними сталкиваемся каждый день. Но вы только посмотрите, как всё это красиво устроено! Столько разных задач решено единым подходом - почти никаких частных случаев, и все выигрывают от общего инструментария. Именно к такому дизайну в своих системах, как мне кажется, и надо стремиться.

Ставь огонёк, если всё есть файл 🔥

#theory #Linux #fs #networks
🔥5
Анализируем файловые дескрипторы lsof

Про подсистему ввода/вывода поговорили, а как это дебажить опять никто не сказал 🤡 Давайте исправлять!

📂 Когда мы открываем файл (пайп, сетевое соединение, и т.д.), операционная система даёт нам файловый дескриптор - это неотрицательное число, которое идентифицирует открытый файл. В дальнейшем, при каждой операции над этим файлом мы будем передавать файловый дескриптор, чтобы ОС могла понять, над каким файлом производится операция.

⚙️ В пространстве ядра у каждого процесса есть таблица открытых файлов. Файловый дескриптор - это по сути номер строчки в этой таблице, т.е. ключ. По ключу хранится значение - контекст открытого файла. В этой структуре хранятся, например:
- позиция чтения/записи (это похоже на положение курсора в тексте);
- флаги, с которыми открыт файл (только для чтения/для чтения и записи, блокирующий/неблокирующий режим и т.п.);
- примитивы для блокировок;
- и прочие переменные, необходимые для операций над открытыми файлами.

🔧 К чему я всё это написал: в UNIX есть замечательная утилита - lsof. Она работает с таблицей открытых файлов и может помочь в отладке операций ввода/вывода. Разберём несколько рецептов:

1️⃣ Вывести таблицу открытых файлов процесса:
lsof -p "<PID>"

☝️ Настоятельно рекомендую выполнить эту команду прямо сейчас, если вы первый раз слышите об lsof. Там куча всего интересного:
- Список открытых файлов на диске - логи, рабочая директория процесса, /dev/null и т.п.
- Список подгруженных разделяемых библиотек
- Список установленных сетевых соединений (btw, lsof может быть реальной альтернативой netstat, при чём под MacOS по моим наблюдениям lsof работает так же, как и под Linux)
В общем, lsof даёт много полезной инфы.

2️⃣ Вывести список процессов, работающих с файлом:
lsof "/path/to/file"


🤔 С какими проблемами может помочь lsof?

 Проверка доступа к файлам и сетевым ресурсам. Когда процесс “должен куда-то ходить” или наоборот “не должен куда-то ходить”, lsof подскажет - пытается он или нет.

 Отладка ошибки Too many open files - размер таблицы открытых файлов ограничен, из-за чего при открытии нового файла (или установке соединения) может возникать такая ошибка. lsof подскажет, имеет ли место утечка файловых дескрипторов (это когда программист close забыл позвать) и какие именно ФД утекли. Или наоборот, при анализе lsof можно провалидировать, что всё ок, и нужно просто поднять лимит.

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

💎 В общем, lsof - невероятно полезная утилита. Меня самого она несколько раз очень сильно выручала. Надеюсь, теперь она будет полезна и вам 😉

Ставь огонёк, если lsof - топ 🔥

#hardskills #tools #Linux #fs #networks #debug
🔥41
Что под капотом у файловых систем?

Не спросил меня ни разу ни один человек в мире 😂 А тема то на самом деле интересная! И раз уж мне в процессе работы в VK довелось написать свою файловую систему, я проявлю инициативу и напишу об этом сюда.

🗂 Итак, файловая система - это ПО для организации файлов и работы с ними. Чаще всего (хотя и не всегда), файловая система является модулем ядра ОС. Придумали ФС прежде всего для того, чтобы спрятать сложные низкоуровневые API жёстких дисков за простым и понятным для пользователя интерфейсом.

🌲 Файлы под управлением ФС в Linux объединяются в дерево: директории (папки) могут быть вложены одна в другую, листьями дерева являются обычные файлы. Как-то так:
# Утилита tree в Linux позволяет вывести поддерево файлов
tree ./test
./test
|-- dir1
| `-- hello_world.txt
`-- dir2

2 directories, 1 file


📑 На одном Linux-хосте одновременно могут эксплуатироваться десятки файловых систем. Выполните команду findmnt, если не верите 😉 При этом каждый инстанс файловой системы отвечает за своё поддерево файлов. Процесс добавления файловой системы в дерево файлов называется монтирование, его можно выполнить вот так: mount <filesystem id> </path/to/directory>

🧩 Почему дерево файлов с точки зрения пользователя выглядит однородным, если за разные его части отвечают разные ФС? Дело в том, что все ФС в Linux реализуют общий интерфейс. Например, все ФС должны уметь open, close, read, write и т.д. За общий интерфейс для всех файловых систем в Linux отвечает подсистема vfs.

⚙️ И вот теперь мы готовы заглянуть под капот. Как написать свою файловую систему? Если кратко, то надо написать программу, которая будет соответствовать интерфейсу vfs. Чтобы требованиям интерфейса было проще соответствовать, внутри ФС лучше будет оперировать теми же сущностями, что и ФС общего назначения. Давайте рассмотрим их подробнее:

1️⃣ inode - блок данных с целочисленным id. Фактически хранит данные, т.е. знает, где именно (по какому адресу) они расположены на диске.

2️⃣ dentry - узел в дереве файлов. У дентри есть три главных поля:
- имя файла (строка) - человеко-читаемый индентификатор файла;
- инода (id) - по сути указатель на блок данных с содержимым файла;
- родительская инода (id) - инода директории, в которой находится файл.
Зачем было разделять файлы на 2 сущности: dentry и inode? Оптимизация! Например, вы можете копировать файл между директориями без фактической перезаписи данных (inode) на диске - вам нужно просто поменять dentry.

3️⃣ struct file - контекст открытого файла, отвечает за блокировки, позицию чтения/записи в файл, контроль доступа к файлу и т.п.

Σ Совокупность этих 3-ёх структур данных образует файл. Забавно получается, в файловой системе отдельной сущности “файл” как раз и нет 😊

🤔 Зачем вам это знать? Не думаю, что вам придётся писать свою ФС - задача довольно редкая. Однако, при разработке stateful приложений или системном администрировании бывает полезно знать, как ФС устроены изнутри. Внутрянка ФС там иногда вылезает наружу: например, иноды могут закончиться 0_o

 А ещё это классный кейс хорошего дизайна систем: разделение данных с разным паттерном доступа на отдельные сущности может обеспечить бОльшую гибкость в работе с этими данными и позволить оптимизировать хранение.

💎 И напоследок хотел бы обратить внимание на vfs - хрестоматийный пример отделения интерфейса от реализации, что позволило обеспечить расширяемость и упростить эволюцию системы.

Ставьте огонёк, если вам интересно читать про внутреннее устройство разных систем 🔥

#theory #Linux #fs
🔥6
Docker - контейнеризация, доступная всем

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

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

📥 Перед началом работы необходимо установить docker. В стандартную поставку docker входят cli-утилита и docker-демон. После установки надо удостовериться, что демон запущен:
docker stats --no-stream
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?

# Ошибка говорит о том, что демон не запущен
# В зависимости от способа установки, надо:
# - либо запустить либо GUI-приложение Docker Desktop
# - либо запустить docker-демон через systemctl

systemctl start docker

docker stats --no-stream
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS


⚙️ Теперь, когда окружение готово, попробуем запустить чего-нибудь в контейнере. Создадим файлик helloworld.py, это наше тестовое приложение. Содержимое файла:
print('Hello world')


📃 Далее нам нужен имейдж - неизменяемый архив, в который мы запакуем окружение для нашего приложения. Для сборки имейджа докеру нужна инструкция. Создадим файлик Dockerfile, в нём мы опишем, какие шаги надо выполнить для сборки имейджа. Содержимое файла:
# Указываем базовый имейдж.
# Нам не обязательно создавать свой имейдж с нуля, можно взять за основу что-то готовое.
# Обычно, за основу берут базовый имейдж с желаемым дистрибутивом Linux и основным ПО.
# В данном случае, дистрибутив нам не важен. Нас устроит любой, где есть python версии 3.
# Имейджи по-умолчанию скачиваются с https://hub.docker.com/
FROM python:3

# Добавляем наше приложение в имейдж
ADD helloworld.py /root/helloworld.py

# По-умолчанию при запуске контейнера из нашего имейджа будет выполнена вот эта команда.
CMD ["python3", "/root/helloworld.py"]

^ больше информации про Dockerfile в официальной документации.

🔨 Собираем имейдж из Dockerfile:
docker build --tag helloworld .
# docker build - собрать имедйж
# --tag helloworld - назвать имейдж "helloworld"
# . - Dockerfile искать в текущей директории


👟 Запускаем контейнер (т.е. стартуем процесс нашего приложения):
docker run --name helloworld_container helloworld
# docker run - запустить контейнер
# --name helloworld_container - назвать контейнер "helloworld_container"
# helloworld - взять окружение для запуска из имейджа "helloworld"

Если вы всё сделали правильно, на этом этапе увидите Hello world в терминале 😉

♻️ Приберём за собой и почистим созданные ранее ресурсы:
# Вывести список контейнеров. Флаг -a означает "в т.ч. завершившихся"
docker ps -a

# Удаляем наш контейнер
docker rm helloworld_container

# Вывести список имейджей
docker images

# Удаляем наш образ
docker rmi helloworld

# Удаляем базовый образ
docker rmi python:3


👍 На сегодня знакомство с docker закончим. Напоминаю, что контейнеры - это очень полезная технология, на её изучение имеет смысл потратить время 😉 Ставь огонёк, если хочешь более продвинутые туториалы по docker 🔥

#hardskills #containers #tools #Linux #docker
🔥5
Отладка docker-контейнеров

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

🐳 Создадим Dockerfile:
# Alpine - это минималистичный дистрибутив Linux
# Его любят использовать для контейнеров, потому что он очень мало весит
FROM "alpine"

# Бесконечно выводим "Hello world" с интервалом 0.5 секунды
CMD ["while", "[[ 1 ]];", "do", "echo", "'Hello world';", "sleep 0.5;", "done"]


🔨 Собираем имейдж:
docker build --tag dockerdebug .


👟 Запускаем контейнер
docker run --name dockerdebug_container -d dockerdebug
# Флаг -d нам раньше не встречался
# Это значит "запустить в бэкграунде", т.е. в режиме демона


🪲 Oh-oh…
41f8f5d28c04f9ba705b4482e26333152071fdd7c06af736933ebc9f0eb83cba
docker: Error response from daemon: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "while": executable file not found in $PATH: unknown

Таким образом мы плавно приходим к теме сегодняшнего туториала: что делать, если контейнер не стартует?

1️⃣ Смотрим на статус контейнера:
docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
41f8f5d28c04 dockerdebug "while '[[ 1 ]];' do…" 9 minutes ago Created dockerdebug_container

Официальная документация говорит, что Created значит “контейнер никогда не стартовал”. Окей, статус узнали, это хорошо. Уточнить текущее состояние важно для отладки, но никакой информации к размышлению это нам не дало. Идём дальше.

2️⃣ Смотрим логи:
docker logs dockerdebug_container

А в логах пусто 😢 В целом, это было ожидаемо, раз контейнер не стартанул. Если бы он “стартанул и сразу упал”, то в логах можно было бы что-то найти. Однако этим шагом никогда не стоит пренебрегать! Логи - это самый простой способ узнать, что происходит с вашим софтом. В случае любых проблем всегда смотрите логи. Ну а мы переходим к тяжёлой артиллерии:

3️⃣ Переопределим команду и запустим контейнер. Подключимся к нему и выполним исходную команду сами. Таким образом, мы получим боевое окружение, в котором сможем запустить свою команду с дополнительным дебагом без пересборки имейджа.
# Удаляем багованный инстанс контейнера
docker rm dockerdebug_container
# Запускаем новый инстанс
# Обратите внимание на `sleep infinity` после тега имейджа
# Эта команда будет запущена при старте _вместо_ команды из Dockerfile
docker run --name dockerdebug_container -d dockerdebug sleep infinity
# Мы получили запущенное боевое окружение, в нём ничего не происходит
# Подключимся к этому окружению, чтобы увидеть мир "изнутри" контейнера
docker exec -it dockerdebug_container /bin/sh
# Мы получили шелл внутри контейнера. Выполним исходную команду.
while [[ 1 ]]; do echo 'Hello world'; sleep 0.5; done
Hello world
^C
# ^ работает, лол 0_o

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

 Но у нас всё заработало как есть, почему так? Это потому что в контейнере мы запускали код в оболочке интерпретатора sh, а докер при старте контейнера делает простой exec. Т.е. мы пытались запустить интерпретируемый код без интерпретатора, такая вот учебная бага 😊

❤️‍🩹 Давайте починим:
FROM "alpine"
CMD ["/bin/sh", "-c", "while [[ 1 ]]; do echo 'Hello world'; sleep 0.5; done"]


docker stop dockerdebug_container
docker rm dockerdebug_container
docker rmi dockerdebug
docker build --tag dockerdebug .
docker run --name dockerdebug_container -d dockerdebug
docker logs dockerdebug_container
Hello world


🤔 Итого, алгоритм дебага docker-контейнеров: уточняем статус docker ps ⇒ смотрим логи docker logs ⇒ если не помогло, переопределяем команду на sleep infinity, подключаемся к контейнеру через docker exec и применяем стандартные #debug инструменты 😉 Этот же алгоритм применим к кейсам, когда контейнер запустился, но не работает. Ставь огонёк, если было полезно 🔥

#hardskills #containers #tools #Linux #docker #debug #logging
🔥6👍3
strace - дебажим системные вызовы

Иногда возникают задачи отдебажить “чужой код”. Например, какое-нибудь проприетарное приложение или стороннюю библиотеку. И всё хорошо, если в этом “чужом коде” всё в порядке с observability: есть понятные логи, по которым можно разобрать причины проблем. А что если нет? Что, если чужой софт просто падает с ошибкой без каких-либо сообщений? Или проблема находится на крайне низком уровне: стандартная библиотека/glibc/ядро ОС?

💣 В этом случае приходится подключать тяжёлую артиллерию. Для меня открытием стала утилита strace. Она позволяет отслеживать выполняемые программой системные вызовы и возвращаемые значения. Давайте сразу пример, вот так выглядит простой вызов echo под strace:
strace echo "Hello world"
execve("/usr/bin/echo", ["echo", "Hello world"], 0x7ffd82112798 /* 36 vars */) = 0
brk(NULL) = 0x56501e97b000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffe71bfbd20) = -1 EINVAL (Invalid argument)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fe1e2ba4000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=16407, ...}) = 0
mmap(NULL, 16407, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fe1e2b9f000
close(3) = 0
openat(AT_FDCWD, "/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0`\256\3\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2089984, ...}) = 0
lseek(3, 808, SEEK_SET) = 808
read(3, "\4\0\0\0\20\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 32) = 32
mmap(NULL, 3950816, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fe1e25b3000
mprotect(0x7fe1e276e000, 2097152, PROT_NONE) = 0
mmap(0x7fe1e296e000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1bb000) = 0x7fe1e296e000
mmap(0x7fe1e2974000, 14560, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fe1e2974000
close(3) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fe1e2b9d000
arch_prctl(ARCH_SET_FS, 0x7fe1e2ba5680) = 0
mprotect(0x7fe1e296e000, 16384, PROT_READ) = 0
mprotect(0x56501e0a9000, 4096, PROT_READ) = 0
mprotect(0x7fe1e2ba6000, 4096, PROT_READ) = 0
munmap(0x7fe1e2b9f000, 16407) = 0
getrandom("\x75\x94\x42\x9a\x46\x55\x16\x6d", 8, GRND_NONBLOCK) = 8
brk(NULL) = 0x56501e97b000
brk(0x56501e99c000) = 0x56501e99c000
brk(NULL) = 0x56501e99c000
openat(AT_FDCWD, "/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=217804320, ...}) = 0
mmap(NULL, 217804320, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fe1d55fc000
close(3) = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x5), ...}) = 0
write(1, "Hello world\n", 12Hello world
) = 12
close(1) = 0
close(2) = 0
exit_group(0) = ?
+++ exited with 0 +++


🤨 Чего тут можно увидеть?
1️⃣ Мы выполнили execve на файл /usr/bin/echo
2️⃣ Процесс инициализировался, подгрузил динамические библиотеки
3️⃣ Потом наш процесс сделал write в stdout и завершился

🤓 Полезные флаги strace:
- -p <PID> - подключиться можно к существующему процессу, не обязательно запускать его под strace.
- --follow-forks - если процесс создаёт дочерние процессы, по-умолчанию strace будет следить за родительским. Этот флаг меняет поведение, strace начинает следить за дочерними процессами.
- -e - к выхлопу strace можно применять фильтры
- -o <FILE> - выхлоп можно направлять в файл (по-умолчанию он летит в stderr)

🔥 strace позволяет получить дебаг информацию о процессе прямо от ОС - это невероятно полезно при низкоуровневых проблемах и при отладке "чёрных ящиков".

#hardskills #Linux #tools #debug
🔥2
Потоки 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
🔥2
Главный источник знаний в проекте

Этот маленький засранец знает ответы на самые сложные вопросы:
- Сколько тредов запускает приложение?
- На каких серверах запущен nginx?
- Какие сервисы ходят в эту БД?

Вот он, святой грааль разработчика:
 fgrep -rie "pthread_create" ./src


Греп по сорцам - один из самых важных хардскиллов, которым я научился за 5 лет в VK. Он позволяет находить новые знания в текущей кодовой базе, без всякой документации, комментов и вопросов к старшим коллегам. Всего того, чего в большом проекте с историей может и не быть.

Grep - это семейство утилит для текстового поиска. Я чаще всего пользуюсь такими вариантами грепа, как:
- fgrep - простой текстовый поиск, никакие символы в поисковом запросе не имеют специального значения
- grep - текстовый поиск по регулярным выражениям
- zgrep / zfgrep - всё то же самое, но позволяет искать по сжатым gzip-файлам без необходимости полной декомпрессии. Используется редко, но бывает полезно для поиска по историческим логам.

Самые важные флаги grep-утилит (одинаковые для всех вариантов):
- -r - рекурсивный поиск по директории, именно этот флаг позволяет искать “по всему проекту” или “по какой-то части проекта”.
- -i - включает case-insensitive поиск (это когда заглавные и строчные буквы интерпретируются одинаково, т.е. между “А” и “а” нет никакой разницы).
- -e - включает extended regexp syntax, я уже привык к этому синтаксису, поэтому включаю этот флаг всегда по привычке. Для fgrep он ничего не делает.
- -v - исключающий поиск, это когда мы ищем “всё, кроме <поисковый запрос>”

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

0️⃣ Начинаем с вопроса: что мы хотим найти? Например, “кто ходит в сервис А” или “сколько соединений открывает сервис Б”. Далее, выбираем область поиска. Если твой вопрос по внутреннему устройству какого-то софта, ищем по его исходному коду. Если твой вопрос про конфигурацию (взаимодействие между сервисами или продакшен-настройки), ищем по IaC репозиторию, если у твоей компании он есть. Например, мы используем Puppet и Kubernetes, многие пользуются Ansible. Не стесняйтесь отсекать директории, в которых точно нет ничего полезного. Например, искать по ./src вместо ., чтобы не заходить в ./third_party, ./vendor или ./node_modules - это сильно ускоряет процесс.

1️⃣ Дальше выполняем рекурсивный поиск. Я убеждён, что самое главное в этом вопросе - контроль фокуса. Мы не хотим найти слишком много результатов, глаз будет замыливаться и можно пропустить что-то важное. Поэтому начинаем с самого простого fgrep -re

2️⃣ Если ничего не нашли, можем расширять поисковый запрос. Добавить флаг -i или перейти на grep и поиск по регулярке.

3️⃣ Скорее всего, после расширения запроса в поисковую выдачу попадёт что-то лишнее. Не стесняемся добавлять | grep -v в конец запроса и отсекать ненужные варианты.

4️⃣ В конце концов, ты либо найдёшь, что ищешь, либо сдашься и пойдёшь спрашивать помощи у коллег. Второй вариант на самом деле неплохой, но тут тоже есть один трюк. Скорее всего, коллега, который ответит тебе на вопрос, тоже этот ответ как-то найдёт. Невозможно запомнить все нюансы и тонкости в большой системе, но возможно научиться находить эту информацию. И трюк тут в том, чтобы после ответа на свой вопрос спросить: “А как ты это нашёл?” Это гораздо важнее, чем сам ответ, потому что позволит развить навыки поиска информации и, в конченом итоге, повысить самостоятельность.

Этот простой алгоритм, когда я был джуном, стал мощным драйвером роста моих навыков, и я им пользуюсь до сих пор. В следующий раз, когда у тебя возникнет вопрос по проекту, не спеши обращаться к коллегам, попробуй найти ответ сам! Ну и grep в помощь, куда же без него 🔥

#hardskills #codereading #selflearning #Linux #tools
3🔥2
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
🔥2👍1
Что под капотом у исключений

Пост для тех, кто раньше об этом не задумывался. Программисты думают об исключениях как о фиче языков программирования. ЯП поддерживает исключения, если предоставляет операторы try, catch, fially, throw. Но всё не так однозначно. Если бы ты писал свою реализацию исключений, как бы ты это сделал?

Рассмотрим фичи исключений.
- Исключение может прервать поток выполнения программы.
- Исключение должно раскручивать стек до ближайшего обработчика.
- Обработчик исключений, если установлен, должен выполниться незамедлительно при срабатывании исключения.
- Если обработчика нет, процесс должен аварийно завершиться.
- Исключения должны быть совместимы с фатальными ошибками ОС, такими как Segmentation fault.
- Обработчик исключений должен возвращать управление в точку после try/catch.

Исключения реализуются с помощью UNIX-сигналов и longjmp. Обе фичи есть стандартной библиотеке С, хотя готовых исключений там нет.

Наколдуем свои исключения.
🐱 Листинг кода: gist.github.com/maksimuimin/c14b066b0b454ac7a1962631c3a238bb
▶️ Результат выполнения:
Try 1
Got signal: Aborted
Exception 1
Try 2
Try 3
Got signal: Segmentation fault
Exception 3


Практические выводы.
📌 ОС сообщает о фатальных ошибках с помощью UNIX-сигналов. Такие ошибки можно обработать и как исключения, и как сигналы. Обработка фатальных ошибок - хорошая практика безопасного программирования, даже если ваш ЯП не поддерживает исключения. Пример из реального мира, почтовый сервер Exim: github.com/Exim/exim/blob/exim-4.97.1/src/src/receive.c#L3805

📌 longjmp работает как goto, но может передавать управление между функциями. Позволяет реализовать оператор throw без сигналов. Пример из реального мира, функция error в Lua: pgl.yoyo.org/luai/i/lua_error

Исключения - сложный механизм. Я предпочитаю модель обработки ошибок на основе возвращаемых значений - там всё просто. Однако, эта модель никак не обрабатывает фатальные ошибки ОС. Поэтому, на практике используется комбинация двух моделей: возвращаемые значения для стандартных сбоев и исключения/сигналы для фатальных ошибок. Это хороший компромисс между простотой и отказоустойчивостью 🔥

#theory #coding #Linux #errorhandling
===
Мои любимые посты в канале t.me/uimindev/37
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥4
Оболочку на прокачку

Настроенная оболочка терминала ускоряет работу программиста. Если в твоём терминале стоят настройки по умолчанию, этот пост для тебя 😉

Немного теории. Под “терминалом” понимают связку из двух программ.
- Эмулятор терминала - программа с графическим интерфейсом, умеет рисовать текст на экране и получать ввод с клавиатуры. Пример: GNOME Terminal, Konsole, XTerm.
- Оболочка - программа с текстовым интерфейсом, умеет пользоваться переменными окружения и выполнять команды. Пример: sh, bash, zsh.

Приступаем к настройке. Работать должно для MacOS и Linux. Вот примерный план:

1️⃣ Поставить цветной эмулятор терминала. Нам нужно 256 цветов, проверить можно так:
curl -s https://gist.githubusercontent.com/HaleTom/89ffe32783f89f403bba96bd7bcd1263/raw/e50a28ec54188d2413518788de6c6367ffcea4f7/print256colours.sh | bash

Если выхоп весь цветной, то всё ок. Если нет, надо поставить другой эмулятор. У меня MacOS, пользуюсь iTerm2. Под Linux слышал хорошие отзывы об Alacritty.

2️⃣ Ставим оболочку Zsh: github.com/ohmyzsh/ohmyzsh/wiki/Installing-ZSH

3️⃣ Ставим фреймворк Oh My Zsh: ohmyz.sh/#install

4️⃣ Чтобы emoji работали, нужно установить шрифты. Качаем из Интернета и прописываем в настройках эмулятора терминала. Я использую Source Code Pro + Powerline + Awesome Regular.

5️⃣ Открываем ~/.zshrc и настраиваем конфиг. Самые важные настройки:
- ZSH_THEME - отвечает за то, какая инфа и в каком виде будет отображаться. Я использую ZSH_THEME="powerlevel10k/powerlevel10k". Библиотека тем.
- plugins - список плагинов Oh My Zsh. Я использую plugins=(git zsh-autosuggestions). Библиотека плагинов.

Заключение. Буду честен, приключение по настройке этого добра - не из приятных. У меня заняло несколько часов, особенно сложным был подбор правильного шрифта. Более подробные инструкции по настройке ищи в Интернете ¯\_(ツ)_/¯ Прокачивай терминал и работай эффективнее 🔥

Хорошей пятницы 🍹

#hardskills #Linux #productivity #tools
===
Мои любимые посты в канале t.me/uimindev/37
2👍1🔥1🦄1
Поиск с контекстом

Программисты ищут информацию в текстах каждый день.
Где в коде используется переменная X и функция Y?
Есть ли в логах ошибки?
В каких git-коммитах упоминается фича Z?

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

Для текстового поиска я использую утилиту grep, уже писал про неё как про главный источник знаний в проекте. Для поиска с контекстом у grep есть флаги:
# Вывести <число> строк после (after) строки, подходящей под <запрос>
grep -A <число> <поисковый запрос>

# Вывести <число> строк до (before) строки, подходящей под <запрос>
grep -B <число> <поисковый запрос>

# Вывести <число> строк вокруг (context) строки, подходящей под <запрос>
grep -C <число> <поисковый запрос>


Пример использования:
# Найдём по коммитам, кто пишет ядро Linux 31 декабря после 23:00

git log | grep -C 2 "Date:.*Dec 31 23"
commit ca42bc4b7bda7c6d68f1cc97c27fc8ff7385c4c7
Author: Al Viro <viro@zeniv.linux.org.uk>
Date: Thu Dec 31 23:23:01 2020 +0000

sh: fix trivial misannotations
--
commit b4229fc67d9b9d8910ad32c508fd94518bd01194
Author: Wambui Karuga <wambui.karugax@gmail.com>
Date: Tue Dec 31 23:57:34 2019 +0300

drm/nouveau: use NULL for pointer assignment.
...


Поиск с контекстом используется нечасто, но про него полезно знать. Поставьте огонёк, если хотите больше постов про работу с текстом в консоли 🔥

#hardskills #Linux #tools #selflearning
===
Закрытый канал https://t.me/tribute/app?startapp=sgsw
Мои любимые посты в канале t.me/uimindev/37
🔥11