Почему же они конфликтуют?
Потому что мир конечен. У тебя ограниченный бюджет, ограниченное время, ограниченные ресурсы. И каждое решение, которое улучшает одно свойство, ухудшает другое.
Хочешь надёжность? Добавляешь реплики, валидацию, проверки, мониторинг. Пишешь код для контролируемой деградации, отлаживаешь управляемую остановку сервисов. Добавляешь код для работы с граничными случаями. Система становится сложнее, медленнее, дороже. Появляется больше абстракций. Сопровождаемость падает. Больше кода, больше конфигов, больше точек отказа.
Хочешь масштабируемость? Разносишь по серверам сервисы, добавляй кеши, настраиваешь шардинг, появляется желание завозить очереди, прости господи. Пишешь больше кода для обеспечения согласованности. Думаешь в сторону микросервисов и распределенки. Система становится сложнее, появляются новые типы сбоев, консистентность данных страдает. Надёжность падает. Больше компонентов, больше сетевых вызовов, больше вероятность упасть.
Хочешь сопровождаемость? Упрощаешь архитектуру, минимизируешь зависимости, пишешь простой код. Но система становится менее гибкой, хуже масштабируется, сложнее оптимизируется. Думаешь в сторону монолита. Масштабируемость падает. Масштабирование по частям становится практически невозможным, нельзя использовать некоторые инструменты.
К сожалению, таковы законы природы. Нельзя получить всё, везде и сразу. Постоянный торг, постоянный поиск компромиссов.
Потому что мир конечен. У тебя ограниченный бюджет, ограниченное время, ограниченные ресурсы. И каждое решение, которое улучшает одно свойство, ухудшает другое.
Хочешь надёжность? Добавляешь реплики, валидацию, проверки, мониторинг. Пишешь код для контролируемой деградации, отлаживаешь управляемую остановку сервисов. Добавляешь код для работы с граничными случаями. Система становится сложнее, медленнее, дороже. Появляется больше абстракций. Сопровождаемость падает. Больше кода, больше конфигов, больше точек отказа.
Хочешь масштабируемость? Разносишь по серверам сервисы, добавляй кеши, настраиваешь шардинг, появляется желание завозить очереди, прости господи. Пишешь больше кода для обеспечения согласованности. Думаешь в сторону микросервисов и распределенки. Система становится сложнее, появляются новые типы сбоев, консистентность данных страдает. Надёжность падает. Больше компонентов, больше сетевых вызовов, больше вероятность упасть.
Хочешь сопровождаемость? Упрощаешь архитектуру, минимизируешь зависимости, пишешь простой код. Но система становится менее гибкой, хуже масштабируется, сложнее оптимизируется. Думаешь в сторону монолита. Масштабируемость падает. Масштабирование по частям становится практически невозможным, нельзя использовать некоторые инструменты.
К сожалению, таковы законы природы. Нельзя получить всё, везде и сразу. Постоянный торг, постоянный поиск компромиссов.
👍4
Пример
Давай покажу примеры, как эти компромиссы работают на практике.
Кеш vs консистентность
Начнём с классики. Хочешь скорость? Добавляй кеш. Данные читаются из памяти, ответы прилетают быстрее. Масштабируемость растёт, нагрузка на базу падает.
Но что происходит с консистентностью? Данные в кеше могут быть устаревшими. Пользователь видит старую информацию. Если кеш упал, время ответа кратно возрастает. Если кеш переполнился, часть данных вытесняется.
Можешь добавить инвалидацию кеша, но тогда кеш становится сложнее. Можешь использовать write-through, но тогда записи становятся медленнее. Можешь использовать eventual consistency, но тогда пользователи видят разные данные в момент времени.
Выбор зависит от требований. Для ленты новостей eventual consistency это нормально. Для банковского баланса совершенно неприемлемо.
Микросервисы vs простота
Хочешь независимые релизы и масштабирование по частям? Завози микросервисы. Каждый сервис можно разрабатывать, тестировать и деплоить отдельно. Команды не мешают друг другу.
Но что происходит со сложностью? Появляются сетевые вызовы, нужно думать про таймауты и ретраи. Руки тянутся к очередям, сложность x10. Данные размазываются по сервисам, консистентность становится проблемой. Отладка усложняется, практически неминуемая большая связность, один запрос проходит через десяток сервисов.
Можешь добавить service mesh, но тогда появляется ещё один слой абстракции. Можешь использовать event sourcing, но тогда система становится ещё сложнее. Можешь минимизировать количество сервисов, но тогда теряешь преимущества микросервисов.
Примерно тут ты вспоминаешь про саги. Способ склеить распределённые транзакции без 2PC. Каждая операция локальна, но цепочка координируется сообщениями. Если шаг падает, запускается компенсирующее действие. В теории красиво, на практике превращается в ад отложенных событий и "ручного" восстановления состояния. Любая ошибка в оркестрации, и данные начинают расходиться.
Выбор зависит от размера команды и сложности предметной области. Для стартапа из трёх человек микросервисы это оверинжиниринг. Для команды из ста человек монолит будет якорем.
Синхронность vs асинхронность
Хочешь простоту и предсказуемость? Делай всё синхронно. Запрос пришёл, обработался, ответ ушёл. Легко отлаживать, легко тестировать, легко понимать.
А что с производительностью? Медленная операция блокирует весь запрос. Если один компонент тормозит, тормозит вся система. Масштабировать можно только вертикально.
Можешь добавить очереди, прости господи, но тогда появляется eventual consistency. Можешь использовать неблокирующие вызовы, но тогда код становится сложнее. Можешь разбить операцию на этапы, но тогда нужно думать про компенсацию, привет саги.
Выбор зависит от требований к задержкам и пропускной способности. Для систем реального времени синхронность может быть критична. Для batch-обработки асинхронность это необходимость.
Давай покажу примеры, как эти компромиссы работают на практике.
Кеш vs консистентность
Начнём с классики. Хочешь скорость? Добавляй кеш. Данные читаются из памяти, ответы прилетают быстрее. Масштабируемость растёт, нагрузка на базу падает.
Но что происходит с консистентностью? Данные в кеше могут быть устаревшими. Пользователь видит старую информацию. Если кеш упал, время ответа кратно возрастает. Если кеш переполнился, часть данных вытесняется.
Можешь добавить инвалидацию кеша, но тогда кеш становится сложнее. Можешь использовать write-through, но тогда записи становятся медленнее. Можешь использовать eventual consistency, но тогда пользователи видят разные данные в момент времени.
Выбор зависит от требований. Для ленты новостей eventual consistency это нормально. Для банковского баланса совершенно неприемлемо.
Микросервисы vs простота
Хочешь независимые релизы и масштабирование по частям? Завози микросервисы. Каждый сервис можно разрабатывать, тестировать и деплоить отдельно. Команды не мешают друг другу.
Но что происходит со сложностью? Появляются сетевые вызовы, нужно думать про таймауты и ретраи. Руки тянутся к очередям, сложность x10. Данные размазываются по сервисам, консистентность становится проблемой. Отладка усложняется, практически неминуемая большая связность, один запрос проходит через десяток сервисов.
Можешь добавить service mesh, но тогда появляется ещё один слой абстракции. Можешь использовать event sourcing, но тогда система становится ещё сложнее. Можешь минимизировать количество сервисов, но тогда теряешь преимущества микросервисов.
Примерно тут ты вспоминаешь про саги. Способ склеить распределённые транзакции без 2PC. Каждая операция локальна, но цепочка координируется сообщениями. Если шаг падает, запускается компенсирующее действие. В теории красиво, на практике превращается в ад отложенных событий и "ручного" восстановления состояния. Любая ошибка в оркестрации, и данные начинают расходиться.
Выбор зависит от размера команды и сложности предметной области. Для стартапа из трёх человек микросервисы это оверинжиниринг. Для команды из ста человек монолит будет якорем.
Синхронность vs асинхронность
Хочешь простоту и предсказуемость? Делай всё синхронно. Запрос пришёл, обработался, ответ ушёл. Легко отлаживать, легко тестировать, легко понимать.
А что с производительностью? Медленная операция блокирует весь запрос. Если один компонент тормозит, тормозит вся система. Масштабировать можно только вертикально.
Можешь добавить очереди, прости господи, но тогда появляется eventual consistency. Можешь использовать неблокирующие вызовы, но тогда код становится сложнее. Можешь разбить операцию на этапы, но тогда нужно думать про компенсацию, привет саги.
Выбор зависит от требований к задержкам и пропускной способности. Для систем реального времени синхронность может быть критична. Для batch-обработки асинхронность это необходимость.
👍4🤣1
И как выбирать эти ваши компромиссы?
Ага, примерно тут, ты достаёшь из широких штанин, толщиною с консервную банку, хм, документ. Тот, что ты собирал с боем в прошлый раз. Требования. Каждое архитектурное решение должно быть обосновано конкретной строкой или абзацем в этом документе.
Если бизнес кричал «нужна скорость», теперь ты смотришь какой ценой.
Где они согласились терпеть боль. Медленную запись? Потерю консистентности? Сложность кода?
Если требовали «надёжность», ты открываешь SLA и смотришь, что на самом деле под этим имелось в виду.
Сколько минут даунтайма они готовы пережить, сколько данных потерять, какие ошибки простить.
Если просили «простоту» ищешь, для кого именно.
Для разработчиков, чтобы быстрее пилить фичи? Для пользователей, чтобы не путались в интерфейсе?
Или для эксплуатации, чтобы не вылавливать зомби-процессы ночью?
Тут появляется концепция "бюджета боли". У тебя есть ограниченный бюджет на сложность, на производительность, на надёжность. Ты не можешь получить всё сразу. Ты можешь только осознанно распределить этот бюджет.
Что за бюджет такой?
Представь, что у тебя есть сто деняк. Ты можешь потратить их на надёжность, на масштабируемость или на сопровождаемость. Но на всё сразу у тебя не хватает.
Если потратишь всё на надёжность, получишь систему, которая не падает, но тормозит и которую никто не понимает. Если потратишь всё на масштабируемость, получишь систему, которая держит нагрузку, но падает от любого чиха. Если потратишь всё на сопровождаемость, получишь систему, которую легко поддерживать, но которая не масштабируется.
Правильный подход потратить бюджет осознанно. Понять, что важнее для твоего конкретного случая, и вложиться в это. А в остальном принять компромиссы.
Ага, примерно тут, ты достаёшь из широких штанин, толщиною с консервную банку, хм, документ. Тот, что ты собирал с боем в прошлый раз. Требования. Каждое архитектурное решение должно быть обосновано конкретной строкой или абзацем в этом документе.
Если бизнес кричал «нужна скорость», теперь ты смотришь какой ценой.
Где они согласились терпеть боль. Медленную запись? Потерю консистентности? Сложность кода?
Если требовали «надёжность», ты открываешь SLA и смотришь, что на самом деле под этим имелось в виду.
Сколько минут даунтайма они готовы пережить, сколько данных потерять, какие ошибки простить.
Если просили «простоту» ищешь, для кого именно.
Для разработчиков, чтобы быстрее пилить фичи? Для пользователей, чтобы не путались в интерфейсе?
Или для эксплуатации, чтобы не вылавливать зомби-процессы ночью?
Тут появляется концепция "бюджета боли". У тебя есть ограниченный бюджет на сложность, на производительность, на надёжность. Ты не можешь получить всё сразу. Ты можешь только осознанно распределить этот бюджет.
Что за бюджет такой?
Представь, что у тебя есть сто деняк. Ты можешь потратить их на надёжность, на масштабируемость или на сопровождаемость. Но на всё сразу у тебя не хватает.
Если потратишь всё на надёжность, получишь систему, которая не падает, но тормозит и которую никто не понимает. Если потратишь всё на масштабируемость, получишь систему, которая держит нагрузку, но падает от любого чиха. Если потратишь всё на сопровождаемость, получишь систему, которую легко поддерживать, но которая не масштабируется.
Правильный подход потратить бюджет осознанно. Понять, что важнее для твоего конкретного случая, и вложиться в это. А в остальном принять компромиссы.
👍2🔥2🤔1
Пример: система уведомлений
Допустим, тебе нужно построить систему уведомлений. Бизнес говорит: "Нужно отправлять уведомления пользователям. Быстро, надёжно, много."
Ты собираешь требования и понимаешь:
- 100k пользователей, 10k уведомлений в день
- Время доставки не критично, можно в течение минуты
- Потеря 1% уведомлений допустима
- Команда из пяти человек, бюджет ограничен
Теперь ты можешь осознанно выбирать компромиссы:
Надёжность vs простота: Можно сделать простую систему, один процесс, одна база, прямой вызов. Но если процесс упадёт, часть задач потеряется. Можно добавить репликацию и мониторинг, но система станет сложнее.
Масштабируемость vs консистентность: Можно гарантировать, что каждое событие будет обработано ровно один раз, но тогда придётся решать вопросы дубликатов и идемпотентности. Можно выбрать eventual consistency, но тогда часть данных может устаревать или теряться.
Сопровождаемость vs производительность: Можно написать простой и прозрачный код, который легко читать и менять. Но он не выдержит пиков. Можно добавить кэширование, пайплайны и оптимизации, но код станет запутаннее и более хрупким.
Исходя из требований, ты выбираешь: простота важнее надёжности (1% потерь допустимы), eventual consistency важнее строгой консистентности (задержка не критична), простота важнее производительности (нагрузка невысокая).
Получается простая система: прямые вызовы, базовая обработка ошибок, минимальный контроль состояния. Не идеально, но соответствует заявленным требованиям.
Допустим, тебе нужно построить систему уведомлений. Бизнес говорит: "Нужно отправлять уведомления пользователям. Быстро, надёжно, много."
Ты собираешь требования и понимаешь:
- 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 - про компромисы в микросервисах
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.
На этот раз расскажу тебе про "Потоки данных", зачем стоит заранее подумать, куда и как текут данные, почему это знание значительно увеличит шансы на выживание проекта, и как не получить потоки говны вместо данных.
Штош. Тут должен быть мем с котом, но его не будет. Немного выбило меня из ритма повествования. Можно меня поздравить, я теперь совсем безработный, буду чилить и возможно даже больше писать "умных мюслей". А может, и не буду, а может, завтра уже буду работать в новом месте, а может, и нет. Посмотримс
Ну ладно, минутка офтопа закончилась. Вернёмся к system design.
В прошлый раз я закончил статью анонсом большого практического материала. Хотел на практике показать, как получается архитектура на основании собранных требований. Материал написал, получилось много, сложно и как-то скомкано. Не получилось впихнуть невпихуемое в рамки одной статьи. Поэтому переобуваюсь в прыжке и предлагаю тебе продолжить знакомится с важными элементами system design.
На этот раз расскажу тебе про "Потоки данных", зачем стоит заранее подумать, куда и как текут данные, почему это знание значительно увеличит шансы на выживание проекта, и как не получить потоки говны вместо данных.
❤2🔥2
Что за потоки и откуда и куда текут?
Есть одна особенность, которую я замечаю у разработчиков, у тех, кто хочет попробовать в архитектуру. Они все думают о системе как о наборе компонентов. Сервис А общается с Сервисом Б. Коллекция users связана с коллекцией balances. Тут мы очередь вкрячим, тут кеш бросим, и мониторинг сверху прикрутим, и чтоб графики красивые. Получается схема, которую можно показать коллегам и сказать: "Смотрите, у меня современная, красивая архитектура!"
Проблема в том, что такая схема не отражает жизнь системы. Она статична. Её можно повесить на стену, но она ничего не скажет о том, почему система начинает деградировать, где начинается нагрузка и что именно может сломаться.
Все забывают, что единственная настоящая работа системы, это преобразование данных из одного состояния в другое. Нет данных, нет и системы. И данные, в отличие от компонента на диаграмме, не стоят на месте. Их может быть много или мало, но в любом случае они как-то попадают в систему, движутся внутри неё, где-то оседают и в какой-то момент исчезают.
Это непрерывный поток. Можно сравнить с рекой или ручьём. В одном месте тихое спокойное течение, в другом месте пороги, стремительный бурный поток и водовороты, где-то разливы, а иногда это всё может превратиться в говнотечь, если вовремя не подумать о берегах.
Поэтому, когда я говорю "потоки данных", я вообще не говорю про Kafka streams, SSE, event sourcing или очередной модный паттерн. Я говорю про базовую механику системы. О том, что данные всегда в движении. И что именно это движение диктует архитектуру куда сильнее, чем выбранная база, фреймворк или микросервисная топология.
По опыту, большинство архитектурных ошибок появляются не из-за того, что выбрали «не ту БД» или «не тот язык». Ошибки появляются из-за того, что никто не задался вопросом: куда эти данные идут и что с ними происходит по пути.
Вот делаешь ты API, которое принимает пост. А дальше что? Он превращается в одну запись? В несколько? Нужно ли рассылать дальше? А подтверждение нужно? Кешировать до преобразования или после? Кто читает? Как часто? Как долго данные остаются актуальными? Как они валидируются?
На большую часть вопросов отвечают требования, для этого ты их и собирал. Важно формализовать поток. Но если ты не можешь ответить на эти вопросы, дальше нет смысла рисовать красивые куберы, очереди (прости господи), базы и реплики. Сколько ни старайся, данные всё равно потекут так, как им положено по природе процесса, а не по твоей диаграмме.
Важно понимать, что данные это не сущности. Данные это движение сущностей.
Пост, пользователь, лайк это не просто строка в таблице или документ в коллекции. Это срез конкретного состояния. Поток данных это не про срез. Это про историю изменений. Не «что лежит», а «что происходило».
А вот понять "что же там происходило", всегда сложнее, чем кажется.
Когда в систему попадает новый пост, это не просто "запись в бд", а момент, когда поток данных смещает нагрузку и меняет контекст.
Чтение поста тоже не про обычный findOne или SELECT. Это ожидание того, что кто-то подготовил данные для чтения, желательно быстрого. Если нет подготовки, значит, система может отдавать данные с задержкой и тормозить поток.
Удаление поста требует, чтобы все связанные данные были стёрты. Пропустил и вот данные уже не консистентны, могут быть ошибки.
Лайк поста это про контроль хаоса, два пользователя пытаются изменить одно и то же. Если не учесть движение потока, гонки могут стать нормой.
Лента вообще живёт, только благодаря предварительной сборке. Кеш, заранее рассчитанный срез, и у системы есть шансы уложиться в SLA.
Всё это не просто функции, это узлы движения данных. Сервис это место, где поток проходит. База это место, где поток оседает. Очередь это место, где поток задерживается.
Есть одна особенность, которую я замечаю у разработчиков, у тех, кто хочет попробовать в архитектуру. Они все думают о системе как о наборе компонентов. Сервис А общается с Сервисом Б. Коллекция users связана с коллекцией balances. Тут мы очередь вкрячим, тут кеш бросим, и мониторинг сверху прикрутим, и чтоб графики красивые. Получается схема, которую можно показать коллегам и сказать: "Смотрите, у меня современная, красивая архитектура!"
Проблема в том, что такая схема не отражает жизнь системы. Она статична. Её можно повесить на стену, но она ничего не скажет о том, почему система начинает деградировать, где начинается нагрузка и что именно может сломаться.
Все забывают, что единственная настоящая работа системы, это преобразование данных из одного состояния в другое. Нет данных, нет и системы. И данные, в отличие от компонента на диаграмме, не стоят на месте. Их может быть много или мало, но в любом случае они как-то попадают в систему, движутся внутри неё, где-то оседают и в какой-то момент исчезают.
Это непрерывный поток. Можно сравнить с рекой или ручьём. В одном месте тихое спокойное течение, в другом месте пороги, стремительный бурный поток и водовороты, где-то разливы, а иногда это всё может превратиться в говнотечь, если вовремя не подумать о берегах.
Поэтому, когда я говорю "потоки данных", я вообще не говорю про Kafka streams, SSE, event sourcing или очередной модный паттерн. Я говорю про базовую механику системы. О том, что данные всегда в движении. И что именно это движение диктует архитектуру куда сильнее, чем выбранная база, фреймворк или микросервисная топология.
По опыту, большинство архитектурных ошибок появляются не из-за того, что выбрали «не ту БД» или «не тот язык». Ошибки появляются из-за того, что никто не задался вопросом: куда эти данные идут и что с ними происходит по пути.
Вот делаешь ты API, которое принимает пост. А дальше что? Он превращается в одну запись? В несколько? Нужно ли рассылать дальше? А подтверждение нужно? Кешировать до преобразования или после? Кто читает? Как часто? Как долго данные остаются актуальными? Как они валидируются?
На большую часть вопросов отвечают требования, для этого ты их и собирал. Важно формализовать поток. Но если ты не можешь ответить на эти вопросы, дальше нет смысла рисовать красивые куберы, очереди (прости господи), базы и реплики. Сколько ни старайся, данные всё равно потекут так, как им положено по природе процесса, а не по твоей диаграмме.
Важно понимать, что данные это не сущности. Данные это движение сущностей.
Пост, пользователь, лайк это не просто строка в таблице или документ в коллекции. Это срез конкретного состояния. Поток данных это не про срез. Это про историю изменений. Не «что лежит», а «что происходило».
А вот понять "что же там происходило", всегда сложнее, чем кажется.
Когда в систему попадает новый пост, это не просто "запись в бд", а момент, когда поток данных смещает нагрузку и меняет контекст.
Чтение поста тоже не про обычный findOne или SELECT. Это ожидание того, что кто-то подготовил данные для чтения, желательно быстрого. Если нет подготовки, значит, система может отдавать данные с задержкой и тормозить поток.
Удаление поста требует, чтобы все связанные данные были стёрты. Пропустил и вот данные уже не консистентны, могут быть ошибки.
Лайк поста это про контроль хаоса, два пользователя пытаются изменить одно и то же. Если не учесть движение потока, гонки могут стать нормой.
Лента вообще живёт, только благодаря предварительной сборке. Кеш, заранее рассчитанный срез, и у системы есть шансы уложиться в SLA.
Всё это не просто функции, это узлы движения данных. Сервис это место, где поток проходит. База это место, где поток оседает. Очередь это место, где поток задерживается.
❤3
На этапе проектирования важно видеть всю траекторию движения данных, чтобы не превратить архитектуру в набор случайных, но красивых решений. Даже при выборе правильных инструментов, база, кеш, протокол, если, не учтён поток данных, система начнёт деградировать со старта. Задержки будут расти, хвосты копится, воркеры будут душить CPU, p99 расти.
А что делать-то?
Когда начинаешь смотреть на систему как на средство управления потоком, а не как на набор красивых квадратиков, внезапно становится ясно: Архитектура то, не про сервисы. Она про движение. Сервисы, базы, очереди всего лишь инструменты управления потоком, через которые поток проходит, оседает, задерживается. Важная для понимания механика. Без этого знания будет сложно построить жизнеспособную систему.
Первое, что приходится принять: Поток всегда в движении. Независимо от того, понимаешь ты его или нет. Можешь хотеть идеальный write-API, предсказуемый кеш или волшебный микросервис, который решит все проблемы. Но данные будут идти по своему маршруту, диктуемому задачей, временем, нагрузкой и законами природы. Система будет вести себя так, как движутся данные, а не так, как ты её нарисовал.
Чтобы совладать с потоком, его нужно разложить на три составляющие части: контур записи, контур чтения и контур обработки данных. Каждая часть со своей динамикой, рисками и болевыми точками.
А что делать-то?
Когда начинаешь смотреть на систему как на средство управления потоком, а не как на набор красивых квадратиков, внезапно становится ясно: Архитектура то, не про сервисы. Она про движение. Сервисы, базы, очереди всего лишь инструменты управления потоком, через которые поток проходит, оседает, задерживается. Важная для понимания механика. Без этого знания будет сложно построить жизнеспособную систему.
Первое, что приходится принять: Поток всегда в движении. Независимо от того, понимаешь ты его или нет. Можешь хотеть идеальный write-API, предсказуемый кеш или волшебный микросервис, который решит все проблемы. Но данные будут идти по своему маршруту, диктуемому задачей, временем, нагрузкой и законами природы. Система будет вести себя так, как движутся данные, а не так, как ты её нарисовал.
Чтобы совладать с потоком, его нужно разложить на три составляющие части: контур записи, контур чтения и контур обработки данных. Каждая часть со своей динамикой, рисками и болевыми точками.
❤4
Часть первая: Контур записи
Это write-path. Здесь важно всё: порядок, момент фиксации истины, нагрузка, валидность, скорость реакции. Любая ошибка здесь, как трещина в фундаменте. Ты можешь её сразу не увидеть, но она обязательно проявится позже. Клеппман в DDIA хорошо пишет про то, что write-path это та зона, где принимаются необратимые решения. Если вход нестабилен, никакие кеши и шарды дальше этого не исправят. Вход обязан быть предсказуемым, иначе поток начинает рвать систему в самых неожиданных местах.
Часть вторая: Контур чтения
Вроде как всё просто: достал данные и отдал. Но по опыту, всё далеко не так. В реальности чтение это доминирующий поток. Пользователи читают в десятки раз чаще, чем пишут. И этот поток агрессивный: ему нужна скорость, консистентность в рамках контракта и минимальная логика. Если читать «как есть», напрямую из структуры записи, ничего не получится, дорого, долго, тяжело. Read-path требует данных, подготовленных заранее. Денормализация, кеш, заранее собранные агрегаты, это не оптимизация, это само условие того, что система вообще будет отвечать. Фаулер много писал про этот конфликт: чтение нельзя обслуживать теми же структурами, что запись, если ты претендуешь на SLA, а не на стартап-демку.
Часть третья: Контур обработки данных
Тут вообще обычно проходят мимо, забывают, что, данные в системе преобразуются, обрабатываются, меняются. А потом, ой, кто-то где-то задушил CPU и IO. И начинают поиск обычно с входа, контура записи. А там всё красиво и гладко, не падает, не тротлит. А ведь именно тут происходят самые тяжёлые операции: пересборка агрегатов, обновление статистик, пересчёт связей, материализация, очистка, TTL, миграция, индексация, репликация и много чего ещё. Это тот самый тихий поток, который незаметно работает сутками, пока однажды не попадает в неудачное окно и не начинает давить всю систему. Иногда достаточно одного крупного пересчёта, который внезапно совпал по времени с пользовательским пиком, чтобы p99 ушёл в стратосферу, а в логах появилось много нового и непечатного.
Это write-path. Здесь важно всё: порядок, момент фиксации истины, нагрузка, валидность, скорость реакции. Любая ошибка здесь, как трещина в фундаменте. Ты можешь её сразу не увидеть, но она обязательно проявится позже. Клеппман в DDIA хорошо пишет про то, что write-path это та зона, где принимаются необратимые решения. Если вход нестабилен, никакие кеши и шарды дальше этого не исправят. Вход обязан быть предсказуемым, иначе поток начинает рвать систему в самых неожиданных местах.
Часть вторая: Контур чтения
Вроде как всё просто: достал данные и отдал. Но по опыту, всё далеко не так. В реальности чтение это доминирующий поток. Пользователи читают в десятки раз чаще, чем пишут. И этот поток агрессивный: ему нужна скорость, консистентность в рамках контракта и минимальная логика. Если читать «как есть», напрямую из структуры записи, ничего не получится, дорого, долго, тяжело. Read-path требует данных, подготовленных заранее. Денормализация, кеш, заранее собранные агрегаты, это не оптимизация, это само условие того, что система вообще будет отвечать. Фаулер много писал про этот конфликт: чтение нельзя обслуживать теми же структурами, что запись, если ты претендуешь на SLA, а не на стартап-демку.
Часть третья: Контур обработки данных
Тут вообще обычно проходят мимо, забывают, что, данные в системе преобразуются, обрабатываются, меняются. А потом, ой, кто-то где-то задушил CPU и IO. И начинают поиск обычно с входа, контура записи. А там всё красиво и гладко, не падает, не тротлит. А ведь именно тут происходят самые тяжёлые операции: пересборка агрегатов, обновление статистик, пересчёт связей, материализация, очистка, TTL, миграция, индексация, репликация и много чего ещё. Это тот самый тихий поток, который незаметно работает сутками, пока однажды не попадает в неудачное окно и не начинает давить всю систему. Иногда достаточно одного крупного пересчёта, который внезапно совпал по времени с пользовательским пиком, чтобы p99 ушёл в стратосферу, а в логах появилось много нового и непечатного.
❤4
Поток данных порождает сложность, а система, в свою очередь, должна иметь внутренние механизмы, способные эту сложность компенсировать. Ага, да, мы опять пришли к закону Эшби про необходимое разнообразие. База она такая.
Если write-path, read-path и фон неразделены, если у них нет изоляции, буферов, независимых темпов, система становится хрупкой. Это и есть тот момент, когда малейший всплеск нагрузки превращается в неконтролируемую деградацию, а простая операция начинает складывать половину инфраструктуры.
Когда разделение потоков становится очевидным, архитектура начинает выстраиваться сама. Нужна быстрая лента, формируем представление данных заранее. Нужна консистентная запись, уделяем внимание входу, а не тому, как красиво выглядит микросервисная схема. Нужны тяжёлые отчёты, их место в фоне, в собственной вселенной, а не рядом с пользовательскими запросами. Нужны жёсткие SLA, чтение и запись никогда не должны жить в одной структуре данных.
И вот мысль, которую я хотел донести: Многие архитектурные решения не про "инструменты". Они про управление потоком данных. Меняешь форму данных, меняешь поток. Меняешь порядок событий, меняется поток. Убираешь в фон тяжёлые операции, подальше от hot path, меняешь поток. Всё сводится к управлению потоком. Архитектура вырастает из движения, а не наоборот.
Поэтому отвечая на вопрос «что делать-то?», ответ простой:
Сначала понять свой поток данных, потом разделить его, затем разобраться, как система должна на этот поток реагировать. И только после этого, выбирать инструменты и рисовать стрелочки с квадратиками.
Только в таком порядке. Иначе получится красивая схема и поток говны по ней.
Если 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 и превращается в дорогой прокси к базе.
Представь, что ты делаешь свою «бедную» 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-ноды. А по факту в одном случае у тебя постоянная война с пиками, а в другом система, которая хотя бы не стреляет себе в ногу каждый раз, когда кто-то открыл главную страницу.
На записи тебе вообще не нужна Mongo на hot-path. Нужно один раз зафиксировать факт: «появилась новая версия набора файлов с таким-то идентификатором». Этого достаточно. Подробный конфиг можно материализовать отдельно. В обработке ты спокойно пересобираешь манифесты, генерируешь простой, плоский формат настроек для каждой edge-ноды, кладёшь его в отдельную коллекцию или вообще в файловый снапшот. А контур чтения должен жить своей жизнью: брать уже готовый, прогретый конфиг целиком в память и не ходить в mongo на каждый чих.
В таком варианте поток записи остаётся тонким: немного изменений метаданных. Поток обработки тяжёлый, но изолированный: он пересобирает конфиги, не трогая горячие запросы. Поток чтения максимально простой: один раз в несколько секунд или минут нода подхватывает свежий снапшот конфига, а все пользовательские запросы работают по нему в памяти, не трогая ни Mongo, ни файловое хранилище лишний раз.
Снаружи два решения могут выглядеть похоже: «у нас CDN, конфиг в монге, всё динамическое». Но одно построено как «каждый запрос это маленькое приключение в базу», а второе уже как нормальная работа с потоком: истина пишется отдельно, конфиг готовится отдельно, чтение живёт на готовых срезах.
Формально те же компоненты, та же монга, те же edge-ноды. А по факту в одном случае у тебя постоянная война с пиками, а в другом система, которая хотя бы не стреляет себе в ногу каждый раз, когда кто-то открыл главную страницу.
❤2
Что в итоге?
Если смотреть на систему честно, становится видно: она держится не на компонентах, а на движении данных. Пока ты рассматриваешь архитектуру как набор сервисов, все решения остаются косметикой. Поток данных идёт как шёл, и именно он определяет, где появится задержка, где начнёт жрать CPU, где система начнёт деградировать.
Когда разбиваешь поток на контуры: запись, чтение, обработку. Все проблемы приобретают форму. Становится ясно, почему запись должна быть предсказуемой, чтение быстрым, а тяжёлая логика жить отдельно. Пример с CDN на монге это хорошо показывает: одинаковые технологии снаружи дают два совершенно разных поведения в зависимости от того, управляешь ты потоком или таскаешься за ним.
Все архитектурные решения на самом деле про одно: про управление движением данных. Меняешь поток, меняется система. Не меняешь, она меняет тебя.
Поэтому порядок такой же простой, как и беспощадный:
увидеть поток, разделить поток, понять реакцию системы – и только потом выбирать инструменты.
В обратном порядке работает только красивая схема и поток говны поверх неё.
Следующая часть как раз будет про реакцию. Про то, что система делает, когда поток выходит из-под контроля. Про обратную связь. Про то, что это, и почему она так важна.
А пока совершенствуйся как специалист и никогда не останавливайся в обучении. Никогда не знаешь, где и когда, какие знания могут пригодиться.
Что еще почитать?
- 2. System design держится на трёх столпах
Если смотреть на систему честно, становится видно: она держится не на компонентах, а на движении данных. Пока ты рассматриваешь архитектуру как набор сервисов, все решения остаются косметикой. Поток данных идёт как шёл, и именно он определяет, где появится задержка, где начнёт жрать 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.
Слушай, ну какой 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
Самое смешное, почему-то этот набор устаревших правил преподносят как замену инженерному мышлению. Люди пользуются SOLID ровно так же, как школьники, пользуются «магическими формулами» на ЕГЭ. Можно собрать систему, строго следуя буквам принципов, и получить кусок говна, которы на ладан дышит и падает на первом всплеске нагрузки. Потому что в современных системах архитектура начинается не в «классах», а в данных, потоках, границах, контрактах, изоляции, стойкости к ошибкам, пропускной способности, наблюдаемости. Архитектура это про данные и обратную связь. Про инварианты, а не интерфейсы. Про то, как система нагрузку держит, а не про, то как красиво UML нарисовали.
SOLID в его буквальном прочтении, это культурный артефакт. Отголосок времени, когда «объект» считался центром вселенной. Сейчас центр это данные и их жизненный цикл. А канонический SOLID всё ещё пытается говорить тебе, что главный вопрос «правильно ли у тебя наследуется метод close()».
Код в 2к25 пишут вообще другими примитивами: функции, модули, композиция, иммутабельность, явные границы, протоколы. Даже backend теперь трактуют больше как связный поток событий, а не как набор объектов. Забавно, что в этих парадигмах SOLID как ритуалу просто… нечего делать. Мы проектируем системы, где потоки событий, идемпотентность, дедупликация, наблюдаемость, отказоустойчивость и границы сервисов решают всё. А SRP из учебника тихо курит в сторонке.
Если ты всё ещё слепо следуешь SOLID, чтобы проектировать системы, значит, ты пока не проектируешь системы. Мантры не помогут вывезти нагрузку.
Думай, а не наследуй. Архитектура – это про мышление, а не слепое следование устаревшим канонам.
SOLID в его буквальном прочтении, это культурный артефакт. Отголосок времени, когда «объект» считался центром вселенной. Сейчас центр это данные и их жизненный цикл. А канонический SOLID всё ещё пытается говорить тебе, что главный вопрос «правильно ли у тебя наследуется метод close()».
Код в 2к25 пишут вообще другими примитивами: функции, модули, композиция, иммутабельность, явные границы, протоколы. Даже backend теперь трактуют больше как связный поток событий, а не как набор объектов. Забавно, что в этих парадигмах SOLID как ритуалу просто… нечего делать. Мы проектируем системы, где потоки событий, идемпотентность, дедупликация, наблюдаемость, отказоустойчивость и границы сервисов решают всё. А SRP из учебника тихо курит в сторонке.
Если ты всё ещё слепо следуешь SOLID, чтобы проектировать системы, значит, ты пока не проектируешь системы. Мантры не помогут вывезти нагрузку.
Думай, а не наследуй. Архитектура – это про мышление, а не слепое следование устаревшим канонам.
🔥6👾1
4. System design. Обратная связь. Если бы мы знали, что это такое, но мы не знаем, что это такое
В прошлый раз я говорил про потоки данных. Про то, как данные движутся по системе, про контуры записи, чтения и обработки. Про то, что архитектура должна строиться вокруг движения данных, а не вокруг технологий, трендовых инструментов и “стандартов индустрии”.
Так вот, есть один момент, который я тогда только упомянул вскользь, а он на самом деле очень важный. Поток данных может выйти из-под контроля. Может прийти больше, чем система способна переварить. Может появиться всплеск нагрузки, который сломает всё к чертям. Может упасть внешняя зависимость, и система начнёт помирать, пытаясь дождаться ответа, которого никогда не будет.
В такие моменты система должна как-то реагировать. Непросто падать с ошибкой 503 и отправлять всех в jopa. А делать что-то умное. Что-то, что позволит ей выжить, пусть и в урезанном виде.
На помощь нам придёт “обратная связь”. Если бы мы знали, что это такое, но мы не знаем, что это такое. Шутка, конечно. На самом деле знаем, но не помним. Штука очень полезная, про неё забывают на этапе проектирования и потом напихивают в разные места, затыкая дыры. Не надо так.
В прошлый раз я говорил про потоки данных. Про то, как данные движутся по системе, про контуры записи, чтения и обработки. Про то, что архитектура должна строиться вокруг движения данных, а не вокруг технологий, трендовых инструментов и “стандартов индустрии”.
Так вот, есть один момент, который я тогда только упомянул вскользь, а он на самом деле очень важный. Поток данных может выйти из-под контроля. Может прийти больше, чем система способна переварить. Может появиться всплеск нагрузки, который сломает всё к чертям. Может упасть внешняя зависимость, и система начнёт помирать, пытаясь дождаться ответа, которого никогда не будет.
В такие моменты система должна как-то реагировать. Непросто падать с ошибкой 503 и отправлять всех в jopa. А делать что-то умное. Что-то, что позволит ей выжить, пусть и в урезанном виде.
На помощь нам придёт “обратная связь”. Если бы мы знали, что это такое, но мы не знаем, что это такое. Шутка, конечно. На самом деле знаем, но не помним. Штука очень полезная, про неё забывают на этапе проектирования и потом напихивают в разные места, затыкая дыры. Не надо так.
❤2🔥1
Что же такое обратная связь?
Обратная связь это когда система реагирует на изменения в окружающей среде и корректирует своё поведение. Звучит просто. И на практике просто, если знать, что делать. Если не знать - сложно. Такая простая штука может значительно влиять на жизнеспособность системы. Без неё система работает ровно до первого неожиданного события, а потом падает и всё: стресс, убытки, всё лежит, все грустят.
Представь термостат. Он измеряет температуру и включает или выключает обогрев, чтобы поддерживать заданную температуру. Это классический пример обратной связи: система реагирует на изменения и корректирует своё поведение. Без этого механизма температура либо постоянно росла бы, либо падала, пока всё не сломалось. В программных системах то же самое. Только вместо температуры у нас нагрузка, задержки, ошибки, доступность ресурсов. И система должна уметь реагировать на эти изменения.
Без обратной связи система становится хрупкой. Она работает ровно до тех пор, пока всё идёт по плану. Как только появляется что-то неожиданное пик нагрузки, сбой компонента, изменение требований система ложится. Потому что у неё нет механизма, который бы сказал: “Оу-оу, полегче, я такое не умею, я такое не могу и вообще мне тяжко”. По-хорошему нужно систему научить на такое реагировать. Для этого нужно как минимум понимать потоки данных в системе.
Обратная связь это когда система реагирует на изменения в окружающей среде и корректирует своё поведение. Звучит просто. И на практике просто, если знать, что делать. Если не знать - сложно. Такая простая штука может значительно влиять на жизнеспособность системы. Без неё система работает ровно до первого неожиданного события, а потом падает и всё: стресс, убытки, всё лежит, все грустят.
Представь термостат. Он измеряет температуру и включает или выключает обогрев, чтобы поддерживать заданную температуру. Это классический пример обратной связи: система реагирует на изменения и корректирует своё поведение. Без этого механизма температура либо постоянно росла бы, либо падала, пока всё не сломалось. В программных системах то же самое. Только вместо температуры у нас нагрузка, задержки, ошибки, доступность ресурсов. И система должна уметь реагировать на эти изменения.
Без обратной связи система становится хрупкой. Она работает ровно до тех пор, пока всё идёт по плану. Как только появляется что-то неожиданное пик нагрузки, сбой компонента, изменение требований система ложится. Потому что у неё нет механизма, который бы сказал: “Оу-оу, полегче, я такое не умею, я такое не могу и вообще мне тяжко”. По-хорошему нужно систему научить на такое реагировать. Для этого нужно как минимум понимать потоки данных в системе.
❤3
Есть несколько типов обратной связи:
Отрицательная обратная связь это когда система компенсирует отклонения и возвращает состояние к норме. Это стабилизация. Rate limit, который замедляет запросы при перегрузке. Circuit breaker, который отключает неработающий сервис, чтобы не тратить ресурсы впустую. Кеш, который отдаёт устаревшие данные, когда свежие недоступны. Всё это примеры отрицательной обратной связи: система реагирует на проблему и пытается вернуть всё в рабочее состояние. Это то, что тебе нужно в 99% случаев.
Положительная обратная связь это когда система усиливает отклонения. Звучит опасненько. Каскадные падения сервисов, когда один упал, нагрузка перераспределилась, другие не выдержали и тоже упали. Runaway-процессы, которые жрут всё больше ресурсов, пока система не умрёт. Но иногда положительная обратная связь нужна: например, когда система должна быстро масштабироваться при росте нагрузки. Главное контролировать этот процесс.
Backpressure это сигнал “стоп, хватит”. Когда вход растёт быстрее, чем система может переварить, она должна как-то сообщить источнику: “Полегче, я не успеваю”. Это может быть явный отказ в обработке, замедление ответов или очередь, прости господи, которая переполняется и начинает отклонять новые задачи.
Управляемая деградация это когда система жертвует частью функциональности, но остаётся работоспособной. Вместо того чтобы упасть целиком, система отключает несущественные фичи, упрощает обработку, отдаёт менее свежие данные. Ядро системы продолжает работать и предоставлять основной функционал. Это не идеальное решение, но лучше, чем полный отказ. Пользователи могут не увидеть свежие рекомендации или точную статистику, но смогут прочитать ленту и создать пост, например.
Все эти механизмы работают вместе, дополняя друг друга, создавая систему, которая может адаптироваться к изменениям. Backpressure защищает от перегрузки, управляемая деградация позволяет продолжать работать при проблемах, отрицательная обратная связь стабилизирует систему, положительная (когда нужна) позволяет быстро масштабироваться.
Где обратная связь нужна в архитектуре?
Обратная связь должна быть на всех уровнях системы. Не только в одном месте, но и везде, где система взаимодействует с внешним миром или внутренними компонентами. Классическая ошибка: добавили rate limit на входе и радуемся, что всё готово. А потом система падает из-за проблем с чтением или фоновыми задачами. “Как так? Я же защитил вход!”, ага, да, но не учёл весь поток данных.
На уровне контура записи обратная связь защищает систему от перегрузки. Rate limit ограничивает количество запросов в единицу времени. Throttling замедляет обработку, когда система перегружена. Валидация отклоняет некорректные данные до того, как они попадут в систему и начнут создавать проблемы. Без этого контур записи может стать точкой отказа: слишком много данных, система не справляется, все грустят. Классическая история: кто-то решил загрузить миллион записей через API, система пытается всё обработать, база задыхается, всё падает, DDoS. А могло бы быть просто: 429 “Слишком много запросов, попробуйте позже”.
Отрицательная обратная связь это когда система компенсирует отклонения и возвращает состояние к норме. Это стабилизация. Rate limit, который замедляет запросы при перегрузке. Circuit breaker, который отключает неработающий сервис, чтобы не тратить ресурсы впустую. Кеш, который отдаёт устаревшие данные, когда свежие недоступны. Всё это примеры отрицательной обратной связи: система реагирует на проблему и пытается вернуть всё в рабочее состояние. Это то, что тебе нужно в 99% случаев.
Положительная обратная связь это когда система усиливает отклонения. Звучит опасненько. Каскадные падения сервисов, когда один упал, нагрузка перераспределилась, другие не выдержали и тоже упали. Runaway-процессы, которые жрут всё больше ресурсов, пока система не умрёт. Но иногда положительная обратная связь нужна: например, когда система должна быстро масштабироваться при росте нагрузки. Главное контролировать этот процесс.
Backpressure это сигнал “стоп, хватит”. Когда вход растёт быстрее, чем система может переварить, она должна как-то сообщить источнику: “Полегче, я не успеваю”. Это может быть явный отказ в обработке, замедление ответов или очередь, прости господи, которая переполняется и начинает отклонять новые задачи.
Управляемая деградация это когда система жертвует частью функциональности, но остаётся работоспособной. Вместо того чтобы упасть целиком, система отключает несущественные фичи, упрощает обработку, отдаёт менее свежие данные. Ядро системы продолжает работать и предоставлять основной функционал. Это не идеальное решение, но лучше, чем полный отказ. Пользователи могут не увидеть свежие рекомендации или точную статистику, но смогут прочитать ленту и создать пост, например.
Все эти механизмы работают вместе, дополняя друг друга, создавая систему, которая может адаптироваться к изменениям. Backpressure защищает от перегрузки, управляемая деградация позволяет продолжать работать при проблемах, отрицательная обратная связь стабилизирует систему, положительная (когда нужна) позволяет быстро масштабироваться.
Где обратная связь нужна в архитектуре?
Обратная связь должна быть на всех уровнях системы. Не только в одном месте, но и везде, где система взаимодействует с внешним миром или внутренними компонентами. Классическая ошибка: добавили rate limit на входе и радуемся, что всё готово. А потом система падает из-за проблем с чтением или фоновыми задачами. “Как так? Я же защитил вход!”, ага, да, но не учёл весь поток данных.
На уровне контура записи обратная связь защищает систему от перегрузки. Rate limit ограничивает количество запросов в единицу времени. Throttling замедляет обработку, когда система перегружена. Валидация отклоняет некорректные данные до того, как они попадут в систему и начнут создавать проблемы. Без этого контур записи может стать точкой отказа: слишком много данных, система не справляется, все грустят. Классическая история: кто-то решил загрузить миллион записей через API, система пытается всё обработать, база задыхается, всё падает, DDoS. А могло бы быть просто: 429 “Слишком много запросов, попробуйте позже”.
❤3
На уровне контура чтения обратная связь обеспечивает доступность данных даже при проблемах. Кеширование позволяет отдавать данные быстро, даже если источник медленный. Fallback на устаревшие данные даёт возможность показать что-то пользователю, когда свежие данные недоступны. Read replicas распределяют нагрузку чтения, не перегружая основную базу. Без этого каждое чтение становится зависимостью от работоспособности всех компонентов, и любая проблема превращается в полный отказ. Например, система, где каждое чтение идёт в основную базу: когда база тормозит, всё падает периодически, и юзер видит йух. А могло бы быть: читаем из реплики, если реплика недоступна из кеша, если кеш пустой из основной базы, но с таймаутом. И система продолжает работать. Да, кода раза в два больше, зато надёжненько.
На уровне контура обработки обратная связь управляет приоритетами и ресурсами. Приоритизация задач позволяет обрабатывать важное в первую очередь, откладывая менее критичное. Отмена долгих операций освобождает ресурсы, когда система перегружена. Изоляция фоновых задач не даёт им мешать пользовательским запросам. Без контроля тяжёлые операции могут заблокировать всю систему, и пользователи перестанут получать ответы. Классика: фоновый джоб по пересчёту статистики запустился в пик нагрузки, сожрал CPU и память, юзеры получают отказ в обслуживании, система прилегла. А могло бы быть так: фоновые задачи останавливаются при высокой нагрузке, возобновляются, когда нагрузка спадает.
На уровне инфраструктуры обратная связь обеспечивает масштабирование и отказоустойчивость. Автоскейлинг добавляет ресурсы при росте нагрузки и убирает при снижении. Circuit breakers отключают неработающие зависимости, чтобы они не тянули систему вниз. Health checks обнаруживают проблемы до того, как они станут критичными. Это позволяет системе адаптироваться, реагировать на сбои и нагрузку. Например, система курильщика, где при падении одного сервиса все остальные продолжают пытаться его вызвать, накапливают таймауты, система деградирует. А в системе здорового человека: circuit breaker глушит упавший сервис, система продолжает работать без этого сервиса, периодически проверяет, не восстановился ли он.
Важно понимать: обратная связь на одном уровне не решает проблему полностью. Нужна система обратных связей, которая работает на всех уровнях одновременно. Иначе получится ситуация, когда ты защитил вход, но система падает из-за проблем с чтением. Или наоборот. Rate limit на входе бесполезен, если чтение из базы блокирует всю систему. Кеширование не поможет, если фоновые задачи жрут все ресурсы. Circuit breaker не спасёт, если проблема внутри, а не во внешних зависимостях.
Поэтому проектировать обратную связь нужно системно. Не “добавим rate limit и всё”, а “посмотрим, где система может сломаться, как идут потоки данных, выявим уязвимые места и добавим обратную связь там, где нужно”. Это требует понимания потоков данных, о которых мы говорили в прошлый раз. Ты можешь добавить обратную связь везде, но это будет оверинжиниринг. Или можешь добавить только в одном месте, но этого будет недостаточно. Нужно найти баланс.
На уровне контура обработки обратная связь управляет приоритетами и ресурсами. Приоритизация задач позволяет обрабатывать важное в первую очередь, откладывая менее критичное. Отмена долгих операций освобождает ресурсы, когда система перегружена. Изоляция фоновых задач не даёт им мешать пользовательским запросам. Без контроля тяжёлые операции могут заблокировать всю систему, и пользователи перестанут получать ответы. Классика: фоновый джоб по пересчёту статистики запустился в пик нагрузки, сожрал CPU и память, юзеры получают отказ в обслуживании, система прилегла. А могло бы быть так: фоновые задачи останавливаются при высокой нагрузке, возобновляются, когда нагрузка спадает.
На уровне инфраструктуры обратная связь обеспечивает масштабирование и отказоустойчивость. Автоскейлинг добавляет ресурсы при росте нагрузки и убирает при снижении. Circuit breakers отключают неработающие зависимости, чтобы они не тянули систему вниз. Health checks обнаруживают проблемы до того, как они станут критичными. Это позволяет системе адаптироваться, реагировать на сбои и нагрузку. Например, система курильщика, где при падении одного сервиса все остальные продолжают пытаться его вызвать, накапливают таймауты, система деградирует. А в системе здорового человека: circuit breaker глушит упавший сервис, система продолжает работать без этого сервиса, периодически проверяет, не восстановился ли он.
Важно понимать: обратная связь на одном уровне не решает проблему полностью. Нужна система обратных связей, которая работает на всех уровнях одновременно. Иначе получится ситуация, когда ты защитил вход, но система падает из-за проблем с чтением. Или наоборот. Rate limit на входе бесполезен, если чтение из базы блокирует всю систему. Кеширование не поможет, если фоновые задачи жрут все ресурсы. Circuit breaker не спасёт, если проблема внутри, а не во внешних зависимостях.
Поэтому проектировать обратную связь нужно системно. Не “добавим rate limit и всё”, а “посмотрим, где система может сломаться, как идут потоки данных, выявим уязвимые места и добавим обратную связь там, где нужно”. Это требует понимания потоков данных, о которых мы говорили в прошлый раз. Ты можешь добавить обратную связь везде, но это будет оверинжиниринг. Или можешь добавить только в одном месте, но этого будет недостаточно. Нужно найти баланс.
❤3
Пример
Давай посмотрим, как это работает на практике. Представь бота, который торгует на нескольких криптобиржах одновременно: Binance, OKX, Bybit. Нормальное состояние: WebSocket-потоки идут стабильно, ордера исполняются, задержки в пределах нормы. Но вот происходит что-то: новость про регуляцию, Трамп что-то сказал, или Хейс чихнул, или все вдруг решили поторговать. Классическая ситуация: всё было хорошо и вдруг всё плохо. Наверное, ты даже где-то такое видел?
Без обратной связи система пытается обработать всё. Каждое обновление из WS обрабатывается, каждый тик анализируется, каждый ордер отправляется на биржу. Потоки начинают душить систему, очередь сообщений растёт, задержки увеличиваются с миллисекунд до секунд. Система начинает принимать плохие решения, потому что цены меняются быстрее, чем система успевает на них реагировать. А потом система вообще ложится. Какой-то сервис положил, допустим, базу или распределённый кеш, откуда читает контур принятия решений. В тг к тебе уже стучатся: “Бот лёг? Там движение! Ордеров нет!” А ты в душе не представляешь, почему оно там лежит и что вообще происходит.
Давай посмотрим, как это работает на практике. Представь бота, который торгует на нескольких криптобиржах одновременно: Binance, OKX, Bybit. Нормальное состояние: WebSocket-потоки идут стабильно, ордера исполняются, задержки в пределах нормы. Но вот происходит что-то: новость про регуляцию, Трамп что-то сказал, или Хейс чихнул, или все вдруг решили поторговать. Классическая ситуация: всё было хорошо и вдруг всё плохо. Наверное, ты даже где-то такое видел?
Без обратной связи система пытается обработать всё. Каждое обновление из WS обрабатывается, каждый тик анализируется, каждый ордер отправляется на биржу. Потоки начинают душить систему, очередь сообщений растёт, задержки увеличиваются с миллисекунд до секунд. Система начинает принимать плохие решения, потому что цены меняются быстрее, чем система успевает на них реагировать. А потом система вообще ложится. Какой-то сервис положил, допустим, базу или распределённый кеш, откуда читает контур принятия решений. В тг к тебе уже стучатся: “Бот лёг? Там движение! Ордеров нет!” А ты в душе не представляешь, почему оно там лежит и что вообще происходит.
❤3
Теперь добавим обратную связь на разных уровнях.
На контуре записи (WS-потоки) ставим backpressure. Когда система не успевает обрабатывать обновления из WebSocket, она сигнализирует: “Падажди, я не успеваю”. Это может быть явный отказ принимать новые сообщения или замедление обработки. Но важно не переборщить: слишком агрессивный backpressure заставит тебя пропустить важные обновления цены, слишком мягкий не поможет при реальном всплеске. Нужно найти золотую середину. Стоит начать с консервативных значений и вручную, по метрикам, подгонять при необходимости.
На контуре чтения (обработка котировок) включаем приоритизацию. Критичные пары BTC/USDT, ETH/USDT обрабатываются в первую очередь, менее ликвидные потом. Если система перегружена, она может временно отключить обработку некоторых пар, сосредоточившись на самых важных. Это лучше, чем пытаться обработать всё и упасть.
На контуре обработки (торговая логика) приоритизируем ордера. Критичные ордера те, что должны исполниться немедленно, например, обрабатываются в первую очередь. Менее критичные ордера откладываются, когда система перегружена. Если нагрузка критическая, система может временно отключить некоторые стратегии, освобождая ресурсы для самых важных.
Для внешних зависимостей бирж, API-провайдеров, ораклов организуем circuit breakers. Если биржа не отвечает или отвечает слишком медленно, circuit breaker “разрывает цепь” и перестаёт отправлять ордера на эту биржу на некоторое время. Система продолжает торговать на других биржах, но не падает из-за проблем с одной биржей. Пример ошибки: бот отправляет ордер на Binance, тот не отвечает (rate limit или просто проблемы с сетью), бот ждёт таймаут, накапливает ордера, падает. А могло бы быть: circuit breaker разрывает цепь, бот продолжает работать на других биржах, периодически проверяет, не восстановился ли Binance.
Всё это вместе создаёт систему, которая может деградировать управляемо. При всплеске волатильности она не падает, а упрощается: обрабатывает только критичные пары, отключает менее важные стратегии, перестаёт торговать на проблемных биржах. Система может заработать меньше, но не потеряет всё из-за полного отказа.
На контуре записи (WS-потоки) ставим backpressure. Когда система не успевает обрабатывать обновления из WebSocket, она сигнализирует: “Падажди, я не успеваю”. Это может быть явный отказ принимать новые сообщения или замедление обработки. Но важно не переборщить: слишком агрессивный backpressure заставит тебя пропустить важные обновления цены, слишком мягкий не поможет при реальном всплеске. Нужно найти золотую середину. Стоит начать с консервативных значений и вручную, по метрикам, подгонять при необходимости.
На контуре чтения (обработка котировок) включаем приоритизацию. Критичные пары BTC/USDT, ETH/USDT обрабатываются в первую очередь, менее ликвидные потом. Если система перегружена, она может временно отключить обработку некоторых пар, сосредоточившись на самых важных. Это лучше, чем пытаться обработать всё и упасть.
На контуре обработки (торговая логика) приоритизируем ордера. Критичные ордера те, что должны исполниться немедленно, например, обрабатываются в первую очередь. Менее критичные ордера откладываются, когда система перегружена. Если нагрузка критическая, система может временно отключить некоторые стратегии, освобождая ресурсы для самых важных.
Для внешних зависимостей бирж, API-провайдеров, ораклов организуем circuit breakers. Если биржа не отвечает или отвечает слишком медленно, circuit breaker “разрывает цепь” и перестаёт отправлять ордера на эту биржу на некоторое время. Система продолжает торговать на других биржах, но не падает из-за проблем с одной биржей. Пример ошибки: бот отправляет ордер на Binance, тот не отвечает (rate limit или просто проблемы с сетью), бот ждёт таймаут, накапливает ордера, падает. А могло бы быть: circuit breaker разрывает цепь, бот продолжает работать на других биржах, периодически проверяет, не восстановился ли Binance.
Всё это вместе создаёт систему, которая может деградировать управляемо. При всплеске волатильности она не падает, а упрощается: обрабатывает только критичные пары, отключает менее важные стратегии, перестаёт торговать на проблемных биржах. Система может заработать меньше, но не потеряет всё из-за полного отказа.
❤4