Как использовать балансировщик MetalLB с BGP-anycast - Академия Selectel

Как использовать балансировщик MetalLB с BGP-anycast

Показываем, как с помощью сетевых сервисов Selectel и Managed Kubernetes настроить геораспределенную базу для инфраструктуры.

Клиенты часто спрашивают, как построить геораспределенный и катастрофоустойчивый Kubernetes. Так, чтобы при отказе целого дата-центра нагрузка переключалась на резервную площадку без смены IP-адресов сервисов.

Можно возразить, что проще изменить DNS-запись и направить трафик на инстанс в другом дата-центр. Но есть риск: кэширование на рекурсорах некоторых провайдеров может негативно повлиять на доступность. Они иногда игнорируют TTL и хранят старые данные неопределенный срок. Поэтому в таких сценариях крайне желательно сохранить IP-адрес.

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

Сетевая схема

Общая сетевая схема.

Многовато на первый взгляд, правда? Схема описывает распределенную отказоустойчивую конфигурацию на основе кластеров Managed Kubernetes (MKs), которая развернута в двух регионах.

  • Bravo — BissoTwo, регион Москва, пул ru-7.
  • Alfa — UnaOne, регион Санкт-Петербург, пул ru-3.

Инфраструктура использует внутренних и внешний BGP-пиринг для маршрутизации трафика между узлами, облачными роутерами, балансировщиками и маршрутизаторами Selectel (на схеме — Betelgeuse и Antares).

Дата-центр Bravo

Характеристики кластера MKs

  • Мастер-нода: 192.168.147.111 — управляющая нода кластера.
  • Рабочая нода 1: 192.168.147.25
  • Рабочая нода 2: 192.168.147.26
  • Балансировщик MetalLB: 203.0.113.67 — внешний IP для сервисов в кластере.

Сеть и маршрутизация

  • Служебная сеть MKs — связывает мастер-ноды кластера и используется для доступа к kube API, мониторинга и обслуживания со стороны провайдера.
  • Облачная сеть Bravo: 192.168.147.0/24 — подключена к облачному роутеру Bravo (192.168.147.1), который используется для резервного доступа в интернет и публичных IP-адресов вне anycast-пула.
  • gateway-bravo-bissotwo (192.168.147.254) — шлюз, через который трафик по умолчанию выходит в интернет и на котором развернуто ПО, обеспечивающее работу BGP (забегая вперед — frrouting); на этой машине также настраивается трансляция сетевых адресов с использованием netfilter/nftables.
  • IBGP peering между рабочими нодами и шлюзом gateway-bravo-bissotwo.
  • EBGP peering между шлюзом и роутером Betelgeuse (198.19.51.230/29).

Дата-центр Alfa

Характеристики кластера MKs

  • Мастер-нода: 192.168.247.211
  • Рабочая нода 1: 192.168.247.101
  • Рабочая нода 2: 192.168.247.102
  • Балансировщик MetalLB: 203.0.113.66

Сеть и маршрутизация

  • Служебная сеть MKs — связывает мастер-ноды кластера и используется для доступа к kube API, мониторинга и обслуживания со стороны провайдера.
  • Облачная сеть Alfa: 192.168.247.0/24 — подключена к облачному роутеру Alfa (192.168.247.1).
  • gateway-alfa-unoone (192.168.247.254) – шлюз.
  • IBGP peering между рабочими нодами и шлюзом gateway-alfa-unoone.
  • EBGP peering между шлюзом и роутером Antares (198.51.100.9).

Межрегиональное взаимодействие

Глобальный роутер не участвует в организации BGP и anycast. Он необходим для обмена трафиком внутри сетевого периметра — например, чтобы настроить репликацию баз данных или организовать Hot Standby между регионами.

Такое взаимодействие позволяет кластерам обмениваться данными напрямую и обеспечивает отказоустойчивость: при недоступности одного региона трафик перенаправляется через другой. Кроме того, это дает возможность использовать anycast-маршрутизацию, если балансировщики MetalLB работают с общими внешними IP-адресами.

Типы пиринга

В схеме используются два типа пиринга. Внутри каждого региона между рабочими нодами и виртуальным шлюзом в соответствующем пуле облака настраивается iBGP (Internal BGP). Это обеспечивает корректное распространение маршрутов внутри автономной системы (AS). Связь с внешними сетями и маршрутизаторами Selectel (Betelgeuse, Antares) реализуется через eBGP (External BGP). Этот уровень отвечает за обмен маршрутами между шлюзами и глобальной сетевой инфраструктурой.

Безопасность и отказоустойчивость

Отказоустойчивость системы обеспечивается работой в двух независимых регионах облачной платформы. Динамическое перенаправление трафика между ними реализовано с помощью BGP-пиринга: внешние IP-адреса (203.0.113.64–203.0.113.71) анонсируются из обоих регионов одновременно. Это позволяет использовать anycast-схемы или быстрое переключение (failover) при сбоях. За балансировку нагрузки на уровне L4 внутри кластеров отвечает MetalLB.

IP-адресация

КомпонентIP-адресПримечание
Bravo Master192.168.147.111
Bravo Worker 1192.168.147.25
Bravo Worker 2192.168.147.26
Bravo Gateway192.168.147.254
Bravo Router192.168.147.1
Alfa Master192.168.247.211
Alfa Worker 1192.168.247.101
Alfa Worker 2192.168.247.102
Alfa Gateway192.168.247.254
Alfa Router192.168.247.1
Cloud Router Bravo192.0.2.56Внешний IP облачного роутера. Выделяется при создании.
Cloud Router Alfa192.0.2.31Внешний IP облачного роутера. Выделяется при создании.
MetalLB Bravo203.0.113.67Внешний IP для сервисов в k8s
MetalLB Alfa203.0.113.66Внешний IP для сервисов в k8s
Betelgeuse (EBGP)198.19.51.230/29
Antares (EBGP)198.51.100.9/29

Взаимодействие по BGP

Схема переключений.

Как в такой схеме организован обмен маршрутами? В примере используются три автономные системы: две принадлежат провайдеру (AS 64600 и AS 65101), а одна — частная (например, AS 64859). Номер частной AS необходимо заранее согласовать с инженерами Selectel.

Провайдер передает нам маршрут по умолчанию (default route), а мы анонсируем в ответ сеть 203.0.113.64/29. При этом виртуальный шлюз может транслировать и более специфичные маршруты (с маской больше /29), если они появятся в таблице маршрутизации. Для этого в конфигурации BGP-демона должны быть активны опции redistribute static или redistribute connected. Маршруты, полученные по iBGP от других «соседей», будут перераспределяться автоматически.

Специфичные маршруты, например /32 для конкретных IP, шлюз получает от балансировщика MetalLB, когда в Kubernetes-кластере создается сервис типа LoadBalancer. Далее шлюз анонсирует их через пиринг с роутерами Selectel «в мир». Такой подход и обеспечивает региональную отказоустойчивость.

Настройка схемы

Описанный процесс настройки подходит не только для кластеров Managed Kubernetes, но и для self-managed кластера Kubernetes, в том числе на выделенных серверах.

1. Заказываем две публичные подсети. В панели управления Selectel это можно сделать в разделе Продукты → Облачные серверы → Сеть → Публичные сети. Эти подсети будут использованы как линковочные.

2. Заказываем BGP + anycast-подсеть. Потребуются линковочные подсети из п.1. один. Процесс описан в документации.

В примере используются: 

  • подсеть 198.19.51.224/29 — для Bravo,
  • подсеть 198.51.100.8/29 — для Alfa.

Не забываем указать в тикете, что при настройке BGP-соседства будет использоваться bidirectional forwarding detection.

3. Выбираем по адресу из каждой линковочной подсети. Эти адреса следует сообщить в тикете для настройки BGP peering-а. В примере взяты:

  • 198.19.51.230 – для Bravo,
  • 198.51.100.14 – для Alfa.

4. Создаем две приватные облачные сети в каждом из пулов. В панели управления Selectel это можно сделать в разделе Продукты → Облачные серверы → Сеть → Приватные сети

В каждой сети будет по одной частной подсети. В этих сетях также будут созданы рабочие ноды Managed Kubernetes. На схеме эти сети обозначены как «Облачная сеть Alfa» и «Облачная сеть Bravo» с подсетями 192.168.247.0/24 и 192.168.147.0/24 соответственно.

В качестве шлюза по умолчанию для подсетей указываем будущие частные адреса ВМ. На схеме используются 192.168.247.254 и 192.168.147.254.

5. Разворачиваем по одной виртуальной машине в каждом выбранном пуле, как это описано в документации. Для внешнего интерфейса используем IP-адрес из тех, что были выбраны во п.2 для настройки BGP-соседства (BGP Neighbor) в соответствующем регионе. В качестве ОС используем AlmaLinux 9 с последними обновлениями.


      cat /etc/redhat-release 
AlmaLinux release 9.6 (Sage Margay)

Не забываем включить на ВМ передачу трафика транзитно, через интерфейсы (forwarding):


      echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/01-ip-forwarding.conf
sudo sysctl --system

6. Добавляем для внутренних портов двух ВМ все IPv4-адреса. Так как ВМ используется как шлюз, то без данной настройки работа невозможна. 
Выполняем на управляющей машине:


      export GW_IP_ADDR=192.168.147.254
GW_PORT_ID=$(openstack port list --fixed-ip ip-address=${GW_IP_ADDR} -f value -c ID 2>/dev/null)
openstack port set --allowed-address ip-address="0.0.0.0/0" $GW_PORT_ID

Не забываем поменять IP-адрес на свой. Подробнее о настройке можно прочитать в документации.

7. На ВМ устанавливаем и настраиваем ПО с поддержкой протокола BGP.
В примере будет использован frrouting.


      export FRRVER="frr-stable"
curl -O https://rpm.frrouting.org/repo/$FRRVER-repo.el9.noarch.rpm
sudo yum install ./$FRRVER-repo.el9.noarch.rpm
sudo yum check-update
sudo yum install frr frr-pythontools

Задействуем запуск bgpd и bfdd при старте frr:


      sudo sed -e 's/^bgpd=no/bgpd=yes/g' -e 's/bfdd=no/bfdd=yes/g' -i /etc/frr/daemons

Приводим конфигурацию каждого из frr (/etc/frr/frr.conf) к следующему виду: 

frr в region alfa


      !
frr defaults traditional
hostname gateway-alfa-unoone
log syslog informational
!
ip prefix-list default_from_mylovelycozycloud seq 10 permit any
ip prefix-list my_anycast_subnet seq 10 permit 203.0.113.64/29 le 32
ip prefix-list my_anycast_subnet seq 100 deny any
!
route-map prpnd_private permit 10
 description For AS PATH prepend
 set as-path prepend 64859
exit
!
ip router-id 198.51.100.14
!
router bgp 64859
 neighbor mks-nodes peer-group
 neighbor mks-nodes remote-as internal
 neighbor mks-nodes bfd
 neighbor mks-nodes bfd profile t-rex
 neighbor mks-nodes update-source eth1
 neighbor 198.51.100.9 remote-as 65101
 neighbor 198.51.100.9 description MyLovelyCozyCloud-region-alfa-peer
 neighbor 198.51.100.9 interface eth0
 neighbor 198.51.100.9 ebgp-multihop
 bgp listen limit 110
 bgp listen range 192.168.247.0/24 peer-group mks-nodes
 !
 address-family ipv4 unicast
  network 203.0.113.64/29
  neighbor 198.51.100.9 prefix-list default_from_mylovelycozycloud in
  neighbor 198.51.100.9 prefix-list my_anycast_subnet out
  neighbor 198.51.100.9 route-map prpnd_private out
 exit-address-family
exit
!
bfd
 profile t-rex
  log-session-changes
  minimum-ttl 127
 exit
 !
 peer 198.51.100.9
  profile t-rex
 exit
 !
exit
!
end

frr в region bravo


      !
frr defaults traditional
hostname gateway-bravo-bissotwo
log syslog informational
!
ip prefix-list default_from_mylovelycozycloud seq 10 permit any
ip prefix-list my_anycast_subnet seq 10 permit 203.0.113.64/29 le 32
ip prefix-list my_anycast_subnet seq 100 deny any
!
route-map prpnd_private permit 10
 description For AS PATH prepend
 set as-path prepend 64859 64859 64859
exit
!
ip router-id 198.19.51.230
!
router bgp 64859
 neighbor mks-nodes peer-group
 neighbor mks-nodes remote-as internal
 neighbor mks-nodes bfd
 neighbor mks-nodes bfd profile t-rex
 neighbor mks-nodes update-source eth1
 neighbor 198.19.51.225 remote-as 64600
 neighbor 198.19.51.225 description MyLovelyCozyCloud-region-bravo-peer
 neighbor 198.19.51.225 interface eth0
 neighbor 198.19.51.225 ebgp-multihop
 bgp listen limit 110
 bgp listen range 192.168.147.0/24 peer-group mks-nodes
 !
 address-family ipv4 unicast
  network 203.0.113.64/29
  neighbor 198.19.51.225 prefix-list default_from_mylovelycozycloud in
  neighbor 198.19.51.225 prefix-list my_anycast_subnet out
  neighbor 198.19.51.225 route-map prpnd_private out
 exit-address-family
exit
!
bfd
 profile t-rex
  log-session-changes
  minimum-ttl 127
 exit
 !
 peer 198.19.51.225 
  profile t-rex
 exit
 !
exit
!
end

Не забываем заменить номер AS (в примере – 64859) на свой, согласованный с технической поддержкой.

Запускаем сервис и задействуем запуск при перезагрузке ВМ:


      sudo systemctl enable --now frr.service

8. Если ВМ-шлюз предполагается использовать для NAT и фильтрации, то не забываем настроить netfilter через iptables/nftables.

9. Разворачиваем два управляемых кластера Kubernetes. Проще сделать это, используя Terraform. 

Пример части манифеста:


      # Получение актуальных версий k8s в MKS
data "selectel_mks_kube_versions_v1" "mks_versions" {
  project_id = var.project_id
  region     = local.region_name
}

# Кластер managed kubernetes services. Базовый вариант без отказоустойчивости master-нод.
resource "selectel_mks_cluster_v1" "mks_cluster_1" {
  name                              = var.mks_cluster_name
  region                            = local.region_name
  project_id                        = var.project_id
  network_id                        = var.network_id
  subnet_id                         = var.subnet_id
  kube_version                      = data.selectel_mks_kube_versions_v1.mks_versions.default_version
  zonal                             = "false"
  enable_patch_version_auto_upgrade = "false"
}

# Нодгруппа в кластере
resource "selectel_mks_nodegroup_v1" "mks_nodegroup_1" {
  cluster_id                   = selectel_mks_cluster_v1.mks_cluster_1.id
  project_id                   = var.project_id
  region                       = local.region_name
  availability_zone            = var.availability_zone
  nodes_count                  = var.nodes_count
  keypair_name                 = var.ssh_keypair_name
  flavor_id                    = var.node_flavor
  volume_gb                    = var.node_volume_size
  volume_type                  = "${var.node_volume_type}.${var.availability_zone}"
  install_nvidia_device_plugin = "false"
}

Если интересует полностью готовый вариант, он представлен в репозитории.

10. Разрешаем anycast-подсеть на всех сетевых портах рабочих нод двух созданных кластеров в разных пулах.

Если кластер создан в панели управления Selectel, то в «разрешенные» подсеть можно добавить через тот же интерфейс или CLI. Если вы выбрали второй вариант, то выполните в терминале управляющей машины следующий блок кода:


      export ANYCAST_CIDR="203.0.113.64/29"
vm_ips=$(openstack server list --long --tags mks_cluster=true -c Networks -f json | jq -r '.[].Networks[][0]')
port_ids=$(openstack port list --any-tags mks_cluster=true --long -f json | jq -r --arg nodes_ip "$vm_ips" '.[]|select(."Fixed IP Addresses"[0].ip_address as $ips | ($nodes_ip|split("\n"))|index($ips))|.ID')
for id in $port_ids
  do
    openstack port set --allowed-address ip-address=${ANYCAST_CIDR} ${id}
    echo "Port AAPs: "
    openstack port show -c allowed_address_pairs ${id}
  done

Не забудьте заменить переменную, задающую подсеть ANYCAST_CIDR в скрипте, на свою.

11. Устанавливаем MetalLB в оба кластера MKs:


      kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.15.2/config/manifests/metallb-native.yaml

12. Добавляем профиль BFD, который будет использоваться для пиринга с нашим шлюзом:


      cat <<EOF | kubectl apply -f -
apiVersion: metallb.io/v1beta1
kind: BFDProfile
metadata:
  name: my-default
  namespace: metallb-system
spec:
  receiveInterval: 300
  transmitInterval: 300
  detectMultiplier: 3
  minimumTtl: 127
EOF

13. Добавляем ВМ-шлюз как BGP peer для MetalLB.

Afla


      cat <<EOF | kubectl apply -f -
apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
  name: gateway-alfa-unoone
  namespace: metallb-system
spec:
  myASN: 64859
  peerASN: 64859
  peerAddress: 192.168.247.254
  bfdProfile: my-default
EOF

Bravo


      cat <<EOF | kubectl apply -f -
apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
  name: gateway-bravo-bissotwo
  namespace: metallb-system
spec:
  myASN: 64859
  peerASN: 64859
  peerAddress: 192.168.147.254
  bfdProfile: my-default
EOF

Снова не забываем заменить номер AS (в примере — 64859) на свой, согласованный с технической поддержкой. Проверяем, что peer добавился:


      kubectl -n metallb-system get bgppeers.metallb.io

Пример вывода:


      NAME                      ADDRESS           ASN     BFD PROFILE   MULTI HOPS
gateway-alfa-unoone       192.168.247.254   64859   my-default

14. Добавляем выделенный провайдером пул anycast-адресов в назначаемые для балансировщиков MetalLB:


      cat <<EOF | kubectl apply -f -
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: anycast-pool
  namespace: metallb-system
spec:
  addresses:
  - 203.0.113.64/29
EOF

15. Добавляем анонсирование для адресов из пула для балансировщиков MetalLB:


      cat <<EOF | kubectl apply -f -
apiVersion: metallb.io/v1beta1
kind: BGPAdvertisement
metadata:
  name: anycast-adv
  namespace: metallb-system
spec:
  ipAddressPools:
    - anycast-pool
EOF

16. Отключаем создание облачных балансировщиков (Octavia), так как будет использоваться MetalLB.


      export NEW_CLOUD_CONF=$(kubectl -n kube-system get secret cloud-config -o jsonpath='{.data.cloud\.conf}' | base64 -d | sed 's#\[LoadBalancer\]#\[LoadBalancer\]\nenabled=false#g' | base64)
kubectl -n kube-system patch secret cloud-config -p '{"data":{"cloud.conf":"'${NEW_CLOUD_CONF}'"}}' && kubectl -n kube-system rollout restart deploy/openstack-cloud-controller-manager

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

Этап настройки завершен. Теперь следует проверить работоспособность решения.

Применение решения

Посмотрим, как работает настроенный MetalLB. Мы будем проверять только функциональность, без нагрузочного тестирования.

1. Разворачиваем простой echoserver в двух кластерах в разных пулах:


      cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cilium-echoserver
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: cilium-echoserver
  template:
    metadata:
      labels:
        app: cilium-echoserver
    spec:
      containers:
        - name: cilium-echoserver
          image: cilium/echoserver:latest
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8080
              protocol: TCP
          env:
            - name: PORT
              value: "8080"
EOF

Проверяем:


      kubectl get pods         
NAME                                 READY   STATUS    RESTARTS   AGE
cilium-echoserver-7f9979f65d-9b8hj   1/1     Running   0          2d10h
cilium-echoserver-7f9979f65d-bvt79   1/1     Running   0          2d10h

2. Публикуем сервис. Применяем для двух кластеров:


      cat <<EOF | kubectl apply -f -

kind: Service
apiVersion: v1
metadata:
  name: cilium-echo-svc-metallb
  annotations:
    loadbalancer.openstack.org/class: non-existent # <- if you do not want to use Octavia LB
    metallb.io/address-pool: anycast-pool # <- use your pool name here
spec:
  selector:
    app: cilium-echoserver
  type: LoadBalancer
  ports:
    - name: http
      port: 80
      targetPort: 8080
      protocol: TCP
EOF

Проверяем:


      kubectl get svc cilium-echo-svc-metallb
NAME                      TYPE           CLUSTER-IP       EXTERNAL-IP     PORT(S)        AGE
cilium-echo-svc-metallb   LoadBalancer   10.107.244.230   203.0.113.65   80:32043/TCP    1h

3. Запрашиваем данные от echo-сервера:


      curl -sS "http://203.0.113.65/?hello_there"

Hostname: cilium-echoserver-7f9979f65d-9b8hj

Pod Information:
	-no pod information available-

Server values:
	server_version=nginx: 1.13.3 - lua: 10008

Request Information:
	client_address=::ffff:192.168.247.237
	method=GET
	real path=/?hello_there
	query=hello_there
	request_version=1.1
	request_scheme=http
	request_uri=http://203.0.113.65:8080/?hello_there

Request Headers:
	accept=*/*
	host=203.0.113.65
	user-agent=curl/8.7.1

Request Body:
	-no body in request-

Запросы на anycast-адрес проходят. В примере нам отвечает под в кластере Alfa.

Манифесты для развертывание стенда

При прочтении возникает впечатление, что развертывание подобной конфигурации можно (и нужно) автоматизировать. Это действительно так — есть даже специальное решение. 

В репозитории доступны готовые Terraform-манифесты — с их помощью можно быстро развернуть всю описанную инфраструктуру. Главное — внимательно прочитать README при применении, так как оно происходит в три этапа. Впоследствии представленный код можно легко модифицировать для создания собственной продуктивной инсталляции.

После применения конфигурации экспортируем конфигурационные файлы K8s:


      # выполняем в папке c описанием инфраструктуры TF

terraform output -raw mks_cluster_alfa_kubeconfig > ../configs/mks_cluster_alfa_kubeconfig.yaml

terraform output -raw mks_cluster_bravo_kubeconfig > ../configs/mks_cluster_bravo_kubeconfig.yaml

export KUBECONFIG=`pwd`/../configs/mks_cluster_alfa_kubeconfig.yaml:`pwd`/../configs/mks_cluster_bravo_kubeconfig.yaml:~/.kube/config

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


      # выводим список доступных контекстов 
kubectl config get-contexts
# переключаемся на кластер Bravo
kubectl config use-context admin@k8s-bravo-bissotwo
# запрашиваем ноды кластра Bravo
kubectl get nodes -o wide
# переключаемся на Alfa
kubectl config use-context admin@k8s-alfa-unoone
# и получаем уже его список нод
kubectl get nodes -o wide

Switchover: плановые переключения

Вы развернули кластеры, приложения в них и начали тестовую эксплуатацию. Как же переключиться между дата-центрами?
Предположим, что вы опубликовали сервис с одинаковыми публичными IP. Конфигурацией это не возбраняется. Для этого при создании сервиса в манифесте должна присутствовать аннотация metallb.io/loadBalancerIPs или спецификация spec.loadBalancerIP. 

Пример манифеста, который применен в двух кластерах:


      kind: Service
apiVersion: v1
metadata:
  name: echo-svc-sameip
  annotations:
    loadbalancer.openstack.org/class: non-existent # <- if you do not want to use Octavia LB
    metallb.io/address-pool: anycast-pool # <- use your pool name here
    metallb.io/loadBalancerIPs: 203.0.113.70
spec:
  selector:
    app: cilium-echoserver
  type: LoadBalancer
  ports:
    - name: http
      port: 80
      targetPort: 8080
      protocol: TCP

Куда попадет клиентский запрос при обращении к IP? В сервис в каком кластере? И как переключить клиентский трафик на другой кластер? Давайте разбираться.

Обратим внимание на конфигурацию frrouting. На ВМ gateway-alfa-unoone мы имеем следующую карту маршрутов (route-map):


      route-map prpnd_private permit 10
 description For AS PATH prepend
 set as-path prepend 64859
exit

А на gateway-bravo-bissotwo — такой route-map:


      route-map prpnd_private permit 10
 description For AS PATH prepend
 set as-path prepend 64859 64859 64859
exit

Вспоминаем: нас есть два основных метода «ухудшения» маршрутов для анонсируемых префиксов, а именно AS-Path prepend и MED. Очевидно, что в своей конфигурации мы используем первый метод. «Хитрости» с community не рассматриваем — это выходит за рамки данной статьи.

Сами же себе отвечаем на первый вопрос. Если адрес анонсируется из двух кластеров K8s одновременно, то трафик будет направлен туда, где AS Path короче, то есть на ВМ Alfa. А для переключения достаточно добавить в AS Path больше ASN, чем присваивается при анонсе anycast-подсети с ВМ в другом дата-центре.

Теперь представим что у нас штатное переключение нагрузки и Alfa в Bravo. Как его сделать?

1. Для начала убедимся, что запрос приходит в тот кластер, нагрузку с которого планируем снять:


      curl -sS 'http://203.0.113.70/?where_am_i'

Hostname: cilium-echoserver-7f8dccd9b6-zgmxf
Pod Information:
	-no pod information available-
Server values:
	server_version=nginx: 1.13.3 - lua: 10008
Request Information:
	client_address=::ffff:192.168.247.56
	method=GET
	real path=/?where_am_i
	query=where_am_i
	request_version=1.1
	request_scheme=http
	request_uri=http://203.0.113.70:8080/?where_am_i
Request Headers:
	accept=*/*
	host=203.0.113.70
	user-agent=curl/8.7.1
Request Body:
	-no body in request-

Запрос пришел на под в кластере Alfa.

2. Заходим по SSH на ВМ в gateway-alfa-unoone и запускаем vtysh, в котором выполняем следующие команды:


      conf t
route-map prpnd_private permit 10
set as-path prepend 64859 64859 64859 64859 64859
exit
exit

То есть мы добавляем к пути еще дополнительно четыре приватных номера автономной системы.

3. Проверяем количество ASN, добавленных к пути. В том же открытом vtysh выполняем:


      show bgp paths

Пример вывода:


      Address Refcnt Path
[0x5569c25e6810:3382778021] (2) 64859 64859 64859 64859 64859
[0x5569c259bbc0:2448081105] (7) 
[0x5569c25a98a0:16672824] (2) 65101

4. Выполняем запрос и убеждаемся, что он приходит на балансировщик в другом кластере (k8s-bravo-bissotwo):


      curl -sS 'http://203.0.113.70/?where_am_i'

Hostname: cilium-echoserver-7f8dccd9b6-gtw4z
Pod Information:
	-no pod information available-
Server values:
	server_version=nginx: 1.13.3 - lua: 10008
Request Information:
	client_address=::ffff:192.168.147.69
	method=GET
	real path=/?where_am_i
	query=where_am_i
	request_version=1.1
	request_scheme=http
	request_uri=http://203.0.113.70:8080/?where_am_i
Request Headers:
	accept=*/*
	host=203.0.113.70
	user-agent=curl/8.7.1
Request Body:
	-no body in request-

Мы добились желаемого.

Стоит учесть: адреса с маской /32, которые анонсируются только из одного кластера, останутся доступными исключительно через соответствующий маршрутизатор.


      kubectl --context admin@k8s-alfa-unoone get svc
NAME                      TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)        AGE
cilium-echo-svc-metallb   LoadBalancer   10.96.3.218    203.0.113.65   80:32110/TCP   8d
echo-svc-sameip           LoadBalancer   10.100.78.94   203.0.113.70   80:31512/TCP   3d1h
kubernetes                ClusterIP      10.96.0.1      <none>          443/TCP        9d


kubectl --context admin@k8s-bravo-bissotwo get svc
NAME                      TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)        AGE
cilium-echo-svc-metallb   LoadBalancer   10.98.244.156   203.0.113.66   80:32731/TCP   8d
echo-svc-sameip           LoadBalancer   10.104.81.133   203.0.113.70   80:30576/TCP   3d1h
kubernetes                ClusterIP      10.96.0.1       <none>          443/TCP        9d

      curl -sS http://203.0.113.65

Hostname: cilium-echoserver-7f8dccd9b6-579hq
Pod Information:
	-no pod information available-
Server values:
	server_version=nginx: 1.13.3 - lua: 10008
Request Information:
	client_address=::ffff:192.168.247.187
	method=GET
	real path=/
	query=
	request_version=1.1
	request_scheme=http
	request_uri=http://203.0.113.65:8080/
Request Headers:
	accept=*/*
	host=203.0.113.65
	user-agent=curl/8.7.1
Request Body:
	-no body in request-

Как видно, трафик на 203.0.113.65 по прежнему идет через балансировщик в кластере Alfa, потому что подсеть анонсируется только при появлении адреса в таблице маршрутизации по iBGP. Это удобно и гибко.

Failover: аварийные переключения

Хороший провайдер IT-инфраструктуры в первую очередь заботится о надежности своих сервисов, но ситуации бывают разные. Давайте сымитируем «падение» одного из дата-центров и посмотрим, как поведет себя система.

Для этого «вредного» действия принудительно выключим рабочие ноды K8s и машину-шлюз в Bravo. Но сначала установим и запустим утилиту для тестирования locust и соберем статистику, сколько запросов пропадает при выключении и автоматическом переключении ВМ.

Напишем простой locustfile:


      from locust import HttpUser, task, between

class TooSimpleTest(HttpUser):
    wait_time = between(1, 2)

    def on_start(self):
        # Set a custom User-Agent
        self.client.headers['User-Agent'] = "JamesBondAgent/007"

    @task
    def simple_hello(self):
        self.client.get("/hello")
        self.client.get("/k8s")

Запускаем locust:


      export ANYCAST_LB_IP=203.0.113.70
locust -H http://${ANYCAST_LB_IP} -u 100 -r 10 -t 10m --autostart

Теперь выключаем ВМ с помощью OpenStack CLI, но не возбраняется сделать это и через панель управления.


      # отключаем ноды K8s, используя тег 
for srv in $(openstack server list --tags mks_cluster=true -c ID -f value 2>/dev/null) ; do openstack server stop $srv 2>/dev/null ; done

# еще один вариант отключения нод
openstack server list --tags mks_cluster=true -c ID -f value | xargs openstack server stop

# отключаем ВМ-шлюз, не забываем поменять IP на свой
export GW_IP_ADDR=198.19.51.230
openstack server stop $(openstack server list --ip $GW_IP_ADDR -c ID -f value 2>/dev/null)

Получаем следующую картину при выключении:

Как видим, «пропало» всего 96 запросов при RPS в 127. Неплохой результат! Понятно, что echoserver возвращает контент небольшого размера, но тем не менее.

Выводы

Чего же мы добились? В первую очередь, получили основу для отказоустойчивого или даже геораспределенного инфраструктурного решения на базе Managed Kubernetes. Реализованная схема подходит и для Kubernetes-кластера, развернутого собственными силами. Отработка отказа на уровне сети происходит быстро и позволяет с минимумом  «боли» пережить даже такой серьезный инцидент, как выход из строя целого дата-центра.

Конечно, схему можно улучшить. Одна ВМ в облаке может выглядеть как точка отказа. Можно создать еще одну ВМ с frr и настроить дополнительный пиринг как с провайдером, так и для MetalLB. Кроме того, есть смысл улучшить shell-operator, пока мы не реализовали автоматическое изменение настроек allowed address pairs в облачных сетях.