Привет, Хабр! С вами Антон Паус. Я четыре года проработал системным инженером в российском офисе Cisco, а сейчас работаю DevOps-инженером в Cloud.ru — занимаюсь облаком собственной разработки

С 2024 года я веду проект по разработке новой архитектуры кластера виртуализации Evolution IaaS на базе Kubernetes. Раньше мы использовали связку Ansible и Docker, и это отлично работало на существующем масштабе, но с ростом клиентской нагрузки столкнулись с рядом сложностей, что в конечно итоге и сподвигло нас на обновление архитектуры. 

Проект получился довольно объемным и со множеством нюансов и сложностей, о которых я, возможно, напишу в следующих статьях. В этой же хотел остановиться на нюансах сетевого взаимодействия в кластере bare metal Kubernetes — в ходе работы возник ряд сложностей (например, с первоначальной установкой K8s на сервер с несколькими интерфейсами), о решении которых я и расскажу. 

Если развернуть кластер Kubernetes на железе — в ваших обозримых планах, надеюсь, мои изыскания будут полезны. А еще в статье довольно много сетевки, так что приготовьтесь!

Что под капотом проекта

Сначала расскажу про нашу архитектуру в целом и особенности решения. Есть кластер железных серверов, на которых развернут софт виртуализации Evolution IaaS. Данный софт работает на хостах в виде контейнеров и доставляется на конечное окружение с помощью большого Ansible-скрипта. Развернутые контейнеры представляют собой распределенные приложения и для общения между собой им нужен сетевой идентификатор. Для этого используется Loopback с маской подсети /32.

Сами железные серверы имеют два uplink-подключения к вышестоящим коммутаторам (сетевой фабрике) для отказоустойчивости. Поскольку loopback имеют маску подсети /32, серверы по умолчанию ничего не знают о loopback друг друга, и эту информацию надо как-то распространить. 

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

Из протоколов используем BGP — популярное решение в сетях разного уровня. Для обнаружения неполадок в сети BFD — протокол, работающий поверх UDP и позволяющий в самые короткие сроки определить нарушение связности. На линках, идущих к коммутаторам, используется /31 адресация, FRR устанавливает BGP соседство с двумя коммутаторами и анонсирует loopback вместе с другой сетевой информацией. FRR также поднимает BFD соединения с коммутаторами. Сам FRR развернут в контейнере, рядом с другими приложениями.

Схема подключения гипервизора к сетевой фабрике без Kubernetes
Схема подключения гипервизора к сетевой фабрике без Kubernetes

Подводные камни при переезде на Kubernetes

При переезде на Kubernetes сразу получаем смену парадигмы доставки приложений на железные хосты: вместо запуска Docker-контейнеров с помощью Ansible для этого теперь используются механизмы Kubernetes, и на этом этапе как раз и появилась первая сложность. 

Для раскатки кластера Kubernetes у нас используется Kubespray — Ansible playbook. Для его запуска нужно указать сетевые идентификаторы узлов (access_ip), которые необходимы для функционирования etcd, развернутого в хосте, kubelet и других сервисов. Можно было бы использовать все тот же Loopback, но тут мы сталкиваемся с проблемой курицы и яйца. Если мы разворачиваем все приложения в Kubernetes, мы можем развернуть тот же FRR в виде DaemonSet, но во время запуска Kubespray FRR еще нет, и информации о чужих Loopback еще нет в нашей таблице маршрутизации. 

Можно было бы прописать маршруты вручную на изначальном этапе с помощью команды ip route add. Это решение, наверное, подошло бы для небольшого dev-окружения, но для production-кластера или любого другого большого кластера количество этих маршрутов становится слишком большим: 2n*(n-1). Нужно было придумать что-то другое.

Решение 1. L2 сеть через Bond

Одно из самых простых решений, которое приходит в голову — использовать Bond и L2 сеть. Bond — технология агрегации нескольких линков в один линк. С точки зрения хоста мы получаем единый интерфейс, на который можем повесить IP-адрес. Для такой агрегации каналов есть специализированные протоколы, например, LACP. Со стороны коммутаторов нужна технология mlag — multi-chassis link aggregation group.

Схема подключения гипервизора к сетевой фабрике через L2 Bond
Схема подключения гипервизора к сетевой фабрике через L2 Bond

Протокол динамической маршрутизации и распространение маршрутов в данном случае нам не нужны, поскольку все IP-адреса Bond находятся в одной L2 сети и mac-адреса соседних серверов мы будем получать через ARP.

Какие недостатки есть у этого решения? Схема хорошо работает в небольших масштабах, но если серверов больше, чем на пару коммутаторов, не обойтись без технологии растягивания VLAN (например, VXLAN и подобной), ведь серверы в этом случае будут находиться на разных парах коммутаторов, а arp-запросы должны ходить свободно между всеми серверами. Это вносит дополнительные сложности в сетевую архитектуру и увеличивает потенциальную цену оборудования. 

Еще один недостаток этого варианта — использование Kubernetes loadbalancer только на уровне L2, когда loadbalancer IP анонсируется по ARP только с одного узла. В итоге нет распределения входящего трафика по нескольким серверам, и есть вопросы к переключению arp на другой сервер при падении сервера, держащего определенный loadbalancer IP.

Решение 2. L3 сеть через Bond

Мы пошли дальше и предположили, что ограничения предыдущего варианта можно обойти благодаря переходу на L3. Теперь каждое соединение сервера с Bond и вышестоящим LAG находится в своем broadcast домене (например, /31). 

Так мы получаем преимущества одного access_ip на сервере, но теперь эти IP нужно как-то распространять. И тут мы возвращаемся к изначальной проблеме — нужно распространять по BGP, но при запуске Кubespray BGP агента на хосте еще нет. Можно развернуть этот BGP-агент (FRR) тем же Ansible совместно с Кubespray.

Теперь мы имеем железный хост с двумя объединенными в Bond-линками, с запущенным BGP-процессом, который устанавливает соединение с вышестоящим BGP-процессом. Но загвоздка в том, что этот процесс нельзя распределить на два коммутатора, он должен базироваться на каком-то одном. И при выходе из строя этого коммутатора BGP упадет, хотя L2-связность сохранится. 

Конечно, можно развернуть по одному BGP-процессу на каждом коммутаторе и запускать две BGP-связности через Bond с разными коммутаторами, но такая конфигурация не является рекомендованной, поскольку вводит потенциальную асимметрию BGP-трафика в случае проблем с физическими линками внутри Bond и усложняет сетевой дизайн в целом.

Схема подключения гипервизора к сетевой фабрике через L3 и BGP
Схема подключения гипервизора к сетевой фабрике через L3 и BGP

Финальное решение: L3 сеть + Loopback

Подумав мы вернулись к изначальной сетевой схеме, но со спецификой Kubernetes. Дано: отдельный loopback /32 на каждом хосте, отдельные /31 point-to-point линки к коммутаторам. BGP-процесс на хосте анонсирует необходимые нам адреса. Процесс запускаем в виде systemd unit на хосте, чтобы отдать среду контейнеризации полностью под управление Kubernetes. Таким образом мы получаем систему, полностью готовую к установке Kubernetes. В качестве CNI решаем использовать популярный в настоящее время Cilium актуальной на момент написания версии 1.17.4. 

После установки мы хотим разворачивать в Kubernetes приложения и вешать на них loadbalancer IP, чтобы затем эти сервисы были доступны вне кластера, и, следовательно, нам нужен механизм анонса этих адресов. Из-за специфики сетевой связности мы не можем использовать ARP-механизмы, поэтому приходим к анонсу адресов через BGP. CNI Cilium помимо множества встроенного функционала умеет устанавливать BGP-соседство и анонсировать адреса. Нам как раз очень кстати этот вариант, тем более что в актуальной версии все остальные способы помечены как deprecated. 

BGP с Cilium можно установить либо в сторону коммутаторов, либо в сторону FRR на том же хосте — local peering. Из-за сложности с настройкой нестандартных портов на наших сетевых коммутаторах (в случае связности с Cilium), а также отсутствия функционала BFD у Cilium, мы выбрали второй вариант. В случае установки локального BGP соседства BFD нам в целом-то и не нужен, а между FRR и коммутаторами он есть.

В итоге получаем схему-гирлянду: Cilium анонсирует IP-адреса сервисов на локальный FRR, FRR добавляет хостовые маршруты и отправляет всё скопом на коммутаторы, а затем принимает маршруты от коммутаторов для установки в хост:

Схема подключения гипервизора c Kubernetes к сетевой фабрике через BGP
Схема подключения гипервизора c Kubernetes к сетевой фабрике через BGP

Подробности конфигурации

Для реализации такой сетевой схемы потребуется нестандартная настройка как на стороне FRR, так и Cilium.

Манифесты Cilium BGP:

---
apiVersion: cilium.io/v2alpha1
kind: CiliumBGPClusterConfig
spec:
  bgpInstances:
    - name: gobgp
      localASN: 65000
      peers:
        - name: frr
          peerASN: 0
          peerAddress: 127.0.0.1
          peerConfigRef:
            name: control

---
apiVersion: cilium.io/v2alpha1
kind: CiliumBGPNodeConfigOverride
bgpInstances:
  - name: gobgp
    routerID: 127.0.0.1
    localPort: 8179
    peers:
      - name: frr
        localAddress: 127.0.0.1

Объясню: поскольку мы имеем 2х BGP-процесса в хостовом Network Namespace и не можем менять порт FRR из-за особенностей работы коммутаторов, нужно установить локальный порт Cilium BGP на нестандартный порт, например, 8179. Соседство FRR и Cilium устанавливаем по локальным адресам — 127.0.0.1. 

Хоть AS для Cilium и выбрана из приватного диапазона, мы все равно будем подменять этот ASN на FRR. Приватный AS выбран для того, чтобы унифицировать CiliumBGPClusterConfig для всех серверов в кластере: если будет 1 000 серверов, не придется плодить 1 000 манифестов.

Для CiliumBGPNodeConfigOverride этот трюк повторить не получится, ведь манифест должен существовать для каждого узла Kubernetes. А всё из-за нестандартного BGP- порта (возможно, это ограничение пофиксят в будущих версиях Cilium). 

А для FRR нам обязательно нужно прописать несколько команд, иначе локальный peering не заведется. Соседа нужно указать с вышеописанными сетевыми/bgp параметрами. «bgp allow-martian-nexthop» нужен для приема маршрутов с локальным nexthop, а маршрут до локального lo (ip route 127.0.0.1/32 Null0) — чтобы пометить принятые от cilium-а маршруты как валидные. Подобный конфиг применим к FRR версии 9.1.3.

BGP часть конфигурации FRR:

ip route 127.0.0.1/32 Null0
router bgp 1234567890
 bgp router-id 10.0.0.1
 neighbor 127.0.0.1 remote-as 65000
 neighbor 127.0.0.1 port 8179
 neighbor 127.0.0.1 update-source lo
 bgp allow-martian-nexthop
address-family ipv4 unicast
 redistribute connected route-map connected
 maximum-paths 2
 neighbor 127.0.0.1 route-map net_10_0_0_0 in
 neighbor 127.0.0.1 soft-reconfiguration inbound

Что в итоге

Что мы получили по итогу? Kubernetes-кластер из любого неограниченного (со стороны сети) количества узлов. На каждом узле — два BGP-агента. Cilium собирает данные с endpoint-ов Kubernetes благодаря встроенному механизму service discovery и анонсирует loadbalancer IP только с нужных узлов. FRR собирает маршруты с хоста, принимает все маршруты Kubernetes и подменяет AS path так, чтобы коммутаторы считали, что все маршруты приходят от FRR. 

Как я писал выше, BFD в итоговой схеме присутствует только на участке между физическими коммутаторами и FRR. При падении одного из двух линков BFD это быстро определит и выключит линк из топологии BGP. Локальная связность на физическом сервере между BGP-агентами не подвержена падению линков, а падение связности скорее всего будет означать падение всего TCP/IP стека на сервере или проблему с одним из BGP-агентов.

Это решение помогло нам внедрить Kubernetes в физические серверы (гипервизоры), не изменяя сетевую архитектуру коммутационной фабрики. Внедрение второго BGP-агента повлияло только на control plane сетевого взаимодействия, а data plane для хостовых IP-адресов проходит так же, как и раньше. До подов внутри Kubernetes IP трафик проходит дополнительные eBPF трансформации.

И в заключение — пару слов об идее развития сетевой архитектуры кластера. В нашей сетевой фабрике есть множество VRF, относящихся как к инфраструктурным, так и к клиентским. BGP-процессы на гипервизорах отправляют множество /32 маршрутов в сетевую фабрику. Всё это порядочно нагружает коммутаторы. Чтобы разгрузить их, удешевить и увеличить масштабируемость, мы планируем перенести VXLAN VTEP функции, присутствующие на коммутаторах, а с этих коммутаторов на гипервизоры. Что из этого получится — расскажу в следующей статье.

А еще приглашаю на GoCloud Tech — послушать мой доклад по этой теме и обсудить нюансы в неформальной обстановке.

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