Введение в NGINX

Время от времени я читаю от некоторых людей, что NGINX (X-движок) сложный. Не буду врать: поначалу понять и использовать его непросто. Прежде всего, нелегко понять его назначение. Это сервер, но что он делает? Как он используется?

NGINX — это сервер, который можно использовать несколькими способами, такими как: балансировщик нагрузки (балансировка запросов для серверов приложений), обратный прокси (получение запроса из интернета и перенаправление его на другой сервер или сервис) и одна из самых фантастических возможностей NGINX, на мой взгляд, это его назначение в качестве кэша.

Щедрая часть интернета работает на NGINX, а некоторые очень важные сегменты используют его в качестве основного сервера, например, платформы потокового вещания, крупномасштабные приложения, такие как магазины, социальные сети, а также он используется в качестве основы для многих CDN.

Почему стоит использовать NGINX?

Возможно, вы уже спрашивали себя: почему я должен использовать NGINX? Не проще ли просто указать DNS на несколько IP?
Это возможно, но у вас не будет надежности, если вы направите свой DNS на балансировщик нагрузки, который сможет сбалансировать ваши запросы более интеллектуальным способом, с поддержкой проверки здоровья в случае, если серверы приложений не отвечают, и алгоритмов балансировки, реализующих правильную модель распределения запросов.

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

Давайте создадим нашу первую конфигурацию, очень простую, только с одним маршрутом, который всегда возвращает 200:

worker_processes auto;

events {
  worker_connections 1024;
}

http {
  server {
    listen 80;

    location / {
      return 200;
    }
  }
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Давайте используем Docker с помощью Docker Compose:

services:
  nginx:
    image: nginx:alpine
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    ports:
      - "8080:80"
Войдите в полноэкранный режим Выход из полноэкранного режима

Проверка нашего маршрута:

curl -v http://localhost:8080

< HTTP/1.1 200 OK
< Server: nginx/1.21.6
< Content-Type: text/plain
< Content-Length: 0
< Connection: keep-alive
Войдите в полноэкранный режим Выход из полноэкранного режима

Наш маршрут сработал. Он возвращает 200, как и ожидалось.

Давайте разберемся, что делает эта конфигурация, сосредоточившись только на блоке http. Блок http называется контекстом, как и события, и оба находятся в контексте под названием main

  • server — это блок, предназначенный для записи настроек сервера внутри вашей конфигурации. Вы можете иметь несколько таких устройств, каждое из которых будет обслуживать свой порт. Вы можете выставить один сервер на всеобщее обозрение и иметь другой внутренний, без кэширования, например, или даже в обход аутентификации, например.
  • listen — здесь вы определяете, на каком порту ваш сервер будет принимать соединения. Обратите внимание, что docker-compose.yml экспортирует 80 как 8080 для хоста, поэтому запрос выполняется на 8080 (порт 80 контейнера).
  • location — директива, используемая для определения маршрутов. Они довольно мощные. Они принимают регулярные выражения, вы можете захватывать переменные и использовать их в конфигурации. Система определения местоположения также учитывает различные типы соответствия.

    • Без модификатора совпадение происходит по началу URI
    • = точное совпадение
    • ~ — это совпадение регулярного выражения

Эти модификаторы я использовал чаще всего до сих пор.

Наше местоположение будет соответствовать любому URI, который начинается с /, т.е. любому URI типа /shopping, /products/list и т.д.

Мы можем добавить новое местоположение с точным совпадением.

location = /teste {
  return 200 "teste route";
}

location / {
  return 200;
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Теперь мы видим, что маршрут /test имеет тело, на которое отвечают.

curl -v http://localhost:8080/teste

< HTTP/1.1 200 OK
< Server: nginx/1.21.6
< Content-Type: text/plain
< Content-Length: 11
< Connection: keep-alive
teste route
Войдите в полноэкранный режим Выход из полноэкранного режима

Мы рассмотрели некоторые основы конфигурирования, теперь пришло время сделать что-то более сложное.

Использование в качестве обратного прокси-сервера

Упрощенное определение обратного прокси — это запрос, который вы делаете на какой-то внутренний адрес, например http://meusite.com.br/minhaloja/produtos/1, не зная, где именно этот запрос будет обработан. И после обработки обратный прокси-сервер отправляет вам содержимое ответа от сервера приложений.

Мы создадим тестовое приложение, которое отвечает на порту 8081, но доступ к которому мы будем получать через наш NGINX.

Приложение будет выполнено на языке Python с использованием Flask.

from flask import Flask

app = Flask(__name__)

@app.route("/")
def index():
    return "<p>Index page!</p>"
Войдите в полноэкранный режим Выход из полноэкранного режима

Мы изменяем конфигурацию нашего сервера, чтобы он содержал только маршрут приложения. Теперь с добавлением директивы proxy_pass, которая используется для того, чтобы сделать NGINX обратным прокси-сервером.

server {
  listen 80;

  location / {
    proxy_pass http://app:8081;
  }
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Обратите внимание, что мы используем имя сервиса, описанное (app) в docker-compose, для ссылки на имя, которое обслуживает приложение.

curl -v http://localhost:8080
< HTTP/1.1 200 OK
< Server: nginx/1.21.6
< Content-Type: text/html; charset=utf-8
< Content-Length: 18
< Connection: keep-alive
<p>Index page!</p>
Войдите в полноэкранный режим Выход из полноэкранного режима

Мы видим, что ответ имеет дополнительный заголовок (Content-Type), теперь в дополнение к ответу, который пришел от приложения.

Как сделать обратный прокси на несколько серверов?

Реальность приложения в производстве, в большинстве случаев, заключается в том, что для его обслуживания требуется более одного сервера. И это объясняется несколькими причинами. Серверы имеют ограниченные ресурсы (сетевая карта, диск, процессор) для обслуживания множества запросов. И дело не только в этом. Что делать, если на сервере произошел сбой оборудования? Или даже сбой в сети? Многочисленные причины могут привести к тому, что ваше приложение окажется без запасного варианта для этих случаев.

Именно здесь нам на помощь приходит директива NGINX upstream, используемая для определения набора серверов, используемых для балансировки вашего приложения.

Сначала мы изменим наш docker-compose, чтобы создать два экземпляра приложения. Для примера мы не будем использовать директиву scale, поскольку она имеет круговую балансировку, и мы хотим показать, как использовать директиву upstream с несколькими серверами приложений.

app_1:
  build: app
  ports:
    - "8081:8081"
  environment:
    - FLASK_RUN_PORT=8081
    - FLASK_RUN_HOST=0.0.0.0
app_2:
  build: app
  ports:
    - "8082:8081"
  environment:
    - FLASK_RUN_PORT=8081
    - FLASK_RUN_HOST=0.0.0.0
Войдите в полноэкранный режим Выход из полноэкранного режима

Давайте изменим нашу конфигурацию, чтобы создать восходящий поток и поместить два имени хостов нашего приложения:

upstream app {
  server app_1:8081;
  server app_2:8081;
  keepalive 100;
}

server {
  listen 80;

  location / {
    proxy_pass http://app;
  }
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Мы определили восходящий поток под названием app, и именно это имя мы будем использовать в директиве proxy_pass. Использование proxy_pass имеет некоторые преимущества перед использованием proxy_pass непосредственно с IP или именем хоста:

  • Определите различные стратегии балансировки для серверов приложений (различные алгоритмы балансировки с различными весами для каждого сервера);
  • Использовать keepalive, целое число общего количества TCP соединений, поддерживаемых NGINX с сервером, что позволяет избежать создания TCP соединений при каждом запросе;
  • Разрешение DNS выполняется, когда сервер идет вверх по потоку. Без определения восходящего потока имя типа google.com в proxy_pass будет генерировать DNS-запрос на каждый запрос. В больших масштабах это может стать узким местом (даже для внутреннего DNS вашей компании, если он существует).

Конфигурация, которую мы сделали, сбалансирует запросы равным образом, по 50% для каждого сервера. Глядя на журналы, мы можем видеть, как это происходит на практике:

nginx-intro-nginx-1  | 192.168.144.1 - - [20/May/2022:16:33:48 +0000] "GET / HTTP/1.1" 200 18 "-" "curl/7.79.1"
nginx-intro-app_1-1  | 192.168.144.4 - - [20/May/2022 16:33:48] "GET / HTTP/1.0" 200 -
nginx-intro-nginx-1  | 192.168.144.1 - - [20/May/2022:16:33:50 +0000] "GET / HTTP/1.1" 200 18 "-" "curl/7.79.1"
nginx-intro-app_2-1  | 192.168.144.4 - - [20/May/2022 16:33:50] "GET / HTTP/1.0" 200 -
nginx-intro-nginx-1  | 192.168.144.1 - - [20/May/2022:16:33:53 +0000] "GET / HTTP/1.1" 200 18 "-" "curl/7.79.1"
nginx-intro-app_1-1  | 192.168.144.4 - - [20/May/2022 16:33:53] "GET / HTTP/1.0" 200 -
nginx-intro-nginx-1  | 192.168.144.1 - - [20/May/2022:16:33:55 +0000] "GET / HTTP/1.1" 200 18 "-" "curl/7.79.1"
nginx-intro-app_2-1  | 192.168.144.4 - - [20/May/2022 16:33:55] "GET / HTTP/1.0" 200 -
Войдите в полноэкранный режим Выход из полноэкранного режима

Теперь давайте рассмотрим, как работает система кэширования NGINX.

Использование NGINX в качестве кэширующего сервера

Большая часть информации, которую мы потребляем, не обязательно должна быть постоянно на 100% актуальной. Если на нашем сайте есть статичный файл, например, скрипт, фотография или видео, нам не нужно постоянно получать или генерировать этот контент. Мы можем использовать NGINX в качестве кэш-сервера в несколько шагов, кэшируя несколько запросов от вашего приложения.

Сначала добавим маршрут, работающий с параметром запроса:

@app.route("/hello")
def hello():
    name = request.args.get("name", "nobody")
    return f"<p>Hello, {name}</p>"
Войдите в полноэкранный режим Выход из полноэкранного режима

Теперь давайте изменим NGINX так, чтобы он кэшировал ответы.

worker_processes auto;

events {
  worker_connections 1024;
}

http {
  proxy_cache_path /tmp/cache levels=1:2 keys_zone=app_cache:5m max_size=10m inactive=20m use_temp_path=off;

  upstream app {
    server app_1:8081;
    server app_2:8081;
    keepalive 100;
  }

  server {
    listen 80;

    location / {
      add_header X-Cache-Status $upstream_cache_status;
      proxy_cache app_cache;
      proxy_cache_valid 200 30s;
      proxy_cache_valid 404 1m;
      proxy_cache_key $host$uri$is_args$args;
      proxy_cache_lock on;
      proxy_pass http://app;
    }
  }
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Что означает каждая добавленная директива прокси?

  • proxy_cache_path — описывает, где мы будем кэшировать. Файлы кэша хранятся в архивах.
  • add_header — добавляет заголовок X-Cache-Status к ответу, чтобы мы могли отладить, когда произошли MISS, HIT и т.д.
    • HIT — содержимое получено непосредственно из кэша, так как оно все еще актуально
    • MISS — содержимое не было найдено в кэше и должно быть получено из источника
  • proxy_cache — зона кэширования. Эта же зона может быть использована в других местах конфигурации.
  • proxy_cache_valid — устанавливает время истечения срока действия кэша для определенного HTTP-кода. Наше приложение не определяет заголовок Cache-Control, поэтому мы настраиваем его непосредственно в NGINX.
  • proxy_cache_key — параметры, используемые для настройки ключа кэша. Это значение настройки чрезвычайно важно для создания безопасного и эффективного кэша.
  • proxy_cache_lock — предотвращает обращение к источнику более чем одного запроса для получения одного и того же содержимого. Важная директива для защиты происхождения контента. Соединения ждут, пока кэш не будет заполнен.

Проверив результат, мы можем сделать запрос на новый маршрут:

curl -v "http://localhost:8080/hello?name=bento"
Войдите в полноэкранный режим Выход из полноэкранного режима
nginx-intro-nginx-1  | 192.168.176.1 - - [20/May/2022:22:04:51 +0000] "GET /hello?name=bento HTTP/1.1" 200 19 "-" "curl/7.79.1"
nginx-intro-app_1-1  | 192.168.176.4 - - [20/May/2022 22:04:51] "GET /hello?name=bento HTTP/1.0" 200 -
nginx-intro-nginx-1  | 192.168.176.1 - - [20/May/2022:22:04:55 +0000] "GET /hello?name=bento HTTP/1.1" 200 19 "-" "curl/7.79.1"
nginx-intro-nginx-1  | 192.168.176.1 - - [20/May/2022:22:04:57 +0000] "GET /hello?name=bento HTTP/1.1" 200 19 "-" "curl/7.79.1"
nginx-intro-nginx-1  | 192.168.176.1 - - [20/May/2022:22:04:58 +0000] "GET /hello?name=bento HTTP/1.1" 200 19 "-" "curl/7.79.1"
nginx-intro-nginx-1  | 192.168.176.1 - - [20/May/2022:22:04:59 +0000] "GET /hello?name=bento HTTP/1.1" 200 19 "-" "curl/7.79.1"
nginx-intro-nginx-1  | 192.168.176.1 - - [20/May/2022:22:06:05 +0000] "GET /hello?name=bento HTTP/1.1" 200 19 "-" "curl/7.79.1"
nginx-intro-app_2-1  | 192.168.176.4 - - [20/May/2022 22:06:05] "GET /hello?name=bento HTTP/1.0" 200 -
Войдите в полноэкранный режим Выход из полноэкранного режима

Обратите внимание, как первый запрос попадает в NGINX и приложение. Последовательные запросы не попадают в приложение, потому что они используют кэш, который мы сказали серверу использовать.

В последних двух запросах видно, что запрос перешел к другому экземпляру приложения, потому что он прошел 30-секундный срок действия, который мы настроили, гарантируя, что кэш нуждается в повторной проверке и что балансировка продолжает работать.

Следующие шаги

Хотя это основы NGINX, этого также достаточно для того, чтобы вы смогли раскрыть больше возможностей его использования. В NGINX можно добавить множество прикладной логики, которая поможет вам создавать приложения, больше ориентированные на бизнес-правила, а не на балансировку, кэширование, авторизацию и т.д.

NGINX имеет поддержку встраивания динамического кода, основными из которых являются Lua и Javascript.

Оцените статью
Procodings.ru
Добавить комментарий