zero deps
57 subscribers
1 photo
27 links
Node.js, V8, highload, архитектура.
Где производительность, а где оверинжиниринг?

Код, профилирование и воспроизводимые бенчмарки.

Всё, что ставим — потом и поддерживаем.
Фиксим то, что сами и сломали.

zerodeps.tech
Download Telegram
1. System design начинается с вопросов

Обычное утро понедельника. Ничего не предвещало беды. Ты наливаешь кофе, открываешь бук, пуллишь проект и начинаешь читать ночные коммиты от коллег. Тишина, спокойствие, идиллия. 

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

Бизнес хочет ленту как у Threads. 
Какие сроки?
” 

И вот тогда это происходит. Момент, когда дыхание замедляется. Ты переживаешь весь спектр эмоций. Сонм мыслей: "Какая вам… лента? Мы же сайт для учёта бобров", "А Твиттер вам не написать?". Мимо пролетают стадии от гнева до принятия. 

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

За красивой формулировкой про "ленту" нет конкретики. Ничего измеримого. Буквально ты не знаешь ничего. 

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

А что ты можешь сделать? Для начала пишешь PM: “Мне нужно собрать требования. Организуй встречу”. 

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

Вот почему system design начинается с вопросов, а не со схем. Вопросы это первая ступень к архитектуре. Архитектуре, которая будет работать в реальном мире, а не на бумаге. 

Какие вопросы задавать и зачем, я сейчас расскажу.
1👍1
Почему вопросы это не просто формальность 

Вся эта история с вопросами кажется банальной. Ну спросил, ну ответили, ну записал. Что тут сложного? А сложного тут всё. 

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

Твоя задача перевести эти хотелки в технические требования. И вот тут начинается самое интересное. 

Каждое "хочу" скрывает десятки неявных предположений. Бизнес не думает о том, что лента должна работать при 10k RPS, что данные могут быть неконсистентными первые 5 секунд, что при падении сервиса пользователи должны видеть кеш, а не белую страницу. 

Они просто хотят, чтобы "всё работало". А что значит "работало" это уже твоя головная боль. 

Вопросы не придирки и не попытка усложнить простую задачу. Это способ вытащить на свет все те скрытые требования, о которых бизнес даже не подозревает. 

Без этого ты будешь строить систему вслепую. Сделаешь что-то, что технически работает, но не решает реальную проблему. Или решает, но с такими компромиссами, что через полгода придётся всё сжечь и написать заново.
2👍1
Какие вопросы задавать 

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

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

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

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

Не существует серебряной пули. Каждое простое решение скрывает за собой сложность. Когда бизнес просит "ленту как у Threads", он просит именно такую пулю, магическое решение, которое решит все проблемы без компромиссов. Твоя задача показать, что такой пули не существует, и помочь выбрать правильные компромиссы. 

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

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

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

Твои вопросы должны вытаскивать эти противоречия на поверхность. Что важнее: время разработки или время отклика? Стоимость инфраструктуры или доступность? Простота поддержки или богатство функций? 

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

Без этих вопросов ты будешь строить систему, которая решает не ту проблему. Или решает правильную проблему, но с неправильными компромиссами. Или с правильными компромиссами, но в неправильном контексте.
👍2🔥1
Как задавать вопросы 

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

Есть два типа сложности: сущностная и случайная. Сущностная это то, что нельзя убрать, не изменив саму задачу. Случайная это то, что мы добавляем сами, неправильно понимая требования. Твои вопросы должны выявить сущностную и минимизировать случайную. 

Начни с широкого контекста. Собери всех заинтересованных лиц и задавай открытые вопросы. Как сейчас работает процесс? Что больше всего бесит в текущем решении? Что будет, если ничего не менять? Цель понять общую картину, не вдаваясь в детали. 

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

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

Не принимай ответы типа "ну, как обычно" или "сделайте нормально". Докапывайся до цифр. Если говорят "быстро", спрашивай "Что для вас значит быстро? Сколько миллисекунд, секунд?". Если говорят "надёжно", спрашивай "Что считать сбоем? Сколько девяток доступности? Что видит пользователь при сбое?".

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

Время для взрослых разговоров. Что важнее: скорость или надёжность? Простота или функциональность? Бюджет или качество? Не пытайся угодить всем. Лучше честно сказать "это требование увеличит сроки в два раза" и дать бизнесу принять решение. 

Оформи всё в документ. Не в виде списка пожеланий, а в виде конкретных, измеримых требований. "Система должна поддерживать тысячу одновременных пользователей" вместо "должна быть надёжной". "Время ответа API не должно превышать 200 миллисекунд в 95 процентах случаев" вместо "должна работать быстро". 

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

Посмотрим как это работает на примере.
👍2🔥1
Cценарий 1: Сам решил

PM приходит с задачей. "Бизнес хочет ленту новостей. Сделайте как у Threads". 

Ты спрашиваешь: "А что именно имеется в виду?

PM отвечает: "Ну, лента. Где посты идут в хронологическом порядке, с лайками и комментариями"

Вроде понятно. Ты киваешь, но в голове уже крутится: "А что, если пользователей будет много? А если нагрузка вырастет? А если понадобится поиск?".

Хоронишь эти вопросы в себе и накидываешь схему. Таблицы posts, likes, comments. API для получения ленты. И вроде красиво. Микросервисы. Кафка, прости господи, чтобы общались. Valkey за кэш. Поиск на эластике. Ну и докер, кубер, графана, прометеус. Всё как у взрослых.

Через месяц выкатываешь на прод. Сотня пользователей заходит одновременно, и система падает. 

Бизнес задаёт неудобные вопросы: "Почему ничего не работает? Почему так медленно? У Threads же быстро работает!" 

Ты начинаешь оптимизировать. Добавляешь кеши, тюнишь Kafka, добавляешь реплики. Каждая правка ломает что-то ещё. Через полгода у тебя зоопарк микросервисов, devops-шапито и система, которую никто не понимает, и все боятся трогать. Поздравляю, ты создал legacy. 

Cценарий 2: Наорал

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

Ты задаёшь вопросы. Сначала PM. Кто пользователи? Сколько постов в день? Кто пишет? Что пишут? Как часто заходят? А сколько пользователей через пол года, год? Что важнее, скорость или актуальность? Что считается сбоем? Можно ли показывать устаревшие данные?  А нужна аналитика? А архивировать будем? Поиск нужен? Реагируем на лайки сразу? Комментарии сразу видно? А подписки? А уведомления? И так далее.

Потом идешь вместе с PM пытать бизнес и эксплуатацию.

Через несколько дней у тебя полная картина. 5к активных пользователей. Сто постов в день. Пик нагрузки в обед и вечером. Главное — скорость. Можно жертвовать актуальностью лайков и комментариев. Простои до получаса допустимы. Архивируем через пол года. Поиск простой. Рост медленный. И тд. и тп.

Ты делаешь схему под это. Одна таблица posts с денормализованными данными. Один Valkey для кеша с TTL пять минут. Никакой плеяды микросервисов. Никаких очередей. Одна база, один сервис. 

Лента открывается быстро. Система держит нагрузку. Код простой, понятный, надёжный. Бизнес доволен. Тебе не задают неудобные вопросы. 

Всё то модное, что ты впихнул в первом сценарии, оказалось ненужным. 

Во втором случае ты потратил часы на вопросы, но сэкономил месяцы на разработке и поддержке. Вопросы помогли увидеть реальную задачу и отбросить лишнее.
👍1🔥1
Что в итоге 

System design это не про схемы. Это умение задавать правильные вопросы и вытаскивать из бизнеса реальные требования, а не слепо следовать его влажным фантазиям.  

Без вопросов ты строишь систему вслепую. С вопросами решаешь конкретную задачу конкретными средствами.  

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

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

А пока перестань кивать на хотелки бизнеса и начни задавать неудобные вопросы. Скажешь себе потом спасибо.

Что еще почитать?

- 0. System design – это тебе не квадратики рисовать
- Ordering Interrogative Questions for Effective Requirements Engineering: The W6H Pattern - немного академки
- System Design: Чек-лист по сбору и фиксации требований на все случае жизни - больше конкретики
- Бунин О.В. Highload 1. Сбор требований. Фронтенд-бэкенд - оч советую посмотреть этот курс целиком
🔥4
2. System design держится на трёх столпах

Допустим, ты успешно собрал требования, был посланы куда-нибудь пару раз, выбил из бизнеса конкретику и понял, что система должна делать и как себя вести. Теперь у тебя есть "документ" с цифрами, SLA, ограничениями, функциональными и нефункциональными требованиями.

Отлично. И вот ты смотришь на этот список, список длинный, много буков и цифров. Что дальше? Как из этого "документа" получить архитектуру?

Для начала нужно налить себе чего-нибудь, например, кофе. Закрыться у себя в кабинете, чтобы тебе никто не мог помешать. А дальше придётся вспомнить много теории и начать думать. Знаю, звучит сложно, особенно про думать, но придётся с этим как-то жить. По-другому никак. 

А вспоминать лучше всего начать с трёх столпов любой архитектуры: надёжности, масштабируемости и сопровождаемости. И все три столпа это компромиссы. Каждый из них конфликтует с остальными. Улучшаешь один и ты убиваешь другие.

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

Поэтому предлагаю тебе устроиться поудобнее и почитать про базу system design. 

Что там за столпы такие?

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

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

Масштабируемость — это способность системы справляться с ростом нагрузки без деградации качества. Не "держать любую нагрузку", а "держать запланированную нагрузку в рамках SLA". Как система ведёт себя при 2x, 10x, 100x росте трафика? Где появляются узкие места? Можно ли масштабировать по частям или только целиком?

Сопровождаемость — это способность системы изменяться без катастрофических последствий. Не "код должен быть красивым", а "изменения должны быть предсказуемыми и безопасными". Сколько времени уходит на добавление фичи? Как часто изменения ломают что-то ещё? Можно ли откатить изменения? Сколько людей нужно для поддержки системы? 

Вот эти три свойства и определяют архитектуру. Все три свойства противоречат друг другу. Постоянная борьба за баланс и компромисс.
👍3
Почему же они конфликтуют?

Потому что мир конечен. У тебя ограниченный бюджет, ограниченное время, ограниченные ресурсы. И каждое решение, которое улучшает одно свойство, ухудшает другое.

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

Хочешь масштабируемость? Разносишь по серверам сервисы, добавляй кеши, настраиваешь шардинг, появляется желание завозить очереди, прости господи. Пишешь больше кода для обеспечения согласованности. Думаешь в сторону микросервисов и распределенки. Система становится сложнее, появляются новые типы сбоев, консистентность данных страдает. Надёжность падает. Больше компонентов, больше сетевых вызовов, больше вероятность упасть.

Хочешь сопровождаемость? Упрощаешь архитектуру, минимизируешь зависимости, пишешь простой код. Но система становится менее гибкой, хуже масштабируется, сложнее оптимизируется. Думаешь в сторону монолита. Масштабируемость падает. Масштабирование по частям становится практически невозможным, нельзя использовать некоторые инструменты.

К сожалению, таковы законы природы. Нельзя получить всё, везде и сразу. Постоянный торг, постоянный поиск компромиссов.
👍4
Пример

Давай покажу примеры, как эти компромиссы работают на практике.

Кеш vs консистентность

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

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

Можешь добавить инвалидацию кеша, но тогда кеш становится сложнее. Можешь использовать write-through, но тогда записи становятся медленнее. Можешь использовать eventual consistency, но тогда пользователи видят разные данные в момент времени.

Выбор зависит от требований. Для ленты новостей eventual consistency это нормально. Для банковского баланса совершенно неприемлемо.

Микросервисы vs простота

Хочешь независимые релизы и масштабирование по частям? Завози микросервисы. Каждый сервис можно разрабатывать, тестировать и деплоить отдельно. Команды не мешают друг другу.

Но что происходит со сложностью? Появляются сетевые вызовы, нужно думать про таймауты и ретраи. Руки тянутся к очередям, сложность x10. Данные размазываются по сервисам, консистентность становится проблемой. Отладка усложняется, практически неминуемая большая связность, один запрос проходит через десяток сервисов.

Можешь добавить service mesh, но тогда появляется ещё один слой абстракции. Можешь использовать event sourcing, но тогда система становится ещё сложнее. Можешь минимизировать количество сервисов, но тогда теряешь преимущества микросервисов.

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

Выбор зависит от размера команды и сложности предметной области. Для стартапа из трёх человек микросервисы это оверинжиниринг. Для команды из ста человек монолит будет якорем.

Синхронность vs асинхронность

Хочешь простоту и предсказуемость? Делай всё синхронно. Запрос пришёл, обработался, ответ ушёл. Легко отлаживать, легко тестировать, легко понимать.

А что с производительностью? Медленная операция блокирует весь запрос. Если один компонент тормозит, тормозит вся система. Масштабировать можно только вертикально.

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

Выбор зависит от требований к задержкам и пропускной способности. Для систем реального времени синхронность может быть критична. Для batch-обработки асинхронность это необходимость.
👍4🤣1
И как выбирать эти ваши компромиссы?

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

Если бизнес кричал «нужна скорость», теперь ты смотришь какой ценой.
Где они согласились терпеть боль. Медленную запись? Потерю консистентности? Сложность кода?

Если требовали «надёжность», ты открываешь SLA и смотришь, что на самом деле под этим имелось в виду.
Сколько минут даунтайма они готовы пережить, сколько данных потерять, какие ошибки простить.

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

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

Что за бюджет такой?

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

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

Правильный подход потратить бюджет осознанно. Понять, что важнее для твоего конкретного случая, и вложиться в это. А в остальном принять компромиссы.
👍2🔥2🤔1
Пример: система уведомлений

Допустим, тебе нужно построить систему уведомлений. Бизнес говорит: "Нужно отправлять уведомления пользователям. Быстро, надёжно, много."

Ты собираешь требования и понимаешь:
- 100k пользователей, 10k уведомлений в день
- Время доставки не критично, можно в течение минуты
- Потеря 1% уведомлений допустима
- Команда из пяти человек, бюджет ограничен

Теперь ты можешь осознанно выбирать компромиссы:

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

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

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

Исходя из требований, ты выбираешь: простота важнее надёжности (1% потерь допустимы), eventual consistency важнее строгой консистентности (задержка не критична), простота важнее производительности (нагрузка невысокая).

Получается простая система: прямые вызовы, базовая обработка ошибок, минимальный контроль состояния. Не идеально, но соответствует заявленным требованиям.
👍2🔥2
Что в итоге

System design это не про то, чтобы найти "правильную" архитектуру, тем более не про "чистую". Правильной архитектуры не существует. Есть только архитектура, которая подходит под твои требования и ограничения, и даёт шанс на выживание проекта.

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

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

Рождение архитектуры, в котором помогает system design, интересный, тяжёлый и завораживающий итеративный процесс, поучаствовать в котором я всем желаю. 

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

А пока читайте книги, всем добра.

Что еще почитать?

- 1. System design начинается с вопросов
- High Scalability — Real World Architectures - много интересных примеров
- Martin Fowler — Microservices Trade-Offs - про компромисы в микросервисах
🔥4👍2😨1
3. System design. Потоки …, гхм, данных

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

Ну ладно, минутка офтопа закончилась. Вернёмся к system design. 

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

На этот раз расскажу тебе про "Потоки данных", зачем стоит заранее подумать, куда и как текут данные, почему это знание значительно увеличит шансы на выживание проекта, и как не получить потоки говны вместо данных.
2🔥2
Что за потоки и откуда и куда текут?

Есть одна особенность, которую я замечаю у разработчиков, у тех, кто хочет попробовать в архитектуру. Они все думают о системе как о наборе компонентов. Сервис А общается с Сервисом Б. Коллекция users связана с коллекцией balances. Тут мы очередь вкрячим, тут кеш бросим, и мониторинг сверху прикрутим, и чтоб графики красивые. Получается схема, которую можно показать коллегам и сказать: "Смотрите, у меня современная, красивая архитектура!"

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

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

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

Поэтому, когда я говорю "потоки данных", я вообще не говорю про Kafka streams, SSE, event sourcing или очередной модный паттерн. Я говорю про базовую механику системы. О том, что данные всегда в движении. И что именно это движение диктует архитектуру куда сильнее, чем выбранная база, фреймворк или микросервисная топология.

По опыту, большинство архитектурных ошибок появляются не из-за того, что выбрали «не ту БД» или «не тот язык». Ошибки появляются из-за того, что никто не задался вопросом: куда эти данные идут и что с ними происходит по пути.

Вот делаешь ты API, которое принимает пост. А дальше что? Он превращается в одну запись? В несколько? Нужно ли рассылать дальше? А подтверждение нужно? Кешировать до преобразования или после? Кто читает? Как часто? Как долго данные остаются актуальными? Как они валидируются? 

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

Важно понимать, что данные это не сущности. Данные это движение сущностей.

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

А вот понять "что же там происходило", всегда сложнее, чем кажется. 

Когда в систему попадает новый пост, это не просто "запись в бд", а момент, когда поток данных смещает нагрузку и меняет контекст. 

Чтение поста тоже не про обычный findOne или SELECT. Это ожидание того, что кто-то подготовил данные для чтения, желательно быстрого. Если нет подготовки, значит, система может отдавать данные с задержкой и тормозить поток. 

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

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

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

Всё это не просто функции, это узлы движения данных. Сервис это место, где поток проходит. База это место, где поток оседает. Очередь это место, где поток задерживается.
3
На этапе проектирования важно видеть всю траекторию движения данных, чтобы не превратить архитектуру в набор случайных, но красивых решений. Даже при выборе правильных инструментов, база, кеш, протокол, если, не учтён поток данных, система начнёт деградировать со старта. Задержки будут расти, хвосты копится, воркеры будут душить CPU, p99 расти.

А что делать-то?

Когда начинаешь смотреть на систему как на средство управления потоком, а не как на набор красивых квадратиков, внезапно становится ясно: Архитектура то, не про сервисы. Она про движение. Сервисы, базы, очереди всего лишь инструменты управления потоком, через которые поток проходит, оседает, задерживается. Важная для понимания механика. Без этого знания будет сложно построить жизнеспособную систему. 

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

Чтобы совладать с потоком, его нужно разложить на три составляющие части: контур записи, контур чтения и контур обработки данных. Каждая часть со своей динамикой, рисками и болевыми точками.
4
Часть первая: Контур записи

Это write-path. Здесь важно всё: порядок, момент фиксации истины, нагрузка, валидность, скорость реакции. Любая ошибка здесь, как трещина в фундаменте. Ты можешь её сразу не увидеть, но она обязательно проявится позже. Клеппман в DDIA хорошо пишет про то, что write-path это та зона, где принимаются необратимые решения. Если вход нестабилен, никакие кеши и шарды дальше этого не исправят. Вход обязан быть предсказуемым, иначе поток начинает рвать систему в самых неожиданных местах.

Часть вторая: Контур чтения

Вроде как всё просто: достал данные и отдал. Но по опыту, всё далеко не так. В реальности чтение это доминирующий поток. Пользователи читают в десятки раз чаще, чем пишут. И этот поток агрессивный: ему нужна скорость, консистентность в рамках контракта и минимальная логика. Если читать «как есть», напрямую из структуры записи, ничего не получится, дорого, долго, тяжело. Read-path требует данных, подготовленных заранее. Денормализация, кеш, заранее собранные агрегаты, это не оптимизация, это само условие того, что система вообще будет отвечать. Фаулер много писал про этот конфликт: чтение нельзя обслуживать теми же структурами, что запись, если ты претендуешь на SLA, а не на стартап-демку.

Часть третья: Контур обработки данных

Тут вообще обычно проходят мимо, забывают, что, данные в системе преобразуются, обрабатываются, меняются. А потом, ой, кто-то где-то задушил CPU и IO. И начинают поиск обычно с входа, контура записи. А там всё красиво и гладко, не падает, не тротлит. А ведь именно тут происходят самые тяжёлые операции: пересборка агрегатов, обновление статистик, пересчёт связей, материализация, очистка, TTL, миграция, индексация, репликация и много чего ещё. Это тот самый тихий поток, который незаметно работает сутками, пока однажды не попадает в неудачное окно и не начинает давить всю систему. Иногда достаточно одного крупного пересчёта, который внезапно совпал по времени с пользовательским пиком, чтобы p99 ушёл в стратосферу, а в логах появилось много нового и непечатного.
4
Поток данных порождает сложность, а система, в свою очередь, должна иметь внутренние механизмы, способные эту сложность компенсировать. Ага, да, мы опять пришли к закону Эшби про необходимое разнообразие. База она такая. 

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

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

И вот мысль, которую я хотел донести: Многие архитектурные решения не про "инструменты". Они про управление потоком данных. Меняешь форму данных, меняешь поток. Меняешь порядок событий, меняется поток. Убираешь в фон тяжёлые операции, подальше от hot path, меняешь поток. Всё сводится к управлению потоком. Архитектура вырастает из движения, а не наоборот. 

Поэтому отвечая на вопрос «что делать-то?», ответ простой:
Сначала понять свой поток данных, потом разделить его, затем разобраться, как система должна на этот поток реагировать. И только после этого, выбирать инструменты и рисовать стрелочки с квадратиками.

Только в таком порядке. Иначе получится красивая схема и поток говны по ней.
2
Пример. Своя CDN на монге и немного боли

Представь, что ты делаешь свою «бедную» CDN. Ничего космического, просто раздача статики: картинки, js, css. Пара edge-нод, один origin, всё это крутится рядом с основным приложением. Хочется контролировать версии, уметь быстро откатываться, иногда включать разные варианты для разных клиентов. И вот тут кто-то говорит знакомое: «давайте всё положим в монгу, сто раз так делали».

Схема рождается быстро. Файлы лежат где-то на диске или в s3, а в Mongo хранятся метаданные: путь, версия, флаги, список разрешённых клиентов, настройки кеша. Приходит запрос на статику, CDN-нода идёт в Mongo, достаёт документ, понимает, какую версию отдавать, смотрит, не выключен ли ресурс, и уже потом лезет в хранилище. Красиво, гибко, всё динамически настраивается.

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

Контур записи ещё терпимый. Разработчик выкатывает новую версию фронта, сервис релизов пишет пачку документов в Mongo, отмечает новую версию как активную, старую как «archived». Поток данных на входе относительно небольшой, немного всплесков, но монга это переваривает. Все довольны, все ходят и рассказывают, что у них «динамическая CDN с конфигом в базе».

Проблемы вылезают в контуре чтения. Любой запрос на статический ресурс начинает с чтения из Mongo. Даже если у тебя есть локальный кеш файлов на edge-ноде, ты всё равно сначала лезешь в базу: вдруг версию поменяли, вдруг ресурс выключили, вдруг флаг изменился. Пока аудитория маленькая, всё живёт. Как только прилетает бОльшая нагрузка, чтение превращается в поток запросов к Mongo, который, вообще-то, никто не проектировал под роль горячего конфига для CDN.

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

А теперь подключается контур обработки.

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

Получается красивая каша. Вход пишет новые версии. Чтение при каждом запросе лезет в Mongo, чтобы понять, что отдавать. Фон пересобирает всё это дело, чистит и переиндексирует. Контуры неразделены, темпы у всех разные, буферов почти нет. Достаточно одного неудачного совпадения: деплой, пара «ручных» скриптов по миграции метаданных и пик трафика. Итог предсказуем. Edge-ноды начинают встать на блокировках к монге, CDN «внезапно» перестаёт быть CDN и превращается в дорогой прокси к базе.
2
Если на это смотреть через призму потоков, картина другая. 

На записи тебе вообще не нужна Mongo на hot-path. Нужно один раз зафиксировать факт: «появилась новая версия набора файлов с таким-то идентификатором». Этого достаточно. Подробный конфиг можно материализовать отдельно. В обработке ты спокойно пересобираешь манифесты, генерируешь простой, плоский формат настроек для каждой edge-ноды, кладёшь его в отдельную коллекцию или вообще в файловый снапшот. А контур чтения должен жить своей жизнью: брать уже готовый, прогретый конфиг целиком в память и не ходить в mongo на каждый чих.

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

Снаружи два решения могут выглядеть похоже: «у нас CDN, конфиг в монге, всё динамическое». Но одно построено как «каждый запрос это маленькое приключение в базу», а второе уже как нормальная работа с потоком: истина пишется отдельно, конфиг готовится отдельно, чтение живёт на готовых срезах. 

Формально те же компоненты, та же монга, те же edge-ноды. А по факту в одном случае у тебя постоянная война с пиками, а в другом система, которая хотя бы не стреляет себе в ногу каждый раз, когда кто-то открыл главную страницу.
2
Что в итоге?

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

Когда разбиваешь поток на контуры: запись, чтение, обработку. Все проблемы приобретают форму. Становится ясно, почему запись должна быть предсказуемой, чтение быстрым, а тяжёлая логика жить отдельно. Пример с CDN на монге это хорошо показывает: одинаковые технологии снаружи дают два совершенно разных поведения в зависимости от того, управляешь ты потоком или таскаешься за ним.

Все архитектурные решения на самом деле про одно: про управление движением данных. Меняешь поток, меняется система. Не меняешь, она меняет тебя.

Поэтому порядок такой же простой, как и беспощадный:
увидеть поток, разделить поток, понять реакцию системы и только потом выбирать инструменты.

В обратном порядке работает только красивая схема и поток говны поверх неё.

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

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

Что еще почитать?

- 2. System design держится на трёх столпах
4
Заметка из курилки. SOLID устарел.

Слушай, ну какой SOLID. Есть вещи, которые пора отправить в музей. Не потому, что они плохие, а потому что эпоха ушла. Дядя Боб и его SOLID как раз из этой категории. Эти принципы почему-то до сих пор заставляют зубрить как "священны закон архитектуры". Хотя, если подумать, SOLID в том виде из 2000х умер примерно между появлением нормальных типов данных, модульных систем и тем, когда вообще перестали писать ООП в том виде, для которого SOLID был придуман. 

SOLID ведь не про архитектуру. Это что-то вроде мантры, шпаргалка, которая успокаивала в 2000-х разработчиков. Это были времена жирных классов на 1к строк и наследования всего от всего. Когда твой язык Java 1.4 без лямбд, композиции нормальной и модулей, тебе приходится искать спасения где-то извне, и эти пять букв приносят успокоение, становится немного легче жить. Но в 2к25 это, как учить студента писать бэк на Perl, ничего против не имею, но давай честно, есть более актуальные инструменты. 

Когда-то SOLID решал реальную боль разработчика, но сегодня это выглядит примерно как попытка чинить современную систему распределённых событий «принципом единой ответственности» из книги 1999 года. Ну комон?

SRP в буквальном прочтении звучит красиво, пока не задаёшь вопрос: «ответственность в какой области?» В доменной? В технической? В организационной? В потоках данных? Или другой вопрос: "Ответственность чья?" Модуля? Разработчика? Менеджера? Команды? Любой достаточно сложный модуль нарушает SRP по любой трактовке. В 2к25 "единая ответственность" как догма теряет свой смысл. И это нормально.

LSP? Наследование, как фундаментальный строительны блок архитектуры, уходит из практики. Не пишем мы больше трёхэтажные иерархии, где каждый объект обязан быть кем-то в цепочке. Наследование как принцип усложняет систему и делает связи хрупкими. Вспомним Go, там нет наследования в привычном ООП смысле, и как тоже с этим живут. 

Слышу крик из-за спины: "А как же JS? Ты же сам пишешь классы и extend используешь". Ну тут не про иерархию, отвечаю я. Тут про удобство: инкапсуляция поведения, переиспользование кода, апи симпатишнее. Современное наследование это тонкий слой над композицией, а не попытка построить идеальный объектный мир. 

Если ты слепо используешь LSP как принцип, который должен регулировать архитектуру без учёта современных реалий, есть плохие новости о твоём проекте. Это не тот уровень абстракции. Решения в 2к25 принимаются выше.

Окей, что там следующее? Open/Closed. Артефакт того времени, когда изменения в коде тянули больше проблем, чем пользы. Сегодня нормальный код живёт в рефакторинге: меняется, ломается, чинится, меняется композиция и архитектура. «Закрыто для модификации» прекрасная надпись на надгробии системы, которую у тебя нет сил развивать. Нет развития, нет системы. 

ISP и DIP мои любимцы. Люди пишут интерфейсы ради интерфейсов, слои ради слоёв, контейнеры зависимостей ради контейнеров зависимостей и уверены, что это повышается качество, сопровождаемость, и вообще "стандарт индустрии". На самом деле это повышает только количество файлов в PR.
🔥6👾1