Шаг 5: динамическое изучение MAC/ARP для бэкендов

MAC каждого члена пула больше не статическая константа в topology.env,
а резолвится демоном hcd через обычный ARP ядра — в реальном
окружении MAC бэкенда заранее не известен (сервер ещё не подключён,
NIC может замениться), топология не описывается статически, в отличие
от стенда.

hcif-порты (единственные адреса узла в ядре) уже были настоящими
L3-интерфейсами в тех же сегментах, что и бэкенды — единственное, что
мешало обычному ARP, это permanent-записи ip neigh в entrypoint.sh.
Убрав их и добавив hc/neigh.go (читает ip -json neigh show, точечно
заливает бандл в таблицу 21 на том же тикере, что и health-пробы),
получили резолвер без нового OpenFlow-контроллера.

Таблица 21 стала единственным источником MAC для обоих путей —
маршрутизируемого и коммутируемого (шаг 3): таблица 12 больше не
дублирует MAC инлайново, ct(commit) ведёт сразу в таблицу 21.

Исправлен попутно найденный баг: после apply.sh (replace-flows)
таблица 21 не восстанавливалась, поскольку syncNeighbors сравнивал
MAC с памятью демона, а не с датапасом. Добавлен force-режим по
аналогии с Agent.apply(), плюс регрессионная проверка в verify.sh.

Проверено на живом стенде: MAC всех четырёх членов резолвлен и
совпадает с реальными интерфейсами; смена MAC "железа" обнаружена и
применена без вмешательства за счёт штатного старения ARP ядра.
Регрессия: 94 из 94 проверок.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
ayurishchevandClaude Opus 5 committed 2026-08-19 12:33:48 +03:00
1 parent f34d2edefa
commit fa389f119b
17 files changed
+678 -61

No files matched your search

+10 -15
View File
@@ -82,31 +82,26 @@ attach_port "$P3_MAC" "$P3_IFNAME" "$P3_OFPORT"
# --- 4a. Порты-источники health-проб -----------------------------------------
# Единственные адреса, которые узел держит в ядре: с них уходят пробы (§7.1
# дизайна v1 — источник проб должен быть уникальным адресом узла, не VIP).
# MAC бэкенда прописывается статически: ARP-резолвер узлу не нужен, MAC членов
# пула и так зафиксированы в docker-compose.yml.
#
# MAC членов пула здесь больше НЕ прописывается статически (шаг 5): hcif-порты
# — настоящие L3-интерфейсы ядра в том же сегменте, что и бэкенды, и ядро само
# резолвит их MAC обычным ARP. Первая проба после старта чуть медленнее (кадр
# ARP-запроса уходит через br-lb и возвращается ответом реального бэкенда), но
# дальше запись живёт в neigh-таблице ядра обычным старением ARP — именно её
# читает hcd (hc/neigh.go) и заливает в таблицу 21 OpenFlow.
hc_port() {
local name="$1" ofport="$2" mac="$3" ip="$4" net="$5"
shift 5
ovs-vsctl --may-exist add-port "$BR" "$name" \
-- set Interface "$name" type=internal ofport_request="$ofport" \
-- set Interface "$name" mac="\"$mac\""
ip link set dev "$name" address "$mac"
ip addr replace "$ip/${net##*/}" dev "$name"
ip link set dev "$name" up
# Остальные аргументы — пары «адрес MAC» членов пула в этом сегменте.
local peers=""
while [ $# -ge 2 ]; do
ip neigh replace "$1" lladdr "$2" dev "$name" nud permanent
peers="$peers $1"
shift 2
done
log "порт $name ($ip) — источник health-проб для$peers"
log "порт $name ($ip) — источник health-проб"
}
hc_port "$HC2_IFNAME" "$HC2_OFPORT" "$HC2_MAC" "$HC2_IP" "$P2_NET" \
"$BE1_IP" "$BE1_MAC" "$BE3_IP" "$BE3_MAC"
hc_port "$HC3_IFNAME" "$HC3_OFPORT" "$HC3_MAC" "$HC3_IP" "$P3_NET" \
"$BE2_IP" "$BE2_MAC" "$BE4_IP" "$BE4_MAC"
hc_port "$HC2_IFNAME" "$HC2_OFPORT" "$HC2_MAC" "$HC2_IP" "$P2_NET"
hc_port "$HC3_IFNAME" "$HC3_OFPORT" "$HC3_MAC" "$HC3_IP" "$P3_NET"
# Ждём, пока vswitchd реально привяжет порты к датапасу.
for name in "$PUB_IFNAME" "$P2_IFNAME" "$P3_IFNAME"; do
+17 -4
View File
@@ -2,6 +2,10 @@
# Генерация конфигурации демона hcd из topology.env — единственного источника
# правды по адресам стенда. Пока пул задан статически; на следующем шаге его
# место займёт модель Load_Balancer -> Listener -> Pool -> Member в OVSDB.
#
# MAC членов пула здесь не фигурирует (шаг 5): hcd резолвит его сам через
# neigh-таблицу ядра (hc/neigh.go) на интерфейсе "iface" и заливает в таблицу
# "adj_table" по указанному "ofport". См. docs/STEP5_SUMMARY.md.
set -euo pipefail
LB_DIR=/opt/lb
@@ -18,6 +22,7 @@ cat > "$OUT" <<EOF
"slots": $SLOTS,
"slot_table": 11,
"dnat_table": 12,
"adj_table": 21,
"probe": "$HC_PROBE",
"http_path": "$HC_HTTP_PATH",
"interval": "$HC_INTERVAL",
@@ -33,7 +38,9 @@ cat > "$OUT" <<EOF
"address": "$BE1_IP",
"port": $BE1_PORT,
"weight": 1,
"source": "$HC2_IP"
"source": "$HC2_IP",
"iface": "$HC2_IFNAME",
"ofport": $BE1_OFPORT
},
{
"id": 2,
@@ -41,7 +48,9 @@ cat > "$OUT" <<EOF
"address": "$BE2_IP",
"port": $BE2_PORT,
"weight": 1,
"source": "$HC3_IP"
"source": "$HC3_IP",
"iface": "$HC3_IFNAME",
"ofport": $BE2_OFPORT
},
{
"id": 3,
@@ -49,7 +58,9 @@ cat > "$OUT" <<EOF
"address": "$BE3_IP",
"port": $BE3_PORT,
"weight": 1,
"source": "$HC2_IP"
"source": "$HC2_IP",
"iface": "$HC2_IFNAME",
"ofport": $BE3_OFPORT
},
{
"id": 4,
@@ -57,7 +68,9 @@ cat > "$OUT" <<EOF
"address": "$BE4_IP",
"port": $BE4_PORT,
"weight": 1,
"source": "$HC3_IP"
"source": "$HC3_IP",
"iface": "$HC3_IFNAME",
"ofport": $BE4_OFPORT
}
]
}
+7
View File
@@ -20,6 +20,7 @@ lbctl — осмотр датапаса стенда hpnn_v2
metrics метрики в формате Prometheus
conns таблица соединений ct (зона $CT_ZONE)
fdb таблица коммутации сегментов: выученные MAC и порты
neigh MAC членов пула, резолвленный hcd через ARP ядра (таблица 21)
trace <src_ip> [src_port] [сегмент]
ofproto/trace для TCP-сессии клиент -> VIP.
Сегмент: pub (по умолчанию), p2, p3 — определяет порт входа
@@ -85,6 +86,12 @@ fdb)
| sed -n 's/.*n_packets=\([0-9]*\).*priority=\(50\|10\),reg0=\(0x[0-9a-f]*\).*/ сегмент \3 приоритет \2: пакетов \1/p' \
| sort
;;
neigh)
api /neigh.txt
echo
echo "Правила таблицы 21 (priority=100 — заливает hcd, priority=90 — learn):"
ovs-ofctl -O "$OF" dump-flows "$BR" table=21 | grep -E 'priority=(100|90),ip' | sort
;;
trace)
src_ip="${2:?укажите IP клиента}"
src_port="${3:-40000}"
+21 -12
View File
@@ -272,16 +272,18 @@ table=11,priority=0 actions=drop
# Слот положил идентификатор члена в reg2. SNAT не выполняется ни в одном из
# путей: бэкенд видит реальный адрес клиента.
#
# Член в том же сегменте, что и клиент (приоритет 200) — выдача коммутацией:
# меняется только MAC назначения, TTL не уменьшается, маршрутизации нет. Для
# ВМ клиент и бэкенд остаются L2-соседями, какими и были.
# Член в том же сегменте, что и клиент (приоритет 200) — TTL не уменьшается,
# маршрутизации нет. MAC назначения здесь больше не пишется (шаг 5): следующий
# переход — таблица 21, единственное место, где живёт актуальный MAC члена
# (резолвит hcd через ARP, см. hc/neigh.go). Для ВМ клиент и бэкенд остаются
# L2-соседями, какими и были — таблица 21 сама не декрементирует TTL.
EOF
for seg_reg in "$P2_REG:$P2_MEMBERS" "$P3_REG:$P3_MEMBERS"; do
reg="${seg_reg%%:*}"
for m in ${seg_reg#*:}; do
eval "mip=\$${m}_IP; mport=\$${m}_PORT; mmac=\$${m}_MAC; mid=\$${m}_ID"
echo "table=12,priority=200,reg0=$reg,ip,reg2=$mid actions=mod_dl_dst:$mmac,ct(commit,zone=$CT_ZONE,nat(dst=$mip:$mport),table=25)"
eval "mip=\$${m}_IP; mport=\$${m}_PORT; mid=\$${m}_ID"
echo "table=12,priority=200,reg0=$reg,ip,reg2=$mid actions=ct(commit,zone=$CT_ZONE,nat(dst=$mip:$mport),table=21)"
done
done
@@ -329,15 +331,22 @@ table=20,priority=0 actions=drop
# =============================================================================
# Таблица 21 — adjacency (next-hop MAC + выходной порт)
# =============================================================================
# Члены пула прописаны статически: их MAC и порт фиксированы в topology.env.
table=21,priority=100,ip,nw_dst=$BE1_IP actions=mod_dl_dst:$BE1_MAC,output:$BE1_OFPORT
table=21,priority=100,ip,nw_dst=$BE2_IP actions=mod_dl_dst:$BE2_MAC,output:$BE2_OFPORT
table=21,priority=100,ip,nw_dst=$BE3_IP actions=mod_dl_dst:$BE3_MAC,output:$BE3_OFPORT
table=21,priority=100,ip,nw_dst=$BE4_IP actions=mod_dl_dst:$BE4_MAC,output:$BE4_OFPORT
# Ответы на health-пробы — в стек узла через internal-порты.
# Единственное место, где применяется MAC следующего перехода — и для
# маршрутизируемого пути (после таблицы 20), и для коммутируемого (напрямую
# из таблицы 12, без decrement TTL). Три источника записей, по приоритету:
#
# priority=100 члены пула — заливает и обновляет hcd (hc/neigh.go),
# резолвя MAC настоящим ARP через hcif-порты. Пусто до
# первого резолва после старта — fail-close ниже, окно то же,
# что у таблицы слотов.
# priority=90 клиенты (публичные и внутрисегментные) — действие learn из
# таблиц 0 и 1: адрес заранее не известен, изучается пассивно.
# priority=0 неизвестный next-hop — drop.
#
# Ответы на health-пробы — в стек узла через internal-порты; это собственные
# адреса узла, а не next-hop, поэтому остаются статичными.
table=21,priority=100,ip,nw_dst=$HC2_IP actions=mod_dl_dst:$HC2_MAC,output:$HC2_OFPORT
table=21,priority=100,ip,nw_dst=$HC3_IP actions=mod_dl_dst:$HC3_MAC,output:$HC3_OFPORT
# priority=90 — записи клиентов, устанавливаемые действием learn из таблиц 0 и 1.
table=21,priority=0 actions=drop
# =============================================================================
+6
View File
@@ -25,6 +25,12 @@ VIP_PORT=80
# Адрес шлюза LB_P2_IP и VIP приватного листенера P2_VIP отвечают одним MAC
# (P2_MAC) — по нему пайплайн отличает L3-трафик к маршрутизатору от
# L2-трафика сегмента.
#
# BE*_MAC (шаг 5): это уже не источник правды для OpenFlow, а «заводской» MAC,
# который attach-segment.sh назначает интерфейсу ВМ — эмуляция того, что в
# реальности прошито в NIC сервера. В таблицу 21 этот MAC не подставляется:
# его заново резолвит hcd через обычный ARP на hcif-порту (hc/neigh.go) и
# заливает в датапас сам. pipeline.sh эти переменные больше не читает.
P2_IFNAME=p2
P2_OFPORT=2
P2_MAC=02:42:0a:14:00:01