Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Липкие сессии (Sticky-session) — это особый вид балансировки нагрузки, при которой трафик поступает на один определенный сервер группы. Как правило, перед группой серверов находится балансировщик нагрузки (Nginx, HAProxy), который и устанавливает правила распределения трафика на доступные сервера.
В первой части цикла мы уже разобрали как создавать липкие сессии с помощью Nginx. Во второй части разберем создание подобной балансировки средствами Kubernetes.
Так как статьи в основном направлены на начинающих - придется коснуться основ kubernetes. Да-да, я знаю в интернете полно материала для изучения куба. Но здесь будет минимум душной теории и максимум практики. Лучше один раз развернуть тестовое приложение в кластере и понять основные принципы, чем читать тонну скучных мануалов.
Кто просто хочет узнать про реализацию липкой сессии в кубе - тыкните сюда.
Минимальный кластер куба состоит из двух узлов (Node) - master и worker. Узлы в кластере - это машины (виртуальные машины, физические серверы), на которых работают ваши приложения. Master узел представляет собой мозг kubernetes, там происходит управление всем кластером. Приложения, как правило, на мастер узле не разворачиваются (это конечно можно сделать, но это частный случай и выходит за рамки статьи). А вот на worker узлах приложения и разворачиваются. Для учебных целей иметь две машины и ставить туда куб не очень-то удобно. Поэтому придумали minikube.
Minikube - это одноузловой кластер, который является сразу master и worker нодой и всё это на локальной машине. В нем есть некоторые упрощения по сравнению с реальным кубом, например установка ingress-controller или запуск борды. Инструмент нужен в основном для локальной разработки и обучения.
Все примеры будут под систему MacOS. Установку для других систем смотрите в официальной документации.
brew install minikube
Чтобы запустить minikube выполните команду:
minikube start
Работать с кубом можно через командную строку, для этого нужно установить kubectl.
minikube kubectl --get po -A
Эта утилитка ставится и для взаимодействия с реальным кубом. Чтобы посмотреть дашборд с графиками и картиночками выполните:
minikube dashboard
Только учтите, что команда заблокирует вам консоль, поэтому лучше откройте еще одно окно.
На этом установка учебного кластера завершена. Как по мне, для понимания основ kubernetes нужно разобрать всего 4 ресурса:
Pod
Deployment
Service
Ingress
В таком порядке и будем рассматривать каждый ресурс по мере запуска приложения в кластере.
Подготовка приложения
Использовать будем код из предыдущей статьи.
from fastapi import FastAPI
from uuid import uuid4
app = FastAPI()
uuid = uuid4()
@app.get("/")
async def root():
return {'uuid': uuid}
Здесь переменная uuid инициализируется вместе с FastAPI приложением. Переменная будет жить, пока работает сервер. Собственно, по значению этой переменной мы будем точно знать, что попали на тот же самый экземпляр приложения.
Собираем образ:
docker build -t mopckou/sticky-session:0.0.5 .
После сборки я запушил образ в свой публичный репозиторий docker hub:
docker login
docker push mopckou/sticky-session:0.0.5
Всё, мы готовы разворачивать приложение. Для этого нужно кубу указать ряд правил, которыми он будет руководствоваться при запуске вашего приложения. Такие правила называются конфигурацией развертывания (Deployments).
Обычно любой ресурс в Kubernetes описывается в yaml файлах. В нашем случае правил развертывания не много, поэтому обойдемся короткой командой по созданию объекта deployment.
kubectl create deployment sticky-d --image=mopckou/sticky-session:0.0.5
В -–image можете указать свое приложение, которое залили в публичный репозиторий docker hub.
При создании ресурса deployment, создается так же пода (Pod) на узле. Пода это минимальная единица куба, в котором запускается контейнер или множество контейнеров докера. Deployment отслеживает состояние и работоспособность под. Если какая выйдет из строя, то он оперативно запустит новую.
Проверим, что создалось два ресурса deployment и pod.
kubectl get deployment,pods -l app=sticky-d
Про "kubectl get"
kubctl довольно user friendly, например в команде get можно попросить как pod, так и pods, deployment или deployments и т.д.. Можно через запятую (без пробела) перечислить ресурсы, которые хотим получить.
Масштабируем наше приложение до двух сервисов.
kubectl scale deployment sticky-d --replicas=2
И снова проверим состояние pods и deployment, вывод должен быть примерно такой:
Новая пода стала запускаться.
эксперименты с deployment
Как только две поды будут запущены, можно наглядно продемонстрировать работу deployment.
Удалим собственоручно поду pod/sticky-d-57444787d8-rdn6q:
Снова запросим текущее состояние pods и deployment:
Не успела пода *-rdn6q удалиться, на ее место пришла уже новая *-6gjmv.
Теперь давайте отправим запросы на развернутые приложения. Напрямую это сделать не удастся, для этого придумали сервис (Service).
Сервис (Service) в Kubernetes — это абстрактный объект (ресурс), который предоставляет доступ к запущенным подам. Хотя у каждого пода есть уникальный IP-адрес, эти IP-адреса не доступны за пределами кластера без использования сервиса. Сервисы позволяют приложениям принимать трафик и являются стандартным балансировщиком нагрузки на поды.
Сервисы могут по-разному открыты, в зависимости от указанного поля type:
ClusterIP (по умолчанию) - открывает доступ к сервису по внутреннему IP-адресу в кластере. Этот тип делает сервис доступным только внутри кластера.
NodePort - открывает сервис на одном и том же порту каждого выбранного узла в кластере с помощью NAT. Делает сервис доступным вне кластера, используя NodeIP:NodePort.
LoadBalancer - создает внешний балансировщик нагрузки в текущем облаке (если это поддерживается) и назначает фиксированный внешний IP-адрес для сервиса.
ExternalName - открывает доступ к сервису с указанным именем (определённое в поле externalName в спецификации) и возвращает запись CNAME. Прокси не используется. Для этого типа требуется версия kube-dns 1.7 или выше.
Создадим сервис с типом LoadBalancer. Балансировка происходит по принципу random balancing (про это можно почитать в этой статье, которая ссылается сюда).
kubectl expose deployment sticky-d --type=LoadBalancer --port=8080
kubectl get services sticky-d
Запустить сервис можно командой:
minikube service sticky-d
Вывод будет примерно такой:
Отправим запросы на сервис и убедимся, что трафик поступает на разные поды:
Балансировщик не работает? Или мы добились своей цели и получили липкие сессии? Почти!
Мы получили липки сессии курильщика. Дело в том, пока открыто TCP соединение с сервером куб направляет трафик только на одну определенную поду. Но если будет обрыв соединения или мы создадим новое соединение, то можем попасть уже на другую поду. Подробнее об этом смотрите в этой отличной статье. Не такие липкие сессии нужны Готему сейчас. Чтобы обойти эту особенность необходимо убрать галочку keep-alive в Postman.
Посмотрим, что произойдет:
Ура! Мы убедились, что трафик балансируется.
Ресурс сервис довольно бедный балансировщик и умеет распределять трафик для определенного развертывания. Тут вступает ingress, который довольно сильно расширяет возможности балансировки в kubernetes.
Если просто создать ресурс ingress, то ничего не произойдет. Нужно создать ingress-controller. Для активации ingress-controller выполните:
minikube addons enable ingress
проблемы с активацией ingress-controller?
Для macOS и Windows на момент написания статьи может произойти ошибка, чтобы это поправить нужно выполнить следующие команды:
minikube config set vm-driver hyperkit
minikube delete
minikube start
minikube addons enable ingress
Это приведет к стиранию всех развертываний и под. Вернитесь назад и повторите развертывание приложения и запуск сервиса.
При успешном запуске Ingress-controller запустится специальная пода, выполните команду:
kubectl get pods -n kube-system
Результат должен примерно таким:
Подробно на простых примерах зачем нужен ingress
Ingress позволяет балансировать трафик по имени хоста или на основе пути в запросе. Рассмотрим простой пример. Допустим у нас есть два приложения app1 и app2. После мы купили доменное имя, пусть будет awesome-company.com. И мы хотим чтобы наши сервисы были доступны по адресу:
awesome-company.com/app1
awesome-company.com/app2
Или даже так:
app1.awesome-company.com
app2.awesome-company.com
Можно так же сделать комбинацию этих двух вариантов.
Ресурс ingress как раз и позволяет это сделать. Ingress балансирует трафик на разные сервисы, являясь такой надстройкой над ними.
В двух словах описанная выше схема выполняется следующим образом. При создании ресурса ingress внешний балансировщик (если вы используете GKE или AWS) выдает ресурсу внешний IP адрес (допустим 1.1.1.1). Ingress при этом становится точкой входа в кластер по портам 80 и 443. Разрешаем доменные имена app1.awesome-company.com и app2.awesome-company.com на ip - 1.1.1.1. В самом ресурсе ingress указываем, что запросы с хостом app1.awesome-company.com проксируются на service app1, а соответственно запросы с хостом app2.awesome-company.com проксируются на service app2.
Теперь создадим ресурс Ingress. Сделайте новый файлик ingress.yml и вставьте следующий текст:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example-ingress
spec:
rules:
- host: sticky.info
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: sticky-d
port:
number: 8080
Обратите внимание на строчку host: sticky.info Нужно локально определить доменное имя и присвоить ему ip адрес ingress ресурса. Перед тем как определять локально доменное имя запустим ingress ресурс.
kubectl apply -f ingress.yml
Проверим, появился ли он:
kubectl get ingresses
Обратите внимание на адрес ресурса, если сделать запрос на 192.168.64.3:80 будет ошибка 404.
"Разрешим" локально доменное имя. Откройте файл /etc/hosts и в последней строчке через пробел запишите ip адрес ingress ресурса и доменное имя сервиса.
И попробуем отправить теперь запрос по адресу sticky.info:
Надо отметить, что балансировка работает несмотря на включенную галочку connection.
небольшой хак
Можно схитрить и не определять локально доменное имя. Просто в заголовке запроса указать хост:
Теперь наконец сделаем наш трафик липким. Добавим в Ingress.yml annotations c 4 параметрами:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example-ingress
annotations:
nginx.ingress.kubernetes.io/affinity: "cookie"
nginx.ingress.kubernetes.io/affinity-mode: "balanced"
nginx.ingress.kubernetes.io/session-cookie-name: "key"
nginx.ingress.kubernetes.io/session-cookie-max-age: 60
spec:
rules:
- host: sticky.info
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: sticky-d
port:
number: 8080
Рассмотрим каждый параметр подробнее:
affinity: укажите значение “cookie”, чтобы включить липкие сессии;
affinity-mode: режим прилипания. Поддерживается два режима — persistent и balanced. При значении balanced трафик будет прилипать к определенному поду только на некоторый промежуток времени. Для вечного прилипания укажите persistent;
session-cookie-name: имя cookie, по значению которого ingress-controller будет ассоциировать трафик с определенной подой;
session-cookie-max-age: время, на которое трафик прилипает к поду, в секундах (настройка нужна если affinity-mode = balanced). После истечения заданного времени ingress-controller сгенеририт новое значение для session-cookie-name и вставит нам в cookie.
Применим изменения:
kubectl apply -f ingress.yml
И проверим работу липких сессий в кубе:
Ответы будут приходить только от одного конкретного экземпляра приложения. Обратите внимание, что ingress-controller сам подставил нам в cookie значение для key и добавил срок годности прилипания. Через 60 секунд nginx-controller снова сгенерит значение для key, и трафик уже прилипнет к другой поде (но не факт, что к другой).
Теперь посмотрим как это выглядит в коде:
import asyncio
from aiohttp import ClientSession
from asyncio import sleep
async def foo():
session = ClientSession()
while 1:
response = await session.get('http://sticky.info')
uuid = (await response.json())['uuid']
print(f"answer: {uuid} /// cookie: {response.cookies}")
await sleep(1)
lp = asyncio.get_event_loop()
lp.run_until_complete(foo())
lp.close()
В цикле делаем запрос к приложению и выводим результат запроса и объект cookie.
Ingress-controller сгенерил key и подставил нам в cookie объект Set-Cookie. Трафик прилип на 60 секунд к определенной поде.
Готово! Мы успешно реализовали липкие сессии в Nginx и Kubernetes.