cat mindflow.txt > /dev/null – Telegram
cat mindflow.txt > /dev/null
180 subscribers
14 photos
87 links
Поток сознания о программировании, технологиях, жизни и вообще
Download Telegram
И в завершение недели постов разродился статьей о всевозможных способах обработки запросов на сервере с примерами на python https://micromind.me/posts/writing-python-web-server-part-2?utm_medium=social&utm_source=tchannel
Идея even loop проста, но программные платформы на основе циклов событий получаются очень мощными. Про внутреннее устройство циклов событий и то, как можно написать свой всего в 100 строках https://micromind.me/en/posts/explain-event-loop-in-100-lines-of-code/?utm_medium=social&utm_source=tchannel
У Python очень богатая стандартная библиотека. Собрал в одном месте десяток способов запустить TCP и/или HTTP сервер с использованием только стандартных средств.

https://micromind.me/ru/posts/over-9000-ways-to-make-web-server-in-python/?utm_medium=social&utm_source=tchannel
Хоть практической пользы в этом видео наверное и нет, это интервью просто обязаны посмотреть все, кто имеет хоть какое-то отношение к программированию. Кен Томпсон: "К счастью, моя жена в тот момент уехала в трехнедельный отпуск с нашим годовалым ребенком, и - неделя, неделя, еще неделя - и получился Unix" https://youtu.be/EY6q5dv_B-o?t=1357
Минутка занимательной этимологии. Возможно, это оказалось новостью только для меня, но глагол "to boot" в компьютерном мире означает далеко не просто "загружать что-либо". Достаточно взглянуть на список значений - увольнять, надевать ботинки, ударить сапогом, помогать https://translate.google.com/#view=home&op=translate&sl=en&tl=ru&text=boot. В компьютерный же мир это слово пришло скорее из более длинной его формы "bootstrap", имеющей в настоящее время значение "get (oneself or something) into or out of a situation using existing resources". То есть справиться с задачей исключительно своими собственными ресурсами, без внешнего воздействия, а в компьютерном мире - ввода дополнительных данных. Вероятно, все началось пару сотен лет назад с гиперболы "pull oneself over a fence by one's bootstraps" (https://en.wikipedia.org/wiki/Bootstrapping), означающей "перебраться через изгородь, потянув самого себя за задники ботинок (bootstrap-ы)", американского варианта подвига барона Мюнхгаузена, вытянувшего себя из болота за свои же волосы.
Docker несколько лет был для меня не более, чем инструментом, чёрным ящиком с чёрной магией внутри, позволяющим запустить своё приложение в воспроизводимой среде. А на самом деле, стоило всего лишь потыкать в него палочкой, чтобы осознать, что, например, image - это не более, чем папка с файлами, обычно с корневой файловой системой какой-нибудь версии Linux.

Аналогичная история с Linux OS. Какая часть сервера - это Linux kernel, а какая - userland? Как происходит загрузка ОС? И опять же, просто нужно было собрать свой дистрибутив и запустить его на виртуальной машине.

https://micromind.me/en/posts/from-docker-container-to-bootable-linux-disk-image/?utm_medium=social&utm_source=tchannel
Используешь себе какой-нибудь линуксовый тул каждый день и беды не знаешь. Но вдруг накрывает, и становится очень интересно, как он работает под капотом. Вот сегодня, например, настала очередь ip из iproute2. Стандартно полез гуглить исходники и уж было начал их курить... В общем, не знаю, почему мне пришло это в голову только сейчас, но... Куда быстрее просто обернуть вызов ip в strace:

strace -e trace=network,file ip addr
execve("/usr/sbin/ip", ["ip", "addr"], [/* 35 vars */]) = 0
...
socket(AF_NETLINK, SOCK_RAW|SOCK_CLOEXEC, NETLINK_ROUTE) = 3
setsockopt(3, SOL_SOCKET, SO_SNDBUF, [32768], 4) = 0
setsockopt(3, SOL_SOCKET, SO_RCVBUF, [1048576], 4) = 0
setsockopt(3, SOL_NETLINK, 11, [1], 4) = -1 ENOPROTOOPT (Protocol not available)
bind(3, {sa_family=AF_NETLINK, pid=0, groups=00000000}, 12) = 0
getsockname(3, {sa_family=AF_NETLINK, pid=4727, groups=00000000}, [12]) = 0
sendto(3, ..., 40, 0, NULL, 0) = 40
recvmsg(3, ..., MSG_PEEK|MSG_TRUNC) = 3752
...
open("/etc/iproute2/group", O_RDONLY) = 4

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 52:54:00:26:10:60 brd ff:ff:ff:ff:ff:ff
inet 10.0.2.15/24 brd 10.0.2.255 scope global noprefixroute dynamic eth0
valid_lft 76662sec preferred_lft 76662sec
inet6 fe80::5054:ff:fe26:1060/64 scope link
valid_lft forever preferred_lft forever


Вот так за 5 секунд мы узнали, что пора читать про AF_NETLINK сокеты https://en.wikipedia.org/wiki/Netlink.
Что такое "операционная система Linux"? Это ядро (Linux kernel, бинарник в несколько мегабайт, хитрая прослойка между железом и софтом) и пользовательское пространство (user space, десяки или сотни мегабайт разных бинарников утилит, помогающих работать с ядром). Ядро всегда одно, отличается лишь версиями, а пользовательских пространств много (Debian, Ubuntu, CentOS, Alpine, busybox, кастомные сборки и т.п.).

Что такое "контейнеризация"? Это способ виртуализации на уровне операционной системы. В частности, это когда на запущенной Linux ОС можно положить разные пользовательские пространства в разные папки и потом запустить их выполняться. В качестве ядра будет использовано ядро родительской ОС, но программы, выполняющиеся в разных контейнерах, будут думать, что каждая из них имеет свою собственную ОСь вокруг. Изменения, сделанные в рамках одного контейнера, могут быть скрыты от остальных контейнеров и родительской ОС. Как это работает? Ядро Linux поддерживает т.н. namespaces, то есть способ изоляции ресурсов. Например, Process ID namespace позволяет иметь несколько деревьев процессов, начинающихся каждое со своего PID 1 и все это в рамках одного и того же экземпляра выполняющегося ядра. Также существуют network, mount, user ID и прочите изолированные пространства. Системный вызов clone(), являющийся основой канонического fork() принимает флаги, позволяющие изолировать новый запускаемый процесс в различных пространствах. Зачем это нужно? Здорово уметь запускать на одной железке как можно больше изолированных окружений, это круто повышает утилизацию ресурсов (альтернативный подход - запускать полностью излоированные ОСи на одной и той же железке - обычно менее эффективен [см. kata containers], правда и более безопасен).

Что такое Docker? Когда-то давно Docker был большим демоном-монолитом (и клиентом, со всеми знакомым нам CLI API), решающим комплексную задачу запуска и обслуживания контейнеров. Что же именно это значит? Когда на диске уже есть папка с набором файлов какого-либо user space, остается лишь создать нужные namespace и запустить бинарник, указанный как entry point в качестве процесса с PID 1. Но после запуска необходимо мониторить состояние контейнера, возможно перезапускать его при падении, очищать ресусры при остановке и т.п. Папку с user space на диске тоже нужно создать умело. Многие контейнеры используют на 100% схожий user space, отличаясь лишь полезной нагрузкой (кто-то хочет запускать nginx в debian:latest, кто-то свой node.js app но тоже в debian:latest и т.п.). Так как речь идет о десятках и сотнях мегабайт, неплохо бы уметь реиспользовать повторяющиеся части. Я могу выделить следующие основные куски Docker-а: 1) обслуживание жизненного цикла контейнеров (запуск, мониторинг состояния, очистка ресурсов после остановки) 2) работа с образами (images), т.е. жонглирование всеми этими большими папками, в частности загрузка их из реестров по сети 3) мощный API для запуска и запроса состояния контейнеров и образов. Так как каждый из этапов вполне себе самодостаточный, а релизить все синхронно - сложно, в какой-то момент Docker решили распилить на runc (запускатор контейнеров, консольная утилита, один запус == один контейнер), containerd (демон, делает все, чтобы runc мог запускать новые контейнеры, в частности работу с образами) и dockerd ("лицевой" демон, общающийся с containerd, чтобы мы могли исползьовать вот эти все удобные плюшки Docker-а в командной строке, через docker client). С момента распила каждый из проектов живет своей отдельной жизнью. Например, runc используется и другими менеджерами контейнеров (см. cri-o), а его интерфейс стандартизирован и он является канонической реалзиацией Container Runtime Specification.
Почему даже после распила на части Docker - все еще монолит? Одна из причин в том, что containerd - это демон, да еще и требующий root (ибо он запускает runc, которому нужен root для создания namespaces). Но работа с образами не требует ни демона ни тем более root. Однако, в случае Docker альтернативы (пока?) нет. Если хочется скачать какой-либо image из сети или собрать свой - придется запускать containerd. К счастью, ребятам из Red Hat это не понравилось и они решили запилить свой тулсет контейнеризации с блекджеком и барышнями. Так, запустить контейнер, используя докеро-подобный CLI можно утилитой podman (https://github.com/containers/libpod), которая под капотом использует опять же runc. Собрать свой образ или модифицировать существующий можно утилитой buildah (https://github.com/containers/buildah), которая под капотом испоьзует крутую либу https://github.com/containers/storage, которую также, например, использует демон cri-o, реализующий Kubernetes Container Runtime Interface (CRI). На мой взгляд, альтернативная (от Docker-вселенной) реализация Red Hat-ом всех этих контейнерных дел получилась куда более гибкой и структурированной. Но Docker все еще очень сильный игрок, благодаря исторически сформировавшемуся полчищу конечных пользователей.
Подводя итог нескольких месяцев, проведенных за изучением мира контейнеров и их оркестрирования, написал обзорную статью про экосистему контейнеризации, попробовав структурировать и описать взаимосвязи наиболее заметных проектов в области (OCI specs, runc, containerd, moby, cri-o, podman, etc) https://iximiuz.com/en/posts/journey-from-containerization-to-orchestration-and-beyond/?utm_medium=social&utm_source=tchannel
Кратенько о том, как Perl и PHP не успевают за реалиями серверной разработки.

Оба языка используют схожий подход к обработке HTTP запросов. Являясь по своей натуре однопоточными (как с точки зрения реализации интерпретатора, так и из-за отсутствия примитивов синхронизации вроде mutex-ов в спецификации языков), они вынуждены назначать выделенный процесс на каждый входящий HTTP-запрос. Таким образом, для обработки 100 одновременных HTTP-запросов на одной машине необходимо иметь 100 процессов-воркеров с запущенным интерпретатором PHP или Perl. 101й запрос будет вынужден находиться в очереди на обработку, пока один из 100 активных запросов не будет завершен. Каждый процесс-воркер может занимать от сотни мегабайт до нескольких гигабайт RAM в resident set size (!), и это без учета объема обрабатываемых в запросе данных. Таким образом, если на гипотетическом сервере у нас 8 ядер и 16 GB RAM, получится запустить в среднем 16-32 воркеров (при memory utilization стремящейся к 100%, что в реальном мире недопустимо), т.е. иметь 16-32 одновременных запроса.
В мире микросервисов и сторонних API редкий обработчик HTTP запроса обходится без десятка [под-]запросов к соседним сервисам. Это, очевидно, заставляет воркеры ожидать I/O, вместо того, чтобы заниматься непосредственно обработкой (т.е. считать что-то на CPU). Да, сетевое взаимодействие в PHP и Perl можно сделать асинхронным (в Perl даже есть промисы), но, в общем случае, эти [под-]запросы могут иметь зависимость по данным, делающую невозможным их конкурентную отправку. Таким образом, процессы-воркеры будут значительную часть времени находиться в ожидании ответов от сторонних сервисов. Что приводит, к неприятной ситуации - overutilization of RAM and underutilization of CPU.
Для сравнения, языки с нативной поддержкой асинхронной обработки запросов (JS/NodeJS асинхронный до мозга костей, Go обычно выделяет по горутине на процесс, в Python есть asynio или gevent-подобные штуки) не требуют оверхеда в виде выделенного процесса на каждый HTTP-запрос, делая возможным иметь тысячи одновременных HTTP-запросов на одной машине. И в конечном итоге позволяют иметь более сбалансированную утилизацию ресурсов сервера.
Про аккуратность и запуск новых процессов в Linux

Когда запускается новый процесс, его потоки ввода/вывода STD(IN|OUT|ERR) определяются родительским процессом. Например, делая cat - в консоли, родительским процессом будет командный интерпретатор (`bash`, zsh, etc), а его (интерпретатора) STD(IN|OUT|ERR) уже связаны с терминалом. cat же просто унаследует тот же набор.

Когда мы запускаем новый процесс программно (fork + exec), мы можем указать его STD(IN|OUT|ERR) файловые дескрипторы (`man dup`). Но по умолчанию форкнутый процесс просто наследует потоки родителя.

Когда мы запускаем новый процесс программно (fork + exec) и хотим дождаться его завершения, нам всего лишь нужно вызвать waitid() (`man waitpid`) в родительском процессе.

Когда мы хотим прочитать то, что запущенный нами процесс печатает в свой STDOUT, можно создать pipe (`man pipe`) и указать его в качестве соответствующего файлового дескриптора для нового процесса, а затем начать читать из этого pipe в родительском процессе.

Когда процесс форкает другой процесс, а затем сам завершается, потоки, унаследованне форком не будут закрыты.

А теперь представьте ситуацию - мы запускаем (fork + exec) процесс (назовем его стартер), который в свою очередь запускает новый процесс (назовем его демон), а сам завершается печатая статус на экран. И в нашем коде мы хотим а) дождаться завершения стартера б) прочитать статус из его STDOUT.

Оказывается, если стартер-процесс не переназначил STD(IN|OUT|ERR) демона, waitpid() нам благополучно сообщит, что стартер завершился, но вот блокирующая операция чтение из pipe на нашей стороне приведет к зависанию нашего процесса до окончания выполнения (в общем случае долгоживущего) демона. И это вряд ли то, что нам нужно. Фактически, демон наследует STDOUT стартера и, несмотря на то, что стартер уже давно завершился, файловый дескриптор останется открытым в течение всей жизни демона.

Вывод - стартеры должны быть умными =) А самое смешное, что вездесущий runc (https://github.com/opencontainers/runc) - пример неумного стартера. Пруф https://github.com/cri-o/cri-o/blob/8a43af20119ad8fe1ffebb4128d8938134eaaeb1/conmon/conmon.c#L1336-L1342. Это умный стартер неумного стартера.
DevOps можно сравнивать с Agile. DevOps определяет не конкретные техники, а скорее общую философию, что разработка (Dev) и эксплуатация (Ops) должны быть тесно связаны. Но не говорит, как. SRE же уместно сравнивать с конкретной Agile методологией, такой как scrum или kanban. SRE отвечает на вопрос как именно практиковать DevOps культуру в отдельно взятой компании. Вспоминается каламбур class SRE implements DevOps от Google.

Более того, раз уж мы упомянули Agile, то на мой взгляд, DevOps зачастую конфликтует с конкретными реализациями последнего. Если у вас в спринте (или на kanban доске) нет задач по Ops, но при этом кандидатам на собеседовании вы заявляете, что практикуете DevOps - где-то что-то не сходится. Ops должен быть представлен как равноправный аспект ежедневной работы.

https://twitter.com/iximiuz/status/1181177923610513409?s=09
В рамках погружения в увлекательный мир оркестрации решил посмотреть, зачем же Kubernetes-у зависимость от Docker-а как прослойки для работы с контейнеризацией, когда Docker сам по себе вполне себе оркестратор со своим мертворожденным swarm. И оказывается она вовсе и не нужна, просто так сложилось исторически. Containerd, который отвечает именно за контейнеризацию в докере сгодится в качестве такой прослойки и для Kubernetes. И оказывается там уже несколько лет как существует формальная спецификация на API такой прослойки - Kubernetes CRI. И есть реализация прослойки даже лайтовее, чем containerd, и имя ей cri-o. В общем, решил я самообразования ради сам написать что-то подобное, так что встречайте - conman - the container manager!

https://iximiuz.com/en/posts/conman-the-container-manager-inception/