Docker – виртуализация сети. Часть 1
Драйвер виртуальной сетевой подсистемы Docker опирается на несколько технологий: пространство имен, VXLAN, Netlink и распределенное хранилище ключей. В этой статье описаны каждая из этих технологий вместе с необходимыми командами по управлению ими, а также рассмотрены практические вопросы их взаимодействия друг с другом при настройке виртуальной сети для ваших контейнеров.
Виртуализация сети Docker
Итак, давайте создадим оверлейную сеть между хостами Docker. В нашем примере мы настроим ее между тремя хостами: двумя Docker хостами и одним хостом с Consul-ом.
Docker будет использовать Consul для хранения метаданных оверлейных сетей, которые должны использоваться всеми Docker Engine-ами: IP-адреса контейнеров, MAC-адреса и месторасположение. До Docker 1.12 Docker требовал внешнего хранилища ключей (etcd или consul) для создания оверлейных сетей, однако, начиная с версии 1.12, Docker может использовать внутреннее хранилище ключей для создания Swarm-кластеров и оверлейных сетей («Swarm mode»). В этой статье мы будем использовать Consul, потому что он позволит нам посмотреть внуть хранилища ключей и разобраться со структурой хранения информации. Прямо сейчас мы запустим Consul на одном узле, но в продуктивной среде, если вы все таки решите использовать Consul в качестве внешнего хранилища ключей, вам понадобится кластер как минимум из трех узлов для отказоустойчивости.
У нашего стенда будет следующая конфигурация:
docker-1: 192.168.1.4
docker-2: 192.168.1.5
consul-1: 192.168.1.6
Запуск сервисов Consul и Docker
Для запуска сервиса Consul его предварительно необходимо скачать отсюда. Далее необходимо выполнить команды:
$ consul agent -server -dev -ui -client 0.0.0.0
Здесь используются следующие флаги:
- server: запустить агент консула в режиме сервера
- dev: создать автономный сервер Consul без какой-либо настойчивости
- ui: запустите небольшой веб-интерфейс, позволяющий легко просматривать ключи, хранящиеся в Docker, и их значения
- client 0.0.0.0: слушать клиентские подключения на всех сетевых интерфейсах (по умолчанию используется 127.0.0.1)
Сразу после инициализации сервера, в его web-интерфейсе не будет никаких ключей:
Инсталляция Docker происходит так, как это написано в официальной инструкции:
$ sudo yum install -y yum-utils device-mapper-persistent-data lvm2
$ sudo yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo
$ sudo yum makecache fast
$ sudo yum -y install docker-ce
$ sudo systemctl start docker
Чтобы настроить Docker Engine на использование Consul в качестве хранилища ключей, необходимо запустить его демон и явно передать с опцию кластерного хранилища (—cluster-store):
$ dockerd -H fd:// --cluster-store=consul://consul:8500 --cluster-advertise=eth0:2376
Указать эти параметры можно в настройках Docker в systemd, а можно создав файл /etc/docker/daemon.json:
{
"cluster-store": "consul://192.168.1.6:8500",
"cluster-advertise": "eth0:2376"
}
Опция —cluster-advertise указывает, какой IP-адрес\сетевой интерфейс использовать для рассылки уведомлений о кластере. Необходимо, чтобы Consul и Docker Engine использовали одну сеть.
Опция —cluster-store указывает, какой IP-адрес использовать для подключения к хранилищу ключей.
Если если посмотреть в Web-интерфейс Consul, мы увидим, что Docker создал некоторую иерархию ключей, но сетевой ключ: http://192.168.1.6:8500/v1/kv/docker/network/v1.0/network/ все еще пуст.
Создание оверлей сети
Давайте создадим оверлей сеть между Docker хостами:
$ docker network create --driver overlay --subnet 192.168.10.0/24 demo-network
0f222fcb18197027638550fd3c711c57c0ab6c16fd20456aed0ab38629884b37
Давайте проверим, что мы правильно настроили нашу сеть, убедившись, что она имеет одинаковый идентификатор на обоих хостах.
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
6280bda9a551 bridge bridge local
0f222fcb1819 demo-network overlay global
0d83e2289916 host host local
f56a90455a6d none null local
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
e34e5a057e7e bridge bridge local
0f222fcb1819 demo-network overlay global
75d8699129f5 host host local
bd8c63dca9df none null local
Обратите внимание на то, что идентифкатор (0f222fcb1819) сети demo-network одинаковый на обоих хостах.
Теперь давайте посмотрим, работает ли наш оверлей. На хосте docker-1 создайте контейнер X-1, подключив его к только что созданной оверлей сети, явно указываем ему IP-адрес (192.168.10.100). На docker-2 мы создаем контейнер, также подключенный к оверлей сети и запускаем команду ping X-1. На хосте docker-1:
$ docker run -d --ip 192.168.10.100 --net demo-network --name X-1 busybox sleep 3600
Unable to find image 'busybox:latest' locally
latest: Pulling from library/busybox
add3ddb21ede: Pull complete
Digest: sha256:b82b5740006c1ab823596d2c07f081084ecdb32fd258072707b99f52a3cb8692
Status: Downloaded newer image for busybox:latest
9ecf798285f71840dabb2d55aa6e8c1ed1991f9864cf0840d610585d2ad95002
На docker-2:
$ docker run --rm --net demo-network --name X-2 busybox ping 192.168.10.100
Unable to find image 'busybox:latest' locally
latest: Pulling from library/busybox
add3ddb21ede: Pull complete
Digest: sha256:b82b5740006c1ab823596d2c07f081084ecdb32fd258072707b99f52a3cb8692
Status: Downloaded newer image for busybox:latest
PING 192.168.10.100 (192.168.10.100): 56 data bytes
64 bytes from 192.168.10.100: seq=0 ttl=64 time=0.522 ms
64 bytes from 192.168.10.100: seq=1 ttl=64 time=0.339 ms
64 bytes from 192.168.10.100: seq=2 ttl=64 time=0.364 ms
^C
--- 192.168.10.100 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
Как видно из примера, пинг между контейнерами проходит. В то же самое время, если попытаться запустить пинг контейнера X-1 с хоста docker-1, на котором этот контейнер запущен, у нас ничего не выйдет:
$ ping 192.168.10.100
PING 192.168.10.100 (192.168.10.100) 56(84) bytes of data.
^C
--- 192.168.10.100 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 999ms
Не выйдет, потому что хост docker-1 ничего не знает об адресации 192.168.10.0/24 внутри оверлей сети, которая организована следующим образом:
После того, как мы построили оверлейную сеть, давайте посмотрим, как она работает.
Сетевая конфигурация контейнеров
Как выглядит сетевая конфигурация X-1 на docker-1? Мы можем выполнить команду exec внутри контейнера и увидеть следующее:
$ docker exec X-1 ip addr show
1: lo: mtu 65536 qdisc noqueue qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
6: eth0@if7: mtu 1450 qdisc noqueue
link/ether 02:42:c0:a8:0a:64 brd ff:ff:ff:ff:ff:ff
inet 192.168.10.100/24 scope global eth0
valid_lft forever preferred_lft forever
9: eth1@if10: mtu 1500 qdisc noqueue
link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.18.0.2/16 scope global eth1
valid_lft forever preferred_lft forever
У нас в контейнере есть два интерфейса (без учета loopback):
- eth0: настроен с IP-адресом в диапазоне 192.168.10.0/24. Этот интерфейс подключен к нашей оверлей сети.
- eth1: настроен с IP-адресом в диапазоне 172.18.0.2/16, который мы нигде не настраивали
А что насчет настроек маршрутизации?
$ docker exec X-1 ip route show
default via 172.18.0.1 dev eth1
172.18.0.0/16 dev eth1 scope link src 172.18.0.2
192.168.10.0/24 dev eth0 scope link src 192.168.10.100
Конфигурация маршрутизации указывает, что маршрут по умолчанию идет через интерфейс eth1, что означает, что этот интерфейс можно использовать для доступа к ресурсам за пределами оверлей сети. Мы можем легко проверить это, проверив соединение с внешним миром:
$ docker exec -ti X-1 ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8): 56 data bytes
64 bytes from 8.8.8.8: seq=0 ttl=56 time=3.478 ms
64 bytes from 8.8.8.8: seq=1 ttl=56 time=2.354 ms
^C
--- 8.8.8.8 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 2.354/2.916/3.478 ms
Обратите внимание, что можно создать оверлейную сеть, в которой контейнеры не будут иметб доступа к внешним сетям, используя флаг —internal.
Тип обоих интерфейсов — veth. Интерфейсы veth всегда находятся в паре и связаны виртуальным проводом. Два veth интерфейса могут находиться в разных пространствах имен, что позволяют трафику перемещаться из одного пространства имен в другое. Оба vethинтерфейса используются для вывода трафика за пределы пространства имен контейнерной сети.
Т.е. прошлую картинку сети необходимо слегка изменить:
Как контейнер связан с реальной сетью
Мы можем идентифицировать другой конец veth, используя команду ethtool. Но т.к. эта команда не доступна в нашем контейнере, мы выполним эту команду как бы внутри нашего контейнера, используя nsenter, которая позволяет нам получить одно или несколько пространств имен, связанных с процессом запущенного контейнера Docker.
Чтобы перечислить пространства имен сетей, созданные Docker, мы можем просто запустить:
$ sudo ls -1 /var/run/docker/netns
3-0f222fcb18
e4b847ee68c5
Чтобы использовать эту информацию, нам нужно определить пространство имен у сети для контейнеров, а эта информация доступна нам в SandboxKey:
$ docker inspect X-1 -f {{.NetworkSettings.SandboxKey}}
/var/run/docker/netns/e4b847ee68c5
Далее мы можем выполнять команды хостовой системы внутри пространства имен сети контейнера (даже если у контейнера нет нужной нам команды):
$ nsenter --net=$(docker inspect X-1 -f {{.NetworkSettings.SandboxKey}}) ip addr show eth0
17: eth0@if18: mtu 1450 qdisc noqueue state UP
link/ether 02:42:c0:a8:0a:64 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 192.168.10.100/24 scope global eth0
valid_lft forever preferred_lft forever
Давайте узнаем, какие индексы интерфейсов имеют интерфейсы контейнера eth0 и eth1:
$ nsenter --net=$(docker inspect X-1 -f {{.NetworkSettings.SandboxKey}}) ethtool -S eth0
NIC statistics:
peer_ifindex: 18
$ nsenter --net=$(docker inspect X-1 -f {{.NetworkSettings.SandboxKey}}) ethtool -S eth1
NIC statistics:
peer_ifindex: 20
Теперь нам надо найти интерфейсы с индексами 18 и 20. Давайте сначала посмотрим на сам хост, где запущен контейнер. На docker-1:
$ ip -details link show
1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 promiscuity 0 addrgenmode eui64
2: eth0: mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000
link/ether d0:0d:85:52:92:a0 brd ff:ff:ff:ff:ff:ff promiscuity 0 addrgenmode eui64
3: docker0: mtu 1500 qdisc noqueue state DOWN mode DEFAULT
link/ether 02:42:51:8a:2c:80 brd ff:ff:ff:ff:ff:ff promiscuity 0
bridge forward_delay 1500 hello_time 200 max_age 2000 addrgenmode eui64
8: docker_gwbridge: mtu 1500 qdisc noqueue state UP mode DEFAULT
link/ether 02:42:e3:c3:bc:d7 brd ff:ff:ff:ff:ff:ff promiscuity 0
bridge forward_delay 1500 hello_time 200 max_age 2000 addrgenmode eui64
20: veth0c77760@if19: mtu 1500 qdisc noqueue master docker_gwbridge state UP mode DEFAULT
link/ether 42:fe:de:c5:61:d8 brd ff:ff:ff:ff:ff:ff link-netnsid 3 promiscuity 1
veth
bridge_slave addrgenmode eui64
Из этого вывода видно, что на хосте у нас нет следов интерфейса 18, однако, мы нашли интерфейс 20 (eth1). Кроме того, этот интерфейс подключен к интерфейсу типа bridge под названием «docker_gwbridge». Что это за мост? Если мы перечислим сети, управляемые докером, мы увидим, что она появилась в списке:
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
6280bda9a551 bridge bridge local
0f222fcb1819 demo-network overlay global
e2a5ce239d78 docker_gwbridge bridge local
0d83e2289916 host host local
f56a90455a6d none null local
Воспользуемся выводом docker inspect:
$ docker inspect docker_gwbridge
[
{
"Name": "docker_gwbridge",
"Id": "e2a5ce239d781e7105516e28c1c49a76f5da3c579c1f4d8ee105baf07744ee5f",
"Created": "2017-08-28T14:37:27.445520037Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"9ecf798285f71840dabb2d55aa6e8c1ed1991f9864cf0840d610585d2ad95002": {
"Name": "gateway_9ecf798285f7",
"EndpointID": "c32ddca8017869b713956dcf1903fea883f12c0032f6525f951b1d6576ebc878",
"MacAddress": "02:42:ac:12:00:02",
"IPv4Address": "172.18.0.2/16",
"IPv6Address": ""
}
},
"Options": {
"com.docker.network.bridge.enable_icc": "false",
"com.docker.network.bridge.enable_ip_masquerade": "true",
"com.docker.network.bridge.name": "docker_gwbridge"
},
"Labels": {}
}
]
Из вывода видно, что:
- эта сеть использует драйвер bridge (тат же, что используется стандартным интерфейсом docker0)
- эта сеть использует подсеть 172.18.0.0/16, что согласуется с eth1
- enable_icc установлен в значение false, что означает, что мы не можем использовать этот bridge для межконтейнерной коммуникации
- для параметра enable_ip_masquerade установлено значение true, что означает, что трафик из контейнера будет проходить через NAT для доступа к внешним сетям (что мы видели ранее, когда мы успешно пинговали 8.8.8.8)
Таким образом, можно дополнить нарисованную нами схему:
Связь интерфейса eth0 с оверлей сетью
Интерфейс, который связан парой с eth0, отсутствует в пространстве имен сети хоста. Если мы снова посмотрим на сетевые пространства имен:
$ ls -1 /var/run/docker/netns
3-0f222fcb18
e4b847ee68c5
Мы увидим пространство имен под названием «3-0f222fcb18». За исключением «3», имя этого пространства имен является началом идентификатора сети нашей оверлей сети. Проверьте сами при помощи команды:
$ docker network inspect demo-network -f {{.Id}}
Это пространство имен явно связано с нашей оверлей сетью. Мы можем посмотреть на интерфейсы, присутствующие в этом пространстве имен:
$ nsenter --net=/var/run/docker/netns/3-0f222fcb18 ip -d link show
1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 promiscuity 0 addrgenmode eui64
2: br0: mtu 1450 qdisc noqueue state UP mode DEFAULT
link/ether ce:23:6c:e6:ca:f4 brd ff:ff:ff:ff:ff:ff promiscuity 0
bridge forward_delay 1500 hello_time 200 max_age 2000 addrgenmode eui64
16: vxlan0: mtu 1450 qdisc noqueue master br0 state UNKNOWN mode DEFAULT
link/ether ce:23:6c:e6:ca:f4 brd ff:ff:ff:ff:ff:ff link-netnsid 0 promiscuity 1
vxlan id 256 srcport 0 0 dstport 4789 proxy l2miss l3miss ageing 300
bridge_slave addrgenmode eui64
18: veth0@if17: mtu 1450 qdisc noqueue master br0 state UP mode DEFAULT
link/ether da:4c:fd:ca:da:0e brd ff:ff:ff:ff:ff:ff link-netnsid 1 promiscuity 1
veth
bridge_slave addrgenmode eui64
В пространстве имен оверлей сети содержится три интерфейса (вместе с lo):
- br0: бридж
- veth2: интерфейс veth, который связан с интерфейсом eth0 в нашем контейнере и который подключен к бриджу
- vxlan0: интерфейс типа «vxlan», который также подключен к бриджу
Интерфейс vxlan четко показывает, что происходит какая-то «оверлейная магия», которую мы рассмотрим подробнее в следующей статье, а пока давайте обновим нашу схему: