Фёдор Борщёв

Сервис уведомлений и веб-сокетов для строительной компании

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

Современная стройка

Все слышали анекдоты, а многие и лично сталкивались с тем, насколько неэффективными бывают стройки, сколько там можно украсть да и просто сглупить. При этом экономия даже единиц процентов на крупной стройке — это миллионы рублей. Это — идеальная среда для инноваций. Крупные строительные компании активно вкладываются в разработку. Строители пытаются перенести в цифру весь процесс, начиная от архитектурного и инженерного плана здания (BIM, Building information modeling) до того, сколько материалов и когда нужно подвезти на площадку и контроля над тем, насколько точно выдержаны стандарты в процессе работ. У нас был целый эпизод подкаста про это с техническим директором группы компаний ПИК. 

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

Наша задача

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

  1. сообщения дошли до адресата — по электронной почте, в телеграм, да хоть смской, но дошли — пропущенные правки в тендер или согласование проекта могут стоить миллионы в сутки;
  2. не забомбить пользователей лишними сообщениями — система должна уметь собирать не критичные уведомления в «дайджесты» и присылать их с настраиваемой периодичностью;
  3. дать возможность посмотреть «входящие» в едином интерфейсе;
  4. возможность управления системой без программирования — например, поменять шаблоны, поменять частоту дайджестов и так далее.

Проработка архитектуры. Интеграция с существующей системой.

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

Обмен информацией между модулями происходит синхронно через REST API и асинхронно через брокер сообщений (RabbitMQ).

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

Выглядел он так:

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

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

Спроектированный сервис представляет собой Django монолит (один репозиторий с кодом), который контейнеризуется и деплоится 4 разными способами:

  • Django Web App - реплицируемый uWSGI сервер приложений за балансировщиком нагрузки для предоставления доступа к административному веб-интерфейсу и REST API
  • Celery App - реплицируемый воркер для асинхронных задач
  • Celery Beat App - единственный инстанс планировщика асинхронных задач
  • Event Consumer - единственный инстанс консьюмера событий для нотификаций

Отдельным микро-сервисом реализован Websocket сервер - инстанс для мгновенного оповещения пользователя в браузер.

Разработка сервиса нотификаций

Наш Django-монолит состоит из консьюмера и двух воркеров: воркера-процессора и воркера-отправителя уведомлений.

Получение события

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

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

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

Процессинг нотификаций

Задача второго этапа - превратить “сырое” событие в объект нотификации и подготовленных сообщений (доставок) по каналам. Нотификации могут быть двух видов: прямые нотификации и подписки.

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

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

Общий подход такой, что процессор получает на вход событие и выполняет следующие действия:

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

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

В чем опасность? Например, при перегруженной очереди (красным на схеме) и частым повтором тасок (event1 -> repeat task for event1) может получиться так, что процессинг выполнится несколько раз. При неудачном сценарии можно отправить несколько десятков одинаковых уведомлений на одно событие.

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

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

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

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

Процессинг подписок на события

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

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

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

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

Приятная особенность в том, что при процессинге мы обогащаем данные через отдельный микро-сервис о каждом пользователе, компании или роли из события. Поэтому, например, при подписке на «роль = проектировщик», мы успешно рассылаем все события с Дмитрием Ивановым (проектировщиком), и не посылаем события с Василием Петровым (монтажником).

Существенных нюансов при реализации добавляют два требования:

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

Слева направо в Статусе Доставки показано как вычисляются статусы для отправки - SKIPPED (пропускает отправку) и PENDING (ожидает отправку).

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

Доставка уведомлений

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

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

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

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

Единственное исключение по каналам доставки — отправка уведомлений в веб-браузеры.

Отправка в браузер. Сервис Web-сокетов.

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

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

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

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

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

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

Общая схема сервиса

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

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

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

Сервис написали на асинхронном python. Для работы с веб-сокетами используем библиотеку websockets. Чтоб получать события из RabbitMQ — aio-pika. Код — простой и лёгкий: один инстанс сервиса может обслуживать сотни и даже тысячи одновременных подключений — для наших требований достаточно.

Убедились на тестах, что сервис справится с нагрузкой: написали скрипт, который имитирует действия пользователей — подключается, подписывается, а затем получает уведомления. Проверили на 4 тысячах подключений: всё в порядке — не задыхается у успевает обрабатывать наш поток сообщений. Запустили на самом нагруженном окружении с более чем 5000 пользователями и сервис даже не напрягся. 🚀

Периодическая рассылка новостей - дайджест

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

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

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

Проверки прав на объекты (контроль доступа)

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

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

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

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

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

Мониторинг

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

Для технического мониторинга, показатели CPU, RAM, количество тасок — выведены в Graphana.

В среднем нагрузка на процессор — несколько процентов CPU, в пиках доходит до 10% CPU.

Потребление памяти - 1,8 Гбайт.

Потребление сервиса веб-сокетов совсем мизерное:

CPU:

RAM:

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

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

Выводы

Кто думает об уведомлениях веб-сервисов как об источнике технической сложности? Дьявол, как всегда, в деталях (требований).

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

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

Над проектом работали:

Архитектор — Алексей Чудин

Бекенд, соавторы этой статьи — Вячеслав Набатчиков и Николай Кирьянов

Фронтенд — Иван Седов

Менеджер — Анастасия Шаркова

Как мы перезапускали Медицинскую информационную систему

Клиники «Чайка» — это сеть премиальных клиник с отличными врачами, куда можно обратиться с любым вопросом — от сломанной ноги до зубной боли. Для организации работы, они используют собственную МИС (медицинскую информационную систему), которую пилит небольшая команда разработчиков в штате.

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

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

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

Задача и генеральная идея

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

До этого проекта, у нас был опыт «технологической трансформации» команд разработки и тогда мы зареклись делать «консалтинг» — это работа, в которой нужно 100% вовлечение технического директора и её сложно делегировать — такой бизнес мы с Федей масштабировать не умеем, а повторять одно и то же, чтобы просто заработать денег — скучно.

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

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

Всё это осложнялось тем, что медицина — самая сложная доменная область, с которой мы встречались. Каждая, даже самая простая задача в ней, на проверку оказывается верхушкой гигантского айсберга. Взять хранение диагнозов. Казалось бы — одна таблица для пациентов, вторая для диагнозов, выбранных из стандартного справочника, третья для их связи. Но нет. Первичный ли это диагноз? Он уже подтверждён, или это промежуточный результат диффиренцированного диагноза? Его поставил один доктор или коллегия? А может это результат автоматизированного измерения? Актуален ли диагноз сейчас, или это запись из анамнеза? По какому справочнику этот диагноз — МКБ или SNOMED? А если диагноз «перелом», то он где — на руке или ноге? Правой или левой? А куда записать информацию об аллергии?

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

Архитектура и планы

Текущую разработку замораживать нельзя, так что новый код для бизнес-фич надо писать сбоку. Учитывая, что бекенд написан на чужом для нас стеке (TypeScript), единственное решение — это event-driven архитектура. Пусть текущая МИС будет одной из многих систем в гетерогенной среде, где программы, обслуживающие разные части бизнеса, общаются друг с другом через события. Скажем, сотрудник ресепшн завёл карточку пациента в привычном интерфейсе — система сформировала событие «пациент заведён», обогатила его всеми данными пациента, и положила это в общее информационное пространство — брокер событий. Все другие программы, которым нужна информация о пациентах, слушают, поток событий и заводят пациента у себя. Точно так же передаются данные о новых докторах, диагнозах, операциях, взаиморасчётах и всех других изменениях в системе.

Чтобы не изобретать велосипед, мы построили свою модель данных на основе готового стандарта — FHIR. Кроме прямой пользы — держать данные в едином стандарте, FHIR помогал нам погружаться в доменную область — изучая описание стандарта, легко узнать, что диагнозы надо привязывать к частям тела (диагноз «кариес», часть тела «22 зуб со стороны языка»).

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

AstraShare

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

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

Сервис мы в итоге запустили за 5 месяцев силами команды из трёх программистов и одного архитектора. Чтобы упростить задачу команде клиента, мы пожертвовали проработкой событийной модели: вместо десятка «удобных» событий «заведён пользователь», «поставлен диагноз», «сделано наблюдение» сделали одно большое событие «медицинская запись изменена», данные из которого уже разбирали в нашей системе. Но даже такое гигантское событие доставлялось плохо — примерно в 0.5% случаев событие не генерировалось. Чтобы понять, насколько это много, представьте, что из вашей карточки в стоматологии с такой вероятностью пропадёт информация о вашей аллергии на обезболивающее.

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

Итак, мы убедились, что архитектура работает. Время браться за более сложные задачи! У бизнеса было три основных запроса: улучшение системы биллинга, мета-лаборатория и новый интерфейс стоматологов. Рассказ о биллинге здесь опустим — это довольно скучная (хотя и безумно сложная для новичков) штука, которую мы успешно делали уже много раз. А вот о лабораториях и интерфейсе стоматологов расскажем подробнее.

AstraLab — мета-лаборатория

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

Любая интеграция такого рода — это сложная задача: другие системы делали программисты со своим представлением о доменной области, о структурах данных и о том, что такое надёжность передачи информации. Одна лаборатория поддерживает FHIR, но с каким-то другим справочником, другая — отдаёт результаты в виде XML-файлов, которые забирает с сервера специальная софтина на винде, у третьей — свой собственный REST API с особым способом аутентификации.

К тому же сразу после запуска сложных интеграций нужно закладывать период в 1–2 недели, когда всё работает нестабильно, а программисты почти в реальном времени правят мелкие несоответствия в форматах данных или протоколах. Это довольно сложно делать на старом монолите из-за длинного релизного цикла: внести одно изменение на проде почти нереально, нужно подсаживать его в «релизный поезд» — пачку изменений, которая уходит на ручное тестирование и будет выпущена только после того, как все фичи из неё будут проверены. В таком режиме даже маленькие исправления могут ждать света дня неделю.

В новой архитектуре у нас получилось реализовать этот модуль отдельным сервисом силами 4 программистов за 6 месяцев.

AstraDental

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

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

Дело в том, что врачи вынуждены заполнять карточки примерно так же, как они хранятся в базе данных: выбрать пациента, указать диагноз, записать текстом наблюдения. То есть удобно для программистов и неудобно для врачей. При этом уже в 60е годы был разработан эффективный формат клинических заметок — SOAP, в котором записи делятся на 4 блока: Subjective (Субъективный), Objective (Объективный), Assessment (Оценка) и Plan (План). Новый интерфейс использует эту же схему, ускоряя заполнение информации, где это возможно.

Внедрить такую систему «на живую» очень сложно: медицинские карточки — это сердце МИС. Чтобы не переписывать всю работающую систему без необходимости, мы выбрали небольшой, и по совместительству самый интересный кусок работы — стоматологов.

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

Дизайнеры Астры выстроили удобный интерфейс для докторов на основе зубной формулы:

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

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

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

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

Финал

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

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

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

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

Вывод

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

Если вам нужна подобная помощь или просто классная разработка — пишите Самату Галимову @samatg.

Технические директора:
Фёдор Борщёв
Самат Галимов

Архитектор:
Антон Давыдов

Бекенд-программисты:
Вячеслав Набатчиков
Денис Сурков
Алексей Чудин
Эдуард Степанов
Николай Кирьянов
Даниил Мальцев
Владимир Войтенко
Анна Агаренко

Фронтенд-программисты:
Александр Нестеров
Алексей Богословский
Владимир Тарановский
Тимур Брачков
Михаил Бурмистров

Менеджеры:
Анастасия Шаркова
Ксения Сафронова
Иван Борисов
Дарья Львова

Сотрудники заказчика:
Алена Салкова
Александр Скоромнюк
Александр Климов
Валентин Раншаков
Виктор Косарев
Вячеслав Ключников
Денис Турьяница
Дмитрий Верижников
Дмитрий Зозулин
Дмитрий Афанасьев
Дмитрий Панов
Мария Чистова
Павел Власенко

Облачная платформа для Wiren Board

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

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

Задача

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

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

Мы решили начать с интерфейса, где пользователи смогут посмотреть все принадлежащие им контроллеры — чтобы можно было проверить показания датчиков и бизнес-метрик, а в случае проблем — подключиться к устройствам по SSH; напомню, наши пользователи — это сисадмины крупных компаний. Забегая вперёд, добавлю, что инструмент оказался полезным не только сисадминам, но и IoT-энтузиастам, которые держат 1– 2 контроллера у себя дома.

Клиент, как всегда, хотел запуститься как можно скорее. Для этого мы максимально использовали готовые opensource-решения.  IoT — уже довольно развитая область и это позволило нам запуститься в рекордные 3 месяца, а ещё не тратить время и деньги на изобретение и поддержку своих собственных велосипедов.

Метрики

Для сбора данных с IoT устройств есть целый ворох протоколов и решений. Мы остановились на связке Telegraf и InfluxDB

Telegraf — это агент для сбора данных, который живёт на устройстве, собирает несчётное количество параметров системы и передаёт их к нам в облако. В итоговом решении мы получаем через него стандартный набор системных метрик вроде состояния сервисов,нагрузки на CPU и количества свободной памяти, а также данные с MQTT-шины, на которой работает системное ПО Wiren Board.

В мире IoT очень важен размер программы. Telegraf — модульная система, так что мы выключили лишнюю функциональность при компиляции и собрали 15Мб образ вместо 211Мб.

Для бэкенда облака мы выбрали базу временных рядов InfluxDB. База временных рядов — это база данных, которая хранит изменяемые во времени значения. Если привычный PostgreSQL работает со статичными таблицами, как в справочнике, то InfluxDB хранит историю изменений — как в выписке из банка есть остаток на счёте после каждой операции. Такой формат позволяет делать быстрые выборки вида «как коррелирует время ответа сервиса с загрузкой CPU между 01:30 и 01:35 5 февраля».

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

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

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

Туннели

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

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

Мы любим простые решения, поэтому остановились на frpFast reverse proxy — маленькая программа, которая очень хорошо решает одну конкретную задачу. Клиентская часть frp устанавливается на контроллер и соединяет выбранные локальные порты контроллера с сервером. Для подключения по SSH мы используем слегка доработанный веб-терминал WebSSH, который внутри сети облака соединяется с открытым портом frp через авторизационный прокси. Прокси нужен, чтобы пользователи не могли получить доступ к чужим контроллерам.

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

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

Схема работы frp с нашим агентом:

Итоги и планы

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

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

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

В проекте участвовали:

  • Ведущий разработчик и автор этого текста — Алексей Чудин,
  • фронтендер — Тимур Брачков,
  • бекендер — Николай Кирьянов,
  • руководитель проекта — Настя Шаркова.

Day One → Obsidian

Несколько месяцев назад я решил отказаться от Day One, которым пользовался 5 лет для ведения личного дневника. Причин было несколько:

  • Невозможные тормоза. Дело тут даже не в электроне, или на чём он там написан: кажется Day One — это просто плохой софт. Удивляюсь, откуда у разработчиков берётся энергия, чтобы писать столько кода, чтобы тормозило даже на новом макбуке, где давно уже даже ноушен летает.
  • Фокус на метаданных. Day One с самого начала обвешивает привычку писать дневник всякими ненужными вещами, которые не представляют интереса уже через день — предлагает логать погоду, место и даже музыку, которую я слушаю. Чтобы ненужных данных было ещё больше, Day One ломает паттерн, привычный людям уже несколько сотен лет — вместо обычных ежедневных заметок, вас заставляют писать по одному посту на каждое событие. Добавил фотку — пост, записал мысль — ещё пост.
  • Переусложнённый формат хранения. Возможно следствие предыдущего пункта, но внутри у Day One довольно сложный формат хранения данных. Вроде бы и маркдаун, но он зарыт внутри довольно сложного JSON, который непонятно как парсить. То есть я как бы владею собственными данными, но вот считать их без Day One я скорее всего не смогу. А если и смогу сегодня — никто не гарантирует, что я смогу повторить это через 10 лет, когда формат переживёт ещё 3 инкарнации, а контента в дневнике станет в 4 раза больше.

Для замены Day One я выбрал Obsidian — он показался мне самым надёжным из всех инструментов для ведения заметок. Самое главное преимущество Obsidian перед специализированным софтом — все данные хранятся в виде MD-файлов на диске. То есть я владею своими данными, бекаплю их как хочу, и они никогда и никуда от меня не денутся.

От встроенной синхронизации решил отказаться, потому что iCloud работает быстрее, а на экосистеме Apple я сижу давно и плотно. Так же пришлось почистить слегка перегруженный интерфейс — помогло, что Obsidian проектировали как инструмент для программистов. Довольно легко мне удалось убрать всё лишнее — отключить штатные плагины вроде Canvas или записи аудио, попрятать ненужные детали интерфейса при помощи Hider.

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

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