Разработка → OpenResty: превращаем NGINX в полноценный сервер приложений

TM_content 17 февраля в 17:12 19,2k
Мы вновь публикуем расшифровку доклада с конференции HighLoad++ 2016, которая проходила в подмосковном Сколково 7—8 ноября прошлого года. Владимир Протасов рассказывает, как расширить функциональность NGINX с помощью OpenResty и Lua.

Всем привет, меня зовут Владимир Протасов, я работаю в Parallels. Расскажу чуть-чуть о себе. Три четверти своей жизни я занимаюсь тем, что пишу код. Стал программистом до мозга костей в прямом смысле: я иногда во сне вижу код. Четверть жизни — промышленная разработка, написание кода, который идёт прямо в продакшн. Код, которым некоторые из вас пользуются, но не догадываются об этом.

Чтобы вы понимали насколько всё было плохо. Когда я был маленьким джуниором, я пришёл, и мне выдали такие двухтерабайтные базы. Это сейчас тут у всех highload. Я ходил на конференции, спрашивал: «Ребят, расскажите, у вас big data, всё круто? Сколько у вас там базы?» Мне отвечали: «У нас 100 гигабайт!» Я говорил: «Круто, 100 гигабайт!» А про себя думал, как бы аккуратненько сохранить покерфейс. Думаешь, да, ребята крутые, а потом возвращаешься и ковыряешься с этими многотерабайтными базами. И это — будучи джуниором. Представляете себе, какой это удар?

Я знаю больше 20 языков программирования. Это то, в чём мне пришлось разобраться в процессе работы. Тебе выдают код на Erlang, на C, на С++, на Lua, на Python, на Ruby, на чем-то еще, и тебе надо это всё пилить. В общем пришлось. Точное количество посчитать так и не удалось, но где-то на 20 число потерялось.

Поскольку все присутствующие знают, что такое Parallels, и чем мы занимаемся, говорить о том, какие мы крутые и что делаем, не буду. Расскажу только, что у нас 13 офисов по миру, больше 300 сотрудников, разработка в Москве, Таллине и на Мальте. При желании можно взять и переехать на Мальту, если зимой холодно и надо погреть спинку.

Конкретно наш отдел пишет на Python 2. Мы занимаемся бизнесом и нам некогда внедрять модные технологии, поэтому мы страдаем. У нас Django, потому что в ней всё есть, а лишнее мы взяли и выкинули. Также MySQL, Redis и NGINX. Ещё у нас — много других крутых штук. У нас есть MongoDB, у нас кролики бегают, у нас чего только нет — но это не моё, и я этим не занимаюсь.

OpenResty


О себе я рассказал. Давайте разберёмся, о чем я буду сегодня говорить:

  • Что такое OpenResty и с чем его едят?
  • Зачем изобретать ещё один велосипед, когда у нас есть Python, NodeJS, PHP, Go и прочие крутые штуки, которыми все довольны?
  • И немножечко примеров из жизни. Мне пришлось сильно урезать доклад, потому что он у меня получался на 3,5 часа, поэтому примеров будет мало.

OpenResty — это NGINX. Благодаря ему мы имеем полноценный веб-сервер, который написан хорошо, он работает быстро. Я думаю, большинство из нас используют NGINX в продакшне. Все вы знаете, что он быстрый и крутой. В нём сделали крутой синхронный ввод/вывод, поэтому нам не надо ничего велосипедить подобно тому, как в Python навелосипедили gevent. Gevent — крутой, здоровский, но если вы напишите сишный код, и там что-то пойдёт не так, то с gevent вы сойдёте с ума это дебажить. У меня был опыт: потребовались целых два дня, чтобы разобраться, что же там пошло не так. Если бы кто-то бы до этого не покопался несколько недель, не нашел проблему, не написал в Интернете, и Google не нашел бы этого, то мы бы вообще свихнулись.

В NGINX уже сделаны кеширование и статический контент. Вам не нужно париться, как это сделать по-человечески, чтобы у вас где-нибудь не затормозило, чтобы вы где-то дескрипторы не потеряли. Nginx очень удобно деплоить, вам не нужно задумываться, что взять — WSGI, PHP-FPM, Gunicorn, Unicorn. Nginx поставили, админам отдали, они знают, как с этим работать. Nginx структурированно обрабатывает запросы. Я об этом немножко позже расскажу. Вкратце у него есть фаза, когда он только принял запрос, когда он обработал и когда отдал контент пользователю.

Nginx крут, но есть одна проблема: он недостаточно гибок даже при всех тех крутых фишках, что ребята впихнули в конфиг, при том, что можно настроить. Этой мощи не хватает. Поэтому ребята из Taobao когда-то давно, кажется, лет восемь назад, встроили туда Lua. Что он даёт?

  • Размер. Он маленький. LuaJIT дает где-то 100-200 килобайт оверхеда по памяти и минимальный оверхед по производительности.
  • Скорость. Интерпретатор LuaJIT во многих ситуациях близок к C, в некоторых ситуациях он проигрывает Java, в некоторых — обгоняет её. Какое-то время он считался state of art, крутейшим JIT-компилятором. Сейчас есть более крутые, но они очень тяжелые, к примеру, тот же V8. Некоторые JS-ные интерпретаторы и джавовский HotSpot в каких-то точках быстрее, но в каких-то местах всё ещё проигрывают.
  • Простота в освоении. Если у вас, допустим, кодовая база на Perl, и вы не Booking, вы не найдёте перловых программистов. Потому что их нет, их всех забрали, а учить их долго и сложно. Если вы хотите программистов на чем-то другом, возможно, их тоже их придётся переучивать, либо находить. В случае Lua всё просто. Lua учится любым джуниором за три дня. Мне потребовалось где-то часа два, чтобы разобраться. Через два часа я уже писал код в продакшн. Где-то через неделю он прямо в продакшн и уехал.

В результате это выглядит вот так:



Тут много всего. В OpenResty собрали кучу модулей, как луашных, так и энджинсовских. И у вас все готовое — задеплоил и работает.

Примеры


Хватит лирики, переходим к коду. Вот маленький Hello World:



Что здесь есть? это энджинсовский location. Мы не паримся, не пишем свой роутинг, не берём какой-то готовый — у нас уже есть в NGINX, мы живем хорошо и лениво.

content_by_lua_block – это блок, который говорит, что мы отдаем контент при помощи Lua-скрипта. Берем энджинсовскую переменную remote_addr и подсовываем её в string.format. Это то же самое, что и sprintf, только на Lua, только правильный. И отдаём клиенту.

В результате это будет выглядеть вот так:



Но вернёмся в реальный мир. В продакшн никто не деплоит Hello World. У нас приложение обычно ходит в базу или ещё куда-то и большую часть времени ждёт ответа.



Просто сидит и ждёт. Это не очень хорошо. Когда приходят 100.000 пользователей, нам очень тяжко. Поэтому давайте в качестве примера накидаем простенькое приложение. Будем искать картинки, например, котиков. Только мы не будем просто так искать, мы будем расширять ключевые слова и, если пользователь поискал «котята», мы ему найдём котиков, пушистиков и прочее. Для начала нам нужно получить данные запроса на бэкенде. Выглядит это так:



Две строчки позволяют вам забрать GET-параметры, никаких сложностей. Дальше мы, допустим, из базы данных с табличкой по ключевому слову и расширению получаем обычным SQL-запросом эту информацию. Всё просто. Выглядит это так:



Подключаем библиотечку resty.mysql, которая у нас уже есть в комплекте. Нам ничего не нужно ставить, всё готовое. Указываем, как подключиться, и делаем SQL-запрос:



Тут немножечко страшно, но всё работает. Здесь 10 — это лимит. Мы вытаскиваем 10 записей, мы ленивые, не хотим больше показывать. В SQL про лимит я забыл.

Дальше мы находим картинки по всем запросам. Мы собираем пачку запросов и заполняем Lua-табличку, которая называется reqs, и делаем ngx.location.capture_multi.



Все эти запросы уходят в параллель, и нам возвращаются ответы. Время работы равно времени ответа самого медленного. Если у нас все отстреливаются за 50 миллисекунд, и мы отправили сотню запросов, то ответ у нас придёт за 50 миллисекунд.

Поскольку мы ленивые и не хотим писать обработку HTTP и кэширования, мы заставим NGINX делать всё за нас. Как вы видели, там был запрос на url/fetch, вот он:



Мы делаем простой proxy_pass, указываем, куда закэшировать, как это сделать, и у нас всё работает.

Но этого недостаточно, нам ещё нужно отдать данные пользователю. Самая простая идея — это всё серилизовать в JSON, легко, в две строчки. Отдаём Content-Type, отдаём JSON.

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

Что с этим делать? Само собой, мы будем отдавать пользователю HTML. Генерировать ручками — не комильфо, поэтому мы хотим использовать шаблоны. Для этого есть библиотека lua-resty-template.



Вы, наверное, увидели три страшные буквы OPM. OpenResty идет со своим пакетным менеджером, через который можно поставить ещё кучу разных модулей, в частности, lua-resty-template. Это простой движок шаблонов, близкий к Django templates. Там можно написать код и сделать подстановку переменных.

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



Мы взяли данные и срендерили шаблон опять же в две строчки. Пользователь счастлив, получил котиков. Поскольку мы расширили запрос, он на котят получил ещё и морского котика. Мало ли, может, он искал именно его, но не мог правильно сформулировать свой запрос.

Всё круто, но мы же в девелопменте, и не хотим пока пользователям показывать. Давайте сделаем авторизацию. Чтобы это сделать, давайте посмотрим, как NGINX обрабатывает запрос в терминах OpenResty:

  • Первая фаза — access, когда пользователь только пришел, и мы на него посмотрели по заголовкам, по IP-адресу, по прочим данным. Можно сразу отрубить его, если он нам не понравился. Это можно использовать для авторизации, либо, если нам приходит очень много запросов, мы можем их легко рубить на этой фазе.
  • rewrite. Переписываем какие-то данные запроса.
  • content. Отдаём контент пользователю.
  • headers filter. Подменяем заголовки ответа. Если мы использовали proxy_pass, мы можем переписать какие-то заголовки, прежде чем отдать пользователю.
  • body filter. Можем подменить тело.
  • log — логирование. Можно писать логи в elasticsearch без дополнительного слоя.

Наша авторизация будет выглядеть примерно так:



Мы добавим это в тот location, который мы описали до этого, и засунем туда такой код:



Мы смотрим, есть ли у нас cookie token. Если нет, то кидаем на авторизацию. Пользователи хитрые и могут догадаться, что нужно поставить cookie token. Поэтому мы ещё положим её в Redis:



Код работы с Redis очень простой и ничем не отличается от других языков. При этом весь ввод/вывод что там, что здесь, он не блокирующий. Если пишете синхронный код, то работает асинхронно. Примерно как с gevent, только сделано хорошо.



Давайте сделаем саму авторизацию:



Говорим, что нам нужно читать тело запроса. Получаем POST-аргументы, проверяем, что логин и пароль правильные. Если неправильные, то кидаем на авторизацию. А если правильные, то записываем token в Redis:



Не забываем поставить cookie, это тоже делается в две строчки:



Пример простой, умозрительный. Мы конечно же не будем делать сервис, который показывает людям котиков. Хотя кто нас знает. Поэтому давайте пройдёмся по тому что можно сделать в продакшне.

  • Минималистичный бэкенд. Иногда нам требуется в бэкенд выдать совсем чуть-чуть данных: где-то нужно дату подставить, где-то какой-то список вывести, сказать, сколько сейчас пользователей на сайте, прикрутить счётчик или статистику. Что-то такое небольшое. Минимальные какие-то кусочки можно очень легко сделать. На этом получится быстро, легко и здорово.

  • Препроцессинг данных. Иногда нам хочется встроить в нашу страничку рекламу, причём эту рекламу мы берём API-запросами. Такое очень легко сделать именно здесь. Мы не загружаем наш бэкенд, который и так сидит тяжело работает. Можно взять и собрать здесь. Мы можем слепить какие-то JS или, наоборот, разлепить, что-то препроцессить прежде, чем отдать пользователю.

  • Фасад для микросервиса. Это тоже очень хороший кейс, я его реализовывал. До этого я работал в компании Tenzor, которая занимается электронной отчётностью, обеспечивает отчётность примерно половины юрлиц в стране. Мы сделали сервис, там при помощи этого же механизма сделаны многие вещи: маршрутизация, авторизация и другое.
    OpenResty можно использовать как клей для ваших микросервисов, который обеспечит единый доступ ко всему и единый интерфейс. Поскольку микросервисы могут быть написаны так, что вот здесь у вас Node.js, здесь у вас PHP, здесь Python, здесь стоит какая-то штука на Erlang, мы понимаем, что не хотим один и тот же код везде переписывать. Поэтому OpenResty можно воткнуть на фронт.

  • Статистика и аналитика. Обычно NGINX стоит на входе, и все запросы идут через него. Именно в этом месте очень удобно собрать. Можно что-то сразу посчитать и куда-нибудь закинуть, например, тот же Elasticsearch, Logstash или просто записать в лог и потом куда-нибудь отправить.

  • Многопользовательские системы. Например, онлайн-игры тоже очень хорошо делать. Сегодня в Кейптауне Александр Гладыш будет рассказывать, как быстро прототипировать многопользовательскую игру при помощи OpenResty.

  • Фильтрация запросов (WAF). Сейчас модно делать всякие web application firewall, есть много сервисов, которые их предоставляют. При помощи OpenResty можно сделать себе web application firewall, который просто и легко будет фильтровать запросы по вашим требованиям. Если у вас Python, то вы понимаете, что PHP вам точно незаинджектят, если вы, конечно, из консоли его не спауните нигде. Вы знаете, что у вас MySQL и Python. Наверное, тут могут попытаться сделать какой-нибудь directory traversal и что-нибудь заинджектить в базу. Поэтому можно отфильтровать стрёмные запросы быстро и дешево сразу на фронте.

  • Сообщество. Поскольку OpenResty построен на базе NGINX, то у него есть бонус — это NGINX-коммьюнити. Оно очень большое, и приличная часть вопросов, которая у вас возникнет поначалу, уже решена NGINX-сообществом.

    Lua-разработчики. Вчера я общался с ребятами, которые пришли на учебный день HighLoad++ и услышал, что на Lua написан только Tarantool. Это не так, на Lua много чего написано. Примеры: OpenResty, XMPP-сервер Prosody, игровой движок Love2D, Lua скриптуется в Warcraft и в других местах. Lua-разработчиков очень много, у них большое и отзывчивое коммьюнити. Все мои вопросы по Lua решались в течение нескольких часов. Когда пишешь в список рассылки, буквально через несколько минут уже куча ответов, расписывают что и как, что к чему. Это очень здорово. К сожалению, не везде такое доброе душевное коммьюнити.
    По OpenResty есть GitHub, там можно завести issue, если что-то сломалось. Есть список рассылки на Google Groups, где можно обсудить общие вопросы, есть рассылка на китайском — мало ли, может, английским вы не владеете, а знания китайского есть.

Итоги


  • Надеюсь смог донести, что OpenResty — это очень удобный фреймворк, заточенный под веб.
  • У него низкий порог вхождения, поскольку код похож на то, на чём мы пишем, язык довольно прост и минималистичен.
  • Он предоставляет асинхронный I/O без коллбеков, у нас не будет лапши, как мы можем иногда написать в NodeJS.
  • У него легкий деплой, поскольку нам нужен только NGINX c нужным модулем и наш код, и всё сразу работает.
  • Большое и отзывчивое сообщество.

Я не рассказал в деталях как делается маршрутизация, там получался очень длинный рассказ.

Спасибо за внимание!


Владимир Протасов — OpenResty: превращаем NGINX в полноценный сервер приложений
Проголосовать:
+39
Сохранить: