К Load Balancers в системах оркестрации (Kubernetes, Nomad и других) предъявляется больше требований, чем просто балансировка нагрузки. Во-первых, Load Balancer должен уметь читать каталог со списком сервисов, на которые необходимо перенаправлять трафик (или, как вариант, давать возможность сервисам регистрироваться на включение их в трафик). Во-вторых, делать это динамически, т.к. системы оркестрации в любой момент могут увеличить или уменьшить количество реплик сервисов, или переместить их на другие адреса в сети. И, в-третьих, делать это без остановки трафика.

В сегодняшнем сообщении я опишу работу с двумя Load Balancers — Traefik и HAProxy. Эти Load Balancers имеют возможность работать с внушительным списком средств оркестрации. В примерах будет описана работа с системой оркестрации Nomad.

В прошлом сообщении я уже приводил пример Load Balancers — Fabio. Его ограничения: работает только с http/https протоколами, и работает только с Consul. В отличие от Fabio, Load Balancers Troefik работает с впечатляющим количеством различных систем. Вот их неполный список, взятый с сайта разработчика: Docker, Swarm mode, Kubernetes, Marathon, Consul, Etcd, Rancher, Amazon ECS,…

Я продолжу пример из предыдущего сообщения, в котором создавалось несколько реплик сервиса django.

Traefik можно загрузить с сайта разработчика в виде исполняемого файла для наиболее распространенных операционных систем. Для интеграции с Nomad (собственно с Consul) необходимо создать файл конфигурации:

[entryPoints]
  [entryPoints.http]
  address = ":5001"

[web]
address = ":8080"

[consulCatalog]
endpoint = "127.0.0.1:8500"
domain = "consul.localhost"
exposedByDefault = false
prefix = "traefik"

И далее запустить на выполнение команду с приведенным файлом конфигурации:

traefik -c  nomad/traefik.toml

После этого на порте 8080 будет доступен UI Traefik, в котором пока не опубликован ни один сервис. Для публикации сервисов существует несколько способов, которые в конечном счете делают одно и тоже — загружают в систему Traefik данные типа ключ/значение. Мы воспользуемся возможностью задавать пары ключ/значение через tags сервисов. Дополним конфигурационный файл сервиса django параметром tags:

job "django-job" {
  datacenters = ["dc1"]
  type = "service"
  group "django-group" {
    count = 3
    restart {
      attempts = 2
      interval = "30m"
      delay = "15s"
      mode = "fail"
    }
    ephemeral_disk {
      size = 300
    }
    task "django-job" {
      driver = "docker"
      config {
        image = "apapacy/tut-django:1.0.1"
        port_map {
          lb = 8000
        }
      }
      resources {
        network {
          mbits = 10
          port "lb" {}
        }
      }
      service {
        name = "django"
        tags = [
          "traefik.enable=true",
          "traefik.frontend.entryPoints=http",
          "traefik.frontend.rule=Host:localhost;PathStrip:/test",
          "traefik.tags=exposed"
        ]
        port = "lb"
        check {
          name     = "alive"
          type     = "http"
          path     = "/"
          interval = "10s"
          timeout  = "2s"
        }
      }
    }
  }
}

В данном примере сервис будет опубликован на хосте localhost и примонтирован к роуту /test. В Troefik разработана гибкая и полная система правил для конфигурирования роутов, включая работу с регулярными выражениями. Перечень параметров для правил в документации разработчика.

После выполнения команды nomad job run nomad/django.conf правила применятся, и на сервис будет направлен трафик с Load Balancer. Соответственно, можно менять эти параметры, деплоить новый вариант сервиса командой nomad job run nomad/django.conf, и все изменения применятся без досадной остановки трафика.

Недостатком Troefik является то что он работает с протоколами семейства http/https (на всякий случай замечу что это семейство включает и веб-сокеты). Но все же есть такая вероятность, что возникнет необходимость работать и с другими протоколами. Поэтому перейдем к следующему более широкому решению на основе HAProxy. Какое-то время назад у HAProxy были проблемы с «мягкой» перегрузкой, что делало его использование с системами оркестрации сложным (на время рестарта приходилось на сетевом уровне приостанавливать движение пакетов). Сейчас это уже не проблема.

Для начала необходимо установить haproxy на компьютер. Здесь не подойдет вариант с установкой внутри контейнера. В haproxy только недавно появилась возможность рестартовать процесс в «мягком» режиме, но при этом все равно контейнер докер останавливается, так как фактически запускается второй процесс с haproxy, просто их смена проходит в режиме ожидания — что не работает с докером и его принципом «один-контейнер-один процесс».

Для работы haproxy необходимо иметь файл конфигурации, в котором прописаны необходимые правила. В Nomad (собственно в Consul) применяется система шаблонов, которая может генерировать конфигурации:

global
  debug
defaults
  log global
  mode http
  option httplog
  option dontlognull
  timeout connect 5000
  timeout client 50000
  timeout server 50000
frontend http_front
  bind *:5001
  stats uri /haproxy?stats
  default_backend http_back
backend http_back
  balance roundrobin{{range service "django"}}
  server {{.Node}} {{.Address}}:{{.Port}} check{{end}}

Ключевое слово range в данном случае выполняет роль итератора. Для трех сервисов «django» будет сформирован такой файл конфигурации:

global
  debug
defaults
  log global
  mode http
  option httplog
  option dontlognull
  timeout connect 5000
  timeout client 50000
  timeout server 50000
frontend http_front
  bind *:5001
  stats uri /haproxy?stats
  default_backend http_back
backend http_back
  balance roundrobin
  server 228.195.86.224 127.0.0.1:21469 check
  server 228.195.86.224 127.0.0.1:25872 check
  server 228.195.86.224 127.0.0.1:25865 check

Для запуска процесса генерации по шаблону «на лету» применяется библиотека https://github.com/hashicorp/consul-template. С ресурса разработчика можно загрузить исполняемый файл для всех распространенных операционных систем, и запускать процесс от имени не привилегированного пользователя командой:

consul-template -template="haproxy/haproxy.cfg.tmpl:haproxy/haproxy.cfg:./haproxy/haproxy.reload.sh"

Параметр -template содержит разделенные двоеточием параметры 1) имя шаблона, 2) имя получаемого файла конфигурации 3) команда, которая выполняется после генерации файла. Файл будет автоматически генерироваться если будут изменены переменные входящие в шаблон (например изменено количество реплик сервиса django).

После запуска шаблонизатора, который сгенерирует первую конфигурацию можно запускать haproxy:

haproxy -D -f haproxy/haproxy.cfg -p `pwd`/haproxy.pid

Мы явно указываем pid-файл, для того чтобы иметь возможность отправить сигнал на «мягкую» перегрузку haproxy:

haproxy -D -f ./haproxy/haproxy.cfg -p `pwd`/haproxy.pid -sf $(cat `pwd`/haproxy.pid)

В данном примере сервис опубликован на порту 5001. На этом же порту можно просмотреть статистику самого haproxy по адресу /haproxy?stats.

UPDATED 24.02.2019 22:43

По комментарию @usego было проведено дополнительное уточнение работы haproxy в контейнере докер, в частности по фрагменту из документации github.com/neo4j/docker-library-docs/tree/master/haproxy#reloading-config

Reloading config

If you used a bind mount for the config and have edited your haproxy.cfg file, you can use HAProxy's graceful reload feature by sending a SIGHUP to the container:

$ docker kill -s HUP my-running-haproxy

The entrypoint script in the image checks for running the command haproxy and replaces it with haproxy-systemd-wrapper from HAProxy upstream which takes care of signal handling to do the graceful reload. Under the hood this uses the -sf option of haproxy so «there are two small windows of a few milliseconds each where it is possible that a few connection failures will be noticed during high loads» (see Stopping and restarting HAProxy).


При таком подходе действительно происходит перезагрузка конфигурации, но в результате прерывания текущего процесса. А это означает что сервисы будут иметь хотя и очень незначительный, но все же период недоступности и часть клиентов может наблюдать сообщение об ошибке. Но иногда это не является главным критерием выбора. Поэтому приведу в дополнение конфигурацию docker-compose.yml для запуска haproxy в докере:

version: '3'
services:
  haproxy_lb:
    image: haproxy
    volumes:
      - ./haproxy:/usr/local/etc/haproxy
    network_mode: host


Также изменится команда, которая будет перегружать конфигурацию haproxy:

consul-template -template="haproxy/haproxy.cfg.tmpl:haproxy/haproxy.cfg:docker kill -s HUP $(docker-compose ps -q haproxy_lb)"


К преимуществам такой реализации следует отнести возможность работать без инсталляции haproxy.

Код примеров доступен в репозитарии

Полезные ссылки:

1. www.haproxy.com/blog/haproxy-and-consul-with-dns-for-service-discovery
2. m.mattmclaugh.com/traefik-and-consul-catalog-example-2c33fc1480c0
3. www.hashicorp.com/blog/load-balancing-strategies-for-consul

apapacy@gmail.com
24 февраля 2019 года

Комментарии (11)


  1. usego
    24.02.2019 22:19
    +1

    > В haproxy только недавно появилась возможность рестартовать процесс в «мягком» режиме, но при этом все равно контейнер докер останавливается

    Это точно? Сам не пробовал, но в официальном докере написано:

    Reloading config
    If you used a bind mount for the config and have edited your haproxy.cfg file, you can use HAProxy's graceful reload feature by sending a SIGHUP to the container:

    $ docker kill -s HUP my-running-haproxy


    1. apapacy Автор
      25.02.2019 00:08

      Спасибо за дополнение. Я дополнил статью Вашим примером из документации.
      В этом случае будет «жесткая» перегрузка haproxy. Т.к. механизм «мягкой загрузки» подразумевает одновременную работу двух процессов. В документации haproxy, на которую ссылается Ваш источник это процесс описан так:

      The graceful stop is triggered when the
      SIGUSR1 signal is sent to the haproxy process. It consists in only unbinding
      from listening ports, but continue to process existing connections until they
      close. Once the last connection is closed, the process leaves.


  1. citius
    25.02.2019 00:27
    +1

    Про fabio зря сказано, что он работает только с Nomad, т.к. на самом деле работает он только с Consul, но сервисы в консуле можно регистрировать как угодно, хоть Nomad, хоть простые виртуалки, хоть другие системы окрестрации.


    1. apapacy Автор
      25.02.2019 00:41

      Исправил в тексте


  1. powerman
    25.02.2019 07:23

    Было бы интересно почитать общее сравнение разных способов реализовать балансировщик, начиная с вариантов "для бедных" вроде DNS плюс обычный nginx (через resolver a.b.c.d; и proxy_pass $backend;), встроенный балансировщик docker swarm, AWS ELB, вышеупомянутые tr?fik и haproxy, платный NGINX Plus, …


    1. alexesDev
      25.02.2019 09:15
      +1

      Оно есть на английском
      www.hashicorp.com/blog/load-balancing-strategies-for-consul

      Там не указан traefik, но это можно просто в его доке посмотреть (в разделе Consul Catalog).


  1. Valsha
    25.02.2019 18:28

    Добрый день. Очень интересная тема, как раз последние 2-3 дня разбираюсь с Load Balancers для систем оркестрации, в моем случае это Docker Swarm.

    Подскажите пожалуйста, я использовал этот материал для развертывания и деплоя — habr.com/ru/post/344324 (запущен 1 manager и 3 воркера)
    В данном примере используется nginx как Load Balancers, но там он работает в моем случае (возможно я что то упустил) очень плохо, часто вижу ошибку 502 Bad Gateway (тайминги/задержки в nginx выставил по 900ms)

    При этом в логах
    dz6prru com.docker.swarm.node.id=tyllayuan6hd5uhtkr9nlz55y,com.docker.swarm.service.id=aincbpoo6d4etcrcat4upmba7,com.docker.swarm.task.id=dz6prrugfy9lypymsvdxomxdp 2019/02/24 22:21:00 [emerg] 1#1: host not found in upstream «web» in /etc/nginx/conf.d/flask.conf:26
    mvchude 10.255.0.2 — - [24/Feb/2019:22:23:38 +0000] «GET / HTTP/1.1» 499 0 "-" «Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0; Waterfox) Gecko/20100101 Firefox/56.2.6» "-"
    54ukzuf 10.255.0.2 — - [24/Feb/2019:22:25:50 +0000] «GET / HTTP/1.1» 502 157 "-" «Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0; Waterfox) Gecko/20100101 Firefox/56.2.6» "-"
    54ukzuf 10.255.0.2 — - [25/Feb/2019:08:41:05 +0000] «GET / HTTP/1.1» 499 0 "-" «Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0; Waterfox) Gecko/20100101 Firefox/56.2.6» "-"
    mvchude 2019/02/24 22:39:53 [error] 6#6: *3 connect() failed (110: Connection timed out) while connecting to upstream, client: 10.255.0.2, server:, request: «GET / HTTP/1.1», upstream: «10.0.1.94:5000/», host: «192.168.21.66:88»
    54ukzuf 2019/02/24 22:25:50 [error] 6#6: *1 connect() failed (110: Connection timed out) while connecting to upstream, client: 10.255.0.2, server:, request: «GET / HTTP/1.1», upstream: «10.0.1.94:5000/», host: «192.168.21.66:88»
    mvchude 10.255.0.2 — - [24/Feb/2019:23:00:42 +0000] «GET / HTTP/1.1» 200 71 "-" «Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0; Waterfox) Gecko/20100101 Firefox/56.2.6» "-"


    Я по ману пробовал настроить Traefik docs.traefik.io/user-guide/swarm-mode
    1. docker network create --driver=overlay traefik-net
    2. docker service create \
    --name traefik \
    --constraint=node.role==manager \
    --publish 90:90 --publish 9090:9090 \
    --mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
    --network traefik-net \
    traefik \
    --docker \
    --docker.swarmMode \
    --docker.domain=traefik \
    --docker.watch \
    --api

    Но на manager_swarm_ip:9090 ничего не открывает тоже.
    Даже и не знаю куда еще «копать».
    Может вы что то посоветуете,
    Спасибо.


    1. apapacy Автор
      25.02.2019 19:52

      Я со swarm не разбирался. Судя по строчке в логах host not found in upstream не настроена сеть.


      1. powerman
        25.02.2019 20:14

        В swarm встроенный балансировщик: подключение на открытый внешний порт любого узла swarm-а автоматически перенаправляет запрос на внутренний порт одного из тех узлов swarm, на котором сейчас запущен данный сервис (контейнер).


        1. Valsha
          26.02.2019 12:50

          Это понятно, что в «swarm встроенный балансировщик», просто не могу разобратся чего одна и та же нода, может так вот работать:

          54ukzuf 10.255.0.2 - - [24/Feb/2019:22:25:50 +0000] "GET / HTTP/1.1" 502 157 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0; Waterfox) Gecko/20100101 Firefox/56.2.6" "-"
          54ukzuf 10.255.0.2 - - [25/Feb/2019:08:41:05 +0000] "GET / HTTP/1.1" 499 0 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0; Waterfox) Gecko/20100101 Firefox/56.2.6" "-"
          54ukzuf 10.255.0.2 - - [25/Feb/2019:20:44:59 +0000] "GET / HTTP/1.1" 200 71 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0; Waterfox) Gecko/20100101 Firefox/56.2.6" "-"
          54ukzuf 10.255.0.2 - - [25/Feb/2019:20:45:00 +0000] "GET / HTTP/1.1" 200 71 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0; Waterfox) Gecko/20100101 Firefox/56.2.6" "-"
          54ukzuf 10.255.0.2 - - [25/Feb/2019:20:45:06 +0000] "GET / HTTP/1.1" 499 0 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0; Waterfox) Gecko/20100101 Firefox/56.2.6" "-"
          54ukzuf 10.255.0.2 - - [26/Feb/2019:04:40:13 +0000] "GET / HTTP/1.1" 499 0 "-" "NetSystemsResearch studies the availability of various services across the internet. Our website is netsystemsresearch.com" "-"
          54ukzuf 10.255.0.2 - - [26/Feb/2019:08:56:37 +0000] "GET / HTTP/1.1" 200 71 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0; Waterfox) Gecko/20100101 Firefox/56.2.6" "-"
          54ukzuf 10.255.0.2 - - [26/Feb/2019:08:56:39 +0000] "GET / HTTP/1.1" 200 72 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0; Waterfox) Gecko/20100101 Firefox/56.2.6"


          Понтяно было бы если разные ноды и может проблема была с каким то из узлов, а так…


          1. powerman
            26.02.2019 16:50

            Я отвечал не Вам. А по Вашей проблеме смотреть надо логи backend-сервиса, в который эти запросы передаются, плюс смотреть не перезапускаются ли постоянно какие-нибудь контейнеры. 499 обычно означает, что backend слишком долго обрабатывал запрос и клиент отвалился, 502 что nginx не достучался до backend — в общем, проблема не с nginx, и его логи тут не сильно помогут.