feat(cluster): 接入深圳专用 Worker 节点

新增深圳 K3s agent 与四节点 WireGuard 全互联接入脚本,将深圳归入香港逻辑 Worker 池并用污点限制为 Worker 专用。\n\n生产验收支持宁波 0、香港逻辑池 2 的双物理节点基线,补充副本拓扑分散、容量档位、链路巡检和零副本站点校验。\n\n已通过 Go 全量测试、go vet、govulncheck、迁移安全检查、ShellCheck、发布脚本测试、前后端 lint/test/build、Compose 与 Kubernetes 渲染校验。
This commit is contained in:
2026-08-01 09:38:10 +08:00
parent 132cda35d8
commit 70b0ffb9ae
12 changed files with 539 additions and 87 deletions
+271
View File
@@ -0,0 +1,271 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck source=scripts/cluster/common.sh
source "$script_dir/common.sh"
action=${1:-all}
[[ $action == preflight || $action == network || $action == install ||
$action == verify || $action == all ]] || {
echo 'usage: add-shenzhen-worker-node.sh [preflight|network|install|verify|all]' >&2
exit 64
}
load_cluster_env
require_commands ssh scp shasum
: "${CLUSTER_SHENZHEN_HOST:?AI_GATEWAY_SHENZHEN_HOST is required}"
node_name=easyai-shenzhen
node_ip=10.77.0.4
k3s_version=v1.36.2+k3s1
k3s_sha256=65a55ec56c24eab44383086166ec620a491952b7e23941a49ddca6e8a4c4b4de
secret_root="$cluster_root/.local-secrets/cluster"
cluster_environment="$secret_root/cluster.env"
k3s_binary="$secret_root/k3s-${k3s_version}-amd64"
registries_file="$cluster_root/deploy/kubernetes/registries.yaml"
assert_join_material() {
assert_private_file "$cluster_environment"
[[ -f $k3s_binary && ! -L $k3s_binary && -f $registries_file && ! -L $registries_file ]] || {
echo 'pinned K3s binary or registry configuration is missing' >&2
return 1
}
[[ $(shasum -a 256 "$k3s_binary" | awk '{print $1}') == "$k3s_sha256" ]] || {
echo 'pinned K3s binary checksum mismatch' >&2
return 1
}
}
preflight_node() {
cluster_ssh "$CLUSTER_SHENZHEN_HOST" bash -s <<'REMOTE'
set -euo pipefail
[[ $(id -u) -eq 0 && $(uname -m) == x86_64 ]]
. /etc/os-release
[[ $ID == debian && ${VERSION_ID%%.*} -ge 12 ]]
(( $(nproc) >= 4 ))
available_kib=$(awk '/MemAvailable:/ {print $2}' /proc/meminfo)
(( available_kib >= 6 * 1024 * 1024 ))
available_bytes=$(df -B1 --output=avail / | tail -n 1 | tr -d ' ')
(( available_bytes >= 20 * 1024 * 1024 * 1024 ))
timedatectl show -p NTPSynchronized --value | grep -Fxq yes
[[ ! -e /etc/rancher/k3s/config.yaml || -f /etc/rancher/k3s/config.yaml ]]
printf 'shenzhen_worker_preflight=PASS cpu=%s memory_available_mib=%s disk_available_gib=%s\n' \
"$(nproc)" "$((available_kib / 1024))" "$((available_bytes / 1024 / 1024 / 1024))"
REMOTE
}
configure_wireguard() {
local -a server_hosts=("$CLUSTER_NINGBO_HOST" "$CLUSTER_HONGKONG_HOST" "$CLUSTER_LOS_ANGELES_HOST")
local -a server_ips=(10.77.0.1 10.77.0.2 10.77.0.3)
local -a server_endpoints=("${CLUSTER_NINGBO_HOST#*@}" "${CLUSTER_HONGKONG_HOST#*@}" "${CLUSTER_LOS_ANGELES_HOST#*@}")
local -a server_keys=()
local shenzhen_key host key
shenzhen_key=$(cluster_ssh "$CLUSTER_SHENZHEN_HOST" 'bash -s' <<'REMOTE' | tail -n 1
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
if ! command -v wg >/dev/null 2>&1; then
apt-get update -qq
apt-get install -y -qq wireguard-tools
fi
install -d -m 0700 /etc/wireguard
if [[ ! -s /etc/wireguard/privatekey ]]; then
umask 077
wg genkey >/etc/wireguard/privatekey
fi
chmod 0600 /etc/wireguard/privatekey
wg pubkey </etc/wireguard/privatekey >/etc/wireguard/publickey
cat /etc/wireguard/publickey
REMOTE
)
[[ $shenzhen_key =~ ^[A-Za-z0-9+/]{43}=$ ]]
for host in "${server_hosts[@]}"; do
key=$(cluster_ssh "$host" 'wg show wg0 public-key')
[[ $key =~ ^[A-Za-z0-9+/]{43}=$ ]]
server_keys+=("$key")
done
cluster_ssh "$CLUSTER_SHENZHEN_HOST" bash -s -- \
"${server_keys[0]}" "${server_endpoints[0]}" "${server_ips[0]}" \
"${server_keys[1]}" "${server_endpoints[1]}" "${server_ips[1]}" \
"${server_keys[2]}" "${server_endpoints[2]}" "${server_ips[2]}" <<'REMOTE'
set -euo pipefail
private_key=$(cat /etc/wireguard/privatekey)
umask 077
temporary_config=$(mktemp /etc/wireguard/wg0.conf.XXXXXX)
cat >"$temporary_config" <<EOF
[Interface]
Address = 10.77.0.4/24
ListenPort = 51820
PrivateKey = ${private_key}
[Peer]
PublicKey = $1
Endpoint = $2:51820
AllowedIPs = $3/32
PersistentKeepalive = 25
[Peer]
PublicKey = $4
Endpoint = $5:51820
AllowedIPs = $6/32
PersistentKeepalive = 25
[Peer]
PublicKey = $7
Endpoint = $8:51820
AllowedIPs = $9/32
PersistentKeepalive = 25
EOF
chmod 0600 "$temporary_config"
mv "$temporary_config" /etc/wireguard/wg0.conf
for peer in "$2" "$5" "$8"; do
ufw allow from "$peer" to any port 51820 proto udp comment easyai-wireguard >/dev/null
done
systemctl enable wg-quick@wg0 >/dev/null
systemctl restart wg-quick@wg0
REMOTE
for host in "${server_hosts[@]}"; do
cluster_ssh "$host" bash -s -- "$shenzhen_key" "${CLUSTER_SHENZHEN_HOST#*@}" "$node_ip" <<'REMOTE'
set -euo pipefail
wg set wg0 peer "$1" allowed-ips "$3/32" endpoint "$2:51820" persistent-keepalive 25
wg-quick save wg0
REMOTE
done
verify_wireguard
}
verify_path() {
local source_host=$1 target_ip=$2 max_rtt=$3 label=$4 output loss average
output=$(cluster_ssh "$source_host" "ping -q -i 0.2 -c 10 -W 2 $target_ip")
loss=$(awk -F',' '/packet loss/ {gsub(/[^0-9.]/, "", $3); print $3}' <<<"$output")
average=$(awk -F'=' '/min\/avg\/max/ {split($2, values, "/"); gsub(/ /, "", values[2]); print values[2]}' <<<"$output")
[[ -n $loss && -n $average ]]
awk -v loss="$loss" -v average="$average" -v max="$max_rtt" \
'BEGIN {exit !(loss < 1 && average < max)}'
echo "wireguard_path=$label loss_percent=$loss average_rtt_ms=$average"
}
verify_wireguard() {
verify_path "$CLUSTER_NINGBO_HOST" "$node_ip" 80 ningbo_to_shenzhen
verify_path "$CLUSTER_SHENZHEN_HOST" 10.77.0.1 80 shenzhen_to_ningbo
verify_path "$CLUSTER_HONGKONG_HOST" "$node_ip" 80 hongkong_to_shenzhen
verify_path "$CLUSTER_SHENZHEN_HOST" 10.77.0.2 80 shenzhen_to_hongkong
verify_path "$CLUSTER_LOS_ANGELES_HOST" "$node_ip" 300 losangeles_to_shenzhen
verify_path "$CLUSTER_SHENZHEN_HOST" 10.77.0.3 300 shenzhen_to_losangeles
local host recent
for host in "$CLUSTER_NINGBO_HOST" "$CLUSTER_HONGKONG_HOST" "$CLUSTER_LOS_ANGELES_HOST" "$CLUSTER_SHENZHEN_HOST"; do
recent=$(cluster_ssh "$host" \
"wg show wg0 latest-handshakes | awk '\$2 > 0 && systime()-\$2 < 180 {count++} END {print count+0}'")
[[ $recent == 3 ]]
done
echo 'shenzhen_wireguard=PASS peers=4 recent_handshakes=12'
}
install_agent() {
assert_join_material
# shellcheck source=/dev/null
source "$cluster_environment"
: "${K3S_TOKEN:?}"
[[ $K3S_TOKEN =~ ^[0-9a-f]{64}$ ]]
cluster_ssh "$CLUSTER_SHENZHEN_HOST" 'install -d -m 0700 /etc/rancher/k3s'
printf '%s\n' "$K3S_TOKEN" | cluster_ssh "$CLUSTER_SHENZHEN_HOST" \
'umask 077; cat >/etc/rancher/k3s/cluster-token; chmod 0600 /etc/rancher/k3s/cluster-token'
cluster_scp "$k3s_binary" "$CLUSTER_SHENZHEN_HOST:/usr/local/bin/k3s"
cluster_scp "$registries_file" "$CLUSTER_SHENZHEN_HOST:/etc/rancher/k3s/registries.yaml"
cluster_ssh "$CLUSTER_SHENZHEN_HOST" bash -s -- "$k3s_version" "$k3s_sha256" <<'REMOTE'
set -euo pipefail
version=$1
expected_sha256=$2
chmod 0755 /usr/local/bin/k3s
echo "$expected_sha256 /usr/local/bin/k3s" | sha256sum -c - >/dev/null
umask 077
cat >/etc/rancher/k3s/config.yaml <<'EOF'
server: https://10.77.0.1:6443
token-file: /etc/rancher/k3s/cluster-token
node-name: easyai-shenzhen
node-ip: 10.77.0.4
flannel-iface: wg0
node-label:
- easyai.io/site=hongkong
- easyai.io/workload=true
- easyai.io/database=false
- easyai.io/worker=true
node-taint:
- easyai.io/worker-only=true:NoSchedule
EOF
chmod 0600 /etc/rancher/k3s/config.yaml /etc/rancher/k3s/registries.yaml
export DEBIAN_FRONTEND=noninteractive
if ! command -v netfilter-persistent >/dev/null 2>&1; then
apt-get update -qq
apt-get install -y -qq iptables-persistent
fi
ensure_rule() {
if ! iptables -C INPUT "$@" 2>/dev/null; then iptables -I INPUT 1 "$@"; fi
}
public_interface=$(ip -o route get 1.1.1.1 | awk '{for(i=1;i<=NF;i++) if($i=="dev"){print $(i+1); exit}}')
ensure_rule -i wg0 -s 10.77.0.0/24 -p tcp -m multiport --dports 6443,10250 \
-m comment --comment easyai-k3s-private -j ACCEPT
ensure_rule -i wg0 -s 10.77.0.0/24 -p udp --dport 8472 \
-m comment --comment easyai-flannel-private -j ACCEPT
ensure_rule -i "$public_interface" -p tcp -m multiport --dports 6443,10250 \
-m comment --comment easyai-k3s-public-drop -j DROP
ensure_rule -i "$public_interface" -p udp --dport 8472 \
-m comment --comment easyai-flannel-public-drop -j DROP
ensure_rule -i "$public_interface" -p tcp --dport 30000:31057 \
-m comment --comment easyai-nodeport-public-drop-low -j DROP
ensure_rule -i "$public_interface" -p tcp --dport 31059:32767 \
-m comment --comment easyai-nodeport-public-drop-high -j DROP
netfilter-persistent save >/dev/null
install_script=$(mktemp)
trap 'rm -f -- "$install_script"' EXIT
curl -fsSL https://get.k3s.io -o "$install_script"
chmod 0700 "$install_script"
INSTALL_K3S_SKIP_DOWNLOAD=true INSTALL_K3S_VERSION="$version" \
INSTALL_K3S_EXEC=agent "$install_script"
systemctl is-active --quiet k3s-agent
systemctl is-enabled --quiet k3s-agent
[[ ! -e /var/lib/rancher/k3s/server/db/etcd ]]
REMOTE
local ready=false
for _ in $(seq 1 90); do
if cluster_ssh "$CLUSTER_NINGBO_HOST" \
"k3s kubectl get node $node_name --no-headers 2>/dev/null | awk '\$2==\"Ready\" {found=1} END {exit !found}'"; then
ready=true
break
fi
sleep 2
done
[[ $ready == true ]]
verify_agent
}
verify_agent() {
verify_wireguard
cluster_ssh "$CLUSTER_NINGBO_HOST" bash -s -- "$node_name" <<'REMOTE'
set -euo pipefail
node=$1
kubectl='k3s kubectl'
[[ $($kubectl get node "$node" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}') == True ]]
[[ $($kubectl get node "$node" -o jsonpath='{.metadata.labels.easyai\.io/site}') == hongkong ]]
[[ $($kubectl get node "$node" -o jsonpath='{.metadata.labels.easyai\.io/workload}') == true ]]
[[ $($kubectl get node "$node" -o jsonpath='{.metadata.labels.easyai\.io/database}') == false ]]
[[ $($kubectl get node "$node" -o json | jq '[.spec.taints[]? | select(.key=="easyai.io/worker-only" and .effect=="NoSchedule")] | length') -eq 1 ]]
[[ $($kubectl get pods -A --field-selector "spec.nodeName=$node" -o json |
jq '[.items[] | select(.metadata.namespace!="kube-system")] | length') -eq 0 ]]
printf 'shenzhen_k3s_agent=PASS node=%s role=worker-only etcd_member=false\n' "$node"
REMOTE
}
case $action in
preflight) preflight_node ;;
network) preflight_node; configure_wireguard ;;
install) preflight_node; install_agent ;;
verify) preflight_node; verify_agent ;;
all) preflight_node; configure_wireguard; install_agent ;;
esac
+30 -27
View File
@@ -11,6 +11,11 @@ require_commands ssh scp
hosts=("$CLUSTER_NINGBO_HOST" "$CLUSTER_HONGKONG_HOST" "$CLUSTER_LOS_ANGELES_HOST")
public_endpoints=("${CLUSTER_NINGBO_HOST#*@}" "${CLUSTER_HONGKONG_HOST#*@}" "${CLUSTER_LOS_ANGELES_HOST#*@}")
wireguard_ips=(10.77.0.1 10.77.0.2 10.77.0.3)
if [[ -n $CLUSTER_SHENZHEN_HOST ]]; then
hosts+=("$CLUSTER_SHENZHEN_HOST")
public_endpoints+=("${CLUSTER_SHENZHEN_HOST#*@}")
wireguard_ips+=(10.77.0.4)
fi
public_keys=()
for host in "${hosts[@]}"; do
@@ -39,26 +44,24 @@ REMOTE
public_keys+=("$public_key")
done
for local_index in 0 1 2; do
peer_indexes=()
for candidate in 0 1 2; do
[[ $candidate -eq $local_index ]] || peer_indexes+=("$candidate")
for local_index in "${!hosts[@]}"; do
peer_arguments=()
for candidate in "${!hosts[@]}"; do
if [[ $candidate -ne $local_index ]]; then
peer_arguments+=(
"${public_keys[$candidate]}"
"${public_endpoints[$candidate]}"
"${wireguard_ips[$candidate]}"
)
fi
done
first=${peer_indexes[0]}
second=${peer_indexes[1]}
echo "[wireguard] configuring ${hosts[$local_index]} as ${wireguard_ips[$local_index]}"
cluster_ssh "${hosts[$local_index]}" bash -s -- \
"${wireguard_ips[$local_index]}" \
"${public_keys[$first]}" "${public_endpoints[$first]}" "${wireguard_ips[$first]}" \
"${public_keys[$second]}" "${public_endpoints[$second]}" "${wireguard_ips[$second]}" <<'REMOTE'
"${peer_arguments[@]}" <<'REMOTE'
set -euo pipefail
local_ip=$1
peer_one_key=$2
peer_one_endpoint=$3
peer_one_ip=$4
peer_two_key=$5
peer_two_endpoint=$6
peer_two_ip=$7
shift
private_key=$(cat /etc/wireguard/privatekey)
umask 077
temporary_config=$(mktemp /etc/wireguard/wg0.conf.XXXXXX)
@@ -67,19 +70,19 @@ cat >"$temporary_config" <<EOF
Address = ${local_ip}/24
ListenPort = 51820
PrivateKey = ${private_key}
EOF
while (( $# >= 3 )); do
cat >>"$temporary_config" <<EOF
[Peer]
PublicKey = ${peer_one_key}
Endpoint = ${peer_one_endpoint}:51820
AllowedIPs = ${peer_one_ip}/32
PersistentKeepalive = 25
[Peer]
PublicKey = ${peer_two_key}
Endpoint = ${peer_two_endpoint}:51820
AllowedIPs = ${peer_two_ip}/32
PublicKey = $1
Endpoint = $2:51820
AllowedIPs = $3/32
PersistentKeepalive = 25
EOF
shift 3
done
(( $# == 0 ))
chmod 0600 "$temporary_config"
mv "$temporary_config" /etc/wireguard/wg0.conf
systemctl enable wg-quick@wg0 >/dev/null
@@ -87,8 +90,8 @@ systemctl restart wg-quick@wg0
REMOTE
done
for source_index in 0 1 2; do
for target_index in 0 1 2; do
for source_index in "${!hosts[@]}"; do
for target_index in "${!hosts[@]}"; do
[[ $source_index -eq $target_index ]] && continue
cluster_ssh "${hosts[$source_index]}" \
"ping -c 3 -W 2 ${wireguard_ips[$target_index]} >/dev/null"
@@ -98,10 +101,10 @@ done
for host in "${hosts[@]}"; do
latest_handshake=$(cluster_ssh "$host" \
"wg show wg0 latest-handshakes | awk '\$2 > 0 { count++ } END { print count + 0 }'")
[[ $latest_handshake == 2 ]] || {
[[ $latest_handshake == $((${#hosts[@]} - 1)) ]] || {
echo "WireGuard handshake verification failed on $host" >&2
exit 1
}
done
echo 'wireguard_bootstrap=PASS peers=3'
echo "wireguard_bootstrap=PASS peers=${#hosts[@]}"
+6 -1
View File
@@ -28,7 +28,8 @@ load_cluster_env() {
CLUSTER_NINGBO_HOST=${AI_GATEWAY_NINGBO_HOST:-$AI_GATEWAY_DEPLOY_HOST}
CLUSTER_LOS_ANGELES_HOST=${AI_GATEWAY_LOS_ANGELES_HOST:-$AI_GATEWAY_DEPLOY_HOST_2}
CLUSTER_HONGKONG_HOST=${AI_GATEWAY_HONGKONG_HOST:-$AI_GATEWAY_DEPLOY_HOST_3}
export CLUSTER_NINGBO_HOST CLUSTER_LOS_ANGELES_HOST CLUSTER_HONGKONG_HOST
CLUSTER_SHENZHEN_HOST=${AI_GATEWAY_SHENZHEN_HOST:-}
export CLUSTER_NINGBO_HOST CLUSTER_LOS_ANGELES_HOST CLUSTER_HONGKONG_HOST CLUSTER_SHENZHEN_HOST
for host in "$CLUSTER_NINGBO_HOST" "$CLUSTER_HONGKONG_HOST" "$CLUSTER_LOS_ANGELES_HOST"; do
[[ $host =~ ^root@[A-Za-z0-9.-]+$ ]] || {
@@ -36,6 +37,10 @@ load_cluster_env() {
return 1
}
done
if [[ -n $CLUSTER_SHENZHEN_HOST && ! $CLUSTER_SHENZHEN_HOST =~ ^root@[A-Za-z0-9.-]+$ ]]; then
echo "Shenzhen Worker host must use root@host syntax" >&2
return 1
fi
[[ $ALI_REGION =~ ^[a-z0-9-]+$ && $ALI_BUCKET =~ ^[A-Za-z0-9.-]+$ ]] || {
echo 'invalid OSS region or bucket' >&2
return 1
@@ -140,12 +140,17 @@ pool_saturated_pods() {
check_wireguard() {
local -a hosts=("$CLUSTER_NINGBO_HOST" "$CLUSTER_HONGKONG_HOST" "$CLUSTER_LOS_ANGELES_HOST")
local -a ips=(10.77.0.1 10.77.0.2 10.77.0.3)
local source target output loss average handshakes
for source in 0 1 2; do
[[ -z $CLUSTER_SHENZHEN_HOST ]] || {
hosts+=("$CLUSTER_SHENZHEN_HOST")
ips+=(10.77.0.4)
}
local source target output loss average handshakes peer_count
peer_count=$((${#hosts[@]} - 1))
for source in "${!hosts[@]}"; do
handshakes=$(cluster_ssh "${hosts[$source]}" \
"wg show wg0 latest-handshakes | awk '\$2 > 0 && systime()-\$2 < 180 {count++} END {print count+0}'")
(( handshakes == 2 )) || return 1
for target in 0 1 2; do
(( handshakes == peer_count )) || return 1
for target in "${!hosts[@]}"; do
(( source == target )) && continue
output=$(cluster_ssh "${hosts[$source]}" "ping -q -c 10 -W 2 ${ips[$target]}") || return 1
loss=$(awk -F',' '/packet loss/ {gsub(/[^0-9.]/,"",$3); print $3}' <<<"$output")
+124 -34
View File
@@ -99,6 +99,10 @@ require_commands curl git go jq node openssl sed shasum
: "${AI_GATEWAY_ACCEPTANCE_ETCD_WAL_FSYNC_MAX_MS:=15}"
: "${AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MAX_REPLICAS_NINGBO:=2}"
: "${AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MAX_REPLICAS_HONGKONG:=2}"
: "${AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_NINGBO:=1}"
: "${AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_HONGKONG:=1}"
: "${AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MIN_REPLICAS_NINGBO:=$AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_NINGBO}"
: "${AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MIN_REPLICAS_HONGKONG:=$AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_HONGKONG}"
: "${AI_GATEWAY_ACCEPTANCE_SOAK_DURATION:=2h}"
: "${AI_GATEWAY_ACCEPTANCE_OVERLOAD_DURATION:=10m}"
: "${AI_GATEWAY_ACCEPTANCE_GATEWAY_TLS_SERVER_NAME:=}"
@@ -167,11 +171,20 @@ fi
echo 'etcd latency gates must be positive integer milliseconds' >&2
exit 1
}
[[ $AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MAX_REPLICAS_NINGBO =~ ^[1-9][0-9]*$ &&
$AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MAX_REPLICAS_HONGKONG =~ ^[1-9][0-9]*$ &&
[[ $AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_NINGBO =~ ^[0-9]+$ &&
$AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_HONGKONG =~ ^[0-9]+$ &&
$AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MIN_REPLICAS_NINGBO =~ ^[0-9]+$ &&
$AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MIN_REPLICAS_HONGKONG =~ ^[0-9]+$ &&
$AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MAX_REPLICAS_NINGBO =~ ^[0-9]+$ &&
$AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MAX_REPLICAS_HONGKONG =~ ^[0-9]+$ &&
$((AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_NINGBO + AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_HONGKONG)) -eq 2 &&
$AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MIN_REPLICAS_NINGBO -le $AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_NINGBO &&
$AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_NINGBO -le $AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MAX_REPLICAS_NINGBO &&
$AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MIN_REPLICAS_HONGKONG -le $AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_HONGKONG &&
$AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_HONGKONG -le $AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MAX_REPLICAS_HONGKONG &&
$AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MAX_REPLICAS_NINGBO -le 16 &&
$AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MAX_REPLICAS_HONGKONG -le 16 ]] || {
echo 'acceptance autoscaling replica caps must be between 1 and 16' >&2
echo 'acceptance requires exactly two baseline Workers and ordered 0..16 per-site autoscaling bounds' >&2
exit 1
}
@@ -263,8 +276,8 @@ image_stable_throughput=
image_admitted_throughput=
video_stable_throughput=
video_admitted_throughput=
certified_max_replicas_ningbo=1
certified_max_replicas_hongkong=1
certified_max_replicas_ningbo=$AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_NINGBO
certified_max_replicas_hongkong=$AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_HONGKONG
runtime_observation_started_at=
cleanup() {
@@ -636,21 +649,45 @@ verify_release_cas() {
-o 'jsonpath={.spec.template.spec.containers[0].image}') == "$api_image" ]]
done
[[ $(remote_kubectl get deployment easyai-worker-ningbo -n "$namespace" \
-o 'jsonpath={.spec.replicas}') == 1 ]]
-o 'jsonpath={.spec.replicas}') == "$AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_NINGBO" ]]
[[ $(remote_kubectl get deployment easyai-worker-hongkong -n "$namespace" \
-o 'jsonpath={.spec.replicas}') == 1 ]]
-o 'jsonpath={.spec.replicas}') == "$AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_HONGKONG" ]]
if (( AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_NINGBO == 0 &&
AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_HONGKONG == 2 )) &&
[[ -n $CLUSTER_SHENZHEN_HOST ]]; then
local worker_node pod_count
for worker_node in easyai-hongkong easyai-shenzhen; do
pod_count=$(remote_kubectl get pods -n "$namespace" \
-l 'app.kubernetes.io/name=easyai-worker,easyai.io/site=hongkong' \
--field-selector "spec.nodeName=$worker_node" -o json | jq \
'[.items[] | select(.status.phase=="Running")] | length')
[[ $pod_count == 1 ]] || {
echo "Hong Kong logical Worker pool is not spread across both physical nodes: node=$worker_node running_pods=$pod_count" >&2
return 1
}
done
fi
}
verify_resource_preconditions() {
local report=$temporary_root/resource-preconditions.csv
local ningbo_nodes los_angeles_nodes invalid_mixed_workloads witness_workloads
ningbo_nodes=$(remote_kubectl get nodes -l 'easyai.io/site=ningbo' \
-o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}')
local worker_nodes los_angeles_nodes invalid_mixed_workloads witness_workloads
worker_nodes=
if (( AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_NINGBO > 0 )); then
worker_nodes+=$(remote_kubectl get nodes -l 'easyai.io/site=ningbo' \
-o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}')
worker_nodes+=$'\n'
fi
if (( AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_HONGKONG > 0 )); then
worker_nodes+=$(remote_kubectl get nodes -l 'easyai.io/site=hongkong' \
-o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}')
fi
worker_nodes=$(printf '%s\n' "$worker_nodes" | sed '/^$/d' | LC_ALL=C sort -u)
los_angeles_nodes=$(remote_kubectl get nodes -l 'easyai.io/site=los-angeles' \
-o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}')
[[ -n $ningbo_nodes && -n $los_angeles_nodes ]]
[[ -n $worker_nodes && -n $los_angeles_nodes ]]
invalid_mixed_workloads=$(remote_kubectl get pods -A -o json |
jq --argjson nodes "$(jq -Rn --arg value "$ningbo_nodes" '$value | split("\n") | map(select(length>0))')" '
jq --argjson nodes "$(jq -Rn --arg value "$worker_nodes" '$value | split("\n") | map(select(length>0))')" '
[.items[]
| select(.spec.nodeName as $name | $nodes | index($name))
| select(.metadata.name | test("staging|runner|(^|-)ci(-|$)"; "i"))
@@ -661,7 +698,7 @@ verify_resource_preconditions() {
(.resources.limits.cpu // "") == ""))]
| length')
[[ $invalid_mixed_workloads == 0 ]] || {
echo 'Ningbo staging/CI workloads must have explicit CPU and memory requests/limits before acceptance' >&2
echo 'Worker-site staging/CI workloads must have explicit CPU and memory requests/limits before acceptance' >&2
return 1
}
witness_workloads=$(remote_kubectl get pods -A -o json |
@@ -687,12 +724,12 @@ verify_resource_preconditions() {
awk '{value=$5; sub(/%$/, "", value); print value}')
[[ $memory_percent =~ ^[0-9]+$ &&
$memory_percent -lt $AI_GATEWAY_ACCEPTANCE_NODE_MEMORY_PRECONDITION_PERCENT ]] || {
echo "Ningbo memory precondition failed: node=$node memory_percent=${memory_percent:-unknown} limit_percent=$AI_GATEWAY_ACCEPTANCE_NODE_MEMORY_PRECONDITION_PERCENT required_window_minutes=15" >&2
echo "Worker node memory precondition failed: node=$node memory_percent=${memory_percent:-unknown} limit_percent=$AI_GATEWAY_ACCEPTANCE_NODE_MEMORY_PRECONDITION_PERCENT required_window_minutes=15" >&2
return 1
}
printf '%s,%s,%s,%s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$node" \
"$memory_percent" "$AI_GATEWAY_ACCEPTANCE_NODE_MEMORY_PRECONDITION_PERCENT" >>"$report"
done <<<"$ningbo_nodes"
done <<<"$worker_nodes"
(( sample == 90 )) || sleep 10
done
cp "$report" "$report_root/resource-preconditions.csv"
@@ -1440,19 +1477,27 @@ fail_acceptance_gate() {
apply_capacity_profile() {
local profile=$1
local slots pool media global
local slots pool media global baseline_replicas
local api_pool=$AI_GATEWAY_ACCEPTANCE_API_DATABASE_MAX_CONNS
local min_idle=4
local max_idle_seconds=30
case $profile in
P24) slots=24; pool=32; media=24; global=48 ;;
P28) slots=28; pool=36; media=28; global=56 ;;
P32) slots=32; pool=40; media=32; global=64 ;;
P24) slots=24; pool=32; media=24 ;;
P28) slots=28; pool=36; media=28 ;;
P32) slots=32; pool=40; media=32 ;;
*) return 1 ;;
esac
baseline_replicas=$((AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_NINGBO + AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_HONGKONG))
global=$((slots * baseline_replicas))
local command_text capacity_output
printf -v command_text \
'AI_GATEWAY_WORKER_REPLICAS_NINGBO=1 AI_GATEWAY_WORKER_REPLICAS_HONGKONG=1 AI_GATEWAY_WORKER_MIN_REPLICAS_NINGBO=1 AI_GATEWAY_WORKER_MIN_REPLICAS_HONGKONG=1 AI_GATEWAY_WORKER_MAX_REPLICAS_NINGBO=1 AI_GATEWAY_WORKER_MAX_REPLICAS_HONGKONG=1 AI_GATEWAY_WORKER_AUTOSCALING_ENABLED=false AI_GATEWAY_ASYNC_WORKER_INSTANCE_HARD_LIMIT=%q AI_GATEWAY_ASYNC_WORKER_HARD_LIMIT=%q AI_GATEWAY_ASYNC_WORKER_GLOBAL_HARD_LIMIT=%q AI_GATEWAY_DATABASE_MAX_CONNS=%q AI_GATEWAY_API_DATABASE_MAX_CONNS=%q AI_GATEWAY_DATABASE_CRITICAL_MAX_CONNS=4 AI_GATEWAY_DATABASE_RIVER_MAX_CONNS=%q AI_GATEWAY_API_DATABASE_RIVER_MAX_CONNS=%q AI_GATEWAY_DATABASE_MIN_IDLE_CONNS=%q AI_GATEWAY_DATABASE_MAX_CONN_IDLE_SECONDS=%q AI_GATEWAY_MEDIA_MATERIALIZATION_CONCURRENCY=%q AI_GATEWAY_MEDIA_REQUEST_CONCURRENCY=%q AI_GATEWAY_WORKER_TARGET_OUTSTANDING_PER_REPLICA=%q AI_GATEWAY_WORKER_MEMORY_REQUEST_MIB=%q AI_GATEWAY_WORKER_CPU_REQUEST_MILLICORES=%q %q capacity' \
'AI_GATEWAY_WORKER_REPLICAS_NINGBO=%q AI_GATEWAY_WORKER_REPLICAS_HONGKONG=%q AI_GATEWAY_WORKER_MIN_REPLICAS_NINGBO=%q AI_GATEWAY_WORKER_MIN_REPLICAS_HONGKONG=%q AI_GATEWAY_WORKER_MAX_REPLICAS_NINGBO=%q AI_GATEWAY_WORKER_MAX_REPLICAS_HONGKONG=%q AI_GATEWAY_WORKER_AUTOSCALING_ENABLED=false AI_GATEWAY_ASYNC_WORKER_INSTANCE_HARD_LIMIT=%q AI_GATEWAY_ASYNC_WORKER_HARD_LIMIT=%q AI_GATEWAY_ASYNC_WORKER_GLOBAL_HARD_LIMIT=%q AI_GATEWAY_DATABASE_MAX_CONNS=%q AI_GATEWAY_API_DATABASE_MAX_CONNS=%q AI_GATEWAY_DATABASE_CRITICAL_MAX_CONNS=4 AI_GATEWAY_DATABASE_RIVER_MAX_CONNS=%q AI_GATEWAY_API_DATABASE_RIVER_MAX_CONNS=%q AI_GATEWAY_DATABASE_MIN_IDLE_CONNS=%q AI_GATEWAY_DATABASE_MAX_CONN_IDLE_SECONDS=%q AI_GATEWAY_MEDIA_MATERIALIZATION_CONCURRENCY=%q AI_GATEWAY_MEDIA_REQUEST_CONCURRENCY=%q AI_GATEWAY_WORKER_TARGET_OUTSTANDING_PER_REPLICA=%q AI_GATEWAY_WORKER_MEMORY_REQUEST_MIB=%q AI_GATEWAY_WORKER_CPU_REQUEST_MILLICORES=%q %q capacity' \
"$AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_NINGBO" \
"$AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_HONGKONG" \
"$AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_NINGBO" \
"$AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_HONGKONG" \
"$AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_NINGBO" \
"$AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_HONGKONG" \
"$slots" "$global" "$global" "$pool" "$api_pool" \
"$AI_GATEWAY_ACCEPTANCE_WORKER_RIVER_MAX_CONNS" "$AI_GATEWAY_ACCEPTANCE_API_RIVER_MAX_CONNS" \
"$min_idle" "$max_idle_seconds" \
@@ -1605,8 +1650,9 @@ apply_autoscaling_profile() {
local max_replicas_ningbo=${2:-$AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MAX_REPLICAS_NINGBO}
local max_replicas_hongkong=${3:-$AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MAX_REPLICAS_HONGKONG}
local slots pool media global
[[ $max_replicas_ningbo =~ ^[1-9][0-9]*$ &&
$max_replicas_hongkong =~ ^[1-9][0-9]*$ ]] || return 1
[[ $max_replicas_ningbo =~ ^[0-9]+$ &&
$max_replicas_hongkong =~ ^[0-9]+$ &&
$((max_replicas_ningbo + max_replicas_hongkong)) -ge 1 ]] || return 1
(( max_replicas_ningbo <= AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MAX_REPLICAS_NINGBO &&
max_replicas_hongkong <= AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MAX_REPLICAS_HONGKONG )) || return 1
case $profile in
@@ -1619,7 +1665,11 @@ apply_autoscaling_profile() {
global=$((slots * (max_replicas_ningbo + max_replicas_hongkong)))
local command_text capacity_output
printf -v command_text \
'AI_GATEWAY_WORKER_REPLICAS_NINGBO=1 AI_GATEWAY_WORKER_REPLICAS_HONGKONG=1 AI_GATEWAY_WORKER_MIN_REPLICAS_NINGBO=1 AI_GATEWAY_WORKER_MIN_REPLICAS_HONGKONG=1 AI_GATEWAY_WORKER_MAX_REPLICAS_NINGBO=%q AI_GATEWAY_WORKER_MAX_REPLICAS_HONGKONG=%q AI_GATEWAY_WORKER_AUTOSCALING_ENABLED=true AI_GATEWAY_WORKER_SCALE_UP_WINDOW_SECONDS=20 AI_GATEWAY_WORKER_SCALE_DOWN_STABILIZATION_SECONDS=600 AI_GATEWAY_WORKER_DRAIN_TIMEOUT_SECONDS=600 AI_GATEWAY_ASYNC_WORKER_INSTANCE_HARD_LIMIT=%q AI_GATEWAY_ASYNC_WORKER_HARD_LIMIT=%q AI_GATEWAY_ASYNC_WORKER_GLOBAL_HARD_LIMIT=%q AI_GATEWAY_DATABASE_MAX_CONNS=%q AI_GATEWAY_API_DATABASE_MAX_CONNS=%q AI_GATEWAY_DATABASE_CRITICAL_MAX_CONNS=4 AI_GATEWAY_DATABASE_RIVER_MAX_CONNS=%q AI_GATEWAY_API_DATABASE_RIVER_MAX_CONNS=%q AI_GATEWAY_DATABASE_MIN_IDLE_CONNS=4 AI_GATEWAY_DATABASE_MAX_CONN_IDLE_SECONDS=30 AI_GATEWAY_MEDIA_MATERIALIZATION_CONCURRENCY=%q AI_GATEWAY_MEDIA_REQUEST_CONCURRENCY=%q AI_GATEWAY_WORKER_TARGET_OUTSTANDING_PER_REPLICA=%q AI_GATEWAY_WORKER_MEMORY_REQUEST_MIB=%q AI_GATEWAY_WORKER_CPU_REQUEST_MILLICORES=%q %q capacity' \
'AI_GATEWAY_WORKER_REPLICAS_NINGBO=%q AI_GATEWAY_WORKER_REPLICAS_HONGKONG=%q AI_GATEWAY_WORKER_MIN_REPLICAS_NINGBO=%q AI_GATEWAY_WORKER_MIN_REPLICAS_HONGKONG=%q AI_GATEWAY_WORKER_MAX_REPLICAS_NINGBO=%q AI_GATEWAY_WORKER_MAX_REPLICAS_HONGKONG=%q AI_GATEWAY_WORKER_AUTOSCALING_ENABLED=true AI_GATEWAY_WORKER_SCALE_UP_WINDOW_SECONDS=20 AI_GATEWAY_WORKER_SCALE_DOWN_STABILIZATION_SECONDS=600 AI_GATEWAY_WORKER_DRAIN_TIMEOUT_SECONDS=600 AI_GATEWAY_ASYNC_WORKER_INSTANCE_HARD_LIMIT=%q AI_GATEWAY_ASYNC_WORKER_HARD_LIMIT=%q AI_GATEWAY_ASYNC_WORKER_GLOBAL_HARD_LIMIT=%q AI_GATEWAY_DATABASE_MAX_CONNS=%q AI_GATEWAY_API_DATABASE_MAX_CONNS=%q AI_GATEWAY_DATABASE_CRITICAL_MAX_CONNS=4 AI_GATEWAY_DATABASE_RIVER_MAX_CONNS=%q AI_GATEWAY_API_DATABASE_RIVER_MAX_CONNS=%q AI_GATEWAY_DATABASE_MIN_IDLE_CONNS=4 AI_GATEWAY_DATABASE_MAX_CONN_IDLE_SECONDS=30 AI_GATEWAY_MEDIA_MATERIALIZATION_CONCURRENCY=%q AI_GATEWAY_MEDIA_REQUEST_CONCURRENCY=%q AI_GATEWAY_WORKER_TARGET_OUTSTANDING_PER_REPLICA=%q AI_GATEWAY_WORKER_MEMORY_REQUEST_MIB=%q AI_GATEWAY_WORKER_CPU_REQUEST_MILLICORES=%q %q capacity' \
"$AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MIN_REPLICAS_NINGBO" \
"$AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MIN_REPLICAS_HONGKONG" \
"$AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MIN_REPLICAS_NINGBO" \
"$AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MIN_REPLICAS_HONGKONG" \
"$max_replicas_ningbo" \
"$max_replicas_hongkong" \
"$slots" "$global" "$global" "$pool" \
@@ -1697,14 +1747,15 @@ run_autoscaling_topology_phase() {
"$(jq -r '.plan.rawDesired // 0' <<<"${status:-{}}")" \
"$(jq -r '.plan.desiredTotal // 0' <<<"${status:-{}}")" \
"$(jq -r '.plan.frozenReason // ""' <<<"${status:-{}}")" "$resource_max" >>"$observations"
if (( ningbo_replicas == 1 && hongkong_replicas == 1 )); then
if (( ningbo_replicas == AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MIN_REPLICAS_NINGBO &&
hongkong_replicas == AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MIN_REPLICAS_HONGKONG )); then
drained=true
break
fi
sleep 10
done
[[ $drained == true ]] || {
echo "capacity controller did not safely drain $phase to topology 1+1" >&2
echo "capacity controller did not safely drain $phase to configured minimum topology" >&2
return 1
}
local unsafe_residue
@@ -1734,15 +1785,18 @@ run_autoscaling_acceptance() {
local report=$report_root/autoscaling.json
local observations=$report_root/autoscaling-observations.csv
local status deadline resource_max_hongkong resource_max_ningbo
local tested_1_plus_2=false tested_2_plus_2=false
local tested_south_pool_scale_out=false tested_1_plus_2=false tested_2_plus_2=false
local requested_hongkong_max=$AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MAX_REPLICAS_HONGKONG
local requested_ningbo_max=$AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MAX_REPLICAS_NINGBO
(( requested_hongkong_max > 2 )) && requested_hongkong_max=2
(( requested_ningbo_max > 2 )) && requested_ningbo_max=2
autoscaling_phase_observed_max=2
autoscaling_phase_observed_max=$((
AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MIN_REPLICAS_NINGBO +
AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MIN_REPLICAS_HONGKONG
))
echo 'timestamp,phase,ningbo_replicas,hongkong_replicas,raw_desired,desired_total,frozen_reason,resource_max_total' >"$observations"
apply_autoscaling_profile "$stable_profile" 1 "$requested_hongkong_max"
apply_autoscaling_profile "$stable_profile" "$requested_ningbo_max" "$requested_hongkong_max"
deadline=$((SECONDS + 180))
while (( SECONDS < deadline )); do
status=$(capacity_controller_status || true)
@@ -1755,7 +1809,17 @@ run_autoscaling_acceptance() {
}
resource_max_hongkong=$(jq -r \
'[.plan.sites[] | select(.site == "hongkong") | .resourceMax][0] // 0' <<<"$status")
if (( requested_hongkong_max >= 2 && resource_max_hongkong >= 2 )); then
if (( AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_NINGBO == 0 &&
AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MIN_REPLICAS_HONGKONG < AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_HONGKONG &&
requested_hongkong_max >= AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_HONGKONG &&
resource_max_hongkong >= AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_HONGKONG )); then
run_autoscaling_topology_phase south-pool-scale-out \
"$AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_NINGBO" \
"$AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_HONGKONG" "$observations"
tested_south_pool_scale_out=true
certified_max_replicas_ningbo=0
certified_max_replicas_hongkong=$AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_HONGKONG
elif (( requested_hongkong_max >= 2 && resource_max_hongkong >= 2 )); then
run_autoscaling_topology_phase 1-plus-2 1 2 "$observations"
tested_1_plus_2=true
certified_max_replicas_hongkong=2
@@ -1785,6 +1849,9 @@ run_autoscaling_acceptance() {
jq -n \
--arg profile "$stable_profile" \
--argjson baselineNingbo "$AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_NINGBO" \
--argjson baselineHongkong "$AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_HONGKONG" \
--argjson testedSouthPoolScaleOut "$tested_south_pool_scale_out" \
--argjson testedOnePlusTwo "$tested_1_plus_2" \
--argjson testedTwoPlusTwo "$tested_2_plus_2" \
--argjson observedMaxReplicas "$autoscaling_phase_observed_max" \
@@ -1792,8 +1859,10 @@ run_autoscaling_acceptance() {
passed:true,
drained:true,
capacityProfile:$profile,
baseline:{ningbo:$baselineNingbo,hongkong:$baselineHongkong},
topologies:{
"1+1":true,
"1+1":($baselineNingbo == 1 and $baselineHongkong == 1),
"0+1_to_0+2":$testedSouthPoolScaleOut,
"1+2":$testedOnePlusTwo,
"2+2":$testedTwoPlusTwo
},
@@ -2002,11 +2071,17 @@ WHERE acceptance_run_id IS NULL
}
activate_certified_autoscaling() {
local deadline status resource_max
local deadline status resource_max test_min_ningbo test_min_hongkong
test_min_ningbo=$AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MIN_REPLICAS_NINGBO
test_min_hongkong=$AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MIN_REPLICAS_HONGKONG
AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MIN_REPLICAS_NINGBO=$AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_NINGBO
AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MIN_REPLICAS_HONGKONG=$AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_HONGKONG
apply_autoscaling_profile \
"$stable_profile" \
"$certified_max_replicas_ningbo" \
"$certified_max_replicas_hongkong"
AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MIN_REPLICAS_NINGBO=$test_min_ningbo
AI_GATEWAY_ACCEPTANCE_AUTOSCALING_MIN_REPLICAS_HONGKONG=$test_min_hongkong
deadline=$((SECONDS + 180))
while (( SECONDS < deadline )); do
status=$(capacity_controller_status || true)
@@ -2023,7 +2098,9 @@ activate_certified_autoscaling() {
'[.plan.sites[] | select(.site == "ningbo") | .resourceMax][0] // 0' <<<"$status")
certified_max_replicas_hongkong=$(jq -r \
'[.plan.sites[] | select(.site == "hongkong") | .resourceMax][0] // 0' <<<"$status")
(( certified_max_replicas_ningbo >= 1 && certified_max_replicas_hongkong >= 1 ))
(( certified_max_replicas_ningbo >= AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_NINGBO &&
certified_max_replicas_hongkong >= AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_HONGKONG &&
certified_max_replicas_ningbo + certified_max_replicas_hongkong >= 2 ))
jq -n \
--arg profile "$stable_profile" \
--argjson ningbo "$certified_max_replicas_ningbo" \
@@ -2035,9 +2112,9 @@ activate_certified_autoscaling() {
resourceMaxTotal:$total
}' >"$report_root/certified-site-capacity.json"
[[ $(remote_kubectl get deployment easyai-worker-ningbo -n "$namespace" \
-o 'jsonpath={.spec.replicas}') == 1 ]]
-o 'jsonpath={.spec.replicas}') == "$AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_NINGBO" ]]
[[ $(remote_kubectl get deployment easyai-worker-hongkong -n "$namespace" \
-o 'jsonpath={.spec.replicas}') == 1 ]]
-o 'jsonpath={.spec.replicas}') == "$AI_GATEWAY_ACCEPTANCE_BASE_REPLICAS_HONGKONG" ]]
stage_capacity_profiles
verify_runtime_gates
}
@@ -2332,6 +2409,14 @@ verify_cluster_links() {
verify_wireguard "$CLUSTER_LOS_ANGELES_HOST" 10.77.0.1 300 'los_angeles_to_ningbo'
verify_wireguard "$CLUSTER_HONGKONG_HOST" 10.77.0.3 300 'hongkong_to_los_angeles'
verify_wireguard "$CLUSTER_LOS_ANGELES_HOST" 10.77.0.2 300 'los_angeles_to_hongkong'
if [[ -n $CLUSTER_SHENZHEN_HOST ]]; then
verify_wireguard "$CLUSTER_NINGBO_HOST" 10.77.0.4 80 'ningbo_to_shenzhen'
verify_wireguard "$CLUSTER_SHENZHEN_HOST" 10.77.0.1 80 'shenzhen_to_ningbo'
verify_wireguard "$CLUSTER_HONGKONG_HOST" 10.77.0.4 80 'hongkong_to_shenzhen'
verify_wireguard "$CLUSTER_SHENZHEN_HOST" 10.77.0.2 80 'shenzhen_to_hongkong'
verify_wireguard "$CLUSTER_LOS_ANGELES_HOST" 10.77.0.4 300 'los_angeles_to_shenzhen'
verify_wireguard "$CLUSTER_SHENZHEN_HOST" 10.77.0.3 300 'shenzhen_to_los_angeles'
fi
}
verify_control_plane_and_recent_logs() {
@@ -2349,6 +2434,11 @@ verify_control_plane_and_recent_logs() {
[[ $error_count == 0 ]] || return 1
fi
done
if [[ -n $CLUSTER_SHENZHEN_HOST && -n $runtime_observation_started_at ]]; then
error_count=$(cluster_ssh "$CLUSTER_SHENZHEN_HOST" \
"journalctl -u k3s-agent --since '$runtime_observation_started_at' --no-pager 2>/dev/null | awk 'BEGIN {IGNORECASE=1} /connection refused/ || /i\/o timeout/ || /node.*not ready/ || /failed to sync/ {count++} END {print count+0}'")
[[ $error_count == 0 ]] || return 1
fi
for workload in api worker; do
while read -r pod; do
error_count=$(remote_kubectl logs "$pod" -n "$namespace" "$log_window" |
+38 -10
View File
@@ -15,14 +15,20 @@ load_cluster_env
hosts=("$CLUSTER_NINGBO_HOST" "$CLUSTER_HONGKONG_HOST" "$CLUSTER_LOS_ANGELES_HOST")
wireguard_ips=(10.77.0.1 10.77.0.2 10.77.0.3)
site_labels=(ningbo hongkong losangeles)
for source_index in 0 1 2; do
if [[ -n $CLUSTER_SHENZHEN_HOST ]]; then
hosts+=("$CLUSTER_SHENZHEN_HOST")
wireguard_ips+=(10.77.0.4)
site_labels+=(shenzhen)
fi
peer_count=$((${#hosts[@]} - 1))
for source_index in "${!hosts[@]}"; do
handshakes=$(cluster_ssh "${hosts[$source_index]}" \
"wg show wg0 latest-handshakes | awk '\$2 > 0 && systime() - \$2 < 180 { count++ } END { print count + 0 }'")
[[ $handshakes -eq 2 ]] || {
[[ $handshakes -eq $peer_count ]] || {
echo "stale WireGuard handshake on ${site_labels[$source_index]}: recent_peers=$handshakes" >&2
exit 1
}
for target_index in 0 1 2; do
for target_index in "${!hosts[@]}"; do
[[ $source_index -eq $target_index ]] && continue
ping_output=$(cluster_ssh "${hosts[$source_index]}" \
"ping -i 0.2 -c 10 -W 2 ${wireguard_ips[$target_index]}")
@@ -46,12 +52,15 @@ for source_index in 0 1 2; do
fi
done
done
echo 'wireguard_acceptance=PASS handshakes=6 packet_loss_lt_1=true'
echo "wireguard_acceptance=PASS peers=${#hosts[@]} handshakes=$((${#hosts[@]} * peer_count)) packet_loss_lt_1=true"
cluster_state=$(cluster_ssh "$CLUSTER_NINGBO_HOST" 'bash -s' <<'REMOTE'
expected_nodes=3
[[ -z $CLUSTER_SHENZHEN_HOST ]] || expected_nodes=4
cluster_state=$(cluster_ssh "$CLUSTER_NINGBO_HOST" bash -s -- "$expected_nodes" <<'REMOTE'
set -euo pipefail
expected_nodes=$1
kubectl='k3s kubectl'
[[ $($kubectl get nodes --no-headers | awk '$2 == "Ready" { count++ } END { print count + 0 }') -eq 3 ]]
[[ $($kubectl get nodes --no-headers | awk '$2 == "Ready" { count++ } END { print count + 0 }') -eq $expected_nodes ]]
ready_output=$($kubectl get --raw='/readyz?verbose')
grep -Fq '[+]etcd ok' <<<"$ready_output"
[[ $($kubectl get node easyai-los-angeles -o json |
@@ -62,8 +71,24 @@ grep -Fq '[+]etcd ok' <<<"$ready_output"
printf 'nodes=%s\n' "$($kubectl get nodes --no-headers | wc -l)"
REMOTE
)
[[ $cluster_state == nodes=3 ]]
echo 'k3s_control_plane=PASS servers=3 witness_business_pods=0'
[[ $cluster_state == nodes="$expected_nodes" ]]
if [[ -n $CLUSTER_SHENZHEN_HOST ]]; then
worker_state=$(cluster_ssh "$CLUSTER_NINGBO_HOST" 'bash -s' <<'REMOTE'
set -euo pipefail
kubectl='k3s kubectl'
node=easyai-shenzhen
[[ $($kubectl get node "$node" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}') == True ]]
[[ $($kubectl get node "$node" -o jsonpath='{.metadata.labels.easyai\.io/site}') == hongkong ]]
[[ $($kubectl get node "$node" -o jsonpath='{.metadata.labels.easyai\.io/database}') == false ]]
[[ $($kubectl get node "$node" -o json | jq '[.spec.taints[]? | select(.key=="easyai.io/worker-only" and .effect=="NoSchedule")] | length') -eq 1 ]]
[[ $($kubectl get pods -A --field-selector "spec.nodeName=$node" -o json |
jq '[.items[] | select(.metadata.namespace!="kube-system" and .metadata.labels["app.kubernetes.io/name"]!="easyai-worker")] | length') -eq 0 ]]
printf 'worker_node=ready\n'
REMOTE
)
[[ $worker_state == worker_node=ready ]]
fi
echo "k3s_control_plane=PASS servers=3 worker_nodes=$((expected_nodes - 3)) witness_business_pods=0"
database_state=$(cluster_ssh "$CLUSTER_NINGBO_HOST" 'bash -s' <<'REMOTE'
set -euo pipefail
@@ -143,8 +168,11 @@ kubectl='k3s kubectl'
for site in ningbo hongkong; do
[[ $($kubectl get deployment "easyai-api-$site" -n easyai \
-o jsonpath='{.status.readyReplicas}') -eq 1 ]]
[[ $($kubectl get deployment "easyai-worker-$site" -n easyai \
-o jsonpath='{.status.readyReplicas}') -eq 1 ]]
worker_replicas=$($kubectl get deployment "easyai-worker-$site" -n easyai \
-o json | jq -r '.spec.replicas')
worker_ready=$($kubectl get deployment "easyai-worker-$site" -n easyai \
-o json | jq -r '.status.readyReplicas // 0')
[[ $worker_ready -eq $worker_replicas ]]
[[ $($kubectl get deployment "easyai-api-$site" -n easyai \
-o jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="AI_GATEWAY_PROCESS_ROLE")].value}') == api ]]
[[ $($kubectl get deployment "easyai-worker-$site" -n easyai \