cat mindflow.txt > /dev/null – Telegram
cat mindflow.txt > /dev/null
180 subscribers
14 photos
87 links
Поток сознания о программировании, технологиях, жизни и вообще
Download Telegram
Что такое "операционная система 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/
О моем восприятии Docker-а

Для тех, кто мог подумать, что я, по каким-то причинам, пытаюсь своими постами выставить Docker в негативном свете. Это далеко не так. Docker - это именно тот проект, с которого началась массовая популярность контейнеров. Docker - это исторически первая удачная упаковка разрозненных фич контейнирзации (Linux namespaces, cgroups, etc) в законченный продукт. Вклад Docker в эту область индустрии просто неоценим. Но у этой медали есть и обратная сторона... Я вижу как минимум 2 проблемы и постоянно пытаюсь обратить на них внимание коллег по цеху.

Во-первых, благодаря своей первоначальной популярности, слово _docker_ для среднестатистического программиста в какой-то момент стало синонимом слова _container_. В то время, как с технической точки зрения было бы корректно говорить "мое приложение выполняется в контейнере", люди традиционно говорят "мое приложение работает в докере". И в этом нет ничего страшного, ровно до тех пор, пока на основе этого искаженного понимания предметной области начинают приниматься технические (в частности архитектурные) решения.

Во-вторых, everything out of the box подход к проблеме контейнеризации, принесший докеру такой стремительный первоначальный успех (что может быть проще, чем yum install docker; docker build .; docker run), оказался и его слабой стороной. Docker старается решить проблемы запуска контейнеров (docker run/exec), управления образами (docker build/push/pull), оркестрирования (docker compose/swarm) и пр. Но далеко не все клиенты Docker заинтересованы сразу во всех его возможностях, в то время, как реализация всех этих фич в одном куске софта налагает как архитектурные (большой демон, управляющий другим демоном), так и операционные (демон еще и запущен под root) издержки. К счастью, кроме Docker существуют и друге проекты, решающие как целиком задачу контейнеризации, так и ее отдельные изолированные подзадачи (podman для управления контейнерами; runc для создания и запуска контейнеров; buildah и skopeo для работы с образами; containerd и cri-o как реализации container runtime для Kubernetes; Kubernetes - как оркестратор; в конце концов kata containers как альтернативный подход к изоляции). И, возвращаясь к первому пункту, для того, чтобы осмысленно начать пользоваться всеми преимуществами этих проектов, необходимо четко представлять зоны отвественности каждого из них.

На мой взгляд, самая сильная сторона Docker сейчас в его применимости в процессе разработки. Установить и начать использовать Docker на локальной машине - проще простого (для контраста попробуйте развернуть локальный Kubernetes cluster). Но если создание образов и выполнение контейнеров в Docker не будет совместимо с конечным окружением, где эти контейнеры выполняются в бою, Docker потеряет и свое последнее преимущество. Благо сейчас каждый из аспектов в этой области стараются завернуть в формальную спецификацию (OCI runtime spec, OCI image spec, Kubenetes CRI spec, etc), и Docker принимает активное участие в этих начинаниях.
Практикующий software engineer может эволюционировать в двух направлениях - generalist и specialist.

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

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

Мне по душе именно обобщение, так как я всегда склонен подмечать одинаковые паттерны, какую бы задачу и на каком стеке я ни решал, и переносить известные решения на новые предметные области.

Как известно, каждой задаче - свой инструмент. Всегда можно забивать гвозди микроскопом, но редко это будет эффективно. То же справедливо и для программистских задач. А у generalist-а их тьма и все они разные. Поэтому я долго думал над минимальным набором языков, эффективным как с точки зрения затрат на его изучение, так и количества областей, которые он будет покрывать. И, после почти 10 лет в индустрии, мой список выглядит так: Python, Go, C/Rust, JavaScript/TypeScript.

https://twitter.com/iximiuz/status/1191622134646411267?s=09
Mirantis купили Docker Enterprise и теперь Swarm официально мертв. Хотя пару лет поддержки для существующих клиентов, пока все не переедут на Kubernetes, все ещё обещают.

https://thenewstack.io/mirantis-acquires-docker-enterprise/
Что можно узнать, построив flamegraph на основе вывода cloc для кодовой базы Kubernetes https://iximiuz.com/en/posts/kubernetes-repository-on-flame/. Всего за 5 минут, без регистрации и смс.
Подвел итоги нет, не года, но десятилетия бытия программистом на полный рабочий день (а иногда и ночь, и почти всегда - выходные и праздники). Статья получилась техническая, с фокусом на мои личные best practices, выработанные за эти годы.

https://iximiuz.com/en/posts/my-10-years-of-programming-experience/?utm_medium=social&utm_source=tchannel
Об отказоустойчиости и cloud-native

Что делать, если что-то перестало работать? Первое правило инженера - выключить и снова включить (aka перезагрузить). А теперь посмотрите, во что превращается серверная разработка. Сплошные микросервисы, состоящие из большого числа эфемерных контейнеров. Если раньше мы мерились, у кого uptime сервера больше (потому что ребут очень часто был болью), то сейчас мы соревнуемся, кому проще прибить и перезапустить контейнер. Да еще и chaos monkey выпускаем, которые только тем и занимаются, что киляют процессы, ожидая, что сервис сам себя восстановит. Получается, что индустрия начала перманентно применять правило "выключи/включи" и это должно привести к качественному увеличению отказоустойчивости наших систем. Причинно-следственная связь правда не ясна. То ли Docker с Kubernetes-ом и AWS ECS нам позволили осуществить этот переход, то ли их для того и сделали, чтобы этот переход свершился...
conman - [the] container manager

В рамках моего pet project по созданию Kubernetes CRI-совместимого менеджера контейнеров (читай, клона containerd или cri-o) я наконец-то добрался до первых интерактивных результатов. Использовали docker run -i? Это примерно о том, как реализовать такую же фичу самостоятельно. На подходе docker run -i -t 🤓

https://twitter.com/iximiuz/status/1219011765943533570?s=20

О предыдущих этапах можно прочитать тут:

- conman - [the] container manager: inception https://iximiuz.com/en/posts/conman-the-container-manager-inception/
- Implementing Container Runtime Shim: runc https://iximiuz.com/en/posts/implementing-container-runtime-shim/
- Implementing Container Runtime Shim: First Code https://iximiuz.com/en/posts/implementing-container-runtime-shim-2/
В продолжение темы интерактивных контейнеров, описал свои приключения с реализацией этой функции в conman:

https://iximiuz.com/en/posts/implementing-container-runtime-shim-3/

#Go #Rust #Containers
И действительно, полностью согласен с результатами этого исследования. Книг про разработку или около-разработку вокруг очень много, но лишь малая часть из них по-настоящему стоящая и неустаревающая классика. Приятно осознавать, что в Top-5 выборки из исследования попали 4 книги из моего личного Top-5. А вот Clean Code by Robert Martin я, к своему стыду, все еще не прочитал.

https://threadreaderapp.com/thread/1229731043332231169.html
С этим вашим карантином работы почему-то стало больше, а свободного времени - заметно меньше. Так что создание собственного контента пришлось пока поставить на паузу. К счастью, кому-то все еще удается писать статьи. И вот одна новая и действительно классная:

"Surprising Things About Working at Well-Known Tech Unicorns"

https://blog.pragmaticengineer.com/surprising-things-about-working-at-tech-unicorns/