пятница, 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.

В качестве эксперимента я взял пример из заметки про 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

enum Type {
    HOME = 0,
    WORK = 1,
    MOBILE = 2,
    IPPHONE = 3,
}

struct PhoneNumber {
    1: required string number,
    2: optional Type type = 0,
}

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-0.6.1.exe -o Messages --gen cpp user.thrift
я получил по одному классу для каждой структуры, объявленной в файле user.thrift. Беглый анализ этих классов показал, что для операций чтения и записи пригодны лишь следующие два метода:
uint32_t read(
    ::apache::thrift::protocol::TProtocol* iprot);
uint32_t write(
    ::apache::thrift::protocol::TProtocol* oprot) const;
Каждый из них требует некий протокол в качестве единственного входного аргумента.

Библиотека 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#:
thrift-0.6.1.exe -o Messages --gen csharp user.thrift
Создаём новый проект, в него включаем сгенерированные классы и полученную библиотеку. Всё, что нам осталось – это написать код сериализации и десериализации. Аналогом C++ классов TMemoryBuffer и TFileTransport в C# является класс TStreamTransport. Вот пример его использования:
string filePath = @"message.dat";

ad.sync.User user = new ad.sync.User();
user.Id = Guid.NewGuid().ToString();
user.Login = new ad.sync.Login();
user.Login.Account = "jdoe";
user.Login.Domain = "neverhood";
user.Display_name = "John Doe";
user.MemberOf = new List<string>();
user.MemberOf.Add("User");
user.MemberOf.Add("Management");
user.PhoneNumber = new List<ad.sync.PhoneNumber>();
ad.sync.PhoneNumber phoneNumber =
    new ad.sync.PhoneNumber();
phoneNumber.Type = ad.sync.Type.WORK;
phoneNumber.Number = "+5 (555) 123x45x67";
user.PhoneNumber.Add(phoneNumber);

using (Stream stream = new FileStream(
    filePath, FileMode.Create))
{
    Thrift.Transport.TTransport transport =
        new Thrift.Transport.TStreamTransport(null, stream);
    Thrift.Protocol.TProtocol protocol =
        new Thrift.Protocol.TBinaryProtocol(transport);
    user.Write(protocol);
}

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 повзрослеет, я изменю своё мнение.

7 комментариев:

Vadim Kantorov комментирует...

Зачем нужна еще одна RPC? Ну ребятам из Facebook понадобилась легкая и подходящая им, вот и реализовали :)

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

Назвать RPC-фреймворк "аналогом SOAP" это конечно сильно.

Алексей Коротаев комментирует...

> Назвать RPC-фреймворк "аналогом SOAP" это конечно сильно

Хм... Что именно вам показалось странным или "сильным"? Или вы считаете, что SOAP - это не RPC?

Buran комментирует...

А я вот покрутил Protobuf с Thrift'ом и заюзал BSON :) И менее формально, и схемы нет, и никаких тебе стаб-генераторов ... но так оказалось удобно в использовании, что на нём и остался.

Алексей Коротаев комментирует...

Так в наличие схемы и сгенерированных стабов и заключаются все плюшки Protobuf`а (как я уже сказал, зачем нужен Thrift, я не понимаю :-) ). Вот как вы будете описывать синтаксис своего протокола, чтобы передать его другому участнику разработки или вообще 3-ей стороне? В Word`е? А как добьётесь гарантий, что это описание актуально? А тут всё в одном флаконе - idl ровно для этих целей в своё время и придумали.

В любом случае спасибо за инфу: надо будет тоже покрутить. Кстати, ссылку не дадите какой именно реализацией вы пользуетесь? Я именно BSON к стыду своему ещё ни разу не пробовал :-)

Unknown комментирует...

Почему ты решил что не сможешь ее скомпилировать под Windows?
Для этого есть MSYS, в окружении которго можно собрать через MinGW, который даст либы совместимого формата для Visual Studio. Попробуй так.

Алексей Коротаев комментирует...

А зачем мне нужны были такие сложности в далёком 2011 году, если был Google Protobuf, который собирался без особых приседаний и работал в достаточных для меня рамках?...

Отправить комментарий