Busrpc — фреймворк для разработки микросервисов
В этой статье я расскажу о собственном проекте ориентированном на микросервисную архитектуру. Этот проект вырос из идей и подходов, которые я применял на протяжении нескольких лет работы связанных с переводом крупного монолитного проекта на микросервисную архитектуру. Я не буду заострять внимание на паттерны, концепции и базовые принципы МСА, поскольку информации подобного рода достаточно в сети. Моя цель — предоставить читателю конкретный вариант реализации микросервисного бэкенда на основе фреймворка busrpc.
Замечу, что называя busrpc фреймворком, я «слегка» преувеличиваю, так как на данном этапе проект содержит всего два компонента:
спецификацию, определяющую терминологию, структуру и протокол busrpc бэкенда, а также некоторые другие технические документы (репозиторий busrpc-spec)
утилиту, предоставляющую команды для проверки реализаций на соответствие спецификации busrpc, генерации документации и другие
До уровня фреймворка этому проекту категорически не хватает таких очевидных вещей, как:
библиотек для разработки busrpc микросервисов под разные языки программирования
клиентов для отладки и мониторинга busrpc микросервисов
В случае, если busrpc фреймворк покажется вам заслуживающим внимания, и вы захотите использовать его в одном из проектов, вам вероятно потребуется инвестировать в разработку нужных вам инструментов. Если при этом у вас будет возможность и желание законтрибьютить вашу работу, я с радостью вам помогу в этом. Все ссылки дублируются к конце статьи.
Предыстория
Долгое время я работал над бэкендом одного крупного и достаточно старого (15+ лет) проекта. Основу ему закладывали еще в далекие 2000-е, а значит, современные инструменты и принципы разработки ПО были еще либо недоступны, либо недостаточно известны, поэтому долгое время проект развивался как вариация монолитной архитектуры. Несмотря на то, что он представлял собой несколько сервисов, которые разделяли между собой бизнес-логику приложения, это нельзя было назвать микросервисной архитектурой по следующим причинам:
сервисы были тесно связаны друг с другом: каждый содержал в себе информацию о других, от которых он зависел, поддерживал с ними прямое соединение и общался по проприетарному протоколу
сервисы совместно использовали общие БД, причем далеко не всегда только для чтения
Как обычно бывает, ситуация ухудшалась постепенно, и долгое время проблема маскировалась высокой личной компетенцией членов команды, большинство из которых много лет работали над проектом.
Тем не менее, в какой-то момент стало очевидно, что в текущей парадигме проект больше не может стабильно развиваться.
В первую очередь мы не могли варьировать размер команды. Добавление новых людей к проекту не приносило качественного изменения в сроках поставки ПО, поскольку даже у квалифицированных разработчиков уходило несколько месяцев, чтобы войти в проект, а после этого еще столько же, чтобы стать в нем относительно самостоятельными.
С другой стороны, потеря любого «старичка» была большой проблемой. И дело даже не в том, что мы теряли производительность, а в том, что мы теряли знания, которыми этот человек обладал, и в проекте появлялась еще одна сумеречная зона, заходить в которую было достаточно опасно. Конечно, у нас была wiki с документацией и регламент по ее периодической актуализации, но она едва ли покрывала даже половину проекта.
Другой проблемой было то, что разработка новых фич могла неожиданно застопориться из-за того, что фича ломала какой-то сервис, а это требовало исправлений в тех сервисах, которые от него зависели и далее по цепочке. По этой причине мы стали часто и порой грубо ошибаться в планировании сроков релизов, что быстро вылилось в обоснованное недовольство нашего заказчика.
Разумеется, сильно страдала надежность и доступность нашего бэкенда. Падения сервисов участились, а восстановить систему после них становилось все сложнее (а в автоматическом режиме — так и вовсе практически нереально), так как вместе с ростом количества сервисов увеличивалась запутанность их взаимодействий и конфигураций. Например, правильное устранение последствий падения сервиса А могло требовать перезапуска сервиса Б.
Если при падении сервиса мы хотя бы получали дамп, по которому могли в точности определить место падения и быстро пофиксить баг, то ситуация с остальными ошибками, на которые жаловались пользователи, обстояла значительно хуже. По сути, единственным доступным нам механизмом поиска проблем на проде были логи сервисов. Так как сервисы были тесно связаны друг с другом, невозможно было с уверенностью сказать, какая часть системы точно не может являться причиной ошибки, а значит поиски могли сильно затянуться.
В итоге мне поручили начать глобальный рефакторинг проекта, которым я и занимался на протяжении полутора лет. Платформа busrpc — это развитие идей, которые были опробованы тогда.
Мой манифест
В процессе набивания шишек, описанных в предыдущем разделе, я сформулировал для себя несколько принципов, которых старался придерживаться в организации работы бэкенд команды. Вспоминая историю создания Agile, мне захотелось несколько высокопарно назвать это своим манифестом. Я ни в коей мере не претендую на новаторство идей лежащих в основе манифеста, думаю, что все из них можно найти в работах других авторов. Однако, раз уж платформа busrpc создается как инструмент организации работы команды в соответствии с этим манифестом, я приведу его здесь.
Команда должна говорить на одном технологическом языке и использовать одинаковую терминологию для один и тех же сущностей. По возможности, терминология не должна вводить новых понятий, а использовать хорошо известные из смежных областей.
Исходные коды разных разработчиков должны быть максимально изолированы друг от друга, в идеале — содержаться в разных репозиториях. Помимо того, что это максимально комфортный режим работы для программиста, это также делает для него очевидной область его личной ответственности. Важность личной ответственности подчеркивается в Agile, где утверждается, что это поддерживает интерес разработчика к проекту и побуждает его к обучению и улучшению качества своей работы. В свою очередь команде этот принцип упрощает управление своими человеческими ресурсами, не привнося при этом дополнительных рисков. Например, новым разработчикам можно сразу давать писать не самые критичные, но полезные фичи, при этом не сильно заботясь о том, что они по неопытности поломают что-то важное.
Из предыдущего пункта вытекает принцип, который я называю «горизонтально-масштабируемой разработкой». Следуя ему, стоит делать новые фичи как отдельные проекты, а не реализовывать их в рамках некоторого существующего (то, что можно было бы назвать «вертикально-масштабируемой разработкой»). Конечно, порой добавить фичу к существующему проекту может быть значительно проще, надежнее и быстрее, поэтому этот принцип не стоит возводить в абсолют.
Проверять соблюдение технических требований бэкенда должны специализированные инструменты в рамках CI/CD пайплайна, а не человек.
Лучшая и самая достоверная документация — это исходный код. Зачастую, никакой иной документации не нужно.
Бэкенд должен быть прозрачным в том смысле, что человек (разработчик, QA или DevOps инженер, системный администратор) должен иметь возможность посмотреть траффик между его компонентами. Это неявно подразумевает возможность фильтрации сообщений и приведения их к текстовому виду, а иначе прозрачность будет номинальная: вряд ли возможность перехватить все сообщения в бинарном виде окажется востребованной, так как мало отличается от прямого анализа логов.
Должен существовать единый центр притяжения для всей информации по бэкенду (API, документация, конфигурация сервисов и т.д), в котором любой член команды (будь то разработчик, QA инженер, системный администратор или сотрудник саппорта) должен иметь возможность найти нужную для его работы информацию.
Основы фреймворка busrpc
Как следует из названия, busrpc представляет собой один из вариантов технологии удаленного вызова процедур для организации взаимодействия между компонентами бэкенда. Кроме того, в качестве транспортного уровня используется шина (bus) сообщений.
Первоначально я хотел назвать свой проект mqrpc, так как термин «очередь сообщений» (message queue) более распространен, чем «шина сообщений» (message bus), однако такой проект уже существует на гитхабе. Разница же между этими понятиями весьма тонкая, и показалась мне несущественной. В статье я считаю эти термины синонимами.
По сравнению с типовыми реализациями RPC (например, gRPC), использование шины сообщения как промежуточного слоя между сервисами дает следующие преимущества:
способствует поддержанию слабой связности между сервисами (не нужно знать адреса других сервисов или поддерживать сложный discovery механизм)
сильно упрощает конфигурирование сервисов, так как им абсолютно необходимо знать лишь адрес шины сообщений
упрощает управление сервисами и их масштабирование
дает возможность мониторить работу бэкенда (см. пункт 6 манифеста)
позволяет использовать другие популярные паттерны взаимодействия из МСА (например, publish/subscribe)
Разумеется, есть и минусы, из которых на ум сразу приходят два:
Тем не менее, в виду того, что сейчас существует большое количество шин сообщений с разными свойствами, как правило можно подобрать какой-то вариант, в котором эти минусы будут нивелированы.
Модель шины сообщений
Мне не хотелось диктовать потенциальным пользователям платформы busrpc какую именно шину сообщений им использовать, поэтому в спецификации busrpc вместо какой-то конкретной технологии используется абстрактная модель. Любая конкретная технология очереди сообщений, которая удовлетворяет этой модели, может использоваться как транспорт busrpc.
Модель описывает две операции, предоставляемые шиной сообщений:
PUBLISH(topic, message, [replyTo])
для отправки сообщения в топикSUBSCRIBE(topic)
для получения сообщений из топика
Топик это последовательность слов, разделенных некоторым специальным символом (во всех известных мне очередях сообщений это .
). Этот термин позаимствован из Kafka, в других проектах ему может соответствовать другое понятие (например, routing key в RabbitMQ, или subject в NATS).
Слова, составляющие топик образуют произвольную иерархию, например, time.us
, time.us.east
, time.us.east.atlanta
и т.д. Модель шины сообщений определяет два специальных символа, которые могут использоваться в качестве слова топика в операции SUBSCRIBE
:
соответствует одному любому слову
соответствует 1-или-N или 0-или-N словам (для спецификации не важно, какая именно нижняя граница используется)
Основным механизмом обмена сообщениями в абстрактной модели является publish-subscribe. Необходимая для RPC модель request-reply обеспечивается с помощью поддержки параметра replyTo
в операции PUBLISH
, который сообщает обработчику топик, на который он должен отправить ответ.
В заключение этого раздела скажу, что в своей работе по переводу монолитной архитектуры в микросервисную я в итоге остановил выбор на NATS. Его неоспоримым преимуществом является минимальная latency для наиболее характерного для RPC размера сообщения по сравнению с другими популярными очередями.
Могу сказать, что NATS стабильно развивается, а его разработчики внимательно относятся к запросам новых фич от своих пользователей, по крайней мере то, что мне очень хотелось видеть, было быстро добавлено. По непонятным для меня причинам, на хабре я не смог найти полноценных статей о нем (в отличие от RabbitMQ, о котором по-моему уже все написали), хотя технология явно заслуживает внимания.
Протокол
В качестве бинарного протокола для платформы busrpc был выбран protobuf. Помимо того, что это просто удобный инструмент с хорошей поддержкой, знакомый большинству разработчиков, protobuf предоставляет возможности, отсутствующие у аналогов (например, Apache Thrift):
механизм кастомных опций, с помощью которого в язык protobuf были добавлены некоторые концепции платформы busrpc
поддержка интроспекции для генерируемых типов, на основе которой можно разрабатывать широкий спектр утилит и библиотек для платформы busrpc
Кроме того (правда это не является какой-то уникальной фичей), protobuf дает возможность легко конвертировать данные из бинарного формата в текстовый и обратно. Первый дает минимальный размер сообщения и уменьшает задержку, вносимую очередью сообщений, а второй позволяет анализировать сообщения человеку и нужен для поддержания концепции «прозрачного» бэкенда (см пункт 6 манифеста).
Дизайн и терминология
Важным элементом дизайна любого фреймворка является терминология. Интуитивно понятная терминология упрощает понимание и использование фреймворка, а также способствует коммуникации между разработчиками, в то время как запутанная и непоследовательная не только вызывает раздражение, но и служит дополнительным источником разного рода ошибок.
Фреймворк busrpc строится на концепциях из области объектно-ориентированного программирования. Это позволяет на верхнем уровне представлять API busrpc-бэкенда как API какой-нибудь библиотеки, написанной в парадигме ООП, а значит разработчик любого уровня быстро сможет войти в курс дела в таком бэкенд-проекте. Кроме того, в МСА достаточно сложным и творческим процессом является декомпозиция бизнес-логики на микросервисы, и я нахожу возможность взглянуть на эту задачу с точки зрения ООП достаточно полезной и способствующей принятию лучших решений.
Класс
Центральной концепцией busrpc является класс, который моделирует аналогичное понятие из области ООП, а именно представляет собой набор схожим образом устроенных сущностей, обладающих одинаковым поведением, выражаемом в виде методов класса. Совокупность всех методов класса называется его интерфейсом.
Объектом класса, как и в ООП, называется конкретная сущность множества, моделируемого классом. Каждый объект характеризуется некоторым уникальный неизменяемым идентификатором объекта.
Метод может быть связан с конкретным объектом или с классом в целом. В последнем случае метод называется статическим. Если класс не имеет объектов, он тоже называется статическим. Статический класс может содержать только статические методы.
Вызов метода представляет собой сетевое сообщение, содержащее в себе параметры метода и (в случае нестатического метода) идентификатор объекта, для которого он вызывается.
Результатом метода также является сетевое сообщение, которое содержит либо возвращаемое значение метода, либо исключение, представляемое специальным встроенным типом данных. Метод может быть объявлен как не возвращающий никакого результата (аналог void
из некоторых языков программирования). Такие методы называются oneway (мне не очень нравятся варианты перевода этого термина, поэтому решил оставить на английском).
Сервис
Сервис это любое приложение, которое использует некоторое ненулевое количество методов busrpc-классов. Под использованием метода подразумевается его:
реализация — в этом случае сервис использует операцию
SUBSCRIBE
для получения вызова метода и операциюPUBLISH
для отправки результата метода (для oneway-метода, операцияPUBLISH
, очевидно, не задействуется)вызов — в этом случае сервис использует операцию
PUBLISH
для отправки вызова метода
Отношение между сервисами и методами представляет собой N-M, то есть один сервис может использовать произвольное количество методов, а один метод может быть использован несколькими сервисами (в том числе, и реализован несколькими сервисами)
Структуры и перечисления
Busrpc структурами и перечислениями называются соответственно типы данных message
и enum
из protobuf.
Некоторые структуры имеют дополнительную семантику в спецификации busrpc и называются предопределенными (predefined). К ним относится особый класс структур, называемых дескрипторами, которые используются для описания высокоуровневых концепций (классов, методов, сервисов и других).
Кодируемым (encodable) protobuf типом называется не-repeated
тип данных, представляющий собой одно из следующих:
скалярный protobuf тип, за исключением
float
иdouble
protobuf
enum
protobuf
message
, не содержащий полей, или содержащий только поля одного из предыдущих типов, не объединенные каким-либоoneof
Примеры кодируемых и не кодируемых busrpc структур:
enum MyEnum {
MYENUM_VALUE_0 = 0;
MYENUM_VALUE_1 = 1;
}
// пустая структура является кодируемой
message Encodable1 { }
// кодируемая структура, т.к. она:
// 1 - состоит только из полей скалярного типа и типа перечисления
// 2 - optional не запрещен явно в определении
message Encodable2 {
optional int32 f1 = 1;
string f2 = 2;
bytes f3 = 3;
MyEnum f4 = 4;
}
// некодируемая структура
// каждое поле не позволяет рассматривать ее как кодируемую
message NotEncodable {
float f1 = 1;
repeated int32 f2 = 2;
oneof MyOneof {
int32 f3 = 3;
string v4 = 4;
}
map f5 = 5;
Encodable1 f6 = 6;
}
Пространство имен
Пространство имен — это группа связанных каким-либо образом классов, структур и перечислений. Пространства имен в busrpc используется для логического разбиения API и изолирования частей.
Исключения
Как упоминалось ранее, результатом метода может быть либо обычное значение, либо исключение, которое представляет собой экземпляр специальной структуры Exception
, которая подробнее будет рассмотрена позднее.
Под выбрасыванием исключение понимается возвращение в качестве результата метода экземпляра Exception
. Перехватом исключения называется обработка ситуации, когда результатом метода является исключение.
Спецификация busrpc требует, чтобы исключения, которые вызывающий сервис не может обработать, отправлялись дальше вверх по цепочке вызовов. Этот механизм в обычных языках программирования называется exception propagation.
Например, предположим, что сервис S0 вызывает метод M1, который реализуется сервисом S1. В рамках обработки этого вызова, сервис S1 вызывает метод M2, реализуемый сервисом S2. Если M2 столкнется с каким-то проблемами, он может выбросить исключение E, которое S2 возвращает на S1 в качестве результата. В свою очередь, M1 может перехватить E и каким-то образом его обработать. Если этого не происходит, то сервис S1 обязан вернуть сервису S0 то же самое исключение E в качестве результата метода M1.
Исключения busrpc могут преобразовываться клиентскими библиотеками в исключения целевого языка программирования. Это нужно учитывать, потому что все механизмы исключений в ЯП имеют свою цену. Например, могут значительно снижать производительность, когда количество выбрасываемых исключений становится слишком велико. Чтобы избежать возможных проблем, нужно помнить, что в busrpc исключения нужны для того, чтобы сообщать об исключительных ситуациях, которые происходят достаточно редко (иначе какие же они исключительные), обычно свидетельствуют о серьезном техническом сбое и как правило имеют тривиальную обработку (вывод в лог, установка алерта и т.д.).
Конечная точка
Конечная точка вызова (call endpoint) — это топик, в который отправляются вызовы метода (указывается в параметре topic
операции PUBLISH
). Конечная точка представляет из себя последовательность
, где:
,
и
являются именем пространства имен, класса и метода соответственно
содержит идентификатор объекта, для которого вызывается метод, или специальное слово
, если вызываемый метод является статическим
представляет собой последовательность слов, каждое из которых содержит значение наблюдаемого параметра метода; наблюдаемые параметры метода идентичны обычным, за исключением того, что они должны иметь кодируемый тип и дополнительно являются частью конечной точки метода (подробнее о них мы поговорим позднее)
— это специальное слово, служащее индикатором конца последовательности наблюдаемых параметров
Конечная точка результата (result endpoint) — это топик, на котором вызывающий ожидает результат метода (указывается в параметре replyTo
операции PUBLISH
). Эта конечная точка имеет формат
, где:
содержит некоторую последовательность слов, точный формат которой определяется в зависимости от используемой шины сообщений; в общем случае префикс содержит информацию, необходимую для демультиплексирования результатов метода и определения, к какому вызову они относятся
содержит конечную точку, использованную для вызова метода
Некоторые популярные виды конечных точек получили отдельные названия:
конечная точка пространства имен (namespace endpoint)
— вызовы всех методов всех классов из пространства имен. конечная точка класса (class endpoint)
— вызовы всех методов класса. . конечная точка метода (method endpoint)
— все вызовы метода. . . конечная точка объекта (object endpoint)
— вызовы всех методов над конкретным объектом. . . . конечная точка значения (value endpoint)
— все вызовы метода с определенными значениями некоторых наблюдаемых параметров. . . . .
Все очереди сообщений запрещают какое-то подмножество символов для использования в своих топиках. Очевидно, что некоторые компоненты конечной точки busrpc могут содержать недопустимые для топика символы, поэтому они дополнительно должны быть закодированы. Алгоритм кодирования компонентов конечной точки не будет рассматриваться в этой статье. Интересующийся читатель может найти его в спецификации.
Область видимости структуры и перечисления
Областью видимости структуры/перечисления является та часть API, в которой эта структура/перечисление может использоваться.
Бэкенд построенный на платформе busrpc может содержать следующие области видимости:
единственную глобальную область видимости, содержащую типы, которые доступны везде
единственную область видимости API, содержащую типы, доступные в любой части API бэкенда
область видимости пространства имен, содержащую типы, доступные только внутри пространства имен
область видимости класса, содержащую типы, доступные только методам класса
область видимости метода, содержащую типы, доступные только в рамках конкретного метода
единственную область видимости реализации, содержащую внутренние типы, используемые сервисами для реализации публичного API
область видимости сервиса, содержащую внутренние типы конкретного сервиса
Области видимости образуют иерархию вложенности таким образом, что типы из более широкой области видимости видны в более узкой, но не наоборот.
Как и в традиционном ООП, рекомендуется помещать busrpc структуры и перечисления в наиболее узкую возможную область видимости. Это позволяет проще контролировать изменения. Например, при изменения какого-то типа из области видимости класса нужно проверить на совместимость только методы этого класса, потому что известно, что в других местах тип не мог быть использован.
Busrpc проект
Фреймворк busrpc определяет следующую структуру директорий busrpc проекта:
/
├── busrpc.proto
├── api/
│ ├── /
| ├── namespace.proto
│ ├── /
│ ├── class.proto
│ ├── /
│ ├── method.proto
├── implementation/
│ ├── /
| ├── service.proto
Компонентами этой структуры являются:
корневая директория busrpc проекта
, содержащая файл busrpc.proto, в котором определены некоторые встроенные типы и кастомные protobuf опции платформыдиректория api/, содержащая API бэкенда
отдельная директория
/ для каждого пространства имен, содержащая поддиректории классов, входящих в пространство имен и файл дескриптора пространства имен namespace.protoотдельная директория
/ для каждого класса, содержащая поддиректории методов класса и файл дескриптора класса class.protoотдельная директория
/ для каждого метода, содержащая файл дескриптора метода method.protoдиректория
/ , являющаяся корнем для непубличных типов и поддиректорий сервисовдиректория
/ , содержащая файл дескриптора сервиса service.proto
Все директории помимо обязательных файлов, указанных выше, могут содержать и другие proto файлы. При этом, каждая директория естественным образом отображается на одну из областей видимости, рассмотренных ранее. Таким образом, расположение proto файла определяет область видимости всех типов, определенных в нем: типы видимы только в той же директории и ее дочерних директориях.
Имена protobuf пакетов
Структура директорий busrpc проекта определяет имена protobuf пакетов (указываются в proto файле с помощью package
) следующим образом:
имя пакета, используемое в файлах из корневой директории проекта должно быть
busrpc
в остальных файлах имя пакета должно состоять из имен директорий, составляющих относительный путь к файлу (например, содержимое файла api/my_namespace/file.proto должно находиться в пакете
busrpc.api.my_namespace
Файл дескриптора пространства имен
Этот файл должен содержать дескриптор пространства имен, который представляет собой предопределенную структуру с именем NamespaceDesc
. На данный момент, спецификация не определяет для этой структуры никакого особого формата, однако она обязательно должна присутствовать:
// file api/chat/namespace.proto
message NamespaceDesc { }
Здесь и далее я буду ссылаться на пример busrpc проекта из репозитория. Пример представляет собой бэкенд простого IM приложения.
Файл дескриптора класса
Этот файл должен содержать дескриптор класса, который представляет собой предопределенную структуру с именем ClassDesc
. Дескриптор класса может содержать вложенные структуры, рассматриваемые в следующих подразделах.
ObjectId
Структура ObjectId
определяет идентификатор объекта класса. Поскольку идентификатор объекта используется в конечных точках, структура ObjectId
должна быть кодируемой.
// class 'user'
// file api/chat/user/class.proto
message ClassDesc {
message ObjectId {
string username = 1;
}
}
Если дескриптор класса не содержит ObjectId
, то класс считается статическим.
Файл дескриптора метода
Этот файл должен содержать дескриптор метода, который представляет собой предопределенную структуру с именем MethodDesc
. Дескриптор метода может содержать вложенные структуры, рассматриваемые в следующих подразделах.
Params и Retval
Предопределенные структуры Params
и Retval
определяют параметры метода и его возвращаемое значение. Если в дескрипторе отсутствует структура Retval
, то метод является oneway. Также спецификация busrpc разрешает не определять структуру Params
— в этом случае метод рассматривается как не имеющий параметров.
// method user::sign_in
// file api/chat/user/sign_in/method.proto
enum Result {
RESULT_SUCCESS = 0;
RESULT_INVALID_PASSWORD = 1;
}
message MethodDesc {
message Params {
string password = 1;
}
message Retval {
Result result = 1;
}
}
Обратите внимание, что в примере выше тип Result
является частью области видимости метода (поскольку определен внутри файла, находящегося в директории метода), а значит не может быть использован нигде, кроме нее.
С помощью кастомной булевой опции observable
поля структуры Params
, которые удовлетворяют условиям кодируемости, могут быть объявлены как наблюдаемые. Напомню, что в этом случае они станут частью конечной точки и по их значению можно будет отфильтровать вызовы с нужным значением параметра. В следующем примере сервис, реализующий метод, может подписаться на получение вызовов только для конкретного получателя.
// method user::send_message
// file api/chat/user/send_message/method.proto
message MethodDesc {
message Params {
string receiver = 1 [(observable) = true];
string text = 2;
}
message Retval { }
}
Static
Предопределенная структура Static
делает метод статическим.
// method user::sign_up
// file api/chat/user/sign_up/method.proto
// user does not exist until he is signed up, so we define this method as static
enum Result {
RESULT_SUCCESS = 0;
RESULT_USERNAME_ALREADY_TAKEN = 1;
RESULT_PASSWORD_TOO_WEAK = 2;
}
message MethodDesc {
message Params {
string username = 1;
string password = 2;
}
message Retval {
Result result = 1;
}
message Static { }
}
Статический класс может содержать только статические методы.
Файл дескриптора сервиса
Этот файл должен содержать дескриптор сервиса, который представляет собой предопределенную структуру с именем ServiceDesc
. Дескриптор сервиса может содержать вложенные структуры, рассматриваемые в следующих подразделах.
Config
Предопределенная структура Config
описывает конфигурационные параметры сервиса.
Фреймворк busrpc предоставляет кастомную строковую опцию default_value
, с помощью которой можно задавать произвольное значение по умолчанию для полей любой структуры. Конечно, сама библиотека protobuf ничего не знает про семантику этой опции, поэтому ее поддержка должна осуществляться в клиентских библиотеках busrpc.
Опция default_value
часто используется для полей таких предопределенных структур, как Params
и Config
.
message ServiceDesc {
message Config {
string bus_ip = 1 [(default_value) = "127.0.0.1"];
uint32 port = 2 [(default_value) = "4222"];
}
}
Implements и Invokes
Предопределенные структуры Implements
и Invokes
содержат информацию о методах, реализуемых и вызываемых сервисом. Эта информация выражается через типы полей структур, в качестве которых используются дескрипторы методов MethodDesc
.
Рассматриваемым структурам по определению приходится ссылаться на типы, которые по общему правилу невидимы для них, т.к. находятся в иной области видимости. Платформа busrpc делает исключение для этих структур и не трактует это как ошибку.
// service 'account'
// file implementation/account/service.proto
message ServiceDesc {
// ...
message Implements {
busrpc.api.chat.user.sign_in.MethodDesc method1 = 1;
busrpc.api.chat.user.sign_up.MethodDesc method2 = 2;
}
message Invokes {
busrpc.api.chat.user.on_signed_in.MethodDesc method1 = 1;
busrpc.api.chat.user.on_signed_up.MethodDesc method2 = 2;
}
}
Встроенные типы
Несколько типов данных играют особую роль в платформе busrpc и предоставляются ее разработчиком в файле busrpc.proto, который можно скачать из репозитория. Эти типы рассматриваются в следующих подразделах
Errc
Тип Errc
представляет собой перечисление, которое содержит коды ошибок, используемых для исключений платформы busrpc. По умолчанию Errc
определяется следующим образом:
// file busrpc.proto
enum Errc {
ERRC_UNEXPECTED = 0;
}
Конкретный busrpc проект может расширять этот тип своими константами.
Exception
Структура Exception
представляет исключение в платформе busrpc и определяется следующим образом:
// file busrpc.proto
message Exception {
Errc code = 1;
}
Конкретный busrpc проект может расширять этот тип, добавляя в него поля с дополнительной информацией об исключении. Например, итоговый тип может иметь такой вид:
// file busrpc.proto
message Exception {
Errc code = 1;
optional string description = 2;
optional string service_name = 3;
optional string namespace_name = 4;
optional string class_name = 5;
optional string method_name = 6;
}
CallMessage
Структура CallMessage
определяет формат сетевого сообщения, с помощью которого передается вызов метода:
// file busrpc.proto
message CallMessage {
optional bytes object_id = 1;
optional bytes params = 2;
}
Поле object_id
содержит сериализованный идентификатор объекта (структура ClassDesc.ObjectId
), для которого вызывается метод. В случае, если вызывается статический метод, это поле не должно устанавливаться.
Поле params
содержит сериализованные параметры метода (структура MethodDesc.Params
). Если метод не имеет параметров, то поле не устанавливается при вызове метода.
Спецификация busrpc запрещает вносить какие-либо изменения в этот тип, так как утилиты для работы с платформой рассчитывают на определенный формат сообщений.
ResultMessage
Структура ResultMessage
определяет формат сетевого сообщения, которое передается в качестве результата метода:
// file busrpc.proto
message ResultMessage {
oneof Result {
bytes retval = 1;
busrpc.Exception exception = 2;
}
}
Поле retval
содержит сериализованное возвращаемое значение метода (структура MethodDesc.Retval
), в случае, если метод завершается без исключения. Иначе поле exception
используется для передачи выброшенного исключения.
Спецификация busrpc запрещает вносить какие-либо изменения в этот тип, так как утилиты для работы с платформой рассчитывают на определенный формат сообщений.
Документирование
Один из принципов моего манифеста говорит о том, что лучшая документация — это исходный код. Исходя из моего опыта, разработчики (я в их числе) не любят тратить время на написание документации, а еще больше не любят затрачивать усилия на поддержание ее в актуальном состоянии.
В платформе busrpc я использую принцип «код как документация», стараясь как можно больше информации предоставлять через код proto файлов и структуру проекта:
сущности API отображаются на поддиректории проекта
конечные точки методов можно определить прямо из дескриптора метода
структура бэкенда (сервисы, их конфигурация и ответственность) также содержится в директории проекта (поддиректория implementation/)
Остальная документация указывается в виде комментариев в proto файлах и дополнительных документирующих командах. Тем, кому доводилось использовать такой инструмент, как Doxygen, этот подход покажется очень знакомым.
Базовые правила
Блочным комментарием называется последовательность из одной или более строк комментариев без пропусков между ним