Слоистая архитектура, часть 2
Каждый МС берет на себя как минимум две ответственности. К чему это приводит? МСы, которые могли отвечать только за UI или только за домен — смешиваются в один слой. Конечно, аналитику или разработчику разобраться, какие МСы и как между собой взаимодействуют, становится сложнее. Это является причиной возникновения циклических зависимостей уже на уровне взаимодействия МСов (когда систему нельзя представить в виде направленного ациклического графа). Такую систему очень тяжело поддерживать и масштабировать.
Получилось так, что архитектура МСа повлияла на архитектуру системы. Исходно в системе имела место ошибка проектирования, при которой произошло перемешивание слоев на всех уровнях. Требовалось выработать новый подход, чтобы наш BFF был поддерживаемым. К какой модели мы пришли (картинка ниже, вторая)?
view-api — это API, отвечающая за агрегацию доменных данных и вывод необходимой информации на экран клиенту. Требования к view-api предъявляются следующие:
- все эндпоинты view-api доступны только с фронтов, т.е. вызывать их из других API нельзя
- данные для отображения на экране, которые не приходят из core-api, должны прописываться в конфигурации view-api (текст, цвета кнопок, фона и др.)
- из view-api нельзя обращаться напрямую в back-системы
core-api — API, отвечающая за получение и обработку доменных данных с возможностью переиспользования. Требования к core-api предъявляются следующие:
- все ручки core-api недоступны с фронтов
- core-api ничего не знают про данные для отображения на экране (текст, цвета кнопок и фона), только если не проксируют эти данные с back-системы
- в идеале обращений между core-api тоже быть не должно
По результатам анализа системы мы пришли к строгой (обращение между узлами одного слоя или строго одного нижележащего слоя) многослойной архитектуре на уровне взаимодействия сервисов в системе. Теперь каждый сервис отвечает либо за работу с доменом, либо за UI. Мы развязали эти независимые модули/слои, которые ранее были скреплены, чтобы добиться лучшего повторного использования (особенно доменных сервисов). За счет чего достигается строгость? Фронты теперь могут обращаться только к view-api, view-api — только к core-api, а core-api — только к back-системам и друг к другу.
Каждый МС берет на себя как минимум две ответственности. К чему это приводит? МСы, которые могли отвечать только за UI или только за домен — смешиваются в один слой. Конечно, аналитику или разработчику разобраться, какие МСы и как между собой взаимодействуют, становится сложнее. Это является причиной возникновения циклических зависимостей уже на уровне взаимодействия МСов (когда систему нельзя представить в виде направленного ациклического графа). Такую систему очень тяжело поддерживать и масштабировать.
Получилось так, что архитектура МСа повлияла на архитектуру системы. Исходно в системе имела место ошибка проектирования, при которой произошло перемешивание слоев на всех уровнях. Требовалось выработать новый подход, чтобы наш BFF был поддерживаемым. К какой модели мы пришли (картинка ниже, вторая)?
view-api — это API, отвечающая за агрегацию доменных данных и вывод необходимой информации на экран клиенту. Требования к view-api предъявляются следующие:
- все эндпоинты view-api доступны только с фронтов, т.е. вызывать их из других API нельзя
- данные для отображения на экране, которые не приходят из core-api, должны прописываться в конфигурации view-api (текст, цвета кнопок, фона и др.)
- из view-api нельзя обращаться напрямую в back-системы
core-api — API, отвечающая за получение и обработку доменных данных с возможностью переиспользования. Требования к core-api предъявляются следующие:
- все ручки core-api недоступны с фронтов
- core-api ничего не знают про данные для отображения на экране (текст, цвета кнопок и фона), только если не проксируют эти данные с back-системы
- в идеале обращений между core-api тоже быть не должно
По результатам анализа системы мы пришли к строгой (обращение между узлами одного слоя или строго одного нижележащего слоя) многослойной архитектуре на уровне взаимодействия сервисов в системе. Теперь каждый сервис отвечает либо за работу с доменом, либо за UI. Мы развязали эти независимые модули/слои, которые ранее были скреплены, чтобы добиться лучшего повторного использования (особенно доменных сервисов). За счет чего достигается строгость? Фронты теперь могут обращаться только к view-api, view-api — только к core-api, а core-api — только к back-системам и друг к другу.
Кто готов посмотреть битву 20$ / month ChatGPT 4 против Completely Free Llama-3?
Попробуем отрефакторить один и тот же метод на языке Kotlin с помощью двух этих моделей здесь.
Попробуем отрефакторить один и тот же метод на языке Kotlin с помощью двух этих моделей здесь.
👨💻4👍2
Рефакторинг. Выделение метода, часть 1
Кажется, все знают, что функции/методы нужно делать короче. Вот как восторгается Роберт Мартин программе, написанной с помощью лаконичных функций:
… каждая функция в программе … занимала всего две, три или четыре строки. Все функции были предельно очевидными. Каждая функция излагала свою историю, и каждая история естественным образом подводила вас к началу следующей истории. Вот какими короткими должны быть функции!
Давайте разберем этот абзац, чтобы понять, как написать программу, которая порадовала бы Роберта Мартина. В первую очередь — это следовать SRP, однако не нужно ударяться в крайности, начиная делить код на множество микроскопических функций. Важно, чтобы те операции, которые вы выносите в функцию, находились на одном и том же уровне абстракции и являлись семантически независимыми (по отношению к остальному коду) и значимыми. Именно в таком случае функции будут представлять из себя строительные блоки, из которых может быть построена поддерживаемая программа. А дальше все просто: будучи носителями одной ответственности/семантики таким функциям несложно будет дать ясное и точное имя.
Кажется, все знают, что функции/методы нужно делать короче. Вот как восторгается Роберт Мартин программе, написанной с помощью лаконичных функций:
… каждая функция в программе … занимала всего две, три или четыре строки. Все функции были предельно очевидными. Каждая функция излагала свою историю, и каждая история естественным образом подводила вас к началу следующей истории. Вот какими короткими должны быть функции!
Давайте разберем этот абзац, чтобы понять, как написать программу, которая порадовала бы Роберта Мартина. В первую очередь — это следовать SRP, однако не нужно ударяться в крайности, начиная делить код на множество микроскопических функций. Важно, чтобы те операции, которые вы выносите в функцию, находились на одном и том же уровне абстракции и являлись семантически независимыми (по отношению к остальному коду) и значимыми. Именно в таком случае функции будут представлять из себя строительные блоки, из которых может быть построена поддерживаемая программа. А дальше все просто: будучи носителями одной ответственности/семантики таким функциям несложно будет дать ясное и точное имя.
👍3
Рефакторинг. Выделение метода, часть 2
Следование SRP выглядит несложно, однако всегда ли мы можем добиться его выполнения? Если вычисления в большом методе идут последовательно, где результаты каждого предыдущего этапа передаются следующему, как по конвейеру, то проблем не возникает. Но что, если это автономные потоки вычислений, которые многократно пронизывают друг друга в процессе прохождения по функции? На ум сразу приходят проекты, в которых используются библиотеки с низким уровнем абстракции. В данном случае, например, пользовательский поток переплетается с техническим, необходимым для управления работой клиента.
Что еще можно привести в качестве примеров? Конечно, использование сквозной функциональности:
- поэтапные логирование и аудит;
- замеры производительности отдельных участков кода и сбор метрик;
- и т.д.
Вот, например, поток аудита переплетается с бизнес-функциональностью подтверждения оффера:
Этот код можно было бы также оформить в виде конечного автомата, однако такое решение кажется здесь избыточным из-за малого числа состояний. Еще одним вариантом разделения двух несвязанных семантик в данном примере может быть применение техник АОП, которые однако не славятся простотой чтения и поддержки.
Что если в препарируемом методе нет автономных потоков вычислений, но и выделение новых методов кажется невозможным? Вероятно, в таком случае мы имеем дело с плохо написанным, запутанным кодом, который требует предварительной подготовки с использованием других техник рефакторинга.
Следование SRP выглядит несложно, однако всегда ли мы можем добиться его выполнения? Если вычисления в большом методе идут последовательно, где результаты каждого предыдущего этапа передаются следующему, как по конвейеру, то проблем не возникает. Но что, если это автономные потоки вычислений, которые многократно пронизывают друг друга в процессе прохождения по функции? На ум сразу приходят проекты, в которых используются библиотеки с низким уровнем абстракции. В данном случае, например, пользовательский поток переплетается с техническим, необходимым для управления работой клиента.
Что еще можно привести в качестве примеров? Конечно, использование сквозной функциональности:
- поэтапные логирование и аудит;
- замеры производительности отдельных участков кода и сбор метрик;
- и т.д.
Вот, например, поток аудита переплетается с бизнес-функциональностью подтверждения оффера:
suspend fun confirmOffer(...) {
...
try {
auditService.updateConfirmationStatus(auditReference, OperationStatus.PROGRESS)
offerService.confirmOffer(...)
auditService.updateConfirmationStatus(auditReference, OperationStatus.SUCCESS)
...
} catch (e: Exception) {
...
auditService.updateConfirmationStatus(auditReference, OperationStatus.FAIL)
...
}Этот код можно было бы также оформить в виде конечного автомата, однако такое решение кажется здесь избыточным из-за малого числа состояний. Еще одним вариантом разделения двух несвязанных семантик в данном примере может быть применение техник АОП, которые однако не славятся простотой чтения и поддержки.
Что если в препарируемом методе нет автономных потоков вычислений, но и выделение новых методов кажется невозможным? Вероятно, в таком случае мы имеем дело с плохо написанным, запутанным кодом, который требует предварительной подготовки с использованием других техник рефакторинга.
👍2
Предлагаю на примере Apache Kafka рассмотреть, какие есть подводные камни при сборе и использовании метрик. Не пугайтесь, если вы не знакомы с Kafka — эти идеи могут всплыть при мониторинге абсолютно любой системы.
👍2
Обзор DevSecOps, часть 1
Продолжаем тему Security при разработке приложений. На этой неделе проходил обучение по теме безопасной разработки ПО. Хочу поделиться с вами некоторой выжимкой.
Так полюбившиеся бизнесу гибкие методологии разработки (на основе Agile-манифеста) сильно повлияли на процессы разработки ПО в целом и на подход к информационной безопасности в частности. Как это выражается? Мы не сможем планомерно отдавать “безопасные” релизы, если будем следовать моделям “предыдущих поколений”, например Waterfall. Так появился Манифест Agile Security (Agile Security Manifesto). Его приоритеты естественным образом вытекают из принципов модели Agile:
- опора на разработчиков и тестировщиков, а не на специалистов безопасности
- проверка на безопасность в процессе, а не после завершения рабочего цикла
- безопасная реализация функциональности, а не добавление функций безопасности
- снижение рисков, а не поиск уязвимостей
Тут стоит отметить, что мы не упраздняем позицию “безопасник” в нашей структуре, а пытаемся с самого начала делать защищенный продукт за счет интеграции элементов Application Security (Sec) во все фазы цикла Development & Operations (DevOps).
Продолжаем тему Security при разработке приложений. На этой неделе проходил обучение по теме безопасной разработки ПО. Хочу поделиться с вами некоторой выжимкой.
Так полюбившиеся бизнесу гибкие методологии разработки (на основе Agile-манифеста) сильно повлияли на процессы разработки ПО в целом и на подход к информационной безопасности в частности. Как это выражается? Мы не сможем планомерно отдавать “безопасные” релизы, если будем следовать моделям “предыдущих поколений”, например Waterfall. Так появился Манифест Agile Security (Agile Security Manifesto). Его приоритеты естественным образом вытекают из принципов модели Agile:
- опора на разработчиков и тестировщиков, а не на специалистов безопасности
- проверка на безопасность в процессе, а не после завершения рабочего цикла
- безопасная реализация функциональности, а не добавление функций безопасности
- снижение рисков, а не поиск уязвимостей
Тут стоит отметить, что мы не упраздняем позицию “безопасник” в нашей структуре, а пытаемся с самого начала делать защищенный продукт за счет интеграции элементов Application Security (Sec) во все фазы цикла Development & Operations (DevOps).
👍3
Обзор DevSecOps, часть 2
Обратимся к графическому представлению DevSecOps и кратко пробежимся по предложенным методам.
SAST — это метод поиска уязвимостей в приложении, основанный на анализе исходного кода без его фактического выполнения. Как любой уважающий себя статический анализатор, он быстро выполняет анализ кода происходит без запуска приложения, но может выдавать как ложноположительные, так и ложноотрицательные срабатывания.
Для проверки на наличие известных уязвимостей и анализа лицензионной чистоты подключаемых компонентов используют:
- на этапе дизайна — OSA (Open-Source Analysis)
- на этапе сборки — SCA (Software Composition Analysis)
Пример опасных уязвимостей, которые может выявить инструмент SCA:
- log4shell – критическая уязвимость в библиотеке log4j, позволяющая запускать неавторизованный удалённый код
- spring4shell – критическая уязвимость во фреймворке Spring, которая позволяет запускать произвольный код на сервере (RCE)
Инструментальные средства DAST анализируют приложения без доступа к их исходному коду, то есть методом «черного ящика». В отличие от SAST, менее привязан к языку программирования, на котором написано приложение, а также позволяет проверить влияние окружения, нюансов развернутой БД, сервера и прочих деталей системы. Конечно, вручную придется разбирать большое число ложных срабатываний.
Более распространенным термином для AppSec.Hub является ASOC (Application Security Orchestration & Correlation). ASOC-платформа обеспечивает оркестрацию проверок и работу с их результатами на всем протяжении жизненного цикла ПО.
Для полноты картины отмечу, что для анализа уровня безопасности мобильных программ используется специальный набор практик и методов тестирования – MAST (Mobile Application Security Testing).
В блоке BCA есть упоминание Container Analysis, который совместно с BAST в случае оркестрации контейнеризированных приложений образуют Container Security (также встречается термин SecOps) — это непрерывный процесс и комплексное мероприятие по обеспечению безопасности контейнеризированных приложений на всех этапах их жизненного цикла, а также платформ и систем, сопровождающих программные продукты. Процесс частично включает в себя практики SAST, SCA, DAST, Security Compliance и Observability. На данный момент основной тулсет составляют решения классов:
IaC scanner — сканирование файлов конфигураций бизнес — и/или инфраструктурных сервисов. Например, docker file, yaml-манифесты, ansible роли и т.д. Чаще всего также имеет функциональность поиска cve используемых компонент и небезопасного хранения секретов
Policy Engine/Admission Controller — контроллер и sec gate для ресурсов при деплое и в runtime микросервисов. Позволяет определять несоответствие спецификациям ИБ, блокировать поставки, изменять конфигурации или создавать дополнительные ресурсы
CSP (Container Security Platform) — обсервабилити решение с прицелом на ИБ функции
LSM — Linux Security Modules а также eBPF-based Security Observability and Runtime Enforcement. Данный класс инструментов направлен на ограничение процессов совершать определенные syscall
Наконец WAF — это аппаратное устройство или программное обеспечение, которое анализирует и фильтрует весь входящий и исходящий HTTP(S)-трафик.
Конечно, все это сверху должно быть приправлено пентестами и программой Bug Bounty для, действительно, комплексной проверки выстроенных мер защиты.
Обратимся к графическому представлению DevSecOps и кратко пробежимся по предложенным методам.
SAST — это метод поиска уязвимостей в приложении, основанный на анализе исходного кода без его фактического выполнения. Как любой уважающий себя статический анализатор, он быстро выполняет анализ кода происходит без запуска приложения, но может выдавать как ложноположительные, так и ложноотрицательные срабатывания.
Для проверки на наличие известных уязвимостей и анализа лицензионной чистоты подключаемых компонентов используют:
- на этапе дизайна — OSA (Open-Source Analysis)
- на этапе сборки — SCA (Software Composition Analysis)
Пример опасных уязвимостей, которые может выявить инструмент SCA:
- log4shell – критическая уязвимость в библиотеке log4j, позволяющая запускать неавторизованный удалённый код
- spring4shell – критическая уязвимость во фреймворке Spring, которая позволяет запускать произвольный код на сервере (RCE)
Инструментальные средства DAST анализируют приложения без доступа к их исходному коду, то есть методом «черного ящика». В отличие от SAST, менее привязан к языку программирования, на котором написано приложение, а также позволяет проверить влияние окружения, нюансов развернутой БД, сервера и прочих деталей системы. Конечно, вручную придется разбирать большое число ложных срабатываний.
Более распространенным термином для AppSec.Hub является ASOC (Application Security Orchestration & Correlation). ASOC-платформа обеспечивает оркестрацию проверок и работу с их результатами на всем протяжении жизненного цикла ПО.
Для полноты картины отмечу, что для анализа уровня безопасности мобильных программ используется специальный набор практик и методов тестирования – MAST (Mobile Application Security Testing).
В блоке BCA есть упоминание Container Analysis, который совместно с BAST в случае оркестрации контейнеризированных приложений образуют Container Security (также встречается термин SecOps) — это непрерывный процесс и комплексное мероприятие по обеспечению безопасности контейнеризированных приложений на всех этапах их жизненного цикла, а также платформ и систем, сопровождающих программные продукты. Процесс частично включает в себя практики SAST, SCA, DAST, Security Compliance и Observability. На данный момент основной тулсет составляют решения классов:
IaC scanner — сканирование файлов конфигураций бизнес — и/или инфраструктурных сервисов. Например, docker file, yaml-манифесты, ansible роли и т.д. Чаще всего также имеет функциональность поиска cve используемых компонент и небезопасного хранения секретов
Policy Engine/Admission Controller — контроллер и sec gate для ресурсов при деплое и в runtime микросервисов. Позволяет определять несоответствие спецификациям ИБ, блокировать поставки, изменять конфигурации или создавать дополнительные ресурсы
CSP (Container Security Platform) — обсервабилити решение с прицелом на ИБ функции
LSM — Linux Security Modules а также eBPF-based Security Observability and Runtime Enforcement. Данный класс инструментов направлен на ограничение процессов совершать определенные syscall
Наконец WAF — это аппаратное устройство или программное обеспечение, которое анализирует и фильтрует весь входящий и исходящий HTTP(S)-трафик.
Конечно, все это сверху должно быть приправлено пентестами и программой Bug Bounty для, действительно, комплексной проверки выстроенных мер защиты.
👍3
А у вас в компании построили DevSecOps?
Anonymous Poll
31%
👍
44%
спасибо, что есть хотя бы DevOps
19%
нам не нужно
6%
🙅♂️
🗿2
Варианты расширения функциональности классов и их влияние на модель данных в Kotlin [1/2]
Возьмем для рассмотрения простой пример. Есть библиотечный класс, который используется во многих местах в нашем сервисе для представления банковских карт пользователя:
Кое-где нам нужно получать приоритет (абсолютный вес) конкретного экземпляра для последующей сортировки и фильтрации.
Какие есть варианты реализации?
1️⃣ Наивный вариант — добавляем в исходный класс новое поле priority
Выглядит удобно: при вызове конструктора однократно выполняются вычисление и присваивание приоритета, после чего в программе это поле переиспользуем. В глаза бросаются сразу два ограничения:
❌ решение технически нереализуемо для закрытых для изменений third-party библиотек
❌ смешиваем технические и доменные поля в одном классе
Даже если мы имеем возможность модифицировать класс Card, то внесение в него нового поля, оторванного от домена, не пойдет на пользу модели данных. Стоит ли говорит о том, что глобальное расширение библиотечного класса усложнит аналитику и тестирование коллегам за пределами нашего сервиса. Также отметим, что приоритет карты даже в рамках одного сервиса нужен не везде.
2️⃣ Используем наследование или паттерн Декоратор (Decorator)
Если библиотечный класс помечен как open, то мы можем использовать наследование, иначе — декоратор. Какие здесь могут быть ограничения? Если область применения вновь созданных классов носит локальный характер, то мы получим колоссальный объем избыточного кода.
✅ подходит для расширения даже third-party библиотек
❗️для решения локальных задач создание новых классов может быть нецелесообразно
С учетом наших вводных создание наследника или декоратора класса Card выглядит неуместным.
Возьмем для рассмотрения простой пример. Есть библиотечный класс, который используется во многих местах в нашем сервисе для представления банковских карт пользователя:
data class Card(val id: String, ...)
Кое-где нам нужно получать приоритет (абсолютный вес) конкретного экземпляра для последующей сортировки и фильтрации.
Какие есть варианты реализации?
1️⃣ Наивный вариант — добавляем в исходный класс новое поле priority
Выглядит удобно: при вызове конструктора однократно выполняются вычисление и присваивание приоритета, после чего в программе это поле переиспользуем. В глаза бросаются сразу два ограничения:
❌ решение технически нереализуемо для закрытых для изменений third-party библиотек
❌ смешиваем технические и доменные поля в одном классе
Даже если мы имеем возможность модифицировать класс Card, то внесение в него нового поля, оторванного от домена, не пойдет на пользу модели данных. Стоит ли говорит о том, что глобальное расширение библиотечного класса усложнит аналитику и тестирование коллегам за пределами нашего сервиса. Также отметим, что приоритет карты даже в рамках одного сервиса нужен не везде.
2️⃣ Используем наследование или паттерн Декоратор (Decorator)
Если библиотечный класс помечен как open, то мы можем использовать наследование, иначе — декоратор. Какие здесь могут быть ограничения? Если область применения вновь созданных классов носит локальный характер, то мы получим колоссальный объем избыточного кода.
✅ подходит для расширения даже third-party библиотек
❗️для решения локальных задач создание новых классов может быть нецелесообразно
С учетом наших вводных создание наследника или декоратора класса Card выглядит неуместным.
Варианты расширения функциональности классов и их влияние на модель данных в Kotlin [2/2]
3️⃣ Используем функции расширения
Kotlin предоставляет возможность расширять класс или интерфейс новой функциональностью без необходимости наследования от класса или использования шаблонов проектирования, таких как Decorator. Это делается с помощью специальных объявлений, называемых функциями расширения (extension functions):
Такие функции можно вызывать обычным способом, как если бы они были методами исходного класса, однако все же потребуется их импортировать явно:
✅ подходит для расширения даже third-party библиотек
✅ минимальное количество нового кода, который не выходит за пределы сервиса
⚠️ доступ к методам, расширяющим исходную модель, ограничен только необходимостью явного импорта
❗️потребовалось введение внешнего хранилища
Несмотря на то, что мы ввели внешнее хранилище, мы оказываем незначительное влияние на исходную модель данных. Можно модифицировать этот код так, чтобы вызовы setPriority и getPriority можно было делать только в явно ограниченном контексте. Для этого можно использовать вложенные классы или функции с приемом контекста. Вот пример использования вложенного класса:
Вот как будет выглядеть вызов:
Пример с использованием функции, которая принимает контекст можно посмотреть здесь.
В чем отличие от исходного варианта?
✅ доступ к методам, расширяющим исходную модель, ограничен в коде явно
⚠️ требуется вызов конструктора на каждый возов логики
Единственным минусом последнего решения могу назвать то, что если не знать об уже имеющейся реализации, то без должного анализа кода сервиса можно не нарочно создать дублирующую реализацию. IntelliJ IDEA, например, вам никак не сообщит о наличии таковой.
📝 Какой можем сделать вывод? Разные технические решения по-разному влияют на модель данных в вашей программе. Руководствуйтесь принципом разумной достаточности при внесении изменений, обращая наибольшее внимание на модификацию библиотечных моделей и корневых классов в иерархии наследования.
3️⃣ Используем функции расширения
Kotlin предоставляет возможность расширять класс или интерфейс новой функциональностью без необходимости наследования от класса или использования шаблонов проектирования, таких как Decorator. Это делается с помощью специальных объявлений, называемых функциями расширения (extension functions):
package edu.some
object CardPriority {
private val priorityMap = WeakHashMap<Card, Int>()
fun Card.setPriority(priority: Int) {
priorityMap[this] = priority
}
fun Card.getPriority(): Int? {
return priorityMap[this]
}
}
Такие функции можно вызывать обычным способом, как если бы они были методами исходного класса, однако все же потребуется их импортировать явно:
package edu.other
import edu.some.CardPriority.getPriority
import edu.some.CardPriority.setPriority
fun main() {
card.setPriority(2)
println("Card priority is ${card.getPriority()}")
}
✅ подходит для расширения даже third-party библиотек
✅ минимальное количество нового кода, который не выходит за пределы сервиса
⚠️ доступ к методам, расширяющим исходную модель, ограничен только необходимостью явного импорта
❗️потребовалось введение внешнего хранилища
Несмотря на то, что мы ввели внешнее хранилище, мы оказываем незначительное влияние на исходную модель данных. Можно модифицировать этот код так, чтобы вызовы setPriority и getPriority можно было делать только в явно ограниченном контексте. Для этого можно использовать вложенные классы или функции с приемом контекста. Вот пример использования вложенного класса:
package edu.some
object CardPriority {
private val priorityMap = WeakHashMap<Card, Int>()
class Context {
fun Card.setPriority(priority: Int) {
priorityMap[this] = priority
}
fun Card.getPriority(): Int? {
return priorityMap[this]
}
}
fun withCardPriorityContext(action: Context.() -> Unit) {
Context().action()
}
}
Вот как будет выглядеть вызов:
package edu.other
import edu.some.CardPriority.withCardPriorityContext
fun main() {
...
withCardPriorityContext {
card.setPriority(2)
println("Card ${card.id} is ${card.getPriority()} priority")
}
// cледующие вызовы не скомпилируются, так как находятся вне контекста
// card.setPriority(2)
// println("Card priority is ${card.getPriority()}")
}
Пример с использованием функции, которая принимает контекст можно посмотреть здесь.
В чем отличие от исходного варианта?
✅ доступ к методам, расширяющим исходную модель, ограничен в коде явно
⚠️ требуется вызов конструктора на каждый возов логики
Единственным минусом последнего решения могу назвать то, что если не знать об уже имеющейся реализации, то без должного анализа кода сервиса можно не нарочно создать дублирующую реализацию. IntelliJ IDEA, например, вам никак не сообщит о наличии таковой.
📝 Какой можем сделать вывод? Разные технические решения по-разному влияют на модель данных в вашей программе. Руководствуйтесь принципом разумной достаточности при внесении изменений, обращая наибольшее внимание на модификацию библиотечных моделей и корневых классов в иерархии наследования.
Сходил в гости к Володе, автору каналу канала System Design World. Проектировали файловое хранилище. Оцените, получилось гуд 😆 или супер гуд 😃 ? Прямая ссылка на YT.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3⚡1
Как вам результат? Хотел написать пост, когда счетчик достигнет 20, но в последнее время отдавал приоритет менти, которые приходят на повторные консультации. Самый крутой результат из этих ребят — джавист с 1 годом коммерческого опыта повысил ЗП со 100 до 265 т.р. на руки, устроившись в финтех на позицию middle. Если вы до сих пор топчитесь на месте по своим карьерным целям, то рекомендую все же заскочить ко мне на бесплатную часовую консультацию. Многим этого более чем достаточно, чтобы сдвинуться с мертвой точки, а иногда и покорить очередную неприступную вершину.
getmentor.dev
Максим Гусев | GetMentor – открытое сообщество IT-наставников
Java Tech Lead @ Альфа-Банк | GetMentor – это открытое комьюнити IT-наставников, готовых делиться своими опытом и знаниями. Наша задача – помогать людям находить ответы на свои вопросы в работе или жизни через прямой доступ к экспертизе в разговоре 1-на-1.
👍1
Как не облажаться, когда раскатываешь фичу на 10 миллионов пользователей?
Я как лид middle-разработки (почему middle? потому что наш кластер находится между бэкендом и потребителями-фронтами, в числе которых мобильные клиенты, сайт и др.) часто принимаю участие в раскатке новой функциональности на пользователей. Как выглядит этот процесс?
Представляете, если бы я так сказал🤡 ? Конечно, все устроено немного иначе.
К сожалению или к счастью, подвергнуть НТ абсолютно все фичи не представляется возможным. Это достаточно расточительное мероприятие, которое съедает самый ценный ресурс — время.
Как быть то? Для каждой новой выводимой фичи мы хотим понять, как она будет работать на проде. Вместо того, чтобы рисковать и выкатывать её всем пользователям сразу, ты просто в коде оборачиваешь её в feature toggle — своего рода переключатель. Настройка этого переключателя позволяет сопровождению включать или отключать определённые функции в приложении без необходимости изменять код или деплоить новые версии.
Теперь можно включить эту фичу только для небольшой группы пользователей или только для себя, чтобы протестировать её в боевых условиях. Если всё ок, можно постепенно расширять к ней доступ. Если что-то пойдёт не так (рост числа ошибок в логах или обращений пользователей) — просто выключаешь переключатель. Это делает процесс выпуска новых функций гораздо гибче и безопаснее.
Однако кроме очевидных проблем, которые отображаются в логах в виде ошибочных сообщений, я и мои разработчики периодически сталкиваемся с менее очевидными. Проанализировав их, я составил чек-лист, который закрывает большинство из них. Этот чек-лист сильно зависит от специфики проекта и выстроенных процессов, и в вашем случае может значительно отличаться:
1️⃣ Проверить, что при обновлении библиотек у сервиса не оторвало логи, метрики, трейсинг и не начала течь память
2️⃣ Проверить, что новые запросы к БД используют созданные индексы
3️⃣ Проанализировать утилизацию ресурсов (самого сервиса и интеграций) и при необходимости скорректировать лимиты HPA (HorizontalPodAutoscaler в k8s), istio
4️⃣Проверить, что распределенный кэш работает корректно
Здесь нужно помнить сразу о нескольких моментах:
1) в кэш в принципе должны попадать нужные нам данные, а затем из него правильно десериализоваться
2) должно быть учтено состояние гонки, которое может возникнуть в момент "одновременной" записи значений несколькими потоками/приложениями по одному и тому же ключу -> должна быть синхронизация, чтобы к бэкенду ушел всего один запрос. То же актуально и при протухании популярного ключа
3) утилизация ресурсов кластера кэша не должна быть слишком высокой, чтобы не потерять в эффективности кэширования
4) если количество ключей для определенной мапы ограничено, то стоит рассмотреть возможность дополнить распределенный кэш in-memory составляющей на уровне сервиса, чтобы не гонять данные по сети
Часть из этого списка уже внедрена или автоматизирована, с другой частью нам еще предстоит проработать решения на всех этапах: от аналитики до сопровождения в проде.
P.S. Хорошим дополнением/альтернативой механизма feature toggle может служить Canary Deployment, который позволяет делать примерно то же за счет задания критериев раскатки фичи на пользователей. Инструмент, однако, обладает меньшим уровнем детализации, так как не внедрен в кодовую базу раскатываемого сервиса и часто ограничен возможностями конкретной реализации.
Я как лид middle-разработки (почему middle? потому что наш кластер находится между бэкендом и потребителями-фронтами, в числе которых мобильные клиенты, сайт и др.) часто принимаю участие в раскатке новой функциональности на пользователей. Как выглядит этот процесс?
После того, как фича разработана и протестирована, она в пятницу вечером отгружается в прод на всех пользователей.
Представляете, если бы я так сказал
Мы проводим нагрузочное тестирование (НТ) для каждой новой фичи и все скрытые проблемы становятся явными.
К сожалению или к счастью, подвергнуть НТ абсолютно все фичи не представляется возможным. Это достаточно расточительное мероприятие, которое съедает самый ценный ресурс — время.
Как быть то? Для каждой новой выводимой фичи мы хотим понять, как она будет работать на проде. Вместо того, чтобы рисковать и выкатывать её всем пользователям сразу, ты просто в коде оборачиваешь её в feature toggle — своего рода переключатель. Настройка этого переключателя позволяет сопровождению включать или отключать определённые функции в приложении без необходимости изменять код или деплоить новые версии.
Теперь можно включить эту фичу только для небольшой группы пользователей или только для себя, чтобы протестировать её в боевых условиях. Если всё ок, можно постепенно расширять к ней доступ. Если что-то пойдёт не так (рост числа ошибок в логах или обращений пользователей) — просто выключаешь переключатель. Это делает процесс выпуска новых функций гораздо гибче и безопаснее.
Однако кроме очевидных проблем, которые отображаются в логах в виде ошибочных сообщений, я и мои разработчики периодически сталкиваемся с менее очевидными. Проанализировав их, я составил чек-лист, который закрывает большинство из них. Этот чек-лист сильно зависит от специфики проекта и выстроенных процессов, и в вашем случае может значительно отличаться:
1️⃣ Проверить, что при обновлении библиотек у сервиса не оторвало логи, метрики, трейсинг и не начала течь память
2️⃣ Проверить, что новые запросы к БД используют созданные индексы
3️⃣ Проанализировать утилизацию ресурсов (самого сервиса и интеграций) и при необходимости скорректировать лимиты HPA (HorizontalPodAutoscaler в k8s), istio
4️⃣Проверить, что распределенный кэш работает корректно
Здесь нужно помнить сразу о нескольких моментах:
1) в кэш в принципе должны попадать нужные нам данные, а затем из него правильно десериализоваться
2) должно быть учтено состояние гонки, которое может возникнуть в момент "одновременной" записи значений несколькими потоками/приложениями по одному и тому же ключу -> должна быть синхронизация, чтобы к бэкенду ушел всего один запрос. То же актуально и при протухании популярного ключа
3) утилизация ресурсов кластера кэша не должна быть слишком высокой, чтобы не потерять в эффективности кэширования
4) если количество ключей для определенной мапы ограничено, то стоит рассмотреть возможность дополнить распределенный кэш in-memory составляющей на уровне сервиса, чтобы не гонять данные по сети
Часть из этого списка уже внедрена или автоматизирована, с другой частью нам еще предстоит проработать решения на всех этапах: от аналитики до сопровождения в проде.
P.S. Хорошим дополнением/альтернативой механизма feature toggle может служить Canary Deployment, который позволяет делать примерно то же за счет задания критериев раскатки фичи на пользователей. Инструмент, однако, обладает меньшим уровнем детализации, так как не внедрен в кодовую базу раскатываемого сервиса и часто ограничен возможностями конкретной реализации.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4🤔1
О volatile замолвите слово
Как в Java разные потоки видят обновления не-volatile полей, если в коде не используются никакие средства синхронизации?
Сразу вспоминается классическая задача Java-собеседований с инкрементированием шареного счетчика:
Возникает встречный вопрос: а почему разные потоки могут не видеть эти обновления? Потоки могут работать на разных ядрах процессора, а у каждого ядра — свой кэш. Таким образом в системе может быть несколько копий одних и тех же данных, которые живут своей жизнью. Рано или поздно данные из кэша, которые больше процессору не нужны, будут сброшены в оперативную память. В итоге мы становимся зависимыми от порядка сброса кэшей.
Для разделяемых полей нам бы всегда хотелось видеть "актуальные данные" и не зависеть от того, как работает процессор⛔️ . Тогда в игру вступает ключевое слово volatile (хотя мы, конечно, могли бы взять и другие механизмы синхронизации). Теперь данные из кэша при изменениях будут принудительно сбрасываться в оперативную память, чтобы все потоки могли работать с самыми актуальными данными. Это то, что мы хотели получить — свойство visibility.
Вроде разобрались. Но в таком случае возникает резонный вопрос: как соотносятся в Java свойство visibility volatile-переменных и когерентность кэшей CPU (есть во всех современных процессорах)? Иными словами, зачем нам нужна инструкция на уровне языка, если есть более низкоуровневый (и быстрый) механизм синхронизации, который должен сделать грязную работу за нас?
Конечно, не будем забывать о том, что volatile не только про visibility (о котором сказали выше), он еще и про reordering и atomicity (reading / writing), но сейчас не об этом.
Где бы нам хотелось получить ответ на наш вопрос? К сожалению, JSR-133 Java Memory Model and Thread Specification не дает нам ответов, чего, однако, стоило ожидать: в спецификациях такие особенности реализации обычно не рассматриваются. тем не менее к прочтению горячо рекомендую😘 .
Разобраться в этом вопросе мне помогла статья. Действительно, кроме кэшей в процессоре есть еще и регистры. Они предназначены для временного хранения данных и инструкций, которые в данный момент активно используются процессором. Если вычисления над одними и теми же данными производятся интенсивно, то данные в регистрах могут оставаться достаточно долго: процессор старается удерживать часто используемые данные в регистрах для повышения эффективности выполнения инструкций. Именно поэтому мы не можем полагаться только на когерентность кэшей, так как к регистрам они не имеют никакого отношения, а следовательно никак не может повлиять на согласованность данных в них. Поэтому мы и ставим "барьеры памяти", как подсказки для CPU.
Еще один взгляд на volatile-переменные в целом и проблему когерентности кэшей в частности можно посмотреть здесь.
Как в Java разные потоки видят обновления не-volatile полей, если в коде не используются никакие средства синхронизации?
Сразу вспоминается классическая задача Java-собеседований с инкрементированием шареного счетчика:
class SharedExample {
public int sharedCounter = 0;
}Возникает встречный вопрос: а почему разные потоки могут не видеть эти обновления? Потоки могут работать на разных ядрах процессора, а у каждого ядра — свой кэш. Таким образом в системе может быть несколько копий одних и тех же данных, которые живут своей жизнью. Рано или поздно данные из кэша, которые больше процессору не нужны, будут сброшены в оперативную память. В итоге мы становимся зависимыми от порядка сброса кэшей.
Для разделяемых полей нам бы всегда хотелось видеть "актуальные данные" и не зависеть от того, как работает процессор
Вроде разобрались. Но в таком случае возникает резонный вопрос: как соотносятся в Java свойство visibility volatile-переменных и когерентность кэшей CPU (есть во всех современных процессорах)? Иными словами, зачем нам нужна инструкция на уровне языка, если есть более низкоуровневый (и быстрый) механизм синхронизации, который должен сделать грязную работу за нас?
Для справки: когерентность памяти (англ. memory coherence) — свойство компьютерных систем, содержащих более одного процессора или ядра, имеющих доступ к одной области памяти, заключающееся в том, что изменённая одним ядром/процессором ячейка памяти принимает новое значение для остальных ядер/процессоров.
Где бы нам хотелось получить ответ на наш вопрос? К сожалению, JSR-133 Java Memory Model and Thread Specification не дает нам ответов, чего, однако, стоило ожидать: в спецификациях такие особенности реализации обычно не рассматриваются. тем не менее к прочтению горячо рекомендую
Разобраться в этом вопросе мне помогла статья. Действительно, кроме кэшей в процессоре есть еще и регистры. Они предназначены для временного хранения данных и инструкций, которые в данный момент активно используются процессором. Если вычисления над одними и теми же данными производятся интенсивно, то данные в регистрах могут оставаться достаточно долго: процессор старается удерживать часто используемые данные в регистрах для повышения эффективности выполнения инструкций. Именно поэтому мы не можем полагаться только на когерентность кэшей, так как к регистрам они не имеют никакого отношения, а следовательно никак не может повлиять на согласованность данных в них. Поэтому мы и ставим "барьеры памяти", как подсказки для CPU.
Еще один взгляд на volatile-переменные в целом и проблему когерентности кэшей в частности можно посмотреть здесь.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3🍓1
Раскрываю 🔝 Главный секрет успеха в IT 🔝
За всю жизнь мне пришлось общаться только с тремя гениями. Наверное, их было больше, но этих троих я видел в деле своими глазами на длинной дистанции.
Двое из них учились со мной в средней школе. Дима и Саша щелкали математику и физику, как орешки, одерживая победы на олимпиадах. Но если Саша приправил свою гениальность трудолюбием, то Дима относился ко всему несерьезно. Результат был вполне предсказуем.Саша стал топ-менеджером в одном из крупнейших банков страны, еще будучи PO , запустив несколько уникальных для нашей страны продуктов в области инвестиций. Дима же не смог закончил университет, вылетев с первого курса.
Позднее, уже в университете, на моей специальности в соседней группе учился настоящий гений мира IT. Азат налету схватывал любой материал, будь то математика, схемотехника или создание Spring-приложений. Он пахал, как проклятый, и достиг значительных успехов в области CS, выступая с докладами на международных конференциях🤩 .
Все мои друзья и товарищи со школы и университета, которые в разряд гениев не попали, но знают тот самый главный секрет успеха, сейчас строят крутую карьеру и хорошо зарабатывают (и не только в IT).
К слову, вы же помните, что я менторством промышляю? Так вот:
И не потому, что задачи невыполнимые, а потому что надо что-то начинать делать, напрягаться⛔️ . Что уж говорить о долгосрочных перспективах и планах.
Кажется, что к этому моменту все стало понятно. Тем не менее, что это за секрет, который объединяет мое успешное окружение📈 ?
Все эти люди долгие месяцы и годы работают над совершенствованием своих навыков. Независимо от исходных природных данных, каждый из них прикладывает сверхусилия и добивается отличных результатов. Они берутся за сложные задачи, преодолевают себя и выходят на новый виток карьеры и зарплаты. Повторюсь, делают это долгие месяцы и годы.
Рассматривайте свое упорство, как одно из сильнейших долгосрочных преимуществ. Продолжайте инвестировать время в саморазвитие.
На моей памяти еще ни разу не было такого, чтобы такие ребята, не достигали успеха .
За всю жизнь мне пришлось общаться только с тремя гениями. Наверное, их было больше, но этих троих я видел в деле своими глазами на длинной дистанции.
Двое из них учились со мной в средней школе. Дима и Саша щелкали математику и физику, как орешки, одерживая победы на олимпиадах. Но если Саша приправил свою гениальность трудолюбием, то Дима относился ко всему несерьезно. Результат был вполне предсказуем.
Позднее, уже в университете, на моей специальности в соседней группе учился настоящий гений мира IT. Азат налету схватывал любой материал, будь то математика, схемотехника или создание Spring-приложений. Он пахал, как проклятый, и достиг значительных успехов в области CS, выступая с докладами на международных конференциях
Все мои друзья и товарищи со школы и университета, которые в разряд гениев не попали, но знают тот самый главный секрет успеха, сейчас строят крутую карьеру и хорошо зарабатывают (и не только в IT).
К слову, вы же помните, что я менторством промышляю? Так вот:
80% (а то и больше) приходящих ко мне менти (англ. mentee – ученик), не могут справиться с соблюдением разработанного плана даже на протяжении одного спринта.
И не потому, что задачи невыполнимые, а потому что надо что-то начинать делать, напрягаться
Кажется, что к этому моменту все стало понятно. Тем не менее, что это за секрет, который объединяет мое успешное окружение
Все эти люди долгие месяцы и годы работают над совершенствованием своих навыков. Независимо от исходных природных данных, каждый из них прикладывает сверхусилия и добивается отличных результатов. Они берутся за сложные задачи, преодолевают себя и выходят на новый виток карьеры и зарплаты. Повторюсь, делают это долгие месяцы и годы.
Рассматривайте свое упорство, как одно из сильнейших долгосрочных преимуществ. Продолжайте инвестировать время в саморазвитие.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥6🏆3👍2
Составил шпаргалку ⬆️ , как не терять данные при работе с Kafka . Подойдет тем, кто еще на Zookeeper задержался, и тем, кто уже пересел на KRaft 🔝 .
В первую очередь думаем о конфигурации самого кластера (таблица 1). Следующим этапом может быть переход к 2 (лучше 3) дата-центрам. В этом случае мы должны выбрать между Stretched- и асинхронным кластером.
Однако помним, что исключительно горизонтального масштабирования недостаточно для отказоустойчивости без оглядки на данные: необходима их репликация👌 . Установив replication.factor, например в 2, мы получим еще одну копию для каждой партиции, которая будет находиться на другом брокере. Kafka производит распределение партиций по брокерам из разных стоек благодаря прописанному (вручную) у каждого брокера идентификатору стойки broker.rack.
В процессе репликации данные на партициях-фолловерах могут отставать от данных на партиции-лидере. Это может привести к неконсистентному состоянию данных, если брокер с лидер-партицией откажет⚰️ . Если это недопустимо, то реплики можно сделать синхронными. Выставляем настройки acks=-1 и enable.idempotence=true на продюсере!, не на брокере, а то многие путают. Консьюмеры не смогут прочитать данные, пока те не были успешно реплицированы на фолловерах, т.к. last commit (high watermarked) offset будет указывать на последнее синхронизированное сообщение. Хороший default для сохранения консистентности данных выглядит так:
Также не вижу повода не следовать рекомендациям разработчиков Kafka:
Чем больше число партиций в кластере, тем более долгое восстановление при падении мы получим🛠 .
Существует еще ряд продвинутых настроек, повышающих отказоустойчивость кластера. 😈 . При активированной настройке auto.leader.rebalance.enable (по умолчание true) каждые leader.imbalance.check.interval.seconds секунд, если количество не-preferred лидеров-партиций превышает leader.imbalance.per.broker.percentage, то происходит ребалансировка. Настройки flush.messages и flush.ms (чрезвычайно большой по умолчанию) позволяют выполнять принудительную запись на диск из dirty buffer, что может уберечь от отказа всех синхронных реплик (однако, вероятность этого крайне мала). Настройки max.connections, max.connection.creation.rate и max.connections.per.ip позволяют ограничить число клиентских коннектов к брокерам.
В первую очередь думаем о конфигурации самого кластера (таблица 1). Следующим этапом может быть переход к 2 (лучше 3) дата-центрам. В этом случае мы должны выбрать между Stretched- и асинхронным кластером.
Однако помним, что исключительно горизонтального масштабирования недостаточно для отказоустойчивости без оглядки на данные: необходима их репликация
В процессе репликации данные на партициях-фолловерах могут отставать от данных на партиции-лидере. Это может привести к неконсистентному состоянию данных, если брокер с лидер-партицией откажет
acks=all
replication.factor=3
min.insync.replicas=2
enable.idempotence=true
Также не вижу повода не следовать рекомендациям разработчиков Kafka:
не создавать более 4k партиций на одном брокере
не создавать более 200k партиций в кластере
Чем больше число партиций в кластере, тем более долгое восстановление при падении мы получим
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
Тихий убийца вашей продуктивности
Но сегодня хотел рассказать не об этом. Вы чувствовали в помещении духоту?Не путать просто с высокой температурой. Особенно она заметна на контрасте с улицей. Обычно виновником является CO2. Углекислый газ не имеет ни цвета ни запаха, поэтому повышение его концентрации в помещении возможно отследить только по своим ощущениям (когда она будет уже высока) или с помощью специальных датчиков.
Для того, чтобы понимать качество воздуха в помещении, в котором работаю (дома на удаленке😏 ), лет 5 назад я приобрел монитор качества воздуха за 8-10 т.р. Можете посмотреть подробный обзор на него здесь . Мне аппарат служит верой и правдой все это время. Кстати, уже вышла вторая версия, которую хвалят еще больше. И обзор на нее уже есть. Сейчас их великое множество и они отлично работают (даже совсем бюджетные), так что можно брать любой. Только обращайте внимание на интеграцию с умными домами и возможность настроить автоматизацию по типу открывания окна или включения кондиционера, если она вам нужна.
Итак, что же он измеряет?
1️⃣ концентрацию CO2 в ppm (parts per million)
2️⃣ концентрацию частиц PM2.5 (меньше 2,5 мкм). Они могут проходить через наши защитные барьеры, поэтому так опасны. Вы перестанете встряхивать постельное белье, увидев, как растет этот показатель
3️⃣ tVOC (летучие органические соединения), которые могут испускать, например, фанера и мебель, краски. Если уровень в вашем помещении стабильно высокий, то вам стоит поискать его причину
4️⃣ ну и, конечно, температуру и влажность
Какие уровни ppm для CO2 приемлемы?
< 800 ppm наслаждаемся жизнью🔝
800 ppm — 2000 ppm появляется усталость, потеря сосредоточенности, неприятное ощущение духоты
2000 ppm — 5000 ppm головная боль, сонливость, хочется покинуть помещение
> 5000 ppm бегите, глупцы!⚰️
Ок, концентрацию CO2 (и других частиц) в воздухе измерили. И что дальше? Лучший способ снизить уровень CO2 в помещении — это разбавлять его воздухом с улицы. Вентиляция помещения — это ключ к успеху.
Летом, если воздух снаружи чистый, то можно просто открыть окно. Если воздух грязный, то его нужно фильтровать (умеют, например, бризеры). Зимой воздух с улицы надо еще и подогревать (те же бризеры), либо подавать в малых объемах (клапаны на пластиковые окна с фильтрами). В идеале каждое помещение в доме/квартире должно быть подключено к HVAC-системе (Heating, Ventilation, and Air Conditioning), которую, вы, вероятно, видели в отелях.
Еще хочу отметить 2 факта!
Дышите полной грудью!
Перестань смотреть TikTok, рилсы и шортсы! Да, да, это чистая правда!🤔
Но сегодня хотел рассказать не об этом. Вы чувствовали в помещении духоту?
Для того, чтобы понимать качество воздуха в помещении, в котором работаю (дома на удаленке
Итак, что же он измеряет?
Какие уровни ppm для CO2 приемлемы?
< 800 ppm наслаждаемся жизнью
800 ppm — 2000 ppm появляется усталость, потеря сосредоточенности, неприятное ощущение духоты
2000 ppm — 5000 ppm головная боль, сонливость, хочется покинуть помещение
> 5000 ppm бегите, глупцы!
Ок, концентрацию CO2 (и других частиц) в воздухе измерили. И что дальше? Лучший способ снизить уровень CO2 в помещении — это разбавлять его воздухом с улицы. Вентиляция помещения — это ключ к успеху.
Летом, если воздух снаружи чистый, то можно просто открыть окно. Если воздух грязный, то его нужно фильтровать (умеют, например, бризеры). Зимой воздух с улицы надо еще и подогревать (те же бризеры), либо подавать в малых объемах (клапаны на пластиковые окна с фильтрами). В идеале каждое помещение в доме/квартире должно быть подключено к HVAC-системе (Heating, Ventilation, and Air Conditioning), которую, вы, вероятно, видели в отелях.
Еще хочу отметить 2 факта!
Подавляющее большинство кондиционеров не освежает воздух, а только охлаждает его, так как воздух из помещения ходит по замкнутому контуру до внешнего блока и обратно.
Домашние воздухоочистители не поглощают углекислый газ, а лишь отфильтровывают загрязняющие частицы.
Дышите полной грудью!
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6🤯1