Мне 37 лет, что по программистским меркам равняется 99 годам. Я достаточно стар, чтобы помнить первые дни публичного Интернета и первых интернет-провайдеров. Впервые я вышел в онлайн через провайдера, который назывался Internet Access Cincinnati (IAC). Он предоставлял доступ по диалапу к серверу Sun SparcStation 10, где пользователи могли запускать почтенные в своей древности терминальные приложения вроде elm (почтовый клиент), emacs, lynx (текстовый веб-браузер), и конечно IRC.
Позже добавили возможность звонить на терминальный сервер CSLIP (предшественник PPP) и подключаться напрямую к Интернету с собственного компьютера под Linux или Windows (при наличии Trumpet WinSock) с настоящим IP-адресом.
Но вернёмся к той SparcStation. Машина была оборудована двумя CPU, которые работали на чудовищной частоте 33 Мгц, и она могла вместить аж 512 МБ памяти, хотя я сомневаюсь, что слоты там были забиты по максимуму. Оперативная память очень дорого стоила в те времена. Сервер с такими скромными ресурсами обслуживал 50-100 активных пользователей одновременно, обрабатывал почту для десятков тысяч, держал IRC-чат, поддерживал ранний HTTP 1.0 через NCSA HTTPd и добровольно выполнял роль FTP-зеркала для Slackware Linux. В целом он неплохо справлялся с нагрузкой и часто показывал аптайм 1-2 месяца.
Уверен, вы предчувствуете напыщенную речь о том, как в наше время распухли программы. Если так, то вы правы. Но отличие этой речи от других подобных в том, что она выдвигает логическую гипотезу, которая может объяснить главные причины такого разбухания. На мой взгляд, это последствия того, что очень простые варианты дизайна прошлого пошли не по той дороге.
Я вспомнил SparcStation, потому что хотел бы начать с очень глупого вопроса: зачем нам виртуализация? Или контейнеры? Как мы пришли к этому взрыву сложности вложений ОС->VM->контейнеры->… вместо простоты многопользовательских операционных систем?
Виртуализация обходится дорого
Мой стартап ZeroTier (прошу прощения за рекламу) работает на облачной инфраструктуре, разбросанной по многим дата-центрам, провайдерам и континентам. Большинство узлов, которые составляют облачное присутствие, выступают стабильными опорными точками в Интернете и последними ретрансляторами. Они принимают и отправляют пакеты. Им нужна полоса и немного CPU, но очень мало памяти или места на диске. Взять к примеру один из резервных ретрансляторов TCP (TCP fallback relay): он обычно передаёт 5-10 Мбит/с трафика, но программному обеспечению требуется лишь 10 МБ памяти и менее одного мегабайта (!!!) места на диске. Он также использует менее 1% ресурсов CPU.
Однако его виртуальная машина занимает 8 ГБ места на диске и по крайней мере 768 МБ памяти. Всё это нужно для хранения полной базовой инсталляции CentOS Linux, полного комплекта стандартных приложений и инструментов, системных сервисов вроде systemd и cron, а также сервера OpenSSH для удалённого доступа. Большое потребление RAM — прямое следствие виртуализации, эдакого «хака, чтобы обмануть ядро, будто оно работает на собственном оборудовании». Вся эта память должна быть доступна VM, потому что ядра как будто работают на отдельных машинах со своей локальной памятью, так что гипервизор вынужден подчиниться. (Современные гипервизоры могут в некоторой степени резервировать память про запас, но излишнее резервирование повышает риск ухудшения производительности при резком повышении нагрузки).
Контейнеры немного более эффективны, чем гипервизоры. Они не задействуют собственное ядро Linux и не тратят лишнюю память на гипервизор, но по-прежнему несут в себе по крайней мере часть всего дистрибутива Linux всего лишь для выполнения одной или нескольких программ. Контейнер для запуска 10-мегабайтной программки может весить несколько гигабайт, а для управления контейнерами нужен свой собственный пухлый набор приложений.
Спросите кого угодно, зачем нам всё это нужно. Он скажет о безопасности, изоляции и масштабируемости — и будет прав.
В старые времена, до виртуализации и контейнеров, компаниям вроде ZeroTier приходилось размещать собственное оборудование в дата-центрах. Это неудобно и не очень выгодно. Виртуализация позволяет обслуживать сотни пользователей с одного очень мощного сервера (моя виртуальная машина на 768 МБ, вероятно, крутится на 16-24-ядерном монстре Xeon с 256+ ГБ памяти).
Сотни пользователей — это же как… хм… та старая SparcStation на 33 МГц...?
Сегодняшний софт на порядки более громоздкий и сложный, чем программы, которые работали на старом сервере IAC, и хотя отчасти это стало результатом увеличения уровней абстракций и ненужного распухания, но при этом нельзя не отметить реальное увеличение функциональности и гигантский рост обрабатываемых данных. Я не говорю, что мы должны впихнуть нагрузку от сотни типичных современных пользователей в кофеварку. Но я считаю, что мы должны иметь возможность вместить хотя бы несколько сайтов WordPress (это типичный пример задачи, которую принято размещать в виртуальной машине) на Raspberry Pi — на компьютере, который примерно в 100 раз превосходит старый сервер по мощности CPU, в несколько раз по RAM и в 10-20 раз по объёму постоянной памяти.
RPi стоит $30 и потребляет менее 15 Вт. Нужно ли говорить, сколько стоит одна монструозная VM и сколько она потребляет?
Время для Pi!
Проделаем маленький мысленный эксперимент, где попытаемся установить RPi в качестве терминального сервера для группы пользователей, которые хотят вести блоги WordPress. Веб-сервер и небольшая база данных вместе займут примерно 10-20 МБ памяти, а у нашего RPi — 1024 МБ, так что он сможет хостить по крайней мере 50 сайтов маленького или среднего размера. (В реальности большая часть RAM избыточна или неактивна, так что со свопом или KSM наш RPi захостит несколько сотен сайтов, но будем консервативными).
Сначала установим Linux. Это наследник Unix, многопользовательская операционная система (верно?), так что создадим 50 аккаунтов. Каждый из этих пользователей теперь может залогиниться. Входящая сессия SSH и шелл занимают всего один-два мегабайта в памяти, а у нашего RPi больше тысячи, так что на данный момент всё должно идти гладко.
Да, да, пока что игнорируем безопасность. Позже займёмся этим вопросом. Но если никто из наших пользователей не будет плохо себя вести, то все они сейчас скачивают и распаковывают WordPress. Всё хорошо. Проблемы начнутся, когда они приступят к установке.
Шаг первый: установить базу данных MySQL.
А, так это просто! Набираем "sudo apt-get install..."
Погодите… sudo? Предоставление прав sudo означает, что вы с тем же успехом могли и не заводить отдельные аккаунты для пользователей.
Оказывается, что вы можете установить MySQL в собственную домашнюю директорию. Если хотите скомпилировать его или вручную распаковать пакет Debian и отредактировать какие-то конфигурационные файлы, то можете сделать это без специальных привилегий. Ему нужно будет использовать папки /home/yourname/… но в итоге вы получите собственный локальный сервер MySQL, работающий в вашем собственном локальном пространстве пользователя.
Шаг второй: сконфигурировать веб-сервер для работы PHP.
Не будем ворчать… снова используем "sudo apt-get install", чтобы загрузить все необходимые компоненты, собрать их вместе и запустить. Оказывается, это тоже можно сделать, и после бритья быка [бесполезные на первый взгляд действия, которые на самом деле необходимы: например, мы решаем проблему, которая решает другую проблему, которая через несколько уровней рекурсии решает реальную проблему, над которой мы работаем, слэнг Массачусетского технологического института — прим.пер.] наш собственный веб-сервер с PHP готов к работе.
Затем вы сталкиваетесь с чем-то вроде "bind: permission denied". Хм? Немного покопавшись, вы находите, что порт 80, то есть веб-порт по умолчанию, является привилегированным портом. Вам нужны права рута, чтобы привязаться к нему.
Неважно. Просто изменим порт. Но этой значит, что теперь всем придётся вводить URL вашего сайта с дурацким :### на конце хоста, чтобы подключиться к нестандартному порту.
Яйца!
Сейчас вы осознали, что без ограничения на привилегированные порты всё значимое в системе Unix не следовало оставлять под рутом и тогда вы могли по крайней мере обеспечить лучшую безопасность системных сервисов. Но не поэтому я назвал этот раздел «яйца». Я назвал его так, потому что для понимания более широких последствий ограничения на привилегированные порты нужно отвлечься на минуту и поговорить о яйцах.
Яички — это жизненно важный орган. Размещение их в маленьком мешочке за пределами мужского тела было глупо, не говоря уже о странном внешнем виде. Самки нашего вида сконструированы более логично: их яичники спрятаны глубоко внутри живота, где они гораздо лучше защищены от ударов, пинков, мечей, заборов и старых тёток.
Зависимость от первоначально выбранного пути — концепция из экономической теории и эволюционной биологии — пытается объяснить, откуда берутся такие причудливые конструкции. Суть в том, что решения, принятые в прошлом, ограничивают в принятии решений, которые легко могли бы быть приняты в настоящем.
Мужские яички находятся за пределами тела, потому что определённые ферменты в мужской сперме лучше функционируют при температуре чуть ниже температуры тела. Это «решение» было принято очень давно, вероятно (если верить самой простой и поэтому наиболее вероятной гипотезе), когда мы либо не были теплокровными, либо имели меньшую температуру тела. Репродуктивные органы — это то, что биологи называют крайне консервативной системой, то есть они не склонны часто изменяться, потому что любые мутации репродуктивной системы с большой вероятностью вычеркнут особь из генетического пула. В результате, «легче» (в смысле пересечения эволюционного графа вероятностей) поместить мужские яички в пикантного вида мешочек между ног, чем вернуться назад и эффективно переработать сперму для работы при более высокой температуре.
Даже если бы мутации для функционирования спермы при более высокой температуре были настолько же вероятны, как те мутации, которые привели к появлению сленгового слова «зависать», не факт, что это бы произошло. Может быть, эти конкретные мутации не произошли бы из-за случайности, не оставив эволюционной системе самообучения никаких вариантов, кроме… э-э-э… неудачного. Ладно-ладно, я закончил.
Технологическую эволюцию движут разумные (по крайней мере, так мы считаем) агенты, но во многих отношениях она похожа на биологическую эволюцию. Как только принято решение, устанавливается путь для будущих решений и система с большей вероятностью движется по проторенной дороге. Иногда изменение старого решения обходится дороже, чем смириться с ним, а иногда это простая инерция.
Возьмите эти странные круглые 12-вольтовые штекеры в автомобилях. Изначально они предназначались для прикуривателей сигарет. Когда стали популярными различные портативные электронные устройства, инженеры задумались, как подключить их к автомобилю. Договориться с автопроизводителями на установку розетки не удалось, так же как уговорить пользователей установить розетки самостоятельно механическим способом, но поскольку во всех автомобилях есть прикуриватели… ну тогда… ответ очевиден. Сделаем розетку, которая помещается в разъём прикуривателя. Теперь автомобили часто даже не выпускают с прикуривателями, но в них есть эта розетка в разъёме прикуривателя, потому что куда же человеку подключить свой смартфон?
(USB постепенно заменяет прикуриватели, но в большинстве машин они по-прежнему установлены).
Не выбранный путь
К этому моменту вы должны были понять, к чему мы ведём. Unix изначально был разработан как многопользовательская система с разделением времени, и с этой целью добавлялись многие функции, чтобы ресурсы системы можно было использовать совместно. Там есть система разрешений для доступа к файлам с пользователями и группами, а на новых системах досконально отрегулированные списки контроля доступа. Там есть квоты на каждого пользователя по памяти, диску и использованию CPU, чтобы какой-то один пользователь не захватил все ресурсы системы. Там есть изоляция процессов, защита памяти и так далее. Но по какой-то причине никто даже не подумал адаптировать её сетевой слой для мультиаренды (мультитенантности) сетевых сервисов. Может быть, в то время привилегированные порты по-прежнему имели смысл и были уместны. Или просто никто не подумал об этом.
Привилегированные порты изначально были функцией безопасности. В те времена все компьютеры принадлежали организациям и управлялись штатными системными администраторами, а сети были закрыты для посторонних устройств (если такие устройства вообще существовали). Список портов, к которым имеет доступ только рут, позволял любому системному сервису знать, что какое-то входящее соединение инициировано программой с разрешения системного администратора.
В начале 90-х, когда Интернет начал становиться публичной сетью, многие системы и сети по-прежнему работали таким образом и полагались на привилегированные порты как на критический механизм аутентификации. Так что когда начали распространяться веб-сайты и каждый захотел иметь собственные веб-серверы на собственных IP-адресах, показалось более концептуально очевидным и менее хлопотным реализовать виртуализацию операционных систем и оставить остальную часть стека в неприкосновенности.
Виртуализация ОС полезна для других целей: для тестирования и отладки кода, для поддержки древнего ПО, которое должно работать на старых (или иных) операционных системах или версиях ядра, и это подливает масла в огонь. В конце концов, вместо адаптации сетевого стека Unix к нуждам мультиаренды мы просто закинули коробки внутрь коробок и пошли дальше.
Всё это сделано за счёт лишнего расхода железа и энергии. Рискну предположить, что тот же 16-24-ядерный Xeon с примерно 256 ГБ RAM, на котором размещается, наверное, меньше сотни 768-мегабайтных VM, мог бы хостить тысячи задач пользователей, если бы они работали напрямую на том же ядре, а не были обвешаны гипервизорами и пухлыми контейнерами. Насколько меньше CO2 попадёт в атмосферу, если каждый дата-центр уменьшить в десять раз?
Контейнеры вроде Docker частично решают проблему. Вы можете сказать, что контейнеризация — это именно та мультиарендность, к которой я стремлюсь, разве что она частично придерживается наследия виртуализации, воспринимая системные образы как гигантские статично связанные бинарники. Это также шаг назад в удобстве пользования. Мне по-прежнему нужны рутовые права, чтобы запустить контейнер. Я не могу (легко) залогиниться в один контейнер и запустить другой, как я делаю с процессами на простой мультиарендной машине Unix. Вместо этого приходится выстраивать массивные консоли центрального управления и инструменты «оркестровки».
Так что можно сделать? Как мог выглядеть путь, по которому мы не пошли?
Мультиарендная работа в сети
В некоторых случаях зависимость от выбранного пути проявляет себя, потому что так много новых решений принято на основе старых, что выходит слишком дорого вернуться и изменить старое решение. Мне кажется, что в этом случае несколько простых изменений могли бы перемотать 20 лет истории DevOps и направить нас по другому пути — к гораздо более простой и кардинально более эффективной архитектуре. Это не решит всех проблем, но устранит некоторую сложность в большинстве типичных сценариев.
Шаг первый: убрать привилегированные порты. Думаю, это изменение на 1-2 строчки. Вероятно, достаточно просто удалить оператор if
. Сломается всё, что полагалось на не-криптографическую защиту вроде номера порта для безопасности, поскольку такие вещи легко подделать.
Шаг второй: расширить пользовательские и групповые разрешения, а также права владения на сетевые ресурсы, введя маски разрешений UID/GID на чтение/запись/привязку (привязка вместо выполнения) для устройств и IP-адресов. По умолчанию вызов bind()
не указывает, что адрес (0.0.0.0 или ::0) будет прослушивать пакеты или соединения на всех интерфейсах, для которых текущий пользователь имеет соответствующее разрешение на привязку, а исходящие соединения по умолчанию будут направляться на первый интерфейс, принадлежащий пользователю.
Шаг третий: вероятно, хорошей идеей будет разрешить в пространстве пользователя создание виртуальных сетевых устройств (tun/tap) по той же причине, по которой пользователи могут создавать свои процессы и файлы. Это не критично, но было бы хорошо. Разрешения для программ вроде tcpdump/pcap тоже придётся изменить в соответствии с новой моделью разрешений для сетевых ресурсов.
Теперь любой сервис можно запустить под обычным пользователем. Рутовый доступ нужен только для обновлений ядра системы, изменений аппаратного обеспечения или драйверов, а также для управления пользователями.
Не выбранная дорога уже заросла
Поскольку мы выбрали путь виртуализации и контейнеризации, то позволили атрофироваться способностям к многоарендности в Unix. Понадобиться некоторая работа, чтобы вернуть их в форму. Но мне кажется, что это может стоить того — ради простоты и для устранения излишнего потребления ресурсов.
Нам нужно вернуться назад и усилить защиту режима пользователя. Не думаю, что это слишком тяжело. Виртуализация не обеспечивает такую защиту, как многие думают, и атаки вроде Rowhammer доказали, что VM — не панацея. Мы потратили невероятное количество человеко-часов на разработку крепкой и безопасной виртуализации и на разработку цепочки инструментов для этого, не говоря уже о том, сколько потрачено на создание экосистемы контейнеров. Думаю, что части этих усилий было бы достаточно, чтобы защитить user-space на минимальном хосте Linux от атак с повышением привилегий и утечек.
Нам нужно подтянуть другие аспекты изоляции и внедрить опции для ограничения того, что пользователь может видеть с помощью команд вроде ps и netstat — информацию следует ограничить только ресурсами пользователя. Нужно изменить пакетные менеджеры, чтобы разрешить установку пакетов в поддиректорию домашней директории пользователя, если он не рут, и т. д. Вероятно, изменения также потребуются для системных элементов вроде динамического компоновщика, чтобы пользовательским бинарникам было проще делать выбор в пользу общих библиотек в своей собственной локальной области, а не в пользу системных библиотек, если таковые имеются. Было бы хорошо, если бы системные сервисы init поддерживали и сконфигурированные пользователем сервисы, чтобы пользователям не приходилось мастерить скрипты watcher и задания cron, а также прибегать к другим хакам.
Конечный результат будет немного похож на контейнеризацию, но без неуклюжести, раздутости, изобретения велосипеда и неудобства. Вы можете развёртывать приложения в git, запускать git checkout и git pull по ssh, а оркестровка будет выполняться либо локально, либо на манер P2P, и вы сможете просто залогиниться на машину и запустить что-то, без необходимости продираться через сложную инфраструктуру управления контейнерами. Приложения станут легче, потому что большинству программ достаточно общих стандартных библиотек (libc, stdc++ и др.) и стандартных инструментов в системе, если нет каких-либо непреодолимых ограничений, вроде проблем с с совместимостью библиотек.
Заключение
Вероятно, всё это дела давно минувших дней. Скорее всего, в реальности будет выбран путь разработки по-настоящему безопасной контейнерной мультиаренды, так что с помощью контейнеров будет решена задача, которую следовало решить расширением модели разрешений Unix на сетевую подсистему в пространстве пользователя.
Цель этой статьи — показать, как маленькие решения, о которых никто не думал, могут повлечь драматические последствия для будущей эволюции технологии (и общества). Решение 1970-х годов использовать номера портов как встроенный сигнальный механизм для проверки безопасности между системами может оказаться ошибкой на триллион долларов, которая направила эволюцию платформы Unix по пути гораздо большей сложности, стоимости и большего потребления ресурсов.
Но погодите, может, не всё ещё решено. Существует более десятка дистрибутивов Linux, и большинство из них работает примерно одинаково. Переход на новую парадигму станет интересным способом выделиться для одного из этих дистрибутивов. Первым делом нужно реализовать сетевые разрешения вроде описанных выше — и предложить патч для ядра. Для обратной совместимости можно сделать так, например, что разрешения активируются через настройку sysctl, или можно выпустить модуль (если модули способны производить такие глубокие изменения).
Автор: m1rko