Docker – виртуализация сети. Часть 1
О технологиях

Docker – виртуализация сети. Часть 1

1669
18 минут

Драйвер виртуальной сетевой подсистемы Docker опирается на несколько технологий: пространство имен, VXLAN, Netlink и распределенное хранилище ключей. В этой статье описаны каждая из этих технологий вместе с необходимыми командами по управлению ими, а также рассмотрены практические вопросы их взаимодействия друг с другом при настройке виртуальной сети для ваших контейнеров.

Виртуализация сети Docker

Итак, давайте создадим оверлейную сеть между хостами Docker. В нашем примере мы настроим ее между тремя хостами: двумя Docker хостами и одним хостом с Consul-ом.

Docker-Networking-Part-1-CROC-Cloud-Console.png

Docker будет использовать Consul для хранения метаданных оверлейных сетей, которые должны использоваться всеми Docker Engine-ами: IP-адреса контейнеров, MAC-адреса и месторасположение. До Docker 1.12 Docker требовал внешнего хранилища ключей (etcd или consul) для создания оверлейных сетей, однако, начиная с версии 1.12, Docker может использовать внутреннее хранилище ключей для создания Swarm-кластеров и оверлейных сетей («Swarm mode»). В этой статье мы будем использовать Consul, потому что он позволит нам посмотреть внуть хранилища ключей и разобраться со структурой хранения информации. Прямо сейчас мы запустим Consul на одном узле, но в продуктивной среде, если вы все таки решите использовать Consul в качестве внешнего хранилища ключей, вам понадобится кластер как минимум из трех узлов для отказоустойчивости.

Docker-Networking-Part-1-Docker-hosts.png

У нашего стенда будет следующая конфигурация:

docker-1: 192.168.1.4
docker-2: 192.168.1.5
consul-1: 192.168.1.6

Запуск сервисов Consul и Docker

Docker-Networking-Part-1-No-Consul-records-1.png

Для запуска сервиса 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-Networking-Part-1-No-Consul-records.png

Инсталляция 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 внутри оверлей сети, которая организована следующим образом:

Docker-Networking-Part-1-Overlay-network.png

После того, как мы построили оверлейную сеть, давайте посмотрим, как она работает.

Сетевая конфигурация контейнеров

Как выглядит сетевая конфигурация 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интерфейса используются для вывода трафика за пределы пространства имен контейнерной сети.

Т.е. прошлую картинку сети необходимо слегка изменить:

Docker-Networking-Part-1-Overlay-network-Detailed.png

Как контейнер связан с реальной сетью

Мы можем идентифицировать другой конец 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)

Таким образом, можно дополнить нарисованную нами схему:

Docker-Networking-Part-1-Overlay-network-Detailed-2.png

Связь интерфейса 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 четко показывает, что происходит какая-то «оверлейная магия», которую мы рассмотрим подробнее в следующей статье, а пока давайте обновим нашу схему:

Docker-Networking-Part-1-Overlay-network-Detailed-3.png

15 ноября 2022
OpenShift остался без поддержки – как решить проблему российским клиентам
Интерес к семейству ПО для контейнеризации OpenShift был довольно высоким в корпоративном сегменте в прежние годы. По данным мониторинговой службы Datadog, только за прошлый год во всем мире количество пользователей платформ от RedHat увеличилось на 28%. Весной IBM объявил об уходе из России и прекращении поддержки всех программных продуктов для текущих клиентов. Разберемся, насколько критичной оказалась данная ситуация для заказчиков, и какие варианты действий существуют, чтобы минимизировать возможные риски отключения от сервиса.
0 минут
530
25 февраля 2021
Свидетели DevOps: мифы и байки про девопсов и тех, кто их нанимает
Те, кто решил стать девопсом, видят в этой профессии заманчивые перспективы. Это новый уровень мышления, это творчество и возможность создавать, это безграничные просторы для самосовершенствования. Не секрет также, что девопсам хорошо платят. Вместе с тем, вокруг понятия DevOps сформировался некий культ, овеянный мифами и легендами.
0 минут
727
4 декабря 2020
Дайджест обновлений К2 Облака осень 2020 г.
За осень в К2 Облаке многое изменилось. Мы активно писали код и не успевали сообщать обо всех переменах. Постараемся исправиться и информировать вас ASAP, чтобы вы могли сразу же использовать новые фичи.
1 минута
218
scrollup