SuperOleg dev notes
1.87K subscribers
56 photos
149 links
Обзоры новостей и статей из мира frontend, интересные кейсы, исследования и мысли вслух

https://github.com/SuperOleg39

https://twitter.com/ODrapeza

@SuperOleg39
Download Telegram
По поводу мира вне наших компонентов.

Для SPA-приложений, самый простой кейс это запросы, критичные для отрисовки страницы. Для SSR тоже валидно если говорим про кастомную реализацию, мета-фреймворки все-таки решают кейс давая механизм для загрузки данных под конкретный роут.

Стандартный паттерн - делать запросы в useEffect. Но это не эффективно, не всегда логично, и не всегда хорошо для UX и перформанса.

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

Не эффективно, так как слишком поздно. Зачем ждать полный цикл рендера и отрисовки в браузере до старта запроса?

На конкретном примере, очень многие приложения используют React Router. До относительно свежей версии 6.4, этот роутер вообще не давал никаких инструментов для загрузки данных. Столкнулся с этим два года назад для пет проекта, удивился, не долго думая добавил костыль с загрузкой через тот же useEffect но c возможностью привязать запрос к компоненту страницы.

Поэтому для React Router появление механизма loader'ов для SPA-приложений это уже большой шаг вперед - в том числе для новых разработчиков, они будут видеть что запрос можно запустить где-то вне useEffect, и это нормально.

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

Как пример, приведу список отдельных модулей для фреймворка Tramvai.
Там все не идеально, такие модули как router / render / server связаны сильнее чем хотелось бы.
Но в любом случае по списку наглядно что все реализовано по отдельности, за счет модульной архитектуры:
- рендеринг и гидрация
- роутинг
- обработка серверных запросов и инициализация сервера
- работа с SEO
- логгер / метрики / куки / client-hints
- интеграция с React Query отдельным модулем

И работать со всем этим мы также можем по отдельности.

Например, используя React Query, сбросить или обновить кэш на любом этапе жизненного цикла запроса за страницей, начиная от механизма экшенов, или сделать префетч конкретной query до рендеринга страницы, а потом использовать ее в компоненте.
👍9
Про "все-в-React" или "все-в-компоненте" и декларативность.

Что мы видели интересного в коде компонентов:
- <Route> и <Redirect> из React Router
- <Script> из Next.js и <Scripts> из Remix
- useQuery из React Query или Apollo
- useForm из React Hook Form
- не могу не вспомнить <FormSpy> из Formik

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

Это не примеры абсолютного зла - так как плюсы без сомнения есть, в очень гибком роутинге, либо в умном механизме запросов без бойлерплейта, либо в минималистичной абстракции логики фреймворка от пользователя.

Но в масштабе, проблемы есть.

Хранить список роутов в компонентах - не масштабируется, подход с заранее объявленными роутами гораздо легче развивать и поддерживать.

Но декларативность в React мире доходит до того, что например в React Router вообще нет такой цельной сущности как Router! Есть только набор хуков, а сделать что-то с роутингом вне компонентов мы просто не имеем возможности.

Например (императивно) создать const router = new Router(), предзаполнить router.addRoute(...), в лоадере/экшены выполнить router.redirect(..), передать в <Router.Provider router={router} > и так далее.

Отсутствует жизненный цикл, настолько RR связан с реактом. Механизм loader'ов добавил хотя бы один этап жизненного цикла. Но для какой-нибудь авторизации разработчики все-равно будут создавать всякие <Auth> и <ProtectedRoute> компоненты.

Наглядный пример в этой статье - https://blog.logrocket.com/authentication-react-router-v6/. Хороший гайд, все аккуратно, react way, но насколько же сильно размазана аутентификация по компонентам, и не существует снаружи.

Просто сравните с Angular, где через DI предоставляется отдельный сервис для аутентификации, используемый и в UI и в гуарде роутера.

React Query большие молодцы, выделяют логику в отдельные сущности, и с ними можно работать где угодно, например QueryClient. Такие вещи очень упрощают интеграцию для SSR фреймворка.

Но и тут есть проблемы, возьмем сами квери. Как отдельной сущности их просто нет, есть набор параметров вида const query = useQuery({ queryKey: ['todos'], queryFn: getTodos }), и только через queryKey возможна связь для одной и той же квери между использованием в компоненте и прямой работой через QueryClient в других местах.

Сложно делать расширяемые и переиспользуемые query, параметры считываются и сохраняются сразу при рендере хука - нельзя сделать queryKey функцией, на момент рендера useQuery надо иметь все параметры для формирования массива ключей.

Используя React Query и React Hook Form, можно делать хороший UX и писать мало кода на сложные кейсы, но очень сложно явно выделить сущности / модели / бизнес-логику, что опять-таки может выстрелить в ногу в масштабе, и уж точно не поможет сделать архитектуру "кричащей".

По поводу таких кейсов как Scripts в Remix, или поддержка метаданных и стилей в React 19.

Реакт или Ремикс не дает нам условный AssetsManager, в который мы смогли бы добавить ресурс явно по конкретному условию, например:
if (analyticsEnabled) {
assetManager.addScript({ src: anaylicsScript, async })
}


Нам предлагается явно рендерить нужный скрипт в компоненте (прямо или косвенно). И хотя для RSC и стриминга понятно почему важно поддержать эти механизмы, выбранный путь мне совершенно не нравится.

Если подвести итог, эта псевдо-декларативность в React и экосистеме, как и явно обозначенная ограниченная область ответственности React (не фреймворк, а библиотека!), напрямую влияет на то как сообщество пишет приложения, на образ мышления новых разработчиков, и как мне кажется влияет негативно.

Получается для меня, главная проблема React это точно не лишние ререндеры, а архитектурные вопросы. И вряд ли экосистема в этом плане заметно поменяется, а область ответственности React только расширяется. Скорее всего в будущем при поддержке RSC и прочих современных возможностей, остро встанет вопрос что мета-фреймворк либо будет написан в react way стиле, либо останется в стороне.
🔥221👍1
TIL - поисковые боты могут парсить инлайн JS и JSON на странице и индексировать ссылки оттуда.

Почему это важно - для SSR приложений базовый механизм передать готовые данные (initial state) с сервера на клиент это как раз JSON в разметке, например как <script type="application/json">

Примеры проблемы:
- https://github.com/vercel/next.js/discussions/39377
- https://stackoverflow.com/questions/47210596/how-to-prevent-google-from-indexing-script-type-application-json-content

При этом адекватного решения проблемы не вижу.

Добавлять лишние кодирование - штраф к перформансу, даже пара ms повлияет на серверный рендеринг.
Собственно и так уже влияет - для JSON с initial state обязательно надо делать и перевод объекта в строку и экранирование, а на клиенте парсить обратно - это все не бесплатно (обычно вторая по нагрузке на CPU работа на сервере после renderToString, хотя и гораздо менее заметная)

Заодно скину ссылку как делаем экранирование стейта, там сразу парочка референсов и ссылка на возможные уязвимости - https://github.com/tramvaijs/tramvai/blob/main/packages/libs/safe-strings/src/encodeForJSContext.ts

Почему например не вынести в отдельный файл стейт - хорошо объясняется тут - https://github.com/vercel/next.js/discussions/42170#discussioncomment-8880248 (спасибо за ссылку @igor_katsuba)
👍93🔥1🤔1
Не так давно писал про интересное изменение в React 19 - последовательная загрузка компонентов в рамках Suspense границ - https://t.me/super_oleg_dev/181

Как я понимаю это сильно ударило по перфу SPA приложений, где нет возможности предзагрузить ассеты и данные параллельно как это делают при SSR.

В итоге будут искать более удачное / универсальное решение:
- https://github.com/facebook/react/issues/29898
- https://x.com/sophiebits/status/1801663976973209620?s=19
👍3
Также, очень приятно видеть в твиттере много довольных мейнтейнеров open source проектов - Microsoft поддержал финансово внушительный список проектов - https://x.com/jeffwilcox/status/1801794149815095495?s=19

Вдохновляет ❤️
6
Привет!

Мысли вслух про экосистему вокруг мета-фреймворков.

Мы привыкли использовать многие инструменты как CLI, например сборщики, но наличие JS API у таких инструментов открывает большие возможности.

Давно было интересно как работает Nitro, и как работает фреймворк Vinxi у которого под капотом Nitro + Vite, и у кого какая область ответственности.

И в целом интересно как так быстро и легко мета-фреймворки новые появляются.

С Vinxi оказывается верхнеуровнего все просто:
- Nitro - билдер и дев сервер для http сервера
- Vite - билдер и дев сервер для фронта

Под капотом у обоих rollup для непосредственно сборки.

Vinxi просто запускает одновременно либо оба dev сервера либо обе production сборки.

Тут сразу хочется опыт Remix вспомнить, который теперь "всего лишь плагин для Vite".

Это конечно не значит, что можно взять условный Vite, и все работает из коробки, код для интеграции писать нужно.

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

А например SSR, React Server Components, всевозможные file-system роутинги и мгновенные hot reload'ы это актуальные потребности на сегодняшний день.

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

Судя по всему у Vite это получается и какой-то баланс найден.

Также, не ясно насколько хороший результат получается, когда фреймворки (Remix, Vinxi, SolidStart, Vike и так далее) собраны из таких инструментов, вместо написания специализированного кода под свои кейсы.

Как минимум это большой буст к скорости разработки, особенно для небольших команд.

С другой стороны есть опыт Vercel, которые делают инструменты непосредственно под фреймворк - Turbopack и Next.js, да и прямо скажем существующая интеграция webpack там очень не простая и многослойная.

В перспективе у некста все должно быть круто, но на текущий момент много репортов на проблемы со скоростью сборки.

Но тут и команда разработчиков мощная, сложно не верить в ребят.

Очень интересно как дальше будет развиваться экосистема, и очень хочется самому на коленке собрать Tramvai на основе Vite/Nitro/Vinxi, и посмотреть так ли все с ними хорошо.
👍8🤔5
Привет!

За недавнее время появилось несколько статей про утечки памяти в JavaScript Closures:
- https://jakearchibald.com/2024/garbage-collection-and-closures/
- https://www.nico.fyi/blog/memory-issue-in-javascript-and-closures

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

Сначала пару моментов про наши приложения.

На tbank.ru активно используются микрофронтенды и SSR.

Серверный рендеринг с микрофронтами устроен так:
- для каждого микрофронта есть точка входа для Node.js окружения (условно header.server.js)
- сервер скачивает эти скрипты, получает строки с JS кодом
- выполняет строки как JS код в изолированном окружении через vm модуль
- на выходе получает обычные React компоненты

Приложения на tbank.ru построены на нашем фреймворке Tramvai, построенном поверх механизма Dependency Injection.

Особенность этого механизма, что на сервере, на каждый запрос пользователя, создается Dependency Injection контейнер. В этом контейнере хранится все, от объекта запроса до итоговой HTML строки которую мы отдадим в ответ пользователю.

Из-за этой особенности как правило факт утечки найти легко - каждый контейнер может весить несколько мегабайт, при утечке эти контейнеры не будут очищаться через Garbage Collector при завершении запросов.

В данном случае интересен именно механизм утечки и цепочка ссылок до контейнера, который не убирал GC.

На скриншоте профайлера, на самом деле сразу видно всю цепочку, но из-за ее особенностей раскопал причину не сразу.
🔥3
Раскручиваем начиная с конца, и это у нас - HttpClient.

В коде HTTP клиента есть безобидная строчка - создается стрелочная функция, если упростить:

createCache: createCache ? (cacheOptions) => createCache('memory', cacheOptions) : undefined,


И тут наше первое замыкание, которое еще само по себе не проблема.

Эта функция создается в контексте фабрики HTTP клиентов, где есть ссылка на некий commandLineExecutionContext - это служебный объект Tramvai который напрямую ссылается на Dependency Injection контейнер запроса.

Таким образом полный пример кода:

js 
const httpClientFactory = ({ ..., commandLineExecutionContext }) => {
const options = {
...,
createCache: createCache ? (cacheOptions) => createCache('memory', cacheOptions) : undefined,
}
}


Где Closure функции createCache теперь всегда ссылается на commandLineExecutionContext, который в свою очередь тянет ссылку на весь DI контейнер (`ChildContainer` на предыдущем скрине)
🔥7👍1
Идем дальше.

До утечки не было никаких криминальных изменений, в профайлере видно, что не очищается ссылка на трамвайный HTTP клиент, который теперь стал использоваться в модулях, которые занимаются всем вокруг загрузки и выполнения кода микрофронтов.

Но по цепочке видно, а также по приложенному стектрейсу, что утечка начинается изнутри кода микрофронтов, которые мы выполняем в изолированном контексте!

Начнем с функции - загрузчика getMM - HttpClient передается туда явно, из приложения, присутствие объекта в замыкании ожидаемо.

Максимально упрощенный код:

const getMM = ({ httpClient }) => {
...
}


Дальше, уже странность. Есть отдельный метод, он используется внутри функции загрузчика, но в его замыкании тоже есть ссылка на httpClient:

js 
const stringToObject = (data) => {
// именно тут в closure вижу httpClient
...
}

const getMM = ({ httpClient }) => {
...
stringToObject(data)
...
}


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

На примере выше, код превращается примерно в такой:

function getMM({ httpClient }) {  
...
var compiled = function stringToObject(data) {
// а вот и httpClient в замыкании :)
...
}(data)
...
}


Дальше разберем как эта ссылка утекает в код микрофронта.

Код в методе stringToObject вызывает vm.runInThisContext, которая нам уже отдает все что экспортирует код микрофронта, который в свою очередь экспортируем специальную фабрику.

Эту фабрику мы тут же вызываем с необходимыми аргументами, один из которых функция из этого же файла, условно:

const customRequire = (...) => { ... };

function getMM({ httpClient }) {
...
var compiled = function stringToObject(data) {
return vm.runInThisContext(data)(..., customRequire, ...)
}(data)
...
}


Конечно же, объявление функции customRequire переместилось и превратилось в анонимную функцию по месту использования:

function getMM({ httpClient }) {  
...
var compiled = function stringToObject(data) {
return vm.runInThisContext(data)(..., (...) => { /* а вот и замыкание для нашего httpClient! */ }, ...)
}(data)
...
}


На скриншоте оригинальный собранный код, только после форматирования в профайлере.
🤯13
Итак, а что же утекает?

Дело в том, что для предотвращения лишних запросов за серверным кодом микрофронтов и экономии на парсинге строки в код, результаты работы этого загрузчика кэшируются в LRU-кэше.

И вот эта маленькая и безобидная стрелочная функция со всем своим богатым Closure сохраняется в памяти приложения практически на все время его жизни.

В приложениях могут быть сотни страниц, на которых используются десятки разных микрофронтов.

А в Tramvai есть модуль прогрева кэшей, который на старте сервера делает запросы к каждому роуту приложения.

Таким образом сразу после релиза, кэши прогреты, приложения работают быстрее, но и потребление памяти растет сразу при наличии утечки.

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

Также, не до конца понятно на каких уровнях надо чинить утечку.

С одной стороны не хочется трястись над каждым замыканием и бояться создавать новые функции (а как мы видим это и тулинг может сделать без нашего участия).

С другой стороны хочется предотвратить возможность выстрела в ногу, и исправить такие места как createCache с ссылкой на commandLineExecutionContext.

В общем есть о чем подумать, интересны ваши мысли и опыт исправления таких вещей.
🤔8
Возможно будет история еще об одной утечке, связанной с Async Local Storage, но как минимум хочу рассказать про интересный кейс, связанный с отладкой этой проблемы.

Мы используем Fastify в качестве веб-сервера для Tramvai, разбираясь с утечкой поставил логи на хуки onRequest и onResponse.

Даю нагрузку на приложение через autocannon, обнаружил что onResponse логов меньше чем onRequest, хотя ответы от приложения приходят все.

Хорошо (хоть и поздно) заметил что не хватает ровно столько onResponse сколько запросов к параллельному вызову я указал для autocannon, условные 10 последних из 100 отправленных.

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

Обнаружил это через подписку на события finish и close объекта Request - оказалос что finish так же не вызывается эти 10 раз, а последние 10 close прилетают пачкой одновременно.

app.addHook('onRequest', async (request, reply) => {
request.raw.on('finish', () => { ... });
request.raw.on('close', () => { ... });
})


Понаставил логов в fastify, посмотрел исходники, и оказывается хук onResponse вызывается как раз на событие finish.

И это получается дефолтное поведение в Node.js, если Request был отменен клиентом (request.aborted), событие finish не срабатывает, даже когда reply.send(...) будет вызван после отмены и фактически завершит стрим.

Это кстати можно мониторить, есть гайд у fastify - https://fastify.dev/docs/latest/Guides/Detecting-When-Clients-Abort/#detecting-when-clients-abort

Вот такой вот случайно обнаруженный сайд-эффект из-за нюансов работы autocannon, который в теории может воспроизводиться и на продакшене, и получается onResponse не самый надежный вариант для очистки чего-либо после завершения запроса.
👍13
Крутой внутренний продукт вышел на публику - https://t.me/unidrawio

Пользуюсь регулярно, в том числе проектировал в нем Microfrontends Platform (о котором уже рассказывал в этом канале), приятно порекламировать.
❤‍🔥72👍2👎1
Привет!

Небольшой но интересный баг с микрофронтами, вебпаком и loadable.

Ранее я уже писал про интеграцию loadable для создания многостраничных микрофронтов Child Apps с разделением кода - https://t.me/super_oleg_dev/183

Вебпак собирает отдельные модуля в чанки, внутри они хранятся в мапе вида:

{
1234: function(...) { исходный код, экспорты/импорты },
5678: function(...) { исходный код, экспорты/импорты },
...
}


Где цифры - уникальные id этих модулей.
Модули из разных чанков после загрузки затем попадают в общую мапу, из которой вебпак будет доставать их при импорте.

Исследуя ошибку, увидел что в экспорте микрофронта получаю объект с переменными из нашего UI-kit.

Сначала грешил на Module Federation, так или иначе все стектрейсы проходят через него.

Но в итоге увидел, что вебпак ID для модуля микрофронта Foo в его чанке такой же, как ID модуля переменных UI-kit, внимание, в чанке другого микрофронта Bar.

Bar загружается раньше, его модуль попадает в общую мапу, и далее при импорте Foo, по указанному ID мы получаем эти переменные вместо микрофронта.

Проблема конечно же в глобальной мапе, в нашем случае это переменная:
window.__LOADABLE_LOADED_CHUNKS__


В которую попадает тысячи модулей.
А учитывая независимые сборки микрофронтов, попасть на такую коллизию было просто делом времени.

Оказывается, вебпак плагин Loadable переопределяет эту переменную для сборки.

Хорошо что есть из коробки возможность переопределить эту переменную, сделал уникальной для каждого микрофронта:
{
chunkLoadingGlobal: `__LOADABLE_LOADED_CHUNKS__child_${name}_${version}__`
}

Проверил разные кейсы, шаринг через Module Federation не пострадал, ошибка ушла.

Потом собрал без Loadable плагина, увидел что создается уникальное название, для моего чанка - webpackChunkchild_app_state_0_3_18, что можно увидеть и в дефолтах вебпака.

То есть проблема появилась только при интеграции нового функционала.

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

И тот случай где ну очень удобно делать отладку через Chrome Overrides, без необходимости публиковать обновленный код пачки микрофронтов.
👍16🔥61
Привет!

Попалась очень интересная статья от Gitpod, про их инфраструктуру для development окружений, и как они ушли от Kubernetes к кастомному решению.

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

В целом, много тем от которых я далек, и погружен не сильно, объяснить не смогу и оставлю ссылки из статьи.

Но всегда были интересно как устроены песочницы, такие как Codesandbox и Stackblitz, в блоге которых тоже попадаются классные статьи, иногда про них пишу - https://t.me/super_oleg_dev/141, и решаемые проблемы в статье Gitpod во многом с ними пересекаются.

Итак, Gitpod это классное облачное решение для разработки.

Такое development окружение имеет ряд особенностей:
- наличие состояния, которое так просто с одной ноды на другую не перенести - исходники, собранный код, кэши и так далее
- вообще не вариант терять изменения в исходном коде при разработке
- непредсказуемое потребление ресурсов, например пиковые потребления на сборку, и минимальные в остальное время
- безопасность, зачастую разработчикам нужен root доступ на ноде (вплоть до возможности развернуть свой Docker и k8s, да, внутри докера и k8s :crazy: ), это не должно аффектить другие нодыв кластере

Kubernetes был выбран для инфраструктуры Gitpod как очевидное решение, но столкнулись с рядом сложностей, и трудности в управлении и поддержки при масштабировании, и то что k8s заточен под контролируемые окружения и нагрузки, далее подробно разбираются отдельные кейсы.

Первая проблема - управление ресурсами, а именно CPU, память и сеть (пропускная способность).

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

Пробовали схемы с Completely Fair Scheduler (CFS), но он не предсказывает потребление, а увеличивает ресурсы когда их не хватает - то есть уже слишком поздно.

Если выделять статичные ресурсы, тут тоже проблема, разные процессы (даже VS Code запустит пачку процессов) в итоге могут конкурировать и CPU так же будет не хватать.

Пробовали приоритезацию процессов, но и там свои трудности, связанные с реализацией механизма.

В итоге все замиксовали и остановились на решении с динамическим выделением ресурсов (появилось в k8s) + CFS + приоритеты процессов основанные на cgroupsv2.
👍63
Следующая проблема - управление памятью.

Просто так выделить фиксированный X gb памяти на ноду - не вариант, оперативка в облаке это один из самых дорогих ресурсов.

Только появление swap-space в k8s позволило решить проблему перенаправления памяти между подами, потому что ранее это означало необходимость прибивать процессы.

В целом, поддержка memory swap практически убрала необходимость в перенаправлении выделенной памяти. Swap (или файл подкачки) - позволяет не активные данные сбросить из оперативки на диск, и обратно.

Далее, проблема - оптимизация производительности хранилища (I/O операции)

При работе с кэшами, бэкапами, большими образами, скорость чтения и записи очень влияет на производительность окружения.

Тут рассказывают про баланс между стоимость, скоростью и надежностью и какие решения смотрели (из всего знаком только с s3):
- SSD RAID 0 - очень быстро, но привязано к конкретной ноде, выйдет из строя конкретный диск - все данные потеряны. Этот подход используют на данный момент, таких инциндентов не было.
- Block Storage - виды хранилищ который привязаны к нодам, те же проблемы с потерей данных, медленнее, но широко распространены
- Persistent Volume Claims - k8s абстракция поверх реального хранилища в кластере, claim - по сути запрос на конкретный размер диска, нужные права и прочее. Гибко, но есть проблемы со временем привязки на старте, надежностью, и другие ограничения.

Бэкапы и восстановление дисков очень затратная операция, тут выбрано интересное решение:
- архивы заливаются в s3 и скачиваются оттуда
- архивы не сжатые
- дело в том что обычно упираются в CPU, а не в пропускную способность сети, и не выгодно сжимать и распаковывать эти архивы (но тут подчеркивают что важен баланс)

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

Используют похоже какой-то кастомный механизм - лимитер на основе cgroups2, нагуглил пример - https://andrestc.com/post/cgroups-io/
🔥9🤔1
Интересный кейс от коллеги из моей команды.

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

Рекомендую подписаться на канал Лёши, рассказывает про интересные кейсы из совершенно разных сфер нашей разработки.

https://t.me/royal_blue_dev
👍2
Forwarded from royal_blue_dev
Методология The Twelve-Factor App требует строгого разделения сборки и рантайма. Технически это значит что docker-образ приложения привязывается к хэшу коммита, на котором собирается. Главное – это позволяет релизить в прод именно тот код, который был протестирован на стенде. Такой подход дает определенные гарантии, однако, порождает и непростые ситуации:
• Продукт одной из команд – это NodeJS SSR приложение. Важная часть таких приложений, это stats.json файл, который содержит информацию о получившихся при сборке файлах. Сами файлы раздаются с CDN, соответственно и загрузка их туда является шагом сборки;
• Поскольку stats.json зашивается внутрь docker-образа и переиспользуется между деплоями с одного коммита, мы точно также должны переиспользовать файлы, загружаемые на CDN, поэтому на этапе сборки пакуем получившиеся файлы в .zip-архив и сохраняем на S3;
• Ссылки на файлы содержат хэши, которые должны быть идемпотентными, но это не всегда так, например, из-за багов в инструментах сборки. Таким образом, мы получаем критическую неочевидную связь образ => архив, так как для образа и архива хэши файлов должны совпадать. Несовпадение хэшей – риск полной недоступности приложения, поскольку приложение будет запрашивать файлы которых может не быть на CDN;
• Образы хранятся в Artifactory, который имеет политики очистки. В нашем случае, для нерелизных образов это 7 дней. Отсюда следует, что если кто-то создаст МР, задеплоит его, подождет 7 дней, а потом задеплоит приложение еще раз или откатится на коммит, для которого нет образа в Artifactory и при этом разойдутся хэши файлов – приложение не будет работать. Это не очень страшно при деплое, но катастрофа при откате: выпустили приложение => решили откатить => откатили и приложение все равно не работает;
• Вишенка на торте: у S3 тоже есть политики очистки, и здесь все работает с точностью да наоборот;

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

Не призываю отказываться от The Twelve-Factor App, считаю ее очень важной и оберегающей от множества ошибок, скорее хочу подсветить, что при разделении системы на части автоматически вылезают проблемы синхронизации состояния. Стоит учитывать это при проектировании.
👍5
Привет!

По возможности продолжаю обзор статьи - https://www.gitpod.io/blog/we-are-leaving-kubernetes

Итак, мы узнали про разные подходы к управлением CPU, памятью и дисковым пространством.

Следующая часть статьи - про оптимизацию старта и автоскейл.

Быстрый старт окружений критично важен для такого сервиса как Gitpod, но это требование конфликтует с желанием максимально утилизировать ресурсы.

Первое что не сработало - это попытка запускать множество воркспейсов на одной ноде, рассчитывали что общие кэши (возможно это про опцию host IPC - https://www.fairwinds.com/blog/kubernetes-basics-tutorial-host-ipc-should-not-be-configured) ускорят старт. Но оказалось что в k8s все равно есть накладные расходы на перемещение контента в нужное место.

Далее провели ряд экспериментов:

Ghost workspaces - с помощью кастомного планировщика задач создавали поды - пустышки, которые занимали место для возможного автоскейла и замены на настоящие поды. Оказалось медленно и ненадежно.

Ballast pods - эволюция предыдущего подхода, поды - призраки заполняли ноду целиком, что ускорило и удешевило замену.

Затем в k8s появился плагин автоскейлер который позволил убрать костыли и отлично решает проблему - https://kubernetes.io/docs/concepts/cluster-administration/cluster-autoscaling/

Для автоскейла при пиковых нагрузках разработали свою систему, которая управляет масштабированием в зависимости от частоты развертывания development окружений. Работает эта система с помощью старта так называемых pause images, что позволяет быстро по требованию подстроиться под нагрузки.

Pause image - это контейнер который хранит (занимает?) network namespace для пода, все прочие контейнеры созданные в поде будут использовать именно его, ссылки по теме:
- https://www.ianlewis.org/en/almighty-pause-container
- https://blog.scottlowe.org/2013/09/04/introducing-linux-network-namespaces/
👍6🔥3
Следующая часть - оптимизация процесса скачки (pull) Docker образов.

Образы воркспейсов могут занимать до 10гб (включает все тулзы доступные для разработчика), скачивать такое долго и дорого, опять таки сильно влияет на скорость старта воркспейса.

Я кстати подзапутался с терминами, но похоже так - воркспейс это чисто термин Gitpod, а прочие (нода/под/контейнер) уже термины k8s - https://bool.dev/blog/detail/chto-takoe-pods-nodes-containers-i-clusters-v-kubernetes

Тут много интересных оптимизаций:

Предзагрузка образов с помощью DaemonSet - https://aws.amazon.com/blogs/containers/start-pods-faster-by-prefetching-images/.
Плохо показывает себя при масштабировании так как при старте новых нод образ там еще не присутствует. Плюс предзагрузка теперь конкурирует за сеть и CPU с процессом старта воркспейса.

Максимальное переиспользование image layers - https://docs.docker.com/get-started/docker-concepts/building-images/understanding-image-layers/.
Даже создали тулзу для сборки образов - https://github.com/gitpod-io/dazzle.
Но поняли что не могут нормально замерять эффективность переиспользования. В причинах указано что-то про Open Container Initiative (https://github.com/opencontainers/image-spec/blob/main/spec.md), не знаком с термином, поэтому не разобрался в чем именно причина.

В любом случае мне кажется что для своих конкретных проектов, где есть проблема с размером образа и скоростью его сборки и загрузки, слои это перспективная оптимизация.
Еще ссылка по теме - https://www.ctl.io/developers/blog/post/caching-docker-images

Далее - предварительно запекание образов (сразу приходит в голову аналогия с запеканием теней и текстур в геймдеве). Образ воркспейса заранее сохраняли в образе ноды (тут должен быть мем pimp my ride). Ускоряет старт, но образы очень быстро устаревают. Также не работает для self-hosted использования.

Попробовали некий Stargazer и ленивую загрузку образов, похоже это про https://medium.com/nttlabs/startup-containers-in-lightning-speed-with-lazy-image-distribution-on-containerd-243d94522361.
Но это требует отдельную обработку каждого образа, плюс не все реестры контейнеров поддерживают.

Интересная кстати вещь этот Stargz Snapshotter. Суть ленивой загрузки образов - контейнер может быть запущен не дожидаясь завершения загрузки! А все необходимые части образа будут дозагружены по требованию (чисто Streaming Rendering в devops мире).
Графики в README показывают мощные результаты - https://github.com/containerd/stargz-snapshotter

Последнее это Registry-facade + IPFS. Сразу два незнакомых мне термина:
- https://github.com/httptoolkit/docker-registry-facade
- https://habr.com/ru/articles/314768/ (на секундочку межпланетная сеть)
Круто работает но сильно усложняет систему. У них есть про это отдельный доклад - https://www.youtube.com/watch?v=kS6aDScfVuw

В итоге нет одной идеальной оптимизации и все по сути набор компромиссов.
👍3🔥31
Представьте себе мир, в котором вы снова можете легко контрибьютить в JS тулинг 😊

Порадовал порт TS на Go, как человека которому Rust не даётся (и в целом похожи на хайптрейн тренды переписывать весь тулинг на него).

Хорошее объяснение почему Go - https://www.reddit.com/r/javascript/s/nUMcx2sIWt

Тут можно вспомнить попытку написать порт на Go автора SWC по таким же причинам - https://kdy1.dev/2022-1-26-porting-tsc-to-go
👍16👎3