От монолита к микросервисам: ускорили банковские релизы в 15 раз

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

    Недавно мы писали о том, как работают IT-архитекторы, а теперь расскажем подробности об одном из наших кейсов и покажем схему работы системы. В этом проекте мы помогли заменить «коробочное» банковское приложение на микросервисное ДБО, при этом наладив быстрый выпуск релизов – в среднем 1 раз в неделю.



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

    Проблемы «коробочного» монолита


    Среди средних и малых компаний востребованы готовые IT-решения, которые можно кастомизировать и выпустить со своим логотипом. Минусы – функции приложений «из коробки» бывают ограничены, а обновления зачастую приходится вносить долго и исключительно через вендора.

    Именно такую «коробочную» систему дистанционного банковского обслуживания (ДБО) использовал один из наших клиентов. Онлайн-банк представлял собой монолитное приложение с достаточно небольшим набором функций.

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

    Таким образом, развивать продукт было сложно из-за ряда ограничений:

    1. Вендор вносил желаемые изменения неприемлемо долго, а переписать мобильный интерфейс в конкретной «коробке» и вовсе было практически невозможно.
    2. Из-за сбоев в шине данных или «коробке» пользователи зачастую не могли войти в онлайн-банк.
    3. Работать с приложением пользователи могли только в том случае, если у них была установлена самая свежая версия.
    4. Для распределения нагрузки инстансы монолита были развернуты на нескольких серверах, каждый из которых обслуживал свою группу абонентов. При падении одного из серверов пропадал доступ у всех абонентов соответствующей группы.
    5. Вся экспертиза хранилась у производителя коробочного решения, а не в банке.
    6. Из-за того, что банк не мог оперативно внести изменения по просьбам пользователей, а функциональность была недостаточной по сравнению с конкурентами, появился риск оттока клиентов.

    В результате банк принял решение – постепенно отказаться от «коробки» и разработать собственное ДБО, с микросервисной архитектурой, чтобы ускорить разработку функций и обеспечить удобство и безопасность для пользователей.

    С чего мы начинали


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

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

    Архитектура

    Вместе с владельцем продукта мы приняли решение использовать Spring Cloud, который предоставляет все необходимые функции для реализации микросервисной архитектуры: это и Service discovery – Eureka, Api Gateway – Zuul, Config server и многое другое. В качестве системы контейнеров для Docker-образов выбрали OpenShift, потому что инфраструктура банка была заточена под этот инструмент.

    Также мы проанализировали, какие особенности старой «коробки» могут осложнять работу пользователей. Один из основных недостатков заключался в том, что система синхронно работала через шину данных, и каждое действие пользователей вызывало обращение к шине. Из-за больших нагрузок часто происходил отказ шины, при этом переставало работать все приложение. Кроме того, как и во многих старых банковских продуктах, накопилось легаси – «наследство» в виде старого и тяжеловесного CORE АБС, переписывать которое было бы сложно и дорого.

    Мы предложили ряд улучшений:

    • Версионирование

    Ранее «коробка» поддерживала только одну версию, а в новом онлайн-банке мы предложили новую архитектуру, которая позволит поддерживать несколько различных версий одновременно и заменять их при необходимости.

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

    • Асинхронность

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

    Это решение помогло повысить стабильность работы приложения. Мобильное приложение теперь не зависит от шины, необходимые пользователю данные мы дублируем в наших сервисах и обновляем их в «фоне», когда есть доступ к банковской шине. Все действия пользователя ставятся в очередь на исполнение, по мере готовности приходят PUSH-уведомления о результатах.

    • Кэширование

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

    Что изменилось в системе


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





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



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

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

    Среди наиболее важных изменений можно отметить следующие:

    • Гибкость масштабирования

    Для увеличения отказоустойчивости системы сервисы распределены по разным серверам. Это позволяет сохранять систему в работоспособном состоянии в случае падения одного из них. Для своевременного реагирования на нестандартные ситуации мы внедрили систему мониторинга, это помогло при необходимости вовремя масштабировать сервисы. Подключили DevOps для настройки с нуля CI/CD на серверах клиента, выстроили процессы развертывания и поддержки будущего приложения на нескольких серверах.

    • Ускорение релизов за счет версионирования

    Ранее при обновлении мобильной версии одни пользователи уже применяли новую версию, а другие нет, но число последних было минимально. При разработке нового ДБО мы реализовали версионирование и возможность выпускать релизы без риска, что приложение перестанет работать у большой части клиентов. Сейчас при реализации отдельных функциональностей в новых версиях мы не ломаем старые версии, а значит, нет необходимости проводить регресс. Это помогло ускорить частоту релизов минимум в 15 раз – теперь релизы выходят в среднем 1 раз в неделю. Команды Backend и Mobile могут одновременно и независимо работать над новыми функциональностями.

    Подводя итоги


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

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

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

    Спасибо за внимание!
    SimbirSoft
    Лидер в разработке современных ИТ-решений на заказ

    Комментарии 34

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

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

        0

        Видимо явное требование было, что не всегда бывает.

          0
          Запас прочности нужен всем, но на практике встречается не так уж часто. У нас достаточно много проектов в работе, и зачастую на старте новых проектов мы находим те или иные недочеты в архитектуре. На ее закладку может не хватать знаний, времени, иногда опыта. Так что считаем, что об этом надо помнить и говорить)
          +1
          День добрый! Спасибо за интересную статью.

          Ряд вопросов:
          1. Как именно вы поддерживаете совместимость мобильного и веб клиентов с логикой бэка (совместимость старых версий через подмену статусов и логики из новых)?
          2. Как идет разработка и поставка мобильного клиента и интеграции в бэке (отдельная ветка в Git для каждой версии клиента и описание интеграции в бэке под каждую версию или как-то иначе)? Сюда же неплохо было бы пояснение про стабильные релизы и вашу версионность — тема не раскрыта.
          3. Как именно идет обработка счетов (так как счета нужны практически для любых продуктов банка)? Что за логика под Middle сервисом счетов?
          4. Как у вас организовано получение данных клиента (так как данные клиента нужны для любых продуктов банка)? Все хранится в какой-то БД и подгруажется в кэш? Или внешняя система -> кэш? Шина -> кэш? Как обеспечивается отказоустойчивость?
          5. В случае критической ошибки на минорной версии (например, берется не тот счет для операции, что не противоречит автотестам, но противорчит логике) — она у вас сразу деплоится с ошибкой? Не слишком ли это рискованно?
          6. Как у вас организован процесс электронной подписи? Вы храните подпись где-то или клиент каждый раз для каждого сервиса должен подгружать ключ?
            0
            Спасибо за ваши вопросы, уточним некоторые данные и вернемся с ответом.
            Сразу отметим, что базы данных для микросервисов изолированы друг от друга, что позволяет конфигурировать серверы баз данных для обеспечения требуемой доступности и скорости работы. Просим извинить, если по тексту сложилось другое впечатление.
              0
              Как и обещали, мы уточнили эти вопросы с командой, постараемся ответить по пунктам.
              1. Новый бэк пока используем только для нового мобильного приложения.
              2. Мобильное приложение завязывается на определенный протокол взаимодействия с API сервера. На gateway указывается, какие версии сервисов соответствуют этому протоколу. Таким образом backend может релизить новые версии сервисов по их готовности, а когда мобилка, в свою очередь, готова с ними работать, мы выпускаем новый протокол.
              3. Не совсем понятен вопрос, вы не могли бы конкретизировать?
              4. Есть сервис, ответственный за данные клиента, который получает из АБС актуальные данные и хранит у себя в БД. При недоступности АБС данные могут браться из БД сервиса, если мы понимаем, что они актуальные (обновлялись недавно).
              5. Есть несколько стендов, в том числе тест/предпрод. Такие ошибки выявляются на этапе тестирования.
              6. К сожалению, не можем раскрыть эти подробности о проекте.
                0
                Уточняю по третьему вопросу: если в системе используется интеграция с несколькими вендорами (такими, как BankMaster) — идет типизация и перетипизация счетов для продуктов запросами во внешние системы. Из-за этого никаких адекватных (быстрых) вариантов с кэшем быть не может. Как реализовано у вас?

                Уточняю по второму вопросу: как именно вы обходите конфликт версий? Если версия 1.4.123, к примеру, использовала адрес клиента одной строкой, а в новой версии 1.5.0 необходимо использовать две строки, то как вы это обходите? Делаете костыль для склеивания строк для старой версии (соответветственно, это идет адаптация под старые версии), дублируете данные (для старой и для новой версии), изначально предусматриваете в старой версии гибкость (дозагрузка атрибутов и ключей с бэка — в приложении просто абстрактная логика работы с тремя-четырьмя типами полей), предусматриваете более жесткие меры (отключение функционала для старых версий)?
                  0
                  1. Таких кейсов не удалось вспомнить, поэтому подсказать, к сожалению, не можем.
                  2. Разные версии сервисов имеют разный API, соответственно МП работает со «своим» сервисом.
                  Здесь вопрос доработки таблиц базы данных для разных версий. В большинстве кейсов разные версии одного сервиса работают с одной таблицей. Есть кейсы, когда новый сервис будет использовать новые поля. Мы добавляем их в таблицу. Старый сервис продолжит только считывать данные и отправлять их пользователю. А новый сервис будет обновлять данные. Принцип немного похож на CQRS.
                    0
                    Судя по вашим ответам (без конкретики по ускорению и стабильности — не указаны статистические данные при заявленном ускорении в 15 раз; так и не получил никаких конкретных пояснений по версионности и веткам в Git, только общие пояснения), у вас SOA, а не микросервисы (увидел только одно место, где может быть микросервис); очень скромный функционал (только мобильный клиент со скромным для универсального (без жесткой специализации — кредитный или обменный) банка функционалом, если судить по описанным сервисам), который можно и монолитом с мажорной версионностью делать быстро и просто без особых трудностей. Как вывод, с учетом ваших ответов, использование микросервисов и ряда представленных решений здесь экономически и технически нецелесообразно.
                      0
                      NDA накладывает на нас определенные ограничения. Нам очень жаль, что мы не можем раскрыть подробности в достаточной степени для более детального обсуждения. Огромное спасибо за проявленный интерес и ваши комментарии. Желаем вам интересных проектов и вызовов в работе!
              0

              Вопросики:


              1) Я один сломал мозг представив это? Кто, кого, когда обновляет… 3 абзаца абсолютно в разные направлениях. Просто прочитайте что вы написали:


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

              API сервис счетов. Сервис сперва проверяет, есть ли актуальные данные пользователя в Cache. При успешном исходе возвращает данные, в ином случае отправляет запрос в Middle сервис счетов.

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

              2) По асинхронности, "сервис получает сообщение о входе пользователя в приложение и сразу же обновляет данные по счетам". Правильно я понимаю что всё асинхронно и допустим высокая нагрузка на систему, и я быстро зайду на страницу счетов, то могу увидеть старые данные? Считаете ли вы приемлемо такое в приложениях уровня финансы?


              3) Вы используете кафку, так почему вы делаете столько промежуточных лишних звеньев(балансировщики, самодельные сервисы, кэши, сигналы для обновления кэшей), а не считаете в реалтайме в кафке?
              Актуальный баланс можно сразу получать с помощью kafka\ktable в реальном времени. А изменения будут в виде отправить сообщений в топик balance {userId: 1, balance: +40},{userId: 1, balance: -40}

                0
                Не один. Тут даже архитектурно у них микросервисы нарисованы, а по рисунку и описанию дальше — чистый веб SOA. Вот Middle сервис счетов — это уже не микросервис, а часть веб SOA; сервис вызывается, когда данные отличаются от хранимых в кэше, что еще раз не микросервисная реализация. При этом написано, что данные обновляются только, если они изменились, при этом: «сервис получает сообщение о входе пользователя в приложение и сразу же обновляет данные по счетам»… Могу единственно предположить, что говорится про запуск проверки актуальности данных. Так?
                  0
                  — В идеальном мире, конечно же, хочется видеть сразу актуальные данные на момент входа. В реальном мире можно использовать, например, систему push уведомлений, которая сообщит мобильному приложению, что счета на сервере обновились и их можно оперативно подгрузить.
                  — Что касается №3, нам бы это решение не подошло, как минимум из-за единой точки отказа.
                  0
                  >Сейчас при реализации отдельных функциональностей в новых версиях мы не ломаем старые версии

                  Как это отражается на необходимости и трудоемкости поддержки старых версий, апи, итп?
                    0
                    При этом архитектуру нужно было заложить с запасом прочности, в расчете на будущее расширение

                    И поэтому вы выбрали микросервисы? Вы не видите в этом противоречие? Микросервисы это изоляция по функционалу и по данным (каждый микросервис должен хранить данные в своей базе данных иначе считается что это не настоящие микросервисы). Ок, вы построили архитектуру и разбили по микросервисам — например платежами/переводами занимается один сервис а данными юзеров занимается второй сервис. Правильно? А потом на следующий день прилетает задача — вот мы хотим добавить программу лояльности и начислять юзеру какие-то баллы за переводы. И теперь для реализации этой задачи микросервису переводов нужно общаться с микросервисом который хранит данные юзеров. А теперь вопрос — как вы будете решать race-conditions и атомарное выполнение этой бизнес-логики? Речь не только про потерю связи, логику retry-ев на транспортном уровне (https://habr.com/ru/company/yandex/blog/442762) а про более фундаментальную проблему консистентной обработки данных и serializable уровня изоляции транзакций — https://www.youtube.com/watch?v=5ZjhNTM8XU8
                    В общем микросервисы можно применять только когда проект уже устоялся и не планирует расширяться, иначе добавление нового функционала имеет тенденцию увеличивать связность данных а это в свою очередь требует атомарного выполнения бизнес-логики которая обращается к разным микросервисам и реализации распределенных serializable-транзакций (иначе привет race-conditions и неконсистетность данных и дыры в безопасности)

                      0
                      Проблема передачи транзакционного и секурити контекстов а так же калбэки — это типичная проблема микросервисов, реализованных через REST/SOAP, но, с другой стороны, никто не запрещает использовать другие протоколы, где эти вопросы решены, например RMI.
                        +1
                        Ок, вы построили архитектуру и разбили по микросервисам — например платежами/переводами занимается один сервис а данными юзеров занимается второй сервис. Правильно?

                        Нет, неправильно. Нет никакого «сервиса юзеров». разные поля данных, относящиеся к юзеру хранятся в разных сервисах.

                        прилетает задача — вот мы хотим добавить программу лояльности и начислять юзеру какие-то баллы за перевод

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

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

                        Конечно же нет. Просто у вас в голове неправильный подход к их проектированию.
                        0

                        Как заметили выше, если микросервисы обращаются к единой БД, то это не кошерный микросервис, а скорее, просто веб сервис. В этом случае теряется смысл их разбивать, так как велико количество внутренних связей. Но это все вопросы терминологии.
                        Теперь было бы интересно услышать как изменилась стоимость владения и поддержки: расследование бага в микросервисной среде требует обычно в разы больше времени. Также сама философия разбиения на микросервисы предполагает создание множества отдельных БД, а значит кучу процессов синхронизации между ними, а значит кучу дополнительных тикетов в поддержку, которых в случае монолита бы не было.
                        В общем ждем статью "От монолита к микросервисам — месяц (или год) спустя"

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

                          Полностью согласен с комментариями выше о целесообразности использования микросервисов. Тоже ломали голову переходить или нет, в итоге не перешли). Оставили монолит, единственное думаем над тем, чтобы можно было разворачивать множество копий монолита на чтение для снижения нагрузки на один сервер и оставить один на изменение(консистентность данных в приоритете), разделив отправку запросов через какую нибудь проксю. Не было у кого-то подобного опыта?

                            0

                            Делал, но не так немного. Все копии равноправны, нагрузка на чтение распределяется по репликам базы данных. 90% SELECT — чистое чтение, а не SELECT… FOR UPDATE

                            0
                            Можно я присоединюсь к голосам выше?
                            История перехода от коробки к собственному продукту — да, она тут есть. И она вам помогла осуществить большую часть написанного. А микросервисы — разве что как аргумент моды.
                            Мы как-то делали монолит, который успешно выкатывался раз в день и ничего, работало
                              0

                              Это может быть не просто данью моды, а способом убедить начальство/заказчика вообще начать что-то делать. Манипуляция, наверное, такая.

                                0
                                Все верно, если команда одна, состоит из мидл+ профессионалов, а область не очень сложная, то монолит будет идеальным решением — и раскатывать можно достаточно часто.

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

                                Этот вопрос не был в полной мере освещен в статье, поскольку не относится к технике, больше к бизнесу.
                                0
                                Хорошо, когда банк один. Попробуйте промасштабировать свое решение хотя бы на десяток банков. А лучше на сотню или десятки тысяч.
                                  0
                                  Если мы правильно поняли ваш посыл, речь идет о коробке, которая обслуживает много банков. Безусловно, облачные технологии заставляют нас использовать другие подходы и другие архитектурные решения. Но рассматриваемый случай к ним не относится. В нашем случае коробочное приложение крутилось на серверах клиента и в общем случае обслуживало даже не один, а всего лишь часть банка. Это было одной из причин изменения архитектуры.
                                  0

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

                                    0
                                    Добрый день, обязательно постараемся на все ответить! У вас было много важных вопросов, некоторые из них мы решили уточнить с командой для более полного описания.
                                    0
                                    Так же стоит вопрос — а нужно ли было вам ускорять выпуск релизов до такой частоты? Одно дело когда это были бизнес-требования, а другое — когда это требования для мобильного приложения. Что там можно и нужно так часто обновлять?
                                      0
                                      Спасибо за фидбек всем, кто читает и комментирует. Как отметили выше, комментарии подсветили ряд важных вопросов, которые мы не затрагивали в материале. Над проектом работала большая команда, уточним некоторые вопросы, чтобы описать подробнее. Просим простить за задержку, обязательно постараемся всем ответить)
                                        0

                                        Первые два больших комментария с 6 и с 3(мой) пунктами, если можно ответьте

                                        0
                                        Забавно: есть огромное количество статей о том, что монолит разобрали на микросервисы (хотя чаще на микросервисный монолит), при этом скромно умалчивается о том, что пришлось заплатить за увеличившуюся сложность и стоимость коммуникаций сервисов.
                                          0

                                          одно дело заплатить трудом, поддержкой и.т.д. а другое кажется они переделали на непозволительные юзкейсы для фин проекта.
                                          Смотрите мой второй пункт (пока они его не прокоментировали):
                                          https://habr.com/ru/company/simbirsoft/blog/512310/#comment_21883146

                                            0
                                            Добрый день! Ответили выше на ваш комментарий, посмотрите, пожалуйста.

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

                                        Самое читаемое