96  /  96

Frontend на nginx+Lua

Просмотров: 4716 (Статистика ведётся с 06.02.2017)
Дата последнего изменения: 17.06.2015

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

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

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

В качестве инструмента учёта при решении задачи используется Navigation Timing API, который позволяет на клиентской стороне очень подробно разобрать каждый запрос: на какой этап сколько времени ушло. Инструмент поддерживается всеми современными браузерами, очень удобен и информативен. Его можно в виде простого счётчика встроить на сайт и получить все необходимые данные, обработать их и показать пользователю в виде диаграмм, графиков.

Постановка задачи

Все данные получаемые через Navigation Timing можно собрать, обработать, но куда это сохранить? Сохранить можно клиенту на его сайт, в его локальную базу. Но таким образом решая задачу производительности мы добавляем нагрузку на клиентскую Базу данных. Это неправильно, надо создавать свой собственный сервис, где мы за клиента агрегировали бы эти данные, делали выборки и отдавали клиенту только готовые графики.

Оценивая ориентировочную нагрузку, которая могла лечь на такой сервис, подсчитали:

  • сколько примерно клиентов обновиться до последней версии (потенциально - десятки тысяч сайтов),
  • среднюю посещаемость проектов,
  • максимальную нагрузку (примерно до 1500 хитов в секунду, десятки миллионов хитов день).

Все эти данные нужно сохранить, агрегировать и выдать пользователю.

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

Сделать это можно с помощью сервиса Kinesis от Amazon'а. Это - некий большой буфер, задача которого как раз: очень быстро принять некий набор данных, так же быстро сказать пользователю: всё принято. А потом из этого буфера можно любыми воркерами, на любом удобном языке программирования достать, обработать данные и построить на этом какую-то аналитику.

Установка перед Kinesis NGINX и использование его как некий буфер для складирования данных в Kinesis не позволяла решить задачу:

  • Обработка большого числа запросов - да, NGINX с этим справится хорошо.
  • Сложная логика для идентификации разных клиентов - тоже получается.
  • Можно делать всё на GET и POST запросах, чтобы не терять данные со старых браузеров - получается.
  • Авторизация на Amazon Web Services, v. 4 - не получается. Чтобы отправить любой запрос в сервис Амазона нужно отправить авторизационный запрос, который выглядит как вложенный 4 или 5 раз, подписанный с помощью SHA 256 набор параметров. Ни один из известных модулей к NGINX не мог решить этой задачи.

Варианты решения:

  • NGINX + бэкенд (PHP) - классический бекенд с его недостатками: низкая производительность, расход ресурсов. В этом случае проект будет очень "дорогой", так как производительность такого решения будет не очень большой, следовательно нужно много машин для горизонтального масштабирования.
  • NGINX + свой модуль (С/C++) - сложно, долго разрабатывать и тестировать. Плюс - отсутствия аналогичного опыта.
  • NGINX + ngx_http_perl_module - блокирующий, так как "модуль экспериментальный, поэтому возможно всё" (из документации к модулю).
  • NGINX + ngx_lua - фронтенд?

Решение на бэкенде

Решение на бэкенде: добавить PHP-FPM, написать простой скрипт в несколько строк, который делает нужные запросы и отвечает ровно так как нужно. Это в целом очень неудачное решение, когда для решения простой задачи использовался сложный и тяжёлый инструмент, который расходует много памяти, запросы с него получаются блокирующими (сколько воркеров PHP есть, столько запросов и можно отправить, остальные выстраиваются в очередь.)

Это приемлемый компромисс для невысоконагруженного сервиса, где не идут хиты пачками. Администратор своего проекта может раз в день, раз в неделю эту проверку выполнить. Компромисс на первое время устраивает, но в будущем могут возникнуть ситуации, когда такое решение работать уже не будет, либо потребует дополнительных мощностей, что сделает его достаточно "дорогим".

Решение на фронтенде

Но наше внимание привлёк новый, неизвестный нам модуль Lua. Реально это оказался очень простой инструмент. Кусок кода можно написать прямо в конфиге NGINX'а, либо вынести в отдельный файл и подключить в конфиге NGINX'а. По сути - это некий скриптовый язык, который может реализовать самую сложную кастомную логику. Плюсы Lua:
  • Очень легкий, например весь модуль меньше чем библиотека по PCRE.
  • Очень быстрый (LuaJIT прекомпелирует в байт-код).
  • Очень гибкий:
    • доступ к HTTP запросу и ответу,
    • синхронные неблокирующие подзапросы (ngx.location.capture, connect),
    • дополнительные модули для работы с дополнительными функциями,
    • Можно встраиваться скриптом в разные фазы обработки запроса (rewrite, content, log и т.д.).

Ниже приведён не полный код (базовые функции), который был реализован для проксирования и складывания запросов в Kinesis:

http {
  lua_package_cpath "/usr/lib64/crypto.so;;";
  server {
    location /bx_stat {
      lua_need_request_body on;
      rewrite_by_lua ‘
        local data = ""
        if ngx.var.request_method == "POST" then
          data = ngx.req.get_body_data()
        else
          data = ngx.var.query_string;
          ngx.req.set_uri_args("");
        end
...
        local crypto = require "crypto"
        local kDate = crypto.hmac.digest("sha256", date_short, "AWS4" .. secret_key, true)
...
        ngx.req.set_header("x-amz-date", date_long)
        ngx.req.set_header("Authorization", "AWS4-HMAC-SHA256 Credential= ...
      ‘;
      proxy_set_header Host kinesis.eu-west-1.amazonaws.com;
      proxy_pass https://kinesis.eu-west-1.amazonaws.com/;

В коде подключается внешняя библиотека для реализации функции шифрования, отбираются параметры, которыми подписывается запрос, генерируется собственно авторизационный запрос в Kinesis, указывается куда дальше проксируется запрос (на амазоновский хост). Так можно избавится от бэкенда и всю логику реализовать на фронтенде.

Требуется при этом произвести дополнительные настройки на стороне NGINX'а:

worker_processes  4;
events {
    worker_connections  10240;
}

worker_rlimit_nofile 100000;

proxy_hide_header Access-Control-Expose-Headers;
proxy_hide_header x-amz-id-2;
proxy_hide_header x-amzn-RequestId;
proxy_hide_header Content-Type;

proxy_method POST;

Так как запросов много, то изменено число коннектов, обрабатываемых одним воркером, оцениваем сколько нам нужно будет открытых файлов и открытых соединений. Убраны заголовки из ответа, чтобы не было видно, что данные отправляются в Амазон. И установлено, что данные отправляются в Kinesis через POST, так как он принимает данные только так.

Так как реально тестирование началось с 1000 хитов в секунду, то пришлось настраивать параметры сети в операционной системе. Эти параметры по умолчанию в Linux стоят очень скромные, не ориентированные под высокую нагрузку, и почти всегда приходится их перенастраивать.

В описываемом случае очень быстро закончился диапазон портов исходящих соединений. С помощью файла /var/log/messages можно легко отследить что происходит с сетью и изменить параметры под потребности. Вот результат настроек, после которых всё заработало как это требовалось:

/etc/sysctl.conf  (man sysctl)
# диапазон портов исходящих коннектов
net.ipv4.ip_local_port_range=1024 65535
# повторное использование TIME_WAIT сокетов
net.ipv4.tcp_tw_reuse=1
# время пребывания сокета в FIN_WAIT_2
net.ipv4.tcp_fin_timeout=15
# размер таблиц файрволла
net.netfilter.nf_conntrack_max=1048576
# длина очереди входящих пакетов на интерфейсе
net.core.netdev_max_backlog=50000
# количество возможных подключений к сокету
net.core.somaxconn=81920
# не посылать syncookies на SYN запросы 
net.ipv4.tcp_syncookies=0

По результатам начальной работы потребовались дополнительные настройки: по логу Kinesis'а стало заметно, что всё ломается на 1000 хитов. Оказалось, что фронтенд со всем справлялся, но, несмотря на то, что увеличен диапазон исходящих портов, их всё равно не хватает. Помогло то, что Kinesis поддерживает keepalive соединения, и то, что NGINX тоже поддерживает эти соединения на бэкенде. Мы в proxy_pass прописали не один конкретный хост, а прописали upstream, в котором указали нужный хост, а через keepalive прописали сколько соединений держать открытыми и не закрывать.

upstream kinesis {
    server kinesis.eu-west-1.amazonaws.com:443 max_fails=0 fail_timeout=10s;
    keepalive 1024;
}

    proxy_pass https://kinesis/;

    proxy_http_version 1.1;
    proxy_set_header Connection "";

На изучение и разработку всего этого ушло не более недели в режиме не полной занятости.

На чём работает

Всё реально заработало на виртуальной машине с 2-я ядрами и 4-мя Гб оперативной памяти, совершенно спокойно обрабатывая нагрузку. Для локализации failover надо просто добавить вторую машину, что сделать очень просто, так как там нечему падать: там нет базы данных, нет нагрузки на диски. Можно также быстро, при необходимости отмасштабировать, поставив сколько нужно машин с балансировщиком.

Использование Lua в других решениях

Решение этой проблемы позволило решить и другие задачи, позволяющие отвязаться от структуры Амазона и увеличить одновременно, скорость доступа для клиентов Битрикс24. В сервисе всё работает хорошо, но RTT не ускорить, если сервера в Ирландии или в США, а клиент в России.

Решение было таким: поставить балансировщик в России, который по максимуму запросы клиентов обрабатывает сам, то есть отдаёт всю статику. Кроме того Композитный кеш тоже отдаётся с этого же балансировщика. Там же настроен SPDY и так далее. Часть из этого списка решалось стандартными средствами NGINX. Но вот сброс кеша по разным условиям не решался штатным функционалом в NGINX'е.

К этой задаче был применён всё тот же подход с использованием Lua. Можно удалять кеш прямо из файловой системы без всяких подзапросов или чего ещё либо. Задача решалась через header_filter_by_lua - удаление кэша высчитыванием пути к файлам и удалением через Lua os.remove В документации к модулю расписано как хранится кеш (задаётся глубина хранения, высчитывается MD5 от ключа, берётся последний символ, если это вложения 1, 2, 3 уровня). Все пути высчитываются, затем файлики удаляются. Всё сделано на нескольких строчках кода. Мы получил то что хотели:

Дополнительно

Есть библиотека Lua-resty, которая предоставляет интерфейсы для работы с:

  • memcached,
  • MySQL,
  • WebSocket,
  • Redis,
  • и другим.

То есть можно написать код, который непосредственно из NGINX'а будет соединяться с базой, выполнять запросы и получать ответы. Это можно применять в конфигурациях, в которых есть очень много условий.


4
Курсы разработаны в компании «1С-Битрикс»

Если вы нашли неточность в тексте, непонятное объяснение, пожалуйста, сообщите нам об этом в комментариях.
Развернуть комментарии