Использование Consul для service discovery и других задач

26 января 2016

Многие слышали, что есть такая штука под названием Consul, созданная в HashiCorp, компании, подарившей миру Vagrant, Packer и целый ряд других замечательных вещей. Кто-то даже знает, что Consul предназначен для service discovery, как и, например, etcd или ZooKeeper. Но далеко не всем известно, что помимо service discovery также Consul имеет огромное количество других возможностей, например, встроенный мониторинг сервисов, распределенные локи, и другие. В этой заметке мы познакомимся с Consul, а также научимся пользоваться хоть и не всем, но существенной частью его функционала.

Коротко о главном

Когда говорят «service discovery», имеют ввиду, что есть некая система, которая знает, где какой сервис находится. Например, вы используете PostgreSQL. У вас есть несколько шардов базы данных, у каждого шарда есть мастер и множество реплик. Когда падает мастер, вручную или автоматически производится фейловер, в результате которого одна из бывших реплик становится мастером. Другими словами, роль мастера является переходящей. Кроме того, число реплик не обязательно является постоянным. Так вот, service discovery — это такая штука, которая может ответить вашему приложению на вопросы «а кто сейчас является мастером у шарда N?» или «дай мне список всех реплик шарда N».

Конечно, можно тупо хранить всю эту информацию в конфигах. Но в случае, когда серверов и приложений на них много, вариант с service discovery становится удобнее. Как минимум, потому что все данные лежат в одном месте, и потому не разъезжаются, а также потому что конфигурация обновляется сильно быстрее, чем при заливке конфигов на все сервера по scp. Также при использовании кластерных платформ вроде Kubernetes, Mesos или Nomad вы не знаете заранее, на каких именно машинах какие сервисы поднимаются и сколько их сейчас, плюс они могут часто переезжать с одной машины на другую. А значит, вы никогда их не найдете, не имея под рукой решение вроде Consul.

Конкретно Consul для решения задачи service discovery предлагает REST API. Также он имеет встроенный DNS сервер, что позволяет использовать его даже в приложениях, которые ничего не знают ни о каком Consul. При этом Consul умеет масштабироваться на несколько датацентров и в отличие, скажем, от etcd, работает не только на Linux и MacOS, но также и на FreeBSD, и даже на Windows. Еще внушает доверие, что HashiCorp тестирует Consul при помощи Jepsen.

В Consul информация о существующих сервисах называется каталогом. Каталог хранится на нескольких серверах Consul, которые общаются между собой при помощи протокола Raft. Помимо серверов также существуют агенты. Агенты устанавливаются на всех машинах, где крутятся сервисы, и сообщают серверам Consul’а текущее состояние как машины, так и сервисов на ней, используя протокол gossip. Таким образом, если машина падает, или одному из сервисов на ней становится плохо, сервера Consul’а узнают об этом, и информация о соответствующих сервисах перестает отдаваться клиентам. Сами сервера также могут быть использованы в качестве агентов.

Помимо service discovery и мониторинга в Consul также есть много другого полезного функционала. С некоторым мы еще успеем познакомимся далее.

Установка и настройка Consul

Все описанные действия были проверены мной на Ubuntu 14.04 LTS, запущенной в трех LXC контейнерах. Скорее всего, на других дистрибутивах Linux и версиях Ubuntu ситуация будет мало чем отличаться. Далее предполагается, что все машины находятся в сети 10.0.3/24.

Consul написан на языке Go и распространяется в виде единственного бинарника (и исходных кодов, само собой разумеется). К сожалению, компания HashiCorp не предоставляет готовых пакетов для Debian/Ubuntu или CentOS/RHEL. Здесь добрый человек создал репозиторий, используя который, можно самостоятельно собрать deb-пакет. Также удалось найти PPA, хоть и не с самой новой версией Consul. Однако ничто не мешает поставить Consul, используя этот PPA, а потом при необходимости подменить бинарник.

Итак, установка:

sudo apt-get update
sudo apt-get install software-properties-common
sudo apt-add-repository ppa:bcandrea/consul
sudo apt-get update
sudo apt-get install consul consul-web-ui dnsutils curl jq

Если хотим самый свежак, также выполняем команды вроде таких:

sudo apt-get install unzip
wget https://releases.hashicorp.com/path/to/consul.zip
unzip consul_VER_linux_amd64.zip
sudo service consul stop
sudo mv /usr/bin/consul /usr/bin/consul.backup
sudo mv consul /usr/bin/consul

Правим файл /etc/consul.d/20-agent.json как-то так:

{
"server": true,
"datacenter": "dc1",
"bootstrap_expect": 3,
"data_dir": "/opt/consul",
"log_level": "INFO"
}

… делая тем самом Consul сервером, а не агентом.

Затем:

sudo service consul restart

На других нодах делаем так же. Затем объединяем их к властер:

consul join 10.0.3.223 10.0.3.224

Агенты подключаются аналогично, только в конфиге достаточно указать DC.

Говорим:

consul members

Должны увидеть что-то вроде:

Node                Address          Status  Type    Build  Protocol
postgresql-master   10.0.3.245:8301  alive   server  0.6.2  2
postgresql-slave-2  10.0.3.224:8301  alive   server  0.6.2  2
postgresql-slave    10.0.3.223:8301  alive   server  0.6.2  2

Также информацию о кластере можно полчить через API:

curl -s localhost:8500/v1/catalog/nodes | jq .

Пример ответа:

[
  {
    "ModifyIndex": 78,
    "CreateIndex": 3,
    "Address": "10.0.3.245",
    "Node": "postgresql-master"
  },
  {
    "ModifyIndex": 72,
    "CreateIndex": 4,
    "Address": "10.0.3.223",
    "Node": "postgresql-slave"
  },
  {
    "ModifyIndex": 45,
    "CreateIndex": 5,
    "Address": "10.0.3.224",
    "Node": "postgresql-slave-2"
  }
]

Если не работает, курим логи — /var/log/upstart/consul.log.

А так можно пробросить порт до веб-панельки:

ssh -N -L 8500:localhost:8500 ubuntu@10.0.3.223

Панелька выглядит примерно так (кликабельно, 67 Кб, 1369x793):

Веб-интерфейс Consul

Она немного глючная, например, не видит появления новых сервисов без нажатия F5. Но в остальном вроде вполне неплоха.

Использование key-value хранилища

Самый простой способ использования Consul — это использовать его тупо в качестве key-value базы данных. В этом случае можно даже не поднимать никаких агентов и не заморачиваться по поводу мониторинга сервисов.

Пример записи:

curl -X PUT -d 'value1' http://localhost:8500/v1/kv/group1/key1
curl -X PUT -d 'value2' http://localhost:8500/v1/kv/group1/key2
curl -X PUT -d 'value3' http://localhost:8500/v1/kv/group1/key3

Эти данные разлатаются по вему кластеру. На соседней машине можно сказать:

curl -s http://localhost:8500/v1/kv/group1/key1 | jq .

Пример ответа:

[
  {
    "ModifyIndex": 21,
    "CreateIndex": 21,
    "Value": "dmFsdWUx",
    "Flags": 0,
    "Key": "group1/key1",
    "LockIndex": 0
  }
]

Как видите, значение лежит в base64, декодировать его можно так:

curl -s http://localhost:8500/v1/kv/group1/key1 | \
  jq -r '.[0]["Value"]' | \
  base64 -d

Обновление:

curl -X PUT -d 'new-value3' http://localhost:8500/v1/kv/group1/key3

Удаление:

curl -X DELETE http://localhost:8500/v1/kv/group1/key3

Заметили выше ModifyIndex? Это поле предусмотрено специально для compare and swap:

curl -X PUT -d 'new-val' http://localhost:8500/v1/kv/group1/key2?cas=22

Если данные не успели поменяться, запись произойдет успешно, и будет получен ответ true. Иначе CAS не пройдет и придет ответ false.

Еще можно подождать изменений ключа так:

curl -s 'http://localhost:8500/v1/kv/group1/key1?index=21&wait=5s' | jq .

Ответ:

[
  {
    "ModifyIndex": 249,
    "CreateIndex": 21,
    "Value": "bmV3LXZhbHVlMQ==",
    "Flags": 0,
    "Key": "group1/key1",
    "LockIndex": 0
  }
]

Если параметр wait не указан, ждать будем вечно.

Service discovery через REST API

Добавление сервиса:

curl -XPUT -d @req.json http://10.0.3.223:8500/v1/agent/service/register

… где файл req.json содержит:

{
  "ID": "postgresql-replica-1",
  "Name": "postgresql-replica",
  "Tags": [
    "postgresql"
  ],
  "Address": "10.0.3.223",
  "Port": 5432,
  "Check": {
    "Script": "service postgresql status",
    "Interval": "5s"
  }
}

Важно! Сервисы нужно добавлять через агент, который крутится на той же машине, что и сам сервис. Иначе сервис добавится, но где-то через минуту будет удален:

[INFO] agent: Deregistered service 'postgresql-replica-1'

Еще обратите внимание на .Check.Script. Здесь мы просто проверяем, что сервис с именем postgresql запущен. На практике, скорее всего, вам понадобятся более сложные проверки, выполняющие какие-то простые запросы к сервису, учитывающие время отставания мастера от реплики, и так далее.

Список всех доступных сервисов и их тэги:

curl -s http://10.0.3.224:8500/v1/catalog/services | jq .

Пример ответа:

{
  "postgresql-replica": [
    "postgresql"
  ],
  "consul": []
}

Вся информация по одному сервису:

curl -s http://10.0.3.224:8500/v1/catalog/service/postgresql-replica \
  | jq .

Пример ответа:

[
  {
    "ModifyIndex": 804,
    "CreateIndex": 802,
    "Node": "postgresql-slave",
    "Address": "10.0.3.223",
    "ServiceID": "postgresql-replica-1",
    "ServiceName": "postgresql-replica",
    "ServiceTags": [
      "postgresql"
    ],
    "ServiceAddress": "10.0.3.223",
    "ServicePort": 5432,
    "ServiceEnableTagOverride": false
  },
  {
    "ModifyIndex": 842,
    "CreateIndex": 841,
    "Node": "postgresql-slave-2",
    "Address": "10.0.3.224",
    "ServiceID": "postgresql-replica-2",
    "ServiceName": "postgresql-replica",
    "ServiceTags": [
      "postgresql"
    ],
    "ServiceAddress": "10.0.3.224",
    "ServicePort": 5432,
    "ServiceEnableTagOverride": false
  }
]

Сказать по правде, не до конца понимаю, почему нужны два поля — Address и ServiceAddress. Подозреваю, это на случай, если мы хотим мониторить нашим агентами сторонние сервисы.

Список сервисов на заданной машине:

curl -s http://10.0.3.224:8500/v1/catalog/node/postgresql-slave | jq .

Пример ответа:

{
  "Services": {
    "postgresql-replica-1": {
      "ModifyIndex": 804,
      "CreateIndex": 802,
      "EnableTagOverride": false,
      "Port": 5432,
      "Address": "10.0.3.223",
      "Tags": [
        "postgresql"
      ],
      "Service": "postgresql-replica",
      "ID": "postgresql-replica-1"
    },
    "consul": {
      "ModifyIndex": 72,
      "CreateIndex": 4,
      "EnableTagOverride": false,
      "Port": 8300,
      "Address": "",
      "Tags": [],
      "Service": "consul",
      "ID": "consul"
    }
  },
  "Node": {
    "ModifyIndex": 804,
    "CreateIndex": 4,
    "Address": "10.0.3.223",
    "Node": "postgresql-slave"
  }
}

Удаление сервиса из агента:

curl http://localhost:8500/v1/agent/service/deregister/postgresql-master

Проверить, жив ли сервис, можно так:

curl -s http://localhost:8500/v1/health/service/postgresql-replica | jq .

Ответ в этом случае приходит довольно большой, поэтому здесь он не приводится.

Service discovery при помощи DNS

Пример запроса:

dig @127.0.0.1 -p 8600 postgresql-replica.service.consul

Пример ответа:

;; QUESTION SECTION:
;postgresql-replica.service.consul. IN A

;; ANSWER SECTION:
postgresql-replica.service.consul.  0 IN A 10.0.3.223
postgresql-replica.service.consul.  0 IN A 10.0.3.224

Можно запросить SRV запись, чтобы в ответе были еще и номера портов:

dig srv @127.0.0.1 -p 8600 postgresql-replica.service.consul

Ответ в этом случае:

;; QUESTION SECTION:
;postgresql-replica.service.consul. IN SRV

;; ANSWER SECTION:
postgresql-replica.service.consul.  0 IN SRV 1 1 5432
                                    postgresql-slave-2.node.dc1.consul.
postgresql-replica.service.consul.  0 IN SRV 1 1 5432
                                    postgresql-slave.node.dc1.consul.
;; ADDITIONAL SECTION:
postgresql-slave-2.node.dc1.consul. 0 IN A 10.0.3.224
postgresql-slave.node.dc1.consul.   0 IN A 10.0.3.223

Ноль в обоих ответах— это TTL, то есть DNS-ответ не должен кэшироваться.

Хелсчеки

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

Получить информацию о состоянии сервиса и его хоста можно через такую ручку:

curl -s http://10.0.3.224:8500/v1/health/service/postgresql-master | jq .

Пример ответа:

[
  {
    "Checks": [
      {
        "ModifyIndex": 13148,
        "CreateIndex": 13148,
        "Node": "postgresql-master",
        "CheckID": "serfHealth",
        "Name": "Serf Health Status",
        "Status": "passing",
        "Notes": "",
        "Output": "Agent alive and reachable",
        "ServiceID": "",
        "ServiceName": ""
      },
      {
        "ModifyIndex": 13155,
        "CreateIndex": 13150,
        "Node": "postgresql-master",
        "CheckID": "service:postgresql-master",
        "Name": "Service 'postgresql-master' check",
        "Status": "critical",
        "Notes": "",
        "Output": "9.5/main (port 5432): down\n",
        "ServiceID": "postgresql-master",
        "ServiceName": "postgresql-master"
      }
    ],
    "Service": {
      "ModifyIndex": 13155,
      "CreateIndex": 13150,
      "EnableTagOverride": false,
      "Port": 5432,
      "Address": "10.0.3.245",
      "Tags": [
        "postgresql"
      ],
      "Service": "postgresql-master",
      "ID": "postgresql-master"
    },
    "Node": {
      "ModifyIndex": 13155,
      "CreateIndex": 13148,
      "Address": "10.0.3.245",
      "Node": "postgresql-master"
    }
  }
]

В данном примере сервис лежит.

А так можно узнать состояние хоста и сервисов на нем:

curl -s http://10.0.3.224:8500/v1/health/node/postgresql-slave-2 | jq .

Пример ответа:

[
  {
    "ModifyIndex": 12827,
    "CreateIndex": 12827,
    "Node": "postgresql-slave-2",
    "CheckID": "serfHealth",
    "Name": "Serf Health Status",
    "Status": "passing",
    "Notes": "",
    "Output": "Agent alive and reachable",
    "ServiceID": "",
    "ServiceName": ""
  },
  {
    "ModifyIndex": 13147,
    "CreateIndex": 12832,
    "Node": "postgresql-slave-2",
    "CheckID": "service:postgresql-replica-2",
    "Name": "Service 'postgresql-replica' check",
    "Status": "passing",
    "Notes": "",
    "Output": "9.5/main (port 5432): online,recovery\n",
    "ServiceID": "postgresql-replica-2",
    "ServiceName": "postgresql-replica"
  }
]

Попробуйте поронять сервисы или хосты и посмотреть, как соответсвующие хелсчеки меняют свое состояние с passing на critical. В веб-панельке при этом хосты и сервисы меняют свой цвет с зеленого на оранжевый. Интересно, что когда сервис лежит, он все так же продолжает отдаваться при запросе каталога через REST API, хотя из DNS-ответа сервис выпиливается. Это поведение можно исправить, добавив в запрос аргумент ?passing.

Информация о кластере и leader election

Consul позволяет посмотреть, кто сейчас есть в кластере:

curl -s http://localhost:8500/v1/status/peers | jq .

Пример ответа:

[
  "10.0.3.223:8300",
  "10.0.3.224:8300",
  "10.0.3.245:8300"
]

Также мы можем узнать, кто сейчас лидер:

curl -s http://localhost:8500/v1/status/leader | jq .

Пример ответа:

"10.0.3.223:8300"

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

curl -s http://localhost:8500/v1/agent/self | jq '.Member.Addr'

Возникает закономерное желание попытаться получить leader election при помощи этого API для своего приложения в 20 строк кода на Python. Но это вряд ли удачная идея. Во-первых, потому что легко наплодить гонок и получить в один момент времени двух лидеров. Решать эту проблему нужно путем ожидания ACK от мажорити кластера, притом в ACK следует включать номер текущего term’а в Raft’е или иной счетчик. Во-вторых, по-хорошему в этом случае необходимо при выполнении любого действия проверять, являемся ли мы по-прежнему лидером.

Как верно сообщил мне @sum3rman, куда проще использовать другой подход, предположительно называемый leader lease. Раз у нас уже есть KV-хранилище с поддержкой CAS, мы можем просто писать в него, что машина такая-то является лидером до истечении такого-то времени. Пока лидер живет и здравствует, он может периодически продлевать это время. Если лидер умрет, его быстро кто-то подменит. В таком варианте достаточно синхронизировать на машинах время при помощи ntpd и при выполнении лидером любого действия проверять, что у него в запасе достаточно времени, чтобы завершить это действие.

Еще один интересный вариант leader election поверх Consul описан в самой документации Consul’а. Он полагается на уже знакомый нам механизм хелсчеков, а также сессии. Сессии по сути представляют собой распределенные локи, автоматически освобождаемые по TTL или при падении сервиса. В рамках этой заметки сессии мы не рассматриваем. Но возможность эта весьма интересная, и я всячески рекомендую с ней познакомиться.

Замечание про безопасность

Примите во внимание, что по умолчанию в Consul не используется какое-либо шифрование трафика. Также, получив доступ всего лишь к одной машине вашего кластера, можно довольно просто устроить DoS всего приложения.

Тут, правда, следует сделать ряд оговорок. Во-первых, все зависит от того, как вы используете Consul. Например, если он используется только как KV хранилище, доступ к нему очень легко ограничить при помощи Nginx и iptables. Во-вторых, имеет право на жизнь точка зрения, что получив доступ к одному из фронтэндов, злоумышленник скорее всего получит доступ и к БД, а значит сможет устроить DoS, просто записав в эту БД ерунду. Поэтому на этом этапе защищаться уже поздно, так зачем создавать себе лишние неудобства?

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

Заключение

Я бы сказал, что Consul — это не service discovery, а целый фреймворк для построения распределенных систем, умеющий помимо самого service discovery еще и локи, KV, лидер элекшен, мониторинг, ACL, сетевые координаты, events, prepared queries и, наверное, что-то еще, о чем я забыл, или что в нем успеет появиться к моменту, когда вы будете читать эти строки. Интересно, что благодаря встроенному мониторингу, при желании Consul’ом можно полностью заменить Zabbix или Nagios.

Ссылки по теме:

А используете ли вы Consul и если да, то как впечатления?

Дополнение: Реальный пример использования Consul вы можете найти в заметке Stolon: создаем кластер PostgreSQL с автофейловером.

Метки: .

Подпишись через RSS, E-Mail, Google+, Facebook, Vk или Twitter!

Понравился пост? Поделись с другими: