- Директива 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 образовано из двух составляющих: up
streamи 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. В четвертом запросе мы не предоставили аргументов и сработал последний не пустой элемент динамического апстрэнда dus1 — us2 с его единственным апстримом b02 и единственным сервером server4. В последнем запросе я показал, что может произойти, если динамический апстрэнд вернет пустое значение. В данном случае значение dus2 оказалось пустым и директива proxy_pass, попытавшись выполнить проксирование на неверно сформированный адрес http://, вернула ошибку 500.
- Конфигурация
Показаны сообщения с ярлыком combined upstream. Показать все сообщения
Показаны сообщения с ярлыком combined upstream. Показать все сообщения
вторник, 1 декабря 2015 г.
Не такой уж простой модуль nginx для создания комбинированных апстримов
Когда-то давно я написал статью о простом модуле nginx для создания комбинированных апстримов. В ней шла речь о реализации простой директивы add_upstream на уровне блока upstream в конфигурации nginx, которая позволяет добавлять серверы из других апстримов: очень удобно, когда вам требуется собрать апстримы, скомбинированные из нескольких других апстримов, без копирования объявлений составляющих их серверов.
На данный момент я больше не могу назвать этот модуль простым, поскольку кроме расширенной функциональности, в его реализации появились разнообразные механизмы nginx, такие как фильтрация заголовков и тела ответов, доступ к переменным и подзапросы (subrequests). Теперь модуль называется модулем комбинированных апстримов, он выложен на гитхабе и снабжен подробной документацией на английском языке. В этой статье я хочу перечислить все возможности данного модуля с примерами их использования.
пятница, 18 сентября 2015 г.
nginx upstrand to configure super-layers of upstreams
Recently I equipped my nginx combined upstreams module with a new entity that I named upstrand. An upstrand is an nginx configuration block that can be defined in the http clause after upstream blocks. It was designed to combine upstreams in super-layers where all upstreams keep their identity and do not get flattened down to separate servers. This can be useful in multiple areas. Let me rephrase here an example from the module documentation.
Imagine a geographically distributed system of backends to deliver tasks to an executor. Let the executor be implemented as a poller separated from the backends by an nginx router proxying requests from the poller to an arbitrary upstream in a list. These upstreams (i.e. geographical parts) may have multiple servers within. Let them send HTTP status 204 if they do not have any tasks at the moment. If an upstream has 10 servers and the first server sends 204 No tasks upon receiving a request from the poller then other 9 servers will presumably send the same status in a short time interval. The nginx upstreams are smart enough to skip checking all servers if the first server returns status 204: the status will be sent to the poller and then the poller must decide what to do next.
This scheme has several shortcomings. Remember words arbitrary upstream that I intentionally emphasized in the previous paragraph? Nginx cannot choose arbitrary upstreams! It can combine several upstreams into a bigger upstream but in this case the poller may sequentially receive up to 10 204 responses from the same upstream trying to get a next task. The concrete value (10 or less) of repeated requests depends on how the bigger upstream combines separate servers from the geographical upstreams. On the other hand the poller may know the topology of the backends and send requests to concrete upstreams. I even do not want to explain how bad this is. And again it will send new requests when already polled upstreams have no tasks.
What if encapsulate all the polling logic inside the proxy? In this case the proxy router would initiate a new request to another upstream itself if the previous server responds with 204 No tasks. It would eventually send back to the poller a new task or the final 204 status when there were no tasks in all upstreams. The single request from the poller and no need for knowledge about the backends topology downstream the router!
This was a good example of what the new upstrand thing can accomplish. Let’s look at a simple nginx configuration that involves upstrands.
worker_processes 1; error_log /var/log/nginx/error.log info; 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; upstream b01 backup; order start_random; next_upstream_statuses 204 5xx; } upstrand us2 { upstream ~^u0; upstream b02 backup; order start_random; next_upstream_statuses 5xx; } server { listen 8010; server_name main; location /us1 { proxy_pass http://$upstrand_us1; } location /us2 { rewrite ^ /index.html last; } location /index.html { proxy_pass http://$upstrand_us2; } } server { listen 8020; server_name server01; location / { return 503; } } server { listen 8030; server_name server02; location / { return 503; } } server { listen 8040; server_name server03; location / { echo "In 8040"; } } server { listen 8050; server_name server04; location / { proxy_pass http://rsssf.com/; } } }Upstrand us1 imports 2 upstreams u01 and u02 using a regular expression (directive upstream regards names as regular expressions when they start with tilde) and backup upstream b01. While searching through the upstreams for a response status that may be sent to the client the upstrand performs 2 cycles: normal and backup. Backup cycle starts only when all upstreams in the normal cycle responded with unacceptable statuses. Directive order start_random indicates that the first upstream in the upstrand after the worker started up will be chosen randomly, next upstreams will follow the round-robin order. Directive next_upstream_statuses lists HTTP response statuses that will sign to the router to postpone the response and send the request to the next upstream. The directive accepts 4xx and 5xx statuses notation. If all servers in normal and backup cycles responded with unacceptable statuses (like 204 No tasks in the above example) the last response is sent to the client. Upstrand us2 does not differ a lot. It imports same u01 and u02 upstreams in normal cycle and b02 in backup cycle. Each upstream in this configuration has a single server. Servers from upstreams u01 and u02 simply return status 503, server from b01 says In 8040, server from b02 proxies the request to a good old days site rsssf.com (which is a great place for soccer stats!) that I chose because it won’t redirect to https and will send back a huge response: a nice thing to test if buffering of the response won’t break it while going through the proxy router and filters of the combined upstreams module. Let’s look how it works. Start nginx with the above configuration and a sniffer on the loopback interface (better in another terminal).
nginx -c /path/to/our/nginx.conf ngrep -W byline -d lo '' tcpRun a client for location /us1.
curl 'http://localhost:8010/us1' In 8040Nice. We are in the backup upstream b01. However it shows too little information. That’s why we ran a sniffer. Here is its output.
###### T 127.0.0.1:44070 -> 127.0.0.1:8010 [AP] GET /us1 HTTP/1.1. User-Agent: curl/7.40.0. Host: localhost:8010. Accept: */*. . ##### T 127.0.0.1:37341 -> 127.0.0.1:8030 [AP] GET /us1 HTTP/1.0. Host: u02. Connection: close. User-Agent: curl/7.40.0. Accept: */*. . ## T 127.0.0.1:8030 -> 127.0.0.1:37341 [AP] HTTP/1.1 503 Service Temporarily Unavailable. Server: nginx/1.8.0. Date: Fri, 18 Sep 2015 12:58:01 GMT. Content-Type: text/html. Content-Length: 212. Connection: close. . <html>. <head><title>503 Service Temporarily Unavailable</title></head>. <body bgcolor="white">. <center><h1>503 Service Temporarily Unavailable</h1></center>. <hr><center>nginx/1.8.0</center>. </body>. </html>. ######## T 127.0.0.1:50128 -> 127.0.0.1:8020 [AP] GET /us1 HTTP/1.0. Host: u01. Connection: close. User-Agent: curl/7.40.0. Accept: */*. . ## T 127.0.0.1:8020 -> 127.0.0.1:50128 [AP] HTTP/1.1 503 Service Temporarily Unavailable. Server: nginx/1.8.0. Date: Fri, 18 Sep 2015 12:58:01 GMT. Content-Type: text/html. Content-Length: 212. Connection: close. . <html>. <head><title>503 Service Temporarily Unavailable</title></head>. <body bgcolor="white">. <center><h1>503 Service Temporarily Unavailable</h1></center>. <hr><center>nginx/1.8.0</center>. </body>. </html>. ######## T 127.0.0.1:55270 -> 127.0.0.1:8040 [AP] GET /us1 HTTP/1.0. Host: b01. Connection: close. User-Agent: curl/7.40.0. Accept: */*. . ## T 127.0.0.1:8040 -> 127.0.0.1:55270 [AP] HTTP/1.1 200 OK. Server: nginx/1.8.0. Date: Fri, 18 Sep 2015 12:58:01 GMT. Content-Type: text/plain. Connection: close. . In 8040 ##### T 127.0.0.1:8010 -> 127.0.0.1:44070 [AP] HTTP/1.1 200 OK. Server: nginx/1.8.0. Date: Fri, 18 Sep 2015 12:58:01 GMT. Content-Type: text/plain. Transfer-Encoding: chunked. Connection: keep-alive. . 8. In 8040 . 0. .The client sent the request to port 8010 (our router’s frontend). Then the router started the normal cycle: it proxied the request to the upstream u02 (8030 server) which responded with unacceptable status 503, after that the router tried the next upstream u01 from the normal cycle with the only 8020 server and received 503 status response again. Finally the router started backup cycle and received response In 8040 with HTTP status 200 from 8040 server that belonged to upstream b01, This response was sent back to the client and shown on the terminal. I won’t show results of testing location /us2: the response was very large while the scenario is essentially the same. To search through the sniffer output is boring. Let’s better show what we are visiting during waiting for response on the terminal. An addition of a simple location
location /echo/us1 { echo $upstrand_us1; }into the frontend 8010 server will make our testing easy. (Beware that older versions of the echo module compiled for nginx-1.8.0 will behave badly in the next tests! I used latest version v0.58 and it worked fine.) Restart nginx and run some curls again.
curl 'http://localhost:8010/echo/us1' u02 curl 'http://localhost:8010/echo/us1' u01 curl 'http://localhost:8010/echo/us1' u02 curl 'http://localhost:8010/echo/us1' u01 curl 'http://localhost:8010/echo/us1' u02Hmm. Servers from the normal cycle following each other in the round-robin manner. Upstrand us1 must return response from the first upstream in normal cycle whose status is not listed in next_upstream_statuses. Directive echo normally returns 200 and this status is not present in the list. Thus the list of the upstreams shown on the terminal corresponds to first upstreams in the normal cycle: they start randomly and proceed in the round-robin sequence. Let’s now show the last upstreams chosen during the two cycles. As soon as servers from upstreams u01 and u02 in the normal cycle will always return unacceptable status 503 we must expect that upstream b01 from backup cycle will always be the last. To check this we will add status 200 which is normally returned by echo into the list of directive next_upstream_statuses in the upstrand us1.
upstrand us1 { upstream ~^u0; upstream b01 backup; order start_random; next_upstream_statuses 200 204 5xx; }Run curls.
curl 'http://localhost:8010/echo/us1' b01 curl 'http://localhost:8010/echo/us1' b01 curl 'http://localhost:8010/echo/us1' b01Nice, as expected. And now let’s show all upstreams the router tries before sending the last response. For such a task there is another upstrand block directive debug_intermediate_stages (do not use it in other cases besides testing because it is not designed for normal usage in upstrands).
upstrand us1 { upstream ~^u0; upstream b01 backup; order start_random; next_upstream_statuses 200 204 5xx; debug_intermediate_stages; }Restart nginx and run curls again.
curl 'http://localhost:8010/echo/us1' b01 u02 u01 curl 'http://localhost:8010/echo/us1' b01 u01 u02 curl 'http://localhost:8010/echo/us1' b01 u02 u01 curl 'http://localhost:8010/echo/us1' b01 u01 u02Looks interesting. The last upstream is shown first and the first upstream is shown last. Do not worry, this is an artefact of the upstrand implementation. And now I want to say a little bit about the implementation. All standard and third-party directives like proxy_pass and echo that magically traverse upstreams in an upstrand refer to a magic variable whose name starts with upstrand_ and ends with the upstrand name. Such magic variables are created automatically in the configuration handler ngx_http_upstrand_block() for each declared upstrand. The handler of these variables ngx_http_upstrand_variable() creates the combined upstreams module’s request context with imported from the module’s main configuration indices of the next upstreams in both normal and backup cycles and shifts the main configuration’s indices forward for the future requests. The module installs response header and body filters. The header filter checks whether the response context exists (i.e. an upstrand variable was accessed) and if so it may start up a subrequest: this depends on the response status of the main request or the previous subrequest if they have been already launched. In nginx a subrequest runs the location’s content handler again. Remember that the both proxy and echo directives referred to our magic variable? This means that their content handlers accessed the variable’s handler on every subrequest because the upstrand variables had attribute nocacheable. The variable’s handler might feed the subrequests with the shifted index of the next upstream in the upstrand after checking whether to move to the backup cycle or finish the subrequests when all upstreams in the both cycles had been exhausted. In the latter case the final response was extracted from the subrequests’ response headers and body buffer chains and returned to the client. Want to know how? In case when the subrequests have been launched, original headers of the main request are replaced by headers of the last subrequest. Thanks to running subrequests from the header filter the response bodies nest in reverse order (this explains why our last test listed visited upstreams from last to first). This makes possible to break feeding next body filters with older responses bodies following the last response body and thus to return the body of the last subrequest. The beginning of the older response bodies is recognized by the last buffer in the last response’s buffer chain. Replacing the main request output headers and the response body buffer chain by those from the last response does the trick.
пятница, 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 ее можно использовать наряду с любыми другими директивами, доступными на этом уровне. Вот пример, который мы будем тестировать ниже:
Теперь перейдем к описанию модуля с точки зрения программирования и сборки. Отличным пособием по созданию модулей nginx могут служить два ресурса от Эвана Миллера (здесь и здесь). Поэтому я не буду поднимать общие вопросы, а перейду сразу к деталям данного модуля.
Прежде всего обратимся к процедуре сборки модуля. Данный модуль состоит из одного файла-исходника на языке C, в котором определена всего одна функция, и вспомогательного файла config, который нужен nginx во время сборки. Да, это может показаться неудобным, но все модули nginx должны линковаться статически во время сборки самого nginx. Файл config содержит мета-информацию о модуле (имя, пути, дополнительные подключаемые библиотеки и т.д.). Вот его содержимое:
После загрузки и распаковки nginx и ngx_echo, конфигурируем nginx следующим образом:
Как я уже отметил, модуль ngx_http_upstream_add_upstream_module очень простой и состоит всего из одной функции, в которой описаны действия nginx при чтении директивы add_upstream. Кроме этой единственной функции в файле ngx_http_upstream_add_upstream_module.c объявлены объекты, необходимые nginx при инициализации модуля. Вот они:
Ниже приводится текст функции ngx_http_upstream_add_upstream(), которая определяет действия nginx в момент чтения директивы 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.
Update. Залил наработки из этой статьи на гитхаб. Теперь модуль называется nginx-combined-upstreams-module и кроме директивы add_upstreams в нем объявлена еще одна директива combine_server_singlets. Ее функции описаны в README.
Апстрим - одна из основных концепций 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 (i = 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 (j = 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.
Подписаться на:
Сообщения (Atom)