Перейти к содержанию

redis2: NGINX upstream модуль для протокола Redis 2.0

Установка

Вы можете установить этот модуль в любой дистрибутив, основанный на RHEL, включая, но не ограничиваясь:

  • RedHat Enterprise Linux 7, 8, 9 и 10
  • CentOS 7, 8, 9
  • AlmaLinux 8, 9
  • Rocky Linux 8, 9
  • Amazon Linux 2 и Amazon Linux 2023
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install nginx-module-redis2
yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install nginx-module-redis2

Включите модуль, добавив следующее в верхнюю часть файла /etc/nginx/nginx.conf:

load_module modules/ngx_http_redis2_module.so;

Этот документ описывает nginx-module-redis2 v0.15, выпущенный 19 апреля 2018 года.


 location = /foo {
     set $value 'first';
     redis2_query set one $value;
     redis2_pass 127.0.0.1:6379;
 }

 # GET /get?key=some_key
 location = /get {
     set_unescape_uri $key $arg_key;  # это требует ngx_set_misc
     redis2_query get $key;
     redis2_pass foo.com:6379;
 }

 # GET /set?key=one&val=first%20value
 location = /set {
     set_unescape_uri $key $arg_key;  # это требует ngx_set_misc
     set_unescape_uri $val $arg_val;  # это требует ngx_set_misc
     redis2_query set $key $val;
     redis2_pass foo.com:6379;
 }

 # множественные конвейерные запросы
 location = /foo {
     set $value 'first';
     redis2_query set one $value;
     redis2_query get one;
     redis2_query set one two;
     redis2_query get one;
     redis2_pass 127.0.0.1:6379;
 }

 location = /bar {
     # $ здесь не специальный...
     redis2_literal_raw_query '*1\r\n$4\r\nping\r\n';
     redis2_pass 127.0.0.1:6379;
 }

 location = /bar {
     # переменные могут использоваться ниже, и $ специальный
     redis2_raw_query 'get one\r\n';
     redis2_pass 127.0.0.1:6379;
 }

 # GET /baz?get%20foo%0d%0a
 location = /baz {
     set_unescape_uri $query $query_string; # это требует модуля ngx_set_misc
     redis2_raw_query $query;
     redis2_pass 127.0.0.1:6379;
 }

 location = /init {
     redis2_query del key1;
     redis2_query lpush key1 C;
     redis2_query lpush key1 B;
     redis2_query lpush key1 A;
     redis2_pass 127.0.0.1:6379;
 }

 location = /get {
     redis2_query lrange key1 0 -1;
     redis2_pass 127.0.0.1:6379;
 }

Описание

Это модуль Nginx upstream, который позволяет nginx взаимодействовать с сервером Redis версии 2.x неблокирующим образом. Полный унифицированный протокол Redis 2.0 был реализован, включая поддержку конвейеризации Redis.

Этот модуль возвращает необработанный TCP-ответ от сервера Redis. Рекомендуется использовать мой lua-redis-parser (написанный на чистом C) для разбора этих ответов в структуру данных Lua при использовании вместе с lua-nginx-module.

При использовании вместе с lua-nginx-module рекомендуется использовать библиотеку lua-resty-redis вместо этого модуля, так как первая гораздо более гибкая и экономная по памяти.

Если вы хотите использовать только команду get Redis, вы можете попробовать HttpRedisModule. Он возвращает разобранную часть содержимого ответа Redis, так как только get необходим для реализации.

Еще один вариант - разобрать ответы Redis на стороне клиента самостоятельно.

Директивы

redis2_query

синтаксис: redis2_query cmd arg1 arg2 ...

по умолчанию: нет

контекст: location, location if

Укажите команду Redis, указав ее отдельные аргументы (включая само имя команды Redis) аналогично утилите redis-cli.

Разрешено несколько экземпляров этой директивы в одном location, и эти запросы будут конвейеризованы. Например,

 location = /pipelined {
     redis2_query set hello world;
     redis2_query get hello;

     redis2_pass 127.0.0.1:$TEST_NGINX_REDIS_PORT;
 }

Тогда GET /pipelined даст два последовательных необработанных ответа Redis

 +OK
 $5
 world

где символы новой строки на самом деле являются CR LF (\r\n).

redis2_raw_query

синтаксис: redis2_raw_query QUERY

по умолчанию: нет

контекст: location, location if

Укажите необработанные запросы Redis, и переменные nginx распознаются в аргументе QUERY.

В аргументе QUERY разрешена только одна команда Redis, иначе вы получите ошибку. Если вы хотите указать несколько конвейерных команд в одном запросе, используйте директиву redis2_raw_queries.

redis2_raw_queries

синтаксис: redis2_raw_queries N QUERIES

по умолчанию: нет

контекст: location, location if

Укажите N команд в аргументе QUERIES. Оба аргумента N и QUERIES могут принимать переменные Nginx.

Вот несколько примеров

 location = /pipelined {
     redis2_raw_queries 3 "flushall\r\nget key1\r\nget key2\r\n";
     redis2_pass 127.0.0.1:6379;
 }

 # GET /pipelined2?n=2&cmds=flushall%0D%0Aget%20key%0D%0A
 location = /pipelined2 {
     set_unescape_uri $n $arg_n;
     set_unescape_uri $cmds $arg_cmds;

     redis2_raw_queries $n $cmds;

     redis2_pass 127.0.0.1:6379;
 }
Обратите внимание, что во втором примере выше директива set_unescape_uri предоставляется модулем set-misc-nginx-module.

redis2_literal_raw_query

синтаксис: redis2_literal_raw_query QUERY

по умолчанию: нет

контекст: location, location if

Укажите необработанный запрос Redis, но переменные Nginx в нем не будут распознаны. Другими словами, вы можете свободно использовать символ доллара ($) в аргументе QUERY.

В аргументе QUERY разрешена только одна команда Redis.

redis2_pass

синтаксис: redis2_pass <upstream_name>

синтаксис: redis2_pass <host>:<port>

по умолчанию: нет

контекст: location, location if

фаза: content

Укажите серверный бэкенд Redis.

redis2_connect_timeout

синтаксис: redis2_connect_timeout <time>

по умолчанию: 60s

контекст: http, server, location

Тайм-аут для подключения к серверу Redis, по умолчанию в секундах.

Рекомендуется всегда явно указывать единицу времени, чтобы избежать путаницы. Поддерживаемые единицы времени: s(секунды), ms(миллисекунды), y(годы), M(месяцы), w(недели), d(дни), h(часы) и m(минуты).

Это время должно быть меньше 597 часов.

redis2_send_timeout

синтаксис: redis2_send_timeout <time>

по умолчанию: 60s

контекст: http, server, location

Тайм-аут для отправки TCP-запросов на сервер Redis, по умолчанию в секундах.

Рекомендуется всегда явно указывать единицу времени, чтобы избежать путаницы. Поддерживаемые единицы времени: s(секунды), ms(миллисекунды), y(годы), M(месяцы), w(недели), d(дни), h(часы) и m(минуты).

redis2_read_timeout

синтаксис: redis2_read_timeout <time>

по умолчанию: 60s

контекст: http, server, location

Тайм-аут для чтения TCP-ответов от сервера Redis, по умолчанию в секундах.

Рекомендуется всегда явно указывать единицу времени, чтобы избежать путаницы. Поддерживаемые единицы времени: s(секунды), ms(миллисекунды), y(годы), M(месяцы), w(недели), d(дни), h(часы) и m(минуты).

redis2_buffer_size

синтаксис: redis2_buffer_size <size>

по умолчанию: 4k/8k

контекст: http, server, location

Этот размер буфера используется для чтения ответов Redis, но не обязательно должен быть таким большим, как самый большой возможный ответ Redis.

Этот размер по умолчанию соответствует размеру страницы, может быть 4k или 8k.

redis2_next_upstream

синтаксис: redis2_next_upstream [ error | timeout | invalid_response | off ]

по умолчанию: error timeout

контекст: http, server, location

Укажите, какие условия сбоя должны привести к перенаправлению запроса на другой upstream сервер. Применяется только тогда, когда значение в redis2_pass является upstream с двумя или более серверами.

Вот искусственный пример:

 upstream redis_cluster {
     server 127.0.0.1:6379;
     server 127.0.0.1:6380;
 }

 server {
     location = /redis {
         redis2_next_upstream error timeout invalid_response;
         redis2_query get foo;
         redis2_pass redis_cluster;
     }
 }

Пул соединений

Вы можете использовать отличный HttpUpstreamKeepaliveModule с этим модулем для обеспечения пула TCP-соединений для Redis.

Пример конфигурации выглядит следующим образом

 http {
     upstream backend {
       server 127.0.0.1:6379;

       # пул с максимум 1024 соединениями
       # и не различает серверы:
       keepalive 1024;
     }

     server {
         ...
         location = /redis {
             set_unescape_uri $query $arg_query;
             redis2_query $query;
             redis2_pass backend;
         }
     }
 }

Выбор баз данных Redis

Redis предоставляет команду select для переключения баз данных Redis. Эта команда не отличается от других обычных команд, таких как get или set. Поэтому вы можете использовать их в директивах redis2_query, например,

redis2_query select 8;
redis2_query get foo;

Совместимость с Lua

Этот модуль может служить неблокирующим клиентом redis2 для lua-nginx-module (но в настоящее время рекомендуется использовать библиотеку lua-resty-redis, которая гораздо проще в использовании и более эффективна в большинстве случаев). Вот пример использования подзапроса GET:

 location = /redis {
     internal;

     # set_unescape_uri предоставляется ngx_set_misc
     set_unescape_uri $query $arg_query;

     redis2_raw_query $query;
     redis2_pass 127.0.0.1:6379;
 }

 location = /main {
     content_by_lua '
         local res = ngx.location.capture("/redis",
             { args = { query = "ping\\r\\n" } }
         )
         ngx.print("[" .. res.body .. "]")
     ';
 }

Тогда доступ к /main даст

[+PONG\r\n]

где \r\n это CRLF. То есть, этот модуль возвращает необработанные TCP-ответы от удаленного сервера Redis. Для разработчиков приложений на Lua они могут захотеть использовать библиотеку lua-redis-parser для разбора таких необработанных ответов в структуры данных Lua.

При перемещении встроенного кода Lua в внешний файл .lua важно использовать последовательность экранирования \r\n напрямую. Мы использовали \\r\\n выше только потому, что код Lua сам нуждается в кавычках, когда помещается в строковый литерал Nginx.

Вы также можете использовать подзапросы POST/PUT для передачи необработанного запроса Redis через тело запроса, что не требует экранирования и декодирования URI, тем самым экономя некоторые циклы ЦП. Вот такой пример:

 location = /redis {
     internal;

     # $echo_request_body предоставляется модулем ngx_echo
     redis2_raw_query $echo_request_body;

     redis2_pass 127.0.0.1:6379;
 }

 location = /main {
     content_by_lua '
         local res = ngx.location.capture("/redis",
             { method = ngx.HTTP_PUT,
               body = "ping\\r\\n" }
         )
         ngx.print("[" .. res.body .. "]")
     ';
 }

Это даст точно такой же вывод, как и предыдущий (GET) пример.

Также можно использовать Lua для выбора конкретного бэкенда Redis на основе некоторых сложных правил хеширования. Например,

 upstream redis-a {
     server foo.bar.com:6379;
 }

 upstream redis-b {
     server bar.baz.com:6379;
 }

 upstream redis-c {
     server blah.blah.org:6379;
 }

 server {
     ...

     location = /redis {
         set_unescape_uri $query $arg_query;
         redis2_query $query;
         redis2_pass $arg_backend;
     }

     location = /foo {
         content_by_lua "
             -- выбрать сервер случайным образом
             local servers = {'redis-a', 'redis-b', 'redis-c'}
             local i = ngx.time() % #servers + 1;
             local srv = servers[i]

             local res = ngx.location.capture('/redis',
                 { args = {
                     query = '...',
                     backend = srv
                   }
                 }
             )
             ngx.say(res.body)
         ";
     }
 }

Конвейерные запросы Redis с помощью Lua

Вот полный пример, демонстрирующий, как использовать Lua для выполнения нескольких конвейерных запросов Redis через этот модуль Nginx.

Прежде всего, мы включаем следующее в наш файл nginx.conf:

 location = /redis2 {
     internal;

     redis2_raw_queries $args $echo_request_body;
     redis2_pass 127.0.0.1:6379;
 }

 location = /test {
     content_by_lua_file conf/test.lua;
 }

В основном мы используем аргументы запроса URI для передачи количества запросов Redis и тело запроса для передачи строки конвейерного запроса Redis.

Затем мы создаем файл conf/test.lua (путь которого относителен к корню сервера Nginx), чтобы включить следующий код Lua:

 -- conf/test.lua
 local parser = require "redis.parser"

 local reqs = {
     {"set", "foo", "hello world"},
     {"get", "foo"}
 }

 local raw_reqs = {}
 for i, req in ipairs(reqs) do
     table.insert(raw_reqs, parser.build_query(req))
 end

 local res = ngx.location.capture("/redis2?" .. #reqs,
     { body = table.concat(raw_reqs, "") })

 if res.status ~= 200 or not res.body then
     ngx.log(ngx.ERR, "failed to query redis")
     ngx.exit(500)
 end

 local replies = parser.parse_replies(res.body, #reqs)
 for i, reply in ipairs(replies) do
     ngx.say(reply[1])
 end

Здесь мы предполагаем, что ваш сервер Redis слушает на порту по умолчанию (6379) на localhost. Мы также используем библиотеку lua-redis-parser для построения необработанных запросов Redis и также используем ее для разбора ответов.

Доступ к расположению /test через HTTP-клиенты, такие как curl, дает следующий вывод

OK
hello world

Более реалистичная настройка - использовать правильное определение upstream для нашего бэкенда Redis и включить пул TCP-соединений через директиву keepalive в нем.

Поддержка публикации/подписки Redis

Этот модуль имеет ограниченную поддержку функции публикации/подписки Redis. Полная поддержка невозможна из-за безсостояния REST и модели HTTP.

Рассмотрим следующий пример:

 location = /redis {
     redis2_raw_queries 2 "subscribe /foo/bar\r\n";
     redis2_pass 127.0.0.1:6379;
 }

Затем опубликуйте сообщение для ключа /foo/bar в командной строке redis-cli. И тогда вы получите два многоблочных ответа от расположения /redis.

Вы, конечно, можете разобрать ответы с помощью библиотеки lua-redis-parser, если вы используете Lua для доступа к этому расположению модуля.

Ограничения для публикации/подписки Redis

Если вы хотите использовать функцию Redis pub/sub с этим модулем, то вы должны учитывать следующие ограничения:

  • Вы не можете использовать HttpUpstreamKeepaliveModule с этим upstream Redis. Работают только короткие соединения Redis.
  • Могут возникнуть некоторые гонки, которые создают безвредные предупреждения Redis server returned extra bytes в вашем error.log Nginx. Такие предупреждения могут быть редкими, но будьте готовы к этому.
  • Вам следует настроить различные настройки тайм-аутов, предоставляемые этим модулем, такие как redis2_connect_timeout и redis2_read_timeout.

Если вы не можете смириться с этими ограничениями, то вам настоятельно рекомендуется перейти на библиотеку lua-resty-redis для lua-nginx-module.

Настройка производительности

  • Когда вы используете этот модуль, пожалуйста, убедитесь, что вы используете пул TCP-соединений (предоставляемый HttpUpstreamKeepaliveModule) и конвейеризацию Redis, где это возможно. Эти функции значительно улучшат производительность.
  • Использование нескольких экземпляров серверов Redis на ваших многопроцессорных машинах также сильно помогает из-за последовательной обработки одного экземпляра сервера Redis.
  • Когда вы проводите тестирование производительности с помощью чего-то вроде ab или http_load, пожалуйста, убедитесь, что уровень логирования ошибок достаточно высок (например, warn), чтобы предотвратить слишком большое количество циклов Nginx workers на очистку файла error.log, который всегда небуферизованный и блокирующий, а значит, очень затратный.

ССЫЛКИ

GitHub

Вы можете найти дополнительные советы по конфигурации и документацию для этого модуля в репозитории GitHub для nginx-module-redis2.