Что такое thrift api

Заметки с передовой

Разработка ПО на переднем крае ИТ отрасли: проблемы, решения, технологии

Страницы

пятница, 1 июля 2011 г.

Зачем нужен Apache Thrift?…

В одной из прошлых заметок я рассказывал о Google Protocol Buffers. В качестве альтернативы последнему иногда упоминают Apache Thrift. Я проанализировал его возможности и в этой заметке хочу высказать свои мысли по поводу данной технологии.

Такой же, но другой… Совсем другой.
Точно так же, как и Protobuf, Thrift имеет специальный декларативный язык, позволяющий определить протокол передачи данных, и собственный компилятор, позволяющий сгенерировать код для 15 различных языков (по крайней мере, это было именно так для последней стабильной сборки с версией 0.6.1, которую я использовал для экспериментов). В число поддерживаемых языков входят, как весьма экзотические: OCaml или Smalltalk, так и более привычные: С++, Java, C#, Python. Но, если Protobuf в первую очередь позиционируется именно как средство определения протоколов для сериализации и десериализации данных, которые вы можете передавать и принимать по любому удобному вам каналу. То Thrift представляет собой полноценный легковесный RPC.

Помимо передаваемых данных, в файле декларации вы можете определить интерфейсы сервера, принимающего эти данные:

struct Login <
1: optional string domain,
2: required string account,
>

service UserManager <
bool Save(1: Login login, 2: string password, 2: string name)
>

Именно этот способ использования является основным. В таком качестве Thrift скорее является аналогом CORBA или SOAP, а не Protobuf.

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

Долой интерфейсы!
Но моей целью была именно сериализация / десериализация, как в Protobuf. Поэтому я решил попробовать обойтись без определения интерфейсов. Ведь, в конце концов, не зря же Thrift называют альтернативой именно Protobuf‘у, а не CORBA.

Что такое thrift api. Смотреть фото Что такое thrift api. Смотреть картинку Что такое thrift api. Картинка про Что такое thrift api. Фото Что такое thrift apiВ качестве эксперимента я взял пример из заметки про Protobuf и попытался переписать определение структур User, Login и PhoneNumber для Thrift. Здесь меня ждал первый неприятный сюрприз. Директива include, прекрасно работающая в случае декларации интерфейсов, отказывалась «включать» структуры. Если структура PhoneNumber располагалась в другом файле, который включался в компилируемый файл с определением структуры User, то компиляция завершалась сообщением об ошибке: Type «PhoneNumber» has not been defined.

Попытка найти ответ в документации не привела ни к чему: документация скудна, особенно в сравнении с документацией по Protobuf‘у (это явилось вторым разочарованием). Поэтому я просто поместил все описания в один файл (user.thrift):

namespace cpp ad.sync
namespace csharp ad.sync

struct Login <
1: optional string domain,
2: required string account,
>

struct User <
1: required string id,
2: required string ldap,
3: required Login login,
4: optional string display_name,
5: list PhoneNumber > phoneNumber,
6: list string > memberOf,
>

Обратите внимание на весьма странное решение с пространствами имён. Нельзя определить единое пространство имён для всех структур (и интерфейсов) в рамках thrift-файла. Зато можно задать (а можно и не задавать) отдельное пространство имён со своим именем и уровнем вложенности для каждого поддерживаемого языка.

После компиляции в код на C++:

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

Библиотека Thrift для C++ содержит несколько классов, реализующих интерфейс TProtocol. Объекты этих классов определяют формат представления данных: бинарный, JSON и так далее. Все реализации протоколов в свою очередь требуют объект реализующий интерфейс TTransport. Именно транспорт в архитектуре Thrift определяет, куда будут сохранены или переданы данные. Среди множества «сетевых» транспортов, вроде THttpTransport, выделяются 2 транспорта с именами TMemoryBuffer и TFileTransport, позволяющих сохранить данные, если верить описанию, в буфер в памяти и в файл. Это должно быть именно тем, что нам нужно.

Фальстарт!
Итак, всё, что нам нужно – это создать проект C++ в Visual Studio, включить в него сгенерированные файлы, проект библиотеки Thrift и всё скомпилировать. Не тут-то было! В этом месте меня постигло самое главное разочарование: библиотека Thrift не имеет реализации под Windows. Чтобы скомпилировать и запустить под Windows приложение, использующее Thrift, вам понадобится развернуть cygwin. Причём, как несложно догадаться, вам придётся распространять cygwin вместе с вашим готовым продуктом. Сомнительное удовольствие, если учесть, что есть более дружелюбные альтернативы.

Я допускаю, что необходимые классы можно аккуратно извлечь из остальной реализации Thrift, тем более что их не так много. Кто-нибудь более настойчивый может это сделать. Вам необходимы классы из файлов: TBinaryProtocol.*, TProtocol.h, TVirtualProtocol.h, TProtocolException.h, TBufferTransports.*, TTransport.h, TVirtualTransport.h, TTransportException.*. Но такое решение чревато проблемами. Не факт, что вы сможете повторить подобное художественное выпиливание, если в будущем захотите обновить, используемую библиотеку до новой версии. Поэтому я просто не стал тратить на это время и перешёл к плану B.

Идём другим путём.
К моему глубокому удивлению Thrift имеет полноценную реализацию для C#. Правда, прежде чем скомпилировать библиотеку, пришлось немного поправить файлы проектов руками, а именно: удалить комментарии с лицензионным соглашением, ибо моя Visual Studio 2010 Express считала ошибкой формата расположение комментария в начале файла. Ещё пришлось исключить из сборки не собирающийся проект с тестами (надо отметить, что тесты не собирались и в случае с Google Protobuf – очевидно, это массовое заболевание подобных библиотек). После этого я успешно скомпилировал проект в библиотеку Thrift.dll.

Итак, компилируем user.thrift в классы C#:

string filePath = @»message.dat» ;

ad.sync. User anotherUser = new ad.sync. User ();
using ( Stream stream = new FileStream (
filePath, FileMode.Open))
<
Thrift.Transport.TTransport transport =
new Thrift.Transport. TStreamTransport (stream, null );
Thrift.Protocol.TProtocol protocol =
new Thrift.Protocol. TBinaryProtocol (transport);
anotherUser.Read(protocol);
>

Это же решение можно использовать и в проектах на C++ под Windows. Только использовать придётся C++/CLI.

Выводы…
Выводы не утешительные. Реализация технологии мне показалась достаточно сырой: странное поведение компилятора thrift-файлов, откровенные ошибки в файлах проектов. Очень скудная документация: разбираться приходится, ориентируясь на примеры и комментарии в коде реализации библиотеки. Всё это не оставляет впечатления серьёзности проекта. Уже этого достаточно, чтобы отказаться от использования Apache Thrift в коммерческой разработке, по крайней мере, в его нынешнем состоянии.

В самом первом предложении на главной странице проекта декларируется его основное преимущество: Фреймворк для масштабируемой мульти-языковой разработки сервисов. Но на поверку заявленная поддержка нескольких языков оказалась не полной. Весьма показателен пример с требованием cygwin‘а для разработки под Windows на C++. Это требование, по сути, ставит крест на подобном использовании Фреймворка. Действительно, если это не портирование существующего проекта, то зачем мне тащить со своим приложением cygwin? Я просто выберу другую технологию.

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

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

Ну и последнее. Мне не понятно, зачем нужна ещё одна RPC технология, причём поддерживаемая только одним производителем, если у нас есть более традиционные и поддерживаемые многими аналоги: CORBA, SOAP, XML RPC и так далее. Если мне понадобится скорость, а не удобство, то данные я буду передавать напрямую через сокеты. Для сериализации в этом случае можно использовать действительно кросс-платформенный Protobuf, JSON (множество реализаций которого доступно для разных языков и платформ) или на худой конец XML (если использовать SAX вместо DOM и не увлекаться проверкой по DTD, то XML вполне быстр и пригоден для передачи данных).

Итак, если вам непременно необходимо написать сервер на Smalltalk и клиента на OCaml, то Apache Thrift – это именно ваш вариант (но не наоборот, ибо реализация Thrift для Smalltalk не содержит клиентских компонентов). Во всех остальных случаях, я бы выбрал другую, более традиционную связку языков и технологий. Ещё раз подчёркиваю, что всё сказанное касается исключительно нынешнего положения дел. Вполне возможно, что в будущем, когда Apache Thrift повзрослеет, я изменю своё мнение.

Источник

Национальная библиотека им. Н. Э. Баумана
Bauman National Library

Персональные инструменты

Apache Thrift

Содержание

Архитектура

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

Thrift также предоставляет несколько серверов, такие как:

Преимущества

Apache Thrift имеет ряд преимуществ такие как:

Сравнение с Protocol Buffers

Apache ThriftProtocol Buffers
РазработчикFacebook, ApacheGoogle
Поддерживаемые языкиC++, Java, JavaScript, Python, PHP, XSD, Ruby, C#, Perl, Objective C, Erlang, Smalltalk, OCaml, and HaskellC++, Java, Python (Perl, Ruby и C# обсуждаются)
Исходящие форматыBinary, JSONBinary
Простые типыbool
byte
16/32/64-bit integers
double
string
byte sequence
map
list
set
bool
32/64-bit integers
float
double
string
byte sequence
повторные свойства работают как списки
КонстантыДаНет
Составной типstructmessage
ИсключенияДаНет
ДокументацияПроблематичноХорошая
ЛицензияApacheBSD-style
Расширения составных типовНетДа

Генерация файла Thrift в исходный код

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

Создание Thrift службы

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

Thrift генерирует код из этой описательной информации. Например, в Java, ComType будет простым перечислением внутри класса Com.

Источник

Техноблог: почему Evernote выбрал Apache Thrift для построения своего API

Что такое thrift api. Смотреть фото Что такое thrift api. Смотреть картинку Что такое thrift api. Картинка про Что такое thrift api. Фото Что такое thrift api
Когда мы начинали планировать, как будет устроен сервис Evernote, в 2007 году, мы знали, что в первый же день нам понадобится поддержка как “тонких” (таких как браузеры), так и “толстых” синхронизируемых клиентов. Это побудило нас задуматься над удаленным протоколами и клиентским API еще до начала работы над любым GUI для веба. В противном случае пришлось бы ждать несколько месяцев, пока прикрутят API на уже существующий веб-сервис.

Один из наших знакомых порекомендовал обратить внимание на недавно открытый фреймворк Thrift, применявшийся в Facebook. Facebook использовал его во внутренней работе на backend-серверах для обмена сообщениями с другими внутренними серверами, где часто приходилось сталкиваться с сопряжением кода на разных языках (например, PHP и C++). Да и другие ребята, насколько мы могли судить, использовали Thrift для похожей задачи: обеспечение коммуникаций внутренних backend-серверов.

А что насчет вас?

Вы собираетесь реализовать API для своего веб-сервиса. Стоит ли и вам использовать Thrift?

Если для вашего приложения характерны точно такие же требования, что и для Evernote, то Thrift может быть удачным выбором. Если же вы не сталкиваетесь с комплексной моделью данных с большими бинарными структурами (п. 2), то ответ не столь очевиден.

Веб-сервисы с более простыми моделями данных, как правило, используют менее сложные REST-протоколы с сериализацией данных через XML или JSON. Такой подход сделает простые операции действительно простыми для тестирования и исполнения. Если мне нужно сделать пару вещей с помощью API Твиттера, я могу протестировать их вручную из командной строки с curl/wget и прикрутить код в мое приложение через printf/println/regexps и т д. Это означает, что стартовый барьер для независимых разработчиков, которые начнут работу с таким типом API, очень низок.

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

С другой стороны, низкий барьер для подобных типов API с упрощенной нетипизированной сериализацией данных, как правило, оборачивается последующими проблемами с совместимостью (п. 3). Наш шлюз для Твиттера использует независимую библиотеку Twitter4J для взаимодействия с API Твиттера, основанном на REST. В прошлом году наш шлюз ломался как минимум пару раз из-за изменений на серверной стороне Твиттера и последующей некорректной интерпретации в Twitter4J структур XML-данных (например, разрядности числа идентификатора твита).

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

Источник

Пишем собственный шлюз для Thrift API

Микросервисы, как ни крути, — наше всё. Можно сопротивляться SOAP 2.0 сколь угодно долго, но рано или поздно или они придут за тобой и обратят в свою веру, или ты придёшь к ним сам и попросишь крестить себя огнём и мечом. Как и у любого архитектурного решения, у микросервисов есть свои минусы. Одним из них является необходимость в каждый микросервис включать какую-то логику по авторизации запросов от внешних систем или других микросервисов. Эта логика может быть напрямую «зашита» внутри микросервиса (и не важно, что это отдельная библиотека), делегирована другому микросервису, а может быть объявлена декларативно. Что значит декларативно? Например, можно договориться, что в каждый микросервис приходит особый HTTP-заголовок, или какая-то структура данных, в которой есть информация о пользователе, делающем запрос. И данным в этой структуре необходимо однозначно доверять. У всех трёх вариантов есть свои недостатки, но в рамках статьи мы разберём последний. Для его реализации обычно используется шаблон проектирования API Gateway:
Что такое thrift api. Смотреть фото Что такое thrift api. Смотреть картинку Что такое thrift api. Картинка про Что такое thrift api. Фото Что такое thrift api

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

В общем случае API Gateway ограничивает количество запросов к внутренним сервисам, авторизует запросы клиентов, производит логирование и аудит, распределяет запросы между клиентами и преобразовывает данные, если это нужно. В качестве примера может быть использован обычный nginx. Рассмотрим функцию авторизации запросов пользователей. Если используется HTTP-протокол, то общепринятой практикой считается добавление некоего токена (не важно как мы его получили) в заголовок Authorization:

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

Всё кажется простым и понятным, но беда в том, что Apache Thrift состоит из нескольких частей:

В общем случае мы не можем завязаться на протокол или транспорт. Можно конечно выбрать что-то одно, всем договориться, что мы используем только HTTP, но это ограничивает возможности по замене транспорта и заставляет делать некие внешние обработчики/фильтры уже внутри самих Thrift-сервисов (ведь для них то http-заголовки не являются нативными).

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

Convention over configuration

Итак, пусть у нас есть следующий внутренний сервис:

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

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

Кишочки

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

Есть три наиболее популярных протокола:

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

Поэтому наш алгоритм должен быть следующим:

Пишем тест

Создадим и заполним внешний сервис тестовыми данными:

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

Метод send_getSomeData сериализует сообщение в наш буфер.

Аналогичные действия сделаем и со внутренним сервисом:

Получим байтовый массив нашего сообщения:

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

Реализация обмена токена (AuthTokenExchanger) может быть разной в разных проектах, поэтому сделаем отдельный интерфейс:

createEmptyAuthToken должен вернуть некий объект, который представляет пустой токен, заполненный MessageTransalator-ом. В методе process нужно реализовать обмен авторизационного токена на данные о пользователе. Для нашего теста используем простую реализация:

Запускаем тесты, и ничего не работает. И это хорошо!

Зеленый свет

Реализуем метод process согласно алгоритму:

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

Реализуем нахождение границ токена в байтовом массиве:

Сериализуем пользовательские данные:

Включаем Шерлока

Итак, тесты для Binary и Compact проходят, но JSON сопротивляется. Что же не так? Уходим в дебаг и смотрим, какие же массивы мы сравниваем:

Не заметили разницы? А она есть. После первого «rec» не хватает двоеточия. API используем один и тот же, а результат разный. Разгадка пришла только после внимательного чтения кода класса TJSONProtocol. Протокол содержит контекст, который хранит различные разделители в стеке, когда обходит JSON-структуру для чтения или записи.

При чтении структуры, считывается и символ «:», а вот обратно он не возвращается, потому что контекста в самом объекте нет.

Вставляем костыль в метод seriaizeUserData:

Запускаем тесты, и теперь то всё ок.

Выброс исключений

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

Сделаем обработку исключений в отдельном методе processError.

Реализация записи TApplicationException в ответный пакет данных довольно проста:

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

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

Здесь интересно то, что для пользовательского исключения тип обратного сообщения не TMessageType.EXCEPTION, а TMessageType.REPLY.

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

В бар врывается Spring

Ок, мы сделали препарацию бинарных пакетов. Теперь самое время сделать практическую реализацию на популярном фреймворке для создания микросервисов. Например, на Spring Boot. Он хорош тем, что, с одной стороны, для него можно найти уже готовые решения, а с другой — его просто и удобно кастомизировать аннотациями добавляя новые возможности двумя-тремя строчками кода. Для роутинга и обработки HTTP-запросов возьмём Netflix Zuul, который входит в набор расширений Spring Cloud. Схема работы Zuul-а представлена на следующем изображении:

Что такое thrift api. Смотреть фото Что такое thrift api. Смотреть картинку Что такое thrift api. Картинка про Что такое thrift api. Фото Что такое thrift api

Если совсем просто, то Netflix Zuul представляет из себя обычный сервлет с цепочкой собственных фильтров, которые могут как загружаться динамически, так и быть включёнными в приложение. Каждый фильтр добавляет новое поведение, и даже запись HTTP-ответа тоже реализована фильтром. Существует несколько типов фильтров, которые выполняются последовательно как показано на картинке выше. Внутри каждого типа, фильтры выполняются в порядке, определённом приоритетом конкретного фильтра. Подключить Zuul к приложению на Spring Boot проще простого (ну ещё зависимости добавить):

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

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

Аннотация ConditionalOnMissingBean предотвратит создание дефолтного бина в том случае, если в приложении будет объявлен собственный бин этого класса. Ранее созданный интерфейс AuthTokenExchanger должен быть в обязательном порядке реализован разработчиком конкретного проекта. Мы не можем, по причинам безопасности, сделать какую-либо дефолтную реализацию, поэтому в методе создания бина выкидывается исключение. Также, нужно определить протокол, используемый для передачи thrift-сообщений. По-умолчанию, это TBinaryProtocol, но всегда можно использовать нужный для проекта, переопределив бин создания фабрики протокола. Но самой важной частью конфигурации безусловно является бин AuthenticationZuulFilter, который реализует бизнес-логику авторизационного слоя.

После получения объектов контекста и HTTP-запроса, создадим MessageTransalator.

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

Если произошла ошибка, то её необходимо обработать:

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

Соединяем всё вместе

Создадим новое Spring Boot приложение и добавим в него две аннотации @EnableZuulProxy и @EnableThriftGateway:

Реализуем простую логику авторизации:

Как видно, если к нам пришёл токен со значением heisours, то мы авторизовываем запрос, а если нет, то выкидываем ошибку. Остаётся только сконфигурировать Zuul:

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *