Мы вновь публикуем расшифровку доклада с конференции 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 в полноценный сервер приложений
Поделиться с друзьями
-->

Комментарии (28)


  1. phoenixweiss
    17.02.2017 17:31
    +1

    В моем сознании всплывает нечто среднее между «Круто» и «Зачем?».
    По выступлению пара шутеечек почти зашла (на самом деле нет). Но по существу идея реально крайне интересная. Сейчас как раз думаем над задачей возможности передачи в nginx состояния авторизации пользователя от рельсового приложения для решения одной нестандартной задачи и эта статья попалась на глаза как нельзя кстати.


    1. saks
      17.02.2017 17:40
      +1

      Мы сначала так и делали. Теперь авторизация происходит на стороне nginx и до медленной rails далеко не всегда дело доходит. Более того, все JSON APIs уже изначально на Lua пишутся.


    1. rumkin
      17.02.2017 21:43

      Мне почему-то такая же идея в голову пришла и я написал auth proxy. Работает корректно. Из минусов сравнительное неудобство тестирования (хотя привыкаешь), отсутствие централизованной инфраструктуры lua/C и индексы массива начинающиеся с единицы. В остальном очень комфортно.


      1. saks
        18.02.2017 16:35

        У нас полноценные интеграционные тесты через lapis написаны. Только одно неудобство, нельзя stub-ить код в другом процессе :)


  1. Ordinatus
    17.02.2017 18:47
    +3

    Для таких велосипедов есть хороший фреймворк для Gateway API — Kong с плагинами и всё это на Lua.


    1. boston
      17.02.2017 20:01

      Kong для проксирования api, не стоит его сравнивать с полноценным сервером приложений.


  1. saks
    17.02.2017 19:16

    Также не стоит забывать про lapis


  1. rumkin
    17.02.2017 21:44

    Еще сюда же можно добавить luvit – node.js только на lua.


    1. saks
      18.02.2017 16:49

      Меня отпугивает подход luvit к асинхронным операциям, очень не люблю callback-и. Но несомненно проект достоин внимания. Кстати есть ещё и cqueues, у нас на нём web UI роутера крутится. Он отличается более тонким контролем event-loop и сильно гибче nginx.


  1. Pilat
    18.02.2017 03:04

    Два вопроса. Предположим, делается REST сервер.
    1) Где и как хранить конфигурационные файлы приложения.
    2) Что надо сделать, чтобы пользоваться не MySQL, а PostgreSQL ?


    1. saks
      18.02.2017 16:53
      +1

      1) вариантов много, самый простой в файле на Lua
      2) pgmoon?


  1. F0iL
    18.02.2017 05:04

    У nginx асинхронная неблокирующая архитектура и нужно избегать тяжелых процессов в обработчиках запросов (именно поэтому, к примеру, в nginx нет классического cgi-интерфейса, только fcgi и uwsgi), в связи с этим вопрос: когда выполняется запрос к БД через db:query, worker/интерпретатор тупо ждет, или там там какой-то свой хитрый event-loop (вариант что плодятся отдельные треды я уж не рассматриваю)?


    1. Jaromir
      18.02.2017 05:26

      > именно поэтому, к примеру, в nginx нет классического cgi-интерфейса

      Скорее из-за принципов и особенностей мышления разработчика. Создавать по запросу cgi-процессы и работать с ними асинхронно через sdtio — не такая уж и сложная задача. Для простых «одноразовых» задач, навроде обновления конфига через веб-интерфейс, нет смысла поднимать fcgi-сервер. Поэтому lighttpd


    1. saks
      18.02.2017 16:38
      +1

      Все операции с TCP/UDP сокетами неблокирующие. Т.е. да, там используется event-loop nginx-а, чтобы ждать ответ от DB.


  1. mxnr
    18.02.2017 05:04

    Вот еще просто Иисусий фреймворк http://leafo.net/lapis/, пилится одним человеком, но как же он круто работает…


  1. iSage
    18.02.2017 13:47

    Игровой движок называется Love2D, а не Lua 2D

    Мы используем у себя опенрести для кучи задач:
    Статистика
    Детект мобильных девайсов ( https://github.com/isage/lua-resty-mobile )
    Блочный кеш (через ssi) в редисе
    Ресайз картинок на cdn ( https://github.com/isage/lua-imagick )
    Сейчас медленно и неторопливо пилю либу для монгодб (хранить метаданные тех же картинок) ( https://github.com/isage/lua-resty-moongoo )

    Минус в том, что OpenResty-сообщество в основном китайско-говорящее и общающееся на англицком через google translate.
    Стоило еще упомянуть в статье, что изначально ngx_lua был разработан в taobao, а после поддерживался/поддерживается cloudflare (куда ушел работать автор)


    1. saks
      18.02.2017 16:39

      Он уже ушёл и из cludflare тоже :)


  1. SBKarr
    18.02.2017 14:03

    Упоминание highload и скриптовых либо VM-языков в одном контексте меня каждый раз смущает. Если это действительно Highload — время на переключение, очистку контекста и GC, не говоря уже о дополнительном времени, которое теряется в рантайм-обёртках этих языков капает в приличные цифры и приличные суммы. Не даром всякие мордокниги и контакты давно перевелись на компилируемые языки.

    ИМХО, настоящий хайлоад это только компилируемые языки без VM, и чем тоньше обёртка между языком и железом — тем лучше. Только хардкор. Тем более, на С++ или Rust нынче можно создать вполне дуракоустойчивый и простой API для серверных программистов классом ниже.

    Мы вот в своё время выбрали старый добрый Apache Httpd вместо nginx. Ключевые причины — богатый API и быстрая + дуракоустойчивая система пулов памяти. Если основная доля работы выполняется пользовательским кодом — асинхронный ввод-вывод nginx вдруг перестаёт приносить существенный прирост производительности. Ещё один важный момент — падение одного воркера апача от какого-нибудь неизбежного в С++ SIGSEGV создаёт меньше проблем, чем падение nginx.

    P.S. Конечно, стоит признать, лучше LuaJIT для указанной в статье цели ничего нет. Если не использовать ничего компилируемого.


    1. khim
      18.02.2017 19:46

      ИМХО, настоящий хайлоад это только компилируемые языки без VM, и чем тоньше обёртка между языком и железом — тем лучше.
      В соответствии с вашими определениями вот это вот — highload (reddit-эффект форум выдержал и даже не поперхнулся), а вот это вот — нет (миллиард пользователей и петабайты данных? это неважно: там Java, а значит это — «ненастоящий» highload).


      1. SBKarr
        18.02.2017 21:45
        -2

        А если бы там изначально вместо Java использовать нечто компилируемое — сколько бы ресурсов железа это сэкономило?


        1. wert_lex
          19.02.2017 11:41

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


          Ну и еще большой вопрос, насколько оно быстрее бы вышло грамотно настроенного netty. Смущают разве что паузы GC.


          Есть у меня подозрение, что highload это в первую очередь не про производительность одного единственного веб-сервера, а про масштабирование всей системы в первую очередь.


          1. SBKarr
            19.02.2017 18:08

            Тем не менее, критические части инфраструктуры, как правило, пишутся дорогостоящими специалистами на компилируемых языках. Просто потому, что без этого — дороже. И, по-моему, именно это делает хайлоад способным к, собственно, хайлоаду.


            1. khim
              19.02.2017 23:53

              Да ладно вам. Hadoop — это типичнейший highload. А там — голимая Java.

              Переписывать на компилируемые языки — дороже будет…


      1. SBKarr
        18.02.2017 21:52

        Кроме того, мне казалось, критические по ресурсам части инфраструктуры Google давно переписаны под Go, разве нет?


        1. khim
          19.02.2017 23:44

          Конкретно GMail — по-прежнему в основном Java. Поиск — это в основном C++. Go тоже используется, но до состояния, когда на нём будет написана существенная часть инфраструктуры ещё очень далеко…


  1. darkslave
    18.02.2017 15:39

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


    1. SBKarr
      18.02.2017 17:22

      Интересный факт, Lua изначально создан как формат конфигурационных скриптов. Отсюда такой интересный синтаксис. Чья безумная мысль породила метатаблицы метатаблиц метатаблиц — не скажу…


  1. mikeus
    20.02.2017 12:53

    … услышал, что на Lua написан только Tarantool. Это не так, на Lua много чего написано. Примеры: OpenResty, XMPP-сервер Prosody, игровой движок Love2D, Lua скриптуется в Warcraft и в других местах.
    Вот ещё пример для встраиваемых систем — Barracuda Application Server и основанный на нем Mako Server. Позиционируется как «RTOS Ready Embedded Web Server on Steroids» использует Lua Server Pages (LSP)