Вчера мы открыли в бету API для Я.ру. Это был первый пост в корпоративном блоге Яндекса с кодом на Питоне, что даже породило фан-арт :-). Для меня этот запуск имеет большое эмоциональное значение, потому что машиночитаемый веб — мой давний интерес, и этот проект — первый неигрушечный публичный API, где я занимаюсь дизайном, и могу смотреть, как выживают на практике теоретические соображения о том, как это должно делаться.
Я говорю тут от своего лица, и чтобы не возникало ложных ощущений, должен сказать, что я это всё делаю, конечно, не один. Начинал писать собственно серверную часть Иван Челюбеев. Моя текущая роль — проектировщик и менеджер. Код пишет сейчас Костя Меренков, а со стороны Я.рушного бэкенда нам помогает Серёжа Чистович.
Этот пост — несколько заметок о том, как всё устроено внутри. Пишите в комментариях, если что-то нужно раскрыть подробнее.
Сервис над сервисом
Сам Я.ру — многоуровневая кодобаза на нескольких языках и технологиях. Серверная её часть общается с внешним для себя миром по CORBA, и поэтому не подходит для публичного API. Кроме того, публичное API поддерживать тяжелее, чем внутреннее, из-за необходимости думать про обратную совместимость. Поэтому API сервиса Я.ру — это по сути ещё один отдельный сервис, который смотрит во внешний мир через HTTP, а внутрь — через CORBA.
Сервис этот написан на Джанго. Он довольно нетипичен для джанговского сервиса, потому что не использует моделей (у него вообще нет своих собственных данных), форм и шаблонов. Тем не менее, он сделан на Джанго... для простоты. Эта шокирующая идея идёт вразрез с общим трендом в тусовке модных питонистов, где принято считать Джангу "тяжёлой" (что бы это ни значило), и отказываться от неё сразу же, как только удастся найти хотя бы один компонент, который в проекте не нужен. У нас всё наоборот. Поскольку мы давно привыкли отлаживать, пакетировать, деплоить и мониторить Джанго-проекты, это делать проще, чем уживаться с любым новым фреймворком. И даже в отсутствии "более основных" компонентов сильно помогают знакомые полезные вещи: urlconf, middleware, модель запросов и ответов с вьюхами, дебагные страницы.
REST
Учитывая моё отношение к REST, вряд ли должно быть сюрпризом, что этот API именно такой. Если совсем честно, это было даже постановкой задачи. То есть не было так, как обычно происходит в статьях по проектированию: собрались люди и стали думать, какими средствами решать задачу "сделать API для Я.ру" и выбрали лучшую технологию для задачи. Это скучно! Мы собрались делать модный REST'овый API, а для чего именно — не суть важно :-).
Ресурсы и операции
Тем не менее, с какой стороны ни подходи, а я считаю, что API такого рода, как Я.ру, ложится на REST'овую идеологию почти идеально. Все ресурсы получаются совершенно естественно: люди, клубы и посты. Никакой тебе процессоорентированности или транзитивных состояний.
Операции над ресурсами есть очевидные, а есть не очень.
Очевидное — это например изменение данных профиля. Оно происходит как GET ресурса, изменение в XML нужных элементов и PUT всего профиля обратно на то же место. Здесь мы какое-то время думали над тем, чтобы поддержать обработку на входе "частичных представлений": поскольку полный профиль пользователя содержит довольно много полей, возможно клиенту было бы удобней передавать не все поля, а только те, что меняются. Тогда XML'ка могла бы выглядеть буквально как <person xmlns="yandex:data"><name>Новое имя</name></person>
. Но это оказалось хлопотно на связке между API и бэкендом, и мы забросили идею. Поэтому руками составлять XML не надо, его надо считывать и менять.
К неочевидным операциям относятся те, которые в Я.ру делаются по сайд-эффекту создания поста: подружение/раздружение, смена настроения, присоединение к клубам. Например очевидным способом подружиться был бы POST человека (в виде URI или документа) в свою коллекцию друзей, но сейчас это делается POST'ом поста специального типа в свой фид. И хотя прямую операцию можно поддержать технически, создавая при этом пустой пост, этого делать не хочется намеренно. Потому что идеология Я.ру в том, чтобы всё интересное происходило во френдленте. А пустой пост — это не интересно, это спам.
Вот прямо сейчас я понял, что это не отражено чётко в нашей документации.
Адресуемость и навигация
REST диктует, что все ресурсы должны иметь свой URI. А вот что часто упускается, так это что генерацию этих URI должен брать на себя по возможности сервер, а не клиент. То есть когда вы получаете GET'ом представление ресурса, в нём должны быть ссылки, URI-шаблоны и формы, по которым клиент понимает, куда и с чем он может обращаться дальше. На практике с этим есть две проблемы:
- многих людей эта идея искренне удивляет и даже пугает, все привыкли составлять URL'ы из кусочков;
- нормальных библиотек, способных обрабатывать URI templates, мало (под Питон, кажется, вообще нет)
Однако нам тут повезло, у нас все юзкейсы укладываются просто в обычные URL-ссылки. В итоге, пользуясь ярушным API, клиенту достаточно знать один входной URL: https://api-yaru.yandex.ru/me/
, а всё остальное достижимо по возвращаемым ссылкам:
/me/
редиректит на URL текущего залогиненного человека/person/<uid>/
/person/<uid>/
содержит ссылки на посты, список друзей, списки клубов- длинные списки содержат ссылки на следующие страницы
- из клубов есть обратные ссылки на своих членов
- и т.д.
Если вы пользуетесь этим API, и нашли, что вам приходится генерировать ссылку из кусочков — пишите, возможно мы просто упустили какую-то ссылку.
Со ссылками связан один весёлый момент :-).
Одно время у профилей и клубов был элемент <id>
, который содержал число. Первого же пользователя API это побудило хранить этот id в виде int'а и составлять ссылки с ним вручную. Не зная об этом, мы в какой-то момент элемент оторвали, рассудив, что идентификатор профиля и так уже есть — лежит в <link rel="self">
. Однако идея использовать длинный URL в качестве идентификатора показалась пользователям слишком радикальной, и в результате короткой битвы мы пришли к компромиссу: id вернули, но в виде короткой строчки :-).
Мораль в том, что глобально уникальные идентификаторы в REST-системах обязательно будут строчками, чтобы иметь возможность их расширять и комбинировать. К этому надо просто привыкнуть, и спокойно их использовать в качестве ключей.
Форматы данных
REST, опять-таки, диктует, чтобы для представлений ресурсов использовались по возможности известные стадартные форматы данных. Если так получается — это сказка. Всего лишь по одному заголовку Content-type клиент полностью понимает, как работать с этим ресурсом.
Поэтому для работы с блог-контентом — лентами и постами — мы выбрали AtomPub, который специально для этого и создавался. Этот же формат используют, например, Blogger и WordPress. Теоретически это означает, что с Я.ру должны заработать клиенты вроде Windows Live Writer и BlogJet. Хотя конечно надо будет поработать над интероперабельностью и подпилить что-то в паре мест. Если кому интересно поотлаживать это с клиентской стороны — опять таки пишите!
Что интересно, что изначально, неблоговый контент — профили людей — тоже были выражены через AtomPub. Штука в том, что он явно позволяет расширять набор атрибутов записей. В постах мы используем это, чтобы передавать чисто ярушные атрибуты: уровень доступа и запрещённость комментариев. Но принципиально это значит, что через Atom можно выразить что угодно. В итоге довольно долго профили у нас были такими вырожденными "постами", в которых большая часть контента была кастомной.
Под конец, буквально за неделю до запуска, мы всё же решили, что профиль — это не пост, и особенной пользы от Atom в этом месте нет. В итоге придумали свой кастомный формат. Это не очень хорошо, и я с удовольствием послушаю идеи, какие известные форматы, выражающий понятия "профиль человека" и "клуб по интересам", можно поддержать. Пока я лениво думаю над тем, чтобы выдавать профиль в формате text/x-vcard, а списки друзей в text/directory или FOAF.
Что нам даёт вся эта REST'овость? Я пока не знаю. Отчасти, это как раз эксперимент, чтобы понять, какие обещания REST реально воплощаются, а каким ещё надо помогать. Например, если блог-клиенты заработают с Я.ру, я буду считать это значительным достижением.
Если пофантазировать, то ещё может хорошо сработать наличие глобально универсального машиночитаемого идентификатора человека. Например, в API фоток, который тоже REST'овый, можно будет реализовать функциональность "добавить в любимые авторы", основываясь как раз на ярушных URI...
OAuth
С авторизацией мы, по сути, обогнали всех :-). Мы реализовываем драфт OAuth 2.0, и я специально употребляю (кривой) глагол несовершенного вида, потому что мы апдейтим реализацию под новые ревизии драфта сразу, как только они выходят. Пока разрабатывались, их сменилось уже три :-). Теперь, после публикации, обновляться будет сложнее, потому что придётся думать про обратную совместимость.
С точки зрения реализации это тоже отдельный сервис. Тоже написан на Джанго, но по-своему интересен, например тем, что в качестве хранилища использует только Redis. Но это, наверное, отдельная история.
Комментарии: 28 (особо ценных: 2)
URI templates http://code.google.com/p/uri-templates/
Быстро ли работает Джанга в Вашем случае? Что использовали для кеширования?
Спасибо за ответы и за пост, полный энтузиазма. Попробую потестить.
Иинтересно, а как решается проблема с параллельными изменениями профиля? То есть что будет если два клиента взяли профиль, изменили его и отправили запросы с изменёнными профилями? В профиле будет те данные, которые пришли последними, или первое обновление пройдёт, а второе вернёт ошибку?
Особо ценный комментарий
Для представления людей разрабатывается http://www.portablecontacts.net/draft-spec.html
Хотелось бы узнать как скоро будет изменен механизм авторизации в Я.Фотках
А не смотрели в сторону OpenSocial, когда делали АПИ?
Что то уж больно на gdata похоже - http://code.google.com/intl/ru-RU/apis/gdata/
Он ведь тоже REST и на Atom основан
очень рад за джанго, а то понапридумывают маленьких веб-фреймворков и объясняют всем, что только они - тру :)
разрешите пару вопросов и уточнений?
но это как-то грустно для конкурентных запросов, а особенно для таких (может, идеологически неверных, но возможных и вполне вероятных) приложений, которые раз этот XML загрузят, а потом только у себя меняют и шлют
это интересная идеология для разработки приложений и внутренних/внешних интерфейсов. а вы не могли бы рассказать поподробнее, как говорится, плюсы, минусы, подводные камни?
сейчас в одном разрабатываемом мною веб-приложении у каждого пользователя есть фид того, что с ним происходит, ну что-то вроде лога (ничего необычного, такое много где есть).возможно, стоило бы попробовать сделать фид не побочным, а основным, как это происходит в Mercurial :-) но я об этом исключительно умозрительное представление имею, очень бы хотел послушать от первого лица
[это, кстати, могло бы служить и ответом на мое первый комментарий о конкурентном изменении профиля, но это так, досужий домысел]
Facebook обогнал всех ;-) http://off.tumblr.com/post/830833540/oauth
Куча ответов:
crash:
Я не понял смысла комментария :-). В посте я про них написал.
Dyadya Zed:
Ей там нечем тормозить, большую часть времени сервис проводит в бэкенде. Мы пока не кешировали ничего, чтобы посмотреть, что реально сервис нагружать будет. Пока там пользователей недостаточно.
Oleg Sukhodolsky:
Сейчас никак не решается, но в REST есть для этого стандартное средство. Каждый ресурс будет выдавать ETag. При операции PUT на этот ресурс клиент передаёт полученный ETag в заголовке If-Match. На стороне сервера проверяется, совпадает ли он с текущим состоянием ресурса, и если нет — отдаёт ошибку. Тогда дело клиента перезапросить ресурс и дать юзеру изменить его ещё раз.
Впрочем, на практике это не будет проблемой в любом случае, потому что менять сейчас пользователь может только сам себя.
Илья:
Спасибо, посмотрим туда!
Lax:
С него, в принципе, всё начиналось. Правда, тогда юзкейсы были другие немного. Впрочем, поскольку это не протокол, а идея, можно сказать, что примерно в этом стиле у нас всё и сделано.
http://seriyps.ru/blog/:
Ну да, это тоже AtomPub. Поэтому и используем его, что формат распространённый.
valyagolev.net:
Это не "идеология" в смысле чего-то большого и философского. Просто так Я.ру построено внутри. Мне трудно придумать, что про это можно ещё рассказать :-).
pyobject.ru:
У них это довольно давно было, не уверен, что они тот же последний драфт реализовали :-). Тут ещё будет полный зоопарк какое-то время.
тоже рад за джанго, привет, Валя %) Я уже 3 года на Я.ру, только узнал, что оно на Django, кстати.
А теперь вопрос по делу: зачем изобретать велосипед, когда есть такая отличная штука, как Piston? Готовый микрофреймворк для REST API, от создателя Bitbucket =) Заодно поддержку OAuth 2.0 добавили бы и прислали автору.
Как раз сейчас приложение делаю - Piston очень помог, API публичный с документацией (там встроенный вид-генератор, ага) минут за 40 сделал (большинство времени ушло на документацию!).
Не нашел ссылки где можно просмотреть мои зарегистрированные приложения и отредактировать их.
Может быть я чего-то не понимаю, подскажите, пожалуйста, почему после регистрации приложения, мне не показали вместе с id еще и secret? Он ведь вроде как нужен для авторизации по OAuth2. Во всяком случае, Githib у меня его спрашивал, а у них OAuth2.
Особо ценный комментарий
У GitHub тоже есть OAuth2 http://github.com/blog/656-github-oauth2-support
Обалдеть... А потом ещё будут говорить, что это у меня в посте так и написно %-).
У меня другое мнение насчёт Piston, хоть я и прочитал только начало документации. Но главное, REST — это архитектурный стиль, а не протокол. То есть это не какая-то штука, которую можно автоматизировать фреймворком. То, что создатель Piston называет словом REST — это некая архитектура для некого ограниченного юзкейса в REST-стиле. Смысла в этом обычно нет.
Пока такого места нет, но будет скоро на https://oauth.yandex.ru/client/my
Хотя, редактировать там особенно нечего. Название и иконка — это identity, которая юзеру показывает, с чем он работает. Её нельзя давать менять внезапно. Callback и URL программы содержат домены, которые тоже неправильно давать менять. Поэтому мы пока решили это не реализовывать. Если расскажешь юзкейс — подумаем.
Насколько я помню, secret опциональный, нет? В любом случае, нам он не нужен — id достаточно.
Насколько я понял, FOAF как раз для представления профилей и предназначен. Для профилей и связей между ними.
Банально логотип сменился или после разработки приложения тестовый урл меняется на настоящий.
Все провайдеры OAuth, которые мне приходилось использовать, позволяют менять данные и даже ключи сбрасывать. Мне кажется, не стоит опасаться, что разработчик будет слишком часто что-то там менять. Но вот во время разработки может понадобиться что-то изменить.
Из документации так и не понял, по какому урлу можно получить access_token. Объясни, пожалуйста.
https://oauth.yandex.ru/token
WLW нужен service-document с списком ресурсов. Как у фоток...
Архитектурный стиль очень даже можно автоматизировать :-) Piston значит не писать велосипед для проверки типа запроса (GET/POST/PUT/DELETE), не писать велосипед для ограничения кол-ва запросов...
И самое главное, не писать велосипед для вывода данных в разных форматах. Сразу же получаешь XML, JSON, YAML и Pickle.
Понимаете, не любая самостоятельно написанная строка кода квалифицируется, как "велосипед". Проверка методов — это вообще не задача, чтобы её автоматизировать. Пытаясь это сделать, Piston навязывет мне модель из классов с CRUD-методами, которая увеличивает boilerplate-код и далеко не всегда удобна. Например, они POST жестко маппят в метод "create", хотя POST в REST-системах этого конкретно не предполагает.
Я бы легко посоветовал взять такой фреймворк начинающему разработчику, но откровенно говоря, я сам хорошо представляю, что мне нужно в системе, чтобы видеть, что эти ходунки нам уже не помогут.
И снова нарушаешь REST. Если это "сразу же", значит все эти форматы — просто тупой маппинг моих внутренних моделей даных, и я не получу Atom-документа из поста или VCard из персоны.
Вообще, в REST-архитектуре больше всего времени стоит уделять как раз выбору и дизайну форматов. То, что Piston это автоматизирует, говорит о том, что ребята не разобрались.
Так что пожалуй, я заберу свою рекомендацию из предыдущего ответа по поводу использования этого фреймворка. Не делайте этого :-).
Это у нас будет, ага.
Ух ты, Redis :) И как он вам? Какой версии, какой объем хранится в нем сейчас? Используете ли VM, слейвов, транзакции?
Редис — приятная штука. Очень быстрый, работать с ним удобно. Версия у нас сейчас 2.0, если память мне не изменяет. Но правда, поскольку мы собираем его сами, по ходу разработки часто брали транковые версии, когда там нужные нам фичи появлялись (за подробностями надо Костю пытать). Объём у нас небольшой. Поскольку сервис работает с ключами, это всё довольно мелочно. Да и используется он сейчас пока только с одним опубликованным API. Поэтому никакой VM там сейчас нет. Слейв есть, работает в fail-over режиме. А транзакций в Редисе нет, если что :-). Multi не обладает характеристиками транзакции.
пруфлинк?
Прошло уже какое-то время после внедрения - получали какой-нибудь фидбэк от девелоперов, использующих API?
Основной вопрос для меня сейчас в том, оправданно ли применение полноценного REST'а с гипертекстом, в ситуациях, когда девелоперы скорее всего не будут разрабатывать полноценный клиент, а когда им нужна какая-то одна конкретная фича.
В таком случае REST получается ощутимо сложнее и для понимания, и для использования, чем простые HTTP API вроде того же твиттера.
Например, если я правильно понимаю, в API Яндекс блогов нужно сделать несколько GET запросов, чтобы получить ссылку для какого-то конкретного действия. При этом нужно еще знать семантику всех промежуточных медиатипов.
Конечная идеализированная цель понятна :) но интересны и практические последствия :)
Ответил вот так: http://softwaremaniacs.org/blog/2010/12/12/on-rest-complexity/