Я написал это как внутренний документ WeWork и неоднократно ссылался на него, когда помогаю командам принимать решения по этому поводу.
Архитектура микросервисов дает большие преимущества. Он обеспечивает основу для слабосвязанной архитектуры, которая позволяет командам работать независимо и независимо друг от друга. Это также позволяет нам иметь большую гибкость в работе. Некоторые службы могут значительно масштабироваться, в то время как для других вам нужно всего несколько экземпляров. Некоторым службам требуется значительный объем памяти, а другие могут работать очень экономно.
Однако после того, как ваши системы разделены на независимые службы, возникает новый набор проблем, которые необходимо решить.
Одна из них - проблема распределенного запроса. Когда у вас есть все данные в единой монолитной базе данных, вы можете использовать мощь реляционного запроса для сбора информации по доменам. Но как только этот монолит будет разделен на микросервисы, как вы сможете удовлетворить потребность в этих запросах, охватывающих данные, принадлежащие более чем одной службе?
Есть несколько возможных подходов, которые я рассмотрю здесь.
Tl;dr
Вот краткое изложение руководства, которое поможет вам понять, какой подход использовать:
- Используйте параллельные запросы к нисходящим службам и объединяйте их в памяти, если логика объединения не слишком сложна, вы можете удовлетворить свои требования к задержке и если вы не имеете дело с сверхбольшими наборами данных. Реактивные библиотеки и сервисы GraphQL, такие как Apollo, действительно могут помочь в этом. Используйте неблокирующий ввод-вывод, если ваш фреймворк и язык его поддерживают.
- Если все вышеперечисленные условия применимы, но параллельные запросы невозможны, используйте сериализованные запросы.
- Если ваши требования к задержке значительны или ваши наборы данных слишком велики, сохраните локальную копию. Обратите внимание, что переключение на локальную копию создает совершенно новый набор проблем и сложностей (которые я описываю в конце этой статьи), поэтому не переходите к этому шагу, если в этом нет необходимости. Например, посмотрите, можете ли вы улучшить производительность систем, которые вы вызываете, или уменьшить объемы данных, которые вы потребляете в запросе.
- Если вам нужно использовать локальное хранилище, а не писать собственную логику запросов, используйте механизм хранения, который поддерживает расширенные запросы, например реляционную базу данных или Elasticsearch.
- Если вы используете локальную копию данных, принадлежащих другой службе, тщательно подумайте о правильности, порядке, идемпотентности, актуальности, сценариях ошибок и способах обнаружения и определения причины ошибок. Убедитесь, что есть способ обнаружить и восстановить, когда что-то выходит из синхронизации, и протестируйте эти сценарии.
Сериализованные множественные запросы
При таком подходе основная служба запрашивает свое локальное хранилище и последовательно вызывает каждую последующую службу для сбора необходимых данных, а затем объединяет, сортирует, фильтрует и агрегирует результаты по мере необходимости. Механизм GraphQL, такой как Apollo, становится очень распространенным способом реализации такого рода решений.
Преимущества
- Довольно просто писать и отлаживать
- Данные из других сервисов актуальны, тогда как с решением для кеширования ваши данные могут быть устаревшими.
Недостатки
- Комбинированная задержка вызова каждой службы может сделать общий запрос слишком медленным.
- Увеличивает нагрузку запросов на последующие сервисы
- По сути, вы создаете важные компоненты системы управления базами данных (слияние / сортировка / фильтр / агрегирование / группировка / и т. Д.) В своем сервисе.
- Часто вам придется загрузить полный набор результатов в память, прежде чем вы сможете применять такие операции, как сортировка, фильтрация, агрегирование. Это влияет как на использование памяти, так и на задержку, и может вызвать непредвиденные проблемы, если размер данных быстро растет.
- Прямые вызовы служб увеличивают взаимосвязь и создают точки отказа. Когда есть сбои, такие как медленная или сломанная сеть, или если одна из нижестоящих служб работает медленно, это не только увеличивает вероятность сбоя запроса, но может вызвать каскадный сбой всех служб, участвующих в пути запроса, как ресурсы заблокированы по всей цепочке.
Когда использовать это
- Вы можете удовлетворить требования к задержке с помощью сериализованных вызовов
- Последующие сервисы надежны и могут безопасно справляться с возросшей нагрузкой.
- Размер набора результатов не имеет значения
- Операции слияния / сортировки / фильтрации / агрегирования не очень сложны.
Параллельные множественные запросы
При таком подходе мы пытаемся решить проблему задержки, выполняя запросы параллельно. Парадигма программирования ReactiveX позволяет довольно легко сделать это, моделируя его как Observer, к которому вы можете применить ряд функций.
Преимущества
- Повышена производительность, поскольку запросы не сериализуются
- Вам гарантируется, что данные из других сервисов актуальны.
- Если задействованные компоненты поддерживают это, модель ReactiveX поддерживает модель неблокирующего ввода-вывода. Это означает, что такие ресурсы, как потоки, не блокируются в ожидании ответа, если нисходящая служба работает медленно. Это предотвращает каскадный сбой цепочки запросов.
Недостатки
- Отладка сложнее, потому что все происходит асинхронно, поэтому отслеживание потока запроса может быть сложной задачей.
- По сути, вы создаете важные компоненты системы управления базами данных (слияние / сортировка / фильтр / агрегирование / группировка / и т. Д.) В своем сервисе.
- Как правило, вы не можете правильно обработать набор результатов, пока не получите все данные, что означает, что вам придется загружать полный набор результатов в память, а не обеспечивать разбиение на страницы, что увеличивает риск нехватки памяти.
- Увеличивает нагрузку запросов на последующие сервисы
Когда использовать это
- Ваши требования к задержке не могут быть выполнены с помощью синхронной модели
- Размер набора результатов не имеет значения
- Операции слияния / сортировки / фильтрации / агрегирования не очень сложны.
- Последующие сервисы надежны и могут безопасно обрабатывать увеличенную нагрузку на запросы.
Запрос и магазин
В этой модели мы получаем значительный выигрыш, копируя все необходимые нам зависимые данные из других сервисов в таблицы в нашей основной базе данных, а затем выполняя запрос в базе данных.
Вы можете сделать это, используя синхронную или асинхронную модель запросов.
Если возможно, воспользуйтесь преимуществами поддержки временных таблиц в реляционной базе данных (например, см. Это руководство по MySQL). Эти таблицы существуют только в течение всего времени существования транзакции, но вы все равно можете выполнять с ними полные операции SQL. Без них у вас будут всевозможные накладные расходы на управление своими таблицами и предотвращение конфликтов между транзакциями, пытающимися использовать одну и ту же таблицу.
Преимущества
- Может использовать систему баз данных по назначению, а не строить логику СУБД в сервисе.
- Может разбивать данные на страницы вместо того, чтобы загружать их все в память перед возвратом каких-либо результатов
- Довольно легко понять и отладить
Недостатки
- Создает высокую нагрузку на запись в базе данных, хотя обычно временные таблицы хранятся в памяти.
- Увеличивает требования к памяти для вашей базы данных
- Повышенная задержка из-за дополнительных обращений к базе данных
- Прямые вызовы других сервисов увеличивают взаимосвязь и создают точки отказа
- Добавляет нагрузку запросов на последующие сервисы
Когда использовать это
- Вы можете справиться с влиянием задержки при запросе зависимой службы для каждого запроса.
- Размер набора результатов значительный и / или слияние / сортировка / фильтр / агрегирование являются сложными
- Последующие сервисы могут безопасно обрабатывать увеличенную нагрузку запросов
- Ваша база данных может справиться с возросшей нагрузкой на запись и требованиями к памяти. Обычно это означает, что у вас нет сверхвысокой нагрузки на запросы.
Чтение из распределенного кеша
Здесь мы решаем проблему задержки, сохраняя локальную копию результатов в распределенном кеше, таком как Redis или memcached. Вы потенциально можете поддерживать кеш в каждом процессе обслуживания, но это действительно может занять вашу память в зависимости от размеров данных, с которыми вы работаете.
Ключ, используемый для хранения данных в кэше, может быть каким-то уникальным идентификатором, который использовался для получения данных от нижестоящей службы. Или вы можете кэшировать конечные результаты запроса, если это возможно.
Важным аспектом этого подхода является то, как вы заполняете кеш и как поддерживаете его в актуальном состоянии. Я буду обсуждать подходы в следующем разделе, так как это очень важная проблема, которую необходимо решить, и есть несколько возможных решений. А пока предположим, что эта проблема решена.
С помощью этого решения вы хотите быть очень избирательными и вдумчивыми в отношении того, сколько вы храните на месте. Если вы храните все данные, принадлежащие нижестоящим службам, в распределенном кеше, вы, по сути, снова получаете монолит.
Очень распространенный шаблон с запросами состоит в том, что у вас есть сводный запрос, который получает широкий диапазон сопоставимых результатов сопоставления со сводными данными, а затем есть подробный запрос, который получает сведения об одном результате. Получив этот подробный запрос, вы можете напрямую вызвать службу-владелец, чтобы получить необходимые подробные данные, а не кэшировать их. В качестве альтернативы ваш клиент может использовать идентификатор, который вы возвращаете в результатах поиска, для вызова службы-владельца для получения подробной информации.
Преимущества
- Уменьшает задержку
- Не создает дополнительной нагрузки на вашу базу данных
- Снижает нагрузку на последующие сервисы
- Уменьшает связь - если нисходящая служба работает медленно или не работает, во многих случаях вы все равно можете работать из-за кеша
Недостатки
- Дополнительная сложность вашей системы
- Необходимо поддерживать кеш в свежем виде
- Ваш микросервис отвечает за такие функции базы данных, как слияние / сортировка / фильтрация.
- Кэш может предъявлять значительные требования к памяти
Когда использовать это
- У вас есть строгие требования к задержке, которые не могут быть выполнены, если вы вызываете нижестоящие службы по каждому запросу.
- Вы хотите уменьшить взаимосвязь в своей системе
- Ваш вариант использования может обрабатывать результаты, которые не являются полностью актуальными на 100%.
- Вы можете справиться с дополнительными требованиями к памяти
Сохраните локальную копию в своей базе данных
Этот подход по сути такой же, как и предыдущий, но вместо того, чтобы помещать данные в неструктурированный кеш на основе памяти, вы сохраняете их в своей базе данных.
Этот подход также очень похож на подход с запросом и сохранением, описанный выше, за исключением того, что вы не сохраняете результаты только на время существования запроса во временной таблице. Вместо этого данные постоянно хранятся в реальных таблицах и обычно содержат данные, необходимые для обслуживания многопользовательских запросов, а не только текущего запроса.
Это действительно полезно, потому что вы можете обрабатывать данные так же, как если бы это были ваши собственные данные; он просто получен из другой службы. Вы можете в полной мере использовать возможности запросов к локальной базе данных и выполнять такие операции, как сортировка, объединение, агрегирование и фильтрация в базе данных, прежде чем какие-либо данные будут доставлены в вашу службу. Это также значительно снижает взаимосвязь с подчиненными сервисами, потому что даже если эти сервисы не работают, вы все равно можете работать, хотя и с данными, которые со временем устаревают.
Преимущества
- Уменьшает задержку
- Значительно снижает нагрузку на последующие сервисы
- Значительно снижает взаимосвязь и повышает устойчивость - если нисходящая служба работает медленно или не работает, вы все равно можете работать, потому что у вас есть локальная копия данных.
- Ваш микросервис не должен создавать такие функции управления базой данных, как сортировка, фильтрация, слияние и агрегирование.
Недостатки
- Вы должны быть очень осторожны в том, как вы управляете согласованностью между сервисом-владельцем и вашим сервисом (подробнее см. Ниже).
- Увеличение требований к хранилищу и памяти в вашей локальной базе данных
- Повышенная нагрузка на вашу локальную базу данных
Когда использовать это
- У вас есть строгие требования к задержке, которые не могут быть выполнены с помощью синхронного вызова.
- Вы хотите уменьшить взаимосвязь в своей системе
- Ваш вариант использования может обрабатывать результаты, которые не являются полностью актуальными на 100%.
- Вы можете справиться с дополнительными требованиями к памяти и хранилищу
Согласованность кеша
Они говорят, что в информатике есть четыре большие проблемы: присвоение имен объектам, поддержание согласованности кеша и единичные ошибки.
Сохранение локальной копии распределенных данных, необходимых для поддержки ваших запросов, - очень эффективный подход. Это может значительно уменьшить задержку. Это также значительно снижает взаимосвязь всей вашей системы, делая вашу систему более устойчивой и надежной, а также повышает независимость ваших групп разработчиков. Кроме того, если вы храните копию в своей базе данных, а не в кеше, это означает, что ваша служба не должна выполнять ту работу, которую ваша база данных может делать за вас.
Обратите внимание, что вам нужен очень сильный контракт данных, чтобы получить преимущества слабой связи. Если производитель вносит несовместимые изменения в схему данных или семантику данных, ваше потребление данных может нарушиться. Общая рекомендация - использовать надежный механизм контракта схемы, такой как Avro или json-schema. Avro обычно предпочтительнее, потому что он поставляется со встроенными проверками совместимости, но json-schema имеет более обширную проверку схемы и может вызывать очень полезные ошибки проверки.
Сохранение согласованности двух копий данных - настоящая проблема. У этой проблемы есть два основных аспекта: актуальность данных и правильная обработка сценариев ошибок, чтобы данные не были потеряны или повреждены.
Есть три основных способа управления кешем: отложенная загрузка, пакетная загрузка и потоковая передача.
Ленивая загрузка
Такой подход может быть хорош, когда
- У вас высокий процент попаданий в кеш
- Вы можете быть уверены, что последующие сервисы будут быстрыми и надежными.
- Свежесть данных не является серьезной проблемой
При таком подходе вы начинаете с чистого листа, а затем при каждом запросе напрямую запрашиваете нижестоящие службы и сохраняете результаты в локальном кэше или базе данных. Это избавляет от необходимости создавать конвейер данных для заполнения вашей локальной копии.
Обратите внимание, что этот подход не работает, если у ваших шаблонов запросов очень низкая частота попаданий в кеш. Это может произойти, если, например, вы поддерживаете поиск по всему миру. Распространенным решением для этого является разделение кеша на основе географического региона, чтобы запрос на определенную область всегда направлялся в один и тот же раздел кеша.
Еще одна проблема, связанная с этим подходом, заключается в том, что вы не полностью устранили зависимость времени запроса от нисходящей службы, что увеличивает общую связь вашей системы с сопутствующими результатами, от тестирования до общей устойчивости системы.
При ленивой загрузке у вас должен быть способ обновлять кэшированные данные без регулярного обновления конвейера данных.
Один из подходов - это наименее недавно использованный (LRU), где вы устанавливаете максимальный размер кеша, а когда у вас заканчивается место, вы удаляете самую старую запись. Это обычно полезно, когда у вас жесткие ограничения памяти и вам нужно решить, какие записи нужно удалить, сохраняя при этом хорошую частоту попаданий в кеш.
Более распространенный подход, когда вы беспокоитесь о свежести больше, чем о пространстве, - это время жизни (TTL). В этом подходе вы назначаете максимальное время, в течение которого запись в кэше может считаться действительной. По заданному запросу, если запись в кэше, но слишком старая, она удаляется, и запрос отправляется службе-владельцу для получения свежих данных и обновления записи в кеше. Обратите внимание, что чем короче TTL, тем хуже качество вашего сервиса до стандартного решения с прямым запросом.
Обработка ошибок
При отложенной загрузке основная ошибка, которую вы должны обработать, заключается в том, что нисходящая служба не отвечает или недоступна. В этой модели вы действительно ничего не можете сделать, потому что вы обращаетесь к сервису потому, что у вас нет локальной копии. Если у вас есть данные, которые прошли TTL, вы можете вернуться к нему, если это приемлемо.
Очень важная вещь, которую вы должны сделать, если вы напрямую вызываете другую службу, - это защитить себя и остальную систему от каскадного сбоя из-за исчерпания потоков запросов или других ресурсов, потому что все они заблокированы в ожидании результатов.
Тайм-аут - это хорошо, но, как правило, его недостаточно. Лучше всего использовать автоматический выключатель. Если вы работаете в управляемой среде, такой как Kubernetes, которая поддерживает сопроводительную машину, такую как Envoy, ваша коляска может реализовать автоматический выключатель за вас. В противном случае вам нужно получить хорошую библиотеку автоматических выключателей для вашего языка и реализовать этот шаблон самостоятельно.
Пакетная загрузка
При пакетной загрузке у вас есть задание, которое выполняется на регулярной основе, потребляет все данные, которые изменились с момента последней загрузки, и заполняет вашу базу данных результатами. Это звучит просто, и многие люди используют этот подход, особенно для поддержки хранилищ данных, но на самом деле он создает проблемы, и я обычно стараюсь отвести людей от этого подхода.
Один подход, который я видел, - это задание, которое напрямую запрашивает базу данных другой службы, получает измененные строки и затем загружает их в вашу базу данных. Проблема в том, что теперь у вас есть зависимость от частного интерфейса (схемы базы данных) другой службы. Я могу гарантировать вам, что вы будете постоянно удивляться тонким и не очень тонким нарушениям предполагаемого, но не согласованного контракта, который представляет собой эта зависимость.
Другой подход - вызвать API в службе-владельце, которая возвращает пакет результатов на основе того, что изменилось с момента последнего вызова. Это может быть очень проблематичным, поскольку создает большую нагрузку на службу и ее базовую базу данных и может быть весьма подвержено ошибкам. Обычно HTTP-запросы не предназначены и не настроены для возврата больших наборов результатов.
Обычно я рекомендую избегать пакетной загрузки из-за этих серьезных проблем. Одна из ситуаций, когда вам нужно использовать пакетную загрузку, - это если вы получаете данные от третьей стороны.
Репликация
Другой довольно популярный подход - использовать репликацию базы данных. Это, в частности, решение для групп данных, которые хотят поддерживать хранилище данных, которое является специализированной версией этой проблемы распределенных запросов.
Но при репликации базы данных некоторые существенные проблемы. Теперь вы не только привязаны к их схеме, но и ваша схема должна соответствовать их. Вы также связаны с требованием, чтобы обе базы данных были совместимы друг с другом (например, они обе должны быть MySQL). Если вы или другая команда решите, что вам нужно перейти на другое решение для хранения данных (например, ElasticSearch или Cassandra), вас ждет множество неприятностей. Я говорю из болезненного опыта.
Публикация / подписка с использованием журнала событий
Если вам нужно поддерживать локальную копию данных другой службы, я обычно рекомендую подход к публикации / подписке с использованием журнала событий, такого как Kafka. Это дает ряд преимуществ:
- У вас есть четко определенный контракт в форме определения схемы. Так же, как вы создаете контракт API, вы используете Avro или json-schema для создания контракта события. Этот контракт обеспечивает гарантию того, что потребители не сломаются по мере развития набора данных издателя, аналогично гарантиям обратной совместимости для API.
- Вы не возлагаете прямую неуправляемую нагрузку на сервисы-владельцы. Они контролируют, как и когда они делают данные доступными.
- Такой контракт разделяет потребителя и производителя. Если потребитель хочет перейти на другой механизм хранения или, возможно, на совершенно другую реализацию службы, он может сделать это, не нарушая работу своих потребителей. Кроме того, больше потребителей могут «запрыгнуть в поезд» без необходимости проделывать дополнительную работу производителю.
- Публикация в постоянном журнале вместо использования брокера сообщений в памяти позволяет повысить устойчивость к сбоям.
Однако за все эти преимущества приходится платить с точки зрения правильности и последовательности. Эти затраты часто не учитываются должным образом и могут создать реальные проблемы, которые трудно решить быстро.
Правильность и обработка ошибок
Если мы используем модель публикации / подписки, то необходимо рассмотреть ряд сценариев. Потеря или несогласованность данных обычно является серьезной проблемой.
Масштабируемость, актуальность, доступность и долговечность потока данных
Поток данных, содержащий все обновления, необходимые для обеспечения надежных гарантий доступности и долговечности. Мы не можем потерять данные, даже если кластер выйдет из строя, сервер выйдет из строя, сеть и так далее. Это должно быть правдой, даже если есть большой всплеск обновлений данных или если общая средняя пропускная способность обновлений со временем растет - конвейер должен быть устойчивым к этому, не теряя данные и не тратя часы на обработку. Обратите внимание, что во многих отношениях данные, получение которых занимает вечность, во многом совпадают с отсутствующими данными.
Kafka при правильной настройке может быть очень надежным и масштабируемым. В наши дни существует множество организаций, предоставляющих кластеры Kafka в качестве услуги. Я очень рекомендую этот подход, потратив слишком много часов на правильную настройку кластера. Это может показаться дорогое, но не стоит тратить деньги на копейки и нафиг.
Идемпотентность и семантика хотя бы один раз
Существуют сценарии, в которых производитель данных не знает, была ли запись успешной или нет (например, сеть зависает или отключается после отправки записи, но до получения подтверждения). В этом случае вам нужно повторить попытку, пока не добьетесь успеха.
Но когда вы это сделаете, возможно, данные уже были записаны один раз. Это называется семантикой хотя бы один раз. Когда у вас такой мир, ваш потребитель должен быть идемпотентным - он должен гарантировать, что если вы получите одну и ту же запись несколько раз, это будет все равно, что получить ее только один раз.
Заказ
Вам нужно либо не заботиться о порядке, в котором обрабатываются обновления, либо гарантировать, что все записи для одного и того же объекта обрабатываются в правильном порядке. С Kafka вы гарантированно упорядочиваете определенный раздел, поэтому, если вы используете ключ объекта в качестве ключа раздела, вы можете получить эту гарантию.
Производительность системы
Поскольку задание выполняется как пакетный, этот пакетный процесс может вызвать всплески нагрузки на вашу систему, а это может повлиять на производительность системы. Вам следует внимательно следить за задержкой и частотой ошибок и быть готовым завершить задание, если оно вызывает проблемы.
Если производительность является проблемой, вы можете чаще выполнять задание, чтобы разница каждый раз была меньше. В результате он работает быстрее и снижает нагрузку на систему.
Обработка ошибок
Что вы делаете, когда пытаетесь обработать данные и сталкиваетесь с ошибкой? Возможно, данные неверные, или у вас есть синтаксическая ошибка SQL. Часто эти ошибки возникают в пакетном режиме, и у вас есть большой поток сообщений, который вы не можете обработать.
В этих сценариях очень важно помнить, что вы не хотите бросать сообщение на пол при возникновении ошибки. Вы не хотите потерять данные.
Обычный подход состоит в том, чтобы записать неудачные сообщения в тему ошибки или «очередь недоставленных сообщений», зарегистрировать ошибку и создать предупреждение. Вам необходимо убедиться, что тема ошибки или очередь недоставленных сообщений способны обрабатывать большое количество входящих сообщений одновременно, в противном случае может закончиться место / память, и сообщения начнут отбрасываться.
Вы также должны подумать, как вы обрабатываете все эти сообщения, которые были отправлены в очередь ошибок, после того, как проблема будет решена. Кому-то может потребоваться вручную исправить их, или вы просто захотите воспроизвести их. Иногда вам нужно выборочно найти определенные сообщения, а затем исправить их определенным образом. Метод, который я видел, заключается в том, чтобы поместить все сообщения в индекс с возможностью поиска, такой как ElasticSearch. Затем специалист службы поддержки может работать с этими сообщениями и решать их. Kafka Connect предоставляет инструмент, который записывает данные непосредственно в ElasticSearch, поэтому вам не нужно писать какой-либо код, чтобы это произошло.
Проверка согласованности
У всех нас есть хорошие намерения, но все же что-то может пойти не так, и мы этого не замечаем. Настоятельно рекомендуется замести следы, регулярно выполняя производственный тест, который проверяет, что все данные исходной системы существуют в целевой системе, и выдает предупреждение при возникновении ошибки. Таким образом, если, например, в развертывании есть ошибка, мы обнаруживаем ее довольно быстро, не дожидаясь звонка рассерженного клиента.
Обычно это делается путем перебора всех идентификаторов в источнике и обеспечения того, чтобы эти идентификаторы существовали в целевом объекте. Вам не нужно запускать это большим пакетом, который захлопнет систему; он может все время работать с низкой скоростью в фоновом режиме.
Другой подход - просто предположить, что со временем что-то выйдет из строя, и регулярно проводить полное обновление всех данных. Желательно, чтобы вам не приходилось этого делать, поскольку это дорого и разрушительно. Но в некоторых случаях это лучший подход.
Заключение
Запросы, которым требуются данные, принадлежащие нескольким микросервисам, - это реальность в распределенной архитектуре микросервисов.
Есть много способов справиться с этим, и стоит тщательно подумать, какой из них лучше всего подходит для вашей ситуации.
Правильность и обработка ошибок являются ключевыми моментами, когда мы начинаем работать с распределенными данными. Вы должны тщательно продумать все сценарии и опираться на существующие шаблоны и лучшие практики, чтобы найти надежные, понятные и простые в обслуживании решения.