Привет!
Меня зовут Андрей Калинин, я директор по IT сервиса кикшеринга «Юрент». Я занимаюсь всей IT-инфраструктурой в компании и когда-то в стародавние времена (хотя это было всего 4 года назад), я с нуля разрабатывал платформу для шеринга самокатов и велосипедов.
В этом году пользователи совершили на наших самокатах больше 30 млн поездок в 114 городах. Это было бы невозможно без существенного увеличения парка электросамокатов: в этом сезоне мы «приросли» более чем на 40%, до 85 тысяч устройств. Такой рост – непростая задача не только с точки зрения бизнеса, обслуживания самокатов, но в первую очередь – с точки зрения увеличения нагрузки на IT-инфраструктуру нашего сервиса.
Аренда самокатов – это высоконагруженный сервис, и его устойчивость зависит не только от того, как хорошо работает наша IT-платформа, но и как она взаимодействует с каждым отдельным самокатом. Исторически у нас сложилась довольно необычная ситуация, когда в парке много моделей самокатов – пять моделей только самокатов Ninebot, несколько видов Okai, и еще с десяток других. А еще для них нужны трекеры, которых тоже много разных и не все согласны «дружить» с той или иной моделью самокатов.
Для масштабирования такого «зоопарка» самокатов нам пришлось искать нестандартное решение, которое позволит эффективно управлять ими и при необходимости добавлять новые. В этом кейсе я расскажу, как устроен сервис кикшеринга с точки зрения IoT-устройств, и. как мы разработали по сути уникальную программную архитектуру, которая позволяет нам унифицированно взаимодействовать с большим числом самокатов разных производителей через различные технологические протоколы.
Самокатный зоопарк
Прообраз Юрент появился в 2018 году на Юге. Мы начинали с байкшеринга, закупив первые велосипеды у китайского сервиса Ofo. Трекеры для них мы разрабатывали собственные вместе со специалистами из Томска. Буквально через два месяца у нас появились первые электросамокаты Ninebot ES4. Никакой специальной «шеринговой» техники, которую мы используем сейчас, тогда не было, и первые самокаты мы купили в магазине.
У нас был опыт разработки платформы для управления парком автомобилей на базе трекеров «Навтелеком» – мы запускали собственный каршеринг UrentCar. Но нам изначально было понятно, что шеринг самокатов потенциально будет иметь больше подключенных устройств. Там, где сервису каршеринга достаточно 1000 машин, микромобильного транспорта должно быть 10 000 единиц. Вызов по масштабированию IT-системы перед нами стоял с первых дней.
Чтобы пользователь подошел к самокату/велосипеду и смог его разблокировать через приложение, а проходящий мимо гражданин не мог уехать на нем бесплатно, каждое устройство должно быть на связи с платформой. Для этого на самокате установлен трекер, который общается с нашей инфраструктурой.
Внутри трекера стоит GSM-модуль с сим-картой, благодаря наличию в трекере GPS-модуля мы знаем координаты самоката. Трекер должен подать команду на контроллер самоката включить мотор-колесо или дать нам заряд батарейки. Также трекер передает нам сообщения об ошибках: самокат лежит, упал, неисправность с дисплеем, неисправность с мотор-колесом и другие. Кстати, такие сообщения мы внутри компании называем «тревогами». Самокат может «тревожиться» по сотням разных причин, и мы можем рассортировать эти тревоги и выделить важные, например, что выключенный самокат кто-то взял и несанкционированно потащил. В таком случае на «тревогу» реагирует наша служба безопасности и едет искать самокат.
Трекер передает сообщения по одному из протоколов, причем разные трекеры могут работать с различными протоколами. Здесь нужно еще отметить, что определенная модель самоката совместима не с каждым трекером. У самоката внутри есть своя электронная часть, которая отвечает за снятие замеров показателей батареи, разблокировку мотор-колеса, дисплей. И некоторые трекеры могут восприниматься «мозгом» самоката как чужеродный элемент.
Как я рассказывал выше, мы начинали разрабатывать шеринговую платформу с велосипедами – трекер для замка на велосипеде мы разрабатывали сразу на протоколе MQTT. Когда мы запустили самокаты Ninebot ES4, мы решили поставить на них трекеры «Навтелеком», который уже использовали в каршеринге. Он довольно компактный, но «дубовый», так как работает на бинарном протоколе поверх TCP/IP. На части парка (около 4000 самокатов) мы живем с ним до сих пор. В 2019 году у нас появились самокаты шведской компании Voi с трекерами их разработки, работающим по MQTT протоколу. В 2020 году при запуске Москвы у нас появился трекер Omni, который работал на чистом TCP/IP.
По мере того как мы увеличивали размер парка, чтобы запускать новые города, мы закупали новые модели самокатов и трекеры. На данные момент у нас порядка 10 видов самокатов и 14 видов трекеров, а также различные сочетания одного с другим. Между собой мы ласково называем это многообразие «наш зоопарк».
Поддерживать такой разнообразный парк – непросто и недешево с точки зрения разработки и операционной деятельности (разные самокаты – это разные «болячки», запчасти и тд). Но для нас это осознанная бизнес-политика: мы выбираем модель самоката под каждый конкретный город, учитывая его размер, состояние инфраструктуры, покупательскую способность населения. Благодаря этому мы можем окупаться как бизнес не только в городах-миллионниках и курортах, но и в небольших городах.
Как устроена платформа для управления парком самокатов
В конце 2017 года Андрей Колесник (основатель Юрент) пришел ко мне с идеей байкшеринга, это был интересный вызов, на который я как разработчик, конечно, согласился. Но я принял эту задачу не только как бизнес челлендж, но и эксперимент для самого себя. Я не стал как большинство стартапов уходить в «монолит-на-коленках» (что быстрее на первых порах), а взял ту архитектуру и тот стек, которые знал только в теории. Я сделал ставку на микросервисы, высокую нагрузку и масштабирование с первых дней разработки. В будущем это оказалось верным решением, которое позволило нам буквально за два месяца после велосипедов запустить самокаты, а потом спокойно развивать ее, добавляя все новые модули. Хотя наверное, если бы Андрей Колесник тогда знал, что я пошел на такой эксперимент с более сложной разработкой, он, наверное, был бы не рад :)
В рамках микросервисной инфраструктуры на каждую функцию продукта создается свой сервис: сервис для управления заказами, сервис управления транспортом, сервис авторизации, сервис управления хранением треков – кстати, сейчас там уже террабайты и триллионы записей, огромные базы. В среднем каждый самокат отправляет 1 сообщение на сервер за 30 секунд, в день весь парк отправляет около 250 млн сообщений. Мы собираем данные со всех самокатов и храним в виде телеметрических данных, чтобы потом выстраивать треки, с которыми работает наш операционный бизнес – например, чтобы понимать, где чаще всего открывают приложение и где нужно ставить самокаты, служба безопасности, операторы управления платформой.
Все сервисы-программы общаются друг с другом через свой собственный протокол.
Отдельно функционируют сервисы, которые преобразуют сообщения от трекера в понятные нашей инфраструктуре язык, – мы называем их драйверы. Каждый производитель трекеров и производитель самокатов работают на разных протоколах. У нас есть HRW-отдел, который учит «дружить» трекеры и драйверы – им нужно изучить все протоколы, чтобы понимать, что нам передает трекер и научить их переводить с языка этого трекера на понятный системе общий язык. Сложно их не только «подружить», но и поддерживать: производитель может выпустить новую прошивку, а она не будет работать с какой-то моделью самокатов (лампочка не включится, дисплей не отвечает или что-то еще). Это постоянный процесс.
Работа с «железом» часто бывает очень увлекательна. Например, купили мы самокаты Х с трекерами Y. Пришлось писать новый драйвер, который понимает трекер Y. А потом мы целый месяц разбирались, почему самокаты выключаются, почему у них режимы не меняются и т.д. Мы достаточно быстро умеем писать новый драйвер, но когда самокат выходит в город, проявляется много нюансов. Пропадает связь, пропускает команды, или если за секунду самокату отправлять больше 2-х команд, он зависает. А в некоторые трекеры приходится отправлять команды включения и смены скорости, потому что смена скорости без команды включения не работает. А если ты сюда еще отправишь команду «спросить состояние», то он зависнет. Про такие нюансы никто не рассказывает, производитель где-то там в Европе или Китае, и нам приходится все это самостоятельно выяснять в процессе работы.
Сообщение, которое приходит от трекера в драйвер, преобразуется в понятный нашей платформе пакет и дальше уходит в систему другим сервисам.
На прикладном уровне мы работаем с двумя видами сообщений: бинарными и текстовыми. Бинарные – самокат передает данные в виде набора единиц и нулей. Это самый удобный протокол с точки зрения экономии трафика. Но самый неудобный для отладки мониторинга и наблюдения, поскольку человеческим глазом читать его очень сложно. Текстовые – самокат передает нам сообщения в виде текста.
У разных самокатов свой способ ограничения скорости. Одни понимают установку скоростью просто числом, например {speed:20}, другие – только режимы, которые надо настроить на разную скорость (15 км/ч, 20 км/ч, 25 км/ч) и затем командой посылать сигнал «включи режим 1». Наш зоопарк приходится подстраивать под такое нюансы, потому что каждый самокат обладает индивидуальными особенностями.
Все самокаты общаются по протоколу TCP/IP (транспортный уровень), но часть трекеров над этим транспортным уровнем имеют прикладной уровень MQTT – это широко распространный в IoT-устройствах протокол. Но большая часть нашего парка не имеют этот слой в протоколе.
При TCP/IP соединении самокат и каждый наш сервис устанавливают «точка-точка» соединение, и каждый сервис имеет N-подключений к самокату. В теории, это должно выглядеть так: если у нас 10 тысяч самокатов и запущены 3 сервиса, то каждый сервис принимает 3333 подключения. Комфортно, когда на один сервис приходится примерно 1-1,5 тыс самокатов. Чем больше самокатов, тем больше нужно запустить одинаковых сервисов, чтобы они могли выдерживать нагрузку – горизонтально масштабировать наш сервис.
В 2019 году наш парк насчитывал 3-4 тыс самокатов, поэтому нагрузка была небольшая. Мы использовали всего по одному виду сервиса и работали в основном с трекерами на TCP/IP протоколе. Когда мы начали увеличивать свой парк, и нагрузка стала расти, мы стали переходить в облачные серверы и использовать Kubernetes для масштабирования инфраструктуры в высокий сезон и распределения ее в моменты высокой нагрузки. Но, как оказалось, у этого «коробочного» решения есть и минусы: различные лимиты, которые мы выясняли по ходу, различные ограничения. Также использование Kubernetes требовало разрабатывать stateless сервисы, тогда как драйверы являются stateful сервисами, так как постоянно держат соединение с самокатами.
При масштабировании обнаружилось еще несколько подводных камней, связанных с работой TCP/IP протокола:
Неравномерное распределение подключений.
Мы обнаружили, что самокаты подключаются к сервисам неравномерно. Например, у нас есть 10 тыс самокатов и 10 сервисов, но подключить к каждому сервису по 1000 самокатов автоматически не получится, на каком-то будет 1500, на каком-то 2000, на каком-то – 500. Балансировка работает следующим образом: как только появляется новое подключение, оно ищет следующий по порядку сервис, и если у нас в этот момент какой-то сервис подвис или не отвечает, то самокат пойдет в следующий сервис. Если GSM-сигнал у сим-карты хороший, то соединение будет держаться, пока его кто-то не оборвет.
Такое неравномерное распределение приводило к тому, что часть сервисов отдыхает, а часть сервисов перегружены и начинают медленнее отвечать на сообщения. Пользователи испытывают из-за этого проблемы: самокат не включается, слишком долгое ожидание ответа самоката в приложении и так далее.
Любая выкатка новых версий обрывает все соединения самокатов.
Мы постоянно дорабатываем сервисы, делаем улучшения и пишем новый функционал. И чтобы выпустить обновления для самокатов, нам нужно выключить старые сервисы и запустить новые. В момент, когда мы выключаем старые сервисы, мы обрываем все соединения с самокатами. И получается, что любая выкатка приводила к обрыву соединений, а соединения восстанавливаются в зависимости от самоката по-разному – какой-то минуту, какой-то 5 минут. Соответственно, мы теряли это время, и пользователь не мог разблокировать самокат.
Как решались болезни роста
В 2020 году мы начали запускаться в Москве сразу с несколькими тысячами самокатов, и терять связь было больно. Две проблемы с TCP/IP протоколом стали проявляться все чаще. Поэтому мы решили доработать архитектуру, переведя все драйверы с чистого TCP/IP на MQTT.
Как мы уже ранее писали, у нас изначально были разные конфигурации трекеров и самокатов, и соответственно протоколов, по которым они общались. И далеко не все самокаты умеют общаться по MQTT, поэтому мы написали маленькие программы – адаптеры, которые переводят TCP/IP в MQTT. Они просто держат соединения с самокатом, получают от него сообщения и кладут эти сообщения в MQTT-брокер. Эти сервисы не перезапускаются и не обновляются за очень редким исключением.
Когда к брокеру подключено 10 тысяч самокатов, а с другой стороны подключено 10 сервисов, то брокер передает сообщения из очереди последовательно. Это распределение по очереди называется round-robin. Здесь мы можем добавить новый 11-ый сервис, и нагрузка на него перераспределится автоматически. Например, на каждый сервис приходится 10% нагрузки, если запускаешь 11 сервис, у каждого нагрузка уменьшается равномерно и условно получается по 9%.
Проблема с отключением при раскатке обновлений драйверов решилась следующим образом. Если мы начинали обновлять сервисы, сообщения просто копились в очереди MQTT-брокера короткий промежуток времени, затем запускалась новая версия драйвера, и очередь начинала разбираться. Эта схема позволяла самокатам не терять соединение с сервером.
Когда мы научились TCP/IP переводить на MQTT протокол, мы решили две основные проблемы масштабирования. Но появилась еще одна сложность. Дело в том, что очередь в MQTT-брокере должна быть именная – то есть на каждый самокат должна автоматически создаваться своя очередь с идентификатором трекера. Так как адаптер создает очередь и читает из нее сообщения, он должен знать номер трекера, его нужно выделить из пакета передаваемых данных. В результате нам пришлось писать адаптеры под каждый вид трекера.
Конечно, мы понимаем, что было бы удобнее работать с меньшим количество типов трекеров, и стратегически мы идем к тому, чтобы сокращать разновидности самокатов и как-то сегментировать парк. Но мы все равно не придем к одной модели трекеров, как наши конкуренты. А еще разработка описанного выше архитектурного решения позволила нам адаптироваться под жизнь в облаках с оркестрацией под Kubernetes даже с существющим «зоопарком» трекеров.
Пишите в комментах, если вам зашла статья или был похожий опыт работы с «железом». Буду рад углубиться в какие-то детали, если будут вопросы!