Липкие сессии для самых маленьких [Часть 1]

  • Tutorial

Липкие сессии (Sticky-session) — это особый вид балансировки нагрузки, при которой трафик поступает на один определенный сервер группы. Как правило, перед группой серверов находится балансировщик нагрузки (Nginx, HAProxy), который и устанавливает правила распределения трафика между доступными серверами.

В первой части цикла мы посмотрим как создавать липкие сессии с помощью Nginx. Во второй же части разберем создание подобной балансировки средствами Kubernetes.

Перед тем как настроить nginx, сделаем простенький сервис на фреймворке FastAPI для наглядной демонстрации распределения трафика. Создадим проект с виртуальным окружением Python 3.6+. В директории проекта должны находится следующие файлы:

Файл requirements.txt содержит несколько зависимостей:

fastapi==0.63.0
uvicorn==0.13.3

main.py содержит следующий код:

from fastapi import FastAPI
from uuid import uuid4

app = FastAPI()
uuid = uuid4()


@app.get("/")
async def root():
    return {'uuid': uuid}

Обратите внимание на переменную uuid, которая инициализируется вместе с FastAPI приложением. Переменная будет жить, пока работает сервер. Собственно, по значению этой переменной мы будем точно знать, что попали на тот же самый экземпляр приложения. Перед тем как запустить сервис нужно установить зависимости:

pip install -r requirements.txt

Запустить сервис можно командой:

uvicorn main:app --port 8080

Вывод будет такой:

Проверим работу сервиса с помощью Postman, указываем 0.0.0.0:8080 и нажимаем Send. Сервер ответит сгенерированным uuid:

Рассмотрим содержание Dockerfile. Предполагается, что у вас есть хотя бы небольшой опыт работы с контейнерами.

FROM python:3.8

WORKDIR app

COPY . /app

RUN pip install -r requirements.txt

EXPOSE 8080

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]

Это очень похоже на то, что мы сделали вручную: создали окружение, установили зависимости, запустили проект. Ну, разве что порт не публиковали.

Соберём образ (не забываем про точку):

docker build -t sticky:0.0.1 .

И запустим контейнер с пробросом портов:

docker run -p 8080:8080 sticky:0.0.1

Пощелкайте Postman убедитесь, что все также работает. Теперь сделаем запуск приложения в несколько реплик.

В корне директории проекта создадим файлик docker-compose.yaml. Вставим следующий код:

version: '3.4'

services:
    web:
        build:
            context: .
        ports:
            - "8080-8081:8080"

Данная инструкция запустит в docker-compose приложение web. В build указывается место расположения образа (в нашем случае Dockerfile лежит в корне директории) на основе которого собирается контейнер. В ports открываем порты 8080 и 8081 которые будут связаны с портом 8080 внутри контейнера. Запись в виде диапазона нужна при запуске приложения в несколько инстансов (реплик). Приложения сами займут свободные порты из предоставленного пула. Только единственное условие: количество портов в пуле должно быть больше либо равно количеству запускаемых реплик, иначе возникнет ошибка.

Запустить несколько контейнеров разом можно командой:

docker-compose up --build --scale web=2

В Postman проверим работу контейнеров 0.0.0.0:8080 и 0.0.0.0:8081 – отвечают оба сервера:

Теперь можно приступить к настройке Nginx!

Создадим папку nginx, а в нем файлик nginx.conf.

Внутри nginx.conf опишем следующее:

worker_processes auto;

events {

}

http {
    upstream sticky-app {
        server web:8080;
        server web:8081;

    }

    server {
        listen 80;
        location / {
            proxy_pass http://sticky-app;
        }
    }
}

Данный конфиг указывает nginx слушать порт 80 и весь трафик проксировать на сервера перечисленные в upstream. В данной конфигурации происходит балансировка типа round-robin, липкие сессии добавим попозже.

Теперь поднимем nginx вместе с приложением web. В docker-compose-yaml добавим следующее:

    nginx:
        image: nginx:latest
        container_name: my-nginx
        depends_on:
          - web
        volumes:
            - ./nginx/nginx.conf:/etc/nginx/nginx.conf
        ports:
            - 80:80
            - 443:443

Тут все просто. В image указывается какой официальный образ забрать при сборке. В Volumes мы монтируем наш конфиг из рабочей директории прямиком в контейнер. Это например, позволит нам изменять конфиг nginx и не пересобирать заново docker-compose, достаточно будет только перезапустить nginx контейнер.

Финальный штрих: подключим приложение web и nginx к новой сети app-net. Благодаря этому в конфиге nginx можно указывать DNS сервиса (web), который определен в docker-compose.

Полный docker-compose.yaml выглядит так:

version: '3.4'

services:

    web:
        build:
            context: .
        ports:
            - "8080-8081:8080"
        networks:
            - app-net

    nginx:
        image: nginx:latest
        container_name: my-nginx
        depends_on:
          - web
        volumes:
            - ./nginx/nginx.conf:/etc/nginx/nginx.conf
        ports:
            - 80:80
            - 443:443
        networks:
            - app-net

networks:
  app-net:
      driver: bridge

Удалим запущенные контейнеры:

docker-compose down

И запустим сервис в двух экземплярах вместе с nginx:

docker-compose up --build --scale web=2

В Postman отправляем запросы на порт 80 и видим, что балансировка работает – ответы приходят разные.

Теперь сделаем наконец липкие сессии! Обновим конфиг nginx добавив одну строчку в блок upstream:

Полный конфиг nginx
worker_processes auto;

events {

}

http {
    upstream sticky-app {
        hash $cookie_key;
        server web:8080;
        server web:8081;
    }

    server {
        listen 80;
        location / {
            proxy_pass http://sticky-app;
        }
    }
}

Nginx будет брать хэш от cookie с именем key и если такой хэш уже был - направит трафик на тот же сервер.

альтернатива для директивы hash

Помимо директивы hash, существует также ip_hash. В этом случае nginx будет перенаправлять трафик в зависимости от ip-адреса клиента.

Перезапустим docker-compose:

docker-compose down
docker-compose up --build --scale web=2

После сборки создадим в Postman cookie c именем key для адреса 0.0.0.0:

Проверим  0.0.0.0:80

Сколько бы я не отправлял запросы - ответ не меняется. Теперь изменим значение key:

И отправим запрос по тому же адресу:

Ответ изменился и больше не меняется с отправкой новых запросов. Готово! Таким простым способом можно реализовать липкие сессии в nginx.

немного саморефлексии

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

В NGINX PLUS (платная версия) реализованы продвинутые липкие сессии. Где можно указать таймаут по истечении которого трафик прилипнет к другому серверу из группы. Это более сбалансированные липкие сессии.

Что делать если не хочется платить? Можно разрулить это на стороне самого приложения и подставлять в cookie другое значение key спустя некоторое время. Но надо учитывать, что в таком случае нарушается принцип единой ответственности. Сервис не должен знать или уметь себя балансировать, это уже заботы вышестоящей инстанции.

В следующей части разберем создание липкой сессии в kubernetes.

ДомКлик
Место силы

Комментарии 9

    +2
    Очень понравилась статья. И про docker и про nginx, базовые сведения в хорошо изложенном материале.
      +4
      Я бы таки поменял местами

      COPY . /app
      RUN pip install -r requirements.txt
      # VVV
      RUN pip install -r requirements.txt
      COPY . /app


      Чтобы docker layers кешировались и не нужно было каждый раз переустанавливать зависимости.
        +1
        А как можно установить зависимости, если файл requirements.txt ещё не скопирован?
          +1
          Справедливо, тогда вот:

          FROM python:3.8

          EXPOSE 8080

          WORKDIR /app

          COPY requirements.txt requirements.txt
          RUN pip install -r requirements.txt

          COPY . .

          CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]


          Как раз именно так рекомендует собирать сам Docker.

          Из минусов — дважды будем копировать requirements.txt, но переживем)
            0
            Очень полезное замечание, спасибо!)

            Или вот аналогичный пример для poetry

            FROM python:3.8

            WORKDIR app

            EXPOSE 8080

            RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python -

            ENV PATH="${PATH}:/root/.poetry/bin"

            COPY poetry.lock pyproject.toml /app/

            RUN poetry config virtualenvs.create false && poetry install

            COPY . .

            CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
              0
              Да, прикольно. Метод кажется немного противоестественным для приложения, но гораздо лучше для докера. Запомню эту технику.
          0
          Всё коротко и чётко, мне нравится.
            0
            подключим приложение web и nginx к новой сети my-app

            в коде app-net
            networks:
              app-net:
                  driver: bridge
            
              0
              спасибо! поправил

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое