вторник, 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, присланный из некоторого апстрима, обозначает отсутствие данных и клиенту следует тут же проверить наличие данных в другом апстриме, то можно ограничить общение клиента с бэкендом всего одним запросом, перенеся опрос всех направлений-апстримов на плечи роутера, в котором все эти апстримы будут помещены в один апстрэнд. Такой подход полезен еще тем, что инкапсулирует знание топологии бэкендов внутри роутера, ведь клиентам это знание больше не нужно.

Комментариев нет:

Отправить комментарий