Показаны сообщения с ярлыком nginx module. Показать все сообщения
Показаны сообщения с ярлыком nginx module. Показать все сообщения

понедельник, 22 августа 2016 г.

Универсальные счетчики в nginx: замечания к реализации модуля

Не так давно реализовал универсальные счетчики для nginx в виде модуля. По задумке счетчики могут быть объявлены как в серверной части конфигурации, так и в отдельных локейшнах внутри сервера. Они должны разделяться всеми рабочими процессами, а также, опционально, наборами отдельных виртуальных серверов — это для того, чтобы их можно было опрашивать из отдельного, выделенного сервера. Вот как это может выглядеть на практике.
http {
    server {
        listen       8010;
        server_name  main monitored;

        counter $cnt_all_requests inc;

        location /test {
            counter $cnt_test_requests inc;
            if ($arg_a) {
                counter $cnt_test_a_requests inc;
                echo "/test?a=$arg_a";
                break;
            }
            return 200;
        }
    }

    server {
        listen       8020;
        server_name  monitor monitored;

        allow 127.0.0.1;
        deny  all;

        location / {
            echo -n "all = $cnt_all_requests";
            echo -n " | /test = $cnt_test_requests";
            echo    " | /test?a = $cnt_test_a_requests";
        }
    }
}
Здесь объявлены два виртуальных сервера main и monitor, слушающие на разных портах 8010 и 8020. Второй сервер нужен только для опроса счетчиков, объявленных в первом сервере, поэтому доступ к нему ограничен директивами allow и deny. В первом сервере объявлены три счетчика: cnt_all_requests, cnt_test_requests и cnt_test_a_requests. Первый счетчик должен увеличиваться на единицу (операция inc со значением по умолчанию 1) при любом запросе к серверу main, второй — при попадании в локейшн /test, третий – при попадании в этот же локейшн, но с условием, что параметр a присутствует в URI запроса. Обратите внимание, что оба сервера имеют одинаковое последнее имя monitored — это и есть тот самый способ объявить набор счетчиков, разделяемый несколькими виртуальными серверами (для этой же цели в модуле имеется отдельная директива counter_set_id). Давайте подумаем, как можно было бы реализовать счетчики из этого примера. Во-первых, все рабочие процессы должны разделять одни и те же значения счетчиков, а это значит, что они должны храниться в разделяемой памяти. А как сделать так, чтобы при попадании в локейшн /test счетчики cnt_all_requests и cnt_test_requests увеличивались бы на единицу, а третий счетчик cnt_test_a_requests делал бы это только в том случае, если в URI присутствует параметр a? Для ответа на этот вопрос обратите внимание на выделенное мною в нем слово попадание. В самом деле, попадание запроса в определенный локейшн есть результат большой работы, проделанной nginx на этапе чтения конфигурации, а именно создания и слияния (merge) конфигураций уровня локейшнов (location configuration). При получении запроса nginx сопоставляет его URI с метками всех конфигураций уровня локейшнов и выбирает наиболее подходящую. С помощью механизма слияния конфигураций мы сможем протолкнуть верхнеуровневые конфигурации со счетчиками cnt_all_requests и cnt_test_requests вниз в целевые конфигурации вплоть до уровней if в локейшнах. Таким образом, мы будем хранить ссылки на метаинформацию о счетчиках (ссылку на значение в разделяемой памяти, которое нужно будет изменить, а также операцию — inc или set и соответствующее ей значение) в кофигурации уровня локейшнов нашего модуля. И не стоит удивляться тому, что счетчик, объявленный на уровне server выше всяких локейшнов имеет собственную конфигурацию уровня локейшнов — просто во время чтения конфигурации nginx сольет эту конфигурацию со всеми другими, объявленными на уровне локейшнов и внутри всех if во всех локейшнах. Правила слияния мы запрограммируем сами, гарантировав, что верхнеуровневые счетчики попадут во все конфигурации ниже. А теперь представьте такой фрагмент конфигурации.
        counter $cnt_requests_1 inc;

        location /test {
            counter $cnt_requests_1 inc;
            if ($arg_a) {
                counter $cnt_requests_1 inc -1;
            }
            return 200;
        }
Здесь мы хотим, чтобы при попадании запроса в локейшн /test счетчик cnt_requests_1 увеличивался на 1, а во все другие локейшны — на 2. Вот таким хитроумным способом, эксплуатируя механизм слияния конфигураций уровня локейшнов, мы можем добиться этого результата. Но… Когда мы говорили о слиянии конфигураций, мы имели ввиду простое добавление разных счетчиков сверху вниз. В данном же случае нам придется не просто добавлять новый счетчик, но каким-то образом сливать метаинформацию одного и того же счетчика в нижней конфигурации. К метаинформации счетчика относятся ссылка на элемент в разделяемой памяти (которая не изменится при слиянии), а также операция и ее значение. В общем, нам нужно изменить операцию и/или значение счетчика на нижнем уровне в зависимости от их значений у счетчика на верхнем уровне. И это не сложно. Если операция нижнего счетчика равна set, то метаинформация на нижнем счетчике не меняется, иначе, если операция верхнего счетчика равна set, то операцией нижнего счетчика становится set, а ее значением является сумма значений операций верхнего и нижнего счетчиков, иначе операция нижнего счетчика не изменяется, а ее значением становится сумма значений операций верхнего и нижнего счетчиков. Внимание, вопрос. Когда мы будем обновлять значения счетчиков в разделяемой памяти? Простой вопрос? Как это ни удивительно, но ответ отрицательный. Поскольку мы опираемся на метаинформацию о счетчиках, которая привязана к конфигурации уровня локейшнов, во время обновления данных счетчиков в разделяемой памяти в метаданных запроса nginx (это указатель на объект типа ngx_http_request_t) должна находится правильная ссылка на конфигурацию уровня локейшнов нашего модуля. Запрос nginx проживает насыщенную событиями жизнь, состоящую из нескольких фаз. В течение всей жизни ссылки на конфигурации уровня локейшнов могут меняться несколько раз. Наибольший вклад в этот процесс изменения конфигураций вносит модуль nginx rewrite с его директивами rewrite, if и return. Директивы модуля rewrite действуют на ранних фазах NGX_HTTP_SERVER_REWRITE_PHASE (только те, которые объявлены на уровне серверов, однако в этом случае они не изменяют локейшны) и NGX_HTTP_REWRITE_PHASE (директивы, объявленные на уровне локейшнов). Более того, директива return может убить запрос на одной из этих фаз. Мы могли бы объявить хэндлер, который обновлял бы значения счетчиков на одной из поздних фаз (см. подробную информацию о фазах nginx здесь). Если бы не return… Если объявить наш хэндлер на самой ранней из доступных фаз (NGX_HTTP_REWRITE_PHASE), до запуска хэндлера модуля rewrite, то ссылка на конфигурацию уровня локейшнов может оказаться неверной, поскольку директивы модуля rewrite могут изменить локейшн. Ввиду изложенных обстоятельств я решил поместить хэндлер для обновления счетчиков в фильтр заголовков ответа (response headers filter): он должен работать всегда вне зависимости от наличия или отсутствия директив модуля rewrite и изменять счетчики, связанные с последним переписанным локейшном. Ну а что, если мы хотим регистрировать входные локейшны, которые связаны с запросом до любых изменений локейшнов директивами модуля rewrite? Для этого случая я добавил так называемые ранние счетчики, хэндлеры которых вызываются в фазе NGX_HTTP_REWRITE_PHASE. Вот пример.
        location /test/rewrite {
            early_counter $ecnt_test_requests inc;
            rewrite ^ /test/0;
        }

        location /test/0 {
            early_counter $ecnt_test0_requests inc;
            rewrite ^ /test;
        }
Хэндлер для раннего счетчика ecnt_test_requests в локейшне /test/rewrite будет вызван до директивы rewrite, поэтому данный счетчик увеличится на единицу до любых rewrite и if уровня локейшнов, но это при условии, что локейшн /test/rewrite был входным. Так, в случае запроса на /test/rewrite, счетчик ecnt_test0_requests в промежуточном локейшне /test/0 не изменится — нет никакого способа получить доступ к промежуточным конфигурациям уровня локейшнов во время последовательных перезаписей локейшнов модулем rewrite. Объявления ранних счетчиков разрешены только на уровне локейшнов, поскольку на уровнях серверов, а тем более в if внутри локейшнов они не имеют смысла. Один и тот же счетчик может быть объявлен как обычный и как ранний при условии, что процедура слияния конфигураций уровня локейшнов не выявит разночтений ни в одном случае — флаг признака раннего счетчика хранится в метаинформации счетчика наряду с операцией и значением и не должен изменяться при слиянии счетчиков. И, наконец, к вопросу о реализации разделения счетчиков между виртуальными серверами. Помимо конфигурации уровня локейшнов, nginx позволяет модулям использовать конфигурации уровня серверов (server configuration) и основную конфигурацию (main configuration). Для организации общего доступа к единому набору счетчиков из разных серверов, можно организовать массив таких наборов в основной конфигурации, а в конфигурациях уровня серверов разместить ссылку на определенный элемент из этого массива, и только в том случае, если данный сервер вообще ссылается на какие-либо счетчики. В качестве меток элементов массива наборов счетчиков в основной конфигурации подойдет любая строковая настройка уровня сервера, которая может разделяться разными виртуальными серверами, например последнее имя сервера или специально созданная для этого директива. Чего здесь не хватает? Представьте, что мы хотим увеличивать счетчик когда приходит запрос POST.
http {
    server {
        listen       8010;
        server_name  main monitored;

        if ($request_method = POST) {
            counter $cnt_post_requests inc;
        }
}
Соблазнительно. Но если бы объявления счетчиков и были разрешены внутри серверных if (а это не так), то это все равно не имело бы никакого смысла, поскольку конфигурации уровня локейшнов не доступны внутри серверных if (см. также мою статью о практической бесполезности серверных if для разработки модулей в таких случаях). Это можно исправить с помощью дополнительной переменной.
http {
    server {
        listen       8010;
        server_name  main monitored;

        if ($request_method = POST) {
            set $inc_post_requests 1;
        }

        counter $cnt_post_requests inc $inc_post_requests;
}
А вспомните предыдущий пример со стеком перенаправлений rewrite, в котором невозможно установить счетчик внутри промежуточных локейшнов, таких как /test/0. Если бы операция счетчика могла ссылаться на переменную, мы могли бы устанавливать значение переменной внутри промежуточного локейшна с помощью директивы set, а объявление счетчика перенести на уровень выше, чтобы оно наследовалось в окончательном локейшне после всех перенаправлений rewrite.
        location /test/rewrite {
            early_counter $ecnt_test_requests inc;
            rewrite ^ /test/0 last;
        }

        counter $cnt_test0_requests inc $inc_test0_requests;

        location /test/0 {
            set $inc_test0_requests 1;
            rewrite ^ /test last;
        }
Ссылка на переменную в качестве значения операции счетчика также полезна при задании сложных условий, с которыми директива if справиться не в состоянии несмотря на богатые возможности языка регулярных выражений (см. мои статьи на эту тему здесь и здесь). Вычисление сложного условия можно произвести в коде универсального высокоуровневого языка, такого как Javascript или Perl, когда переменная-значение операции счетчика связана с соответствующим хэндлером, ссылающимся на этот код. Ввиду богатых перспектив такого подхода, ссылка на переменную-значение операции разрешена в объявлении счетчиков. Это немного усложняет вычисление значения операции, поскольку теперь это значение может вычисляться не только в процедуре слияния счетчиков во время чтения конфигурации, но и во время обработки запроса (назовем это время рантаймом, а переменные-значения рантайм-переменными). Приведу пример. Давайте увеличивать счетчик в случае, если некоторое base64-закодированное значение (например, куки Misc), содержит тэг версии типа v=1. Я буду использовать код на языке Haskell из соответствующего модуля nginx. Вот полная конфигурация.
user                    nobody;
worker_processes        4;

events {
    worker_connections  1024;
}

error_log               /tmp/nginx-test-custom-counters-error.log warn;

http {
    default_type        application/octet-stream;
    sendfile            on;

    access_log          /tmp/nginx-test-custom-counters-access.log;

    # uncomment next line on ghc ambiguous interface error
    #haskell ghc_extra_flags '-hide-package regex-pcre';

    haskell compile standalone /tmp/ngx_haskell.hs '

import Data.ByteString.Base64
import Data.Maybe
import Text.Regex.PCRE

hasVTag = either (const False) (isJust . matchOnce r) . decode
    where r = makeRegex "\\\\bv=\\\\d+\\\\b" :: Regex

NGX_EXPORT_B_Y (hasVTag)

    ';

    server {
        listen          8010;
        server_name     main monitored;

        haskell_run hasVTag $hs_inc_cnt_vtag $cookie_misc;
        counter $cnt_cookie_misc_vtag inc $hs_inc_cnt_vtag;

        location / {
            return 200;
        }
    }

    server {
        listen          8020;
        server_name     monitor;
        counter_set_id  monitored;

        allow 127.0.0.1;
        deny  all;

        location / {
            echo "vtag_reqs = $cnt_cookie_misc_vtag";
        }
    }
}
Обратите внимание на то, что в регулярном выражении, переданном в функцию makeRegex, вместо одного обратного слэша используются четыре. Это связано с тем, что как строки Haskell, так и значения в директивах nginx требуют двойного обратного слэша для интерполяции в одинарный. Протестируем.
curl 'http://127.0.0.1:8020/'
vtag_reqs = 0
curl -b 'Misc=bj0yO3Y9MQ==' 'http://127.0.0.1:8010/'
curl 'http://127.0.0.1:8020/'
vtag_reqs = 1
curl -b 'Misc=bj0yO3Y9MQ=' 'http://127.0.0.1:8010/'
curl 'http://127.0.0.1:8020/'
vtag_reqs = 1
Значение bj0yO3Y9MQ== — это base64-закодированная строка n=2;v=1, то же самое значение без последнего символа = — просто сломанная base64-закодированная строка. Так что работает верно. Перейдем к вопросу о реализации разделяемой памяти для счетчиков. В модуле использован стандартный интерфейс nginx (детали его реализации можно найти в этом прекрасном руководстве), включающий вызов функции ngx_shared_memory_add() с размером выделяемой памяти, равным двум страницам (т.е. 2 раза по 4096 байт в Linux). Это минимальный размер сегмента, необходимый слабовому аллокатору ngx_slab_allocator. Каждый набор счетчиков, связанный с одним виртуальным сервером или группой, получает собственный сегмент памяти. Счетчики хранятся в массиве, поскольку на этапе инициализации разделяемой памяти есть возможность подсчитать их общий размер. Каждый элемент массива имеет размер 8 байт на 64-битной машине, первый элемент хранит общее количество счетчиков — эта информация используется при перезагрузке конфигурации, когда nginx получает сигнал HUP, для возможности восстановления старых значений счетчиков. Если слабовый аллокатор использует целую страницу памяти для своих нужд (я пока так и не выяснил этого), то остается целых 4092 байта на счетчики — этого хватит более чем на 500 элементов на один набор. Старые значения счетчиков восстанавливаются после перезагрузки конфигурации, если директива-флаг counters_survive_reload установлена в значение on на уровне сервера или основной конфигурации. Значения счетчиков не восстанавливаются, если их общее количество в наборе изменилось после перезагрузки. Изменение порядка объявления счетчиков в определенном наборе после перезагрузки приведет к тому, что они подхватят значения счетчиков, которые до этого были объявлены на этих позициях. Я не буду приводить исходный код модуля и объяснять значения отдельных строк — надеюсь, что приведенная здесь информация поможет разобраться в нем самостоятельно. Отмечу лишь, что основная, серверная и локейшн-конфигурации модуля соответствуют типам ngx_http_cnt_main_conf_t, ngx_http_cnt_srv_conf_t и ngx_http_cnt_loc_conf_t соответственно, метаинформация счетчиков описана в типе ngx_http_cnt_data_t, а информация, связанная с набором счетчиков — в типе ngx_http_cnt_set_t. В функции ngx_http_cnt_init() происходит инициализация хэндлера фазы NGX_HTTP_REWRITE_PHASE и фильтра заголовков ответа — обе функции вызывают одну и ту же рабочую функцию ngx_http_cnt_update(), в которой происходит изменение значений счетчиков в разделяемой памяти. Функция ngx_http_cnt_get_value() является хэндлером переменной-счетчика и осуществляет доступ к значению в разделяемой памяти.

воскресенье, 27 декабря 2015 г.

nginx module to enable haskell binding to nginx configuration files

Do you like haskell and nginx? I love them both and this inspired me to write an nginx module for inlining haskell source code straight into nginx configuration files. The module was published on github as nginx-haskell-module and shipped with an nginx configuration example to show its basic usage. Let’s look at it.
user                    nobody;
worker_processes        2;

events {
    worker_connections  1024;
}

http {
    default_type        application/octet-stream;
    sendfile            on;

    haskell ghc_extra_flags '-hide-package regex-pcre';

    haskell compile /tmp/ngx_haskell.hs '

import qualified Data.Char as C
import           Text.Regex.PCRE
import           Safe

toUpper = map C.toUpper
NGX_EXPORT_S_S (toUpper)

takeN x y = take (readDef 0 y) x
NGX_EXPORT_S_SS (takeN)

NGX_EXPORT_S_S (reverse)

matches :: String -> String -> Bool
matches a b = not $ null (getAllTextMatches $ a =~ b :: [String])
NGX_EXPORT_B_SS (matches)

    ';

    server {
        listen       8010;
        server_name  main;
        error_log    /tmp/nginx-test-haskell-error.log;
        access_log   /tmp/nginx-test-haskell-access.log;

        location / {
            haskell_run toUpper $hs_a $arg_a;
            echo "toUpper ($arg_a) = $hs_a";
            if ($arg_b) {
                haskell_run takeN $hs_a $arg_a $arg_b;
                echo "takeN ($arg_a, $arg_b) = $hs_a";
                break;
            }
            if ($arg_c) {
                haskell_run reverse $hs_a $arg_c;
                echo "reverse ($arg_c) = $hs_a";
                break;
            }
            if ($arg_d) {
                haskell_run matches $hs_a $arg_d $arg_a;
                echo "matches ($arg_d, $arg_a) = $hs_a";
                break;
            }
        }
    }
}
Haskell source code is placed inside the second argument of the directive haskell compile. In this example it contains some imports, three definitions of functions and four special export directives to introduce the functions on the nginx configuration level. There are four types of export directives: NGX_EXPORT_S_S, NGX_EXPORT_S_SS, NGX_EXPORT_B_S and NGX_EXPORT_B_SS for functions of types String -> String, String -> String -> String, String -> Bool and String -> String -> Bool respectively. The code gets written into the path specified in the first argument of the directive haskell compile (it must be an absolute path) and then compiled to a shared library at the very start of nginx. Sometimes ghc may require extra options besides defaults. And here is the case. As soon as import of Text.Regex.PCRE can be ambiguous (because two haskell packages regex-pcre and regex-pcre-builtin provide it), ghc must know which package to use. There is a special ghc flag -hide-package for hiding unwanted packages and it was used in this example by the directive haskell ghc_extra_flags. There is another nginx haskell directive haskell load which is similar to the haskell compile except it does not require the second argument (i.e. the haskell source code). The directive tries to load compiled shared library that corresponds to the path specified in its first argument (/tmp/ngx_haskell.so in this example). If the code argument is present but there is not compiled shared library, the latter will be first compiled and then loaded. If the haskell code fails to compile then nginx won’t start (or won’t reload workers if the code had been wrongly changed in the configuration file and nginx has been sent the SIGHUP signal). Any errors will be logged. To run the compiled haskell code in the nginx context there is another nginx directive haskell_run. It is allowed in server, location and location-if configuration clauses and may accept three or four arguments depending on the arity of the exported function to run which is specified in the first argument of the directive. The second argument introduces an nginx variable where return value of the haskell function will be saved. For example directive
                haskell_run takeN $hs_a $arg_a $arg_b;
introduces a new nginx variable $hs_a which will be calculated on demand as result of running an exported haskell function takeN with arguments $arg_a and $arg_b. Let’s do some curl tests. First of all nginx must be built and run. Besides the haskell module the build requires nginx echo module. It must be specified in options --add-module of nginx configure script.
./configure --add-module=<path-to-nginx-echo-module> --add-module=<path-to-nginx-haskell-module>
Placeholders <path-to-nginx-... are to be replaced with real paths to the modules. After running make we start the nginx daemon.
./objs/nginx -c /home/lyokha/devel/nginx-haskell-module/nginx.conf
[1 of 1] Compiling NgxHaskellUserRuntime ( /tmp/ngx_haskell.hs, /tmp/ngx_haskell.o )
Linking /tmp/ngx_haskell.so ...
Nginx option -c specifies location of the configuration file. The shared library /tmp/ngx_haskell.so was built upon start which was printed on the terminal. And now we are going to ask nginx server to do some haskell calculations!
curl 'http://localhost:8010/?a=hello_world'
toUpper (hello_world) = HELLO_WORLD
curl 'http://localhost:8010/?a=hello_world&b=4'
takeN (hello_world, 4) = hell
curl 'http://localhost:8010/?a=hello_world&b=oops'
takeN (hello_world, oops) = 
curl 'http://localhost:8010/?c=intelligence'
reverse (intelligence) = ecnegilletni
curl 'http://localhost:8010/?d=intelligence&a=^i'
matches (intelligence, ^i) = 1
curl 'http://localhost:8010/?d=intelligence&a=^I'
matches (intelligence, ^I) = 0
Hmm, I did not escape caret inside the URL in the matches examples: seems that curl allowed it, anyway the calculations were correct. Let’s do some changes in the haskell code inside the configuration file, say takeN will be
takeN x y = ("Incredible! " ++) $ take (readDef 0 y) x
, reload nginx configuration
pkill -HUP nginx
and do curl again.
curl 'http://localhost:8010/?a=hello_world&b=4'
takeN (hello_world, 4) = Incredible! hell
Nice, kind of runtime haskell reload! Now I want to explain some details of implementation and concerns about efficiency and exceptions. Haskell code gets compiled while reading nginx configuration file. It means that ghc must be accessible during nginx runtime (except when using haskell load with already compiled haskell library). Only one haskell compile (or haskell load) directive is allowed through the whole configuration. Directive haskell compile (or haskell load) must be placed before directives haskell_run and after directive haskell ghc_extra_flags (if any). Compiled haskell library gets loaded by dlopen() call when starting a worker process (i.e. haskell runtime is loaded for each nginx worker process separately). The haskell code itself is wrapped inside a special haskell FFI code (it can be found in /tmp/ngx_haskell.hs in case of the above example). The FFI tries its best to minimize unnecessary allocations when reading arguments that were passed from nginx, however for S_S and S_SS function types it does allocate a new string for return value which gets promptly copied to an nginx memory pool and freed. It was possible to avoid allocations for return values, but in this case ghc should have known about internal nginx string implementation, so I decided to sacrifice efficiency to avoid runtime dependency on nginx source code (again, this dependency would not have been necessary if this special FFI interface had been compiled in a separate module, but… probably I’ll do that in future). Another concern about efficiency is using in the exported haskell handlers haskell Strings which are simple lists of characters (which is assumed to be inefficient). On the other hand they are lazy (and this often means efficient). Ok, this is a traditional haskell trade-off matter… What about haskell exceptions? C code is unable to handle them and this is a bad news. However writing pure haskell code makes a strong guarantee about high level of exception safety (at least all IO exceptions must go). In the above example I used function readDef from module Safe instead of its partial counterpart read which increased safety level as well.

вторник, 1 декабря 2015 г.

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

Когда-то давно я написал статью о простом модуле nginx для создания комбинированных апстримов. В ней шла речь о реализации простой директивы add_upstream на уровне блока upstream в конфигурации nginx, которая позволяет добавлять серверы из других апстримов: очень удобно, когда вам требуется собрать апстримы, скомбинированные из нескольких других апстримов, без копирования объявлений составляющих их серверов. На данный момент я больше не могу назвать этот модуль простым, поскольку кроме расширенной функциональности, в его реализации появились разнообразные механизмы nginx, такие как фильтрация заголовков и тела ответов, доступ к переменным и подзапросы (subrequests). Теперь модуль называется модулем комбинированных апстримов, он выложен на гитхабе и снабжен подробной документацией на английском языке. В этой статье я хочу перечислить все возможности данного модуля с примерами их использования.
  • Директива add_upstream в блоке upstream. Это то, с чего все начиналось.
    • Конфигурация
      events {
          worker_connections  1024;
      }
      
      http {
          upstream u1 {
              server localhost:8020;
          }
          upstream u2 {
              server localhost:8030;
          }
          upstream u3 {
              server localhost:8040;
          }
          upstream ucombined {
              add_upstream u1;
              add_upstream u2;
              add_upstream u3 backup;
          }
      
          server {
              listen       127.0.0.1:8010;
              server_name  main;
      
              location / {
                  proxy_pass http://ucombined;
              }
          }
          server {
              listen       127.0.0.1:8020;
              server_name  server1;
              location / {
                  echo "Passed to $server_name";
              }
          }
          server {
              listen       127.0.0.1:8030;
              server_name  server2;
              location / {
                  echo "Passed to $server_name";
              }
          }
          server {
              listen       127.0.0.1:8040;
              server_name  server3;
              location / {
                  echo "Passed to $server_name";
              }
          }
      }
      
    • Тест
      for i in `seq 10` ; do curl 'http://localhost:8010/' ; done
      Passed to server1
      Passed to server1
      Passed to server2
      Passed to server2
      Passed to server1
      Passed to server1
      Passed to server2
      Passed to server2
      Passed to server1
      Passed to server1
      
      Правильно. Если вам интересно, почему каждый сервер опрашивается по два раза подряд, то ответ таков. Мой системный файл /etc/hosts содержит следующие две строки.
      127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
      ::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
      
      Это значит, что loopback интерфейс имеет два адреса — для IPv4 и IPv6 (по крайней мере в /etc/hosts, который nginx читает на старте). Для каждого адреса nginx создает отдельный элемент-сервер в списке round robin peers. Достаточно закомментировать вторую строку в /etc/hosts и перезапустить nginx, чтобы получить настоящий round robin цикл в этом тесте.
  • Директива combine_server_singlets в блоке upstream. Эта штука позволяет плодить апстримы в невероятных количествах :) Представьте, что у вас есть такой апстрим
        upstream u1 {
            server localhost:8020;
            server localhost:8030;
            server localhost:8040;
        }
    
    и вы хотите создать три следующих производных апстрима-синглета (не надо спрашивать зачем, у меня была такая задача и я точно знаю, что она имеет смысл).
        upstream u11 {
            server localhost:8020;
            server localhost:8030 backup;
            server localhost:8040 backup;
        }
        upstream u12 {
            server localhost:8020 backup;
            server localhost:8030;
            server localhost:8040 backup;
        }
        upstream u13 {
            server localhost:8020 backup;
            server localhost:8030 backup;
            server localhost:8040;
        }
    
    Не нужно их создавать вручную! Достаточно поместить новую директиву внутрь порождающего апстрима
        upstream u1 {
            server localhost:8020;
            server localhost:8030;
            server localhost:8040;
            combine_server_singlets;
        }
    
    и апстримы-синглеты будут созданы автоматически. Для тонкой настройки имен порожденных апстримов директива предоставляет два опциональных параметра: суффикс и разрядное выравнивание порядкового номера апстрима.
    • Конфигурация
      events {
          worker_connections  1024;
      }
      
      http {
          upstream u1 {
              server localhost:8020;
              server localhost:8030;
              server localhost:8040;
              combine_server_singlets;
              combine_server_singlets _tmp_ 2;
          }
      
          server {
              listen       127.0.0.1:8010;
              server_name  main;
      
              location /1 {
                  proxy_pass http://u11;
              }
              location /2 {
                  proxy_pass http://u1_tmp_02;
              }
              location /3 {
                  proxy_pass http://u1$cookie_rt;
              }
          }
          server {
              listen       127.0.0.1:8020;
              server_name  server1;
              location / {
                  add_header Set-Cookie "rt=1";
                  echo "Passed to $server_name";
              }
          }
          server {
              listen       127.0.0.1:8030;
              server_name  server2;
              location / {
                  add_header Set-Cookie "rt=2";
                  echo "Passed to $server_name";
              }
          }
          server {
              listen       127.0.0.1:8040;
              server_name  server3;
              location / {
                  add_header Set-Cookie "rt=3";
                  echo "Passed to $server_name";
              }
          }
      }
      
    • Тест
      curl 'http://localhost:8010/1'
      Passed to server1
      curl 'http://localhost:8010/2'
      Passed to server2
      curl 'http://localhost:8010/3'
      Passed to server1
      curl -D- -b 'rt=2' 'http://localhost:8010/3'
      HTTP/1.1 200 OK
      Server: nginx/1.8.0
      Date: Tue, 01 Dec 2015 10:59:00 GMT
      Content-Type: text/plain
      Transfer-Encoding: chunked
      Connection: keep-alive
      Set-Cookie: rt=2
      
      Passed to server2
      curl -D- -b 'rt=3' 'http://localhost:8010/3'
      HTTP/1.1 200 OK
      Server: nginx/1.8.0
      Date: Tue, 01 Dec 2015 10:59:10 GMT
      Content-Type: text/plain
      Transfer-Encoding: chunked
      Connection: keep-alive
      Set-Cookie: rt=3
      
      Passed to server3
      
      Обмен кукой rt дает подсказку, где синглетные апстримы могут быть полезны.
  • Апстрэнды (upstrands). Это такие комбинированные апстримы, внутри которых составляющие их апстримы не теряют свою целостность и идентичность. Слово upstrand образовано из двух составляющих: upstream и strand и означает пучок или жилу апстримов. Я касался деталей реализации апстрэндов в этой статье на английском языке. В двух словах, апстрэнд представляет собой высокоуровневую структуру, которая может опрашивать составляющие ее апстримы по кругу (round robin) до тех пор, пока не найдет апстрим, удовлетворяющий заданному условию — код ответа апстрима (HTTP response) не должен входить в список, заданный директивой next_upstream_statuses. Технически апстрэнды являются блоками — такими же как и апстримы. Они точно так же задаются внутри секции http конфигурации nginx, но вместо серверов составляющими их компонентами являются обычные апстримы. Апстримы добавляются в апстрэнд с помощью директивы upstream. Если имя апстрима начинается с символа тильда, то оно рассматривается как регулярное выражение. Отдельные апстримы внутри апстрэнда могут быть помечены как бэкапные, также имеется возможность блэклистить апстримы на определенное время с помощью параметра blacklist_interval. Опрос нескольких апстримов внутри апстрэнда реализован с помощью механизма подзапросов (subrequests). Этот механизм запускается в результате доступа к встроенной переменной upstrand_NAME, где NAME соответствует имени существующего апстрэнда. Я предполагаю, что в основном апстрэнды будут применяться в директиве proxy_pass модуля nginx proxy, однако здесь нет искусственных ограничений: доступ к механизму запуска подзапросов через переменную позволяет использовать апстрэнды в любом пользовательском модуле. На случай, если имя апстрэнда заранее неизвестно (например, оно приходит в куке), предусмотрена директива dynamic_upstrand, которая записывает имя следующего апстрима предполагаемого апстрэнда в свой первый аргумент-переменную на основании оставшегося списка аргументов (имя апстрэнда будет соответствовать первому не пустому аргументу из этого списка). Директива доступна на уровнях конфигурации server, location и location-if. Апстрэнды предоставляют несколько статусных переменных, среди них upstrand_addr, upstrand_status, upstrand_cache_status, upstrand_connect_time, upstrand_header_time, upstrand_response_time, upstrand_response_length — все они соответствуют аналогичным переменным из модуля upstream, только хранят значения всех посещенных апстримов в рамках данного HTTP запроса — и upstrand_path, в которой записан хронологический порядок (путь) посещения апстримов в рамках данного запроса. Статусные переменные полезны для анализа работы апстрэндов в access логе. А теперь пример конфигурации и curl-тест.
    • Конфигурация
      events {
          worker_connections  1024;
      }
      
      http {
          upstream u01 {
              server localhost:8020;
          }
          upstream u02 {
              server localhost:8030;
          }
          upstream b01 {
              server localhost:8040;
          }
          upstream b02 {
              server localhost:8050;
          }
      
          upstrand us1 {
              upstream ~^u0 blacklist_interval=10s;
              upstream b01 backup;
              next_upstream_statuses 5xx;
          }
          upstrand us2 {
              upstream b02;
              next_upstream_statuses 5xx;
          }
      
          log_format  fmt '$remote_addr [$time_local]\n'
                          '>>> [path]          $upstrand_path\n'
                          '>>> [addr]          $upstrand_addr\n'
                          '>>> [response time] $upstrand_response_time\n'
                          '>>> [status]        $upstrand_status';
      
          server {
              listen       127.0.0.1:8010;
              server_name  main;
              error_log    /tmp/nginx-test-upstrand-error.log;
              access_log   /tmp/nginx-test-upstrand-access.log fmt;
      
              dynamic_upstrand $dus1 $arg_a us2;
      
              location /us {
                  proxy_pass http://$upstrand_us1;
              }
              location /dus {
                  dynamic_upstrand $dus2 $arg_b;
                  if ($arg_b) {
                      proxy_pass http://$dus2;
                      break;
                  }
                  proxy_pass http://$dus1;
              }
          }
          server {
              listen       8020;
              server_name  server1;
      
              location / {
                  echo "Passed to $server_name";
                  #return 503;
              }
          }
          server {
              listen       8030;
              server_name  server2;
      
              location / {
                  echo "Passed to $server_name";
                  #return 503;
              }
          }
          server {
              listen       8040;
              server_name  server3;
      
              location / {
                  echo "Passed to $server_name";
              }
          }
          server {
              listen       8050;
              server_name  server4;
      
              location / {
                  echo "Passed to $server_name";
              }
          }
      }
      
    • Тест
      for i in `seq 6` ; do curl 'http://localhost:8010/us' ; done
      Passed to server1
      Passed to server2
      Passed to server1
      Passed to server2
      Passed to server1
      Passed to server2
      
      В логах nginx мы увидим
      tail -f /tmp/nginx-test-upstrand-*
      ==> /tmp/nginx-test-upstrand-access.log <==
      
      ==> /tmp/nginx-test-upstrand-error.log <==
      
      ==> /tmp/nginx-test-upstrand-access.log <==
      127.0.0.1 [01/Dec/2015:16:52:03 +0300]
      >>> [path]          u01
      >>> [addr]          (u01) 127.0.0.1:8020
      >>> [response time] (u01) 0.000
      >>> [status]        (u01) 200
      127.0.0.1 [01/Dec/2015:16:52:03 +0300]
      >>> [path]          u02
      >>> [addr]          (u02) 127.0.0.1:8030
      >>> [response time] (u02) 0.000
      >>> [status]        (u02) 200
      127.0.0.1 [01/Dec/2015:16:52:03 +0300]
      >>> [path]          u01
      >>> [addr]          (u01) 127.0.0.1:8020
      >>> [response time] (u01) 0.000
      >>> [status]        (u01) 200
      127.0.0.1 [01/Dec/2015:16:52:03 +0300]
      >>> [path]          u02
      >>> [addr]          (u02) 127.0.0.1:8030
      >>> [response time] (u02) 0.001
      >>> [status]        (u02) 200
      127.0.0.1 [01/Dec/2015:16:52:03 +0300]
      >>> [path]          u01
      >>> [addr]          (u01) 127.0.0.1:8020
      >>> [response time] (u01) 0.000
      >>> [status]        (u01) 200
      127.0.0.1 [01/Dec/2015:16:52:03 +0300]
      >>> [path]          u02
      >>> [addr]          (u02) 127.0.0.1:8030
      >>> [response time] (u02) 0.001
      >>> [status]        (u02) 200
      
      А теперь давайте закомментируем директивы echo и раскомментируем директивы return 503 в локейшнах двух первых бэкендов (server1 и server2), перезапустим nginx и протестируем снова.
      for i in `seq 6` ; do curl 'http://localhost:8010/us' ; done
      Passed to server3
      Passed to server3
      Passed to server3
      Passed to server3
      Passed to server3
      Passed to server3
      
      Логи nginx.
      ==> /tmp/nginx-test-upstrand-access.log <==
      127.0.0.1 [01/Dec/2015:16:58:06 +0300]
      >>> [path]          u01 -> u02 -> b01
      >>> [addr]          (u01) 127.0.0.1:8020 (u02) 127.0.0.1:8030 (b01) 127.0.0.1:8040
      >>> [response time] (u01) 0.001 (u02) 0.000 (b01) 0.000
      >>> [status]        (u01) 503 (u02) 503 (b01) 200
      127.0.0.1 [01/Dec/2015:16:58:06 +0300]
      >>> [path]          b01
      >>> [addr]          (b01) 127.0.0.1:8040
      >>> [response time] (b01) 0.000
      >>> [status]        (b01) 200
      127.0.0.1 [01/Dec/2015:16:58:06 +0300]
      >>> [path]          b01
      >>> [addr]          (b01) 127.0.0.1:8040
      >>> [response time] (b01) 0.000
      >>> [status]        (b01) 200
      127.0.0.1 [01/Dec/2015:16:58:06 +0300]
      >>> [path]          b01
      >>> [addr]          (b01) 127.0.0.1:8040
      >>> [response time] (b01) 0.000
      >>> [status]        (b01) 200
      127.0.0.1 [01/Dec/2015:16:58:06 +0300]
      >>> [path]          b01
      >>> [addr]          (b01) 127.0.0.1:8040
      >>> [response time] (b01) 0.000
      >>> [status]        (b01) 200
      127.0.0.1 [01/Dec/2015:16:58:06 +0300]
      >>> [path]          b01
      >>> [addr]          (b01) 127.0.0.1:8040
      >>> [response time] (b01) 0.000
      >>> [status]        (b01) 200
      
      Ждем десять секунд — заблэклисченные апстримы должны разблэклиститься, и повторяем снова.
      for i in `seq 2` ; do curl 'http://localhost:8010/us' ; done
      Passed to server3
      Passed to server3
      
      Логи nginx.
      127.0.0.1 [01/Dec/2015:17:01:44 +0300]
      >>> [path]          u01 -> u02 -> b01
      >>> [addr]          (u01) 127.0.0.1:8020 (u02) 127.0.0.1:8030 (b01) 127.0.0.1:8040
      >>> [response time] (u01) 0.000 (u02) 0.000 (b01) 0.001
      >>> [status]        (u01) 503 (u02) 503 (b01) 200
      127.0.0.1 [01/Dec/2015:17:01:44 +0300]
      >>> [path]          b01
      >>> [addr]          (b01) 127.0.0.1:8040
      >>> [response time] (b01) 0.000
      >>> [status]        (b01) 200
      
      А теперь протестируем работу динамических апстрэндов (предварительно вернув оригинальные настройки локейшнов двух первых бэкендов).
      curl 'http://localhost:8010/dus?a=us1'
      Passed to server1
      curl 'http://localhost:8010/dus?a=us2'
      Passed to server4
      curl 'http://localhost:8010/dus?a=foo&b=us1'
      Passed to server2
      curl 'http://localhost:8010/dus'
      Passed to server4
      curl 'http://localhost:8010/dus?b=foo'
      <html>
      <head><title>500 Internal Server Error</title></head>
      <body bgcolor="white">
      <center><h1>500 Internal Server Error</h1></center>
      <hr><center>nginx/1.8.0</center>
      </body>
      </html>
      
      В первом запросе мы через аргумент a попали на один из апстримов апстрэнда us1 — им оказался апстрим u01, который содержит единственный сервер server1. Во втором запросе, тоже через аргумент a, мы попали на апстрэнд us2 — апстрим b02 — сервер server4. В третьем запросе мы задействовали новый динамический апстрим dus2 через аргумент b, который отправил нас на второй апстрим (round robin же) u02 апстрэнда us1 и сервер server2. В четвертом запросе мы не предоставили аргументов и сработал последний не пустой элемент динамического апстрэнда dus1us2 с его единственным апстримом b02 и единственным сервером server4. В последнем запросе я показал, что может произойти, если динамический апстрэнд вернет пустое значение. В данном случае значение dus2 оказалось пустым и директива proxy_pass, попытавшись выполнить проксирование на неверно сформированный адрес http://, вернула ошибку 500.
    Апстрэнды могут быть полезны, во-первых, для создания двумерных round robin циклов, когда вы знаете, что если некоторый сервер из определенного апстрима вернул неудовлетворительный ответ, то нет необходимости обращаться к другим серверам этого апстрима, а следует незамедлительно переходить к следующему апстриму — простой upstream round robin механизм не способен эмулировать такое поведение, поскольку серверы внутри апстрима не могут образовывать кластеры, и, во-вторых, для переноса части логики протокола уровня приложения с клиента на роутер. Например, если в логике приложения код ответа 204, присланный из некоторого апстрима, обозначает отсутствие данных и клиенту следует тут же проверить наличие данных в другом апстриме, то можно ограничить общение клиента с бэкендом всего одним запросом, перенеся опрос всех направлений-апстримов на плечи роутера, в котором все эти апстримы будут помещены в один апстрэнд. Такой подход полезен еще тем, что инкапсулирует знание топологии бэкендов внутри роутера, ведь клиентам это знание больше не нужно.

понедельник, 13 августа 2012 г.

nginx: расширяем модуль myutil

Здесь мы начали разрабатывать новый модуль nginx myutil, в котором, как мы задумывали, будут реализованы разные полезные директивы. На данный момент в модуле представлена единственная директива myutil_var_alias. В этой статье будет показано, как организовать поиск по списку переменных с целью получения первого непустого значения. Пусть соответствующая директива будет называться myutil_first_not_empty. Тогда строка в конфигурации вида
            myutil_first_not_empty $fne_data $arg_a $arg_b default;
указывает, что следует найти первое непустое значение среди списка переменных $arg_a и $arg_b (которые соответствуют аргументам a и b строки запроса) и записать его в новую переменную $fne_data; в случае, если переменные $arg_a и $arg_b отсутствуют, либо не содержат непустых значений, в переменную $fne_data следует записать значение default. Будем предполагать, что литералы типа default являются равноправными членами наряду с переменными типа $arg_a и $arg_b и могут записываться где-то в середине списка аргументов (хотя очевидно, что из смысла директивы myutil_first_not_empty следует, что литерал должен быть только один и он должен находиться в конце списка). Сам список аргументов может быть произвольной длины.

Какие типы данных нам понадобятся? Ну, во-первых, как и в случае директивы myutil_var_alias, это массив типа ngx_array_t, соответствующий множеству переменных $fne_... (не забываем совет, приведенный в конце прошлой статьи и добавляем узнаваемый префикс, в данном случае $fne_, ко всем переменным, объявленным нашей новой директивой). Добавляем его в ngx_http_myutil_loc_conf_t:
typedef struct
{
    ngx_array_t  varalias_data;
    ngx_array_t  first_not_empty_var_data;
}  ngx_http_myutil_loc_conf_t;
В директиве myutil_var_alias в качестве элементов массива varalias_data мы использовали простую структуру
typedef struct
{
    ngx_int_t    self;
    ngx_int_t    index;
}  ngx_http_myutil_var_elem_t;
При этом значение self соответствовало индексу новой переменной, созданной директивой myutil_var_alias (ее первому аргументу), а значение index - исходной переменной, т.е. той, значение которой копировалось в новую переменную (это второй аргумент myutil_var_alias).

На этот раз мы имеем дело не с двумя аргументами, а с аргументом - новой переменной, создаваемой директивой myutil_first_not_empty, и списком неопределенной длины. Очевидно, что одним из элементов новой структуры должен быть массив:
typedef struct
{
    ngx_array_t  data;
    ngx_int_t    index;
}  ngx_http_myutil_fne_elem_t;
Здесь data - указанный массив, а index - индекс новой переменной. Какой тип элементов следует выбрать для массива data? Вспомните, что мы хотели записывать в него как переменные, так и литералы. Переменная легко идентифицируется индексом, а литерал - строкой, при этом будем считать, что литерал тоже имеет индекс, равный ( ngx_uint_t )NGX_ERROR: это позволит понимать, с чем мы имеем дело (переменной или литералом) в хендлере ngx_http_myutil_get_first_not_empty_value(), в котором происходит присваивание нового значения переменной $fne_.... Итак, тип элемента из списка аргументов директивы myutil_first_not_empty имеет вид
typedef struct
{
    ngx_str_t    key;
    ngx_int_t    index;
}  ngx_http_myutil_var_handle_t;
Добавляем директиву myutil_first_not_empty в список ngx_http_myutil_commands[], теперь он выглядит так:
static ngx_command_t  ngx_http_myutil_commands[] =
{
    {
        ngx_string( "myutil_var_alias" ),
        NGX_HTTP_LOC_CONF | NGX_HTTP_LIF_CONF | NGX_CONF_TAKE2,
        ngx_http_myutil_var_alias,
        NGX_HTTP_LOC_CONF_OFFSET,
        0,
        NULL
    },
    {
        ngx_string( "myutil_first_not_empty" ),
        NGX_HTTP_LOC_CONF | NGX_HTTP_LIF_CONF | NGX_CONF_2MORE,
        ngx_http_myutil_first_not_empty,
        NGX_HTTP_LOC_CONF_OFFSET,
        0,
        NULL
    },

    ngx_null_command
};
Также обновляем функции создания и слияния конфигураций уровня location:
static void *  ngx_http_myutil_create_loc_conf( ngx_conf_t *  cf )
{
    ngx_http_myutil_loc_conf_t *  lcf = NULL;

    lcf = ngx_pcalloc( cf->pool, sizeof( ngx_http_myutil_loc_conf_t ) );

    if ( ! lcf )
        return NULL;

    if ( ngx_array_init( &lcf->varalias_data, cf->pool, 1,
                         sizeof( ngx_http_myutil_var_elem_t ) ) != NGX_OK )
        return NULL;

    if ( ngx_array_init( &lcf->first_not_empty_var_data, cf->pool, 1,
                         sizeof( ngx_http_myutil_fne_elem_t ) ) != NGX_OK )
        return NULL;

    return lcf;
}


static char *  ngx_http_myutil_merge_loc_conf( ngx_conf_t *  cf, void *  parent,
                                               void *  child )
{
    ngx_http_myutil_loc_conf_t *  prev = parent;
    ngx_http_myutil_loc_conf_t *  conf = child;

    ngx_uint_t                    i;

    /* all data in next sections will be imported from parent location in
     * addition to already collected data in child location */

    for ( i = 0; i < prev->varalias_data.nelts; ++)
    {
        ngx_http_myutil_var_elem_t *  elem = NULL;

        elem = ngx_array_push( &conf->varalias_data );

        if ( ! elem )
            return NGX_CONF_ERROR;

        *elem = ( ( ngx_http_myutil_var_elem_t * )
                                    prev->varalias_data.elts )[ i ];
    }

    for ( i = 0; i < prev->first_not_empty_var_data.nelts; ++)
    {
        ngx_http_myutil_fne_elem_t *  elem = NULL;

        elem = ngx_array_push( &conf->first_not_empty_var_data );

        if ( ! elem )
            return NGX_CONF_ERROR;

        *elem = ( ( ngx_http_myutil_fne_elem_t * )
                                    prev->first_not_empty_var_data.elts )[ i ];
    }

    return NGX_CONF_OK;
}
Хендлером конфигурации при регистрации директивы myutil_first_not_empty в списке ngx_http_myutil_commands[] была объявлена функция ngx_http_myutil_first_not_empty(), вот ее тело (строки пронумерованы для дальнейших комментариев):
367 static char *  ngx_http_myutil_first_not_empty( ngx_conf_t *  cf,
368                                             ngx_command_t *  cmd, void *  conf )
369 {
370     ngx_str_t *                   value     = cf->args->elts;
371     ngx_http_variable_t *         v         = NULL;
372     ngx_http_myutil_loc_conf_t *  lcf       = conf;
373     ngx_int_t                     v_idx     = NGX_ERROR;
374     ngx_uint_t *                  v_idx_ptr = NULL;
375     ngx_http_myutil_fne_elem_t *  resvar    = NULL;
376 
377     ngx_uint_t                    i;
378 
379     if ( value[ 1 ].data[ 0 ] != '$' )
380     {
381         ngx_conf_log_error( NGX_LOG_EMERG, cf, 0,
382                             "invalid variable name '%V'", &value[ 1 ] );
383         return NGX_CONF_ERROR;
384     }
385 
386     value[ 1 ].len--;
387     value[ 1 ].data++;
388 
389     /* It must never occur due to command arguments number restriction */
390     if ( cf->args->nelts < 3 )
391         return NGX_CONF_ERROR;
392 
393     resvar = ngx_array_push( &lcf->first_not_empty_var_data );
394 
395     if ( ! resvar )
396         return NGX_CONF_ERROR;
397 
398     if ( ngx_array_init( &resvar->data, cf->pool, cf->args->nelts - 2,
399                          sizeof( ngx_http_myutil_var_handle_t ) ) != NGX_OK )
400         return NGX_CONF_ERROR;
401 
402     for ( i = 2; i < cf->args->nelts; ++)
403     {
404         ngx_http_myutil_var_handle_t *  res   = NULL;
405         ngx_int_t                       index = NGX_ERROR;
406 
407         ngx_uint_t  isvar = value[ i ].data[ 0 ] == '$' ? 1 : 0;
408 
409         res = ngx_array_push( &resvar->data );
410 
411         if ( ! res )
412             return NGX_CONF_ERROR;
413 
414         if ( ! isvar )
415         {
416             res->index = NGX_ERROR;
417             res->key   = value[ i ];
418             continue;
419         }
420 
421         value[ i ].len--;
422         value[ i ].data++;
423 
424         index = ngx_http_get_variable_index( cf, &value[ i ] );
425 
426         if ( index == NGX_ERROR )
427             return NGX_CONF_ERROR;
428 
429         res->index = index;
430         res->key   = value[ i ];
431     }
432 
433     v = ngx_http_add_variable( cf, &value[ 1 ], NGX_HTTP_VAR_CHANGEABLE );
434 
435     if ( v == NULL )
436         return NGX_CONF_ERROR;
437 
438     v_idx = ngx_http_get_variable_index( cf, &value[ 1 ] );
439 
440     if ( v_idx == NGX_ERROR )
441         return NGX_CONF_ERROR;
442 
443     v_idx_ptr = ngx_pnalloc( cf->pool, sizeof( ngx_uint_t ) );
444 
445     if ( ! v_idx_ptr )
446         return NGX_CONF_ERROR;
447 
448     resvar->index  = v_idx;
449 
450     *v_idx_ptr     = v_idx;
451     v->data        = ( uintptr_t )v_idx_ptr;
452     v->get_handler = ngx_http_myutil_get_first_not_empty_value;
453 
454     return NGX_CONF_OK;
455 }
В строках 370-377 объявлены переменные, используемые внутри функции. В строках 379-384 проверяется, что первый аргумент директивы (переменная $fne_...) начинается с символа $. В строках 386-387 первый символ $ убирается из названия переменной (мы теперь и так знаем, что это переменная, и это нас устраивает). В строках 390-391 убеждаемся, что в директиву передано более одного аргумента (хотя эта проверка излишняя: флаг NGX_CONF_2MORE, указанный при объявлении директивы, не позволит запустить nginx, если в какой-либо из директив myutil_first_not_empty в конфигурации будет менее двух аргументов). В строках 393-400 мы создаем новый элемент массива first_not_empty_var_data из структуры ngx_http_myutil_loc_conf. На этом этапе мы знаем количество элементов внутри его подмассива data (это количество элементов директивы минус два), поэтому сразу же инициализируем data указанным значением элементов.

В строках 402-431 мы проходим по оставшимся аргументам директивы и заполняем инициализированные элементы массива data index и key значениями index и value[ i ] соответственно (строки 429-430). При этом, если текущий аргумент начинается с символа $ (т.е. является переменной), значение index получаем из возвращаемого значения функции ngx_http_get_variable_index(), а если это литерал - то записываем в качестве index значение ( ngx_uint_t )NGX_ERROR. В обоих случаях значением key будет имя текущего аргумента value[ i ] (хотя для аргумента-переменной это значение использоваться не будет).

В строках 433-441 создаем новую переменную v, соответствующую нашей переменной $fne_... и получаем ее индекс v_idx. Далее выделяем память под элемент типа ngx_uint_t (переменная v_idx_ptr): здесь будет храниться значение v_idx, а сам этот участок памяти будет передаваться в функцию ngx_http_myutul_get_first_not_empty_value() в качестве третьего аргумента для поиска нужного элемента в массиве first_not_empty_var_data (благодаря присваиванию resvar->index значения v_idx, а  v->data - адреса v_idx_ptr в строках 448-451). В строке 452 присваиваем v->get_handler() адрес функции ngx_http_myutul_get_first_not_empty_value(), а затем возвращаем значение NGX_CONF_OK.

Хендлер ngx_http_myutil_get_first_not_empty_value() выглядит так:
225 static ngx_int_t  ngx_http_myutil_get_first_not_empty_value(
226                         ngx_http_request_t *  r, ngx_http_variable_value_t *  v,
227                         uintptr_t  data )
228 {
229     ngx_http_myutil_loc_conf_t *    lcf         = NULL;
230     ngx_http_variable_value_t *     var         = NULL;
231     ngx_array_t *                   fne_data    = NULL;
232     ngx_http_myutil_fne_elem_t *    fne_elts    = NULL;
233     ngx_http_myutil_fne_elem_t *    fne_elem    = NULL;
234     ngx_array_t *                   exprarray   = NULL;
235     ngx_http_myutil_var_handle_t *  expr        = NULL;
236     ngx_int_t *                     index       = NULL;
237     ngx_uint_t                      found_index = ( ngx_uint_t )NGX_ERROR;
238 
239     ngx_uint_t                      i;
240 
241     if ( ! data )
242         return NGX_ERROR;
243 
244     lcf = ngx_http_get_module_loc_conf( r, ngx_http_myutil_module );
245 
246     fne_data = &lcf->first_not_empty_var_data;
247     fne_elts = fne_data->elts;
248     index    = ( ngx_int_t * )data;
249 
250     for ( i = 0; i < fne_data->nelts; ++)
251     {
252         if ( *index != fne_elts[ i ].index )
253             continue;
254 
255         found_index = i;
256         break;
257     }
258 
259     if ( found_index == ( ngx_uint_t )NGX_ERROR )
260         return NGX_ERROR;
261 
262     fne_elem = &fne_elts[ found_index ];
263 
264     exprarray = &fne_elem->data;
265     expr      = exprarray->elts;
266 
267     for ( i = 0; i < exprarray->nelts; ++)
268     {
269         ngx_http_variable_value_t *  found = NULL;
270 
271         if ( expr[ i ].index == NGX_ERROR )
272         {
273             v->len          = expr[ i ].key.len;
274             v->data         = expr[ i ].key.data;
275             v->valid        = 1;
276             v->no_cacheable = 0;
277             v->not_found    = 0;
278 
279             return NGX_OK;
280         }
281 
282         found = ngx_http_get_indexed_variable( r, expr[ i ].index );
283 
284         if ( found && found->len > 0 )
285         {
286             var = found;
287             break;
288         }
289     }
290 
291     if ( ! var )
292         return NGX_ERROR;
293 
294     v->len          = var->len;
295     v->data         = var->data;
296     v->valid        = 1;
297     v->no_cacheable = 0;
298     v->not_found    = 0;
299 
300     return NGX_OK;
301 }
Здесь все относительно просто. В переменную index записываем значение третьего аргумента функции (значение, которое мы предусмотрительно сохранили в ngx_http_myutil_first_not_empty()) - строка 248. В строках 250-257 ищем соответствующий элемент в массиве first_not_empty_var_data. На найденный элемент будет указывать переменная fne_elem (строка 262). Получаем элементы массива data из fne_elem (строки 264-265). Далее проходим по всем элементам. Если текущий элемент переменная, то получаем ее значение по ее индексу (строка 282). В строках 284-288 проверяем, пусто ли это значение: если пусто, то продолжаем поиск, иначе выходим из цикла и присваиваем переменной v найденное значение (строки 294-298). Если же текущий элемент - литерал, то просто берем его значение key и присваиваем переменной v (строки 271-280; заметьте, что пустой литерал '' тоже сработает, так как проверка на expr[ i ].key.len не производится!).

Ну вот и все. Осталось запустить элементарную проверку. Будем проверять на следующей конфигурации:
events {
    worker_connections  1024;
}

http {
    server {
        listen          8010;
        server_name     router;

        location /test_first_not_empty.html {
            myutil_first_not_empty $fne_1 $arg_a $arg_b default;
            myutil_first_not_empty $fne_2 $arg_aa $arg_bb 'no data';
            if ($arg_c) {
                myutil_first_not_empty $fne_1 $arg_a $arg_c;
                echo $fne_1;
                echo $fne_2;
                break;
            }
            echo $fne_1;
            echo $fne_2;
        }
    }
}
Как видим, внутри блока if в локейшне /test_first_not_empty.html переменная $fne_1 переопределена. Нам нужно проверить, действительно ли она будет переопределена. Мы в праве этого ожидать, поскольку элементы массива first_not_empty_var_data родительской конфигурации добавляются после элементов дочерней конфигурации в функции ngx_http_myutil_merge_loc_conf(). Проверяем:
$ curl 'http://localhost:8010/test_first_not_empty.html?b=HiB'
HiB
no data
$ curl 'http://localhost:8010/test_first_not_empty.html?b=HiB&c=HiC'
HiC
no data
Действительно, присутствие аргумента c в строке запроса привело к тому, что сработала директива myutil_first_not_empty $fne_1 ... внутри if. Еще пара проверок:
$ curl 'http://localhost:8010/test_first_not_empty.html?bb=HiBB&a=HiA&b=HiB'
HiA
HiBB
$ curl 'http://localhost:8010/test_first_not_empty.html?aa=907'
default
907
Исходники модуля вместе с тестовой конфигурацией можно взять здесь.