пятница, 28 октября 2011 г.

Простой модуль nginx для создания комбинированных апстримов

nginx - популярный и очень быстрый веб-сервер, который легко настраивается в качестве реверсного http-прокси. Это означает, что nginx может быть настроен таким образом, чтобы в соответствии с правилами маршрутизации, которые администратор прописывает в конфигурационных файлах, внешние http запросы направлялись в различные участки внутренней сети. Самый удобный способ определить такие участки внутри сети - это создать апстримы (upstreams), в которые будут входить один или несколько концептуально (или географически, или как-нибудь еще) связанных серверов.

Апстрим - одна из основных концепций nginx - это просто коллекция серверов, осуществляющих реальную обработку http-запросов, поступающих из внешней сети на вход nginx, работающего в данном контексте как http-прокси. Выбор конкретного сервера из апстрима для обработки очередного запроса определяется опциями, заданными для всех серверов апстрима внутри конфигурационного файла, чаще всего это простая round-robin модель.

К сожалению, внутри определения апстрима невозможно сослаться на другой апстрим, составив таким образом комбинацию апстримов. Однако было бы чрезвычайно удобно иметь такую возможность. Представьте, что мы определили апстрим u1 для серверов особого типа, находящихся в Москве, через некоторое время мы добавляем еще один набор серверов такого же типа u2, размещенных, например, во Владивостоке. А теперь представим, что наши правила маршрутизации требуют создания комбинированного апстрима ucombined, состоящего из всех серверов данного типа, находящихся в Москве и Владивостоке. Очевидное решение - включить данные из u1 и u2 в ucombined. Это можно сделать с помощью директивы include, но это очень неудобно, так как данные о серверах придется помещать в отдельные файлы, и, к тому же, что нам делать, если все сервера из Владивостока нужно пометить как backup? Остается единственный выход - продублировать объявления серверов в апстриме ucombined. А это уже совсем нехорошо - лишний копи-паст может легко стать источником ошибок, если в одной из его инстанций сделать изменения, а в других - забыть.

В представленном модуле к существующим директивам, доступным на уровне блока upstream, добавляется еще одна - add_upstream, которая включает в текущий апстрим серверы из другого, уже объявленного апстрима. Директива add_upstream - полноценный член сообщества директив для upstream, поэтому внутри блока upstream ее можно использовать наряду с любыми другими директивами, доступными на этом уровне. Вот пример, который мы будем тестировать ниже:
    upstream u1 {
        server localhost:8020;
    }
    upstream u2 {
        server localhost:8030;
    }
    upstream ucombined {
        server localhost:8030;
        add_upstream u1;
        add_upstream u2 backup;
    }
Апстрим ucombined включает серверы localhost:8020 и localhost:8030 c помощью директив add_upstream u1 и add_upstream u2 backup. Во втором случае используется единственная доступная для add_upstream опция backup, которая помечает все серверы из апстрима u2 как backup. Все опции (backup, weight и т.д.) серверов, переданных из u1 и u2 в ucombined, будут сохранены в ucombined. В данном примере в апстрим ucombined дополнительно, с помощью директивы server, включен сервер localhost:8030. Поскольку он также входит в апстрим u1, но при этом с помощью опции add_upstream backup помечается как backup, то общий вес его остается 1.

Теперь перейдем к описанию модуля с точки зрения программирования и сборки. Отличным пособием по созданию модулей nginx могут служить два ресурса от Эвана Миллера (здесь и здесь). Поэтому я не буду поднимать общие вопросы, а перейду сразу к деталям данного модуля.

Прежде всего обратимся к процедуре сборки модуля. Данный модуль состоит из одного файла-исходника на языке C, в котором определена всего одна функция, и вспомогательного файла config, который нужен nginx во время сборки. Да, это может показаться неудобным, но все модули nginx должны линковаться статически во время сборки самого nginx. Файл config содержит мета-информацию о модуле (имя, пути, дополнительные подключаемые библиотеки и т.д.). Вот его содержимое:
ngx_addon_name=ngx_http_upstream_add_upstream_module
HTTP_MODULES="$HTTP_MODULES $ngx_addon_name"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_upstream_add_upstream_module.c"
Название ngx_http_upstream_add_upstream_module выглядит неуклюжим, но зато говорит само за себя и соответствует правилу, принятому в nginx для именования модулей. Кроме того, из значения определения NGX_ADDON_SRCS видно, что имя нашего исходного файла ngx_http_upstream_add_upstream_module.c. Директория, в которой размещены файл config и исходник, может быть произвольной, она указывается в опции --add-module скрипта configure во время сборки nginx. Итак, нам понадобятся исходники nginx, которые можно взять на официальном сайте. Кроме того, в целях тестирования я добавлю замечательный модуль ngx_echo, который следует загрузить отдельно.

После загрузки и распаковки nginx и ngx_echo, конфигурируем nginx следующим образом:
./configure --add-module=/path/to/nginx_http_upstream_add_upstream_module --add-module=/path/to/ngx_echo
(вместо /path/to нужно подставить реальные пути к директориям модулей). Затем стандартная процедура make, make install и можно начинать тестирование. Но перед этим я хочу прокомментировать содержание исходного файла.

Как я уже отметил, модуль ngx_http_upstream_add_upstream_module очень простой и состоит всего из одной функции, в которой описаны действия nginx при чтении директивы add_upstream. Кроме этой единственной функции в файле ngx_http_upstream_add_upstream_module.c объявлены объекты, необходимые nginx при инициализации модуля. Вот они:
static ngx_command_t  ngx_http_upstream_add_upstream_commands[] = {

    { ngx_string("add_upstream"),
      NGX_HTTP_UPS_CONF|NGX_CONF_1MORE,
      ngx_http_upstream_add_upstream,
      0,
      0,
      NULL },

      ngx_null_command

};

static ngx_http_module_t  ngx_http_upstream_add_upstream_module_ctx = {
    NULL,                                  /* preconfiguration */
    NULL,                                  /* postconfiguration */

    NULL,                                  /* create main configuration */
    NULL,                                  /* init main configuration */

    NULL,                                  /* create server configuration */
    NULL,                                  /* merge server configuration */

    NULL,                                  /* create location configuration */
    NULL                                   /* merge location configuration */
};

ngx_module_t  ngx_http_upstream_add_upstream_module = {
    NGX_MODULE_V1,
    &ngx_http_upstream_add_upstream_module_ctx, /* module context */
    ngx_http_upstream_add_upstream_commands,    /* module directives */
    NGX_HTTP_MODULE,                            /* module type */
    NULL,                                       /* init master */
    NULL,                                       /* init module */
    NULL,                                       /* init process */
    NULL,                                       /* init thread */
    NULL,                                       /* exit thread */
    NULL,                                       /* exit process */
    NULL,                                       /* exit master */
    NGX_MODULE_V1_PADDING
};
В массиве ngx_http_upstream_add_upstream_commands определен список директив, которые предоставляет данный модуль. В нашем модуле определена единственная директива add_upstream, которая может быть использована внутри блока upstream (что отражено включением флага NGX_HTTP_UPS_CONF), и поддерживает один или более параметров (что отражено включением флага NGX_CONF_1MORE). Обработчиком директивы при чтении конфигурации является функция ngx_http_upstream_add_upstream() (та самая единственная функция модуля о которой мы говорили). Структура ngx_http_upstream_add_upstream_module_ctx описывает действия, которые nginx будет выполнять с конфигурациями модуля (или, другими словами, персистентными состояниями модуля) на различных этапах обращения к ним (создание, слияние (merge) конфигурации при переходе парсера с высокого уровня конфигурации (http или server) на уровень ниже (server или location) и т.п.). Наш модуль не имеет состояний (в самом деле, все данные, необходимые для наполнения блока апстрима серверами из другого апстрима с помощью директивы add_upstream доступны в момент чтения конфигурации парсером и не нуждаются в сохранении), поэтому все элементы структуры ngx_http_upstream_add_upstream_module_ctx пусты. И, наконец, в последней - главной структуре модуля - ngx_http_upstream_add_upstream_module, определены ссылки на только что заданный контекст, директивы и хуки модуля на различных этапах его существования (инициализация мастер-процесса, модуля, завершение работы мастер-процесса и т.д.). В нашем простом модуле все эти хуки тоже не нужны.

Ниже приводится текст функции ngx_http_upstream_add_upstream(), которая определяет действия nginx в момент чтения директивы add_upstream. Для удобства комментирования кода строки пронумерованы.
 86 static char *
 87 ngx_http_upstream_add_upstream(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
 88 {
 89     ngx_uint_t                      i, j;
 90     ngx_http_upstream_main_conf_t  *usmf;
 91     ngx_http_upstream_srv_conf_t   *uscf, **uscfp;
 92     ngx_http_upstream_server_t     *us;
 93     ngx_str_t                      *value;
 94     ngx_uint_t                      backup = 0;
 95 
 96     usmf = ngx_http_conf_get_module_main_conf(cf, ngx_http_upstream_module);
 97     uscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module);
 98     uscfp = usmf->upstreams.elts;
 99     value = cf->args->elts;
100 
101     if (cf->args->nelts > 3) {
102         ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
103                            "number of parameters must be 1 or 2");
104         return NGX_CONF_ERROR;
105     }
106 
107     if (value[1].len == uscf->host.len &&
108         ngx_strncasecmp(value[1].data, uscf->host.data, value[1].len) == 0) {
109         ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
110                            "upstream \"%V\" makes recursion", &value[1]);
111         return NGX_CONF_ERROR;
112     }
113 
114     if (cf->args->nelts == 3) {
115         if (ngx_strncmp(value[2].data, "backup", 6) == 0) {
116             backup = 1;
117             uscf->flags |= NGX_HTTP_UPSTREAM_BACKUP;
118         } else {
119             ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid parameter \"%V\"",
120                                &value[2]);
121             return NGX_CONF_ERROR;
122         }
123     }
124 
125     for (= 0; i < usmf->upstreams.nelts; i++) {
126         if (uscfp[i]->host.len == value[1].len &&
127             ngx_strncasecmp(uscfp[i]->host.data,
128                             value[1].data, value[1].len) == 0) {
129             if (uscf->servers == NULL) {
130                 uscf->servers = ngx_array_create(cf->pool, 4,
131                                             sizeof(ngx_http_upstream_server_t));
132                 if (uscf->servers == NULL) {
133                     return NGX_CONF_ERROR;
134                 }
135             }
136             us = ngx_array_push_n(uscf->servers, uscfp[i]->servers->nelts);
137             if (us == NULL) {
138                 return NGX_CONF_ERROR;
139             }
140             ngx_memcpy(us, uscfp[i]->servers->elts,
141                 sizeof(ngx_http_upstream_server_t) * uscfp[i]->servers->nelts);
142             uscf->flags |= uscfp[i]->flags;
143             if (backup) {
144                 for (= 0; j < uscfp[i]->servers->nelts; j++) {
145                     us[j].backup = 1;
146                 }
147             }
148             return NGX_CONF_OK;
149         }
150     }
151 
152     ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "upstream \"%V\" not found",
153                        &value[1]);
154     return NGX_CONF_ERROR;
155 }
В строках 89-99 определены объекты, с которыми данная функция будет работать. В частности usmf - указатель на главную конфигурацию модуля ngx_http_upstream_module, из которой нам понадобится получить список всех апстримов, прочитанных парсером на данный момент. Этот список хранится в элементе upstreams типа ngx_array_t. Тип ngx_array_t - это простой массив, который предоставляет информацию о количестве элементов в значении nelts, а сами элементы массива доступны через указатель elts. Для удобства определена переменная uscfp, которая представляет собой указатель на начало списка апстримов из usmf->upstreams. Переменная uscf инициализируется указателем на текущий апстрим. Переменная us будет инициализирована указателем на список серверов, который будет создан для добавления в текущий апстрим, если все пойдет хорошо. Переменная value - это указатель типа ngx_str_t, который инициализируется указателем на начало строки директивы. Так, если наша директива записывается как add_upstream u1;, то value[0] будет соответствовать строка add_upstream, а value[1] - строка u1. Тип ngx_str_t представляет собой быструю строку с двумя элементами - len типа size_t и data типа u_char*; понятно, что элемент len должен соответствовать длине строке, а указатель data - началу строки, при этом сама строка не обязана заканчиваться нулевым символом. Переменная backup понадобится для определения была ли задана одноименная опция директивы add_upstream.

Дальше все просто. В строках 101-112 проверяются недопустимые условия, в частности количество заданных опций и возможная недопустимая рекурсивность текущего апстрима. В строках 114-123 проверяется, что если опция директивы задана, то она может быть только backup.

В строках 125-150 происходит самое интересное. Проходим по всем апстримам, прочитанным парсером на данный момент и находим апстрим, название которого совпадает с первым параметром директивы (т.е. со значением value[1]). В случае совпадения в строках 129-135 проверяем, была ли выделена память под список серверов текущего апстрима (память уже выделена, если первой директивой в блоке апстрима была директива server, в противном случае, если это первая директива add_upstream в блоке - нет). Если память не была выделена, выделяем ее с помощью функции ngx_array_create(). Память выделяется в пуле и за ее освобождением проследит nginx.

В строках 136-141 добавляем необходимое количество элементов, равное количеству серверов, объявленных в апстриме, на который ссылается данная директива add_upstream, и копируем элементы-серверы из этого апстрима в выделенный участок. Очевидно, что все опции, установленные в оригинальных серверах, скопируются в новый апстрим. В строке 142 добавляем флаги апстрима, из которого были скопированы серверы, в текущий апстрим. В строках 143-147 добавляем ко всем серверам, скопированным из апстрима-источника флаг backup в том случае, если текущая директива add_upstream имеет опцию backup. В строке 148 успешно выходим. Если исполняемый поток по какой-то причине не достигнет строки 148, то в строках 152-154 будет выведено диагностическое сообщение о том, что апстрим, на который ссылается директива add_upstream не найден, а функция вернет значение NGX_CONF_ERROR. Фактически это будет означать, что конфигурационный файл содержит ошибки и nginx не запустится.

Это все. А теперь немного потестируем наш модуль. Для этого в конфигурационном файле создадим три виртуальных сервера с именами main, server1 и server2.
    server {
        listen       8010;
        server_name  main;
        location / {
            proxy_pass http://ucombined;
        }
    }
    server {
        listen       8020;
        server_name  server1;
        location / {
            echo "Passed to $server_name";
        }
    }
    server {
        listen       8030;
        server_name  server2;
        location / {
            echo "Passed to $server_name";
        }
    }
Сервер main при обращении на него должен перенаправлять запрос на серверы server1 и server2 в соответствии с правилами, определенными в комбинированном апстриме ucombined. В случае конфигурации, приведенной выше, сервера server1 и server2 будут иметь одинаковые веса, поскольку апстрим u2 в ucombined забекаплен, но при этом он все равно в игре за счет первой директивы server. Сервер server1 входит в ucombined за счет директивы add_upstream u1 и имеет тот же вес, что и u2. Соответственно последовательные запросы к серверу main должны проксироваться попеременно на server1 и server2. Запускаем nginx с указанной конфигурацией и проверяем (с помощью curl):
$ curl http://localhost:8010
Passed to server1
$ curl http://localhost:8010
Passed to server2
$ curl http://localhost:8010
Passed to server1
$ curl http://localhost:8010
Passed to server2
$ curl http://localhost:8010
Passed to server1
$ curl http://localhost:8010
Passed to server2
$
Отлично. Убираем опцию backup в директиве add_upstream u2. Теперь вес сервера server2 становится 2, а вес сервера server1 по-прежнему 1, соответственно server2 должен срабатывать последовательно 2 раза, а server1 - один раз. Перезапускаем nginx и проверяем:
$ curl http://localhost:8010
Passed to server2
$ curl http://localhost:8010
Passed to server1
$ curl http://localhost:8010
Passed to server2
$ curl http://localhost:8010
Passed to server2
$ curl http://localhost:8010
Passed to server1
$ curl http://localhost:8010
Passed to server2
$ curl http://localhost:8010
Passed to server2
$ curl http://localhost:8010
Passed to server1
$
Все работает правильно. Исходный код модуля, а также тестовый образец конфигурации можно взять здесь.

Update. Залил наработки из этой статьи на гитхаб. Теперь модуль называется nginx-combined-upstreams-module и кроме директивы add_upstreams в нем объявлена еще одна директива combine_server_singlets. Ее функции описаны в README.