Русский
Русский
English
Статистика
Реклама

Блог компании mail.ru group

Микрофронтенды разделяй и властвуй

14.04.2021 16:05:26 | Автор: admin


Всем привет! Меня зовут Аня, я фронтенд-разработчик в Delivery Club. Хочу рассказать про использование микрофронтендов. Поговорим о том, что же это за зверь такой микрофронтенд, почему мы решили использовать этот подход в своих проектах и с какими проблемами столкнулись при внедрении.

Для чего они нам понадобились


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

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

Поэтому нам нужна была возможность:

  • использовать любые инструменты и технологии, снижая риск устаревания;
  • разрабатывать и деплоить независимо;
  • изолировать реализации.

Устройство проекта


Для начала расскажу, как сейчас устроен наш проект.

  • Основное старое приложение на AngularJS, к которому мы планируем подключать новые микроприложения.
  • Dashboard-приложение на Angular 6, подключенное через iframe (но оно со временем разрослось и от описанных выше проблем не избавило). К нему подключаются приложения, здесь хранятся старые страницы.
  • Приложения на VueJS, которые используют самописную библиотеку компонентов на VueJS.





Мы поняли, что ограничения тормозят развитие проекта. Поэтому сформулировали возможные пути:

  • Разделение приложения на страницы по маршрутам.
  • iframe.
  • Микрофронтенды.

Мы сразу отказались от варианта с iframe (почему мы так решили, читайте ниже). У разделения страниц по маршрутам есть один большой недостаток: дублирование логики в каждом приложении (типа навигации) и вообще всего, кроме специфики страницы. Поддерживать такую систему в актуальном состоянии очень сложно. Поэтому мы выбрали микрофронтенды.

Что такое микрофронтенды


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


Сейчас наш проект только переходит на микрофронтенды, наша команда стала первопроходцем. И мы столкнулись с рядом проблем.

Проблемы внедрения микрофронтендов


1. Ещё один iframe? Может, уже хватит?


К великому разочарованию, наше dashboard-приложение открывается внутри iframe (исторически так сложилось), и нам очень не хотелось делать один iframe внутри другого. Однако это был запасной план. Если вдруг что-то пойдёт не так, то просто заворачиваем микрофронтенд в iframe и готово рабочее приложение.

Мы видели несколько недостатков:

  • Неудобная навигация. Каждый раз для редиректа на внешнюю ссылку нужно использовать window.postMessage.
  • Сложно верстать в iframe.

К счастью, нам удалось этого всего этого избежать, и микрофрентенд мы подключили как веб-компонент с shadow dom: <review-ui-app></review-ui-app>. Такое решение выгодно с точки зрения изоляции кода и стилей. Веб-компонент мы сделали с помощью модифицированного vue-web-component-wrapper. Почитать подробнее о нём можно здесь.

Что мы сделали:

  1. Написали скрипт, который добавляет ссылку на сборку микрофронтенда в разделе head страницы при переходе на соответствующий маршрут.
  2. Добавили конфигурацию для микрофронтенда.
  3. Добавили в window.customElements тег review-ui-app.
  4. Подключили review-ui-app в dashboard-приложение.

Так мы сразу убили несколько зайцев. Во-первых, приложение представляет собой кастомный тег, как мы привыкли компонент. Во-вторых, кастомный тег принимает данные как свойства, следит за ними и выбрасывает события, которые слушает обёртка. Ну и в-третьих, избавились от iframe внутри iframe :)

2. А где стили?


Ещё одна неприятная проблема нас ждала дальше. Компоненты в микрофронтенде работали, только вот стили не отображались. Мы придумали несколько решений:

  • Первое: импортировать все стили в один файл и передать его во vue-wrapper (но это слишком топорно и пришлось бы добавлять вручную каждый новый файл со стилями).
  • Второе: подключить стили с помощью CSS-модулей. Для этого пришлось подкрутить webcomponents-loader.js, чтобы он вшивал собранный CSS в shadow dom. Но это лучше, чем вручную добавлять новые CSS-файлы :)

3. Теперь про иконки забыли!


Затем мы подключили внутреннюю библиотеку, но в ней не отображались SVG.
С иконками нам пришлось повозиться подольше. Для их подключения нам пришлось сделать несколько итераций:

  1. Сначала мы попытались подрубить спрайт так же, как и стили, через appendChild. Они подключились, но всё равно не отображались.
  2. Затем мы решили подключить через sprite.mount(this.shadowRoot). Добавили в вебпаке в svg-sprite-loader опцию spriteModule: path.resolve(__dirname, './src/renderers/sprite.js). Внутри sprite.js экспортировали BrowserSprite, и иконки начали отображаться! Мы, счастливые, подумали, что победили, но не тут-то было. Да, иконки отображались, и мы даже выкатились с этой версией в прод. Но потом нашли один неприятный баг: иконки пропадали, если походить по вкладкам dashboard-приложения.
  3. Наконец, во vue-wrapper мы подключили DcIconComponent (библиотечный компонент, позволяющий работать с библиотечными иконками) и в нём подключили иконки из нашего проекта. Получили отображение без багов :)

4. Без авторизации никуда!


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

  • Токен с авторизацией передаём с помощью свойств веб-компонента.
  • С помощью AuthRequestInterceptor подключаем токен-запросы для API.
  • Используем токен, пока он не протухнет. После протухания ловим ошибку 401 и кидаем в dashboard-приложение событие обнови токен, пожалуйста (ошибка обрабатывается в AuthResponseInterceptor).
  • Dashboard-приложение обновляет токен. Следим за его изменением внутри main-сервиса, и когда токен обновился, заворачиваем его в промис и подписываемся на обновления токена в AuthResponseInterceptor.
  • Дождавшись обновления ещё раз повторяем упавший запрос, но уже с новым токеном.

5. Нас волнуют зависимости


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

  • В микрофронтенд-приложении указываем в webpack.config.prod.js в разделе externals те зависимости, которые хотим вынести:

    module.exports = {externals: {vue: Vue},
    

    Здесь мы указываем, что под именем Vue в window можно будет найти зависимость vue.
  • В рамках оболочки (в нашем случаем в dashboard-приложении) выполняем npm install vue (и другие npm install-зависимости).
  • В dashboard-приложении импортируем все зависимости:

    import Vue from vue(window as any).Vue = Vue;
    
  • Получаем удовольствие.

6. Разные окружения


Мы имеем три окружения: локальное, тестовое и прод. Сначала у нас была проблема при переходе с тестового окружения на прод: для микрофронтенда приходилось явно прописывать в проекте ссылку.

Решили мы это следующим образом:

  1. Добавили в микрофронтенд файл, в котором определяем конфигурацию для приложения в runtime браузера. Также добавили в Docker системный пакет, который предоставляет команду envsubst. Она подставляет значения в env.js, который тянет микрофронтенд-приложение, и эти переменные пишутся в window['${APP_NAME}_envConfig'].
  2. Добавили переменные окружения отдельно для прода и отдельно для тестового окружения.

Так мы решили несколько проблем:

  • настроили локальные моки (раньше приходилось вручную включать их локально);
  • настроили тестовое окружение без дополнительных ручных переключений на прод и обратно.

Выводы


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

Думаю, со временем всё больше проектов будут отказываться писать монолит и переходить на более универсальные подходы. Одним из таких подходов могут стать и микрофронтенды. Надеюсь, эта статья поможет кому-то в реализации :).
Подробнее..

Подход Multicloud Native Service что это такое и как поможет сделать IT-систему максимально отказоустойчивой

28.04.2021 16:12:49 | Автор: admin


Хабр, привет! Меня зовут Николай Бутенко, я руководитель Private Cloud в Mail.ru Cloud Solutions, и сегодня хочу обсудить с вами одно из самых больших заблуждений, с которыми я встречаюсь каждый день. Если вы когда-либо работали с облачными сервисами, то наверняка знаете о распространенном мнении, что перенос приложения в облако является панацеей от всех возможных с ним проблем. Я регулярно сталкиваюсь с такой позицией на встречах с самыми разнообразными заказчиками.


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


Поэтому если перед вами стоит задача построить на 100% отказоустойчивую и высокодоступную (High Availability, HA) систему, я рекомендую придерживаться подхода Multicloud Native Service, сочетающего лучшее в подходах Multicloud и Cloud Native. Такой подход не сводится только к использованию контейнеров: чтобы приложение могло пережить любой отказ, нужно подумать и об инфраструктуре, в частности использовать не одну, а минимум две независимые площадки, например провайдера публичного облака и частную инфраструктуру.


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


Принципы построения Cloud Native-приложений


Прежде чем рассматривать варианты построения Cloud Native-архитектур, необходимо, чтобы ваши приложения были готовы к переносу в облако. По мнению фонда Cloud Native Computing Foundation (CNCF), ставшего своего рода амбассадором в области развития облачных технологий, существует пять базовых принципов, которые выделяют Cloud Native-приложения из числа прочих.


1. Динамичность


Это возможность быстрого развертывания и конфигурирования вашей системы на любой новой площадке, что становится особенно актуальным при неожиданной смене облачного провайдера. К средствам достижения динамичности можно отнести CI/CD и подход Infrastructure As Code (IaC). Остановимся подробнее на втором.


Вся ваша инфраструктура должна быть декларативно описана в виде кода. Описание может включать перечень необходимых сервису виртуальных машин, требования к их конфигурации, топологию сети, DNS-имена и так далее. Стандартом де-факто для решения этой задачи в последнее время стал Terraform, однако существуют и альтернативы: Ansible, Salt, Cloudify, Foreman, Pulumi, AWS Cloud Formation.


Задача этих инструментов возможность быстрого развертывания, настройки инфраструктуры и контроль сохранения ее состояния, в том числе при смене провайдера. Если при небольшом количестве виртуальных машин их ручная настройка вполне возможна, то в том случае, когда это количество измеряется сотнями и дополняется сложной сетевой топологией, процесс переноса инфраструктуры может занять в лучшем случае несколько недель, а то и месяцев. Цель подхода IaC сократить это время. При наличии декларативных описаний все, что обычно требуется при переходе, это использовать другой Terraform-провайдер, изменить Environment-переменные и немного отформатировать код.


Хорошей практикой считается сочетание инструментов для декларативного описания с Post Install-скриптами, которые после запуска инфраструктуры конфигурируют информационную систему нужным образом, например устанавливают последние обновления используемого ПО и так далее. Для этой цели предназначены Ansible Playbooks, Puppet, Chef и другие инструменты, либо можно использовать cloud-unit.


2. Возможность эксплуатации


Это возможность управления жизненным циклом ваших систем в автоматизированном режиме. Это свойство тесно связано с динамичностью и включает в первую очередь применение пайплайнов CI/CD для автоматического развертывания приложений. У вас должна быть возможность оперативно выпускать новые сборки, отслеживать статус их выполнения на различных средах и выполнять откат в случае сбоев. Если вы строите приложение на основе микросервисов, для каждого сервиса рекомендуется настраивать независимый жизненный цикл (отдельную сборку). Обязательно учитывайте возможность отката обновлений или обновление только части ваших информационных систем (Rolling Update), это поможет избежать ошибок на уровне выкатки, которые могут привести к недоступности всего сервиса.


Современный IT-рынок предлагает множество решений для построения CI/CD: GitLab CI/CD, Jenkins, GoCD и другие.


3. Возможность наблюдения


Это мониторинг инфраструктуры и разнообразных бизнес-метрик ваших систем. Необходимость мониторинга для любого приложения, будь то Startup- или Enterprise-решение, сложно переоценить. Однако я регулярно сталкиваюсь с проектами, где мониторинг отсутствует, что в большинстве случаев приводит к серьезным проблемам. При переходе в облако мониторинг как минимум поможет проверять заявленный SLA провайдера и получать ретроспективу производительности при разборе спорных ситуаций, как максимум находить узкие места в архитектуре и исправлять их до того, как они станут влиять на пользователей.


В большинстве случаев облачные провайдеры поддерживают все стандартные инструменты мониторинга, такие как Prometheus, Grafana, Fluentd, OpenTracing, Jaeger, Zabbix и другие, а также могут предлагать собственный встроенный мониторинг.


4. Эластичность


Это возможность масштабирования ваших приложений в зависимости от изменяющейся нагрузки. Когда речь заходит об автоматическом масштабировании, первым из доступных инструментов на ум приходит, конечно же, Kubernetes, ставший своего рода стандартом оркестрации контейнерных приложений. И действительно, он поддерживает все необходимые уровни масштабирования, а у некоторых провайдеров в том числе даже автомасштабирование на уровне кластера из коробки. Однако отмечу, что его использование отнюдь не является обязательным: можно построить эластичное приложение на основе чистого IaaS (Infrastructure as a Service), используя мониторинг и преднастроенные хуки для обработки изменений нагрузки. Да, Kubernetes значительно упрощает этот процесс, но не стоит забывать, что он является всего лишь инструментом.


Чтобы соответствовать подходу Cloud Native, крайне важно планировать свою архитектуру так, чтобы в ней не было узлов, масштабируемых только вертикально (путем увеличения/уменьшения выделенных ресурсов), так как в конечном итоге вы будете ограничены мощностями нижележащего оборудования или гипервизоров в облаке. Постарайтесь обеспечить каждому узлу возможность горизонтального масштабирования за счет изменения числа экземпляров самого приложения.



Отдавайте предпочтение горизонтальному масштабированию


Также не стоит забывать оборотную сторону скейлинга возможные проблемы с DDoS-атаками. Нужно всегда тщательно подбирать границы для автомасштабирования (минимальное и максимальное число узлов) и использовать AntiDDos. В противном случае DDoS-атаки, не отличаемые системой автомасштабирования от полезной нагрузки, могут привести вас к серьезным финансовым потерям.


5. Отказоустойчивость


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


Приведу несколько основных рекомендаций:


  1. Составьте план учений DRP (Disaster Recovery Plan) и регулярно проводите согласно нему стресс-тесты для выявления единых точек отказа в системе (Single Point Of Failure, SPOF). Очень важно учитывать все возможные домены отказа, в том числе проверяя поведение системы в боевом режиме при выходе из строя отдельных узлов или целого плеча системы. Конечно, обеспечивая возможность их быстрого включения обратно.
  2. Используйте георепликацию для обеспечения высокой доступности ваших данных. Георепликация гарантирует хранение копий данных в нескольких дата-центрах. Но учитывайте, что геораспределенное хранилище имеет более высокие задержки отклика и не подходит для решений, требующих низкого Latency, например высоконагруженных баз данных.
  3. Используйте балансировку нагрузки на всех уровнях вашего приложения: перед web-серверами, серверами приложений и серверами баз данных. Все сетевые вызовы должны направляться не на прямые адреса виртуальных машин, а на адреса балансировщиков во избежание создания дополнительных точек отказа. Большинство облачных провайдеров предлагает балансировку нагрузки как сервис (Load Balancer as a Service, LBaaS). Учитывайте возможность выхода из строя части узлов, чтобы перераспределенная нагрузка не убила оставшиеся экземпляры сервиса. Используйте также надежные GSLB для балансировки между разными провайдерами.
  4. Используйте резервное копирование. Хотя я в своей практике сталкивался с примерами сервисов и информационных систем, где была построена правильная Cloud Native-архитектура и вовсе отказались от ведения бэкапов: они стали не актуальны. Так что ведение бэкапов желательно, но вовсе не обязательно но только если вы грамотно используете Multicloud Native-подход. Если вы сомневаетесь, всегда делайте бэкапы!
  5. Проведите аудиты безопасности, например Penetration Test, на предмет уязвимостей и устойчивости к разным типам атак.

С другими рекомендациями по созданию отказоустойчивых приложений от Mail.ru Cloud Solutions можно ознакомиться по ссылке.


Виды рисков при работе с облаком


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


1. Географические риски


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


Думаю, многие помнят пожар в московском дата-центре OST провайдера DataLine в июне 2019-го. Но тогда удалось обойтись малыми жертвами все клиенты были оперативно переведены на резервные площадки, да и урона машинному залу фактически нанесено не было: пострадали лишь кровля и система кондиционирования. К гораздо большим потерям привел недавний пожар в дата-центре SBG2 провайдера OVH в Страсбурге, обернувшийся падением множества сервисов по всему миру, полным уничтожением одного ЦОДа (SBG2) и вынужденной потерей второго ЦОДа, расположенного рядом и частично пострадавшего от пожара (SBG1). В этом как раз и состоит суть географических рисков: когда ЦОДы провайдера территориально расположены близко друг к другу, в случае катастрофы они не страхуют друг друга, а все оказываются под угрозой.


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


  1. У провайдера несколько ЦОДов.
  2. ЦОДы расположены на достаточном расстоянии друг от друга, по возможности используют разные каналы связи, интернет-провайдеров и питаются от различных электростанций.


На что обратить внимание при выборе облачного провайдера для минимизации географических рисков


2. Government-риски


Сюда входят различные политические и законодательные решения, которые могут неожиданно потребовать смены провайдера. В качестве наиболее яркого примера последних лет можно назвать бан социальной сети Parler в январе 2021 года, когда Amazon отказал этой платформе в дальнейшем хранении данных и приложение фактически стало недоступным. В России можно вспомнить 152-ФЗ, запрещающий хранить персональные данные пользователей за пределами РФ, что автоматически ограничивает выбор провайдеров для определенных организаций (банковский сектор, медицинские организации и так далее).


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


3. Риски сбоев на стороне провайдера


Это технические сбои на уровне провайдера в целом, чаще всего связанные с человеческим фактором, например выходом неверных обновлений, ошибками в прогнозировании потребляемых ресурсов и так далее. Даже у таких общепризнанных облачных лидеров, как Amazon и Google, регулярно фиксируются сбои в сервисах. Например, в Google Cloud за последний год произошло свыше 100 инцидентов, в AWS сбои происходят минимум раз в год. В качестве примера можно вспомнить крупный сбой на стороне AWS в ноябре 2020 года, в результате которого возникли неполадки в работе множества сайтов и приложений, включая iRobot, Flickr, Roku и Adobe Spark.


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


Таким образом, мы рассмотрели три вида рисков, с которыми вы можете столкнуться после перевода своих приложений в облако. Но если первая группа рисков легко устраняется за счет выбора провайдера с географически разнесенными ЦОДами, то риски из второй и третьей групп, по моему мнению, можно исключить, только применив Multicloud Native Service-подход.


image
Для устранения всех возможных рисков при работе в облаке рекомендуется подход Multicloud Native Service


Multicloud Native Service и Multicloud: в чем разница?


В первую очередь следует различать подходы Multicloud и Multicloud Native Service.


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


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



Вариант применения подхода Multicloud: использование данных приложения, запущенного в одном облаке, для анализа в другом облаке



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


Подход Multicloud Native Service подразумевает, что вы разрабатываете свой продукт таким образом, что он соответствует принципам Cloud Native и полностью абстрагируется от конкретного облачного провайдера. В первую очередь это достигается за счет отказа от проприетарных сервисов, о которых мы еще подробно поговорим. Такой подход позволяет одновременно развертывать ваши приложения в нескольких выбранных вами облаках и легко переносить их из одного облака в другое без простоев.


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



Подход Multicloud Native Service: развертывание и использование приложения в нескольких облаках одновременно


Таким образом, если Multicloud это про количество облачных провайдеров, то Multicloud Native Service это больше про сами приложения, их соответствие всем принципам Cloud Native и возможность развертывания на нескольких независимых площадках. Чаще всего Multicloud Native Service строится на основе Multicloud, но так происходит не всегда, так как в качестве площадок в этом случае могут выступать не только публичные облака или гибридные инфраструктуры, но и варианты с собственными ЦОДами компании.


В дальнейшем я буду говорить именно про подход Multicloud Native Service.


Варианты построения архитектур Multicloud Native Service


Существует два основных способа построения архитектур по подходу Multicloud Native Service:


1. Использование нескольких публичных провайдеров


В этом случае одно плечо инфраструктуры строится на стороне одного публичного провайдера, а второе на стороне другого провайдера. Например, можно комбинировать локального провайдера, действующего на территории РФ (Mail.ru, Яндекс, МТС, Ростелеком и другие), и глобального, действующего во всем мире (AWS, Google Cloud, Alibaba, Azure и другие), либо использовать двух локальных провайдеров одновременно.


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


2. Сочетание частной инфраструктуры и публичного провайдера


Это более распространенный вариант в российских реалиях, когда клиент продолжает использовать традиционную инфраструктуру на своей стороне (это может быть голое железо, виртуализированная среда или даже приватное облако) плюс добавляет к ней публичного облачного провайдера. Чаще всего получившуюся архитектуру называют гибридным облаком (Hybrid), но если ПО и инфраструктура в этом случае реализуют все принципы Cloud Native-приложений, то систему в целом можно называть Multicloud Native Service.


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



Упрощенная схема Multicloud Native Service-архитектуры: два плеча, в роли которых могут выступать два облачных провайдера либо один провайдер и частная инфраструктура


Возможные проблемы при переходе на Multicloud Native Service и как их избежать


Разумеется, у подхода Multicloud Native Service есть свои подводные камни. Ниже приведен перечень наиболее часто встречающихся проблем и описаны способы их решения:


1. Сетевые задержки


Если для вашей системы приоритетны низкие задержки, например требуются синхронные реплики БД, то, скорее всего, вам не подойдет вариант сочетания локального и глобального провайдеров в силу их территориальной удаленности. Следует выбирать провайдеров, которые смогут обеспечить минимальный Latency, например через выделенный канал Direct Connect. Это могут быть два локальных провайдера на территории РФ либо сочетание локального облачного провайдера и частной инфраструктуры. Также Direct Connect позволяют делать некоторые глобальные провайдеры.


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


2. Разница в производительности вычислительных ресурсов


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


3. Vendor lock-in


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


Пояснить проблему Vendor lock-in проще всего на примере референсных архитектур (Architecture Reference), предлагаемых большинством ведущих облачных провайдеров. Эти архитектуры описывают готовые решения (комбинации компонентов, их взаимосвязи и так далее) для типовых архитектурных задач, которые могут возникнуть при переходе в облако. Например, референсные архитектуры от Google и AWS.


На рисунке ниже приведен пример референсной архитектуры от AWS для обработки логов, которые впоследствии используются для построения мониторинга. Из рисунка очевидно, что для решения архитектурной задачи провайдер предлагает сразу несколько своих проприетарных сервисов: AWS Lambda, Amazon Kinesis Data Streams и так далее.



Пример референсной архитектуры от AWS. Источник


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


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


Какому провайдеру отдать предпочтение


В предыдущих разделах мы определились, что для построения отказоустойчивых приложений лучше всего использовать пару различных облачных провайдеров. Но как сделать правильный выбор из множества доступных на современном IT-рынке вариантов, какие возможности предлагает идеальный провайдер? А также на какие сервисы стоит ориентироваться, чтобы не попасть в ловушку Vendor lock-in?


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


  1. Реализация подхода IaC (Infrastructure as Code). В большинстве случаев это подразумевает поддержку Terraform, но не обязательно. В Mail.ru Cloud Solutions работает стандартный Terraform-провайдер, также можно использовать для управления инфраструктурой наш собственный Terraform-провайдер, доработанный под нашу платформу. Также сюда обязательно входит управление жизненным циклом виртуальных машин (Virtual Machine Lifecycle Management).
  2. Наличие хорошо документированных CLI и API. Необходимо для быстрого и удобного управления облачными ресурсами.
  3. Возможность подключения DC (Direct Connect). Это установка выделенного сетевого соединения между облаком и своим собственным ЦОДом или офисом с целью изолированного сетевого соединения, а также увеличения пропускной способности и обеспечения более устойчивой работы, чем через подключение по интернету. При этом уровень, на котором настраивается соединение, не принципиален (L2, L3).
  4. Поддержка CDN (Content Delivery Network). Использование географически распределенной сетевой инфраструктуры, лежащей в основе CDN, позволяет быстро раздавать контент по всему миру с задержкой в считаные миллисекунды даже во время пиковых нагрузок.
  5. Наличие DNS и балансировщиков сетевой нагрузки. Это важная часть настройки любых виртуальных сетей.
  6. Соответствие местному законодательству и наличие прочих сертификатов. Например PCI DSS, ISO 27*, GPDR для Европы, ФСТЭК для России и так далее. Это поможет избежать дополнительной головной боли при локализации.
  7. Технологическое соответствие между провайдерами. Если, например, ваша исходная инфраструктуру использует VMWare, логично выбрать провайдера предоставляющего гипервизор ESXi, желательно той же версии. Это сильно упростит миграцию. Тоже справедливо и для других платформ и гипервизоров. Требование не является обязательным, можно разместиться и на разных платформах.
  8. Поддержка основного пула сервисов:
    • контейнеризация/оркестрация (общепризнанный стандарт K8s или OpenShift);
    • базы данных как сервис;
    • S3 объектное хранилище для хранения больших объемов данных и статического контента;
    • очереди сообщений (Simple Queue Service, SQS);
    • Big Data с этим сервисом стоит быть аккуратным, чтобы не получить Vendor lock-in, так как некоторые провайдеры реализуют его по-своему;
    • мониторинг инфраструктуры и бизнес-метрик, в идеале сервис мониторинга должен быть независимым;
    • бессерверные вычисления (Function as a Service, FaaS). Незаменимы для обработки непрогнозируемой нагрузки без сохранения состояния (чат-боты, социальные сети и так далее);
    • аудит как сервис;
    • стандартные функции вида биллинга, сервисов ИБ (такие как WAF, AntiDDOS, возможность проведения аудитов ИБ), возможности использовать idP, RBAC;
    • выбирайте надежный Global Server Load Balancer, не надейтесь только на балансировку на уровне облачного провайдера, так как она подвержена тем же рискам.

Выводы


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


Если для ваших приложений приоритет отказоустойчивость и высокая доступность, лучшим решением будет использовать подход Multicloud Native Service. Да, это сложно, дорого, требует новых подходов к разработке, применимо не ко всем типам сервисов и чаще всего доступно исключительно крупному бизнесу. Но это позволяет исключить все виды возможных рисков.


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


Также важно понимать, что подходы Cloud Native и построенный на его основе Multicloud Native Service это в большей степени не про используемые вами программные инструменты. Например, они не требуют от вас жесткого использования контейнеров и Kubernetes, как принято считать. Применение этих подходов в первую очередь означает, что вы полностью продумали инфраструктуру и проверили все возможные домены отказа. И в случае сбоев вы не возлагаете всю ответственность на своего провайдера, осознавая, что успешная работа приложений в облаке во многом определяется и вашими действиями тоже.


Любите облака, и они ответят вам взаимностью.


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


  1. VMware и OpenStack: сравниваем две платформы для развертывания инфраструктуры.
  2. Как правильно посчитать TCO инфраструктуры.
  3. Наш канал в Телеграм о цифровой трансформации и IT-бизнесе.
Подробнее..

Как MCS и Х5 построили частное облако в энтерпрайзе, чтобы быстро получать готовые сервисы

07.06.2021 12:06:42 | Автор: admin


Castle in the sky by PiotrDura


Публичное и частное облако одного провайдера два разных продукта или одна и та же платформа, просто развернутая на разном оборудовании? На примере решения для Х5 Retail Group я, Илья Болучевский, технический директор Mail.ru Private Cloud, расскажу, в чем отличия и как построен процесс внедрения облака вендора в корпоративную инфраструктуру.


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


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


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

Ниже о том, как двум опытным командам MCS и Х5 удалось построить гибкую облачную платформу во внутреннем контуре заказчика в предельно сжатые сроки, какие сложности и подводные камни ожидали нас на этом пути и как данное решение помогает компании достичь поставленных целей.



Почему Х5 пошли в частное облако и каких результатов достигли


Компания столкнулась с необходимостью обеспечить быстрое выделение ресурсов под внутренние проекты, а также запустить новые процессы: микросервисную разработку в новом формате, CI/CD-пайплайны на базе облака и Managed-сервисов, автоматизацию на уровне IaC.


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


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


Результаты. После внедрения облака значительно улучшились KPI: раньше ресурсы виртуализации выдавались внутренним заказчикам в течение нескольких дней, теперь за минуты. Командам компании стало удобнее запускать пилоты, среды разработки, пересоздавать их.


Также компания интегрировала с платформой собственную разработку, которая автоматизирует доступ к ресурсам для внутренних клиентов. Благодаря ей внутренние клиенты разработчики, тестировщики, проектные менеджеры получили больше прав для самостоятельной работы с сервисами. Раньше с задачами они шли в службу поддержки и заводили там заявку, которая двигалась по очереди между несколькими командами. Сейчас все работает как Self-Service: автоматически, без ручного труда. Внутренние заказчики могут выбирать сервисы, смотреть демо, рассчитывать стоимость инфраструктуры под проект, что значительно ускорило процесс согласования.


Кроме того, выгоднее стала платформа контейнеризации за счет особенностей лицензирования решения: компания внедрила Kubernetes как сервис, который построен на Open-Source-технологии. Он дает возможность внутренним заказчикам получать полноценные кластеры с возможностью автомасштабирования, что помогает гибко управлять ресурсами. Также для ряда задач в качестве платформы контейнеризации продолжают использовать OpenShift и ванильный Kubernetes.


Наше частное облако стало основой для дальнейшей автоматизации: действия на платформе можно совершать не только через UI, но и через API, что позволяет сделать слой инфраструктуры абсолютно прозрачным и работать через любые приложения.


В итоге внедрение частного облака позволило нашему клиенту ускорить Time-to-Market IT-продуктов компании. Однако достичь этого было непросто: ниже разберу, какие проблемы возникают в процессе и как мы их решаем.


Почему так сложно развернуть частное облако


Создание частного облака в существующем IT-ландшафте это как стыковка Союз Аполлон: нужно соединить две совершенно разные системы сложившуюся корпоративную и внешнюю, которую приносит провайдер частного облака. Даже имея готовые компоненты, нельзя взять публичное облако и развернуть такое же во внутреннем контуре компании, ничего не меняя. У каждой компании свои требования, требуются сложные кастомизации, интеграции и доработки.


Например, у нас в Mail.ru Cloud Solutions есть готовый Kubernetes aaS. Для Х5 Retail Group мы полностью переписали его под другую топологию сети не потому, что он чем-то плох, а потому, что корпоративное частное облако иначе работает. Фактически публичное облако приходится каждый раз частично пересоздавать под каждого клиента.


По мере наработки клиентской базы мы стали лучше работать с запросами клиентов. Большинству нужны одни и те же доработки: интеграция с ADFS, кастомизация образов, в том числе для баз данных, свои ролевые модели, SSO-интеграции. Есть уникальные требования вроде RBAC-модели по сетевым портам.


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


Другая сложность взаимодействие команд. Нужно сработать вместе: наша команда обладает компетенцией в своем продукте, Х5 компетенцией в своих системах. Нужно объединить компетенции, договариваться, чтобы достичь результата. В Х5 Retail Group работает квалифицированная команда, которая на равных участвовала в процессе интеграции и выстроила процессы работы изнутри.


На уровне взаимодействия команд могут быть подводные камни. Например, виртуализацию и облачные платформы не стоит путать, но такая путаница встречается в компаниях, которые внедряют частное облако. Любая облачная платформа отличается тем, что есть Self-Service, пулинг ресурсов. Некоторые инженеры пытаются работать с ней как с классической системой виртуализации, а далеко не все принятые в таком случае подходы применимы к облаку.


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


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


Какую инфраструктуру мы построили для Х5


В Х5 Retail Group сейчас развернута облачная инфраструктура с единой консолью администрирования, маркетплейсом приложений и порталом самообслуживания для внутренних заказчиков. Им доступны платформенные сервисы, в которых у компании была потребность в момент внедрения, аналогичные таким же из нашего публичного облака. Компания получила наш Kubernetes как сервис и облачные базы данных, которые прошлые платформы не предоставляли: ранее их запускали в IaaS руками, на внутренней системе виртуализации.


В состав платформы входят следующие наши сервисы:


  • инфраструктурные сервисы (IaaS) виртуальные машины в различных конфигурациях, диски, балансировщики нагрузки, настройки Firewall;
  • PaaS кластеры Kubernetes как базовая платформа контейнеризации, набор баз данных PostgreSQL, Redis, MongoDB, MySQL, чтобы внутренние заказчики могли выбрать СУБД под потребности проекта/продукта. Все сервисы адаптированы под требования информационной безопасности компании;
  • отказоустойчивые хранилища на базе CEPH с тройной репликацией данных и интеграцией с СХД клиента;
  • маркетплейс, который в X5 используют для размещения нужных внутренним заказчикам приложений.

В нашем публичном облаке реализован биллинг потребляемых ресурсов/сервисов по модели pay-as-you-go на базе платформы in-memory вычислений Tarantool. Для Х5 в него внесены изменения: в частном облаке это не биллинг оплаты провайдера, а система внутренней отчетности и планирования ресурсов. Она нужна, чтобы мониторить мощности, которые использует каждый проект, контролировать соблюдение лимитов по квотам ресурсов, формировать финансовую отчетность для точной оценки расходов по каждому проекту. Это в том числе требуется и для финансового планирования на старте проекта: использование внутренней инфраструктуры не бесплатно, подсчет расходов на нее позволяет точнее оценить ROI проекта. В такой форме решение может быть адаптировано и для других клиентов MCS.


Также у нас есть отдельный инструмент маркетплейс, это витрина с SaaS-приложениями, которые можно использовать внутри платформы. Х5 применяют его как витрину приложений для внутренних клиентов: там размещены приложения для разработки и DevOps.


Мы провели обучение для команды компании, как паковать приложения с помощью Git. Теперь специалисты Х5 Retail Group сами его наполняют приложениями, которые нужны внутренним заказчикам. По сути, идут в SaaS-историю, используя платформу как оркестратор и витрину, ориентируясь на востребованность сервисов, добавляя то, что будет использовано продуктовыми командами.


С платформой интегрирована внутренняя разработка X5, созданная инженерами компании и позволяющая автоматизировать доступ к ресурсам для внутренних клиентов. Благодаря ей и возможностям облака разработчики, тестировщики, проектные менеджеры получили больше прав для самостоятельной работы с сервисами. Раньше с задачами они шли в службу поддержки и заводили там заявку, которая двигалась по очереди между несколькими командами. Сейчас все работает как Self-Service: автоматически, без ручного труда. Где-то процессы доступа были упразднены, а где-то согласование осталось, но было автоматизировано. Внутренние заказчики могут выбирать сервисы, смотреть демо, рассчитывать стоимость инфраструктуры под проект, что значительно ускорило процесс согласования.


Чтобы получить готовое к работе частное облако, нужно пройти как минимум 10 шагов.


Какие этапы надо пройти, чтобы внедрить частное облако


Работа над проектом Х5 Retail Group до приема в эксплуатацию заняла у нас около трех месяцев: два месяца на установку и месяц на основные кастомные интеграции. На этом работа еще не закончена, мы делаем дальнейшие кастомизации. Но остальные доработки это уже работа на эксплуатируемой инфраструктуре.


Вот из каких этапов состоит процесс внедрения частного облака:


  1. Определяем бизнес-задачи первый шаг с нашей стороны: понять, что нужно бизнесу от облака, от этого зависят дальнейшие действия.
  2. Определяем ограничения они могут быть по технологиям, которые компания может использовать или нет, по информационной безопасности и по бюджету.
  3. Разрабатываем общую архитектуру решения из каких компонентов будет состоять, какие сервисы будут использованы.
  4. Выясняем специфику реализации сервисов сетевая архитектура, потребность в информационной безопасности.
  5. Прорабатываем интеграции по архитектуре интеграция состоит из двух частей: на стороне облака и на стороне целевой корпоративной системы. Здесь большая часть связана с ролевыми моделями и правами пользователей.
  6. Составляем финансовую модель биллинг, что и как считается, как это учитывать при планировании проектов.
  7. Устанавливаем и настраиваем оборудование подготовка инфраструктуры для облака. В отличие от многих провайдеров мы можем развернуть облако на оборудовании клиента, если оно подходит по параметрам. Это позволяет переиспользовать оборудование, которое освобождается после ухода от традиционной инфраструктуры.
  8. Реализуем кастомные доработки и разворачиваем решение тут, как правило, проявляется все, что не учли на предыдущих этапах, вплоть до возврата назад и переработки архитектуры решения.
  9. Проходим приемо-сдаточные испытания (ПСИ) решения и вводим его в эксплуатацию.
  10. Дорабатываем в процессе использования.

Далее расскажу про некоторые этапы подробнее. Начнем с того, как мы вместе с Х5 готовили инфраструктуру к внедрению нашей облачной платформы.


Как мы с Х5 готовились к внедрению частного облака


На этапе пресейла мы либо получаем от клиента готовое ТЗ, либо выясняем, какие у него потребности, чтобы формализовать их в рамках ТЗ. Уточняется, какая инфраструктура в наличии, какое подходящее оборудование уже есть от старой инфраструктуры, что нужно дозакупить или поставить. Вместе с клиентом рисуем Best Practice-инфраструктуру. После чего все требования формализуются в рамках ТЗ, из которого выписывается ЧТЗ частное техническое задание, то есть как будет выглядеть реализация по ТЗ.


Когда мы все согласовали и подписали контракт, начинается этап предподготовки инфраструктуры, которым занимаемся мы или клиент по нашим требованиям. В последнем случае мы тесно взаимодействуем с командой компании, как это было на проекте Х5.


Инженеры Х5 Retail Group готовили оборудование, проводили необходимую коммутацию, настраивали сеть, создавали структуры в AD, DNS, предустанавливали операционные системы под гипервизоры, выдавали деплой-ноды, создавали необходимые технические учетные записи, согласовывали доступы, настраивали интеграцию с системами ИБ, проводили кастомизацию портала самообслуживания платформы.


Команда инженеров Х5 очень оперативно сработала и создала окружение, в котором могло работать наше решение. Примерно за две недели они подготовили инфраструктуру для начала деплоя.


Теперь мы могли приступить к развертыванию нашей платформы.


Как происходило развертывание приватного облака


После того как команда Х5 подготовила инфраструктуру, в дело вступили наши инженеры.


Процесс развертывания выглядит так:


  1. Сначала мы создаем деплой-ноду это нода, с которой устанавливается все остальное облако и распределяются роли по всем остальным серверам и серверам хранения.
  2. Затем начинается деплой IaaS-части, которая позволяет запускать виртуальные машины: она включает менеджмент, гипервизоры, подсистемы хранения.
  3. Поверх нее разворачиваем PaaS: Kubernetes, базы данных, большие данные в зависимости от требования клиента по ТЗ какие-то сервисы могут быть не нужны. В Х5 мы разворачивали Kubernetes aaS и облачные СУБД.

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


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


Как мы дорабатываем PaaS под требования клиента


К процессу развертывания PaaS в Х5 были требования, связанные с информационной безопасностью. Также нужно было, чтобы разворачиваемый образ сразу попадал в мониторинг.


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


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


К Kubernetes как сервису могут быть разные требования со стороны клиентов. Например, заранее прописанный Docker Registry или устаревшая версия Kubernetes: в публичном облаке самая младшая версия 16, можно взять 14, если компания ее использует. Для Х5 мы серьезно переделали KaaS, что было связано с особенностями топологии сети частного облака.


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


И это не единственные доработки нашей платформы: кроме доработки PaaS, требуются и другие кастомные интеграции, которые приходится реализовать.


Какие кастомные интеграции облачной платформы необходимы для On-premises


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


Я уже говорил, что требования корпоративных клиентов редко совпадают с коробочным решением. На этом этапе мы проводим интеграции с существующими ITSM-процессами и технологиями компании: попадание виртуальных машин в DNS, интеграция с AD, кастомная пересборка образов для авторизации через их AD, кастомная сборка образов под базы данных, интеграция с CMDB, S3-хранилищем, настройка резервного копирования на наш менеджмент.


Такие интеграции отличаются у разных клиентов это кастомная работа, автоматизировать ее в большинстве случаев нельзя. В случае Х5 Retail Group также были существующие системы типа AD, CMDB, DNS и так далее, со всем этим пришлось интегрироваться.


Также был запрос по работе биллинга: публичное облако заточено под мультитенантную модель, а приватные облака сделаны так, что один отдел отвечает за утилизацию облака; нужны были специальные типовые отчеты сразу по всем тенантам, то есть проектам. Из-за этого пришлось дописывать кусок биллинга, который бы учитывал требования. То же самое связано с некоторыми лицензиями сторонних вендоров: наш биллинг заточен на одни лицензии, в Х5 используют свои виды лицензий, пришлось эту логику также переделать.


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


Эти кастомные доработки стандартные и не слишком сложные, однако на любом проекте возникают настоящие вызовы. В Х5 Retail Group таким вызовом для нас стала переделка всей сетевой топологии, частично затронувшая PaaS.


Самое сложное: переделка сетевой топологии


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


В публичном облаке используется топология сети, позволяющая применять internal-сети внутри проектов и публикации в интернете через external-сеть, используя для этого floating IP (белые адреса).


В приватном облаке сеть выглядит иначе: она плоская, поделена на сегменты, которых нет в публичном облаке, архитектурно реализована по-другому. Любая виртуальная машина сразу попадает во Vlan, нет никаких floating IP и серых адресов, есть только блок плоских адресов.


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


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


С сетевой топологией была связана и самая большая переделка нашего Kubernetes aaS. По дефолту мы создавали его во внутренних сетях с балансировкой через наш балансировщик в публичном облаке MCS, имеющий выход во внешнюю сеть. Пришлось переделать всю логику работы, потому что Kubernetes надо было создавать в плоской сети и завязывать работу балансировщика, находящегося в частной сети.


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


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


Как мы выполнили требования информационной безопасности клиента


Приватное облако в Х5 отчасти выбрали из-за внутренних правил ИБ. Для компании мы выполнили интеграцию с ArcSight. Это SIEM-система: управление информацией о безопасности и событиями о безопасности. Все действия, которые происходят в инфраструктуре, должны регистрироваться в ней.


Например, произошел ресайз или удаление виртуальной машины SIEM фиксирует, во сколько была удалена ВМ и каким пользователем. Такую интеграцию с ArcSight используют многие клиенты. Некоторые используют другие SIEM-системы, но у всех крупных компаний они есть.


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


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


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


В проверки ПМИ входят тесты: высокая доступность (High Availability, HA), тесты отказоустойчивости, стресс-тесты выхода той или иной ноды или группы узлов, при которых все должно сохранить рабочее состояние. HA достаточно капризная, ее нам приходится долго и тонко настраивать с учетом всех кастомизаций.


Эти процессы заняли у нас много времени: месяц подготовки и проверок, потом доработки и устранение замечаний. В итоге через 2 месяца смогли предложить Х5 Retail Group решение, которое компания смогла взять в эксплуатацию, часть улучшений еще находится в процессе доработки.


Кастомные доработки сервисов и платформы первая сложность, соблюдение требований ИБ вторая, но есть еще и третья. В процессе внедрения нам еще надо преодолеть те технические ограничения, которые возникают у клиента. В случае с Х5 они были некритичными.


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


Технологические ограничения в проекте для Х5 Retail Group были разнообразные, расскажу про пару примеров.


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


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


Такие некритичные ограничения достаточно легко преодолеваются, но требуют времени, экспертности и совместной работы команд.


Итоги


После ввода проекта в эксплуатацию Х5 Retail Group увеличили скорость вывода на рынок новых продуктов. Наша команда получила бесценный опыт внедрения частного облака в энтерпрайзной коммерческой структуре и готовые решения, которые мы можем далее использовать в инфраструктурах других клиентов.


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


  1. Как реализуется отказоустойчивая веб-архитектура в платформе Mail.ru Cloud Solutions.
  2. Как выбрать облачную систему хранения данных, чтобы получить лучшую производительность и оптимизировать стоимость.
  3. Наш телеграм-канал об IT-бизнесе и цифровой трансформации.
Подробнее..

Как и зачем Mail.ru Group провела редизайн мобильной версии главной страницы портала

04.06.2021 16:20:58 | Автор: admin


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

Как было раньше


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


Старая мобильная версия главной страницы Mail.ru

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

План выглядел так:

  • Изменить визуальный стиль главной страницы, опираясь на новую дизайн систему Mail.ru;
  • Сделать доступ ко всем сервисам удобным и быстрым;
  • Добавить навигации по странице развлекательный характер за счет индивидуальных рекомендаций материалов и способов их оформления;
  • Сохранить видимость всех продуктов и учесть интересы тех, на которые пользователи чаще всего переходят с главной страницы Почты,Поиска и Новостей;
  • Адаптировать изменения под устройства с маленькими экранами, чтобы каждому пользователю было комфортно.

С чего начали


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

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


Первые UI варианты страницы

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

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


Новая лента

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

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

Изменение UX страницы


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


Варианты главной страницы для UX исследований

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

  • Поиск разместили сверху, так как он означает начало изучения страницы, и такое расположение в целом более привычное для пользователя;
  • Почту расположили под Поиском в виде виджета, чтобы человек не тянулся постоянно наверх. В дальнейшем мы будем выводить не только виджет Почты, но и календарь, Облако и социальные сети. Так пользователь сможет быстро и удобно переключаться между задачами и продуктами. Ниже расположили основную информацию на день: погоду, котировки и новости;
  • В процессе работы Погоду и котировки мы меняли местами с виджетом Почты. По итогам тестирования выбрали вариант, когда погода была ниже и не разделяла Поиск и Почту. Поменяли расположение рекламы;
  • Убрали акцентный цвет и сделали страницу максимально чистой.

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

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

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

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

  • Наверху расположен навигационный блок с меню, геолокацией и профилем;
  • Поиск выше Почты, а Погода рядом с Новостями;
  • Меньше акцентного цвета (синими остались только логотип и Поиск).

Финальные штрихи


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

У нас было несколько вариантов с разными формами и деталями. В итоге страница стала чистой: белого цвета с грамотно забрендированными ключевыми блоками. Фирменный цвет органично выделял Поиск, Почту и Новости. Строку Поиска сделали закругленной, чтобы она сильнее выделялась и не сливалась с виджетами ниже. Погоду и котировки разделили горизонтальные блоки более адаптивны и позволяют добавлять новые элементы.


Добавление виджетов

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


Сравнение старого и нового дизайна мобильной версии главной страницы Mail.ru

Как новая главная живет и процветает


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



В результате мы заметили такие изменения:

  • +12% к переходам на медиапроекты, включая погоду и ковид;
  • +4% к переходам в Поиск;
  • +5% к переходам в Почту.

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

Состав творческой группы:

  • Алена Китабова менеджер продукта.
  • Вячеслав Яшков руководитель команды дизайна Search & AI.
  • Елена Джуга дизайнер Search & AI.
  • Игорь Фролов руководитель группы frontend-разработки главной страницы и портальной навигации.
  • Владимир Францкевич программист группы frontend-разработки главной.
  • Денис Стасьев младший программист группы frontend-разработки главной.
  • Александр Буки программист группы frontend-разработки главной.
  • Анастасия Краснова ведущий инженер по тестированию.
Подробнее..

Перевод Fiberы новая фича в PHP 8.1

08.04.2021 14:12:31 | Автор: admin

PHP пытается восполнить недостаток возможностей в своей кодовой базе, и Fiberы одно из значимых нововведений. Они появились в PHP 8.1 в конце 2020 и привнесли в язык своего рода асинхронное программирование. Файберы представляют собой легковесные потоки исполнения (известные как сопрограммы, или корутины (coroutine)). Они исполняются параллельно, но обрабатываются исключительно самой runtime-средой, а передаются напрямую в процессор. Разные реализации сопрограмм есть во многих основных языках, но принцип один и тот же: позволить компьютеру одновременно выполнять две и больше задач и ждать, пока они все не завершатся.

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

Как работают файберы?


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

final class Fiber{    public function __construct(callable $callback) {}    public function start(mixed ...$args): mixed {}    public function resume(mixed $value = null): mixed {}    public function throw(Throwable $exception): mixed {}    public function isStarted(): bool {}    public function isSuspended(): bool {}    public function isRunning(): bool {}    public function isTerminated(): bool {}    public function getReturn(): mixed {}    public static function this(): ?self {}    public static function suspend(mixed $value = null): mixed {}}

Если создать новый экземпляр файбера с вызываемым объектом, то ничего не произойдёт. Пока вы не запустите файбер, callback будет выполняться так же, как любой другой нормальный PHP-код.

$fiber = new Fiber(function() : void {  echo "I'm running a Fiber, yay!";});$fiber->start(); // I'm running a Fiber, yay!

Я не упоминал, что файберы асинхронные? Они такие, но только до тех пор, пока вы не нажмёте на тормоза с помощью вызова Fiber::suspend() внутри callbackа. После этого файбер передаёт управление наружу, но помните, что эта файбер-машина ещё жива и ждёт возвращения к работе.

$fiber = new Fiber(function() : void {  Fiber::suspend();  echo "I'm running a Fiber, yay!";});$fiber->start(); // [Nothing happens]

Пока файбер стоит на паузе, нужно убрать ногу с тормоза вызвать извне метод resume().

$fiber = new Fiber(function() : void {   Fiber::suspend();   echo "I'm running a Fiber, yay!";});$fiber->start(); // [Nothing happened]$fiber->resume(); // I'm running a Fiber, yay!

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

Есть нюансы в том, как методы start(), suspend() и resume() принимают аргументы:

  • Метод start() передаёт аргументы вызываемому объекту и возвращает всё, что принимает метод suspend().
  • Метод suspend() возвращает любое значение, которое принял метод resume().
  • Метод resume() возвращает всё, что принято при следующем вызове suspend().

Это относительно упрощает взаимодействие между основным потоком и файбером:

  • resume() используется для помещения в файбер значений, принятых suspend(),
  • а suspend() используется для отправки значений, принятых resume().

Так будет гораздо проще понять пример из официальной документации:

$fiber = new Fiber(function (): void {    $value = Fiber::suspend('fiber');    echo "Value used to resume fiber: ", $value, "\n";});$value = $fiber->start();echo "Value from fiber suspending: ", $value, "\n";$fiber->resume('test');

Если выполнить этот код, то вы получите подобное:

Value from fiber suspending: fiberValue used to resume fiber: test

Скоро у нас будет свой полноценный веб-сервер


Посмотрим правде в глаза: в 99 % случаев PHP используется вместе с nginx/Apache, в основном из-за того, что этот язык не многопоточный. Сервер в PHP блокирующий, он используется только для каких-нибудь тестов или отображения информации на клиенте. Файберы могут помочь PHP эффективнее работать с сокетами, и тогда у нас будет что-нибудь наподобие WebSockets, серверных событий, групповых подключений к базе данных, даже HTTP/3, и всё это без необходимости компилировать расширения, писать хаки с помощью непредназначенных для этого функций, инкапсулировать PHP во внешнюю runtime-среду или прибегать к другим ухищрениям, создающим проблемы.

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

Вы не будете пользоваться файберами напрямую


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

Авторы некоторых высокоуровневых фреймворков (вроде Symfony, Laravel, CodeIgniter, CakePHP и прочих) возьмут паузу, чтобы выбрать подход к файберам и создать набор инструментов для работы с ними с точки зрения разработчика. А некоторые низкоуровневые фреймворки наподобие amphp и ReactPHP уже предлагают файберы в своих свежайших версиях.


Хотя это освободит вас от необходимости думать о файберах, а не о своих идеях, однако теперь нас ждёт всплеск конкуренции со всеми её достоинствами и недостатками.

По одному файберу за раз



Процитирую Аарона Пиотровски из PHP Internals Podcast #74:

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

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

При этом никаких каналов


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

Новичок на районе


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

names := make(chan string)go doFoo(names)go doBar(names)

С этой точки зрения Go далеко впереди PHP с его зачаточным распараллеливанием. Если вам нужна настоящая многопоточность, то пишите на Go, или даже на Rust, если вам нужно напрямую использовать процессорные потоки.

Это не означает, что PHP не может конкурировать с какими-либо моделями распараллеливания, но в своей основе это ещё синхронный язык в угоду удобству и простоте понимания. К примеру, Go страдает от чрезмерного plumbing. Если нужна настоящая модель распараллеливания, как в Go, то PHP придётся пересоздавать с нуля. Но зато это откроет много возможностей в мире информатики, уже охваченном многопоточностью.
Подробнее..

Перевод Парсим протобаф на скорости больше 2 Гбс. как я научился любить хвостовую рекурсию в C

12.05.2021 18:04:50 | Автор: admin


Отличную функцию недавно добавили в основную ветку компилятора Clang. С помощью атрибутов [[clang::musttail]] или __attribute__((musttail)) теперь можно получить гарантированные хвостовые (tail) вызовы в C, C++ и Objective-C.

int g(int);int f(int x) {    __attribute__((musttail)) return g(x);}

(Онлайн-компилятор)

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

Применение этого подхода в парсинге протокола сериализации дало поразительные результаты: мы смогли парсить на скорости свыше 2 Гб/с., более чем вдвое быстрее лучшего предыдущего решения. Это ускорение будет полезно во многих ситуациях, так что неверно будет ограничиваться одним лишь утверждением хвостовые вызовы == двукратное ускорение. Однако эти вызовы ключевой элемент, благодаря которому стал возможен такой прирост скорости.

Я расскажу, в чём достоинства хвостовых вызовов, как мы с их помощью парсим протокол и как можно распространить эту методику на интерпретаторы. Думаю, что благодаря ей интерпретаторы всех основных языков, написанных на С (Python, Ruby, PHP, Lua и т.д.), могут получить значительный прирост производительности. Главный недостаток связан с портируемостью: сегодня musttail является нестандартным расширением компилятора. И хотя я надеюсь, что оно приживется, однако пройдет некоторое время, прежде чем расширение распространится достаточно широко, чтобы C-компилятор вашей системы мог его поддерживать. При сборке вы можете пожертвовать эффективностью в обмен на портируемость, есть окажется, что musttail недоступно.

Основы хвостовых вызовов


Хвостовой вызов это вызов любой функции, находящейся в хвостовом положении, последнее действие перед тем, как функция вернёт результат. При оптимизации хвостовых вызовов компилятор компилирует для хвостового вызова инструкцию jmp, а не call. Тогда не выполняются служебные действия, которые в обычной ситуации позволяют вызываемой g() возвращать вызывающей f(), например, создание нового фрейма стека или передача адреса возврата. Вместо этого f() напрямую обращается к g(), как если бы та была частью неё самой, а g() возвращает результат напрямую тому, кто вызвал f(). Эта оптимизация безопасна, потому что фрейм стека f() больше не нужен после начала хвостового вызова, ведь стало невозможно обратиться к какой-либо локальной переменной f().

Пусть это и выглядит банально, но у такой оптимизации есть две важные особенности, открывающие новые возможности по написанию алгоритмов. Во-первых, при выполнении n последовательных хвостовых вызов стек памяти уменьшается с O(n) до O(1). Это важно потому, что что стек ограничен и переполнение может обрушить программу. Так что без этой оптимизации некоторые алгоритмы являются опасными. Во-вторых, jmp избавляет от избыточной производительности call, и в результате вызов функции становится таким же эффективным, как и любая другая ветка. Обе эти особенности позволяют использовать хвостовые вызовы в качестве эффективной альтернативы обычным итеративным структурам управления вроде for и while.

Эта идея вовсе не нова и восходит к 1977 году, когда Гай Стил (Guy Steele) написал целую работу, в которой утверждал, что вызовы процедур повышают чистоту архитектур по сравнению с GOTO, и что при этом оптимизация хвостовых вызовов позволяет не терять в скорости. Это была одна из Лямбда-работ, написанных в период с 1975 по 1980, в которых был сформулировано много идей, лёгших в основу Lisp и Scheme.

Оптимизация хвостовых вызовов даже для Clang не в новинку. Он и до этого мог оптимизировать их, как GCC и многие другие компиляторы. На самом деле атрибут musttail в этом примере совсем не меняет результат работы компилятора: Clang уже оптимизировал хвостовые вызовы под -O2.

Новое здесь это гарантия. Хотя компиляторы зачастую успешно оптимизируют хвостовые вызовы, однако это лучшее из возможного, и вы не можете на это полагаться. В частности, оптимизация наверняка не сработает в неоптимизированных сборках: онлайн-компилятор. В этом примере хвостовой вызов скомпилирован в call, так что мы вернулись к стеку размером O(n). Вот почему нам нужен musttail: пока у нас нет гарантии от компилятора, что наши хвостовые вызовы будут всегда оптимизированы во всех режимах сборки, писать алгоритмы с такими вызовами для итерации будет небезопасно. А придерживаться кода, который работает только при включённых оптимизациях, будет довольно жёстким ограничением.

Проблема с циклами интерпретатора


Компиляторы это невероятная технология, но они не совершенны. Майк Пол (Mike Pall), автор LuaJIT, решил написать интерпретатор LuaJIT 2.x на ассемблере, а не C, и он назвал это главным фактором, благодаря которому интерпретатор получился таким быстрым. Позднее Пол подробнее объяснил, почему С-компиляторам трудно даются главные циклы интерпретатора. Два главных тезиса:

  • Чем больше функция и чем сложнее и обширнее набор соединений её потока управления, тем сложнее распределителю регистров в компиляторе поддерживать в регистрах самые важные данные.
  • Когда в одной функции смешаны быстрые и медленные пути, наличие медленных путей снижает качество кода быстрых путей.

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

Вам может показаться странным сравнивать циклы интерпретатора с парсерами протокола сериализации. Однако их неожиданное для вас сходство определяется природой wire-формата протокола: это набор пар ключ-значение, в которых ключ содержит номер поля и его тип. Ключ работает как код операции в интерпретаторе: он говорит нам, какую операцию нужно выполнить для парсинга этого поля. Номера полей в протоколе могут идти в любом порядке, так что нужно быть готовым в любой момент перейти к любой части кода.

Логично будет писать такой парсер с помощью цикла while с вложенным выражением switch. Это был лучший подход для парсинга протокола сериализации в течение всего времени существования протокола. Вот, например, код парсинга из текущей С++-версии. Если представить поток управления графически, то получим такую схему:


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


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

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

Улучшаем циклы интерпретатора с помощью хвостовых вызовов


Описанные выше рассуждения это по большей части перефразированные наблюдения Пола об основных циклах интерпретатора. Но вместо того, чтобы кидаться в ассемблер, мы обнаружили, что адаптированная под хвостовые вызовы архитектура может дать нам необходимое управление для получения из С почти оптимального кода. Я работал над этим со своим коллегой Гербеном Ставенгой (Gerben Stavenga), который придумал большую часть архитектуры. Наш подход аналогичен архитектуре WebAssembly-интерпретатора wasm3, в котором этот паттерн назван метамашиной.

Мы поместили код нашего двухгигабитного парсера в upb, маленькую protobuf-библиотеку на C. Он полностью рабочий и проходит все тесты соответствия протоколу сериализации, но пока нигде не развёрнут, а архитектура не реализована в С++-версии протокола. Но когда в Clang появилось расширение musttail upb обновили для его использования), упал один из главных барьеров на пути к полному внедрению нашего быстрого парсера.

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

Код
#include <stdint.h>#include <stddef.h>#include <string.h>typedef void *upb_msg;struct upb_decstate;typedef struct upb_decstate upb_decstate;// The standard set of arguments passed to each parsing function.// Thanks to x86-64 calling conventions, these will be passed in registers.#define UPB_PARSE_PARAMS                                          \  upb_decstate *d, const char *ptr, upb_msg *msg, intptr_t table, \      uint64_t hasbits, uint64_t data#define UPB_PARSE_ARGS d, ptr, msg, table, hasbits, data#define UNLIKELY(x) __builtin_expect(x, 0)#define MUSTTAIL __attribute__((musttail))const char *fallback(UPB_PARSE_PARAMS);const char *dispatch(UPB_PARSE_PARAMS);// Code to parse a 4-byte fixed field that uses a 1-byte tag (field 1-15).const char *upb_pf32_1bt(UPB_PARSE_PARAMS) {  // Decode "data", which contains information about this field.  uint8_t hasbit_index = data >> 24;  size_t ofs = data >> 48;  if (UNLIKELY(data & 0xff)) {    // Wire type mismatch (the dispatch function xor's the expected wire type    // with the actual wire type, so data & 0xff == 0 indicates a match).    MUSTTAIL return fallback(UPB_PARSE_ARGS);  }  ptr += 1;  // Advance past tag.  // Store data to message.  hasbits |= 1ull << hasbit_index;  memcpy((char*)msg + ofs, ptr, 4);  ptr += 4;  // Advance past data.  // Call dispatch function, which will read the next tag and branch to the  // correct field parser function.  MUSTTAIL return dispatch(UPB_PARSE_ARGS);}


Для такой маленькой и простой функции Clang генерирует код, который практически невозможно превзойти:

upb_pf32_1bt:                           # @upb_pf32_1bt        mov     rax, r9        shr     rax, 24        bts     r8, rax        test    r9b, r9b        jne     .LBB0_1        mov     r10, r9        shr     r10, 48        mov     eax, dword ptr [rsi + 1]        mov     dword ptr [rdx + r10], eax        add     rsi, 5        jmp     dispatch                        # TAILCALL.LBB0_1:        jmp     fallback                        # TAILCALL

Обратите внимание, что здесь нет пролога и эпилога, нет вытеснения регистров, вообще не используется стек. Единственные выходы jmp из двух хвостовых вызовов, но для передачи параметров код не нужен, потому что аргументы уже лежат в правильных регистрах. Пожалуй, единственное возможное улучшение, которое мы здесь видим, заключается в условном переходе для хвостового вызова jne fallback вместо jne с последующим jmp.

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

Мы можем независимо оптимизировать каждую последовательность инструкций. И компилятор будет обрабатывать все последовательности тоже независимо, потому что они расположены в отдельных функциях (при необходимости можно предотвращать инлайнинг с помощью noinline). Так мы решаем описанную выше проблему, при которой код из fallback-путей ухудшает качество кода быстрых путей. Если поместить медленные пути в полностью отдельные функции, то можно гарантировать стабильность скорости быстрых путей. Красивый ассемблер остаётся неизменным, на него не влияют никакие изменения в других частях парсера.

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

#define PARAMS unsigned RA, void *table, unsigned inst, \               int *op_p, double *consts, double *regs#define ARGS RA, table, inst, op_p, consts, regstypedef void (*op_func)(PARAMS);void fallback(PARAMS);#define UNLIKELY(x) __builtin_expect(x, 0)#define MUSTTAIL __attribute__((musttail))void ADDVN(PARAMS) {    op_func *op_table = table;    unsigned RC = inst & 0xff;    unsigned RB = (inst >> 8) & 0xff;    unsigned type;    memcpy(&type, (char*)&regs[RB] + 4, 4);    if (UNLIKELY(type > -13)) {        return fallback(ARGS);    }    regs[RA] += consts[RC];    inst = *op_p++;    unsigned op = inst & 0xff;    RA = (inst >> 8) & 0xff;    inst >>= 16;    MUSTTAIL return op_table[op](ARGS);}

Получившийся ассемблер:

ADDVN:                                  # @ADDVN        movzx   eax, dh        cmp     dword ptr [r9 + 8*rax + 4], -12        jae     .LBB0_1        movzx   eax, dl        movsd   xmm0, qword ptr [r8 + 8*rax]    # xmm0 = mem[0],zero        mov     eax, edi        addsd   xmm0, qword ptr [r9 + 8*rax]        movsd   qword ptr [r9 + 8*rax], xmm0        mov     edx, dword ptr [rcx]        add     rcx, 4        movzx   eax, dl        movzx   edi, dh        shr     edx, 16        mov     rax, qword ptr [rsi + 8*rax]        jmp     rax                             # TAILCALL.LBB0_1:        jmp     fallback

Я вижу здесь лишь одну возможность улучшения не считая вышеупомянутой jne fallback: по какой-то причине компилятор не хочет генерировать jmp qword ptr [rsi + 8*rax], вместо этого он загружает в rax и затем выполняет jmp rax. Это небольшие проблемы генерирования кода, которые, надеюсь, в Clang скоро исправят без излишних затруднений.

Ограничения


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

#define PARAMS unsigned RA, void *table, unsigned inst, \               int *op_p, double *consts, double *regs#define ARGS RA, table, inst, op_p, consts, regstypedef void (*op_func)(PARAMS);void fallback(PARAMS);#define UNLIKELY(x) __builtin_expect(x, 0)#define MUSTTAIL __attribute__((musttail))void ADDVN(PARAMS) {    op_func *op_table = table;    unsigned RC = inst & 0xff;    unsigned RB = (inst >> 8) & 0xff;    unsigned type;    memcpy(&type, (char*)&regs[RB] + 4, 4);    if (UNLIKELY(type > -13)) {        // When we leave off "return", things get real bad.        fallback(ARGS);    }    regs[RA] += consts[RC];    inst = *op_p++;    unsigned op = inst & 0xff;    RA = (inst >> 8) & 0xff;    inst >>= 16;    MUSTTAIL return op_table[op](ARGS);}

Неожиданно получаем вот это
ADDVN:                                  # @ADDVN        push    rbp        push    r15        push    r14        push    r13        push    r12        push    rbx        push    rax        mov     r15, r9        mov     r14, r8        mov     rbx, rcx        mov     r12, rsi        mov     ebp, edi        movzx   eax, dh        cmp     dword ptr [r9 + 8*rax + 4], -12        jae     .LBB0_1.LBB0_2:        movzx   eax, dl        movsd   xmm0, qword ptr [r14 + 8*rax]   # xmm0 = mem[0],zero        mov     eax, ebp        addsd   xmm0, qword ptr [r15 + 8*rax]        movsd   qword ptr [r15 + 8*rax], xmm0        mov     edx, dword ptr [rbx]        add     rbx, 4        movzx   eax, dl        movzx   edi, dh        shr     edx, 16        mov     rax, qword ptr [r12 + 8*rax]        mov     rsi, r12        mov     rcx, rbx        mov     r8, r14        mov     r9, r15        add     rsp, 8        pop     rbx        pop     r12        pop     r13        pop     r14        pop     r15        pop     rbp        jmp     rax                             # TAILCALL.LBB0_1:        mov     edi, ebp        mov     rsi, r12        mov     r13d, edx        mov     rcx, rbx        mov     r8, r14        mov     r9, r15        call    fallback        mov     edx, r13d        jmp     .LBB0_2


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

В идеале это проблему можно решить с помощью добавления __attribute__((preserve_most)) в fallback-функции с последующим нормальным вызовом, а не хвостовым. Атрибут preserve_most возлагает на вызываемого ответственность за сохранение почти всех регистров. Это позволяет перекладывать задачу вытеснения регистров на fallback-функции, если такое понадобится. Мы экспериментировали с этим атрибутом, но столкнулись с таинственными проблемами, решить которые не смогли. Возможно, мы где-то допустили ошибку, вернёмся к этому в будущем.

Главное ограничение заключается в том, что musttail не является портируемым. Очень надеюсь, что атрибут приживётся, его внедрят в GCC, Visual C++ и других компиляторах, а однажды даже стандартизируют. Но это случится не скоро, а что нам делать сейчас?

Когда расширение musttail недоступно, нужно для каждой теоретической итерации цикла выполнить хотя бы один верный return без хвостового вызова. Мы ещё не реализовали такой fallback в библиотеке upb, но думаю, что она превратится в макрос, который в зависимости от доступности musttail будет либо делать хвостовые вызовы, либо просто возвращать.
Подробнее..

Рабочее место в кафе, улучшение фотографий и команды для хакатонов выпускные работы наших студентов

10.06.2021 20:10:36 | Автор: admin

2 июня прошли защиты выпускных проектов у студентов образовательных проектов Mail.ru Group. Ребята шли к этому моменту два года, и последний семестр был почти полностью посвящен реализации этих идей. Свои работы в защищали 8 команд из Технопарка (МГТУ им. Н. Э. Баумана) и 3 из Технополиса (СПбПУ им. Петра Великого). Все они реализовали разные проекты, объединенные одной целью улучшить жизнь людей с помощью технологий.

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

TeamUP Online как найти лучшую команду


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

Существующие решения больше похожи на маркетплейсы для хакатонов. Они решают проблему для организаторов: тот покупает лицензию, и быстро создает мероприятие. Но для участника подобного решения нет. Команда Фиксики решила поставить интересы хакатонщиков в приоритет. В их проекте TeamUp человек может просто указать хакатон, в котором хочет попробовать свои силы, и быстро подобрать себе команду по духу.



Поиск здесь работает, как в Тиндере: если кто-то понравился, можете послать запрос на то, чтобы объединить свои силы. Вы видите его навыки, интересы, предыдущие места работы. Даже Гитхаб и профили в соцсетях (если он их указал). Там же, на сервисе посмотреть на состав сформированной команды и провести голосование за тимлида. Тимлид может управлять входящими заявками, удалять людей или менять название команды.





Студенты Технопарка использовали современный и общепринятый стек технологий React Golang. В дизайне отталкивались от Material-UI и VKUI, и сделали крутую мобильную верстку (отбирать заявки в команду можно даже на бегу).



Команда проекта: Сергей Куклин, Михаил Балицкий (miksti.me) и Олег Елизаров.

Умные визуальные эффекты к фотографиям


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

У ребят из Технополиса появилась идея: почему бы не сделать систему, способную создавать эти эффекты на основе метаинформации. А отсюда увеличить активность пользователей сервиса (в данном случае Одноклассников). Всем ведь интересно посмотреть на фотографии с красивыми и необычными эффектами. Откуда брать метаинформацию? Во-первых, из самого фото. Например, из EXIF или XMP. Во-вторых, её можно генерировать, используя методы машинного обучения.

Фокусировка на главном объекте и размытие фона достигается за счет карты глубины, Depth Map (той же технологии, благодаря которой работает Face ID у iPhone). Depth Map также помогает достичь эффекта параллакса и эффектов dolly zoom in/zoom out, когда главный объект остается статичным, а остальные приближаются или удаляются.


Depth Map получают или из фото, если камера смартфона поддерживает построение карты глубины. Или с помощью ML (в итоге команда остановилась на MidasNet). Всё это происходит со стороны сервера, так что девайс пользователя не нагружен.


Основная программа Технополиса Java-разработчик высоконагруженных приложений, поэтому под капотом у проекта Javа, а также Android, OpenGL и другие вспомогательные технологии.

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

Команда проекта: Елисей Василевский, Илья Гусаров, Владислав Зыбкин.

Сервис Рабочее место


Многие из нас устали от работы из дома. Хочется что-то поменять. Или просто сделать так, чтобы домочадцы не отвлекали. Решением может показаться кафе. Но и тут не всё просто. Бывает, пришли, открыли ноутбук, уже хотите хорошо погрузиться в проект а тут оказывается, что розетки по соседству нет. И Wi-Fi запаролен. Или очень слаб. Вы просто зря потеряли ещё полчаса на дорогу.



Команда студентов Технопарка придумала решение: сервис Рабочее место. В приложении собраны все кафе и рестораны вашего города, где можно спокойно прийти и поработать. С информацией о времени работы, адресе, освещенности, Wi-Fi, розетках. Можете по фото проверить дизайн. А если взять подписку вам там даже бесплатно нальют чай/кофе или сделают скидку на заказ.



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

Для разработки фронтенда использовались React, MobX, TypeScript, SCSS. Бэкенд Django, Docker, PostgreSQL, Grafana, Nginx.



Состав команды: Александр Аверкиев, Дмитрий Болдин, Сергей Петренко, Артур Потапчук.

Проекты, которые показали другие студенты:

Технопарк:

  • Телеграм-бот ассистент для календаря Mail.ru
  • Ежедневник для самозанятых
  • Конструктор системы лояльности для кафе
  • Мобильное приложение для взаимодействия клиента с кафе/рестораном
  • Платформа для ведения спортивных чемпионатов
  • Приложение для заказа еды из столовых МГТУ.

Технополис:

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

Полное видео с этой защиты можно посмотреть у нас во ВКонтакте.

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

Если вы уже давно не студент или не хотите ждать нового набора образовательные проекты Mail.ru Group делятся ИT-знаниями безвозмездно на ютуб-канале Технострим. Смотрите, подписывайтесь, ставьте лайки!
Подробнее..

Геопространственное моделирование с применением методов машинного обучения

18.06.2021 18:20:53 | Автор: admin


Всем привет! Меня зовут Константин Измайлов, я руководитель направления Data Science в Delivery Club. Мы работаем над многочисленными интересными и сложными задачами: от формирования классических аналитических отчетов до построения рекомендательных моделей в ленте приложения.

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

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

Статья написана по мотивам выступления с Евгением Макиным на конференции Highload++ Весна 2021. Для тех, кто любит видео, ищите его в конце статьи.

Бизнес-модель работы Delivery Club


Бизнес-модель Delivery Club состоит из двух частей:

  • ДДК (доставка Деливери Клаб): мы передаем заказ в ресторан и доставляем его клиенту, то есть ресторану остается только приготовить заказ к определенному времени, когда придет курьер.
  • МП (маркетплейс): мы передаем заказ в ресторан, а он своими силами доставляет заказ в пределах своей согласованной зоны.

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

При этом построить зону доставки это полбеды. Ведь география ДДК огромна и охватывает более 170 городов России с тысячами ресторанов в каждом из них, и у каждого ресторана свои индивидуальные особенности, которые необходимо учитывать при построении эффективного сервиса доставки.

Рисуем зону доставки ресторана


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



Как процесс выглядел раньше


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

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



Стоит упомянуть и про SLA (Service Level Agreement соглашение о максимальной длительности отрисовки зоны доставки для одного партнера): онбординг партнера или подготовка его зоны для внедрения в нашу платформу составляли порядка 40 минут для одного заведения. Представьте, что к вам подключилась городская сеть с сотней ресторанов, а если это ещё и жаркий сезон, например, после проведения рекламной акции Вот наглядное доказательство неэффективности ручной отрисовки:

$T = R * SLA = 100 * 40\ минут =\ \sim 67\ часов\ ручной\ работы$


где $T$ время, которое будет затрачено на отрисовку всех зон доставки партнера,
$R$ количество ресторанов,
$SLA$ время на отрисовку одной зоны.

Проблемы ручной отрисовки зон:


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

Baseline


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

При этом оставались недостатки:

  • артефакты в зонах доставки (стандартный случай с переходом через реку);
  • единообразный подход к партнерам (не учитываются индивидуальные KPI партнеров).

Через Delivery Club ежедневно проходят сотни тысяч заказов, и артефакты зон доставки могли дать большую нагрузку на колл-центр.



Пробовали алгоритмы на основе Convex Hull и Alpha Shape, с их помощью можно было бы устранить артефакты. Например, используя границы водных массивов как опорные точки для построения форм удавалось обходить реки. Однако всё равно отсутствовал индивидуальный подход к партнерам.

Преимущества технологии H3


Мы стремились прийти к универсальному решению. Каких-то простых и популярных подходов найти не удалось, поэтому мы сосредоточились на разработке собственного алгоритма. А за основу взяли технологию H3 компании Uber.


Источник: eng.uber.com/h3

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

Также стоит отметить, что:

  1. Существует хорошая библиотека для работы с H3, которую и выбрала наша команда в качестве основного инструмента. Библиотека поддерживает многие языки программирования (Python, Go и другие), в которых уже реализованы основные функции для работы с гексагонами.
  2. Наша реляционная аналитическая база Postgres поддерживает работу с нативными функциями H3.
  3. При использовании гексагональной сетки благодаря ряду алгоритмов, работающих с индексами, можно очень быстро получить точную информацию о признаках в соседних ячейках, например, определить вхождение точки в гексагон.
  4. Преимуществом гексагонов является возможность хранить информацию о признаках не только в конкретных точках, но и в целых областях.

Алгоритм построения зон доставки


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


  2. Убираем точки-выбросы. Анализируем все постройки и сразу отбрасываем те, которые нас не интересуют. Например, какие-то мелкие нежилые объекты. Далее с помощью DBSCAN формируем кластеры точек и отбрасываем те, которые для нас не являются важными: например, если кластер находится далеко от ресторана или нам экономически невыгодно доставлять туда.


  3. Далее на основе очищенного набора точек применяем триангуляцию Делоне.


  4. Создаем сетку гексагонов H3.


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


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



    В текущей версии алгоритма мы используем следующие функции ошибок:

    • минимизацию времени доставки при фиксированном покрытии;
    • максимизацию охвата пользователей при фиксированном времени доставки.

    Пример функции ошибки для минимизации времени доставки:

    ${L_{min}}_{time}\;=\;min(\sum_{i=1}^n\;({t_{rest}}_i)/n),$


    где $L_{min_ {time}}$ функция ошибки минимизации времени доставки с фиксированным покрытием,
    $t_{rest_ {i}}$ время от ресторана i до клиента,
    $n$ количество клиентов в зоне доставки.

  7. Далее строим временной градиент в получившихся зонах (с очищенными выбросами) и с заранее определенными интервалами (например, по 10-минутным отрезкам пешего пути).



Зона построена, теперь очередь оптимизации бизнес-метрики. В алгоритме используются оптимизации покрытия клиентов с фиксированием времени доставки и оптимизация времени доставки с фиксированным покрытием.

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

Внедрение


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

Здесь мы столкнулись с тем, что очень трудно было менять сложившиеся бизнес-процессы, которые были налажены годами. Операторы очень не хотели передавать свои KPI машине.

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

Но тут пришел COVID-19

  • тысячи ресторанов закрывают свои филиалы;
  • увеличиваются зоны доставки;
  • сокращается меню и меняется целевая аудитория;
  • меняется формат работы ресторанов;
  • поменялось еще и географическое распределение заказов: до начала пандемии много заказов приходило из офисов, а во время пандемии все сидели дома и заказывали оттуда.

Всё это отчасти помогло нам убедить партнёров активнее тестировать и переходить на алгоритм автоматического построения зон доставки. В итоге, за первую неделю после начала пандемии и введения ограничения для ресторанов порядка 50% наших партнеров перешло на новые зоны доставки, которые уже были построены автоматически, а в течение следующих 3 недель уже почти не осталось партнеров с зонами, построенными вручную.

Оценка


После решения всех горящих проблем нам нужно было немного отдышаться и понять, что мы вообще наделали. Для этого воспользовались A/B-тестом, а точнее его вариацией switch-back. Мы сравнивали зоны ресторанов с одинаковыми входными параметрами, оценивали GMV и время доставки, где в качестве контроля у нас были простые автоматически отрисованные зоны в виде окружностей и прямоугольников, либо зоны, отрисованные операторами вручную.



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

А время, затрачиваемое на построение зон для партнера из примера выше, теперь выглядит более оптимистично:

$T = 100 * 3,6\ секунды =\ \sim 6\ минут$


Ускорение в 670 раз!

Текущая ситуация и планы


Сервис работает в production. Зоны автоматически строятся по кнопке. Появился более гибкий инструмент для работы со стоимостью доставки для клиентов в зависимости от их удаленности от ресторана. 99,9% ресторанов (изредка ещё нужно ручное вмешательство) перешли на алгоритмические зоны, а наш алгоритм поспособствовал переходу бэкенда на H3.

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

Работа с географией и зонами открыла нам дорогу к новым проектам: рекомендации для размещения, гранулярная работа с зонами доставки, более точный прогноз длительности доставки для клиента, ценообразование.

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

Всем спасибо!

Подробнее..

Долголетие технологии и рынок. Будущее, которое мы заслужили, эпизод 1

12.05.2021 12:08:48 | Автор: admin


На днях мы запустили новый подкаст Будущее, которое мы заслужили. Первый выпуск посвятили технологиям долголетия. И для тех, кому удобнее читать, а не слушать, мы подготовили рассказ по мотивам беседы наших гостей: Максима Холина и Петра Федичева, сооснователей компании Gero, разрабатывающей лекарства для системной борьбы со старением с помощью искусственного интеллекта.

Для чего замедлять старение?


Пётр Федичев закончил Физтех и продолжил научные исследования в Голландии и Австрии в области теоретической физики. С 2004 года перешел в биотехнологические проекты. Вскоре Петру и его коллегам стало очевидно, что проблема современной медицины не в том, что фарминдустрия не может подобрать лекарственную молекулу против конкретной биологической мишени. А в том, что биологи и врачи зачастую просто не знают, против каких белков, генов и мишеней нужно сделать лекарство, чтобы вылечить какую-либо болезнь. Размышляя о том, как решить эту проблему с помощью своих знаний и опыта, учёные пришли к выводу, что единственная болезнь, которую сегодня можно решить с точки зрения физики и математики это старение.

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

Идея разрабатывать препарат именно против старения, а не какой-то болезни, довольно необычна для фармацевтической и медицинской промышленности. Обычно вам сначала нужно заболеть, чтобы вас начали лечить. А от старения нужно начинать лечить до того, как вы заболели. Привлекать деньги под такие проекты до недавнего времени было тяжело, но сейчас находить инвесторов стало проще. Население во многих странах стареет. Например, через несколько лет в Китае будет 250 млн человек старше 60. Очевидно, что их трудоспособность будет ограничена, они будут страдать от целого букета возрастных заболеваний. А из-за того, что в Китае раньше был разрешён только один ребёнок на семью, сейчас это ограничение уже снято, ситуация со старением населения начнёт улучшаться только лет через 20. В развитых странах похожие проблемы: если количество стариков будет увеличиваться в том же темпе, что и сегодня, да ещё на фоне постоянного снижения населения, то нагрузка на пенсионную и медицинскую систему скоро станет непомерной.

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

Голые землекопы


Сегодня в технологии замедления старения активно инвестируют многие компании, в том числе технологические гиганты. Google создал компанию Calico, которая провела эксперименты с 6000 голых землекопов это маленькие роющие грызуны, живущие под землёй. Они размером чуть больше мыши, но если мыши живут 1,5 года, то голые землекопы около 30, и на протяжении всей жизни у них не наблюдается видимых возрастных изменений. Это животные с развитой формой социального устройства. У них интересная реакция на стресс: если он очень сильный, то землекопы выбирают одного из них и убивают, чтобы расслабиться. Ведь должен же кто-то ответить за безобразие. Во многом похоже на реакцию на страх в человеческом обществе.

Ученых Gero вдохновляют примеры голого землекопа и других нестареющих животных (обладающих так называемым пренебрежимым старением). Отсутствие видимых признаков старения у значительного (и постоянно растущего) числа видов заставляет думать о том, что старение не закон природы, природа уже сделала многих животных нестареющими. Еще интереснее то, что выключение старения произошло у разных животных разными способами, что дает надежду и нам, людям.

У человека риск смерти удваивается каждые 8 лет начиная примерно с 40 лет. Если бы наше здоровье можно было сохранить на уровне 30-40 лет, то ожидаемая продолжительность жизни была бы 300-500 лет.Если вам сейчас 30 лет, то вы проживете, скорее всего, ещё 40. Если вам 70 лет, то вы проживете, вероятно, ещё 10, если вам 80 лет, то впереди у вас считанные годы. То есть ожидаемая продолжительность жизни постоянно сокращается. А у землекопа в любом возрасте по состоянию его организма впереди 30 лет жизни. Он не стареет, то есть его организм не испытывает никакого функционального ухудшения своего состояния.

Кровь молодых и мёртвых


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

Англия страна запасливая. Там есть и банк замороженной крови уже умерших доноров, которые сдавали кровь несколько раз в течение 15 лет. Учёные из Gero исследовали содержание более 1000 белков в каждом образце крови и с помощью машинного обучения выявили те белки, которые оказались причиной старения доноров. Эти белки ученые с помощью антител заблокировали у мышей, и впервые в мире после одной инъекции получили эффект системного омоложения. В результате экспериментов с очень старыми животными удалось добиться задержки смертности, а также улучшения состояния животного, определяемого при помощи функциональных, молекулярных и клеточных признаков старения. Специалисты надеются, что лекарства, разработанные против предложенной мишени могут стать сильным средством против старения у человека.

Анализ данных


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

Оценку биологического возраста, т.е. риска смертности от всех причин, можно оценить даже по спонтанной двигательной активности пользователя смартфона или другого носимого устройства. В 2021 году компания Gero сделала бесплатное мобильное приложение GeroSense, которое может скачать и использовать любой человек, а также выпустило API для индустрии велнеса, здорового питания, страховых и любых других игроков, заинтересованных в мониторинге изменений здоровья пользователя в ответ на изменения образа жизни или медицинских интервенций. С появлением подобных технологий, индустрия ЗОЖ должна трансформироваться и предложить людям продукты с доказанной эффективностью, включая средства персонализации рекомендаций по улучшению его здоровья и продаваемых ему продуктов и услуг.

Сейчас для того чтобы что-то продавать в велнесе закон не требует клинических испытаний, подтверждающих положительное влияние на здоровье потребителя, а сами клинические испытания дороги и во многих случаях экономически не оправданы из-за невозможности защитить патентом продаваемые продукты. Многие ранее модные опции, типа мультивитаминов, оказались совсем не всем и не так полезны, как хотелось бы представить их производителям. Кроме того, все люди разные. Что-то, что поможет одному, причинит вред второму и будет бесполезной покупкой и тратой времени и сил для третьего. В итоге потребитель вынужден полагаться не на объективные данные, а на рекламу, моду и удачу при выборе именно для него. Шансы угадать не велики. GeroSense призван изменить эту ситуацию, дав всем заинтересованным сторонам возможность неинвазивно получить обратную связь о здоровье и полностью изменить рынок.

Любой стресс повышает биологический возраст. При снятии стресса, если человек здоров, маркер биологического возраста через 2-3 месяца возвращается к норме, а сама скорость возврата в норму после пережитого стресса выражаясь по-научному резилиенс является чрезвычайно важным показателем состояния здоровья и биомаркером старения человека

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

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

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

Возраст и ЗОЖ


Какой возраст является границей, после которой никакой спорт и полезное питание уже не помогут? Условно говоря, до 25 лет человек ведёт здоровый образ жизни, чтобы лучше выглядеть, а после 35 чтобы не получить хроническую болезнь. Многие возмутятся моими словами, но пока у тебя нет хронической болезни, то при отказе от курения твои маркеры функциональные, молекулярные, клеточные через несколько месяцев придут в норму. Об этом хорошо известно страховым компаниям. То же самое касается и спорта: пока им занимаешься, маркеры немного изменятся, как только бросишь они скоро вернутся к прежним значениям. После 35 лет ищите либо хорошего доктора, либо хороший спортзал. Спорт позволяет отсрочить возникновение хронических заболеваний. 10 раз сходить в спортзал, чтобы продлить жизнь на 5 лет, не выйдет. Спортом нужно заниматься постоянно, иначе эффект исчезнет.

В развитых странах, на первое место по смертности вместо болезни сердца, как было раньше, сейчас выходит онкология. К слову, Москва очень похожа по структуре хронических заболеваний и смертности на развитые страны. Возраст первого инвалидизирующего заболевания, от которого качество жизни меняется травматически, перешагнул за 65 лет. И если завтра появится таблетка, которая вылечит все виды рака за один день, то сначала смертность снизится, но продолжительность жизни вырастет всего на 2-3 года. Потому что те, кто сейчас умирают от рака, умрут через несколько лет от другой хронической болезни. Кстати, если при появлении рака есть шансы, выживаемость превысила 50 %, то от деменции лечения нет вообще никакого. Все экспериментальные лекарства провалились при клинических исследованиях. Поэтому, если мы не остановим старение, то вылечив сердечно-сосудистые заболевания и онкологию мы получим эпидемию деменции в районе 70 лет, что приведёт к чудовищным расходам на лечение и уход.

Если вам 70 лет и вы случайно еще здоровы, то правильной стратегией будет участие в любом клиническом исследовании любого препарата от старения. Информация о клинических исследованиях доступна. Если вам 20 лет, то достаточно велика вероятность, что в течение последующих 20 лет будет создано эффективное лекарство от старения. Интересная дилемма возникает у тех людей, которым сейчас около 50 лет. Те интервенции, которые сегодня в стадии клинических исследований, будут доступны, наверное, лет через 5, но сперва их будут применять к очень больным людям. Еще 5 лет уйдёт на то, чтобы врачи освоились с новыми лекарствами, поняли, как их применять, и начали назначать тем, кому сегодня 50 и кто условно здоров. И если вы потеряете здоровье в течение ближайших 10 лет, то вы уже не успеете воспользоваться результатами революции в долголетии. Те, кому сейчас 40, у кого есть 15 лет до возникновения первого хронического заболевания, могут относительно расслабиться.

Приведу пример. У бывшего американского президента Джимми Картера недавно в возрасте 90 лет начался рак кожи, в мозге возникли метастазы. 10-20 лет назад это был бы приговор. Картеру провели экспериментальную терапию, и он полностью излечился. Причём ему ещё повезло, что он заполучил рак в 90, а не в 70-80, как другие. За это время разработали технологии, которые позволили излечить Картера, который, возможно, проживёт ещё лет 10.

Никогда раньше эффект от ЗОЖ не был так силён. Он сам по себе невелик, 5-10 лет жизни, но с учетом того, как быстро развиваются технологии, прирост может вырасти экспоненциально за счет появления новых медицинских технологий. 5 лет на авокадо и сельдерее, и спортзал вместо картошки фри и бара и есть шанс, что ты сохранишь здоровье до появления прорывных технологий долголетия. В этом году опубликованы результаты эксперимента: в результате полуторалетней интервенции применения комбинации экспериментальных препаратов удалось снизить биологический возраст испытуемых на 1,5 года.

Перенаселение и социум


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

В 1970-е прогнозировали, что к 2025 году людей будет гораздо больше, чем в оказалось в действительности. Рост населения замедлился. Вместо взрывного, экспоненциального роста, популяция людей приближается к стабильному уровню. Сегодня считается, что этот уровень будет около 12 млрд человек.

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

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

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

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

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

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

Как попасть в отрасль?


Правительства всех стран заинтересованы в решении проблем хронических заболеваний и старения. Например, в Сингапуре с населением около 6 млн человек директор клинического центра с виноватым видом говорил мне: Понимаете, у нас оцифровано пока только 2 млн человек, извините, пожалуйста, но к концу года будут оцифрованы все. Кроме того, продукты на пересечении биотеха, ИТ и медицины позволят создавать огромную добавленную стоимость, что только увеличивает привлекательность этой отрасли как для инвесторов, так и для тех, кто подумывает о смене рода деятельности. Я уверен, что шести-семизначные зарплаты, опционы и доли будущих биотех-компаний доступны специалистам по математике, обработке и анализу данных, машинному обучению. Такие люди нужны всем участникам этого рынка. Если же у вас нет подобного образования, то в качестве первого шага можно пойти в онлайн-школу GeekBrains, которая недавно запустила курс по большим данным в медицине.

Прогноз на будущее


По мнению Петра, в течение 20 лет с вероятностью больше 80 % удастся достигнуть Longevity escape velocity ситуации, когда биотехнологии смогут добавлять людям в среднем больше лет жизни, чем люди будут терять за то же время. К чему это приведет? Ответ на этот вопрос выходит за рамки инженерной дискуссии. Учитывая достижения ИТ, мы можем оказаться в цифровом концлагере, в котором будем жить не 50 лет, а 500. Но хочется надеяться, что жить мы будем не только долго, но и весело.

Интересные ссылки


Подробнее..

Перевод Внутренности Linux как procselfmem пишет в недоступную для записи память

26.05.2021 12:10:08 | Автор: admin

Странная причудливость псевдофайла /proc/*/mem заключается в его пробивной семантике. Операции записи через этот файл будут успешными даже если целевая виртуальная память помечена как недоступная для записи. Это сделано намеренно, и такое поведение активно используется проектами вроде компилятора Julia JIT или отладчика rr.

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

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

Патчим libc с помощью /proc/self/mem


Как выглядит эта пробивная семантика? Рассмотрим код:

#include <fstream>#include <iostream>#include <sys/mman.h>/* Write @len bytes at @ptr to @addr in this address space using * /proc/self/mem. */void memwrite(void *addr, char *ptr, size_t len) {  std::ofstream ff("/proc/self/mem");  ff.seekp(reinterpret_cast<size_t>(addr));  ff.write(ptr, len);  ff.flush();}int main(int argc, char **argv) {  // Map an unwritable page. (read-only)  auto mymap =      (int *)mmap(NULL, 0x9000,                  PROT_READ, // <<<<<<<<<<<<<<<<<<<<< READ ONLY <<<<<<<<                  MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);  if (mymap == MAP_FAILED) {    std::cout << "FAILED\n";    return 1;  }  std::cout << "Allocated PROT_READ only memory: " << mymap << "\n";  getchar();  // Try to write to the unwritable page.  memwrite(mymap, "\x40\x41\x41\x41", 4);  std::cout << "did mymap[0] = 0x41414140 via proc self mem..";  getchar();  std::cout << "mymap[0] = 0x" << std::hex << mymap[0] << "\n";  getchar();  // Try to writ to the text segment (executable code) of libc.  auto getchar_ptr = (char *)getchar;  memwrite(getchar_ptr, "\xcc", 1);  // Run the libc function whose code we modified. If the write worked,  // we will get a SIGTRAP when the 0xcc executes.  getchar();}

Здесь /proc/self/mem используется для записи в две недоступные для записи страницы памяти. Первая содержит сам код, а вторая принадлежит libc (функции getchar). Последняя часть вызывает больший интерес: код записывает байт 0xcc (точка прерывания в приложениях под x86-64), который в случае своего исполнения заставит ядро предоставить нашему процессу SIGTRAP. Это буквально меняет исполняемый код libc. И если при следующем вызове getchar мы получим SIGTRAP, то будем знать, что запись была успешной.

Вот как это выглядит при запуске программы:


Работает! Посередине выводятся выражения, которые доказывают, что значение 0x41414140 было успешно записано и считано из памяти. Последний вывод показывает, что после патчинга наш процесс получил SIGTRAP в результате нашего вызова getchar.

На видео:


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

Оборудование


На платформе x86-64 есть две настройки процессора, которые управляют возможностью ядра обращаться к памяти. Они применяются модулем управления памятью (MMU).

Первая настройка бит защиты от записи, Write Protect bit (CR0.WP). Из руководства Intel (том 3, раздел 2.5) мы знаем:

Защита от записи (16-й бит CR0). Если задана, он не даёт процедурам уровня супервизора записывать в защищённые от записи страницы. Если бит пуст, то процедуры уровня супервизора могут записывать в защищённые от записи страницы (вне зависимости от настроек бита U/S; см. раздел 4.1.3 и 4.6).

Это не позволяет ядру записывать в защищённые от записи страницы, что, естественно, по умолчанию разрешено.

Вторая настройка предотвращение доступа в режиме супервизора, Supervisor Mode Access Prevention (SMAP) (CR4.SMAP). Полное описание, приведённое в томе 3, в разделе 4.6, многословное. Если вкратце, то SMAP полностью лишает ядро способности записывать или читать из памяти пользовательского пространства. Это предотвращает эксплойты, которые наполняют пользовательское пространство зловредными данными, которые ядро должно прочитать в ходе исполнения.

Если код ядра использует для доступа к пользовательскому пространству только одобренные каналы (copy_to_user и т.д.), то SMAP можно смело игнорировать, эти функции автоматически задействуют его до и после обращения к памяти. А что насчёт защиты от записи?

Если CR0.WP не задан, то реализация /proc/*/mem в ядре действительно сможет бесцеремонно записывать в защищённую от записи память пользовательского пространства.

Однако CR0.WP задаётся при загрузке и обычно живёт в течение всего времени работы систем. В этом случае при попытке записи будет выдаваться сбой страницы. Это, скорее, инструмент для копирования при записи (Copy-on-Write), чем средство защиты, поэтому не накладывает на ядро никаких реальных ограничений. Иными словами, требуется неудобная обработка сбоев, которая не обязательна при заданном бите.

Давайте теперь разберёмся с реализацией.

Как работает /proc/*/mem


/proc/*/mem реализован в fs/proc/base.c.

Структура file_operations содержит функции обработчика, а функция mem_rw() полностью поддерживает обработчик записи. mem_rw() использует для операций записи access_remote_vm(). А access_remote_vm() делает вот что:

  • Вызывает get_user_pages_remote(), чтобы найти физический фрейм, соответствующий целевому виртуальному адресу.
  • Вызывает kmap(), чтобы пометить этот фрейм доступный для записи в виртуальном адресном пространстве ядра.
  • Вызывает copy_to_user_page() для финального выполнения операций записи.

Эта реализация полностью обходит вопрос о способности ядра записывать в незаписываемую память пользовательского пространства! Контроль ядра над подсистемой виртуальной памяти позволяет полностью обойти MMU, позволяя ядру просто записывать в свое собственное адресное пространство, доступное для записи. Так что обсуждения CR0.WP становится неактуальным.

Рассмотрим каждый из этапов:

get_user_pages_remote()

Для обхода MMU ядру нужно, чтобы в приложении было вручную выполнено то, что MMU делает аппаратно. Сначала необходимо преобразовать целевой виртуальный адрес в физический. Этим занимается семейство функций get_user_pages(). Они проходят по таблицам страниц и ищут фреймы физической памяти, которые соответствуют заданному диапазону виртуальных адресов.

Вызывающий предоставляет контекст и с помощью флагов меняет поведение get_user_pages(). Особенно интересен флаг FOLL_FORCE, который передаётся mem_rw(). Флаг запускает check_vma_flags (логику проверки доступа в get_user_pages()), чтобы игнорировать запись в незаписываемые страницы и продолжить поиск. Пробивная семантика полностью относится к FOLL_FORCE (комментарии мои):

static int check_vma_flags(struct vm_area_struct *vma, unsigned long gup_flags){        [...]        if (write) { // If performing a write..                if (!(vm_flags & VM_WRITE)) { // And the page is unwritable..                        if (!(gup_flags & FOLL_FORCE)) // *Unless* FOLL_FORCE..                                return -EFAULT; // Return an error        [...]        return 0; // Otherwise, proceed with lookup}

get_user_pages() стоже соблюдает семантику копирования при записи (CoW). Если определяется запись в таблицу незаписываемой страницы, то эмулируется сбой страницы с помощью вызова handle_mm_fault, основного обработчика страничных ошибок. Это запускает соответствующую подпрограмму обработки копирования при записи посредством do_wp_page, которая при необходимости копирует страницу. Так что если записи через /proc/*/mem выполняются приватным разделяемым маппингом, например, libc, то они видимы только в рамках процесса.

kmap()

После того, как найден физический фрейм, его нужно отобразить в виртуальное адресное пространство ядра, доступным для записи. Делается это с помощью kmap().

На 64-битной платформе x86 вся физическая память отображается через область линейного отображения виртуального адресного пространства ядра. В этом случае kmap() работает очень просто: ему лишь нужно добавить начальный адрес линейного отображения в физический адрес фрейма, чтобы вычислить виртуальный адрес, в который отображён этот фрейм.

На 32-битной платформе x86 линейное отображение содержит подмножество физической памяти, так что функции kmap() может потребоваться отобразить фрейм с помощью выделения highmem-памяти и манипулирования таблицами страницы.

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

copy_to_user_page()

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

Обсуждение


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

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

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

Заключение


Изучив подробности пробивной семантики в реализации /proc/*/mem мы можем отразить взаимосвязи между ядром и процессором. На первый взгляд, способность ядра писать в недоступную для записи память вызывает вопрос: до какой степени процессор может влиять на доступ ядра к памяти? В руководстве описаны механизмы управления, которые могут ограничивать действия ядра. Но при внимательном рассмотрении оказывается, что ограничения в лучшем случае поверхностны. Это простые препятствия, которые можно обойти.
Подробнее..

Что такое графовые нейронные сети

13.05.2021 20:21:56 | Автор: admin

Графовые сети это способ применения классических моделей нейронных сетей к графовым данным. Графы, не обладая регулярной структурой как изображения (каждый пиксель имеет 8 соседей) или тексты (последовательность слов), долгое время оставались вне поля зрения классических нейронных моделей, которые получили широкое распространение в области машинного обучения и искусственного интеллекта. Большинство моделей векторизации графов (построения векторного представления вершин в графе) были достаточно медленными и использовали алгоритмы на основе матричной факторизации или спектральной декомпозиции графа. В 2015-16 годах появились более эффективные модели (DeepWalk, Line, Node2vec, Hope) на основе случайных блужданий. Однако и они имели ограничения, потому что никак не затрагивали при построении векторной модели графа дополнительных признаков, которые могут храниться в вершинах или на ребрах. Появление графовых нейронных сетей стало логичным продолжением исследований в области графовых эмбеддингов и позволило унифицировать под единым фреймворком предыдущие подходы.



Для чего они нужны и как устроены


Один слой графовой нейросети это обычный полносвязный слой (fully-connected layer) нейронной сети, но веса в нм применяются не ко всем входным данным, а только к тем, которые являются соседями конкретной вершины в графе, в дополнение к ее собственному представлению с предыдущего слоя. Веса для соседей и самой вершины могут задаваться общей матрицей весов или двумя отдельными. Могут добавляться нормализации для ускорения сходимости; могут меняться нелинейные функции активаций, но общая конструкция остается похожей. При этом графовые сверточные сети получили свое название благодаря агрегации информации от своих соседей, хотя гораздо ближе к этому определению стоят графовые механизмы внимания (GAT) или индуктивная модель обучения (GraphSAGE).

Применение


Рекомендательные системы


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

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

Pinterest предложила модель PinSage, которая эффективно подбирает соседние узлы с помощью персонализированного PageRank и обновляет эмбеддинги вершин с помощью агрегирования информации от соседей. Следующая модель PinnerSage уже может работать с мультиэмбеддингами, чтобы учитывать разные вкусы пользователей. Это лишь пара примечательных примеров в сфере рекомендательных систем. Можете ещё почитать об исследовании Amazon графов знаний и графовых нейросетей, или об использовании компанией Fabula AI графовых нейросетей для определения фальшивых новостей. Но и без этого очевидно, что графовые нейросети демонстрируют многообещающие результаты при значительном сигнале от пользовательских взаимодействий.

Комбинаторная оптимизация


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

Команда Google Brain использовала графовые нейросети для оптимизации энергопотребления, площади и производительности чипов для нового оборудования вроде Google TPU. Компьютерный процессор можно представить в виде графа памяти и компонентов логики, каждый со своими координатами и типом. Определение местоположения для каждого компонента с учётом ограничений по плотности размещения и маршрутизации нагрузки до сих пор является трудоёмким процессом, произведением искусства инженеров-электронщиков. Сочетание графовой модели с политикой и обучением с подкреплением позволяет находить оптимальное размещение микросхем и создавать более производительные чипы по сравнению с разработанными людьми.

Другой подход подразумевает интеграцию модели машинного обучения в уже существующие инструменты решения. Например, коллектив под руководством М. Гасса предложил графовую сеть, которая обучается политикам выбора переменных по методу ветвей и границ: это критически важная операция в инструментах решения на основе частично-целочисленных линейных программ (mixed-integer linear program, MILP). В результате выученные представления пытаются минимизировать длительность работы инструментов решения и демонстрируют хороший компромисс между скоростью вывода и качеством решений.

В более свежей совместной работе DeepMind и Google графовые сети использованы в двух ключевых подзадачах, решаемых MILP-инструментами: совместном присвоении переменных и ограничении целевых значений. Предложенный подход на основе нейросетей оказался в 2-10 раз быстрее по сравнению с существующими инструментами решения при использовании огромных наборов данных, в том числе применяемых в Google систем упаковки продукции и планирования. Если вас интересует это направление, то можно порекомендовать пару недавних исследований (1, 2), в которых гораздо глубже обсуждается сочетание графовых нейросетей, машинного обучения и комбинаторной оптимизации.

Компьютерное зрение


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

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

Другой источник графов в компьютерном зрении сопоставление двух взаимосвязанных изображений. Это классическая задача, для решения которой раньше вручную создавали дескрипторы. Специализирующаяся на трёхмерной графике компания Magic Leap создала архитектуру на основе графовых нейросетей под названием SuperGlue. Эта архитектура позволяет в реальном времени сопоставлять видеозаписи для трёхмерного воспроизведения сцен, распознавания мест, одновременной локализации и построения карты (SLAM). SuperGlue состоит из графовой нейросети на основе механизма внимания. Она учит находить ключевые точки изображения, которые затем передаются на оптимальный транспортный слой для сопоставления. На современных видеокартах модель способна работать в реальном времени и может быть интегрирована в SLAM-системы. Подробнее о сочетании графов и компьютерного зрения рассказано в этих исследованиях: 1, 2.

Физика и химия


Представление взаимодействий между частицами или молекулами в виде графов и прогнозированием свойств новых материалов и веществ с помощью графовых нейросетей позволяет решать различные естественнонаучные задачи. Например, в рамках проекта Open Catalyst Facebook и CMU ищут новые способы хранения возобновляемой энергии солнца и ветра. Одно из возможных решений заключается в преобразовании этой энергии с помощью химических реакций в иные виды топлива, скажем, в водород. Но для этого нужно создать новые катализаторы высокоинтенсивных химических реакций, а известные сегодня методы вроде DFT очень дороги. Авторы проекта выложили крупнейшую подборку катализаторов, DFT-затуханий и базовых уровней для графовых нейросетей. Разработчики надеются найти новые дешёвые симуляции молекул, которые дополнят текущие дорогие симуляции, выполняющиеся в течение дней, эффективными оценками энергии и межмолекулярных сил, которые вычисляются в течение миллисекунд.

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

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

Разработка лекарств


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

Вероятно, самым многообещающим результатом использования графовых нейросетей в этой сфере стала работа исследователей из MIT, опубликованная в Cell в 2020-м. Они применили модель глубокого обучения под названием Chemprop, которая прогнозировала антибиотические свойства молекул: подавление размножения кишечной палочки. После обучения всего лишь на 2500 молекул из библиотеки, одобренной Управлением по контролю за продуктами и лекарствами, Chemprop применили к более крупному набору данных, в том числе к Drug Repurposing Hub, содержащему молекулу Halicin, переименованную в честь ИИ HAL 9000 из фильма Космическая одиссея 2001 года. Примечательно, что до этого Halicin изучали только применительно к лечению диабета, потому что её структура сильно отличается от известных антибиотиков. Но клинические эксперименты in vitro и in vivo показали, что Halicin является антибиотиком широкого спектра. Обширное сопоставление с сильными нейросетевыми моделями подчеркнуло важность обнаруженных с помощью графовых нейросетей свойств Halicin. Помимо практической роли этой работы архитектура Chemprop интересна и другим: в отличие от многих графовых нейросетей она содержит 5 слоев и 1600 скрытых измерений, что намного больше типичных параметров графовых нейросетей для таких задач. Надеюсь, что это было лишь одно из немногих ИИ-открытий в будущей новой медицине. Подробнее об этом направлении читайте здесь и здесь.

Когда графовые нейросети стали трендом


Пик популярности графовых эмбеддингов пришелся на 2018 год, когда предложенные в 2016 году модели структурных эмбеддингов и графовых нейронных сетей были проверены во многих практических приложениях и показали высокую эффективность, включая наиболее известный пример модели PinSAGE, используемой в рекомендациях социальной сети Pinterest. С тех пор рост исследований по этой тематике стал экспоненциальным, находится вс больше применений в областях, где раньше методы были не способны эффективно учитывать в моделях связи между объектами. Примечательно, что автоматизация машинного обучения и поиск новых эффективных архитектур нейронных сетей также получили новый толчок к развитию благодаря графовым нейронным сетям.

Являются ли они трендом и в России тоже


К сожалению, Россия в большинстве случаев отстает от современных исследований в области искусственного интеллекта. Количество статей на международных конференциях и в ведущих журналах на порядок меньше статей ученых из США, Европы и Китая, а финансовая поддержка исследований в новых областях встречает сопротивление в закостенелой среде академиков, застрявших в подходах из прошлого века и выдающих гранты скорее по принципу геронтократического кумовства, чем по реальным достижениям. В результате на ведущих конференциях, таких как TheWebConf, ICDM, WSDM, KDD, NIPS фамилии российских ученых в основном аффилированы с западными университетами, что отражает тенденцию на утечку мозгов, а также серьезную конкуренцию со стороны передовых стран в области разработки ИИ, в особенности Китая.

Если посмотреть на конференции в области компьютерных наук с наивысшим рейтингом A* по версии Core, например, конференции по высокопроизводительным вычислениям, то средний размер гранта у принятых в основной трек статей составляет $1 млн., что проигрывает максимальным программам от российского научного фонда в 15-30 раз. В таких условиях исследования, проводимые в крупных компаниях с организацией R&D, по сути являются единственными драйверами в поиске новых подходов на основе графовых нейронных сетей.

В России теорией графовых нейронных сетей занимаются в НИУ ВШЭ под моим руководством, также есть группы в Сколтехе и МФТИ, прикладные исследования ведутся в ИТМО, КФУ, а также в Лаборатории ИИ Сбера, в R&D-проектах JetBrains, Mail.ru Group, Yandex.

На мировой сцене драйверами являются компании Twitter, Google, Amazon, Facebook, Pinterest.

Мимолетная или долгосрочная тенденция?


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

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

Tarantool и кодогенерация на Lua

21.05.2021 14:10:59 | Автор: admin

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

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

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

Немного про Tarantool и LuaJIT

Tarantool это платформа для in-memory вычислений флакон, объединяющий сервер приложений и базу данных. Сам Tarantool написан на языке С, но пользователь может работать с ним с помощью языка Lua. А если совсем точно, то одной из его реализаций LuaJIT не с просто интерпретатором, а ещё и с поддержкой и JIT-компиляции. И часто при работе возникают задачи по трансформации сущностей при записи в базу или после извлечения из неё, а также их валидации на соответствие схеме, заданной пользователем. Типичный подход для решения этой и схожих задач написание функций для преобразования данных. Эти функции не привязаны к конкретной схеме и зачастую представляют из себя набор замыканий. Однако не стоит забывать, что мы работаем с LuaJIT языком, который способен компилировать и достаточно быстро выполнять "горячие" участки кода.

Но, к сожалению, не всё подряд может быть скомпилировано, у платформы есть ряд ограничений это так называемые NYI (Not yet implemented) функции. Кроме того, работа с данными активно использует дополнительные структуры массивы и хэш-мапы. В Lua они представлены общим типом данных "table" (таблица). Перед нами две основные проблемы использование части функций серьезно влияет на производительность, а избыточное использование вспомогательных структур приводит к излишней нагрузке на GC, с которым у и Lua 5.1, и у LuaJIT проблемы. Поэтому задача написание кода, который сможет быть скомпилирован LuaJIT, и будет приводить к минимально возможному количеству аллокаций.

К реальным задачам

Данный подход мы будем разбирать на реальном примере, на примере модуля CRUD. Задача данного модуля это упрощение работы с шардированными данными. То есть данные распределены между несколькими стораджами (инстансами Tarantool, хранящими данные), и мы, обращаясь к ним через роутер (по сути, клиент), не хотим задумываться, на каком именно из стораджей лежат интересующие нас данные, а просто указываем условие поиска, и модуль возвращает нам уже готовые данные. Немного про хранение. Tarantool хранит данные в спейсах (spaces) аналог таблиц в реляционных БД. Единица хранения кортеж (tuple) массив заданных нами значений. При этом нам привычно работать именно с Lua-таблицами обращаться к полю по названию, а не по номеру в кортеже. В качестве аналогии можно привести формат JSON. Обычно именно в таком формате поступают данные из внешних систем которые затем парсятся в Lua-таблицы, "сплющиваются" и сохраняются в базу. Соответственно типичными для тарантула операциями являются так называемый "флаттенинг" (flatten) и "анфлаттенинг" (unflatten) получение из луа-таблицы плоского тапла и наоборот. И в частном случае пользователь может написать руками все эти операции.

-- Создаем space - аналог таблицы в реляционных БДbox.schema.space.create('data')-- Создаем первичный ключbox.space.data:create_index('primary_key')-- Попробуем вставить в наш space следующий объектobject = { id = 1, key = "key", value = "value" }-- Выполняем "сплющивание" объекта - flattentuple = {object["id"], object["key"], object["value"]}-- Единицей хранение в Tarantool является tuple - кортеж из значенийbox.space.data:insert(tuple)-- После сохранения мы можем достать наш объект по первичному ключуtuple = box.space.data:get({1})-- Преобразуем объект в исходное состояние - unflattenobject = {   id = tuple[1],   key = tuple[2],   value = tuple[3],}

Здесь мы явно захардкодили порядок полей в спейсе. Однако в общем случае схема задается извне некоторым форматом, и мы пишем простенькие функции, которые занимаются трансформацией объекта в соответствии с этим форматом. Модуль CRUD, как и сам Tarantool, имеет функцию replace она точно также вставляет кортеж в базу. Для упрощения жизни пользователям была также добавлена функция replace_object которая принимает объект, преобразует в плоский вид в соответствии с форматом спейса, а затем уже сохраняет.

Ближе к коду и измерению производительности

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

-- test_data.lua-- Формат - 8 строковых полей + bucket_id-- (специальное поле, необходимое при шардировании данных).local format = {    {name = 'field1', type = 'string', is_nullable = false},    {name = 'field2', type = 'string', is_nullable = false},    {name = 'field3', type = 'string', is_nullable = false},    {name = 'field4', type = 'string', is_nullable = false},    {name = 'field5', type = 'string', is_nullable = false},    {name = 'field6', type = 'string', is_nullable = false},    {name = 'field7', type = 'string', is_nullable = false},    {name = 'field8', type = 'string', is_nullable = false},    {name = 'bucket_id', type = 'unsigned', is_nullable = false},}-- Объект необходимого форматаlocal data = {    field1 = 'string1',    field2 = 'string2',    field3 = 'string3',    field4 = 'string4',    field5 = 'string5',    field6 = 'string6',    field7 = 'string7',    field8 = 'string8',    bucket_id = nil,}return {    format = format,    data = data,}

Функция, замеряющая время выполнения нашего кода.

-- bench.lua-- Замеряем, сколько времени займет 1 миллион итерацийlocal clock = require('clock')local count = 1e6local function run(f, ...)    local start = clock.time()    for _ = 1, count do        f(...)    end    return clock.time() - startendreturn {    run = run,}

И входная точка, исполняемый файл, в который поочередно можно будет добавлять тесты:

#!/usr/bin/env tarantool-- init.lualocal bench = require('bench')local test_data = require('test_data')-- Это наш первый тестlocal naive = require('naive')local res = bench.run(naive.flatten, test_data.data, test_data.format, 1)print(string.format('Naive result: %0.3f s', res))-- После добавления нужного модуля, мы раскомментируем каждый фрагмент.-- local code_gen_v1 = require('code_gen_v1')-- local res = bench.run(code_gen_v1.flatten, test_data.data, test_data.format, 1)-- print(string.format('code_gen_v1 result: %0.3f s', res))-- local code_gen_v2 = require('code_gen_v2')-- local res = bench.run(code_gen_v2.flatten, test_data.data, test_data.format, 1)-- print(string.format('code_gen_v2 result: %0.3f s', res))

Давайте рассмотрим, как раньше выполнялось данное преобразование самый наивный подход:

-- naive.lualocal system_fields = { bucket_id = true }local function flatten(object, space_format, bucket_id)    if object == nil then return nil end    local tuple = {}    local fieldnames = {}    for fieldno, field_format in ipairs(space_format) do        local fieldname = field_format.name        local value = object[fieldname]        if not system_fields[fieldname] then            if not field_format.is_nullable and value == nil then                return nil, string.format("Field %q isn't nullable", fieldname)            end        end        if bucket_id ~= nil and fieldname == 'bucket_id' then            value = bucket_id        end        tuple[fieldno] = value        fieldnames[fieldname] = true    end    for fieldname in pairs(object) do        if not fieldnames[fieldname] then            return nil, string.format("Unknown field %q is specified", fieldname)        end    end    return tupleendreturn {    flatten = flatten,}

Пример слегка упрощен. Но стоит заметить несколько вещей:

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

  • Обходим весь объект в соответствии с форматом. При этом формат нам известен и меняется достаточно редко. Это одна из предпосылок для использования кодогенерации.

Запускаем:

  tarantool init.lua Naive result: 1.109 s

На моём ноутбуке этот тест выполнился за 1 секунду.

Теперь попробуем написать функцию, которая развернет нам цикл. Объект же продолжит обрабатываться в соответствии с форматом.

-- code_gen_v1.lua-- Небольшой хелпер для работы со строкамиlocal function append(lines, s, ...)    table.insert(lines, string.format(s, ...))end-- Кэш, где ключ - таблица с "форматом", а значение - функция флаттенинга.-- Для простоты считаем, что формат не меняется, не занимаемся инвалидацией кэша.local cache = {}local function flatten(object, space_format, bucket_id)    -- В случае если функция уже сгенерирована,    -- берем её из кэша. Иначе приступаем к кодогенерации.    local fun = cache[space_format]    if fun ~= nil then        return fun(object, bucket_id)    end    -- Будем "готовить" наш код построчно и сохранять в массив lines.    local lines = {}    append(lines, 'local object, bucket_id = ...')    append(lines, 'local result = {}')    for i, field in ipairs(space_format) do        if field.name ~= 'bucket_id' then            append(lines, 'result[%d] = object[%q]', i, field.name)        else            append(lines, 'result[%d] = bucket_id', i)        end    end    append(lines, 'return result')    -- Конкатенируем элементы массива, чтобы получить полный текст функции.    local code = table.concat(lines, '\n')        -- Раскомментриуйте, чтобы увидеть результат    -- print(code)        -- С помощью функции "load" преобразуем текст функции в саму функцию    fun = assert(load(code))    cache[space_format] = fun    return fun(object, bucket_id)endreturn {    flatten = flatten,}

В результате выполнения получим следующий код:

local object, bucket_id = ...local result = {}result[1] = object["field1"]result[2] = object["field2"]result[3] = object["field3"]result[4] = object["field4"]result[5] = object["field5"]result[6] = object["field6"]result[7] = object["field7"]result[8] = object["field8"]result[9] = bucket_idreturn result

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

Что еще мы можем улучшить? В самом начале сгенерированного кода мы создаем таблицу result и постепенно её заполняем. Такой подход приводит к неоднократным реаллокациям, что плохо и довольно бессмысленно ведь размер таблицы известен заранее. Давайте учтём это и поменяем строку append(lines, 'local result = {}') на append(lines, 'local result = {%s}', string.rep('box.NULL,', #space_format)). Так мы сразу создадим массив нужного нам размера local result = {box.NULL, ..., box.NULL}. Запуск бенчмарка выдает 0.2 секунды.

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

-- code_gen_v2.lualocal function append(lines, s, ...)    table.insert(lines, string.format(s, ...))endlocal cache = setmetatable({}, {__mode = 'k'})local function flatten(object, space_format, bucket_id)    local fun = cache[space_format]    if fun ~= nil then        return fun(object, bucket_id)    end    local lines = {}    append(lines, 'local object, bucket_id = ...')    append(lines, 'for k in pairs(object) do')    append(lines, '    if fieldmap[k] == nil then')    append(lines, '        return nil, format(\'Unknown field %%q is specified\', k)')    append(lines, '    end')    append(lines, 'end')    local len = #space_format    append(lines, 'local result = {%s}', string.rep('NULL,', len))    local fieldmap = {}    for i, field in ipairs(space_format) do        fieldmap[field.name] = true        if field.name ~= 'bucket_id' then            if field.is_nullable ~= true then                append(lines, 'if object[%q] == nil then', field.name)                append(lines, '    return nil, \'Field %q isn\\\'t nullable\'', field.name)                append(lines, 'end')            end            append(lines, 'result[%d] = object[%q]', i, field.name)        else            append(lines, 'if bucket_id ~= nil then')            append(lines, '    result[%d] = bucket_id', i, field.name)            append(lines, 'else')            append(lines, '    result[%d] = object[%q]', i, field.name)            append(lines, 'end')        end    end    append(lines, 'return result')    local code = table.concat(lines, '\n')    local env = {        pairs = pairs,        format = string.format,        fieldmap = fieldmap,        NULL = box.NULL,    }    fun = assert(load(code, '@flatten', 't', env))    cache[space_format] = fun    return fun(object, bucket_id)endreturn {    flatten = flatten,}

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

Бенчмарк показал 0.3 секунды.

  tarantool init.lua Naive result: 1.109 scode_gen_v1 result: 0.210 scode_gen_v2 result: 0.299 s

Также стоит отметить, что у функции load появились дополнительные аргументы, а именно chunkname название нашей функции (может быть полезным при отладке), mode t мы создаем функцию на основе обычного текста, а не байткода и env окружение, доступное внутри нашей функции. На последний аргумент стоит обратить особое внимание. Кроме возможности создавать удобные песочницы для выполнения пользовательского кода (обычно не давать доступа к "опасным" функциям), данная опция позволяет передавать в глобальное окружение нужные нам функции и аргументы. В нашем случае это pairs, format, fieldmap и NULL. Отдельно стоит отметить, что load это функция из Lua 5.2 расширение LuaJIT. Тот, кто работает с чистым Lua 5.1, может использовать функции loadstring для создания функции и setfenv для установки окружения у этой функции.

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

Небольшой пример:

local function is_string(value)    return type(value) == 'string'end-- Функции is_string нет в языке Lua,-- но с помощью окружения мы можем добавить в нужные нам функции-- и убрать лишние.local code = [[local value = ...local result = {NULL}if not is_string(value) then    error("value is not a string")endresult[1] = valuereturn result]]local fun = load(code, '@test', 't', {    error = error,    -- Функция is_string будет доступна внутри    -- загружаемого нами кода    is_string = is_string,    NULL = box.NULL,})

Как в будущем всё не сломать

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

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

Более качественная оценка результатов

В статье я привел не так много результатов тестирования просто сравнил несколько цифр: время выполнения программы до и после. При этом я говорил, что мы уменьшили количество аллокаций и написали код, пригодный для компиляции LuaJIT'ом. Но как это можно проверить, как в этом можно убедиться?

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

Во-первых, это memory profiler, который появился в версии 2.7.1 инструмент, который покажет в каких именно местах и в каких количествах выделяется/реаллоцируется память. Как по мне, вывод довольно удобен а в будущем станет ещё удобнее. Воспользовавшись этим инструментом, можно показать количественную разницу между кодом до и кодом после. В нашем случае мы получили бы вывод в формате @<filename>:<function_line>, line <line where event was detected>: <number of events> <allocated> <freed>. Для наглядности напротив некоторых строк я помещу фрагменты кода, которые находятся на этих строках:

Для кода "до" (naive.lua):

ALLOCATIONSINTERNAL: 39999533600003800@../naive.lua:4, line 26: 10000383840039360           // fieldnames[fieldname] = true@../naive.lua:4, line 7: 1000000640000000           // local tuple = {}@../naive.lua:4, line 9: 1000000640000000           // local fieldnames = {}@../naive.lua:4, line 25: 163840@../naive.lua:4, line 0: 46720REALLOCATIONSINTERNAL: 199998211200056064000288Overrides:@../naive.lua:4, line 0@../naive.lua:4, line 25INTERNAL@../naive.lua:4, line 25: 100002213600123272000704Overrides:@../naive.lua:4, line 25INTERNALDEALLOCATIONSINTERNAL: 59535720784628243Overrides:@../naive.lua:4, line 0@../naive.lua:4, line 25@../naive.lua:4, line 26@../naive.lua:4, line 7@../naive.lua:4, line 9INTERNAL@../naive.lua:4, line 26: 10000220192001584Overrides:@../naive.lua:4, line 26INTERNAL

Для кода "после" (code_gen_v2.lua):

ALLOCATIONS@flatten:0, line 7: 10000001440000000 // local result = {NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,}@../code_gen_v3.lua:7, line 55: 1480REALLOCATIONSINTERNAL: 519843968Overrides:INTERNALDEALLOCATIONSINTERNAL: 9742980140298062Overrides:@flatten:0, line 7

Во-вторых, сам LuaJIT поставляется с профилировщиком require('jit.p')

Для кода "до":

52%  ../naive.lua:11  // for fieldno, field_format in ipairs(space_format) do30%  ../naive.lua:26  // fieldnames[fieldname] = true12%  ../naive.lua:9   // local fieldnames = {}

Для кода "после":

36%  flatten:3  // if fieldmap[k] == nil then36%  flatten:7  // local result = {NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,}11%  ../code_gen_v3.lua:9 // выбираем значение из кэша 4%  flatten:39 4%  flatten:2 4%  ../code_gen_v3.lua:8 4%  flatten:8 4%  ../code_gen_v3.lua:10

А также для тех, кто хочет копнуть совсем глубоко, есть возможность дампа байткода, который LuaJIT генерирует и выполняет require('jit.dump')

Заключение

Мы рассмотрели применение кодогенерации при разработке на Tarantool. Это позволило достаточно просто ускорить в 3 раза один из участков кода в реальном проекте патч был принят. При разработке не стоит забывать о специфике платформы. По возможности стоит генерировать код, который будет приводить к выделению минимально возможного количества памяти, а также не использовать медленные функции в нашем случае те, которые не компилируются LuaJIT. Также советую обратить внимание на то, что в проекте CRUD и до этого использовалась кодогенерация. C её помощью создаются быстрые функции для проверки соответствия тапла пользовательским условиям.

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

Подробнее..

Перевод Развеиваем мифы об управлении памятью в JVM

28.05.2021 16:10:14 | Автор: admin

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

Структура памяти JVM


Сначала давайте посмотрим на структуру памяти JVM. Эта структура применяется начиная с JDK 11. Вот какая память доступна процессу JVM, она выделяется операционной системой:


Это нативная память, выделяемая ОС, и её размер зависит от системы, процессор и JRE. Какие области и для чего предназначены?

Куча (heap)


Здесь JVM хранит объекты и динамические данные. Это самая крупная область памяти, в ней работает сборщик мусора. Размером кучи можно управлять с помощью флагов Xms (начальный размер) и Xmx (максимальный размер). Куча не передаётся виртуальной машине целиком, какая-то часть резервируется в качестве виртуального пространства, за счёт которого куча может в будущем расти. Куча делится на пространства молодого и старого поколения.

  • Молодое поколение, или новое пространство: область, в которой живут новые объекты. Она делится на рай (Eden Space) и область выживших (Survivor Space). Областью молодого поколения управляет младший сборщик мусора (Minor GC), который также называют молодым (Young GC).
    • Рай: здесь выделяется память, когда мы создаём новые объекты.
    • Область выживших: здесь хранятся объекты, которые остались после работы младшего сборщика мусора. Область делится на две половины, S0 и S1.
  • Старое поколение, или хранилище (Tenured Space): сюда попадают объекты, которые достигли максимального порога хранения в ходе жизни младшего сборщика мусора. Этим пространством управляет старший сборщик (Major GC).

Стеки потоков исполнения


Это стековая область, в которой на один поток выделяется один стек. Здесь хранятся специфические для потоков статичные данные, в том числе фреймы методов и функций, указатели на объекты. Размер стековой памяти можно задать с помощью флага Xss.

Метапространство


Это часть нативной памяти, по умолчанию у неё нет верхней границы. В более ранних версиях JVM эта память называлась пространством постоянного поколения (Permanent Generation (PermGen) Space). Загрузчики классов хранили в нём определения классов. Если это пространство растёт, то ОС может переместить хранящиеся здесь данные из оперативной в виртуальную память, что может замедлить работу приложения. Избежать этого можно, задав размер метапространства с помощью флагов XX:MetaspaceSize и -XX:MaxMetaspaceSize, в этом случае приложение может выдавать ошибки памяти.

Кеш кода


Здесь компилятор Just In Time (JIT) хранит скомпилированные блоки кода, к которым приходится часто обращаться. Обычно JVM интерпретирует байткод в нативный машинный код, однако код, скомпилированный JIT-компилятором, не нужно интерпретировать, он уже представлен в нативном формате и закеширован в этой области памяти.

Общие библиотеки


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

Использование памяти JVM: стек и куча


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

class Employee {    String name;    Integer salary;    Integer sales;    Integer bonus;    public Employee(String name, Integer salary, Integer sales) {        this.name = name;        this.salary = salary;        this.sales = sales;    }}public class Test {    static int BONUS_PERCENTAGE = 10;    static int getBonusPercentage(int salary) {        int percentage = salary * BONUS_PERCENTAGE / 100;        return percentage;    }    static int findEmployeeBonus(int salary, int noOfSales) {        int bonusPercentage = getBonusPercentage(salary);        int bonus = bonusPercentage * noOfSales;        return bonus;    }    public static void main(String[] args) {        Employee john = new Employee("John", 5000, 5);        john.bonus = findEmployeeBonus(john.salary, john.sales);        System.out.println(john.bonus);    }}

Здесь вы можете увидеть, как исполняется вышеприведённая программа и как используются стек и куча:

https://files.speakerdeck.com/presentations/9780d352c95f4361bd8c6fa164554afc/JVM_memory_use.pdf

Как видите:

  • Каждый вызов функции добавляется в стек потока исполнения в качестве фреймового блока.
  • Все локальные переменные, включая аргументы и возвращаемые значения, сохраняются в стеке внутри фреймовых блоков функций.
  • Все примитивные типы вроде int хранятся прямо в стеке.
  • Все типы объектов вроде Employee, Integer или String создаются в куче, а затем на них ссылаются с помощью стековых указателей. Это верно и для статичных данных.
  • Функции, которые вызываются из текущей функции, попадают наверх стека.
  • Когда функция возвращает данные, её фрейм удаляется из стека.
  • После завершения основного процесса объекты в куче больше не имеют стековых указателей и становятся потерянными (сиротами).
  • Пока вы явно не сделаете копию, все ссылки на объекты внутри других объектов делаются с помощью указателей.

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

Управление памятью JVM: сборка мусора


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

JVM управляет куче с помощью сборки мусора. Чтобы освободить место для создания нового объекта, JVM очищает память, занятую потерянными объектами, то есть объектами, на которые больше нет прямых или опосредованных ссылок из стека.


Сборщик мусора в JVM отвечает за:

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

Сборщики мусора в JVM работают по принципу поколений (объекты в куче группируются по возрасту и очищаются во время разных этапов). Есть много разных алгоритмов сборки мусора, но чаще всего применяют Mark & Sweep.

Сборщик мусора Mark & Sweep


JVM использует отдельный поток демона, который работает в фоне для сборки мусора. Этот процесс запускается при выполнении определённых условий. Сборщик Mark & Sweep обычно работает в два этапа, иногда добавляют третий, в зависимости от используемого алгоритма.


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

Такой тип сборщиков ещё называют stop-the-world, потому что пока они убираются, возникают паузы в работе приложения.

JVM предлагает на выбор несколько разных алгоритмов сборки мусора, и в зависимости от вашего JDK может быть ещё больше вариантов (например, сборщик Shenandoah в OpenJDK). Авторы разных реализаций стремятся к разным целям:

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

Сборщики в JDK 11


JDK 11 это текущая версия LTE. Ниже приведён список доступных в ней сборщиков мусора, и JVM выбирает по умолчанию один из них в зависимости от текущего оборудования и операционной системы. Мы всегда можем принудительно выбрать какой-либо сборщик с помощью переключателя -XX.

  • Серийный сборщик: использует один поток, эффективен для приложений с небольшим количеством данных, наиболее удобен для однопроцессорных машин. Его можно выбрать с помощью -XX:+UseSerialGC.
  • Параллельный сборщик: нацелен на высокую пропускную способность и использует несколько потоков, чтобы ускорить процесс сборки. Предназначен для приложений со средним или большим количеством данных, исполняемых на многопоточном/многопроцессорном оборудовании. Его можно выбрать с помощью -XX:+UseParallelGC.
  • Сборщик Garbage-First (G1): работает по большей части многопоточно (то есть многопоточно выполняются только объёмные задачи). Предназначен для многопроцессорных машин с большим объёмом памяти, по умолчанию используется на большинстве современных компьютеров и ОС. Нацелен на короткие паузы и высокую пропускную способность. Его можно выбрать с помощью -XX:+UseG1GC.
  • Сборщик Z: новый, экспериментальный, появился в JDK11. Это масштабируемый сборщик с низкой задержкой. Многопоточный и не останавливает исполнение потоков приложения, то есть не относится к stop-the-world. Предназначен для приложений, которым необходима низкая задержка и/или очень большая куча (на несколько терабайтов). го можно выбрать с помощью -XX:+UseZGC.

Процесс сборки мусора


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

Младший сборщик


Он поддерживает чистоту и компактность пространства молодого поколения. Запускается тогда, когда JVM не может получить в раю необходимую память для размещения нового объекта. Изначально все области кучи пусты. Рай заполняется первым, за ним область выживших, и в конце хранилище.

Здесь вы можете увидеть процесс работы этого сборщика:

https://files.speakerdeck.com/presentations/f4783404769145f4b990154d0cc05629/JVM_minor_GC.pdf

  1. Допустим, в раю уже есть объекты (блоки с 01 по 06 помечены как используемые).
  2. Приложение создаёт новый объект (07).
  3. JVM пытается получить необходимую память в раю, но там уже нет места для размещения нового объекта, поэтому JVM запускает младший сборщик.
  4. Он рекурсивно проходит по графу объектов начиная со стековых указателей и помечает используемые объекты как (используемая память), остальные как мусор (потерянные).
  5. JVM случайно выбирает один блок из S0 и S1 в качестве целевого пространства (To Space), пусть это будет S0. Теперь сборщик перемещает все живые объекты в целевое пространство, которое было пустым, когда мы начали работу, и повышает их возраст на единицу.
  6. Затем сборщик очищает рай, и в нём выделяется память для нового объекта.
  7. Допустим, прошло какое-то время, и в раю стало больше объектов (блоки с 07 по 13 помечены как используемые).
  8. Приложение создаёт новый объект (14).
  9. JVM пытается получить в раю нужную память, но там нет свободного места для нового объекта, поэтому JVM снова запускает младший сборщик.
  10. Повторяется этап разметки, который охватывает и те объекты, что находятся в пространстве выживших в целевом пространстве.
  11. Теперь JVM выбирает в качестве целевого свободный блок S1, а S0 становится исходным. Сборщик перемещает все живые объекты из рая и исходного в целевое (S1), которое было пустым, и повысил возраст объектов на единицу. Поскольку некоторые объекты сюда не поместились, сборщик переносит их в хранилище, ведь область выживших не может увеличиваться, и этот процесс называют преждевременным продвижением (premature promotion). Такое может происходить, даже если свободна одна из областей выживших.
  12. Теперь сборщик очищает рай и исходное пространство (S0), а новый объект размещается в раю.
  13. Так повторяется при каждой сессии младшего сборщика, выжившие перемещаются между S0 и S1, а их возраст увеличивается. Когда он достигает заданного максимального порога, по умолчанию это 15, объект перемещается в хранилище.

Мы рассмотрели, как младший сборщик очищает память в пространстве молодого поколения. Это процесс типа stop-the-world, но он настолько быстрый, что его длительностью обычно можно пренебречь.

Старший сборщик


Следит за чистотой и компактностью пространства старого поколения (хранилищем). Запускается при одном из таких условий:

  • Разработчик вызывает в программе System.gc() или Runtime.getRunTime().gc().
  • JVM решает, что в хранилище недостаточно памяти, потому что оно заполнено в результате прошлых сессий младшего сборщика.
  • Если во время работы младшего сборщика JVM не может получить достаточно памяти в раю или области выживших.
  • Если мы задали в JVM параметр MaxMetaspaceSize и для загрузки новых классов не хватает памяти.

Процесс работы старшего сборщика попроще, чем младшего:

  1. Допустим, прошло уже много сессий младшего сборщика и хранилище почти заполнено. JVM решает запустить старший сборщик.
  2. В хранилище он рекурсивно проходит по графу объектов начиная со стековых указателей и помечает используемые объекты как (используемая память), остальные как мусор (потерянные). Если старший сборщик запустили в ходе работы младшего сборщика, то его работа охватывает пространство молодого поколения (рай и область выживших) и хранилище.
  3. Сборщик убирает все потерянные объекты и возвращает память.
  4. Если в ходе работы старшего сборщика в куче не осталось объектов, JVM также возвращает память из метапространства, убирая из него загруженные классы, если это относится к полной сборке мусора.

Заключение


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

Но для большинства JVM-разработчиков (Java, Kotlin, Scala, Clojure, JRuby, Jython) этого объёма информации будет достаточно. Надеюсь, теперь вы сможете писать более качественный код, создавать более производительные приложения, избегая различных проблем с утечкой памяти.

Ссылки


Подробнее..

Деплоим проект на Kubernetes в Mail.ru Cloud Solutions. Часть 1 архитектура приложения, запуск Kubernetes и RabbitMQ

07.04.2021 18:21:37 | Автор: admin

О Kubernetes и его роли в построении микросервисных приложений известно, пожалуй, большинству современных IT-компаний. Однако при его внедрении часто возникает вопрос какой вариант установки выбрать: Self-Hosted или Managed-решение от одного из облачных провайдеров. О недостатках первого варианта, думаю, известно всем, кто проходил через ручное конфигурирование K8s: сложно и трудоемко. Но в чем лучше Cloud-Native подход?

Я Василий Озеров, основатель агентства Fevlake и действующий DevOps-инженер (опыт в DevOps 8 лет), покажу развертывание Kubernetes-кластера на базе облака Mail.ru Cloud Solutions. В этом цикле статей мы создадим MVP для реального приложения, выполняющего транскрибацию видеофайлов из YouTube.

На его базе мы посмотрим все этапы разработки Cloud-Native приложений на K8s, включая проектирование, кодирование, создание и автомасштабирование кластера, подключение базы данных и S3-бакетов, построение CI/CD и даже разработку собственного Helm-чарта. Надеюсь, этот опыт позволит вам убедиться, что работа с K8s может быть по-настоящему удобной и быстрой.

В первой части статьи мы выберем архитектуру приложения, напишем API-сервер, запустим Kubernetes c балансировщиком и облачными базами, развернем кластер RabbitMQ через Helm в Kubernetes.

Также записи всех частей практикума можно посмотреть: часть 1, часть 2, часть 3.

Выбор архитектуры приложения

Определимся с архитектурой будущего приложения. В первую очередь нам потребуется API, к которому будет обращаться клиентское приложение. Будем использовать стандартные форматы: HTTPS и JSON. В JSON необходимо передавать URL видео, а также некоторый идентификатор или уникальное имя запроса для возможности отслеживания его статуса.

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

Далее нам потребуются обработчики, которые будут читать сообщения из очереди и заниматься непосредственной конвертацией запрошенных видео в текст. Назовем их Worker. Для транскрибации будем использовать не внешнее API, а какую-нибудь библиотеку, установленную локально. Так как для этого потребуются ресурсы, обязательно настроим автомасштабирование в кластере, чтобы число обработчиков изменялось пропорционально количеству сообщений в очереди.

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

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

Сценарий взаимодействия выбранных компонентов включает в себя следующие шаги:

  1. Клиент отправляет на API-сервер запрос POST, передавая в теле запроса имя и URL видео на YouTube, которое необходимо перевести в текст.

  2. API-сервер формирует сообщение с полученными параметрами и передает его в очередь RabbitMQ.

  3. API-сервер сохраняет информацию о полученном запросе на конвертацию видео в базе данных PostgreSQL. Статус обработки запроса по умолчанию равен false.

  4. API-сервер информирует клиента об успешном завершении операции. Клиент может продолжать свою работу, не дожидаясь конвертации видео.

  5. Свободный обработчик Worker извлекает сообщение из очереди RabbitMQ.

  6. Получив сообщение, Worker выполняет его обработку: загружает видео по указанному URL, получает из него аудио и переводит при помощи стороннего ПО в текст.

  7. Обработав видео, Worker сохраняет транскрипт видео в хранилище S3.

  8. Worker отправляет в API-сервер информацию об успешной обработке запроса с исходным именем. В запросе передается статус обработки, равный true, и ссылка на текстовый файл в S3. Endpoint для отправки статуса обработки запросов можно либо жестко прописывать в environment-переменных обработчика Worker, либо передавать его в теле сообщений наряду с другими параметрами. В нашем MVP будет реализован первый вариант. То есть обработчикам будет известно, какой API вызвать для обновления статуса запросов.

  9. API-сервер обновляет полученную от Worker информацию о запросе в базе данных PostgreSQL. Альтернативный вариант можно настроить обновление базы данных непосредственно из обработчиков Worker, однако это потребует знания структуры БД с их стороны, что чревато проблемами при миграциях БД. Поэтому в нашем приложении взаимодействие с БД будет происходить исключительно через API-сервер.

  10. Клиент спустя некоторое время после отправки исходного видео запрашивает статус его обработки, передавая в API-сервер имя исходного запроса.

  11. API-сервер извлекает данные о запросе из PostgreSQL по полученному имени.

  12. API-сервер получает информацию о запросе из PostgreSQL.

  13. API-сервер отправляет данные о запросе клиенту. Клиент получает статус обработки и URL, по которому сможет в дальнейшем загрузить транскрипт исходного видео из S3.

Упрощенная схема архитектуры будущего приложенияУпрощенная схема архитектуры будущего приложения

Настройка кластера Kubernetes в облаке MCS

Начинаем с создания кластера Kubernetes. Для этого в панели управления облаком MCS необходимо выбрать пункт меню Контейнеры Кластеры Kubernetes и добавить новый кластер.

На первом шаге настраивается конфигурация будущего кластера. Можно выбрать тип среды и один или несколько предустановленных сервисов. Мы выберем среду Dev и сразу добавим Ingress Controller Nginx для управления внешним доступом к кластеру:

На следующем шаге вводим название кластера и выбираем тип виртуальной машины для ноды Master. Оставим стандартную конфигурацию с 2 CPU и 4 ГБ памяти. Далее можно указать зону доступности мы оставим для нее автоматическое заполнение:

Далее на этом же шаге выбирается тип и размер диска. Нам достаточно HDD размером 20 Гб. Оставляем одну Master-ноду, выбираем предварительно добавленную подсеть и назначаем внешний IP для удобного доступа к кластеру извне:

На следующем шаге создаются группы рабочих узлов. В рамках проекта нам потребуются две группы. Сейчас создадим первую для развертывания API и RabbitMQ, а впоследствии добавим еще одну, для обработчиков Worker.

Вводим название группы узлов и указываем конфигурацию: 2 CPU и 4ГБ памяти. Для зоны доступности вновь выбираем автоматический выбор:

Чтобы обеспечить работу RabbitMQ, выбираем более производительный тип дисков SSD размером 50 ГБ. Оставляем один узел, автомасштабирование пока не указываем его рассмотрим позднее на примере другой группы узлов:

На последнем шаге запускается процесс формирования кластера, который может занять некоторое время: от 5 до 20 минут.

При успешном добавлении кластера на экране отобразится информация о его параметрах:

Для последующей работы с кластером необходимо:

  1. Установить локальный клиент kubectl и запустить его.

  2. Экспортировать в локальный клиент конфигурационный файл созданного кластера с расширением .yaml командой export KUBECONFIG=<путь к файлу>.

  3. Для безопасного подключения к кластеру запустить proxy-сервер командой kubectl proxy.

Эта инструкция отображается под списком параметров кластера после его добавления.

У нас kubectl установлен поэтому берем из загрузок сформированный конфигурационный файл kub-vc-dev_kubeconfig.yaml и экспортируем его в kubectl:

После экспорта конфигурационного файла можно убедиться в работоспособности кластера:

  1. Сначала смотрим доступные контексты: kubectl config get-contexts

    Видим, что у нас создался кластер kub-vc-dev:

  2. Смотрим доступные ноды: kubectl get nodes

    В кластере создались две ноды master и workload:

  3. Смотрим доступные Namespace: kubectl get ns

    Получаем ответ:

  4. Смотрим доступные поды: kubectl -n ingress-nginx get pods

    В Namespace ingress-nginx запущены поды для Nginx Controller:

  5. Смотрим доступные сервисы: kubectl -n ingress-nginx get svс

В списке сервисов также отображается Nginx Controller, для которого указан внешний адрес, который мы сможем прописывать в DNS, чтобы попадать в наши сервисы извне:

Разработка API-сервера на Go

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

Ниже отображена структура проекта. Это стандартное Go-приложение. В файлах go.mod, go.sum описываются зависимости, в папке migrations миграции для базы данных PostgreSQL. В main.go содержится основная логика программы, в requests.go реализация API на добавление, редактирование, удаление и выборку запросов. И есть Dockerfile.

Структура API-сервераСтруктура API-сервера

Остановимся подробнее на содержимом main.go.

Вначале импортируем нужные зависимости. В первую очередь, это migrate для автоматического осуществления миграций, database/sql для работы с базами данных, go-env для работы с переменными окружения, web-фреймворк Gorilla и AMQP для работы с RabbitMQ:

package mainimport (    "encoding/json"    "os"    "github.com/golang-migrate/migrate/v4"    "github.com/golang-migrate/migrate/v4/database/postgres"    _ "github.com/golang-migrate/migrate/v4/source/file"    "database/sql"    env "github.com/Netflix/go-env"    _ "github.com/lib/pq"    "log"    "net/http"    "github.com/gorilla/handlers"    "github.com/gorilla/mux"    "github.com/streadway/amqp")

Далее идут environment, которые мы будем использовать. PGSQL_URI и RABBIT_URI нужны для того, чтобы подключиться к PostgreSQL и RabbitMQ соответственно, LISTEN номер порта, на котором необходимо слушать входящие запросы:

type environment struct {    PgsqlURI  string `env:"PGSQL_URI"`    Listen    string `env:"LISTEN"`    RabbitURI string `env:"RABBIT_URI"`}

Далее следует функция main, которая занимается инициализацией. Сначала происходит чтение environment-переменных, подключение к базе данных PostgreSQL и запуск миграций:

func main() {var err error// Getting configurationlog.Printf("INFO: Getting environment variables\n")cnf := environment{}_, err = env.UnmarshalFromEnviron(&cnf)if err != nil {    log.Fatal(err)}// Connecting to databaselog.Printf("INFO: Connecting to database")db, err = sql.Open("postgres", cnf.PgsqlURI)if err != nil {    log.Fatalf("Can't connect to postgresql: %v", err)}// Running migrationsdriver, err := postgres.WithInstance(db, &postgres.Config{})if err != nil {    log.Fatalf("Can't get postgres driver: %v", err)}m, err := migrate.NewWithDatabaseInstance("file://./migrations", "postgres", driver)if err != nil {    log.Fatalf("Can't get migration object: %v", err)}m.Up()

Затем следует подключение к RabbitMQ и инициализация работы с ним:

// Initialising rabbit mq// Initing rabbitmqconn, err := amqp.Dial(cnf.RabbitURI)if err != nil {    log.Fatalf("Can't connect to rabbitmq")}defer conn.Close()ch, err = conn.Channel()if err != nil {    log.Fatalf("Can't open channel")}defer ch.Close()err = initRabbit()if err != nil {    log.Fatalf("Can't create rabbitmq queues: %s\n", err)}

И в завершение запускается web-сервер. При этом каждому из возможных API-запросов сопоставляется функция обработки, описанная в отдельном файле requests.go:

// Setting handlers for querylog.Printf("INFO: Starting listening on %s\n", cnf.Listen)router := mux.NewRouter().StrictSlash(true)// PROJECTSrouter.HandleFunc("/requests", authMiddleware(getRequests)).Methods("GET")router.HandleFunc("/requests", authMiddleware(addRequest)).Methods("POST")router.HandleFunc("/requests/{name}", authMiddleware(getRequest)).Methods("GET")router.HandleFunc("/requests/{name}", authMiddleware(updRequest)).Methods("PUT")router.HandleFunc("/requests/{name}", authMiddleware(delRequest)).Methods("DELETE")http.ListenAndServe(cnf.Listen, handlers.LoggingHandler(os.Stdout, router))

Далее следует аутентификация в сильно упрощенном варианте, так как на стадии MVP этого достаточно. Разумеется, при разработке Enterprise-решений указание токенов и прочих переменных в явном виде неприемлемо:

func authMiddleware(next http.HandlerFunc) http.HandlerFunc {    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {        tokenString := r.Header.Get("X-API-KEY")        if tokenString != "804b95f13b714ee9912b19861faf3d25" {            w.WriteHeader(http.StatusUnauthorized)            w.Write([]byte("Missing Authorization Header\n"))            return        }        next(w, r)    })}

Переходим к инициализации RabbitMQ. Тут мы будем использовать два Exchange и три очереди.

Первый Exchange VideoParserExchange. К нему подключены две очереди:

  • VideoParserWorkerQueue это основная очередь, которую будут слушать обработчики (на иллюстрации для примера приведен один обработчик Worker-0).

  • VideoParserArchiveQueue архивная очередь, в которую дублируются сообщения на случай возникновения ошибок. Вместо нее можно использовать другие средства бэкапирования, например хранилище S3.

У VideoParserExchange тип fanout, это значит, что все сообщения из него будут отправляться во все подключенные очереди одновременно.

Второй Exchange VideoParserRetryExchange, к нему подключена очередь VideoParserWorkerRetryQueue. К ней не подключены обработчики.

Архитектура очередей сообщенийАрхитектура очередей сообщений

Цель такого решения отложить попытки отправки сообщений на вышедшие из строя Worker до момента, когда они с большей долей вероятности смогут вернуться к обработке.

Например, если во время обработки сообщения из основной очереди обработчик по какой-то причине отключится и не обработает сообщение, то оно отправится в VideoParserRetryExchange. Этот переход настроен при помощи параметра x-dead-letter-exchange.

Далее VideoParserRetryExchange отправит сообщение в очередь VideoParserWorkerRetryQueue. В ней при помощи параметра x-message-ttl ограничено время хранения сообщения. Также при помощи параметра x-dead-letter-exchange мы указываем, что по прошествии таймаута сообщение должно вернуться в VideoParserExchange для последующей обработки.

Алгоритм работы очередей сообщенийАлгоритм работы очередей сообщений

Вся эта логика описана в функции initRabbit. Сначала мы объявляем два Exchange:

func initRabbit() error {    err := ch.ExchangeDeclare(        "VideoParserExchange", // name        "fanout",              // type        true,                  // durable        false,                 // auto delete        false,                 // internal        false,                 // no wait        nil,                   // arguments    )    if err != nil {        return err    }    err = ch.ExchangeDeclare(        "VideoParserRetryExchange", // name        "fanout",                   // type        true,                       // durable        false,                      // auto delete        false,                      // internal        false,                      // no wait        nil,                        // arguments    )    if err != nil {        return err    }

Далее инициализируются три очереди:

args := amqp.Table{"x-dead-letter-exchange": "VideoParserRetryExchange"}    queue, err = ch.QueueDeclare(        "VideoParserWorkerQueue", // name        true,                     // durable - flush to disk        false,                    // delete when unused        false,                    // exclusive - only accessible by the connection that declares        false,                    // no-wait - the queue will assume to be declared on the server        args,                     // arguments -    )    if err != nil {        return err    }    args = amqp.Table{"x-dead-letter-exchange": "VideoParserExchange", "x-message-ttl": 60000}    queue, err = ch.QueueDeclare(        "VideoParserWorkerRetryQueue", // name        true,                          // durable - flush to disk        false,                         // delete when unused        false,                         // exclusive - only accessible by the connection that declares        false,                         // no-wait - the queue will assume to be declared on the server        args,                          // arguments -    )    if err != nil {        return err    }    queue, err = ch.QueueDeclare(        "VideoParserArchiveQueue", // name        true,                      // durable - flush to disk        false,                     // delete when unused        false,                     // exclusive - only accessible by the connection that declares        false,                     // no-wait - the queue will assume to be declared on the server        nil,                       // arguments -    )    if err != nil {        return err    }

И далее очереди связываются с соответствующими Exchange: VideoParserExchange с очередями VideoParserWorkerQueue и VideoParserArchiveQueue, а VideoParserRetryExchange с очередью VideoParserWorkerRetryQueue:

err = ch.QueueBind("VideoParserWorkerQueue", "*", "VideoParserExchange", false, nil)    if err != nil {        return err    }    err = ch.QueueBind("VideoParserArchiveQueue", "*", "VideoParserExchange", false, nil)    if err != nil {        return err    }    err = ch.QueueBind("VideoParserWorkerRetryQueue", "*", "VideoParserRetryExchange", false, nil)    if err != nil {        return err    }    return nil}

Переходим к файлам миграций БД. Они находятся в отдельной папке migrations:

Devices_up.sql предназначен для создания таблицы requests. В ней содержатся следующие поля:

  • id уникальный идентификатор запроса;

  • name уникальное имя, которое мы будем передавать в API при создании нового запроса и в дальнейшем использовать его для поиска нужного запроса;

  • description описание запроса;

  • video_url ссылка на исходное видео на YouTube, в котором необходимо распарсить текст;

  • text_url ссылка на место хранения результирующего текстового файла в S3;

  • processed логический признак того, что обработка запроса успешно завершена;

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

  • created_at, updated_at временные метки для сохранения времени создания и последнего редактирования, соответственно.

Итак, создаем таблицу requests:

CREATE TABLE IF NOT EXISTS requests (    id SERIAL,    name VARCHAR(256),    description VARCHAR(2048),    video_url VARCHAR(64),    text_url VARCHAR(64),    processed BOOL DEFAULT FALSE,    archived BOOL DEFAULT FALSE,    created_at TIMESTAMP DEFAULT now(),    updated_at TIMESTAMP DEFAULT null,    UNIQUE(name));

В devices_down.sql описывается удаление таблицы requests:

DROP TABLE requests;

Переходим к файлу requests.go. В нем содержатся функции, которые обрабатывают запросы:

  • addRequest для добавления запроса;

  • updRequest для редактирования запроса;

  • delRequest для удаления запроса;

  • getRequest для получения запроса по имени;

  • getRequests для получения всех запросов.

Все функции довольно простые, в них выполняется проверка входных данных и отправка SQL-запроса в PostgreSQL. Поэтому приведем только фрагмент кода основной функции addRequest. Остальные функции можно посмотреть по ссылке выше.

Здесь происходит попытка отправить сообщение в VideoParserExchange, вывод сообщения в случае ошибки и добавление новой записи в таблицу requests, рассмотренную выше:

func addRequest(w http.ResponseWriter, r *http.Request) {    // Parsing event    req := postRequestRequest{}    err := json.NewDecoder(r.Body).Decode(&req)    if err != nil {        log.Printf("WARNING: Can't parse incoming request: %s\n", err)        returnResponse(400, "Can't parse json", nil, w)        return    }    request := Request{}    if req.Name == nil {        returnResponse(400, "name can't be null", nil, w)        return    }    request.Name = *req.Name    if req.Description != nil {        request.Description = *req.Description    }    if req.Processed != nil {        request.Processed = *req.Processed    }    if req.VideoURL != nil {        request.VideoURL = *req.VideoURL    }    if req.TextURL != nil {        request.TextURL = *req.TextURL    }    // Publishing data to rabbitmq    msg, err := json.Marshal(request)    if err != nil {        log.Printf("ERROR: Marshaling request: %s\n", err)        returnResponse(500, "Can't marshal request ", nil, w)        return    }    err = ch.Publish(        "VideoParserExchange", // exchange        "",                    // routing key        false,                 // mandatory - could return an error if there are no consumers or queue        false,                 // immediate        amqp.Publishing{            DeliveryMode: amqp.Persistent,            ContentType:  "application/json",            Body:         msg,        })    if err != nil {        log.Printf("ERROR: Publishing to rabbit: %s\n", err)        returnResponse(500, "Can't publish to rabbit ", nil, w)        return    }    stmt := `INSERT INTO requests (name, description, processed, video_url, text_url) VALUES ($1, $2, $3, $4, $5) RETURNING id`    err = db.QueryRow(stmt, &request.Name, &request.Description, &request.Processed, &request.VideoURL, &request.TextURL).Scan(&request.ID)    if err != nil {        log.Printf("ERROR: Adding new request to database: %s\n", err)        returnResponse(500, "Can't add new request ", nil, w)        return    }    returnResponse(200, "Successfully added new request", nil, w)}

В завершение рассмотрим Dockerfile, с помощью которого можно собрать приложение. Здесь используется образ golang-alpine, выполняется статическая компиляция, затем берется чистый alpine, куда переносится приложение со всеми миграциями и необходимыми файлами:

FROM golang:1.15-alpine AS build# Installing requirementsRUN apk add --update git && \    rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/*# Creating workdir and copying dependenciesWORKDIR /go/src/appCOPY . .# Installing dependenciesRUN go getENV CGO_ENABLED=0RUN go build -o api main.go requests.goFROM alpine:3.9.6RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing/" >> /etc/apk/repositories && \    apk add --update bash && \    rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/*WORKDIR /appCOPY --from=build /go/src/app/api /app/apiCOPY ./migrations/ /app/migrations/CMD ["/app/api"]

Создание БД PostgreSQL в облаке MCS

Базу данных для хранения статуса обработки запросов на конвертацию видео будем создавать из консоли управления облаком MCS. Для этого нужно выбрать пункт меню Базы данных и добавить БД PostgreSQL:

На первом шаге определяется конфигурация. Выберем последнюю версию PostgreSQL и тип конфигурации Single: для среды Dev нам достаточно единичного инстанса:

На следующем шаге указываем имя инстанса БД и выбираем конфигурацию виртуальной машины. Нам достаточно 1 CPU и 2 ГБ памяти. Для зоны доступности оставляем автоматический выбор:

В качестве диска выберем SSD размером 20 ГБ. Сеть можно создать отдельную, мы возьмем текущую. Внешний IP назначать не будем: база будет во внутренней сети. В настройках Firewall при необходимости можно указать ограничения на доступ, нам пока они не нужны все разрешаем. Создание реплики нам также не нужно. Ключ для доступа по SSH создаем свой. И устанавливаем периодичность резервного копирования раз в сутки:

На следующем шаге указываем имя БД, имя пользователя и генерируем пароль:

Далее запускается процесс создания инстанса, который займет некоторое время. После успешного создания параметры БД будут выведены на экран, в том числе внутренний IP-адрес сети, который впоследствии нам понадобится:

Установка RabbitMQ через Helm в Kubernetes

Для установки RabbitMQ воспользуемся Helm-чартом bitnami/rabbitmq. Достоинство чартов в том, что не нужно устанавливать по отдельности все необходимые сервису ресурсы: можно установить их одновременно в рамках общего релиза. А при изменениях в любом из ресурсов можно вынести новый релиз, в котором все обновления будут собраны воедино.

Создадим папку helm, добавим в нее репозиторий bitnami и найдем нужный нам Helm Chart bitnami/rabbitmq:

mkdir helmcd helmhelm repo add bitnami https://charts.bitnami.com/bitnamihelm search repo bitnami

Теперь мы нашли нужный чарт:

Копируем его имя, загружаем и распаковываем:

helm pull bitnami/rabbitmqtar zxv

Переходим в папку rabbitmq/templates. Здесь находятся все ресурсы, которые нужно будет создать в Kubernetes для корректной работы RabbitMQ: конфигурация, Ingress, сертификаты, сетевые политики, сервисные аккаунты, секреты, правила Prometheus и так далее. И Helm позволяет это сделать единой командой, без установки каждого файла по отдельности:

Возвращаемся в родительскую папку helm, чтобы посмотреть возможность настройки файла values.yaml. Скопируем содержимое rabbitmq/values.yaml в наш собственный файл values.dev.yaml и откроем его для редактирования:

cp rabbitmq/values.yaml ./values.dev.yamlvi values.dev.yaml

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

В данном файле содержится очень много параметров, которые можно настраивать под нужды своего проекта: режим debug, плагины RabbitMQ для подключения, необходимость включения TLS и memoryHighWatermark, аутентификация через LDAP, количество реплик, nodeSelector для создания RabbitMQ на нодах с определенной меткой, требования к CPU и памяти и многое другое.

Нас в первую очередь интересуют настройки Ingress. Находим секцию ingress, устанавливаем в enabled значение true и прописываем в поле hostname имя rabbitmq.stage.kis.im. Эта настройка необходима для внешнего доступа к RabbitMQ, без нее он будет доступен только внутри кластера. Kis.im это мой существующий домен:

Далее переходим непосредственно к развертыванию RabbitMQ. Создаем новый namespace stage и применяем к нему созданный файл values.stage.yaml (изменив dev на stage в названии для единообразия):

kubectl create ns stagehelm instal -n stage rabbitmq -f values.dev.yamlmv values.dev.yaml values. stage. yamlhelm install -n stage rabbitmq -f values.stage.yanl ./rabbitmq/

Вот, что получилось, когда Namespace создан:

После успешной установки можно посмотреть список подов и сервисов в Namespace stage rabbitmq успешно добавлен. Он имеет кластерный IP 10.254.178.84. Но так как наше приложение будет находиться в том же Namespace, мы сможем обращаться к нему по имени rabbitmq.

Еще один сервис rabbitmq-headless не имеет кластерного IP. Он используется при добавлении нескольких RabbitMQ для их автообнаружения и объединения в кластер с помощью kubectl -n stage get svc:

С помощью Helm можно получить дополнительные сведения о релизе: время последнего обновления, статус, название чарта, версию приложения, используем helm -n stage list:

Кроме этого, можно посмотреть Persistent Volumes, выделенные RabbitMQ, с помощью kubectl get pv. В нашем случае Volume имеет размер 8 ГБ и Storage Class csi-hdd:

При необходимости нужный Storage Class можно было прописать непосредственно в YAML-файле:

Список всех возможных классов можно вывести командой kubectl get storageclasses:

Здесь важен параметр RECLAIMPOLICY: в зависимости от его значения при удалении запроса на данный ресурс (PVC, Persistent Volume Claim) сам Persistent Volume будет удален или сохранен для будущего использования.

Осталось обеспечить внешний доступ к нашему сервису. Проверяем добавление ресурса Ingress для RabbitMQ командой kubectl -n stage get ingress:

Затем получаем внешний адрес Ingress Controller с помощью kubectl -n ingress-nginx get svc:

В Cloudflare прописываем DNS для RabbitMQ, связывая его внешний Hostname и IP-адрес Ingress Controller:

После этого RabbitMQ становится доступен по адресу rabbitmq.stage.kis.im:

Имя пользователя user. Пароль сохранился в переменные окружения после развертывания RabbitMQ, его можно получить с помощью команды env | grep RABBITMQ_PASSWORD.

Развертывание и предварительная проверка API

RabbitMQ мы развернули с помощью Helm. Для нашего приложения с API в последующем мы также создадим собственный Helm Chart, но пока посмотрим, как выполняется развертывание приложения вручную на основе YAML-файлов.

Образ приложения мною уже создан при помощи Dockerfile, который мы рассматривали ранее.

Далее определим необходимые ресурсы. Очевидно, что локальное хранилище приложению не нужно, так как приложение уже взаимодействует с PostgreSQL и RabbitMQ, размещенными в облаке. Поэтому Persistent Volumes создавать не будем. Основные ресурсы, которые нам потребуются, описывают файлы deployment.yaml, ingress.yaml и svc.yaml:

Начнем с deployment.yaml. Здесь описывается ресурс Deployment. Тут мы описываем шаблон пода, который будем запускать. Указываем, что будем запускать контейнер с именем api, образ vozerov/video-api:v1 (этот образ я уже залил на hub.docker.com).

Далее в блоке env указываем переменные, используемые в нашем API:

  • В переменной RABBIT_URI вводим сформированные при создании RabbitMQ имя и пароль пользователя, название сервиса rabbitmq и номер порта 5672 (имя сервиса можно проверить с помощью команды kubectl -n stage get svc).

  • В переменной LISTEN устанавливаем номер порта 8080.

  • В переменной PGSQL_URI заполняем сформированные при создании PostgreSQL имя и пароль пользователя, внутренний адрес БД 10.0.0.10, номер порта 5432 и название БД vc-dev. Все параметры БД можно найти в консоли управления облаком.

deployment.yaml: описываем шаблон подаdeployment.yaml: описываем шаблон пода

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

Применяем сформированный файл:

kubectl -n stage apply -f deployment.yamlkubectl -n stage get deploy

Video-api создан:

И проверяем создание нового пода с помощью kubectl -n stage get pods:

После успешного применения deployment.yaml можно зайти в RabbitMQ и убедиться в создании всех необходимых очередей и Exchange.

Созданные очередиСозданные очередиСозданные ExchangeСозданные Exchange

Следующий ресурс, который нам необходимо добавить для доступа к сервису извне это Service. Он описывается в файле svc.yaml. Мы указываем, что приложение video-api будет принимать входящие соединения на порт 8080 и пробрасывать их в контейнер на порт 8080. Применяем svc.yaml стандартной командой kubectl apply -n stage -f svc.yaml:

Последний ресурс, который необходим для нашего сервиса Ingress. В файле ingress.yaml мы указываем правила, по которым нужно направлять запросы к сервису. Заполняем внешнее имя api.stage.kis.im и в блоке path указываем, что все корневые запросы направляем на сервис video-api-svc, созданный на прошлом шаге. Применяем сформированный файл kubectl apply -n stage -f Ingress.yaml:

Убеждаемся в добавлении Ingress для нашего сервиса с помощью kubectl -n stage get ingress:

Затем добавляем запись в DNS аналогично тому, как делали это ранее для RabbitMQ:

Теперь можно провести первое тестирование API, используя отправку запросов через curl. В заголовках всех запросов нужно передавать X-API-KEY со значением токена из кода программы main.go.

Для начала с помощью метода GET получим список всех записей requests:

curl -H 'X-API-KEY: 804b95f13b714ee9912b19861faf3d25' -s http://api.stage.kis.im/requests | jq .

На текущий момент он пуст:

Отправим новый запрос на конвертацию видео, используя метод POST. В имени запроса (name) укажем test1. В ссылке на видео (video_url) введем тестовое значение, так как у нас пока нет обработчиков Worker:

curl -X POST -d '{"name": "test1", "video_url": "https://google.com" }' -H 'X-API-KEY: 804b95f13b714ee9912b19861faf3d25' -s http://api.stage.kis.im/requests | jq .

Запрос успешно создан:

Далее можно получить запрос по имени test1 и убедиться в наличии всех переданных при создании параметров:

curl -H 'X-API-KEY: 804b95f13b714ee9912b19861faf3d25' -s http://api.stage.kis.im/requests/request1 | jq .

Запрос создан, все параметры верные:

В очереди RabbitMQ сообщение также будет добавлено. Заходим в очередь:

Видим сообщение:

Осталось зайти в базу PostgreSQL и проверить ее структуру. Внешний доступ мы не настраивали поэтому можно подключиться, например, через psql из отдельно запущенного пода. Мы видим наличие таблицы requests, а в ней добавленный нами запрос:

Таким образом, проверка работы API пройдена.

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

Новым пользователям платформы Mail.ru Cloud Solutions доступны 3000 бонусов после полной верификации аккаунта. Вы сможете повторить сценарий из статьи или попробовать другие облачные сервисы.

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

Что еще почитать по теме:

  1. Как развернуть кластер Kubernetes на платформе MCS.

  2. Запускаем etcd-кластер для Kubernetes.

  3. Как устроен Kubernetes aaS на платформе Mail.ru Cloud Solutions.

Подробнее..

Перевод Apache Spark 3.1 Spark on Kubernetes теперь общедоступен

22.04.2021 12:11:11 | Автор: admin


С выходом Apache Spark 3.1 в марте 2021-го проект Spark on Kubernetes официально перешел в статус общедоступного и готового к эксплуатации. Это стало результатом трехлетней работы быстрорастущего сообщества, участники которого помогали в разработке и внедрении (изначально поддержка Spark on Kubernetes появилась в Spark 2.3 в феврале 2018 года). Команда Kubernetes aaS от Mail.ru Cloud Solutions перевела самое важное из статьи об основных возможностях Spark 3.1, в которой автор подробно остановился на улучшениях в Spark on Kubernetes.


Полезные источники информации:



Путь Spark on Kubernetes: от бета-поддержки в 2.3 до нового стандарта в 3.1


С выходом Spark 2.3 в начале 2018 года Kubernetes стал новым диспетчером для Spark (помимо YARN, Mesos и автономного режима) в крупных компаниях, возглавляющих проект: RedHat, Palantir, Google, Bloomberg и Lyft. Сначала поддержка имела статус экспериментальной, функций было мало, стабильность и производительность были невысокими.


С тех пор сообщество проекта получило поддержку многочисленных крупных и маленьких компаний, которых заинтересовали преимущества Kubernetes:


  1. Нативная контейнеризация. Упаковывайте зависимости (и сам Spark) с помощью Docker.
  2. Эффективное совместное использование ресурсов и ускорение запуска приложений.
  3. Обширная Open Source-экосистема уменьшает зависимость от облачных провайдеров и вендоров.

В проект было внесено несколько нововведений: от базовых требований вроде поддержки PySpark и R, клиентского режима и монтирования томов в версии 2.4 до мощных оптимизаций вроде динамического выделения (в 3.0) и улучшения обработки выключения нод (в 3.1). За последние три года вышло больше 500 патчей, сильно повысивших надежность и производительность Spark on Kubernetes.



График внесения улучшений в Spark с 2018 по 2021 годы


В результате в новых Spark-проектах в 2021 году Kubernetes все чаще рассматривается в роли стандартного менеджера ресурсов: это следует из популярности Open Source-проекта оператора Spark on Kubernetes и объявлений крупных вендоров, внедряющих Kubernetes вместо Hadoop YARN.


С выходом Spark 3.1 проект Spark on Kubernetes получил статус общедоступного и готового к эксплуатации. В этом релизе было внесено больше 70 исправлений и улучшений производительности. Давайте рассмотрим самые важные функции, которых с нетерпением ждали заказчики.


Улучшенная обработка выключения нод: постепенное отключение исполнителя (новая функция в Spark 3.1)


Эту функцию (SPARK-20624) реализовал Holden Karau, и пока что она доступна только для автономных развертываний и в Kubernetes. Называется функция улучшенная обработка выключения нод, хотя еще одно подходящее название постепенное отключение исполнителя (Graceful Executor Decommissioning).


Эта функция повышает надежность и производительность Spark при использовании Spot-нод (вытесняемые ноды в GCP). Перед остановкой спота с него перемещаются shuffle-данные и содержимое кэша, поэтому влияние на работу Spark-приложения оказывается минимальное. Раньше, когда система убивала спот, все shuffle-файлы терялись, поэтому их приходилось вычислять заново (снова выполнять потенциально очень долгие задачи). Новая фича не требует настройки внешнего shuffle-сервиса, для которого нужно по запросу запускать дорогие ноды хранения и который совместим с Kubernetes.



Новая функция Spark предотвращает внезапное уничтожение спотов и постепенно выключает исполнитель без потери драгоценных данных!


Что делает эта функция?


  • Исполнитель, который нужно выключить, вносится в черный список: драйвер Spark не будет назначать ему новые задачи. Те задачи, которые сейчас исполняются, не будут принудительно прерваны, но если они сбоят из-за остановки исполнителя, то перезапустятся в другом исполнителе, как и сейчас, а их сбой не будет учитываться в максимальном количестве сбоев (новинка).
  • Shuffle-файлы и кэшированные данные мигрируют из выключаемого исполнителя в другой. Если другого нет, например, мы выключаем единственный исполнитель, то можно настроить объектное хранилище (вроде S3) в качестве запасного.
  • После завершения миграции исполнитель умирает, а Spark-приложение продолжает работать как ни в чем не бывало.

Когда работает эта функция?


  • При использовании Spot / вытесняемых нод облачный провайдер уведомляет о выключении за 60120 секунд. Spark может использовать это время для сохранения важных shuffle-файлов. Этот механизм используется и в тех случаях, когда экземпляр на стороне провайдера по каким-то причинам выключают, допустим, при обслуживании EC2.
  • Когда нода Kubernetes пустеет, например для технического обслуживания, или когда вытесняется под исполнителя Spark, например более высокоприоритетным подом.
  • Когда убранный исполнитель является частью динамического выделения при уменьшении размера системы из-за простоя исполнителя. В этом случае тоже будут сохранены кэш и shuffle-файлы.

Как включить функцию?


  • С помощью конфигурационных флагов. Нужно включить четыре основных флага Spark:


    • spark.decommission.enabled;
    • spark.storage.decommission.rddBlocks.enabled;
    • spark.storage.decommission.shuffleBlocks.enabled;
    • spark.storage.decommission.enabled.

    Другие доступные настройки рекомендую поискать прямо в исходном коде.


  • Для получения предупреждения от облачного провайдера о скором отключении ноды, например из-за выключения спота, нужна специфическая интеграция.



Новые опции с томами в Spark on Kubernetes


Начиная со Spark 2.4 при использовании Spark on Kubernetes можно монтировать три типа томов:


  1. emptyDir: изначально пустая директория, существующая, пока работает под. Полезна для временного хранения. Можно поддерживать ее с помощью диска ноды, SSD или сетевого хранилища.
  2. hostpath: в ваш под монтируется директория прямо из текущей ноды.
  3. Заранее статически создаваемый PersistentVolumeClaim. Это Kubernetes-абстракция для разных типов персистентного хранилища. Пользователь должен создать PersistentVolumeClaim заранее, существование тома не привязано к поду.

В Spark 3.1 появилось два новых варианта: NFS и динамически создаваемый PersistentVolumeClaims.


NFS это том, который могут одновременно использовать несколько подов и который можно заранее наполнить данными. Это один из способов обмена информацией, кодом и конфигурациями между Spark-приложениями либо между драйвером и исполнителем внутри какого-нибудь Spark-приложения. В Kubernetes нет NFS-сервера, вы можете запустить его самостоятельно или использовать облачный сервис.


После создания NFS вы можете легко монтировать этот том в Spark-приложение с помощью таких настроек:


spark.kubernetes.driver.volumes.nfs.myshare.mount.path=/sharedspark.kubernetes.driver.volumes.nfs.myshare.mount.readOnly=falsespark.kubernetes.driver.volumes.nfs.myshare.options.server=nfs.example.comspark.kubernetes.driver.volumes.nfs.myshare.options.path=/storage/shared


NFS (Network File System) популярный способ обмена данными между любыми Spark-приложениями. Теперь он работает и поверх Kubernetes


Второй новый вариант динамический PVC. Это более удобный способ использования персистентных томов. Раньше нужно было сначала создавать PVC, а затем монтировать их. Но при использовании динамического выделения вы не знаете, сколько можно создать исполнителей в ходе работы вашего приложения, поэтому старый способ был неудобен. К тому же приходилось самостоятельно вычищать ненужные PersistentVolumeClaims либо смиряться с потерей места в хранилище.


Со Spark 3.1 все стало динамическим и автоматизированным. Когда вы инициализируете Spark-приложение или в ходе динамического выделения запрашиваете новые исполнители, в Kubernetes динамически создается PersistentVolumeClaims, который автоматически предоставляет новый PersistentVolumes запрошенного вами класса хранилища. При удалении пода ассоциированные с ним ресурсы автоматически удаляются.


Другие функции Spark 3.1: PySpark UX, стейджинговая диспетчеризация, повышение производительности


В Spark 3.1 появилось два крупных улучшения в UX для разработчиков на PySpark:


  • Документация PySpark полностью переделана, теперь она больше соответствует Python и удобна для использования.
  • Теперь поддерживаются подсказки типов: получение в IDE бесплатного автозавершения кода и обнаружения статических ошибок.


Автозавершение кода в PySpark с выходом Apache Spark 3.1


Spark History Server, который отображает интерфейс Spark после завершения вашего приложения, теперь может показывать статистику выполненных вами запросов на Structured Streaming.


Стейджинговая диспетчеризация (SPARK-27495) применима только для YARN- и Kubernetes-развертываний при включенном динамическом выделении. Она позволяет вам управлять в коде количеством и типом запрашиваемых для исполнителя ресурсов с точностью на уровне стадий. В частности, вы можете настроить приложение под использование исполнителей с процессорными ресурсами в ходе первой стадии (например, при выполнении ETL или подготовке данных), а в ходе второй стадии на использование видеокарт (скажем, для моделей машинного обучения).


В Spark 3.1 улучшили производительность shuffle хеш-соединения, добавили новые правила для прерывания подвыражений и в оптимизатор Catalyst. Для пользователей PySpark будет полезно то, что в Spark теперь применяется колоночный формат in-memory хранения Apache Arrow 2.0.0 вместо 1.0.2. Это повысит скорость работы приложений, особенно если вам нужно преобразовывать данные между Spark и фреймами данных Pandas. Причем все эти улучшения производительности не потребуют от вас менять код или конфигурацию.


Что еще почитать по теме:


  1. Как и зачем разворачивать приложение на Apache Spark в Kubernetes.
  2. Наш телеграм-канал Вокруг Kubernetes в Mail.ru Group.
  3. MLOps без боли в облаке: как развернуть Kubeflow в production-кластере Kubernetes.
Подробнее..

Self-Hosted, или Kubernetes для богатых почему самостоятельное развертывание кластера не всегда способ сэкономить

02.06.2021 18:06:54 | Автор: admin


Идея самостоятельно развернуть кластер Kubernetes на собственных серверах или в облаке выглядит привлекательной: кажется, что это дешевле, чем платить за Managed-решение от провайдера. На самом деле все не так однозначно: на практике можно обнаружить скрытые расходы и подводные камни.


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


Я Дмитрий Лазаренко, директор по продукту облачной платформы Mail.ru Cloud Solutions (MCS). В статье расскажу, в чем особенности развертывания Self-Hosted-кластера Kubernetes и о чем нужно знать перед запуском.


Для старта понадобятся время, деньги и администраторы, разбирающиеся в Kubernetes


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


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


В реальности развернуть кластер только половина дела. В таком виде он будет работать до первой проблемы, которая неизбежно возникнет через неделю или месяц. Например, перестанут создаваться поды из-за неверной конфигурации ресурсов на controller-manager. Или кластер начнет работать нестабильно из-за проблем с дисками у etcd. Или запущенные СronJob из-за ошибок controller-manager начнут бесконечно плодить новые поды. Или в кластере будут возникать сетевые ошибки из-за неправильного выбора конфигурации DNS.


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


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


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


Конечно, если запускать Kubernetes только ради деплоя контейнеров, то можно не разбираться и не развивать кластер. Но тогда возникает вопрос: зачем вам Kubernetes? Можно взять более простой в настройке и поддержке инструмент, тот же Docker Swarm. Если вы хотите от Kubernetes что-то простое, просто его не используйте. Нет смысла тратить время на развертывание кластера лишь ради запуска простого кода. Эта технология предназначена для проектов, где постоянно идет разработка, часто запускаются новые релизы и нужно выдерживать требования HighLoad.

По этой причине Self-Hosted Kubernetes в большинстве случаев могут успешно запустить только крупные компании, где есть возможность выделить сотрудников для обслуживания кластера и нет потребности экономить ресурсы.


Кроме того, самостоятельное развертывание кластера дело небыстрое. Если понадобится запустить кластер в короткие сроки для проекта или тестовых сред, то на Self-Hosted это не выйдет: развертывание займет несколько часов, а то и недель. К этому стоит быть готовыми. Для сравнения: в облаке вы запустите кластер KaaS за 10 минут и сможете сразу его использовать, но это получается потому, что над инфраструктурной частью уже заранее поработали специалисты провайдера.


Kubernetes требует прокачки: он не работает сам по себе


Как я уже говорил выше, Kubernetes отдельная экосистема, которой нужно заниматься и подключать к ней дополнительные инструменты. Если брать Self-Hosted, то все это придется делать самостоятельно.


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


Например, понадобится мониторить и сам кластер, и приложения в нем. Причем стандартного мониторинга через Zabbix вам не хватит, потребуется специфический Prometheus или Telegraph.


С логами аналогичная ситуация: из коробки вы получите только историю логов для уже запущенных приложений, при передеплое она исчезнет. Вручную собирать логи с Kubernetes не получится, нужно подключать сборщики логов вроде Fluentd и систему хранения, например Elasticsearch или Loki. Отдельно придется заниматься балансировкой нагрузки: понадобится отказоустойчивый балансер вроде MetalLB.


Системы хранения для Self-Hosted Kubernetes еще одна головная боль


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


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


Для нормальной работы приложения без изменения его логики понадобятся Persistent Volumes хранилища, связанные с подами. Они подключаются внутрь контейнеров как локальные директории, позволяя приложению хранить данные под собой. Среди рабочих вариантов CephFS, Glusterfs, FC (Fiber Channel), полный список СХД можно посмотреть в официальной документации.


Интеграция Kubernetes c Persistent Volumes нетривиальная задача. Чтобы развернуть тот же Ceph, недостаточно взять мануал с Хабра и выполнить ряд команд. Плюс в дальнейшем СХД должен кто-то заниматься опять нужен отдельный инженер, а то и несколько.


Если же Self-Hosted-кластер развернут не на железе, а на виртуальных машинах в облаке, то все немного проще собственный кластер Ceph поднимать не нужно. Можно взять кластер хранилища у провайдера и научить его работать с кластером K8s, если провайдер готов предоставить вам API к своей системе хранения данных, что есть не везде. Писать интеграцию при этом придется самостоятельно.


Правда, у провайдеров, предоставляющих IaaS, можно арендовать объектное хранилище или облачную СУБД, но только если логика приложения позволяет их использовать. А в Managed-решениях Kubernetes уже из коробки есть интегрированные Persistent Volumes.


Отказоустойчивость кластера отдельная проблема


С Kubernetes проще обеспечить отказоустойчивость приложений, однако потребуется еще и реализовать отказоустойчивость кластера.


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


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


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


Резерв зависит от того, какое количество вышедших из строя нод вероятно в вашем случае:


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

Если проще, то это выглядит так: когда в кластере 10 нод и вы хотите без проблем пережить потерю одной ноды, то вам потребуется 10-процентный запас ресурсов. Если же приложения должны работать даже при потере 50% кластера, значит, на всех нодах нужен запас в 50%.


При этом лучше, если ноды в кластере небольшие по объему, но их много. Допустим, у вас есть пул ресурсов 100 ГБ оперативной памяти и 100 ядер CPU. Такой объем позволяет запустить 10 виртуалок и 10 нод кластера Kubernetes. И в случае выхода из строя одной ноды вы теряете только 10% кластера.

На железных серверах такую конфигурацию не создашь. Например, используя 300 ГБ оперативной памяти и 50 ядер CPU, вы развернете всего 23 ноды кластера. И в случае выхода из строя одной ноды рискуете сразу потерять 3050% кластера.

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


Автомасштабирование кластера нетривиальная задача


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


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


Плюс если мы говорим о Self-Hosted на Bare Metal, то все серверы, необходимые для работы приложений на случай нагрузки, придется держать в рабочем состоянии и постоянно за них платить.


Если Self-Hosted-кластер развернут на IaaS, то схема похожая: инженер добавляет новую виртуальную машину и вносит ее в кластер. Другой вариант взять API провайдера, если он его предоставляет, подключить через него кластер Kubernetes, научить его запускать для себя новые серверы и так реализовать автомасштабирование. Но потребуется разрабатывать отдельное решение это сложная задача, предполагающая высокий уровень экспертности в Kubernetes и облаках.


Кроме того, для быстрого масштабирования Self-Hosted-кластера на IaaS придется резервировать нужное количество ресурсов провайдера и создавать из них новые виртуальные машины по мере надобности. И за эти зарезервированные ресурсы придется платить: практика брать плату за выключенные ресурсы бывает у реселлеров VMware. На нашей платформе в случае отключенных ВМ вы не платите за ресурсы, только за диски. В некоторых Managed-решениях автоскейлинг включается по кнопке, уточните эту возможность у вашего провайдера.


Подводные камни Self-Hosted Kubernetes


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

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


Тут можно почитать, как устроен наш Kubernetes aaS на платформе Mail.ru Cloud Solutions: что у него под капотом и что в него еще входит, кроме собственно Kubernetes.
Подробнее..

Tarantool vs Redis что умеют in-memory технологии

01.04.2021 18:23:17 | Автор: admin

В этой статье я хочу сравнить Redis и Tarantool. У меня нет цели сделать громогласный вывод Tarantool лучше! или Redis круче!. Я хочу понять их сходства и отличия, разобраться, для каких задач какую технологию выбрать. Потому что это очень близкие на первый взгляд вещи, и вопросы про их отличия я вижу часто.

Для этого мы посмотрим на технологии в трёх частях:

  • Вначале посмотрим глазами новичка. Что такое БД в памяти? Какие задачи они решают лучше дисковых БД?
  • Потом посмотрим архитектурно. Как обстоит вопрос с производительностью, надёжностью, масштабированием?
  • В третьей части лезем в технические вещи поглубже. Типы данных, итераторы, индексы, транзакции, ЯП, репликация, коннекторы.

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

Поехали!

Содержание


  1. Вводная часть
    • Что такое БД в памяти
    • Зачем нужны решения в памяти
    • Что такое Redis
    • Что такое Tarantool
  2. Архитектурная часть
    • Производительность
    • Надёжность
    • Масштабируемость
    • Валидация схемы данных
  3. Технические особенности
    • Какие типы данных можно хранить
    • Вытеснение данных
    • Итерация по ключам
    • Вторичные индексы
    • Транзакции
    • Персистентность
    • Язык программирования для хранимых процедур
    • Репликация
    • Коннекторы из других языков программирования
    • Под какие задачи плохо подходят
    • Экосистема
    • Чем Redis лучше
    • Чем Tarantool лучше
  4. Вывод
  5. Ссылки

1. Вводная часть


Что такое БД в памяти


Redis и Tarantool это in-memory технологии. Их ещё называют резидентными БД, но я буду писать короче в памяти или in-memory. Так что такое БД в памяти?

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

Если данных слишком много, БД в памяти способны сохранять данные на диск. Можно перезагрузить узел и не потерять информацию. Стереотип про ненадёжность БД в памяти сильно устарел, их можно использовать как основное хранилище в production. Например, Mail.ru Cloud Solutions использует Tarantool как основную БД для хранения метаинформации в своём объектном хранилище [1].

БД в памяти нужны для высокой скорости доступа к данным, условно от 10 000 запросов в секунду. Например, запросы к ленте новостей Кинопоиска в день релиза Снайдерката Лиги Справедливости, Яндекс.Маркет перед Новым Годом или Delivery Club вечером в пятницу.

Зачем нужны решения в памяти


Кеш. БД в памяти традиционно используют как кеш для более медленных баз данных. Это логично, память быстрее диска (даже SSD). Но в жизненном цикле любого кеша случаются перезагрузки, падения, сетевая недоступность, нехватка памяти и прочие инфраструктурные беды.

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

  • Персистентность кеши сохраняют данные на диск. После перезагрузки восстанавливаем своё состояние без обращений к основному хранилищу. Если этого не делать, то обращение к холодному кешу будет очень долгим или даже положит основную БД.
  • Резервирование кеши содержат функции репликации данных. Если упал один узел, то второй возьмёт на себя запросы. Основное хранилище не упадёт от перегруза, нас спасает резервная нода.
  • Шардирование если горячие данные не помещаются в оперативную память одного узла, мы используем несколько узлов параллельно. Это горизонтальное масштабирование.

Шардирование это большая система. Резервирование это надёжная система. Вместе с персистентностью получается кластерное хранилище данных. Можно положить туда терабайты информации и крутить их на скорости 1 000 000 RPS.


OLTP. Расшифровывается как Online Transaction Processing, обработка транзакций в реальном времени. In-memory решения подходят для такого типа задач благодаря своей архитектуре. OLTP это большое количество коротких on-line транзакций (INSERT, UPDATE, DELETE). Главное в OLTP-системах быстро обработать запросы и обеспечить целостность данных. Эффективность чаще всего определяется количеством RPS.

Что такое Redis


  • Redis называет себя in-memory хранилище структур данных.
  • Redis это key-value.
  • Больше всего известен по кешированию дисковых баз. Если вы будете искать по ключевым словам кеширование баз данных, то в каждой статье найдёте упоминания Redis.
  • Redis поддерживает первичные индексы, не поддерживает вторичные.
  • Redis содержит в себе механизм хранимых процедур на Lua.

Что такое Tarantool


  • Tarantool называет себя платформа для in-memory вычислений.
  • Tarantool умеет в key-value. А еще в документы и реляционную модель данных.
  • Создан для горячих данных кеширования MySQL в соцсети. С течением времени развился в полноценную базу данных.
  • Tarantool может строить произвольное количество индексов по данным.
  • В Tarantool тоже можно написать хранимую процедуру и тоже на Lua.

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

2. Архитектурная часть


Производительность


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

MacBook Pro 2,9 GHz Quad-Core Intel Core i7Redis version=6.0.9, bits=64Tarantool 2.6.2

Redis

redis_test.gopackage mainimport (    "context"    "fmt"    "log"    "math/rand"    "testing"    "github.com/go-redis/redis")func BenchmarkSetRandomRedisParallel(b *testing.B) {    client2 := redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379", Password: "", DB: 0})    if _, err := client2.Ping(context.Background()).Result(); err != nil {        log.Fatal(err)    }    b.RunParallel(func(pb *testing.PB) {        for pb.Next() {            key := fmt.Sprintf("bench-%d", rand.Int31())            _, err := client2.Set(context.Background(), key, rand.Int31(), 0).Result()            if err != nil {                b.Fatal(err)            }        }    })}

Tarantool

tarantool>box.cfg{listen='127.0.0.1:3301', wal_mode='none', memtx_memory=2*1024*1024*1024}box.schema.user.grant('guest', 'super', nil, nil, {if_not_exists=true,})box.schema.space.create('kv', {if_not_exists=true,})box.space.kv:create_index('pkey', {type='TREE', parts={{field=1, type='str'}},                                   if_not_exists=true,})

tarantool_test.gopackage mainimport (    "fmt"    "math/rand"    "testing"    "github.com/tarantool/go-tarantool")type Tuple struct {    _msgpack struct{} `msgpack:",asArray"`    Key      string    Value    int32}func BenchmarkSetRandomTntParallel(b *testing.B) {    opts := tarantool.Opts{        User: "guest",    }    pconn2, err := tarantool.Connect("127.0.0.1:3301", opts)    if err != nil {        b.Fatal(err)    }    b.RunParallel(func(pb *testing.PB) {        var tuple Tuple        for pb.Next() {            tuple.Key = fmt.Sprintf("bench-%d", rand.Int31())            tuple.Value = rand.Int31()            _, err := pconn2.Replace("kv", tuple)            if err != nil {                b.Fatal(err)            }        }    })}

Запуск Чтобы полностью прогрузить базы данных, используем больше потоков.

go test -cpu 12 -test.bench . -test.benchtime 10sgoos: darwingoarch: amd64BenchmarkSetRandomRedisParallel-12          929368         15839 ns/opBenchmarkSetRandomTntParallel-12            972978         12749 ns/op

Результаты. Среднее время запроса к Redis составило 15 микросекунд, к Tarantool 12 микросекунд. Это даёт Redis 63 135 RPS, Tarantool 78 437 RPS.

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


Надёжность


Для надёжности хранения данных используют две основные техники:

  • Персистентность. При перезагрузке БД загрузит свои данные с диска, не будет запросов в сторонние системы.
  • Репликация. Если упал один узел, то есть копия на втором. Бывает асинхронная и синхронная.

И Redis, и Tarantool содержат эти функции. Технические подробности мы рассмотрим далее.

Масштабируемость


Масштабирование может рассматриваться для двух задач:

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

Redis

Узлы Redis можно соединить друг с другом асинхронной репликацией. Такие узлы будем называть репликационной группой, или replica set. Управлением такой репликационной группой занимается Redis Sentinel.

Redis Sentinel это один или несколько объединенных в кластер специальных процессов, которые следят за узлами Redis. Они выполняют четыре основные задачи:

  • Мониторинг узлов в группе: живой или мертвый.
  • Уведомление администратора или какой-то системы, если что-то случилось в группе.
  • Автоматическое переключение мастера.
  • Провайдер конфигурации для внешних клиентов, чтобы они знали, к кому подключиться.

В случае, когда данные необходимо расшардировать на несколько узлов, Redis предлагает open source-версию Redis Cluster. Она позволяет построить кластер, состоящий из нескольких репликационных групп. Данные в кластере шардируются по 16 384 слотам. Диапазоны слотов распределяются между узлами Redis.

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

Tarantool

Tarantool также содержит в себе оба механизма масштабирования: репликацию и шардирование. Основной инструмент управления масштабированием Tarantool Cartridge. Он объединяет узлы в репликационные группы. В этой ситуации вы можете построить одну такую репликационную группу и использовать её аналогично Redis Sentinel. Tarantool Cartridge может управлять несколькими репликационными группами и шардировать данные между ними. Шардирование выполняется с помощью библиотеки vshard.

Различия

Администрирование

  • Администрирование Redis Cluster с помощью скриптов и команд.
  • В Tarantool Cartridge администрирование с помощью web-интерфейса или через API.

Корзины шардирования

  • Количество корзин шардирования в Redis фиксированное, 16 тыс.
  • Количество корзин шардирования Tarantool Cartridge (vshard) произвольное. Указывается один раз при создании кластера.

Ребалансировка корзин (решардинг)

  • В Redis Cluster настройка и запуск вручную.
  • В Tarantool Cartridge (vshard) автоматически.

Маршрутизация запросов

  • Маршрутизация запросов в Redis Cluster происходит на стороне клиентского приложения.
  • В Tarantool Cartridge маршрутизация запросов происходит на узлах-роутерах кластера.

Инфраструктура

  • Tarantool Cartridge также содержит:

    • механизм map/reduce запросов;
    • утилиту по упаковке приложения в пакеты rpm, dep и tar.gz;
    • Аnsible-роль для автоматического развёртывания приложения;
    • экспорт параметров мониторинга кластера.


Валидация схемы данных


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

В Tarantool на стороне сервера можно использовать валидацию по схеме данных:

  • с помощью встроенной валидации box.space.format, которая затрагивает только верхний уровень полей;
  • с помощью установленного расширения avro-schema.

3. Технические особенности


Какие типы данных можно хранить


В Redis ключом может быть только строка. В Redis можно хранить и манипулировать следующими типами данных:

  • строки;
  • списки строк;
  • неупорядоченные множества строк;
  • хешмапы или просто строковые пары ключ-значение;
  • упорядоченные множества строк;
  • Bitmap и HyperLogLog.

В Tarantool можно хранить и манипулировать следующими типами данных:

  • Атомарными:
    • строки;
    • логический тип (истина, ложь);
    • целочисленный;
    • с плавающей запятой;
    • с десятичной плавающей запятой;
    • UUID.

  • Комплексными:
    • массивы;
    • хешмапы.


Типы данных Redis лучше подходят для счётчиков событий, в том числе уникальных, для хранения небольших готовых витрин данных. А типы данных Tarantool лучше подходят для хранения объектов и/или документов, как в SQL и NoSQL СУБД.

Вытеснение данных


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

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

  • TTL вытеснение объектов по завершении срока жизни;
  • LRU вытеснение давно использованных данных;
  • RANDOM вытеснение случайно попавшихся под руку объектов;
  • LFU вытеснение редко используемых данных.

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

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

Итерация по ключам


В Redis можно это сделать с помощью операторов:

  • SCAN;
  • итерация по ключам.

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

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

Например:

results = {}for _, tuple in box.space.pairs('key', 'GE') do    if tuple['value'] > 10 then        table.insert(results, tuple)  endendreturn results

Вторичные индексы


Redis

У Redis нет вторичных индексов. Есть некоторые трюки, чтобы их имитировать:

  • В упорядоченных множествах можно использовать порядковый номер элемента как вторичный ключ.
  • Использовать хешмапы, ключ которых является, в некотором смысле, индексом данных.

Tarantool

В Tarantool можно строить произвольное количество вторичных индексов для данных:

  • Вторичные ключи могут состоять из нескольких полей.
  • Для вторичных индексов можно использовать типы HASH, TREE, RTREE, BITSET.
  • Вторичные индексы могут содержать уникальные и не уникальные ключи.
  • У любых индексов можно использовать настройки локали, например, для регистронезависимых строковых значений.
  • Вторичные индексы могут строиться по полям с массивом значений (иногда их называют мультииндексы).

Вывод

Вторичные ключи и удобные итераторы позволяют строить в Tarantool реляционные модели хранения данных. В Redis такую модель построить невозможно.

Транзакции


Механизм транзакций позволяет выполнить несколько операций атомарно. И Redis, и Tarantool поддерживают транзакции. Пример транзакции в Redis:

> MULTIOK> INCR fooQUEUED> INCR barQUEUED> EXEC1) (integer) 12) (integer) 1

Пример транзакции в Tarantool:

do  box.begin()  box.space.kv:update('foo', {{'+', 'value', 1}})  box.space.kv:update('bar', {{'+', 'value', 1}})  box.commit()end

Персистентность


Персистентность данных обеспечивается двумя механизмами:

  • периодическим сбросом in-memory данных на диск snapshoting;
  • последовательной упреждающей записью всех приходящих операций в файл transaction journal.

И Redis, и Tarantool содержат оба механизма персистентности.

Redis

Redis периодически сбрасывает все данные из памяти на диск. Происходит это по-умолчанию каждые 60 секунд (настраивается). Redis использует механизм ОС fork для копирования текущих данных в памяти, затем информация сохраняется на диск. Если происходит аварийное завершение, то Redis восстановит состояние из последнего сохранения. Если последний снапшот был сделан давно, то данные, пришедшие после снапшота, будут утеряны.

Журнал операций используется для сохранения всей приходящей в базу информации. Каждая операция сохраняется в журнал на диске. Так, при запуске Redis восстанавливает своё состояние из снапшота и затем донакатывает оставшиеся транзакции из журнала.

  • Снапшот в Redis называется RDB (redis database).
  • Журнал операций в Redis называется AOF (append only file).

Tarantool

  • Механизм персистентности взят из архитектур баз данных.
  • Он является целостным снапшоты и журналирование.
  • Этот же механизм позволяет существовать надежной WAL-based репликации.

Tarantool периодически сохраняет текущие in-memory данные на диск и записывает каждую операцию в журнал.

  • Снапшот в Tarantool называется snap (snapshot). Можно делать с произвольной частотой.
  • Журнал транзакций в Tarantool называется WAL (write ahead log).

И в Redis, и в Tarantool каждый из механизмов может быть выключен. Для надёжного хранения данных оба механизма надо включить. Для максимального быстродействия можно отключить снапшотинг и журналирование, заплатив персистентностью. Слабоумие и отвага!

Различия

Для снапшотинга в Redis используется механизм ОС fork. Tarantool использует внутренний readview всех данных, это работает быстрее чем fork.

В Redis по умолчанию включён только снапшотинг. В Tarantool включён снапшотинг и журнал.

Redis хранит и использует только по одному файлу для снапшотов и журнала операций. Tarantool по-умолчанию хранит два файла снапшотов (можно настроить и больше) и консистентно дополняющее неограниченное количество журналов операций. При повреждении снапшот-файла Tarantool сможет загрузиться из предыдущего. Для Redis необходимо наладить механизм бекапов.

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

Troubleshooting

Если повреждён файл журнала в Redis:

redis-check-aof --fix

Если повреждён файл журнала в Tarantool:

tarantool> box.cfg{force_recovery=true}

Язык программирования для хранимых процедур


Хранимые процедуры это код, выполняющийся рядом с данными. И Redis, и Tarantool предлагают Lua для создания хранимок. С точки зрения пользователя это очень простой язык. Он создавался для людей, для которых программирование будет инструментом решения задач в предметной области.

C точки зрения разработчика базы данных:

  • Lua это язык, который легко встраивается в существующее приложение.
  • Он просто интегрируется с объектами и процессами приложения.
  • Lua имеет динамическую типизацию и автоматическое управление памятью.
  • Язык имеет сборщик мусора incremental Mark&Sweep.

Различия

Реализация

  • В Redis используется ванильная реализация PUC-Rio.
  • В Tarantool используется LuaJIT.

Таймаут задач

  • В Redis можно задать таймаут, после которого выполнение хранимой процедуры прервётся.
  • В Tarantool хранимые процедуры компилируются и выполняются быстрее, но в этом механизме нет возможности выставить таймаут. Для прерывания хранимой процедуры пользователь должен предусмотреть механизм проверки флага прерывания.

Runtime

  • В Redis используется однозадачность: задачи выполняются по одной и целиком.
  • В Tarantool используется кооперативная многозадачность. Задачи выполняются по одной, но при этом задача отдаёт управление на операциях ввода-вывода или явно с помощью yield.

Вывод

  • В Redis Lua это просто хранимые процедуры.
  • В Tarantool это кооперативный runtime, в котором можно взаимодействовать со внешними системами.

Репликация


Репликация это механизм копирования объектов с одного узла на другой. Бывает асинхронная и синхронная.

  • Асинхронная репликация: при вставке объекта на один узел мы не дожидаемся, когда этот же объект будет отреплицирован на второй узел.
  • Синхронная репликация: при вставке объекта мы дожидаемся, когда он будет сохранён на первом и втором узлах.

И Redis, и Tarantool поддерживают асинхронную репликацию. Только Tarantool умеет в синхронную репликацию.

На практике бывают ситуации, когда мы хотим дождаться репликации объекта. И в Redis, и в Tarantool есть способы для этого:

  • В Redis это команда wait. Она принимает два параметра:
    • сколько реплик должны получить объект;
    • сколько ждать, пока это произойдёт.

  • В Tarantool это можно сделать фрагментом кода:

псевдокод:

local netbox = require('net.box')local replica = netbox.connect(...)local replica_vclock, err = replica.eval([[    return box.info().vclock]])while not vclock_compare(box.info().vclock, replica_vclock) do    fiber.sleep(0.1)end

Синхронная репликация

В Redis нет синхронной репликации. Начиная с Tarantool 2.6 синхронная репликация доступна [2].

Коннекторы из других языков программирования


И Redis, и Tarantool поддерживают коннекторы для популярных языков программирования:

  • Go;
  • Python;
  • NodeJS;
  • Java.

Полные списки:


Под какие задачи плохо подходят


И Redis, и Tarantool плохо подходят для решения OLAP-задач. Online analytical processing имеет дело с историческими или архивными данными. OLAP характеризуется относительно низким объёмом транзакций. Запросы часто очень сложны и включают агрегацию.

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

Redis и Tarantool однопоточные базы данных, что не позволяет распараллелить аналитические запросы.

Экосистема


Redis

Модули Redis представлены в трёх категориях:

  • Enterprise;
  • проверенные и сертифицированные для Enterprise и Open source;
  • непроверенные.

Enterprise-модули:

  • полнотекстовый поиск;
  • хранение и поиск по bloom-фильтрам;
  • хранение временных рядов.

Сертифицированные:

  • хранение графов и запросы к ним;
  • хранение JSON и запросы к нему;
  • хранение и работа с моделями машинного обучения.

Все модули, отсортированные по количеству звёзд на Github: https://redis.io/modules

Tarantool

Модули представлены в двух категориях:


Чем Redis лучше


  • Проще.
  • В интернете представлено больше информации, 20 тыс. вопросов на Stackoverflow (из них 7 тыс. без ответов).
  • Ниже порог входа.
  • Как следствие, проще найти людей, которые умеют работать с Redis.

Чем Tarantool лучше


  • Русскоязычная бесплатная поддержка в Telegram от разработчиков.
  • Есть вторичные индексы.
  • Есть итерация по индексу.
  • Есть UI для администрирования кластера.
  • Предлагает механику сервера приложений с кооперативной многозадачностью. Эта механика похожа на однопоточный Go.

4. Вывод


Redis это классный продвинутый кеш, но его нельзя брать как основное хранилище. Tarantool это мультипарадигменная база данных, можно брать как основное хранилище. Tarantool поддерживает:

  • Реляционную модель хранения с SQL.
  • Распределённое NoSQL-хранилище.
  • Создание продвинутых кешей.
  • Создание брокера очередей.

У Redis ниже порог входа. У Tarantool выше потолок в production.

Сравнение одной таблицей:

Redis Tarantool
Описание Продвинутый кэш в памяти. Мультипарадигменная СУБД с сервером приложений.
Модель данных Key-value Key-value, документы, реляционная
Сайт redis.io www.tarantool.io
Документация redis.io/%C2%ADdocumentation www.tarantool.io/ru/doc/latest
Разработчик Salvatore Sanfilippo, Redis Labs mail.ru Group
Текущий релиз 6.2 2.6
Лицензия The 3-Clause BSD License The 2-Clause BSD License
Язык реализации C C, C++
Поддерживаемые ОС BSD, Linux, MacOS, Win BSD, Linux, MacOS
Схема данных Key-value Гибкая
Вторичные индексы Нет Есть
Поддержка SQL Нет Для одного инстанса, ANSI SQL
Foreign keys Нет Есть, с помощью SQL
Триггеры Нет Есть
Транзакции Оптимистичные блокировки, атомарное выполнение. ACID, read commited
Масштабирование Шардинг по фиксированному диапазону. Шардинг по настраиваемому количеству виртуальных бакетов.
Многозадачность Да, сериализация сервером. Да, кооперативная многозадачность.
Персистентность Снапшоты и журналирование. Снапшоты и журналирование.
Концепция консистентности Eventual ConsistencyStrong eventual consistency with CRDTs Immediate Consistency
API Проприетарный протокол. Свой открытый бинарный протокол.
Язык скриптов Lua Lua
Поддерживаемые языки C
C#, C++, Clojure, Crystal, D, Dart, Elixir, Erlang, Fancy, Go, Haskell, Haxe, Java, JavaScript (Node.js), Lisp, Lua, MatLab, Objective-C, OCaml, Pascal, Perl, PHP, Prolog, Pure Data, Python, R, Rebol, Ruby, Rust, Scala, Scheme, Smalltalk, Swift, Tcl, Visual Basic
C, C#, C++, Erlang, Go, Java, JavaScript, Lua, Perl, PHP, Python, Rust

5. Ссылки


  1. Архитектура S3: три года эволюции Mail.ru Cloud Storage
  2. Синхронная репликация в Tarantool
  3. Скачать Tarantool можно на официальном сайте, а получить помощь в Telegram-чате.
Подробнее..

Дайджест релизов мобильной разработки Mail.ru Group за время пандемии

02.04.2021 18:19:41 | Автор: admin

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

Почта Mail.ru

Почта Mail.ru представила новое мобильное приложение, в котором объединились Почта, Облако, Видеозвонки, Календарь, Задачи и голосовой помощник Маруся. Суперприложение Mail.ru можно адаптировать под себя: настроить порядок вкладок, чтобы самые актуальные сервисы были под рукой, выбрать тему оформления или вовсе отключить дополнительные сервисы, оставив только почту.

Также мы интегрировали Почту Mail.ru с Google Pay и Apple Pay, и теперь можно оплачивать счета прямо из писем. Такое внедрение Google Pay стало первым в своем роде не только для нас, но и для Google. Поэтому команда Android-разработки Почты прошла много этапов согласования сценариев использования и технологических особенностей внедрения. Теперь пользователи Android могут оплачивать счета в один клик.

А пользователи iOS теперь могут легко и быстро оплатить счета за коммунальные услуги или связь с помощью Apple Pay прямо из списка входящих даже не надо открывать письмо с квитанцией на оплату.

ICQ New

В апреле 2020 года вышла обновлённая аська ICQ New. Она обзавелась групповыми аудио- и видеозвонками, функцией быстрых ответов на сообщения, стала ещё лучше расшифровывать голосовые сообщения и запустила собственную платформу для создания ботов.

В мае 2020 спустили на воду сервис видеозвонков. Создать звонок может любой пользователь с аккаунтом в Почте Mail.ru, а присоединиться к уже созданным звонкам может кто угодно, без регистрации. Также в видеозвонках есть технология интеллектуального шумоподавления. Сервис поддерживает групповые звонки с количеством участников до 100 человек.

Delivery Club

В начале изоляции команда Delivery Club выпускала в среднем 2,5 версии мобильного приложения в неделю! Далее перешли в режим release train и стали стабильно выпускать по одной версии в неделю. Коллеги обновляли не только клиентское приложение, но и приложение для партнёров (рестораны, магазины, аптеки и т.д.) и курьеров.

Команда DC уже подготовила прототип для запуска App Clip. App Clip это маленькая часть основного приложения, обычно с какой-то одной функцией. Кроме того, в прошедшем году мобильное приложение Delivery Club стало поддерживать виджеты iOS 14 и было зафичерено в App Store.
Запустили PickerApp отдельное приложение для сборщиков продуктов крупных магазинов. В частности, для сети Магнит сделали несколько крупных фич:

  • Изменили используемую библиотеку сканирования на более продвинутую, что сократило время на сканирование каждого SKU.

  • Подключили сторонний SDK, позволяющий устанавливать сканер на ТСД (терминал сбора данных), на котором для сканирования товаров автоматически вызывается лазерный сканер, а не камера.

  • Впервые среди продуктов Delivery Club внедрили stories-функциональность.

Запустили доставку совместно с Ситимобил: это расширило зоны доставки и открыло новые возможности для клиентов. Теперь можно заказывать готовую еду из ресторанов, которые находятся далеко от клиента, а также делать более объёмные, чем при доставке пешим курьером, заказы в магазинах. Реализовали фильтр Навынос, возможность оставить чаевые курьеру, а также бронирование столиков в заведениях прямо в приложении Delivery Club.

Ещё одно достижение команды мобильной разработки Delivery Club адаптация Android-приложения для работы с HMS и выход в магазине приложений Huawei App Gallery. О том, какой путь они проделали, подробнее рассказали в статье. Кроме того, разработчики интегрировали оплату с помощью Google Pay: теперь обед можно оплачивать в один клик.

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

ТамТам

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

Android-разработчики ТамТам интегрировали Entity Extraction API для распознавания значимых объектов в тексте, таких как адрес или номер телефона, чтобы совершать быстрые действия с ними проложить маршрут, позвонить, отправить письмо, отследить почтовое отправление, скопировать номер банковской карты всего 5 типовых сценариев. На Хабре есть подробная статья о том, как мы делали распознавание.

Hi-Chef

Команда проекта Hi-Chef (сайт с рецептами-сториз, созданный специально под мобильные телефоны) опубликовала версию проекта в Google Play как TWA. С рассказом о том, как обернуть сайт в приложение, разработчик Хайшефа выступил на Chrome Dev Summit Russia 2020 Extended.

Игры

И несколько интересных игровых релизов:

Летом вышла одна из самых популярных игр в портфеле продуктов MY.GAMES, разработанная новосибирской студией Deus Craft, Grand Hotel Mania. На сегодняшний день Android-версия установлена больше 5 млн раз. В игре нужно открывать новые отели, улучшать их и выполнять пожелания гостей, чтобы зарабатывать больше денег и расширять свою гостиничную империю. В 2020 году Grand Hotel Mania также стала номинантом Google Play Best of 2020 Users Choice Award!

В последнем квартале 2020 запущена ещё одна очень популярная игра Rush Royale в жанре защита башни. Android-версия установлена уже свыше 1 млн раз. Игрокам предстоит собрать отряд из магов, лучников и воинов, которые будут защищать крепость от жутких чудовищ. В Rush Royale можно выбрать один из двух режимов в реальном времени: PvP, в котором герои состязаются друг с другом, и кооперативный, где два пользователя сообща отражают всё более сильные волны врагов.

Проекту Left to Survive исполнилось два года, в честь этого события команда выпустила большое обновление и объявила акции для пользователей. Проект получил поддержку фичерингом в App Store и Google Play!

Вышла 300-я серия мультсериала American Dad, и в честь этого в мобильной игре American Dad! Apocalypse Soon мы вместе с 20th Television запустили специальное событие, которое стало продолжением юбилейной серии. Мы реализовали в нём для поклонников сериала множество отсылок к реальному эпизоду, а также добавили возможность заполучить ценные ресурсы для своей базы, особые бонусы и редкие награды, вдохновлённые сюжетом знакового эпизода. Проект получил фичеринг в App Store и Google Play!

15 сентября 2020 на онлайн-презентации Time Flies продемонстрировали технические возможности нового iPad, показав, как на нём работает обновлённая версия нашего мультиплеерного шутера War Robots Remastered.

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

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

Какие крупные мобильные релизы вам запомнились в прошлом году? Что в них было самое масштабное или прорывное?

Подробнее..

We need to go deeper как пасхалка в приложении Delivery Club сократила субъективное время ожидания еды

11.06.2021 16:11:37 | Автор: admin


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

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

Сашин скриншот того, как всё начиналось.

Саша:
У меня есть свой небольшой проект словесно-карточная игра Кто из нас? На одном из очередных разборов игры родилась мысль, что можно внутри игры спрятать ещё одну игру. Люди найдут её и скажут: Ого, я играл в одну, а сейчас играю в совершенно другую. Но игра, которую я делаю, не настолько популярна, поэтому если туда прятать ещё одну игру, то её просто никто не увидит. Возникла вторая идея. Я работаю в Delivery Club, и у нас миллионы пользователей, у которых есть общая проблема время ожидания заказа. Нужно либо сокращать время доставки, либо как-то развлечь пользователя, пока он ожидает. И я пошёл по второму пути: придумал решение в виде небольшой игры внутри основного приложения.
Дизайн для первого прототипа змейки сделала девушка Саши, а звуки Саша создал сам в интернет-синтезаторе.


Прототипы первых экранов.

Уже в январе ребята буквально за полдня в коворкинге написали MVP на Swift и SpriteKit. Там были крупные кнопки и небольшой игровой экран, как в тетрисе. В финальной версии от кнопок на экране отказались: из-за того, что они плоские, а не объёмные, как на настоящей консоли, змейкой было неудобно управлять.
Саша:
Мы собрались с Сахеем и начали писать приложение в духе лучших стартапов. Заказали пиццу. Пива там не было, поэтому мы пили кофе. За один день у нас был готов прототип, который уже выполнял свою задачу. Игра запускалась, был первый квадратик, который начинал ездить. Он поедал эмодзи, змейка росла; врезавшись в стенку, она умирала. Осталось её только докрутить: добавить включение и выключение звука, доработать экраны начала и конца игры. Этим мы занялись уже в свободное время дома.
Сахей:
Очень понравился этот режим стартапа. Обычно на работе есть все спецификации и документация протоптанная тропинка, по которой ты идёшь. А тут был полный полёт мысли. Мы ещё хотели изначально поменять рендеринг на SwiftUI или на UIKit, но не стали так извращаться, SpriteKit отлично подходил для нашей задачи. Он оптимизирован под отрисовку спрайтов. Если в UIKit 100-200 вьюшек, то FPS очень сильно проседает, а в SpriteKit нет. Но мы фана ради хотели попробовать.


Разработка MVP.

В феврале ребята пришли с готовым прототипом к руководителю направления и показали, как змейка могла бы выглядеть внутри приложения Delivery Club. Мы собрали небольшую рабочую группу, в которую, помимо ребят, вошла дизайнер продукта Лера Зуйкова. И решили доводить проект до совершенства.

Финальную версию змейки пришлось достаточно сильно упростить ребята увлеклись и придумали много дополнительных возможностей. Решили оставить простой и красивый вариант, чтобы не отвлекать пользователя от основной функции приложения.
Саша:
Когда Лера показала свой дизайн, у нас с Сахеем загорелись глаза. Мы такие: Вау! Можно же настолько красиво и современно всё сделать! У нас появилась куча новых идей. Можно нарабатывать опыт у змейки с каждой игрой и набирать очки, за них покупать змейке какой-то апгрейд, типа шапочки или щита, чтобы она врезалась в стенку, а у неё на одну жизнь больше было. Но всё-таки история со змейкой должна была где-то кончиться, чтобы дойти до релиза. И мы решили упростить игру, чтобы не смещать фокус с основного функционала Delivery Club.
Лера:
На самом деле, когда в работе есть много рутинных задач, и внезапно кто-то приходит и предлагает сделать игру, это очень воодушевляет. Поэтому мы сразу активно включились в историю со змейкой и стали делать супер-красивые дизайны, продумывать механики. Все механики не были добавлены в финальный релиз ещё и потому, что вначале мы хотели всё проверить. Просто потратить кучу времени на разработку и сделать фичу, которую потом нашли бы три человека, было бы странно. Поэтому мы сошлись на простом решении, которое можно было быстро сделать, запустить и посмотреть реакцию пользователей. Если это окажется интересным, мы будем развивать игру: делать рейтинги, баллы и так далее.
Георгий:
После того, как ребята реализовали версию под iOS, мы принялись адаптировать приложение под Android. Разработка велась на Kotlin, всё сделали нативно, без использования сторонних библиотек. В конечном итоге версию под Android получилось написать всего за четыре дня. Идея была творческой и разнообразила череду рутинных задач.
Реализацию Android-версии Змейки можно посмотреть на GitHub.



Промежуточные варианты дизайна экранов.

18 мая змейку добавили в приложение Delivery Club. Чтобы в неё поиграть, нужно потрясти телефон на экране с заказом. Мы нигде не писали про пасхалку, но уже сейчас в неё играют 5-7 тысяч человек в день со средним временем игры 3 минуты 10 секунд. Нескольким тысячам пользователей игра скрасила минуты ожидания заказа. Надеемся, теперь игроков станет больше.

А если после прочтения у вас появились собственные идеи для пасхалок в Delivery Club, принимайте участие в нашем конкурсе. Лучшие идеи будут опубликованы на N + 1 и, возможно, реализованы в нашем приложении.
Подробнее..

ИТ-амбассадор на шаг ближе к профессии

29.03.2021 16:17:23 | Автор: admin

Амбассадор, по определению Кембриджского словаря, это важное должностное лицо, которое работает в другой стране, представляя там свою страну, и которое официально принято на эту должность. У нас в компании тоже есть своя программа амбассадоров. Главные задачи этих людей в Mail.ru Group представлять бренд и отдельные проекты, рассказывать молодёжи об ИТ-отрасли, о работе в нашей компании, о наших образовательных проектах, помогать в проведении конференций, митапов, лекций и семинаров, вебинаров и хакатонов.

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

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

Наши амбассадоры рассказывают студентам своего вуза о Mail.ru Group, наших образовательных проектах и перспективах работы в компании. От нашего лица они участвуют в разных мероприятиях в своём вузе и городе, помогают нам в проведении проектов. И также амбассадоры организуют в своих вузах от имени компании лекции, мастер-классы, хакатоны, квизы и другие мероприятия.

Чтобы попасть в нашу программу амбассадоров, нужно подать заявку до 30 апреля и пройти ряд вступительных испытаний Для успешного прохождения отбора нужно будет в течение мая выполнять различные задания, раскрывающие ваши личностные навыки: умение искать и анализировать информацию, представлять себя, критически мыслить, работать в команде и творчески решать поставленные задачи. Уметь программировать не обязательно, хотя любые ИТ-навыки будут плюсом. Вам поможет активное использование продуктов Mail.ru Group, знания о компании и желание ими делиться. В июне-июле пройдёт онлайн-обучение тех, кто качественно и в срок выполнит все майские задания. А в августе финальный этап, онлайн-собеседования.

В программу будут приняты все, кто успешно пройдёт отбор. Мы не ограничиваем количество участников. Программа продлится в течение одного учебного года, и лучших выпускников программы мы пригласим на НЕстажировку в нашу стажерскую программу, чтобы поработать над реальными проектами, а позже стать сотрудниками компании. Подать заявку можно здесь: vk.com/ambassador (напоминаем до 30 апреля).

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru