Раньше ПО работало шустро, потому что иначе было никак

Раньше ПО работало шустро, потому что иначе было никак - 1

Несколько недель назад мы обсуждали Java-компонент, запускающий кластер Spark. Его основная задача — координация. Он поднимает всю необходимую инфраструктуру, прокидывает конфигурацию, дожидается нужных сигналов и отходит на второй план.

Моё изначальное предложение прозвучало просто: «Ему вполне должно хватить одного ядра и 2 ГБ RAM. Это же всего лишь лаунчер». Хотя даже 2 ГБ казалось будто бы мало, ведь речь о продакшене, а не о каких-то экспериментах на личном ноутбуке. Но как раз в таком мышлении и кроется проблема. В процессе развития сферы вычислений мы постепенно перестали всерьёз воспринимать небольшие числа при обсуждении ресурсов, так как дорожим устойчивостью системы. Но в продакшене нужно, наоборот, распоряжаться ресурсами более аккуратно.

Думаю, мы все знаем, как это бывает. Если вы уже варитесь в этом бизнесе какое-то время, то сами так поступали. Пайплайн даёт сбой — увеличиваем память. Сервис начинает тормозить — добавляем процессорных ядер. Роллаут прошёл с натягом — и вот дежурный разработчик уже закладывает огромный буфер, потому что никто не хочет снова подрываться ночью по тревоге. Большие числа работают, система выглядит надёжной, и мы движемся дальше.

В итоге через полгода исходный контекст напрочь забывается, и такая конфигурация становится для сервиса объективным стандартом. Уже никто не рассматривает её как временный фикс. Теперь — это жёсткое требование. Временная заплатка затвердевает и превращается в непреложный факт, и никто не хочет ставить это под сомнение — ведь система работает.

Получается эдакий парадокс. JVM десятилетиями оптимизировалась, сборщики мусора стали намного эффективнее, процессоры — быстрее, а облачные ресурсы выделяются по щелчку мыши. В теории мы должны буквально купаться в плодах этих достижений. Но по факту мы продолжаем их растрачивать, подтверждая закон Никлауса Вирта, сформулированный им ещё в 1995:

«Программное обеспечение замедляется быстрее, чем ускоряется железо».

Комитет экономичности одобряет выделение ещё 64 ГБ просто «на всякий случай»

Комитет экономичности одобряет выделение ещё 64 ГБ просто «на всякий случай»

К чему нас привёл прогресс

Но плоды достижений никуда не исчезли. Просто мы растратили их на раздутые рантаймы, бесконечные графы зависимостей, тяжеловесные контейнеры, тонны телеметрии, расширенные запасы прочности и стандартные шаблоны платформ, из-за которых все сервисы выглядят одинаково. Когда появление очередной высокоуровневой абстракции повышает эффективность программирования, мы не начинаем писать меньше кода или создавать более простые системы. Мы тратим эту эффективность на повышение сложности, дополнительные интеграции и увеличение числа подвижных компонентов.

Когда инженер задирает лимит памяти контейнера «про запас», эргономика JVM воспринимает этот запас как предложение ни в чём себе не отказывать. Как результат, дефолтный размер кучи увеличивается до установленной доли от выделенных ресурсов. Сборщик мусора начинает лениться, так как теперь есть где разгуляться. А среда выполнения с комфортом обживает тот простор, который ей предоставили. Программное обеспечение начинает есть больше не от того, что теперь нужно больше работать. Оно просто расширяется, поглощая тот ресурс, который ему выделили.

Да, отчасти это утяжеление оправдано. Я не хочу угодить в ловушку технической ностальгии. Но нам нужно провести черту между неизбежной сложностью и излишними тратами. Современные программы выполняются во враждебной глобальной среде. Часть их веса вполне оправдана — это и безопасность, и доступность, и распределённые системы, и соответствие стандартам, и наблюдаемость, и глобальное масштабирование. Сегодняшние системы выполняют намного больше всевозможной работы, чем старые. Наша же ошибка в том, что мы используем этот аргумент для защиты любой неудачной базовой конфигурации, возникшей в процессе работы.

Если проанализировать явные излишки, то в глаза бросаются раздутые деревья зависимостей, в которых значительное число подключенных библиотек запускаются в рантайме редко или не запускаются вовсе. У каждого слоя современного программного стека есть свой аппетит. Логирование, трассировка, SDK платформы, и базовый образ контейнера — все хотят свою долю. Причём по отдельности ни один из них вроде и не требует чего-то запредельного, но все вместе они уже способны превратить небольшую утилиту в неповоротливого монстра. Раздутость ПО не возникает на ровном месте, она становится результатом длинной серии вполне разумных ситуативных решений.

Раньше машины умели сказать «Нет»

В прошлом ПО работало быстро не от того, что разработчики отличались высокой сознательностью. Просто машина могла сказать «Нет». В 2000-х годах было нормальным запускать веб-сервис на двухъядерной или даже одноядерной машине с несколькими гигабайтами памяти. Сервер был скромным, поэтому код приходилось затачивать под эти рамки. Нужно было расставлять приоритеты, подстраивать систему и точно понимать, что конкретно делает этот сервис. Ограничения заставляли людей действовать вдумчиво и экономично.

Современная же инфраструктура куда менее строга. Мы с лёгкостью добавляем буферы, повторные попытки и стандартные слои платформы — просто на всякий случай. С одной стороны, эта аппаратная снисходительность позволяет нам создавать более масштабные системы, а с другой, постепенно превращает временные перерасходы в постоянные и уже как будто нормальные. Использование инстанса более высокого уровня может нивелировать запутанную архитектуру. Увеличение кучи — скрыть утечку памяти. А применение стандартного шаблона платформы — скрыть тот факт, что крохотный координатор наследует аппетит более масштабного сервиса — потому что выделение 2 ГБ здесь кажется какой-то шуткой.

Но есть и обратная сторона медали. Сервис, использующий лишь жалкие 10% от выделенных ему 64 ГБ, на графике мониторинга отражается красивым, умиротворяющим зелёным цветом. Для команды эксплуатации этот сигнал означает, что с ним всё в порядке, хотя на деле он маскирует возможные проблемы оптимизации под триумф операционной безопасности. Мы разработали исчерпывающие механизмы тревоги на случаи перегрузки систем, но редко создаём такие механизмы для проверки структурной пустоты.

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

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

Теперь отдувается железо

Заткнуть проблему железом бывает несложно, и зачастую это действительно является экономически оправданным решением. Человеческий труд намного дороже машинного. Если трата нескольких долларов на вычисления способна сэкономить дорогостоящее время разработчика, то ручная оптимизация кода станет неудачным вложением средств.

Но проблема в том, что аппаратное масштабирование становится как будто единственным решением. Когда так происходит, любое узкое место в системе превращается исключительно в проблему выделения ресурсов. Если мы видим, что нода перегружается, то для стабилизации системы переходим на более крупную. Но здесь кроется подвох, потому что стабильность может иметь под собой два совершенно разных фундамента. Иногда стабильная система имеет действительно стройную форму. В других же случаях мы просто заливаем проблему деньгами, чтобы она не вскрылась ночным сбоем.

И второй вид можно легко по ошибке принять за успех, так как прод работает исправно. В итоге возникает чувство «победы», и мы забываем, какой ценой эта победа нам досталась.

Избыточность живёт за чужой счёт

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

В итоге раздувание продолжается, даже если его никто не одобряет, потому что циклы обратной связи нарушены. Когда сервис падает, фиксируется инцидент, проводится разбор полётов и возникает ощущение неотложности. Если же сильно тупит корпоративный инструмент, то это не порождает ничего, кроме ежедневной волокиты. Любой, кто пользовался кривым корпоративным софтом, знает, что эта медлительность никогда не переходит в разряд инцидентов. Она просто превращается в фоновые издержки при выполнении работы.

Решение: бюджетирование ресурсов

Ответом на эту проблему станет введение бюджетов на ресурсы — простых, занудных и жёстко фиксированных бюджетов. Тогда и лаунчеру будет выделен свой бюджет, и сервис получит стартовый бюджет, и контейнер получит бюджет на размер. Когда же выданный лимит будет превышен, кому-то придётся объяснить, что конкретно изменилось, и чем реально оправдывается этот лишний расход.

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

Если простому лаунчеру Spark действительно нужно тридцать гигабайт, хорошо. Но покажите мне «чеки». Что именно загружается при запуске? Что постоянно висит в памяти? Какая часть этой памяти действительно обеспечивает отказоустойчивость системы, а какая лишь оплачивает проценты на старые, давно забытые решения?

Если никто не сможет ответить на эти вопросы, то такое выделение памяти — это не инженерия, а суеверие. Поэтому в следующий раз, когда какой-то отдельный компонент потребует огромных ресурсов, не нужно оправдывать его, оглядываясь на аппетиты соседних сервисов. Оцените структуру выполняемой им работы. Задайтесь вопросом, что конкретно делает тот или иной механизм, и что нам нужно изменить, чтобы перестать панически бояться скромных цифр.

Автор: Bright_Translate

Источник

Оставить комментарий