Потоки и цена переключения контекста
Из чата спрашивают, какое издание читают: отвечают, что последнее, четвёртое. Прежде чем говорить о потоках, ведущий вспоминает учебник того же автора по операционным системам — у него он был чёрным кирпичом из серии Classics in Computer Science, глава про процессы там разобрана заметно глубже, а книга про распределённые системы то и дело ссылается на функции, которые внутри одной машины даёт операционная система. Один из гостей разворачивает мысль: согласовывать ресурсы приходится уже внутри одной машины, между процессами и параллельными потоками, и те же проблемы рекурсивно повторяются, когда мы выходим за её пределы и добавляем сеть. Дальше — цена переключения: процесс тяжелее потока, а при прерывании больше всего времени уходит не на сам свитч, а на косвенный эффект: после обработчика кэш приходится набивать заново. В книге упоминается цифра в 80%, но участники тут же оговариваются, что всё зависит от того, как поставлен эксперимент.
Авторы дают четыре причины заводить потоки. Блокирующие вызовы: в однопоточном приложении один такой вызов останавливает всё, и следующий запрос ждёт, пока доработает предыдущий. Эффективное использование нескольких ядер — но только при аккуратной синхронизации, чтобы потоки не блокировали друг друга. Приложение из множества процессов вынуждено постоянно делать переключения контекста, а между потоками они дешевле. И структурирование приложения: отдельную работу можно вынести в поток, а не в процесс. Дальше — три варианта реализации: на уровне пользователя, на уровне ядра и их комбинация, с моделями «один к одному», «много к одному» и смешанной, которая лучше ложится на многоядерные системы. Участники смеются, что у гибридного варианта в таблице оказались одни плюсы и ни одного минуса. И формулируют претензию к книге: авторы не называют вещи своими именами — если бы описанное было прямо названо корутинами, читать стало бы проще.
Серверы: от Apache к автомату nginx
Зачем в книге про распределённые системы вообще глава про потоки, авторы объясняют на клиентах и серверах: держать нужно много логических соединений одновременно, и на большом трафике блокирующие вызовы — не выход. Классический процессный сервер поднимает отдельный процесс на каждый новый коннект клиента к базе данных, но на практике перед ним ставят потоковый прокси, который постоянно держит соединения. Один из участников напоминает: проектировать систему, не понимая, что открывается на входящий байт — поток или процесс, нельзя, потому что число соединений упирается в жёсткие ограничения. Александр вспоминает Apache, который форкал процесс на запрос, и nginx, вставший перед ним, когда тот перестал держать нагрузку: у nginx мастер-процесс и воркеры, и каждый воркер держит много коннектов и бегает по ним, продвигая конечный автомат запроса и докидывая клиенту очередной кусок ответа от медленного Apache позади.
Итог — «матрица два на два»: параллелизм против блокирующих и неблокирующих вызовов. Один процесс с блокирующими вызовами легко программировать, автомат с неблокирующими программировать сложно, зато он позволяет обслуживать десятки тысяч запросов в секунду на одном ядре. Метафора Александра: сеанс одновременной игры на тридцати досках, где гроссмейстер делает ход и идёт дальше, а состояние партии остаётся рядом с ним; в процессной модели к каждой доске пришлось бы ставить нового гроссмейстера. Из чата спрашивают, годится ли автомат, когда важен порядок операций, — порядок как раз и задан самим автоматом. Дальше четыре задачи сервера: конкурентное или итеративное исполнение; как найти, куда подключаться, — общепринятые порты, DNS, service discovery вроде Consul с его SRV-записью или суперсервер, который поднимает процесс по требованию; как прервать запрос — авторы шутят, что радикальный способ прервать взаимодействие с сервером это выключить машину; и, наконец, stateless против stateful.
Виртуализация, контейнеры и миграция кода
Виртуализация — промежуточный слой, отклеивающий программу от интерфейса конкретного железа. Начиналась она с портирования: мощные машины выходили чаще нового софта, и старый код надо было на чём-то исполнять; вторую жизнь дало облако с упаковкой зависимостей, лёгким перемещением и репликацией. Отдельная ценность — изоляция: клиенту облака нужно обещать, что в его мощности и процессы никто не залезет. Участники вспоминают железные серверы, где база и приложение интерферировали и понять, куда утекают ресурсы, было невозможно, — и признают, что шумные соседи никуда не делись: провайдер продаёт вчетверо больше ядер, чем есть, а под общей нагрузкой вы получаете четверть обещанного, при троттлинге и того меньше. Виртуализация процессов — это Java-машина или Wine с эмуляцией Windows API, а монитор виртуальных машин ставят поверх хостовой системы, у которой уже есть драйвера. Контейнеры в третьем издании 2017 года, по словам ведущего, ещё не были устоявшейся технологией, а Михаил вспоминает, что LXC в Авито использовали до всякого общего оркестратора: читали, как устроены cgroups, и руками писали конфиги распределения ресурсов.
Клиентскую часть проходят быстро: тонкий клиент вроде VNC гонит картинку по пикселям, X Window System вспоминают как источник многочасовой боли при настройке нестандартного монитора, а Chrome, по книге, сравним по объёму кодовой базы с Linux и фактически стал операционной системой для приложений — с отдельным процессом и песочницей на вкладку. Последняя тема — миграция кода: обычно данные несут к процессу, но можно принести код к данным, и участники напоминают, что именно отсюда вырос MapReduce, а из недавнего — федеративное обучение, когда данные нельзя выгрузить. Тот же ход работает на устройствах: раньше телефон отдавал вычисления серверу, теперь на нём применяют и дообучают модели. Классификацию из книги — удалённое исполнение, code on demand и мобильные агенты, про которых ведущий честно говорит, что не уловил замысел авторов, — дополняет деление на слабую и сильную мобильность: слабая переносит код и перезапускает его на новом месте, как под в контейнерном мире, сильная тащит ещё и сегмент исполнения.
Что стоит унести с собой
- 01Потоки берут не ради красоты: блокирующий вызов останавливает однопоточное приложение, переключения контекста между процессами дороже, чем между потоками, а при прерывании основная потеря — заново набиваемый кэш, а не сам свитч.
- 02Процесс на соединение масштабируется хуже конечного автомата: воркер nginx хранит состояние каждого висящего запроса и вытягивает десятки тысяч запросов в секунду на ядре — ценой того, что программировать его тяжело.
- 03Слои виртуализации надо считать и мониторить по отдельности: изнутри гостевой системы переподписка провайдера не видна, и вместо обещанных ядер под нагрузкой можно получить четверть.
- 04Миграция кода — это выбор между переносом данных к процессу и кода к данным; слабая мобильность просто перезапускает код на новом месте, сильная переносит и состояние исполнения, а это принципиально сложнее.