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

Блог компании delivery club tech

Доступность это просто, Или 5 смертных грехов доступности

10.12.2020 12:12:32 | Автор: admin

Привет, Хабр! Меня зовут Алексей Устинов, я Frontend-разработчик в Delivery Club. В свободное время я интересуюсь вопросами доступности интерфейсов. Это первая из двух статей, в которых я хочу рассказать о проблемах с доступностью в вебе. Я расскажу про 5 простых правил, соблюдая которые можно значительно улучшить доступность сайта. Также мы рассмотрим самые распространённые проблемы, я объясню, почему они являются проблемами, и дам простые советы по их решению. Во второй статье я, наоборот, приведу примеры элементов страницы, сделать доступными которые совсем нетривиальная задача.

Я уверен, что ты, %username%, слышал про правило 80/20: 80% результата можно достичь за 20% трудозатрат, а на достижение остальных 20% необходимо потратить 80% трудозатрат. Именно это правило объединяет эту и следующую статью.

Как работает скринридер

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

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

Грех 1. Бардак в заголовках

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

Пример иерархии заголовковПример иерархии заголовков

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

Другой частой ошибкой является неправильная последовательность в заголовках. Согласно стандарту, сначала используется заголовок H1, он должен быть один на странице. Следующий заголовок всегда будет H2. Если нужен вложенный заголовок, то используется H3, а если нужен равнозначный заголовок, то снова используется H2.

Я часто встречаю страницы, на которых есть несколько заголовков H1, или на которых вслед за H2 идет, например, H4.

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

Варианты решения

  • Объявлять заголовки с помощью тегов H1H6.

  • Использовать заголовки в иерархической последовательности от H1 до H6.

Грех 2. Кнопки с неопределенным назначением

Пример кнопок недоступных для скринридераПример кнопок недоступных для скринридера

Посмотрите на картинку: вы понимаете, что делает каждая из кнопок? Если да, то как вы это поняли? О значении кнопок вам рассказывают иконки, но что делать, если вы не видите иконок? Тогда вы, в лучшем случае, услышите что-то в роде button, а в худшем даже не сможете попасть фокусом на этот элемент.

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

Если же кнопка сделана через элемент button, то скринридер догадается, что это кнопка, но вот её предназначение он будет брать из атрибута aria-label или текста, находящегося внутри тега button.

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

Варианты решения

  • Добавлять назначение кнопки.

Грех 3. Невидимые картинки

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

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

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

Варианты решения

  • Всегда добавлять описание к картинкам в атрибут alt.

Грех 4. Отсутствие описания форм

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

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

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

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

пример капчипример капчи

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

Варианты решения

  • К каждому инпуту добавлять тег label с достаточным описанием.

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

Грех 5: фоновая музыка или автовоспроизведение

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

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

сайт с огромным количеством рекламных попаповсайт с огромным количеством рекламных попапов

Варианты решения

  • Не использовать фоновую музыку, которую нельзя выключить.

  • Не использовать видеоплееры, которые начинают играть при переходе на страницу.

  • Не использовать плееры, кнопки которых недоступны для скринридера.

Вывод

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

Всем добра!

Подробнее..

Доступность это не так просто

22.12.2020 14:23:50 | Автор: admin


Привет, Хабр! В предыдущей статье я рассказывал о простых случаях проблем с доступностью, исправив которые можно сделать свой сайт или web-приложение гораздо доступнее. Я упоминал о правиле 80/20 и писал о проблемах, которые при наименьших затратах дают наибольший результат. Сегодня я бы хотел поговорить о другой группе проблем, которые входят в 20% и для решения которых нет готовых рецептов вроде всегда заполняйте атрибут alt или используйте верные заголовки.

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

Формализуем проблему


Как я уже говорил, для некоторых проблем в сфере доступности невозможно предложить какое-то простое решение. Эти проблемы очень вариативны, а иногда и вовсе уникальны для конкретного приложения. Помимо этого, ситуация осложняется различиями в работе разных скринридеров в разных браузерах и на разных операционных системах. Дополняет всё это то, что решение лежит на стыке двух дисциплин: доступности с её стандартами и Interaction Design (Проектирование взаимодействия) с её достаточно гибкими правилами.

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

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

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

К таким элементам можно отнести:

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

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

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

Реализация триггера


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

В итоге я прихожу к первому вопросу: должен ли быть триггер ссылкой или кнопкой?

Ответ на этот вопрос звучит примерно так: в зависимости от ситуации, но в целом, вы не ошибётесь, если будете всегда использовать кнопку.

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

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

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

<button>Trigger Text</button> <div id="target">   <p>Target content.</p></div>

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

<a href="#target">Trigger Text</a>  <div id="target">  <p>Target content.</p></div>


Реальный пример


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

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

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

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

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

// Таргет не активирован
Триггер - aria-expanded="false",
Целевой контент - aria-hidden="true".

// Пользователь нажал на таргет элемент
Триггер - aria-expanded="true",
Целевой контент - aria-hidden="false".


Ещё существует атрибут aria-controls, который позволяет явно указать взаимосвязь между целевым компонентом и триггером.

<button aria-controls="target">Trigger Text</button><div id="target">  <p>Target content.</p></div>

Правда, он не всегда работает так, как это можно ожидать.

В качестве заключения


Для более наглядного примера я реализовал два codepan: один с триггером в виде кнопки, а второй со ссылкой (кнопка, ссылка). В чём разница, спросите вы? Всё просто: в примере со ссылкой целевой контент сразу получил фокус без каких-то дополнительных телодвижений со стороны разработчика.

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

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

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

Спасибо за внимание, всем добра!
Подробнее..

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

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.

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

Delivery Club x GIST

10.11.2020 12:07:03 | Автор: admin


Привет! Меня зовут Илья Воробьёв, в Delivery Club я отвечаю за направление клиентских продуктов. Вместе с Андреем Евсюковым я расскажу о том, как в растущей команде мы пересобирали процессы планирования и к чему это привело.

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

  • Мария Чебакова, руководитель группы оптимизации процессов. Отвечает за формирование и настройку бизнес- и операционных процессов.
  • Антон Сокольников, руководитель проектов в направлении логистики. Отвечает за развитие логистических продуктов.
  • Станислав Ураков, руководитель разработки в направлении логистики. Отвечает за систему, архитектуру и архитектурные решения, а также команды разработки.

С чего всё началось


К лету 2019 года переход к кросс-функциональным продуктовым командам дошёл до финальных стадий во всех продуктовых направлениях Delivery Club. В итоге мы получили понятный и достаточно предсказуемый темп движения на уровне менеджера продукта и разработки (кстати, про это мы уже рассказывали тут), но на контрасте стали более заметны трудности уровнем выше взаимодействие продуктовых команд и менеджеров продукта со стейкхолдерами.
Мария:
Год назад с каждой командой нужно было общаться по-разному. Например, у логистического продукта были планирования раз в две недели. С R&D не было планирования. Мы сидели за соседними столами и нормально всё решали. У остальных команд были разные процессы в зависимости от количества заказчиков, которых нужно подружить между собой и поделить ресурсы, и от количества исполнителей у каждой из команд.


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


Стас:
До внедрения GIST у нас применялся классический подход, когда компания находится в стадии переходного периода, внутри продуктов часто творился хаос. У менеджеров продукта был свой roadmap, но у разработчиков не было к нему доступа. Product-менеджеры где-то что-то фиксировали и с этим работали. Выдержки в виде продуктового описания приносили командам разработки, которые формировали технические требования. Появилась потребность весь этот хаос упорядочить, потому что работать становилось очень сложно.

Проблемы быстро обострялись, и этому способствовали:

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

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

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

  • Не очень понятно, кому нести задачу. Вроде бы, надо менеджеру продукта, но какому? А в каком виде?
  • Неясно, что дальше произойдет с задачей. Кто и как её будет делать? Когда будет готова?
  • Нет прозрачного способа ускорить выполнение задачи (а ведь она, конечно же, самая важная).

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

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

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

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

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

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

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

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


Иногда сессии по разбору задач растягивались на долгие часы и выглядели как-то так.

Поиск пути


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

Запросы удалось уложить в четыре лаконичных пункта:

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

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

Для заполнения пробелов в коммуникации и планировании стоит ввести новую роль в команде?


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

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

Мы решили обратиться к этому средству, если не сможем найти другого решения.

Существующего процесса будет достаточно, если внедрить систему скоринга?


Баталий на встречах можно избежать (ну, или хотя бы снизить накал страстей), если внедрить общую и понятную для всех систему оценки идей и гипотез. В голову сразу приходят подходы RICE (Reach, Impact, Confidence, Effort) или ICE (Impact, Confidence, Ease), которые позволяют при заполнении показателей получить почти автоматическую приоритизацию.

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

В итоговой реализации GIST мы действительно оцениваем Impact и Effort, но не пытаемся свести оценку задач к какой-то единообразной шкале. Но ICE мы тоже не без успехов используем в некоторых командах как вспомогательный инструмент.

Может выделить в команде слоты под задачи каждого из стейкхолдеров (или группы стейкхолдеров)?


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

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

Подойдём системно и перестроим процесс!


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

Изучая практики с рынка, нас заинтересовали подходы Итамара Гилада (Itamar Gilad, кстати, очень крутой дядя с большим количеством интересных трудов). Работая в Google, он собрал и внедрил этапную систему работы над задачами, которую назвал по первым буквам этапов жизненного цикла работы GIST: Goals, Ideas, Step-Projects, Tasks.

Базовые принципы подхода:

  • Goals цели, которые обозначаются командой на достаточно продолжительный промежуток времени. Они должны быть измеримы и понятны. При генерировании идей команда, менеджеры и стейкхолдеры сосредоточиваются на достижении общих целей.
  • Ideas любой член команды может закинуть идею. Команда регулярно приоритизирует и разбирает идеи, выделяет минимум, который позволит проверить ценность предложения минимальной ценой.
  • Step-Projects большие комплексные идеи разбиваются на этапы, которые можно сделать относительно быстро и проверить жизнеспособность и ценность на реальных данных. Чем быстрее такой проект можно завершить, тем лучше.
  • Tasks декомпозиция проекта на конкретные задачи, которые команда будет брать в разработку.
  • Каждый из этапов команда разбирает на регулярных сессиях и сосредоточивает ресурсы на идеях, которые приносят наибольший результат.



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


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

Have Space Suit Will Travel


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

При этом какие-то части процесса не были описаны Итамаром совсем, а некоторые вполне конкретно обозначены, и более того, многие компоненты уже были у нас внедрены:

  • Этапы Step-Projects и Tasks перенести на наши реалии было нетрудно: команды уже работали в рамках двухнедельных итераций, умели декомпозировать и запускать задачи в хорошем темпе (напоминаю, что детали можно посмотреть в этой статье).
  • C Goals тоже не было проблем. На уровне всего Delivery Club есть понятная стратегия развития, которая довольно просто трансформируется в цели и задачи на уровне команд и направлений. Например, у логистики, с которой мы и хотели начать пересобирать процессы, есть хорошо измеримые цели по загруженности курьеров, среднему времени и стоимости доставки заказа.
  • Самая мякотка для нас заключалась в этапе Ideas: как правильно научиться собирать идеи и выстроить работу по их приоритизации.

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

Идея и как её создать


Рассмотрев успешные кейсы в Delivery Club, мы выделили несколько общих факторов:

  • Почти у каждой выстрелившей идеи или гипотезы было проработанное бизнес-обоснование. Например, в начале 2019 года часть команд клиентского направления приступила к проработке решения по запуску доставки продуктов из магазинов. Но до перехода к проектированию мы очень подробно изучили динамику изменения рынка онлайн-ритейла за последние годы и перспективы роста на будущее. Это позволило сразу сфокусироваться на модели маркетплейса для магазинов решения, которое в полной мере расцвело в безумный 2020 год.
  • Время на проектирование и реализацию в продукте всегда было ограничено либо внешними факторами, либо самой командой. Стараясь уложиться в срок, ребята находили оригинальные решения и отрезали всё, что не было важно для проверки гипотезы на запуске. Подчеркнём, что короткие итерации в разработке это не что-то новое или революционное. Хитрость в том, чтобы у команд было изначальное ограничение на количество итераций для финального решения. Кстати, в книге Shape Up Райана Сингера есть подробный разбор этой механики и её использования для развития продукта в Basecamp.
  • Инициатор гипотезы работал в тесной связке с менеджером продукта и командой на протяжении всего процесса: от проектирования и разработки до запуска и оценки результатов. В ряде случаев такое вовлечение позволяло отказываться от реализации на ранних этапах. А где-то раскопать несколько новых идей и гипотез на следующие этапы.

В этот момент проявились контуры того, как мы хотели построить работу:

  • Любой член команды может завести идею и положить её в общее пространство.
  • У идеи должен быть набор обязательных параметров к заполнению:
    • Суть.
    • Почему возникла идея и какую ценность она несёт. Чем больше здесь доказательств, тем лучше. Именно из этого и складывается Impact, который кратко описывается.
    • Как выглядит верхнеуровневая реализация по мнению инициатора.
    • Какой результат ожидаем и почему.
  • Менеджер продукта будет периодически просматривать новые идеи с командой и давать комментарии по описанию, а также дополнять идею данными со своей стороны и фиксировать ограничение по времени на поиск и реализацию решения (это отражает для нас Effort).
  • При первичном разборе часть идей будет склеиваться с уже существующими, часть будет отсеиваться, а часть наполняться деталями. На обсуждение выносим те идеи, которые инициатор готов защищать не только силой своей харизмы, но и набором фактов с пониманием ограничений на реализацию.
  • Раз в две недели проводим общую встречу по приоритизации, на которой происходит три важных события:
    • инициаторы публично защищают свои идеи;
    • команда разбирает результаты завершенных проектов;
    • пересматриваем приоритеты по задачам, которые находятся в бэклоге, и команда сообщает, что уйдёт в разработку далее.


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

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

Как проходит защита


Стало понятнее, но последним пробелом остаётся сама защита. Да и как проходит встреча по приоритизации, тоже неясно. А устроено это так:

  • В полном составе команды, стейкхолдеры и менеджеры встречаются раз в две недели.
  • Должны быть разобраны три блока: защита идей, разбор результатов по завершённым проектам, реприоритизация бэклога.
  • Для простоты и прозрачности все идеи и проекты это задачи в Jira, которые разложены на Kanban-доске (доска идей). Кроме очевидных Done и Closed доступны ещё шесть статусов:



    • Inbox список новых идей. Задачи из этого статуса регулярно просматривают менеджеры продукта, помогая сформировать правильное описание и дойти до защиты.
    • Pitch идеи на защиту, которые будут обсуждаться на ближайшей приоритизации. Успешная защита означает перемещение в бэклог.
    • Backlog, Next, Now отражают состояние защищённой идеи в бэклоге. Соответственно, ожидаем реализации в рамках квартала, следующего спринта, или уже следим за реализацией.
    • Analysis запущенные проекты, по которым команда и стейкхолдеры должны собрать результаты и оценить, насколько идея была успешна (или неуспешна) и почему.

  • У самих задач на доске для наглядности вывели поля Impact (влияние идеи на бизнес и продукт) и Effort (какое время команда берёт на проектирование и реализацию решения). Оба значения заполняются в свободной форме. Обычно в Impact попадает несколько ключевых метрик и прогнозируемые изменения, а в Effort количество спринтов на реализацию.
  • Итогом каждой встречи должно стать следующее: колонка Pitch пуста, BacklogNextNow отражает актуальное положение дел, а в Analysis остаются только те задачи, по которым ещё не набралось достаточно данных.
  • Пара слов про защиту. Инициатор должен отстоять идею, команда и менеджер будут бороться за слот на реализацию. Ограничений по формату нет, но на каждую защиту выделяется 5-7 минут, за которые нужно успеть доказать ценность идеи, согласовать ограничения на реализацию и перенести в бэклог. Изначально мы предполагали собирать на защиту комитет из людей с независимой позицией, но решили отложить эту схему на случай крайней необходимости.

Внедрение


Всё вышеописанное, начиная от формата задач до повестки встреч, зафиксировали в Confluence. Потом завели проект в Jira и настроили Kanban-доску. На этом простая часть закончилась и началась сложная внедрение.

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



Какие шаги предприняли:

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

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

Антон:
Сначала нужно было свыкнуться с тем, что у тебя есть соседи по целям и проектам, и с ними придётся делиться ресурсами. Мы аккуратно, постепенно ко всему этому приходили. Начали с доски, в которой мы заводили задачи и описывали их, то есть приучали бизнес к тому, что задачи надо завести. Раньше они просто набрасывали свои идеи, а менеджеры всё заводили.

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

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

В тот момент как раз поступила крупная задача на 2,5-3 месяца разработки. Мы её оформили в Идею, выделили MVP это и был наш Step-Project. Дальше его поделили на таски и попытались поставить их на продакшен за два спринта в составе Step-Projectа. Поняли, что идея нам подходит, есть прогресс со стороны бизнеса. Дальше сформулировали следующий этап итерации.

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

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

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

Эти встречи позволили:

  • Объяснить правильную структуру и принципы описания идеи.
  • Выстроить и зафиксировать прозрачные для стейкхолдеров цели.
  • Научиться получать верхнеуровневые оценки по разработке на ранних этапах.
  • Мыслить в формате влияния на бизнес и метрики.
  • Технической команде приносить и высказывать продуктовые идеи.

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

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

И жили они долго и счастливо?


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

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

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

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

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

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

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

Если подняться на уровень выше, то любопытно посмотреть и на культурные изменения, которые произошли с момента запуска пилота:

  • У команд и стейкхолдеров вырабатывается привычка подробно оценивать идеи с точки зрения влияния на бизнес и продукт. А это, в свою очередь, отличная предпосылка к поддержанию и развитию целеполагания по OKR.
  • Ориентация на единый инструмент (Jira) и возможность отследить движение идеи добавляет прозрачности и снижает использование сторонних решений (меньше Google-таблиц и их аналогов, да).
  • Понятные шаги по созданию и продвижению идей постепенно вовлекают в работу с продуктовыми командами людей из бизнеса, обычно далёких от разработки.

Результаты в направлении логистики нас порадовали, и началось постепенное внедрение в других командах. К осени 2020 года GIST проник во все продуктовые направления Delivery Club, но не везде прижился как основной процесс.

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

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

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

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

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

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.

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

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

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

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

Подробнее..

Вначале был монолит как мы меняем нашу архитектуру, не мешая бизнесу

18.09.2020 16:04:09 | Автор: admin


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

Сейчас Delivery Club (как и весь рынок фудтеха) растёт очень быстро, что порождает огромное количество вызовов для технической команды, которые можно обобщить двумя самыми важными критериями:

  • Нужно обеспечивать высокую стабильность и доступность всех частей платформы.
  • Одновременно с этим держать высокий темп разработки новых фич.

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

Но нам удаётся (пока) и то, и другое. О том, как мы это делаем, и пойдет речь далее.

Во-первых, я расскажу про нашу платформу: как мы её трансформируем с учетом постоянно растущих объемов данных, какие критерии предъявляем к нашим сервисами и с какими проблемами сталкиваемся на этом пути.

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

Начнём с платформы.

Вначале был монолит


Первые строчки кода Delivery Club были написаны 11 лет назад, и в лучших традициях жанра архитектура представляла собой монолит на PHP. Он в течение 7 лет всё больше и больше наполнялся функциональностью, пока не столкнулся с классическими проблемами монолитной архитектуры.

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

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

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

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

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

Экосистема


Как рассказывал Андрей Евсюков в статье про наши команды, у нас выделены главные направления по доменным областям: R&D, Logistics, Consumer, Vendor, Internal, Platform. В рамках этих направлений уже сосредоточены основные доменные области, с которыми работают сервисы: например, для Logistics это курьеры и заказы, а для Vendor рестораны и позиции.

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

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

Низкие нагрузки, синхронные запросы, всё работает круто.

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


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

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

Шина данных


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

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

  • mobile-gateway, который является backend for frontend для мобильного приложения;
  • courier-tracker, который хранит логику получения и отдачи координат;
  • logistics-couriers, который хранит эти координаты. Они присылаются из мобильных приложений курьеров.



В первоначальной схеме это всё работало синхронно: запросы из мобильного приложения раз в минуту шли через mobile-gateway к сервису courier-tracker, который обращался к logistics-couriers и получал координаты. Конечно, в этой схеме было не всё так просто, но в итоге всё сводилось к простому выводу: чем больше у нас активных заказов, тем больше запросов на получение координат приходило в logistics-couriers.

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

Транспорт


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

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

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

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

Для решения этой проблемы мы написали обёртку микросервис на Go, который скрыл Kafka за своим API. Это добавило два преимущества:

  • валидация данных в момент отправки и приёма. По сути, это одни и те же DTO, поэтому мы всегда уверены в формате ожидаемых данных.
  • быстрая интеграция наших сервисов с этим транспортом.

Таким образом, работа с Kafka стала максимально абстрагированной для наших сервисов: они лишь работают с верхнеуровневым API этой обёртки.

Вернёмся к примеру


Переводя синхронное взаимодействие на шину событий, нам необходимо инвертировать поток данных: то, за чем мы обращались, должно теперь само попадать к нам через Kafka. В примере речь идёт о координатах курьера, для которых теперь мы заведём специальный топик и будем продюсить их по мере получения от курьеров сервисом logistics-couriers.

Сервису courier-tracker остаётся аккумулировать координаты в нужном объёме и на нужный срок. В итоге наш эндпоинт становится максимально простым: взять данные из базы сервиса и отдать их мобильному приложению. Рост нагрузки на неё теперь для нас безопасен.



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

Eventually consistency


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

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



Таким образом, мы четко разделяем наши сервисы на те, что являются мастерами данных и те, кто использует эти данные. По сути, это headless commerce из evolutionary archicture у нас четко отделены все витрины (сайт, мобильные приложения) от производителей этих данных.

Денормализация


Ещё пример: у нас есть механизм таргетированных уведомлений курьерам это сообщения, которые придут им в приложение. На стороне бэкенда есть мощное API для отправки таких уведомлений. В нём можно настраивать фильтры рассылки: от конкретного курьера до групп курьеров по определённым признакам.

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

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

Часть логики генерации сообщений и фильтрования курьеров не является нагруженной, она выполняется в фоне, поэтому вопроса о нагрузках на сервис logistics-couriers не стоит. Но если выбрать первый вариант, мы столкнёмся с набором проблем:

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

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

В итоге у нас сформулированы несколько важных принципов к проектированию сервисов:

  • У сервиса должна быть конкретная ответственность. Если для его полноценного функционирования нужен ещё сервис, то это ошибка проектирования, их нужно либо объединять, либо пересматривать архитектуру.
  • Критично смотрим на любые синхронные обращения. Для сервисов в одном направлении это допустимо, но для общения между сервисами разных направлений нет
  • Share nothing. Мы не ходим в БД сервисов в обход них самих. Все запросы только через API.
  • Specification First. Сначала описываем и утверждаем протоколы.

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



У нас уже есть шина данных в виде Kafka, которая уже имеет существенное количество потоков данных, но всё ещё остаются синхронные запросы между направлениями.

Как мы планируем развивать нашу архитектуру


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

Дальше мы будем продолжать внедрять выработанные подходы во все сервисы Delivery Club: строить экосистемы сервисов вокруг платформы с транспортом в виде шины данных.

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

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

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



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

Дальше из шины данных сервисы будут потреблять данные из нужных топиков. И мы сами будем использовать эти данные для своих систем: например, стримить через Kafka Connect в ElasticSearch или в DWH. С последним процесс будет сложнее: чтобы информация в нём была доступна для всех, её необходимо очистить от любых персональных данных.

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

Как заниматься таким рефакторингом прозрачно для клиентов


Расскажу на ещё одном примере: каталоге ресторанов. Очевидно, что это очень нагруженное место, и мы решили выносить его в отдельный сервис на Go. Для ускорения разработки мы поделили вынос на два этапа:

  1. Сначала внутри сервиса ходим напрямую в реплику базы нашего монолита и получаем оттуда данные.
  2. Затем начинаем стримить нужные нам данные через Debezium и аккумулировать в базе самого сервиса.



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

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

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

Архитектурный комитет


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

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

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

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

В итоге, проблему с контролем крупных изменений мы закрыли, остаётся вопрос общего подхода к качеству кода в Delivery Club: конкретные проблемы кода или фреймворка в разных командах могут решаться по-разному. Мы пришли к гильдиям по модели Spotify: это объединения неравнодушных к какой-то технологии людей. Например, есть гильдии Go, PHP и Frontend.

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

Код на прод


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

В чек-листе обычно указывается:

  • ответственный за сервис (это обычно тех-лид сервиса);
  • ссылки на дашборд с настроенными алертами;
  • описание сервиса и ссылка на Swagger;
  • описание сервисов, с которым будет взаимодействовать;
  • предполагаемая нагрузка на сервис;
  • ссылка на health-check. Это URL, по которому служба эксплуатации настраивает свои мониторинги. Health-check раз в какой-то период дёргается: если вдруг он не ответил с кодом 200, значит, с сервисом что-то не так и к нам прилетает алерт. В свою очередь, health check может дёргать такие же URLы критичных для него сервисов, а также обязательно включать проверку всех компонентов сервиса, например, PostgreSQL или Redis.

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

Для примера можно взять уже рассмотренный выше сервис courier-tracker, отслеживающий курьеров на карте. Одна из основных метрик в нём это количество курьеров, у которых обновляются координаты. Если вдруг какие-то маршруты долго не обновляются, приходит алерт что-то пошло не так. Может быть, где-то за данными не сходили, или неправильно зашли в базу, или отвалился какой-нибудь другой сервис. Это не техническая метрика и не продуктовая, но она показывает жизнеспособность сервиса.

Для метрик мы используем Graylog и Prometheus, строим дашборды и настраиваем алерты в Grafana.

Несмотря на объём подготовки, доставка сервисов в прод достаточно быстрая: все сервисы изначально упакованы в Docker, в stage выкатываются автоматически после формирования типизированного чарта для Kubernetes, а дальше всё решается кнопкой в Jenkins.

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

Под капотом


Сейчас у нас 162 микросервиса, написанные на PHP и Go. Они распределились между сервисами примерно 50% на 50%. Изначально мы переписали на Go некоторые высоконагруженные сервисы. Дальше стало ясно, что Go проще в поддержке и мониторинге в продакшене, у него низкий порог входа, поэтому в последнее время мы пишем сервисы только на нём. Цели переписать на Go оставшиеся PHP-сервисы нет: он вполне успешно справляется со своими функциями.

В PHP-сервисах у нас Symfony, поверх которого мы используем свой небольшой фреймворк. Он навязывает сервисам общую архитектуру, благодаря которой мы снижаем порог входа в исходный код сервисов: какой бы сервис вы ни открыли, всегда будет понятно, что и где в нём лежит. А также фреймворк инкапсулирует слой транспорта общения между сервисами, для разработчика запрос в сторонний сервис выглядит на высоком уровне абстракции:
$courierResponse = $this->courierProtocol->get($courierRequest);
Здесь мы формируем DTO запроса ($courierRequest), вызываем метод объекта протокола конкретного сервиса, который является обёрткой над конкретным эндпоинтом. Под капотом наш объект $courierRequest преобразуется в объект запроса, который заполняется полями из DTO. Это всё гибко настраивается: поля могут подставляться как в заголовки, так и в сам URL запроса. Далее запрос посылается через cURL, получаем объект Response и обратно его трансформируем в ожидаемый нами объект $courierResponse.

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

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

В итоге, изменения в протоколе сервиса могут превратиться в несколько пулл-реквестов: в сам сервис, в его SDK, и в сервис, которому нужен этот протокол. В последнем нам нужно поднять версию импортированного SDK, чтобы туда попали изменения. Это часто вызывает вопросы у новых разработчиков: Я ведь только изменил параметр, почему мне нужно делать три реквеста в три разных репозитория?!

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

Технический радар




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

  • Python, на котором пишет команда Data Science.
  • Kotlin и Swift для разработки мобильных приложений.
  • PostgreSQL в качестве базы данных, но на некоторых старых сервисах всё ещё крутится MySQL. В микросервисах используем несколько подходов: для каждого сервиса своя БД и share nothing мы не ходим в базы данных в обход сервисов, только через их API.
  • ClickHouse для узкоспециализированных сервисов, связанных с аналитикой.
  • Redis и Memcached в качестве in-memory хранилищ.


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

Long story short


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

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

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

На этом у меня всё, спасибо, что дочитали!
Подробнее..

Go-swagger как основа взаимодействия микросервисов

04.08.2020 14:20:22 | Автор: admin


Здравствуй, NickName! Если ты программист и работаешь с микросервисной архитектурой, то представь, что тебе нужно настроить взаимодействие твоего сервиса А с каким-то новым и ещё неизвестным тебе сервисом Б. Что ты будешь делать в первую очередь?

Если задать такой вопрос 100 программистам из разных компаний, скорее всего, мы получим 100 разных ответов. Кто-то описывает контракты в swagger, кто-то в gRPC просто делает клиенты к своим сервисам без описания контракта. А кто-то и вовсе хранит JSON в гуглодоке :D. В большинстве компаний складывается свой подход к межсервисному взаимодействию на основании каких-либо исторических факторов, компетенций, стека технологий и прочего. Я хочу рассказать, как сервисы в Delivery Club общаются друг с другом и почему мы сделали именно такой выбор. И главное как мы обеспечиваем актуальность документации с течением времени. Будет много кода!

Ещё раз привет! Меня зовут Сергей Попов, я тим-лид команды, отвечающей за поисковую выдачу ресторанов в приложениях и на сайте Delivery Club, а также активный участник нашей внутренней гильдии разработки на Go (возможно, мы об этом ещё расскажем, но не сейчас).

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

К чему, в итоге, мы хотели прийти:

  1. Обеспечить актуальность контрактов сервисов. Это должно ускорить внедрение новых сервисов и упростить коммуникацию между командами.
  2. Прийти к единому способу взаимодействия по HTTP между сервисами (пока не будем рассматривать взаимодействия через очереди и event streaming).
  3. Стандартизировать подход к работе с контрактами сервисов.
  4. Использовать единое хранилище контрактов, чтобы не искать доки по всяким конфлюенсам.
  5. В идеале, генерировать клиенты под разные платформы.

Из всего перечисленного на ум приходит Protobuf как единый способ описания контрактов. Он имеет хороший инструментарий и может генерировать клиенты под разные платформы (наш п.5). Но есть и явные недостатки: для многих gRPC остается чем-то новым и неизведанным, а это сильно усложнило бы его внедрение. Ещё одним важным фактором было то, что в компании давно принят подход specification first, и документация уже существовала на все сервисы в виде swagger или RAML-описания.

Go-swagger


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

Go-swagger это не только про генерирование транспортного слоя. Фактически он генерирует каркас приложения, и тут я бы хотел немного упомянуть о культуре разработки в DC. У нас есть Inner Source, а это значит, что любой разработчик из любой команды может создать pull request в любой сервис, который у нас есть. Чтобы такая схема работала, мы стараемся стандартизировать подходы в разработке: используем общую терминологию, единый подход к логированию, метрикам, работе с зависимостями и, конечно же, к структуре проекта.

Таким образом, внедряя go-swagger, мы вводим стандарт разработки наших сервисов на Go. Это еще один шаг навстречу нашим целям, на который мы изначально не рассчитывали, но который важен для разработки в целом.

Первые шаги


Итак, go-swagger оказался интересным кандидатом, который, кажется, может покрыть большинство наших хотелок требований.
Примечание: весь дальнейший код актуален для версии 0.24.0, инструкцию по установке можно посмотреть в нашем репозитории с примерами, а на официальном сайте есть инструкция по установке актуальной версии.
Давайте посмотрим, что он умеет. Возьмём swagger-спеку и сгенерируем сервис:

> goswagger generate server \    --with-context -f ./swagger-api/swagger.yml \    --name example1

Получилось у нас следующее:



Makefile и go.mod я уже сделал сам.

Фактически у нас получился сервис, который обрабатывает запросы, описанные в swagger.

> go run cmd/example1-server/main.go2020/02/17 11:04:24 Serving example service at http://127.0.0.1:54586   > curl http://localhost:54586/hello -iHTTP/1.1 501 Not ImplementedContent-Type: application/jsonDate: Sat, 15 Feb 2020 18:14:59 GMTContent-Length: 58Connection: close "operation hello HelloWorld has not yet been implemented"

Второй шаг. Разбираемся с шаблонизацией


Очевидно, что сгенерированный нами код далёк от того, что мы хотим видеть в эксплуатации.

Что мы хотим от структуры нашего приложения:

  • Уметь конфигурировать приложение: передавать настройки подключения к БД, указывать порт HTTP-соединений и прочее.
  • Выделить объект приложения, который будет хранить состояние приложения, подключение к БД и прочее.
  • Сделать хэндлеры функциями нашего приложения, это должно упростить работу с кодом.
  • Инициализировать зависимости в main-файле (в нашем примере этого не будет, но мы всё равно этого хотим.

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



Нам необходимо описать файлы шаблонов (`*.gotmpl`) и файл для конфигурации (`*.yml`) генерирования нашего сервиса.

Далее по порядку разберем те шаблоны, которые сделал я. Глубоко погружаться в работу с ними не буду, потому что документация go-swagger достаточно подробная, например, вот описание файла конфигурации. Отмечу только, что используется Go-шаблонизация, и если у вас уже есть в этом опыт или приходилось описывать HELM-конфигурации, то разобраться не составит труда.

Конфигурирование приложения


config.gotmpl содержит простую структуру с одним параметром портом, который будет слушать приложение для входящих HTTP-запросов. Также я сделал функцию InitConfig, которая будет считывать переменные окружения и заполнять эту структуру. Вызывать буду из main.go, поэтому InitConfig сделал публичной функцией.

package config import (    "github.com/pkg/errors"    "github.com/vrischmann/envconfig") // Config structtype Config struct {    HTTPBindPort int `envconfig:"default=8001"`} // InitConfig funcfunc InitConfig(prefix string) (*Config, error) {    config := &Config{}    if err := envconfig.InitWithPrefix(config, prefix); err != nil {        return nil, errors.Wrap(err, "init config failed")    }     return config, nil}

Чтобы этот шаблон использовался при генерировании кода, его нужно указать в YML-конфиге:

layout:  application:    - name: cfgPackage      source: serverConfig      target: "./internal/config/"      file_name: "config.go"      skip_exists: false

Немного расскажу про параметры:

  • name несёт чисто информативную функцию и на генерирование не влияет.
  • source фактически путь до файла шаблона в camelCase, т.е. serverConfig равносильно ./server/config.gotmpl.
  • target директория, куда будет сохранен сгенерированный код. Здесь можно использовать шаблонизацию для динамического формирования пути (пример).
  • file_name название сгенерированного файла, здесь также можно использовать шаблонизацию.
  • skip_exists признак того, что файл будет сгенерирован только один раз и не будет перезаписывать существующий. Для нас это важно, потому что файл конфига будет меняться по мере роста приложения и не должен зависеть от генерируемого кода.

В конфиге кодогенерирования нужно указывать все файлы, а не только те, которые мы хотим переопределить. Для файлов, которые мы не меняем, в значении source указываем asset:<путь до шаблона>, например, как здесь: asset:serverConfigureapi. Кстати, если интересно посмотреть оригинальные шаблоны, то они здесь.

Объект приложения и хэндлеры


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

Опишем шаблон функции и заглушки:

package app import (    api{{ pascalize .Package }} "{{.GenCommon.TargetImportPath}}/{{ .RootPackage }}/operations/{{ .Package }}"    "github.com/go-openapi/runtime/middleware") func (srv *Service){{ pascalize .Name }}Handler(params api{{ pascalize .Package }}.{{ pascalize .Name }}Params{{ if .Authorized }}, principal api{{ .Package }}.{{ if not ( eq .Principal "interface{}" ) }}*{{ end }}{{ .Principal }}{{ end }}) middleware.Responder {    return middleware.NotImplemented("operation {{ .Package }} {{ pascalize .Name }} has not yet been implemented")}

Немного разберём пример:

  • pascalize приводит строку с CamelCase (описание остальных функции здесь).
  • .RootPackage пакет сгенерированного веб-сервера.
  • .Package название пакета в сгенерированном коде, в котором описаны все необходимые структуры для HTTP-запросов и ответов, т.е. структуры. Например, структура для тела запроса или структура ответа.
  • .Name название хэндлера. Оно берётся из operationID в спецификации, если указано. Я рекомендую всегда указывать operationID для более очевидного результата.

Конфиг для хэндлера следующий:

layout:  operations:    - name: handlerFns      source: serverHandler      target: "./internal/app"      file_name: "{{ (snakize (pascalize .Name)) }}.go"      skip_exists: true

Как видите, код хэндлеров не будет перезаписываться (skip_exists: true), а название файла будет генерироваться из названия хэндлера.

Окей, функция с заглушкой есть, но веб-сервер ещё не знает, что эти функции нужно использовать для обработки запросов. Я исправил это в main.go (весь код приводить не буду, полную версию можно найти здесь):

package main {{ $name := .Name }}{{ $operations := .Operations }}import (    "fmt"    "log"     "github.com/delivery-club/go-swagger-example/{{ dasherize .Name }}/internal/generated/restapi"    "github.com/delivery-club/go-swagger-example/{{ dasherize .Name }}/internal/generated/restapi/operations"    {{range $index, $op := .Operations}}        {{ $found := false }}        {{ range $i, $sop := $operations }}            {{ if and (gt $i $index ) (eq $op.Package $sop.Package)}}                {{ $found = true }}            {{end}}        {{end}}        {{ if not $found }}        api{{ pascalize $op.Package }} "{{$op.GenCommon.TargetImportPath}}/{{ $op.RootPackage }}/operations/{{ $op.Package }}"        {{end}}    {{end}}     "github.com/go-openapi/loads"    "github.com/vrischmann/envconfig"     "github.com/delivery-club/go-swagger-example/{{ dasherize .Name }}/internal/app") func main() {    ...    api := operations.New{{ pascalize .Name }}API(swaggerSpec)     {{range .Operations}}    api.{{ pascalize .Package }}{{ pascalize .Name }}Handler = api{{ pascalize .Package }}.{{ pascalize .Name }}HandlerFunc(srv.{{ pascalize .Name }}Handler)    {{- end}}    ...}

Код в импорте выглядит сложным, хотя на самом деле здесь просто Go-шаблонизация и структуры из репозитория go-swagger. А в функции main мы просто присваиваем хэндлерам наши сгенерированные функции.

Осталось сгенерировать код с указанием нашей конфигурации:

> goswagger generate server \        -f ./swagger-api/swagger.yml \        -t ./internal/generated -C ./swagger-templates/default-server.yml \        --template-dir ./swagger-templates/templates \        --name example2

Финальный результат можно посмотреть в нашем репозитории.

Что мы получили:

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

Третий шаг. Генерирование клиентов


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

Для проектов на Go принято складывать публичные пакеты в ./pkg, мы сделаем так же: положим клиент для нашего сервиса в pkg, а сам код сгенерируем следующим образом:

> goswagger generate client -f ./swagger-api/swagger.yml -t ./pkg/example3

Пример сгенерированного кода здесь.

Теперь все потребители нашего сервиса могут импортировать себе этот клиент, например, по тэгу (для моего примера тэг будет example3/pkg/example3/v0.0.1).

Шаблоны клиентов можно настраивать, чтобы, например, прокидывать open tracing id из контекста в заголовок.

Выводы


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

Давайте проверим, получилось ли достигнуть первоначальных целей:

  1. Обеспечить актуальность описанных для сервисов контрактов, это должно ускорить внедрение новых сервисов и упростить коммуникацию между командами Да.
  2. Прийти к единому способу взаимодействия по HTTP между сервисами (пока не будем рассматривать взаимодействия через очереди и event streaming) Да.
  3. Стандартизировать подход к работе с контрактами сервисов, т.к. мы давно пришли к подходу Inner Source в разработке сервисов Да.
  4. Использовать единое хранилище контрактов, чтобы не искать документацию по всяким конфлюенсам Да (фактически Bitbucket).
  5. В идеале, генерировать клиенты под разные платформы Нет (на самом деле, не пробовали, шаблонизация не ограничивает в этом плане).
  6. Внедрить стандартную структуру сервиса на Go Да (дополнительный результат).

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

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

Как синхронизировать сотни таблиц базы в Kafka, не написав ни одного продюсера

25.11.2020 14:11:26 | Автор: admin


Привет, Хабр! Меня зовут Сергей Бевзенко, я ведущий разработчик Delivery Club в команде Discovery. Наша команда занимается навигацией пользователя по приложению Delivery Club: мы отвечаем за основную выдачу ресторанов, поиск и всё, что с этим связано.

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

План


1. Предпосылки
2. Как используется Kafka Connect
2.1. Как запустить Kafka Connect
3. Запуск коннекторов
4. Настройка коннекторов
4.1. Причины выбора коннекторов
4.2. Jdbc и Debezium
4.3. Debezium Connector
4.4. JdbcSinkConnector
4.5. Трансформеры
5. Deploy
5.1. Deploy Kafka Connect Delivery Club
6. Что нам дало использование Kafka Connect

Предпосылки


Delivery Club не молодая компания. Она основана в сентябре 2009 года. Мы постоянно развиваемся и улучшаем наши сервисы, без этого рост невозможен.

У нас есть 10-летний Legacy-монолит. Он служит основой многих процессов. Да, новые сервисы мы, конечно же, пишем. Делаем это на Go, и иногда на PHP. Это два основных языка backend-разработки в Delivery Club. Также мы переходим на событийную модель с использованием шины событий: все изменения данных в системе это события, попадающие в шину, и любой сервис может подписаться на них.

Какие это события?


В компании есть множество интеграции с различными ресторанами, магазинами, аптеками и т.д. Также у нас есть служба логистики, которая работает с курьерами, их маршрутами, заказами, распределением. Есть и отличный отдел R&D, который занимается различными исследованиями и околонаучной разработкой. И, конечно, есть другие отделы. У каждого направления множество сервисов, и все они генерируют огромное количество событий. В качестве шины для них мы используем Apache Kafka. Но десятилетний Legacy никуда не делся. Внутри него множество админок, которые являются источниками данных. Без крайней нужды трогать их не рекомендуется.

Сервис Каталог


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

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

Единственным оптимальным решением было написать на Go новый сервис, который помог бы решить все проблемы, имевшиеся в монолите. К тому же мы смогли сильно (в три раза) сократить время ответа.

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

Как писать продюсеры в условиях 10-летнего Legacy


В самой первой версии Catalog MVP мы ходили в реплику монолита, чтобы быстро запуститься (для нас важен Time to market). Но оставлять так мы не хотели, поэтому нужно было денормализовать данные из монолита. А для этого необходимо начать продьюсить данные.

Есть несколько подходов:

  1. Переписать монолит. Тут вспоминаем все те статьи, доклады и книги о том, как переписывать монолит. Это сложный и долгий процесс. Он связан с большим количеством рисков. Конечно, мы выносим функциональность из монолита, но делаем это постепенно, аккуратно. Не в ущерб бизнесу.
  2. Писать свои продюсеры в монолите. Надо найти все места в коде, где происходит изменения в базе. В этих местах добавлять также отправку событий в шину. Если у вас хорошая архитектура монолита, с выделенным слоем репозитория, то сделать это лишь вопрос времени. Но Legacy не будет Legacy, если там всё хорошо с архитектурой. Так что этот вариант тоже очень сложен и трудозатратен.
  3. Использовать готовые решения для интеграции базы данных и Kafka. Можно использовать фреймворк Kafka Connect.

Kafka Connect


Как он используется


Чаще всего Kafka используют так:

Source => Kafka

Kafka => Kafka

Kafka => Storage

Kafka => App



То есть нам приходится писать собственные консьюмеры и продюсеры и решать однообразные задачи при их разработке:

  • Прописывать правила подключения к источникам.
  • Обрабатывать ошибки.
  • Прописывать правила ретраев.

Наиболее полно API Kafka поддерживается только в языках Java и Scala. В других языках поддержка не всегда полная. Поэтому разработчики Kafka предложили свои инструменты для решения таких задач: фреймворки Kafka Connect и Kafka Streams:

Source => Kafka (connect)

Kafka => Kafka (streams)

Kafka => Storage (connect)

Kafka => App



Когда говорят, что Kafka Connect поставляется вместе с Kafka, это не какая-то скрытая функциональность Kafka-брокеров. Это именно отдельное приложение, которое имеет настройки подключения к Kafka и источнику/приёмнику. Работу с Kafka Connect мы рассмотрим ниже.

Но сначала нужно ввести три важных термина:

  • worker инстанс/сервер Kafka Connect;
  • connector Java class + пользовательские настройки;
  • task один процесс connector'a.

Worker экземпляр Kafka Connect. Kafka Connect можно запускать в двух режимах: standalone и distributed, на нескольких нодах или виртуальных машинах. То есть можно просто запустить один worker или собрать кластер workerов. Рекомендуется использовать standalone-режим при локальной разработке, настройке и отладке коннекторов, а distributed в боевых условиях.

Преимущество distributed mode


Предположим, мы запустили четыре worker'а Kafka Connect и создали три connector'а с разным количеством task'ов.

  • Во-первых, Kafka Connect автоматически распределит таски коннекторов по разным воркерам.
  • Во-вторых, Kafka Connect отслеживает своё состояние в кластере. Если обнаружит, что один из воркеров недоступен, выполнит перебалансировку и перераспределит недоступные таски по работающим воркерам.

Какие ещё задачи решает Kafka Connect:

  • отказоустойчивость (fault tolerance);
  • принцип только один раз (exactly once);
  • распределение (distribution);
  • упорядочивание (ordering).



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

  • Source Connectors;
  • Sink Connectors.



Коннекторов уже очень много написано. Например, на сайте confluent их сейчас 163, а на просторах интернета ещё больше.

Вы можете написать свой коннектор на Java и Scala. Для этого нужно создать подключаемый jar-файл, реализовав простой интерфейс коннектора.

Как запустить Kafka Connect


Локально


Поставляется вместе с Kafka

Идём на сайт Kafka и скачиваем нужную нам версию: http://kafka.apache.org/downloads.

Binary downloads:

  • Scala 2.12 - kafka_2.12-2.6.0.tgz (asc, sha512)
  • Scala 2.13 - kafka_2.13-2.6.0.tgz (asc, sha512)

Например, выберем версию Scala 2.12 (kafka_2.12-2.6.0.tgz). Распакуем архив и посмотрим в директорию kafka_2.12-2.6.0/bin. Там будут скрипты для запуска Apache Kafka (kafka-server-start.sh, kafka-server-stop.sh) и утилиты для работы с ней. Например, kafka-console-consumer.sh, kafka-console-producer.sh. А также там будут скрипты для запуска Kafka Connect (connect-distributed.sh, connect-standalone.sh), и многое другое.

Рекомендую зайти в директорию kafka_2.12-2.6.0/config там вы увидите настройки по умолчанию запуска и Kafka-брокера, и Kafka Connect.

  • connect-distributed.properties
  • connect-standalone.properties

Вот так выглядит конфигурация по умолчанию config/connect-distributed.properties:

bootstrap.servers=localhost:9092rest.port=8083group.id=connect-clusterkey.converter=org.apache.kafka.connect.json.JsonConvertervalue.converter=org.apache.kafka.connect.json.JsonConverterkey.converter.schemas.enable=truevalue.converter.schemas.enable=trueinternal.key.converter=org.apache.kafka.connect.json.JsonConverterinternal.value.converter=org.apache.kafka.connect.json.JsonConverterinternal.key.converter.schemas.enable=falseinternal.value.converter.schemas.enable=falseoffset.storage.topic=connect-offsetsconfig.storage.topic=connect-configsstatus.storage.topic=connect-statusoffset.flush.interval.ms=10000plugin.path=/opt/kafka/plugins

Kafka Connect можно запускать в режиме standalone. Это удобно для локальной разработки и тестирования, но в боевых условиях рекомендуется использовать connect-distributed (причины были описаны выше).

Режим standalone чаще всего используется для локальной разработке и тестирования.

Чтобы запустить Kafka Connect, выполните команду:

cd kafka_2.12-2.6.0bin/connect-standalone.sh config/connect-standalone.properties

Docker

Во многих Docker-образах используется этот же подход, поэтому вам достаточно переопределить CMD в Dockerfile, чтобы получить образ с Kafka Connect.

Например:

CMD ["bin/connect-distributed.sh", "cfg/connect-distributed.properties"]

Конечно, есть и готовые образы. Я рекомендую использовать варианты от компании Confluent:


Запуск коннекторов


После того, как вы запустите Kafka Connect, вы можете запускать на нём свои коннекторы.

Для управления Kafka Connect используется REST API. Полную документацию по нему можно посмотреть на сайте. Я опишу лишь те методы, которые нам понадобятся для демонстрации работы Kafka Connect.

Запросим список классов коннекторов, которые добавлены в ваш Kafka Connect:

curl -X GET "${KAFKA_CONNECT_HOST}/connector-plugins" -H "Content-Type: application/json"

В ответ мы получим нечто подобное:

HTTP/1.1 200 OK

[    {        "class": "io.debezium.connector.mysql.MySqlConnector"    },    {        "class": "io.confluent.connect.jdbc.JdbcSinkConnector"    }]

То есть вы можете создавать коннекторы только этих классов. Если хотите добавить новый класс, нужно скачать jar этого коннектора и добавить в директорию plugin.path из настройки Kafka Connect. См. файл connect-distributed.properties.

Запросим список запущенных коннекторов:

curl -X GET "${KAFKA_CONNECT_HOST}/connectors" -H "Content-Type: application/json"

В ответ получим:

HTTP/1.1 200 OK

Content-Type: application/json ["my-source-debezium", "my-sink-jdbc"]

Видим, что у нас создано два коннектора с именами my-source-debezium и my-sink-jdbc.

Получение информации о запущенном коннекторе

Общая информация:

curl -X GET "${KAFKA_CONNECT_HOST}/connectors/my-sink-jdbc" -H "Content-Type: application/json"

Конфигурация запущенного коннектора (config):

curl -X GET "${KAFKA_CONNECT_HOST}/connectors/my-sink-jdbc/config" -H "Content-Type: application/json"

Состояние запущенного коннектора (status):

curl -X GET "${KAFKA_CONNECT_HOST}/connectors/my-sink-jdbc/status" -H "Content-Type: application/json"

Создание коннектора


Пример:

curl -X POST "${KAFKA_CONNECT_HOST}/connectors" -H "Content-Type: application/json" -d '{ \    "name": "my-new-connector", \    "config": { \      "connector.class": "io.confluent.connect.jdbc.JdbcSinkConnector", \      "tasks.max": 1,      "topics": "mysql-table01,mysql-table02", \      "connection.url": "jdbc:postgresql://postgres:5432/catalog", \      "connection.user": "postgres", \      "connection.password": "postgres", \      "auto.create": "true" \    } \  }'

То есть необходимо методом POST отправить конфигурацию коннектора.

Обратите внимание, что имя коннектора должно быть уникальным в вашем кластере Kafka Connect. Но вы можете создавать несколько коннекторов одного класса с разными настройками.

Также у любого коннектора есть три обязательных параметра:

  • name уникальное имя;
  • connector.class класс коннектора;
  • tasks.max максимальное количество потоков, в которых может работать коннектор.

Настройка коннекторов


Я хотел бы рассказать про настройку коннекторов на примере DebeziumMysqlConnector и JdbcSinkConnector. С этих классов мы в Delivery Club начали работу. Но сначала я расскажу, почему вы выбрали именно их.

Причины выбора коннекторов


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

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

Для MVP Каталога решили использовать Shared Database. То есть наш новый сервис обращался в базу монолита.



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



Две главные задачи, которые мы решали:

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

Jdbc и Debezium


Когда ищешь коннекторы для баз данных, первое, что находишь JdbcSourceConnector и JdbcSinkConnector.

Нам отлично подходит JdbcSinkConnector в качестве sink-коннектора. Он подписывается на топик Kafka и выполняет запросы на добавление, изменение и удаление данных в базе.

Но в качестве Source-коннектора он нам не подходит, так как делает SQL-запросы в базу по таймеру, а это создает ещё большую нагрузку на базу-источник. Мы как раз хотим от этого уйти.

Но нам подходит DebeziumMysqlConnector. Он делает одну классную вещь: подключается к MySQL-кластеру как обычная MySQL-реплика и умеет читать бинлог. Таким образом, мы не создаём дополнительную нагрузку на базу (за исключением встроенных механизмов MySQL-репликации).



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

Debezium Connector




Все настройки коннектора можно посмотреть на сайте.

Давайте рассмотрим настройки коннектора и обсудим выбор некоторых параметров.

Файл debezium-config.json:

{  "name": "my-debezium-mysql-connector",  "config": {    "tasks.msx": 1,    "connector.class": "io.debezium.connector.mysql.MySqlConnector",    "database.hostname": "${MYSQL_HOST}",    "database.serverTimezone": "Europe/Moscow",    "database.port": "${MYSQL_PORT}",    "database.user": "${MYSQL_USER}",    "database.password": "${MYSQL_PASS}",    "database.server.id": "223355",    "database.server.name": "monolyth_db",    "table.whitelist": "${MYSQL_DB}.table_name1",    "database.history.kafka.bootstrap.servers": "${KAFKA_BROKER}",    "database.history.kafka.topic": "monolyth_db.debezium.history",    "database.history.skip.unparseable.ddl": true,    "snapshot.mode": "initial",    "time.precision.mode": "connect"  }}

Подключения к базе данных:

    "database.hostname": "${MYSQL_HOST}",    "database.serverTimezone": "Europe/Moscow",    "database.port": "${MYSQL_PORT}",    "database.user": "${MYSQL_USER}",    "database.password": "${MYSQL_PASS}",

Следует иметь в виду, что этот пользователь должен иметь права:

GRANT SELECT, RELOAD, SHOW DATABASES, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'user' IDENTIFIED BY 'password';

ID реплики, под которым будет зарегистрирован коннектор, и его имя сервера:

    "database.server.id": "223355",    "database.server.name": "monolyth_db",

Список таблиц для синхронизации:

"table.whitelist": "${MYSQL_DB}.table_name1,${MYSQL_DB}.table_name2",

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

Настройки создания snapshot'а:

    "database.history.kafka.bootstrap.servers": "${KAFKA_BROKER}",    "database.history.kafka.topic": "debezium.db.history",    "snapshot.mode": "initial",

Для чего нужен snapshot

Когда ваш коннектор Debezium MySQL запускается в первый раз, он выполняет начальный согласованный снимок вашей базы данных и сохраняет его в топик Kafka. Даже если вы будете отслеживать только несколько таблиц из базы, в database.history будет записана вся схема. Но можно не переживать из-за размера этого топика, он будет очень маленьким (менее 1 Мб).

Пропуск определений в снимке, которые по каким-то причинам не удалось распарсить:

    "database.history.skip.unparseable.ddl": true,

Эту опцию мы включили, потому что сталкивались с такими ошибками, когда определения в бинлоге использовали неверный синтаксис. Сервер MySQL более-менее интерпретирует эти инструкции и потому не падает. Но анализатор SQL-запросов в DebeziumConnector'е с ними не справляется и падает с ошибкой. Чтобы не падать, а игнорировать нечитаемые запросы, необходимо включить эту опцию.

Точность типа данных time:

"time.precision.mode": "connect",

Эта настройка уменьшает точность типа данных time с микросекунд до миллисекунд.

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

Также нашу конфигурацию можно дополнить различными трансформерами по преобразованию данных и маршрута топиков. И один из важнейших трансформеров в проекте Debezium io.debezium.transforms.ExtractNewRecordState. Почитать подробнее о нём можно в документации. Если кратко: вам потребуется его использовать для преобразования формата Debezium в формат Jdbc.

В целом, все трансформации рекомендуется использовать на стороне Sink-коннектора, а Source-коннекторы должны отправлять данные в топик Kafka без изменений.

Создание Debezium MySqlConnector:


curl  -X POST ${KAFKA_CONNECT_HOST}/connectors -H "Content-Type: application/json" -d @debezium-config.json

При создании коннектора вы можете получить ошибку:

Connector configuration is invalid and contains the following 1 error(s):
Configuration is not defined: database.history.connector.id
Configuration is not defined: database.history.connector.class
Unable to connect: Communications link failure


The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
You can also find the above list of errors at the endpoint `/connector-plugins/{connectorType}/config/validate````


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

После создания коннектора можно проверить его состояние. Этот метод также используется в качестве метрики.

curl  -X GET ${KAFKA_CONNECT_HOST}/connectors/my-debezium-mysql-connector/status

Мы увидим такой ответ:

{  "name": "my-debezium-mysql-connector",  "connector": {    "state": "RUNNING",    "worker_id": "connect:8080"  },  "tasks": [    {      "id": 0,      "state": "RUNNING",      "worker_id": "connect:8080"    }  ],  "type": "source"}

После того, как мы запустили source connector, можно убедиться, что топики были созданы и можно прочитать из них данные. Для работы с Kafka будем использовать удобную утилиту kafkacat.

Какие топики были созданы нашим коннектором:

kafkacat -b ${KAFKA_BROKER} -L | grep 'monolyth_db'

Чтение данных из топика monolyth_db.debezium.history:

kafkacat -b ${KAFKA_BROKER} -t monolyth_db.debezium.history -C -f 'Offset: %o\nKey: %k\nPayload: %s\n--\n'

Чтение данных из топика monolyth_db.table_name1 (${MYSQL_DB} имя вашей базы данных):

kafkacat -b ${KAFKA_BROKER} -t monolyth_db.${MYSQL_DB}.table_name1 -C -f 'Offset: %o\nKey: %k\nPayload: %s\n--\n'

В топиках вы увидите сообщения в формате avro (если вы использовали JsonSerializer для key, value серилизаторов). Вид и описание формата лучше прочитать в документации.

JdbcSinkConnector


В качестве Sink коннектора будем использовать JdbcSinkConnector.



Рассмотрим его конфигурацию

Создадим файл my-jdbc-sink-connector.json:

{  "name": "my-jdbc-sink-connector",  "config": {    "connector.class": "io.confluent.connect.jdbc.JdbcSinkConnector",    "tasks.max": "2",    "connection.url": "jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}",    "connection.user": "${POSTGRES_USER}",    "connection.password": "${POSTGRES_PASS}",    "topics": "monolyth_db.${MYSQL_DB}.table_name1,monolyth_db.${MYSQL_DB}.table_name2",    "pk.fields": "id",    "pk.mode": "record_key",    "auto.create": "false",    "auto.evolve": "false",    "insert.mode": "upsert",    "delete.enabled": "true",    "transforms": "route,unwrap,rename_field,ts_updated_at,only_fields",    "transforms.route.type": "org.apache.kafka.connect.transforms.RegexRouter",    "transforms.route.regex": "([^.]+)\\.([^.]+)\\.([^.]+)",    "transforms.route.replacement": "${PG_DB}.public.$3",    "transforms.unwrap.type": "io.debezium.transforms.ExtractNewRecordState",    "transforms.unwrap.drop.tombstones": "false",    "transforms.rename_field.type": "org.apache.kafka.connect.transforms.ReplaceField$Value",    "transforms.rename_field.renames": "isDeleted:is_deleted,isActive:is_active",    "transforms.ts_updated_at.type": "org.apache.kafka.connect.transforms.TimestampConverter$Value",    "transforms.ts_updated_at.target.type": "Timestamp",    "transforms.ts_updated_at.field": "updated_at",    "transforms.ts_updated_at.format": "yyyy-MM-dd'T'HH:mm:ssXXX",    "transforms.only_fields.type": "org.apache.kafka.connect.transforms.ReplaceField$Value",    "transforms.only_fields.whitelist": "id,title,url_tag,sort,hide,created_at,updated_at"  }}

Тут, конечно, три обязательных для любого коннектора параметра:

"name": "my-jdbc-sink-connector","connector.class": "io.confluent.connect.jdbc.JdbcSinkConnector","tasks.max": "2",

Настройки подключения:

"connection.url": "jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}","connection.user": "${POSTGRES_USER}","connection.password": "${POSTGRES_PASS}",

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

"topics": "monolyth_db.${MYSQL_DB}.table_name1,monolyth_db.${MYSQL_DB}.table_name2",

JdbcConnector использует один топик для одной таблицы. Сопоставление топика и таблицы происходит по имени. Для коррекции используется route-трансформер. О трансформерах поговорим чуть ниже.

Если вы указываете несколько топиков, то у них у всех должны быть одинаковые pk.fields.

Сообщения в Kafka имеют ключ, создаваемый на основании первичного ключа (Primary Key) таблицы. Какой именно PR в таблице, необходимо указать в параметрах pk.fields, чаще всего это просто id:

"pk.fields": "id","pk.mode": "record_key",

Ключ может быть составной. Например, для кросс-таблиц:

"pk.fields": "user_id,service_id",

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

"auto.create": "false","auto.evolve": "false","insert.mode": "upsert","delete.enabled": "true",

Трансформеры


Последний блок настроек касается трансформеров.

"transforms": "route, unwrap, rename_field, ts_updated_at, only_fields",

Этот параметр указывает, какие трансформеры и в каком порядке выполнять. Они расположены в этой же конфигурации коннектора. Каждый трансформер имеет type (класс) и параметры.

Например, трансформер route отвечает за сопоставление имени топика и имени таблицы:

"transforms.route.type": "org.apache.kafka.connect.transforms.RegexRouter","transforms.route.regex": "([^.]+)\\.([^.]+)\\.([^.]+)","transforms.route.replacement": "${PG_DB}.public.$3",

Он используется в Debezium MySqlConnector: отправляет данные в Kafka топики с именами {server_name}.{database_name}.{table_name}, а JdbcSinkConnector принимает {database_name}.{schema_name}.{table_name}. Так как целевая база и таблица могут отличаться по именам (и у вас вряд ли имя базы будет public), то этот коннектор изменяет целевое имя топика.

Второй важный трансформер unwrap:

"transforms.unwrap.type": "io.debezium.transforms.ExtractNewRecordState","transforms.unwrap.drop.tombstones": "false",

Он преобразует формат Debezium в формат, с которым прекрасно работает JdbcSinkConnector.

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

"transforms.ts_updated_at.type": "org.apache.kafka.connect.transforms.TimestampConverter$Value","transforms.ts_updated_at.target.type": "Timestamp", "transforms.ts_updated_at.field": "updated_at", "transforms.ts_updated_at.format": "yyyy-MM-dd'T'HH:mm:ssXXX",

Deploy


В каждой компании деплой происходит по-разному: где-то используют Jenkins, где-то Gitlab CI или Bitbucket Pipelines, а кто-то пишет скрипты.

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

Как я отмечал, Kafka Connect это отдельное stateless-приложение. Оно не зависит от Kafka-брокера и даже от версии Kafka. Если у вас уже есть Kafka старой версии, можно использовать новую версию Kafka Connect. Я рекомендую это и сделать. Например, мы использовали последнюю на тот момент версию Kafka Connect 2.5.0 с Kafka-брокером 0.10.х.

Поэтому нет каких-то общих советов и нюансов, как деплоить сервисы. Расскажу, как это происходит у нас.

Deploy Kafka Connect в Delivery Club


Kubernetes

Перед запуском в стейдж мы экспериментировали локально. Создавали свой Docker-образ на основе cp-kafka-connect, куда просто добавляли свои коннекторы.

Для стейджа было достаточно из этого образа собрать контейнер и выложить в Kubernetes, что мы и сделали.

Отмечу только, что 2 Гб памяти поду под Kafka Connect не хватает, и у нас поды по 4 Гб.

Production

На проде у нас внедрение совпало с внедрением нового кластера Kafka-брокеров. Мы приняли специфическое решение поднимать Kafka Connect на тех же серверах, где будут находиться Kafka-брокеры. Для этого использовали rpm-пакет от Confluent.

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

Но это уже отдельная история как работать с Kafka Connect в проде, которая зависит от инфраструктуры компании.

Что нам дало использование Kafka Connect


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

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

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

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

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

Резюме


Я очень рад, что вам удалось добраться до конца. В этой статье вы:

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

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

Выходим на рынок Huawei, или Как мы адаптировали приложение для работы с HMS

15.03.2021 12:08:42 | Автор: admin


Привет, Хабр! Меня зовут Георгий Гигаури, я разрабатываю Android-приложение Delivery Club. Эта статья появилась после доклада на конференции Mobius 2020, где мы выступали вместе с Павлом Борзиковым. Для тех, кто любит видео, ищите его в конце статьи.

Почему мы вообще обратили внимание на Huawei-устройства? Всё началось с того, что Huawei теперь не может распространять свои устройства с сервисами Google Play. Да, они могут использовать ОС Android, так как это открытая операционная система, но чтобы распространять устройства с сервисами Google Play, необходимо иметь лицензию. К сожалению, Huawei не может получить её из-за разногласий между Китаем и США. Поэтому Huawei приходится разрабатывать свои собственные Mobile Services. Справедливости ради, они этим занимались уже давно, но теперь им приходится расширять кодовую базу, активно увеличивать количество сервисов.

Почему стоит обратить внимание на экосистему Huawei


Смартфоны Huawei очень популярны: в 2020 году в России они занимали почти 18% рынка (Рис.1), а в мире 11% (Рис.2), (источник). Huawei заявила, что более 490 млн человек в более чем в 170 странах мира пользуются AppGallery (источник). Поскольку аудитория у Huawei-устройств огромная, мы не можем это игнорировать и решили поддержать пользователей нашего приложения. Далее поэтапно рассмотрим, что же нужно сделать.


Рис.1


Рис.2

Этап 1: проверка наличия Services


Если у вас в приложении при входе есть проверка наличия Google Services, то придётся от этого отказаться, и проверять наличие соответствующих сервисов только по мере необходимости.

fun Context.getMobileServiceSource(): MobileServicesSource {    val googleApi = GoogleApiAvailability.getInstance()    if (googleApi.isGooglePlayServicesAvailable(this) == com.google.android.gms.common.ConnectionResult.SUCCESS) {        return MobileServicesSource.GOOGLE    }    val huaweiApi = HuaweiApiAvailability.getInstance()    if (huaweiApi.isHuaweiMobileServicesAvailable(this) == com.huawei.hms.api.ConnectionResult.SUCCESS) {        return MobileServicesSource.HMS    }    return MobileServicesSource.NONE}enum class MobileServicesSource {    GOOGLE,    HMS,    NONE}

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

Этап 2: карты


В приложении Delivery Club три основные страницы:

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

На устройствах Huawei все эти карты не работают. Чтобы это исправить, можно просто заменить зависимости: вместо пакета com.google.android.gms использовать com.huawei.hms:





Конечно, есть нюансы, но мы уже сделали большую часть работы. Huawei сделала Maps SDK с контрактами, по большей части соответствующий Google Maps SDK. Однако у Google есть deprecated-методы, если вы их используете, то аналогов у Huawei может и не найтись. Например, для получения местоположения пользователя мы используем:

LocationServices.FusedLocationApi.getLastLocation(googleApiClien)

Такой подход считается deprecated, и если мы просто скопируем код для Huawei Maps и заменим зависимости, то работать не будет. Нужно поменять так:

LocationServices.getFusedLocationProviderClient().getLastLocation().addOnSuccessListener()

PolyUtil. Расшифровка с помощью Polyline


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



После расшифровки мы получили список координат курьера.

Реализация поддержки двух карт


Для поддержки нескольких карт необходимо создать обёртку для самих карт и для объекта.

Добавляем общий интерфейс, например, IMapWidget. Не забываем сделать общий класс для LatLng список координат курьера. У Google он лежит в пакете com.google.android.gms.maps.model.LatLng, а у Huawei в com.huawei.hms.maps.model.LatLng. Кладём список в PolyLineOptions и задаём ширину и цвет линии маршрута.

interface IMapWidget {    void animateCamera(...);    void setListener(OnMapEventListener listener);    void setMapPadding(...);    MapMarker addMarker(...);    ...}

Добавляем Custom Map View реализующего интерфейс IMapWidget:



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

class MapWrapper : FrameLayout() {    fun setupMap(widget: IMapWidget) {        removeAllViews()        addView(widget as View)    }}

И в нужном месте вызываем метод добавления карты.

override fun onCreateView(...) {    ...    val map: IMapWidget = MapFactory.createMap()    viewMapWrapper.setupMap(map)    ...}

Такие обёртки класса нужно создать для всего: объектов, маркеров, PolyUtil, PolyLine и т.д.

Проблема: Карта не работает




Однажды нам сообщили о баге. Пользователь с устройством Huawei, находившийся в центре Москвы (Рис.3), открыл приложение, нажал на кнопку Переместиться на своё местоположение, и его перенесло в пустоту (Рис.4). Пользователь не видит, ни улиц, ни зданий, и он решил, что карта не работает.

Мы попробовали воспроизвести у себя эту проблему. И действительно попадали в неопределённое пространство. Когда попробовали чуть-чуть уменьшить масштаб карты, то оказалось, что мы попали в пригород Мариуполя (Рис.5). То есть из московских координат (55.819207, 37.493424) перенеслись в мариупольские (47.187447, 37.593137). Мы были в полном недоумении. Может быть, где-то у нас с числами что-то не то происходит. Возможно, происходят некие вычитания наших координат. Очень долго искали решение этой проблемы или хотя бы причину. Оказалось, что мы заменили импорты из Google-карт, и поэтому всё перестало работать. В конце концов мы добрались до paddingа.



Давайте быстро вспомним, что такое padding у карты. На (Рис.6) показан экран авторизации, карта занимает всю область экрана, даже под плашкой ручного ввода адреса. В таком случае, если мы не добавим padding карте, её центр будет находиться на месте зелёного треугольника, но мы хотим, чтобы он был в центре рабочей области карты. Padding сужает рабочую область (Рис.7). Не видимую, а именно рабочую. Карта будет по-прежнему занимать весь экран, но размер её рабочей области изменится. И когда вы будете переходить в новую координату, она будет принимать положение новой рабочей карты. Как оказалось, баг был именно из-за этого.

Первое решение: убрать padding. Как вы понимаете, такой вариант нам не подошёл. Мы хотели, чтобы всё отображалось красиво.

Второе решение проблемы: использовать анимированное перемещение, но с масштабированием.

val zoom = map.cameraPosition.zoommap.animateCamera(CameraUpdateFactory.newLatLngZoom(position, zoom))

При переходе с изменением масштаба карты всё работало правильно. Здорово! Мы подумали, что это нам подходит. На самом деле нет. У нас ещё есть третий экран, на котором нужно увеличивать карту относительно двух маркеров, чтобы zoom сам рассчитывался, поэтому мы не можем задать какое-то константное масштабирование. То есть такой вариант нам тоже не подошёл. Начали думать дальше и нашли новое решение.

Третье решение проблемы: вообще отказаться от анимации. Как оказалось, если вместо animateCamera сделать просто move, то перемещение будет происходить правильно. Так мы и сделали. Надеемся, в скором времени Huawei устранит эту проблему.

Этап 3: push-сервис


Идём дальше. На Huawei-устройства не приходят уведомления нашего приложения. Дело в том, что мы не можем получить токен. Давайте его получим. В Google мы получаем задачу и извлекаем токены так:

FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->    if (task.isSuccessful) {        val token = task.result    }}

Наше решение:

class ImplementationHuaweiMessagingService : HmsMessageService() {    override fun onNewToken(token: String?) {        val commonApi = getComponentFactory().get(CommonApi::class.java)        commonApi.settingsManager().setPushToken(token)    }    override fun onMessageReceived(message: RemoteMessage?) {        message?.let {            val appManagersComponent = getComponentFactory().get(AppManagersApi::class.java)            appManagersComponent.pushManager().handle(it.dataOfMap)        }    }

Выглядит всё так же, как и с реализацией FirebaseMessagingService(), даже есть callbackи onNewToken и onMessageReceived. Однако без нюансов не обойтись. Случается, что на некоторых редких устройствах onMessageReceived вызывается в главном потоке, поэтому лучше не использовать здесь долго выполняющиеся задачи.

Получаем токены на Huawei:

val token = HmsInstanceId.getInstance(context)    .getToken(appId, com.huawei.hms.push.HmsMessaging.DEFAULT_TOKEN_SCOPE)public static final String DEFAULT_TOKEN_SCOPE = "HCM";

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

Мы можем вообще не использовать getToken, а прописать в манифесте автоматическую инициализацию или в коде методом setAutoInitEnabled() и всегда получать token в onNewToken (подробнее). Это решит ещё одну проблему: getToken в версиях EMUI ниже 10 вообще возвращает null.

<meta-data    android:name="push_kit_auto_init_enabled"    android:value="true"/>

Этап 4: Chrome Custom Tabs


Наше приложение при запуске регулярно вылетает с ошибкой ActivityNotFoundException. Чтобы от этого избавиться, нужно обработать отсутствие Chrome Tabs.

fun Context.openLink(url: String, customTabsSession: CustomTabsSession? = null): Boolean {    try {        openLinkInCustomTab(url, customTabsSession)        return true    } catch (throwable: Throwable) {        Timber.tag("Context::openLink").e(throwable, "CustomTabsIntent error on url: $url")    }    return openLinkInBrowser(url)}@Throws(Throwable::class)fun Context.openLinkInCustomTab(url: String, customTabsSession: CustomTabsSession? = null) {    CustomTabsIntent.Builder(customTabsSession)        .build()        .launchUrl(this, Uri.parse(url))}private fun Context.openLinkInBrowser(url: String): Boolean {    val intent: Intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply {        addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_DOCUMENT)    }    if (intent.resolveActivity(packageManager) != null) {        startActivity(intent)        return true    }    return false}

Мы просто обернули openLinkInCustomTab() в try catch и в случае ошибки пытаемся открыть в браузере. Но бывает такого, чтобы на устройстве не было подходящего браузера, способного обработать наш неявный intent. Поэтому если метод openLinkInBrowser() возвращает false, мы открываем страницу в webview.

Этап 5: аналитика


Аналитика у Huawei похожа на Google Analytics. Покажу замену на примере Firebase. Сначала инициализируем: HiAnalytics.getInstance(context). Затем с помощью HAEventType.STARTCHECKOUT копируем все наши события из Firebase в отдельный файл huaweiAnalytics:

huaweiAnalytics.onEvent(name, bundle)

Системные параметры: HAParamType.PRICE, HAParamType.CURRNAME

Даже если у вас нет Firebase, добавить аналитику в Huawei очень просто. У них отличная документация, контракт соблюдается. Также у Huawei есть отличные инструменты для исследования аудитории.

Этап 6: crashlytics


Следующий инструмент, который нам тоже стало интересно попробовать, это Crashlytics от Huawei, которая называется AGConnectCrash. Она позволяет с минимальными усилиями собирать и анализировать информацию о падении приложения.

Инициализируем crashlytics:

AGConnectCrash.getInstance().enableCrashCollection(true)

Добавляем свои ключи и журналируем нужные события:

AGConnectCrash.getInstance().setUserId("testuser")AGConnectCrash.getInstance().log(Log.DEBUG, "set debug log.")AGConnectCrash.getInstance().log(Log.INFO, "set info log.")AGConnectCrash.getInstance().log(Log.WARN, "set warning log.")AGConnectCrash.getInstance().log(Log.ERROR, "set error log.")AGConnectCrash.getInstance().setCustomKey("stringKey", "Hello world")AGConnectCrash.getInstance().setCustomKey("booleanKey", false)AGConnectCrash.getInstance().setCustomKey("doubleKey", 1.1)AGConnectCrash.getInstance().setCustomKey("floatKey", 1.1f)AGConnectCrash.getInstance().setCustomKey("intKey", 0)AGConnectCrash.getInstance().setCustomKey("longKey", 11L)

Этап 7: покупки в приложении


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

Всё очень похоже на реализацию Google. При запуске приложения запрашиваем все прошлые покупки пользователя:

fun getOwnedPurchases(    activity: Activity,    ownedPurchasesResultOnSuccessListener: OnSuccessListener<OwnedPurchasesResult>,    failureListener: OnFailureListener) {    val ownedPurchasesReq = OwnedPurchasesReq()    // priceType: 0: consumable; 1: non-consumable; 2: auto-renewable subscription    ownedPurchasesReq.priceType = IapClient.PriceType.IN_APP_SUBSCRIPTION    // To get the Activity instance that calls this API.    val task: Task<OwnedPurchasesResult> = Iap.getIapClient(activity)        .obtainOwnedPurchases(ownedPurchasesReq)    task.addOnSuccessListener(ownedPurchasesResultOnSuccessListener)        .addOnFailureListener(failureListener)}

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

fun loadProduct(    context: Context,    productInfoResultOnSuccessListener: OnSuccessListener<ProductInfoResult>,    onFailureListener: OnFailureListener) {    // obtain in-app product details configured in AppGallery Connect, and then show the products    val iapClient: IapClient = Iap.getIapClient(context)    val task: Task<ProductInfoResult> = iapClient.obtainProductInfo(createProductInfoReq())    task.addOnSuccessListener(productInfoResultOnSuccessListener)        .addOnFailureListener(onFailureListener)}private fun createProductInfoReq(): ProductInfoReq {    val req = ProductInfoReq()    // 0: consumable ; 1: non-consumable ; 2: auto-renewable subscription    req.priceType = IapClient.PriceType.IN_APP_SUBSCRIPTION    val productIds = ArrayList<String>()    productIds.add("PRODUCT_ID")    req.productIds = productIds    return req}

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

fun gotoPay(activity: Activity, productId: String, type: Int) {    val client: IapClient = Iap.getIapClient(activity)    val task: Task<PurchaseIntentResult> = client.createPurchaseIntent(createPurchaseIntentReq(type, productId))    task.addOnSuccessListener { result ->        result?.let {            val status: Status = result.status            if (status.hasResolution()) {                try {                    status.startResolutionForResult(activity, PAY_RESULT_ARG)                } catch (exception: SendIntentException) {                    Timber.e(exception)                }            } else {                Timber.d("intent is null")            }        }    }.addOnFailureListener { exception ->        Timber.e(exception)    }}

Так как это Activity, мы передаём ему аргумент, по которому можно отловить OnActivityResult и понять, успешно ли прошла оплата и как закончилась транзакция:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {    super.onActivityResult(requestCode, resultCode, data)    if (resultCode == PAY_RESULT_ARG) {        val purchaseResultInfo: PurchaseResultInfo = Iap.getIapClient(this).parsePurchaseResultInfoFromIntent(data)        when (purchaseResultInfo.returnCode) {            OrderStatusCode.ORDER_STATE_SUCCESS -> {                successResult(purchaseResultInfo)            }            OrderStatusCode.ORDER_STATE_CANCEL -> {            }            OrderStatusCode.ORDER_PRODUCT_OWNED -> {            }        }    }}

У нас есть специальные статусы: ORDER_SUCCESS, CANCEL, OWNED. Первый означает успешную оплату. Второй пользователь просто закрыл страницу без покупки, тогда мы обрабатываем этот callback и предлагаем скидку, чтобы уговорить на покупку. А третий статус означает, что товар уже куплен пользователем. Если товар разовый или подписочный, то на этом моменте нужно остановиться, в противном случае виртуально доставить покупку.

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

private fun successResult(purchaseResultInfo: PurchaseResultInfo) {    val inAppPurchaseData = InAppPurchaseData(purchaseResultInfo.inAppPurchaseData)    val req = ConsumeOwnedPurchaseReq()    req.purchaseToken = inAppPurchaseData.purchaseToken    val client: IapClient = Iap.getIapClient(this)    val task: Task<ConsumeOwnedPurchaseResult> =        client.consumeOwnedPurchase(req)    task.addOnSuccessListener {        // Consume success    }.addOnFailureListener { exception ->        Timber.e(exception)    }}

Если не сделать доставку, то функциональность товара будет у пользователя заблокирована, а деньги возвращены. В Google Play Billing Library до третьей версии этого делать не нужно было, но потом Google тоже это добавил, и если мы не доставим товар, через 48 часов покупка отменится, а деньги вернутся пользователю. То есть в Huawei покупки реализованы как в третьей версии Google Play Billing.

Выводы


На реализацию поддержки Huawei-устройств не уйдёт много времени. Даже без реальных устройств вы сможете проверить работоспособность вашего приложение: у Huawei есть своя тестовая лаборатория с виртуальными устройствами наподобие Samsung Remote test lab. Количество пользователей быстро растёт, и бизнесу может оказаться выгодным вложиться в доработку продуктов, а отличная документация поможет разработчикам всё сделать быстро. Поддержка HMS активно отвечает на любые вопросы, если вы не сможете в документации что-то найти.

Видеозапись доклада с конференции Mobius 2020.

Полезные ссылки


Подробнее..

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 и, возможно, реализованы в нашем приложении.
Подробнее..

Разделяй и властвуй. Модульное приложение из монолита на Objective-C и Swift

11.08.2020 14:11:07 | Автор: admin


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

Я хочу рассказать, как разбивал существующий проект на Objective-C и Swift на отдельные модули frameworkи. Согласно Apple, framework это директория определенной структуры.

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

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

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

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

Проект содержал много legacy-кода, перекрестных зависимостей от классов на Objective-C и Swift, разных targetов в терминах iOS-разработки, внушительный список CocoaPods. Любой шаг в сторону от этого монолита приводил к тому, что проект переставал собираться в Xcode, обнаруживая порой ошибки в самых неожиданных местах.

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

Первые шаги


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

1. Создаем первый модуль: File New Project Cocoa Touch Framework

2. Добавляем модуль в workspace проекта





3. Создаем зависимость основного проекта от модуля, указав последний в разделе Embedded Binaries. Если в проекте несколько targetов, то модуль надо будет включить в раздел Embedded Binaries каждого зависящего от него targetа.

От себя добавлю только один комментарий: не спешите.

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

Как выделить модуль? Наиболее логичный подход по фичам (features), то есть по какой-то пользовательской задаче. Например, чат с техподдержкой, экраны регистрации/авторизации, bottom sheet с настройками основного экрана. Кроме этого, скорее всего, понадобится какая-то базовая функциональность, которая представляет из себя не feature, а лишь набор UI-элементов, базовых классов и т.д. Эту функциональность следует вынести в общий модуль, аналогичный знаменитому файлу Utils. Не бойтесь раздробить и этот модуль. Чем меньше кубики, тем проще их вписать в основную постройку. Мне кажется, так можно сформулировать еще один из принципов SOLID.

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

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

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

Чтобы сделать все обособленные сущности доступными извне модуля, придётся принять во внимание особенности Swift и Objective-C.

5. В Swift все классы, перечисления и протоколы должны быть помечены модификатором доступа public, тогда к ним можно будет получить доступ снаружи модуля. Если в отдельный framework перемещается базовый класс, его следует пометить модификатором open, иначе не получится создать от него класс-потомок.

Сразу следует вспомнить (или впервые узнать), какие есть уровни доступа в Swift, и получить profit!



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



Затем необходимо добавить импорт нового frameworkа в Swift-файл, где используется выделенная функциональность, наряду с каким-нибудь UIKit. После этого ошибок в Xcode должно стать меньше.

import UIKitimport FeatureOneimport FeatureTwoclass ViewController: UIViewController {//..}

С Objective-C последовательность действий немного сложнее. Кроме того, использование bridging headerа для импорта классов Objective-C в Swift не поддерживается во frameworkах.



Поэтому поле Objective-C Bridging Header должно быть пустым в настройках frameworkа.



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

6. У каждого frameworkа есть собственный заголовочный файл umbrella header, через который будут смотреть во внешний мир все публичные интерфейсы Objective-C.

Если в этом umbrella header указать импорт всех прочих заголовочных файлов, то они будут доступны в Swift.



import UIKitimport FeatureOneimport FeatureTwoclass ViewController: UIViewController {        var vc: Obj2ViewController?        override func viewDidLoad() {        super.viewDidLoad()        // Do any additional setup after loading the view, typically from a nib.    }

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



7. Когда все файлы поодиночке перенесены в отдельный модуль, нужно не забыть о Cocoapods. Файл Podfile требует реорганизации, если какая-то функциональность окажется в отдельном frameworkе. У меня так и было: pod с графическими индикаторами надлежало вынести в общий framework, а чат новый pod был включён в свой собственный отдельный framework.

Необходимо явно указать, что проект теперь не просто проект, а рабочее пространство с подпроектами:

workspace 'myFrameworkTest'

Общие для frameworkов зависимости следует вынести в отдельные переменные, например, networkPods и uiPods:

def networkPods     pod 'Alamofire'end def uiPods     pod 'GoogleMaps' end

Тогда зависимости основного проекта будут описаны следующим образом:

target 'myFrameworkTest' doproject 'myFrameworkTest'    networkPods    uiPods    target 'myFrameworkTestTests' do    endend 

Зависимости frameworkа с чатом таким образом:

target 'FeatureOne' do    project 'FeatureOne/FeatureOne'    uiPods    pod 'ChatThatMustNotBeNamed'end


Подводные камни


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

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

Первая проблема скрывалась в реализации чата. На просторах сети проблема встречается и в других podах, достаточно загуглить Library not loaded: Reason: image not found. Именно с таким сообщением происходило падение.

Более элегантного решения я не нашёл и был вынужден продублировать подключение podа с чатом в основном приложении:

target 'myFrameworkTest' do    project 'myFrameworkTest'    pod 'ChatThatMustNotBeNamed'    networkPods    uiPods    target 'myFrameworkTestTests' do    endend

Таким образом Cocoapods позволяет приложению видеть динамически подключенную библиотеку при запуске и при компиляции проекта.

Другая проблема заключалась в ресурсах, про которые я благополучно забыл и нигде не встречал упоминания о том, что этот аспект надо держать в уме. Приложение падало при попытке зарегистрировать xib-файл ячейки: Could not load NIB in bundle.

Конструктор init(nibName:bundle:) класса UINib по умолчанию ищет ресурс в модуле главного приложения. Естественно, об этом ничего не знаешь, когда разработка ведется в монолитном проекте.

Решение указывать bundle, в котором определен класс ресурса, либо позволить компилятору сделать это самому, используя конструктор init(for:) класса Bundle. Ну и, конечно, впредь не забывать о том, что ресурсы теперь могут быть общими для всех модулей или специфичными для одного модуля.

Если в модуле используются xibы, то Xcode будет, как обычно, предлагать для кнопок и UIImageView выбирать графические ресурсы из всего проекта, но в run time все расположенные в других модулях ресурсы окажутся не загруженными. Я загружал изображения в коде, используя конструктор init(named:in:compatibleWith:) класса UIImage, где вторым параметром идёт Bundle, в котором расположен файл изображения.

Ячейки в UITableView и UICollectionView теперь также должны регистрироваться подобным образом. Причем надо помнить, что Swift-классы в строковом представлении включают в себя ещё и имя модуля, а метод NSClassFromString() из Objective-C возвращает nil, поэтому рекомендую регистрировать ячейки, указывая не строку, а класс. Для UITableView можно воспользоваться таким вспомогательным методом:

@objc public extension UITableView {    func registerClass(_ classType: AnyClass) {        let bundle = Bundle(for: classType)        let name = String(describing: classType)        register(UINib(nibName: name, bundle: bundle), forCellReuseIdentifier: name)    }}


Выводы


Теперь можно не переживать, если в одном pull request окажутся изменения в структуре проекта, сделанные в разных модулях, потому что у каждого модуля свой xcodeproj-файл. Можно распределять работу так, чтобы не приходилось тратить несколько часов на сведение файла проекта воедино. Полезно иметь модульную архитектуру в больших и распределенных командах. Как следствие, должна увеличиться скорость разработки, но верно и обратное. На свой самый первый модуль я потратил гораздо больше времени, чем если бы создавал чат внутри монолита.

Из очевидных плюсов, на которые также указывает Apple, возможность снова использовать код. Если в приложении имеются различные targetы (app extensions), то это самый доступный подход. Возможно, чат не самый лучший вариант для примера. Следовало начать с вынесения сетевого слоя, но давайте будем честными сами с собой, это очень длинная и опасная дорога, которую лучше разбить на небольшие отрезки. А так как за последние пару лет это было внедрение второго сервиса для организации технической поддержки, хотелось внедрить его не внедряя. Где гарантии, что скоро не появится третий?

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

Сценарий идеального технического собеседования

07.10.2020 14:10:47 | Автор: admin


Дисклеймер: это сценарий идеального технического собеседования в Delivery Club Tech. Мнение нашей команды может не совпадать с мнением читателей.

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

1. Собесы бывают разные: жёлтые, зелёные, красные (лирическое отступление)


Есть мнение, что сложные технические собесы не работают. Сооснователь платформы для рекрутинга Interviewing.io Алин Лернер ранее писала, что компании, которые подбирают сотрудников, опираясь на сложные технические собеседования, тратят ресурсы на множество кандидатов, которые не понимают игровую сущность собеседований. В результате на финишную прямую в таких компаниях выходят кандидаты, которые хороши именно в прохождении интервью.

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

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

В 1990-х годах с бумом доткомов последовал рост найма технических специалистов, и Microsoft взяла на вооружение подход прошлых лет. Их примеру некоторое время следовала Google.

Впоследствии Google и Microsoft отказались от популярных головоломок из серии как передвинуть гору Фудзи. Что касается найма, то мы обнаружили, что головоломки это пустая трата времени. Сколько мячей для гольфа вы можете поместить в самолет? Сколько заправочных станций на Манхэттене? Полная трата времени. Они ничего не предсказывают. Они служат, в первую очередь, для того, чтобы интервьюер чувствовал себя умным, признал старший вице-президент по работе с персоналом в Google в интервью New York Times.

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

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

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

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

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

2. Идеальный формат, идеальный кандидат (формируем требования)


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

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

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

На помощь пришел давнишний подход, впервые применённый в Гарвардской бизнес-школе в 1924 году ситуационное интервью, или кейс-интервью. Условно его можно разделить на три большие части:

  • ценности и взгляды кандидата, soft skills;
  • профессиональные навыки и умения, hard skills;
  • модели поведения и индивидуально-личностные качества.

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

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

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

Конечно, эпидемиологическая ситуация в мире внесла свои коррективы, и Zoom вытеснил Skype, но скрининг был и остаётся нашим бессменным первым этапом.

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

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

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

3. О бедном эйчаре замолвите слово (про важность хорошего HR-специалиста)


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

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

Крупная компания, известный бренд могут также осложнять работу HR-специалиста, будучи у всех на слуху. Хорошее знание проекта, процессов разработки и команды позволяют рекрутеру уже на первом этапе принять решение о том, подходит ли кандидат для вакансии. В результате до 60% кандидатов доходят до технического интервью, говорит руководитель направления подбора персонала Mail.ru Group Карина Пушкина. Однако объёмы таковы, что 60% это не 6 человек.

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

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

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

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



4. Как играть на поле кандидата, не забывая про себя (требования к интервьюерам)


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

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

Это отнюдь не означает, что к собеседованию не нужно готовиться. Здесь важно соблюдать баланс. Предложите кандидату самому выбрать и объяснить задачу и её решение при помощи его любимой технологии. Попросите решить типовую задачу из вашей предметной области, но при помощи его инструментов. Так или иначе вы сможете совместно поработать над проблемой, знакомясь и с точкой зрения кандидата, и с привычной ему технологией. Типовые задачи, которые подходят для такого кейс-интервью, как правило, хорошо иллюстрируются проектированием архитектуры сервиса, модуля или экрана. Кандидат абстрагируется от конкретной реализации, а вам не нужно погружаться в техническую специфику. Поэтому открывайте draw.io или любой другой сайт для проектирования блок-схем и диаграмм, и вперёд! Этот формат отлично подходит для Zoom.

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

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

Задавать технические вопросы, как и проводить техническое собеседование, интервьюер должен структурировано. Нил Роузман предостерегает: Если вы провели собеседование и всё, что можете сказать это: Ну да, он вроде ничего, мне понравился, тогда вы потратили время зря. Структурированный подход должен стать рутиной в подготовке и проведении собесов.

5. Сценарий идеального технического собеседования


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

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

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

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

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

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

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

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

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

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

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

Сценарий идеального технического интервью. Драма в пяти действиях


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

Действующие лица:

  • Олег молодой, перспективный iOS-разработчик
  • Василий умудрённый опытом тех лид команды iOS

Действие первое




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

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

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


Действие второе




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

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


Действие третье




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

Действие четвертое




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

Для понимания последнего кейса Василий предлагает Олегу архитектурную задачу.


Действие пятое




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

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




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



На этом всё. Спасибо, что дочитали!
Подробнее..

Зачем нам 170 разработчиков

21.07.2020 14:09:04 | Автор: admin
image

Привет, Хабр! Меня зовут Андрей Евсюков, я заместитель CTO в Delivery Club. Наша компания устроена сложнее, чем может показаться, когда представляешь себе сервис по доставке еды. Даже когда примерно знаешь, что там может быть под капотом.

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

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

Начну со статьи про особенности индустрии foodtech, которые напрямую влияют на то, как всё организовано внутри Delivery Club. И в процессе постараюсь объяснить, для чего нам 170 разработчиков и почему это не может быть просто outsource.

Особенности FoodTech в России и отличия от классического e-commerce


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

Доставка еды сильно отличается от большинства других доставок


Давайте рассмотрим доставку канцелярии, книг или одежды: заказ сформирован, собран в коробку, лежит на складе. Автоматически формируется маршрутный лист, даже если это same day delivery это не происходит мгновенно. Курьер забирает заказы и развозит их по списку: одному клиенту, другому всё по порядку. Всё заранее известно. Если происходит задержка в небольшом диапазоне времени, это не так критично все товары останутся целыми, клиент, как правило, готов немного подождать. А если диапазон хотя бы часовой, то точное время доставки доподлинно неизвестно.

С едой всё не так.

  • Мы должны контролировать время! Когда пользователь делает заказ, он голоден. Он не может ждать. Еда должна быть горячей, каждая минута на счету.
  • Невозможно составить маршрутный лист. В Delivery Club только 2% предзаказов. А в остальном никто не заказывает еду заранее это всегда происходит on demand.

  • Процесс работы курьеров динамичный. Ситуация меняется каждые 5-15 минут. Когда начинается дождь или снег, всегда повышается спрос. А когда на улице солнечно и не хочется сидеть дома, спрос снижается. В праздничные и выходные дни профиль спроса отличается от будней. Дорожная ситуация и пробки также вносят свои коррективы, особенно в тех зонах, где превалируют авто/мото-курьеры.

Давайте ещё раз посмотрим на ситуацию на рынке:

  1. Рынок быстро меняется. Появляются новые вертикали и направления. Представьте, Delivery Club уже 10 лет. С 2009 по 2016 мы были маркетплейсом. В 2016-м начали развивать логистику. За последние полгода мы запустили доставку продуктов, доставку из аптек, Takeaway и начали предоставлять сервис экспресс-логистики сторонним компаниям (Связной). Мы видим ещё много ниш, в которых мы можем упростить жизнь пользователей.
  2. Ожидания от уровня сервиса меняются быстро. Ещё пару лет назад мы заказывали суши по телефону и были готовы ждать пиццу три часа. Сегодня пользователь привык получать бургер и любимый боул с авокадо через 40 минут, сделав пару кликов в смартфоне. FoodTech это одна из тех индустрий, где сегодня создаются новые потребительские привычки, и происходит это прямо сейчас!
  3. Количество заказов продолжает расти с колоссальной скоростью. Представьте: за весь 2018 год мы выполнили 4 миллиона заказов, в то время как только за один сентябрь 2019-го 3 миллиона, а уже в марте 2020-го вышли на показатель в 1 миллион заказов в неделю!
  4. С другой стороны, нам нужно быстро реагировать на новые возникающие потребности пользователей. В период пандемии нужно было быстро катить обновления. Организовать быструю доставку продуктов в период самоизоляции. Вводить новые меры для профилактики коронавирусной инфекции, обеспечить безопасность курьеров, клиентов и партнеров. Отменить расчёт наличными.

Почему в России всё по-другому


Конечно же, мы смотрим на опыт компаний на тех рынках, где foodtech более развит в Европе, Юго-Восточной Азии, Индии. Но этот опыт невозможно использовать as is, так как у них другая география и топология, условия, покупательская способность. У нас крупнейшая страна мира по площади, организовать здесь логистику уникальная задача. Инфраструктура наших городов тоже отличается: другое разделение на авто/мото/пешую доставку, другая плотность расположения ресторанов (много ТЦ и отдельных маленьких кафешек).

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

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

Как мы с этим справляемся

Маршрутные листы нам заменяет прогнозирование спроса. Мы знаем погоду, день недели, месяц, составляем зоны доставки и смены. Для коммуникации с курьерами мы сделали мобильное приложение RiderApp.

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

При разработке новой функциональности мы используем гипотезы. Мы оцениваем, как изменения в продукте будут влиять на пользователя, проводим исследования, подкрепляем эти результаты теми аналитическими данными, которые у нас уже есть. Делим разработку на этапы, чтобы понять, где можно сделать проще, и быстрее выпустить MVP. Это особенно актуально при выходе на новые вертикали рынка. Чтобы собрать всё это вместе, мы внедрили отдельный процесс построения и проверки гипотез. Про это я подробно расскажу в отдельной статье GIST фреймворк верификации гипотез в Delivery Club.

Структуры команд мы поделили по продуктовым направлениям и провели продуктовую трансформацию. Все команды у нас отвечают за свой продукт или value stream внутри отдельно взятого продукта. Стратегию продуктовых изменений определяет Product Manager. Это позволяет, во-первых, командам более четко фокусироваться на поставленной задаче и добиваться результата в ожидаемые сроки. Во-вторых, позволяет разработчикам лучше понимать конечных пользователей и быть вовлеченными.

Сама трансформация у нас началась в конце 2018 года, а новый процесс разработки закрепился в начале 2019 года. С середины 2019-го у нас идет активный найм. За это время мы выросли в 4 раза, а это +120 человек. Поэтому я бы сказал, что процесс трансформации продолжается до сих пор. О нём я расскажу в отдельной статье.

За десять лет Delivery Club стал лидером доставки еды в России с присутствием более чем в 150 городах, 22 тысячами ресторанов-партнеров и более 5,5 млн заказов ежемесячно. Чтобы быстро реагировать на все изменения, скорость роста количества заказов и новые вызовы, и при этом оставаться лидерами, мы должны понимать свою аудиторию, быть гибкими и адаптивными, ориентироваться на результат и строить такие процессы внутри, которые помогли бы достигнуть этих целей. Всё это находит отражение в нашей культуре.

Особенности культуры Delivery Club Tech


Давайте подытожим, какие есть особенности у современного рынка FoodTech в России:

  • Опыт не всегда можно скопировать.
  • Появляются новые вертикали, рынок быстро меняется.
  • Один из самых быстрорастущих сегментов e-commerce.
  • Формирование новых потребительских привычек.
  • Нужно быстро реагировать на возникающие пользовательские потребности.

Из этих особенностей и строятся основные принципы нашей культуры:



Инженерная культура, в свою очередь, является субкультурой естественным продолжением культуры компании. В ней отражены не только принципы работы, но и то, как мы выбираем технологии, строим архитектуру, нанимаем людей и строим команды. Сейчас я остановлюсь только на одном аспекте: in-house разработке!

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

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

Но приложение же работает нормально, зачем вам 170 человек?


Как я писал выше, e-com похож на айсберг, со стороны всегда видна лишь малая верхняя часть. Вся операционная деятельность, которая в основном сосредоточена в оффлайне, скрыта от глаз обычного пользователя.

В основе бизнеса Delivery Club лежат четыре самых важных вектора:

  1. Клиент, который покупает еду.
  2. Доставщик.
  3. Партнёр (ресторан/магазин).
  4. Техподдержка: call-центр и диспетчеры, которые контролируют процесс.

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

В прошлом году мы сформировали ещё два направления: R&D и Platform. Направление R&D решает наукоёмкие задачи, работает с зоной низкой определенности, которая сейчас в основном сосредоточена вокруг логистических задач. Ребята вместе с отделом Операций оптимизируют бизнес-процессы и автоматизируют ручные и рутинные действия.

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

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

Выводы


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

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

Для этого мы выбрали путь in-house разработки. А все особенности рынка FoodTech отразили в своих принципах инженерной культуры. Кстати, вот они, взгляните: tech.delivery-club.ru/culture.

Инженерная культура, в свою очередь, нам подсказывает, какие Soft Skills важны для сотрудников IT-отдела Delivery Club. Эти качества стали основой нашего фреймворка найма.

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

Технические аспекты мы также не пропустим. Отдельную статью посвящу Платформе и Архитектуре. А также отдельно поговорим про Go-Swagger и Kafka Connect.

Надеюсь, мне удалось погрузить вас в контекст foodtech-рынка и объяснить, зачем Delivery Club 170 разработчиков.

Спасибо, что дочитали!
Подробнее..

Категории

Последние комментарии

  • Имя: Murshin
    13.06.2024 | 14:01
    Нейросеть-это мозг вселенной.Если к ней подключиться,то можно получить все знания,накопленные Вселенной,но этому препятствуют аннуннаки.Аннуннаки нас от неё отгородили,установив в головах барьер. Подр Подробнее..
  • Имя: Макс
    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