Когда мы начинали планировать, как будет устроен сервис Evernote, в 2007 году, мы знали, что в первый же день нам понадобится поддержка как “тонких” (таких как браузеры), так и “толстых” синхронизируемых клиентов. Это побудило нас задуматься над удаленным протоколами и клиентским API еще до начала работы над любым GUI для веба. В противном случае пришлось бы ждать несколько месяцев, пока прикрутят API на уже существующий веб-сервис.
Наши приложения предъявляют к API определенные требования, как то:
- Кроссплатформенность. Когда мы запустились в феврале 2008, у нас был готовый код, который на серверной стороне использовал Java, а на клиентской — Win32 С++ и Objective-C Cocoa.
- Компактность передачи данных. Клиентские приложения Evernote cинхронизируют заметки, которые могут содержать сотни встроенных изображений суммарным объемом в десятки мегабайт. Нам хотелось иметь API, в котором передача 15-мегабайтной заметки означала бы передачу именно этих 15 мегабайт, не больше.
- Прямая/обратная совместимость. После того как пользователь однажды установил себе версию нашего клиента на свой компьютер, мы не хотим принуждать его к обновлению софта каждый раз, когда расширяем нашу модель структуры данных.
- Привязки к языкам программирования (bindings). Мы не хотели писать кучу кода для парсинга и сериализации структур данных для каждого клиента. Это занимает кучу времени и приводит к возникновению ошибок, да и к тому же делает третий пункт неосуществимым на практике.
- Базирование на стандартах и/или открытые исходники. При прочих равных мы не хотели связывать API нашего сервиса с проприетарными технологиями по понятным причинам.
- Компактность. Мы бы предпочли не добавлять по мегабайту кода и по 200 классов в каждый наш мобильный клиент.
Мы потратили пару месяцев на исследование и тестирование различных альтернатив. XML-RPC или SOAP удовлетворяли одним требованиям (1, 5), ICE от ZeroC — другим (2, 4). Мы даже на какой-то момент задумались о том, чтобы изобрести велосипед и выкатить наш собственный маленький ad hoc протокол.
Один из наших знакомых порекомендовал обратить внимание на недавно открытый фреймворк Thrift, применявшийся в Facebook. Facebook использовал его во внутренней работе на backend-серверах для обмена сообщениями с другими внутренними серверами, где часто приходилось сталкиваться с сопряжением кода на разных языках (например, PHP и C++). Да и другие ребята, насколько мы могли судить, использовали Thrift для похожей задачи: обеспечение коммуникаций внутренних backend-серверов.
Мы же искали немного другое: фреймворк, который можно было бы использовать не только для связей «сервер-сервер», но еще и для массивной синхронизации «клиент-сервер» через Интернет. Вместе с тем Thrift идеально подходил под все наши требования:
- Кросс-платформенность. Мы определяем нашу модель данных и операций сервиса, используя в Thrift язык я зык описания интерфейса (Interface Definition Language), и после компиляции получаем на выходе заготовки клиентского и серверного кода для дюжины разных языков.
- Компактность передачи данных. Если в описании структуры указать, что есть бианарное поле, и положить туда 1 мегабайт данных, то в результате по каналам связи будет передан ровно этот 1 мегабайт.
- Прямая/обратная совместимость. Вот где Thrift действительно бесподобен. При определенной аккуратности и понимании того, как работает Thrift (что не всегда дается сразу, разумеется), вы можете добавлять структуры, поля, служебные методы и параметры функций без нарушений в существующих клиентах. Клиенты для Windows или Mac, которые мы выпустили 3 года назад, и сегодня могут синхронизироваться с Evernote.
- Привязки к языкам программирования (bindings). См. пункт 1. В самом начале в Thrift не было никакой поддержки Objective-C Cocoa, так что Andrew McGeachie (наш “человек-команда” по разработке Mac-клиента) добавил эту поддержку в компилятор Thrift.
- Базирование на стандартах и/или открытые исходники. Facebook передал разработку Thrift под управление Apache Software Foundation, что очень щедро с их стороны.
- Компактность. Исполняемые библиотеки Thrift и сгенерированный код получались весьма небольшими и понятными. Их можно было легко прочитать и точно понять, что они делают (с тех пор, правда, он немного обросли всяким дополнительным кодом, но и на сегодня, по нашему ощущению, это все еще это самый компактный вариант по сравнению с альтернативами).
В конечном итоге мы получили Evernote Service API, который обеспечил всем нашим клиентам (и сотням партнерских приложений) взаимодействие через общий API с использованием сгенерированного нативного кода. С учетом более чем трех миллионов активных пользователей, зачастую использующих Evernote на нескольких платформах, мне кажется, что на большинстве компьютеров/устройств, использующих Thrift, стоят клиенты Evernote.
А что насчет вас?
Вы собираетесь реализовать 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 для разработчиков затем может быть компенсирована для некоторых сервисов, создатели которых заинтересованы в стабильности и долгой жизни клиентского кода.