负载均衡重构: 子网感知选择 + 节点健康监控 + 节点注册上报API

- 新增 subnet-aware 负载策略: 根据客户端IP匹配最近/16子网节点
- 节点注册端点 (POST /api/nodes/register): 边缘节点自动注册
- 节点心跳端点 (POST /api/nodes/heartbeat): 上报CPU/RAM/带宽/连接数
- 健康汇总 (GET /api/nodes/health): 服务端汇总所有节点状态
- 配置节点新增subnet/is_origin字段
- 前端NodeManager: 实时健康状态/CPU/RAM进度条/带宽/连接数
- X-Forwarded-For/X-Real-IP透传支持客户端IP
This commit is contained in:
admin committed 2026-08-10 14:45:28 +08:00
1 parent 5f19fc6aa5
commit 980b91f41f
7 files changed
+319 -87

No files matched your search

+12 -5
View File
@@ -12,13 +12,20 @@ class Settings(BaseSettings):
media_origin_hls_port: int = 8888 media_origin_hls_port: int = 8888
edge_nodes: list[dict] = [ edge_nodes: list[dict] = [
{"id": "edge-1", "name": "Edge Node 1", "host": "127.0.0.1", {
"hls_port": 8889, "api_port": 9998, "weight": 100, "enabled": True}, "id": "default",
{"id": "edge-2", "name": "Edge Node 2", "host": "127.0.0.1", "name": "Origin (Default)",
"hls_port": 8890, "api_port": 9999, "weight": 50, "enabled": True}, "host": "127.0.0.1",
"hls_port": 8888,
"api_port": 9997,
"weight": 100,
"enabled": True,
"subnet": "0.0.0.0/0",
"is_origin": True,
},
] ]
load_balance_strategy: Literal["round-robin", "weighted", "least-conn"] = "weighted" load_balance_strategy: Literal["round-robin", "weighted", "least-conn", "subnet"] = "subnet"
qualities: list[dict] = [ qualities: list[dict] = [
{"label": "1080p", "width": 1920, "height": 1080, "bitrate": "5000k"}, {"label": "1080p", "width": 1920, "height": 1080, "bitrate": "5000k"},
+74 -2
View File
@@ -1,5 +1,6 @@
from fastapi import APIRouter, HTTPException, Depends from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional
from config import settings from config import settings
from routes.auth import require_auth from routes.auth import require_auth
from services.load_balancer import lb from services.load_balancer import lb
@@ -11,6 +12,7 @@ router = APIRouter(prefix="/api/nodes", tags=["nodes"])
class NodeUpdate(BaseModel): class NodeUpdate(BaseModel):
weight: int weight: int
enabled: bool | None = None enabled: bool | None = None
subnet: str | None = None
class StrategyUpdate(BaseModel): class StrategyUpdate(BaseModel):
@@ -18,18 +20,42 @@ class StrategyUpdate(BaseModel):
multicast_enabled: bool | None = None multicast_enabled: bool | None = None
class NodeHeartbeat(BaseModel):
node_id: str
cpu_percent: float | None = None
memory_percent: float | None = None
bandwidth_mbps: float | None = None
active_connections: int | None = None
hls_readers: int | None = None
class NodeRegister(BaseModel):
node_id: str
name: str
host: str
hls_port: int
api_port: int
weight: int = 100
subnet: str = "0.0.0.0/0"
@router.get("") @router.get("")
async def list_nodes(_: bool = Depends(require_auth)): async def list_nodes(_: bool = Depends(require_auth)):
nodes = [] nodes = []
for n in settings.edge_nodes: for n in settings.edge_nodes:
nid = n["id"]
nodes.append({ nodes.append({
"id": n["id"], "id": nid,
"name": n["name"], "name": n["name"],
"host": n["host"], "host": n["host"],
"hls_port": n["hls_port"], "hls_port": n["hls_port"],
"api_port": n["api_port"], "api_port": n["api_port"],
"weight": n["weight"], "weight": n["weight"],
"enabled": n.get("enabled", True), "enabled": n.get("enabled", True),
"subnet": n.get("subnet", "0.0.0.0/0"),
"is_origin": n.get("is_origin", False),
"online": lb.is_online(nid),
"metrics": lb._node_health.get(nid),
}) })
return nodes return nodes
@@ -45,7 +71,7 @@ async def get_strategy(_: bool = Depends(require_auth)):
@router.put("/strategy") @router.put("/strategy")
async def update_strategy(body: StrategyUpdate, _: bool = Depends(require_auth)): async def update_strategy(body: StrategyUpdate, _: bool = Depends(require_auth)):
if body.strategy in ("round-robin", "weighted", "least-conn"): if body.strategy in ("round-robin", "weighted", "least-conn", "subnet"):
settings.load_balance_strategy = body.strategy settings.load_balance_strategy = body.strategy
if body.multicast_enabled is not None: if body.multicast_enabled is not None:
settings.multicast_enabled = body.multicast_enabled settings.multicast_enabled = body.multicast_enabled
@@ -60,8 +86,54 @@ async def update_node(node_id: str, body: NodeUpdate, _: bool = Depends(require_
n["weight"] = body.weight n["weight"] = body.weight
if body.enabled is not None: if body.enabled is not None:
n["enabled"] = body.enabled n["enabled"] = body.enabled
if body.subnet is not None:
n["subnet"] = body.subnet
found = True found = True
break break
if not found: if not found:
raise HTTPException(status_code=404, detail="Node not found") raise HTTPException(status_code=404, detail="Node not found")
return {"ok": True} return {"ok": True}
@router.post("/register")
async def register_node(body: NodeRegister):
for n in settings.edge_nodes:
if n["id"] == body.node_id:
n.update({
"name": body.name,
"host": body.host,
"hls_port": body.hls_port,
"api_port": body.api_port,
"weight": body.weight,
"subnet": body.subnet,
})
return {"ok": True, "action": "updated"}
settings.edge_nodes.append({
"id": body.node_id,
"name": body.name,
"host": body.host,
"hls_port": body.hls_port,
"api_port": body.api_port,
"weight": body.weight,
"enabled": True,
"subnet": body.subnet,
"is_origin": False,
})
return {"ok": True, "action": "registered"}
@router.post("/heartbeat")
async def node_heartbeat(body: NodeHeartbeat):
lb.report_health(body.node_id, {
"cpu_percent": body.cpu_percent,
"memory_percent": body.memory_percent,
"bandwidth_mbps": body.bandwidth_mbps,
"active_connections": body.active_connections,
"hls_readers": body.hls_readers,
})
return {"ok": True}
@router.get("/health")
async def get_all_health():
return lb.get_health_summary()
+20 -5
View File
@@ -1,4 +1,4 @@
from fastapi import APIRouter, HTTPException, Depends, Query from fastapi import APIRouter, HTTPException, Depends, Query, Request
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional from typing import Optional
@@ -26,6 +26,18 @@ class StreamUpdate(BaseModel):
scheduled_at: Optional[str] = None scheduled_at: Optional[str] = None
def _get_client_ip(request: Request) -> str | None:
forwarded = request.headers.get("X-Forwarded-For")
if forwarded:
return forwarded.split(",")[0].strip()
client = request.headers.get("X-Real-IP")
if client:
return client
if request.client:
return request.client.host
return None
@router.get("") @router.get("")
async def list_streams(db: AsyncSession = Depends(get_db)): async def list_streams(db: AsyncSession = Depends(get_db)):
svc = StreamService(db) svc = StreamService(db)
@@ -114,17 +126,19 @@ async def delete_stream(stream_id: int, db: AsyncSession = Depends(get_db),
@router.get("/{stream_id}/watch") @router.get("/{stream_id}/watch")
async def get_watch_urls(stream_id: int, db: AsyncSession = Depends(get_db)): async def get_watch_urls(stream_id: int, request: Request, db: AsyncSession = Depends(get_db)):
svc = StreamService(db) svc = StreamService(db)
stream = await svc.get_stream(stream_id) stream = await svc.get_stream(stream_id)
if not stream: if not stream:
raise HTTPException(status_code=404, detail="Stream not found") raise HTTPException(status_code=404, detail="Stream not found")
urls = await svc.get_watch_urls(stream.stream_key) client_ip = _get_client_ip(request)
urls = await svc.get_watch_urls(stream.stream_key, client_ip)
return { return {
"stream_id": stream.id, "stream_id": stream.id,
"title": stream.title, "title": stream.title,
"status": stream.status.value, "status": stream.status.value,
"multicast_available": urls["multicast_available"], "multicast_available": urls["multicast_available"],
"selected_node": urls.get("selected_node"),
"lines": urls["lines"], "lines": urls["lines"],
"qualities": urls["qualities"], "qualities": urls["qualities"],
} }
@@ -153,12 +167,13 @@ async def stream_end(stream_id: int, db: AsyncSession = Depends(get_db),
@router.get("/detail/{stream_key}") @router.get("/detail/{stream_key}")
async def get_stream_by_key(stream_key: str, db: AsyncSession = Depends(get_db)): async def get_stream_by_key(stream_key: str, request: Request, db: AsyncSession = Depends(get_db)):
svc = StreamService(db) svc = StreamService(db)
s = await svc.get_stream_by_key(stream_key) s = await svc.get_stream_by_key(stream_key)
if not s: if not s:
raise HTTPException(status_code=404, detail="Stream not found") raise HTTPException(status_code=404, detail="Stream not found")
urls = await svc.get_watch_urls(stream_key) client_ip = _get_client_ip(request)
urls = await svc.get_watch_urls(stream_key, client_ip)
return { return {
"stream_id": s.id, "stream_id": s.id,
"title": s.title, "title": s.title,
+122 -25
View File
@@ -1,43 +1,140 @@
import json import ipaddress
import asyncio import time
import subprocess
from config import settings from config import settings
class LoadBalancer: class LoadBalancer:
def __init__(self): def __init__(self):
self._rr_index = 0 self._rr_index = 0
self._conn_counts: dict[str, int] = {n["id"]: 0 for n in settings.edge_nodes} self._conn_counts: dict[str, int] = {}
self._node_health: dict[str, dict] = {}
def get_enabled_nodes(self): def get_enabled_nodes(self):
return [n for n in settings.edge_nodes if n.get("enabled", True)] return [n for n in settings.edge_nodes if n.get("enabled", True)]
def select_nodes(self, strategy: str | None = None): def get_node_by_id(self, node_id: str):
nodes = self.get_enabled_nodes() for n in settings.edge_nodes:
strategy = strategy or settings.load_balance_strategy if n["id"] == node_id:
return n
return None
def select_best_node(self, client_ip: str | None = None):
nodes = self.get_enabled_nodes()
if not nodes:
return None
if client_ip:
best = self._find_subnet_match(client_ip, nodes)
if best:
return best
strategy = settings.load_balance_strategy
if strategy == "round-robin": if strategy == "round-robin":
if not nodes: online = [n for n in nodes if self.is_online(n["id"])]
return [] if not online:
chosen = nodes[self._rr_index % len(nodes)] online = nodes
chosen = online[self._rr_index % len(online)]
self._rr_index += 1 self._rr_index += 1
return [chosen] return chosen
elif strategy == "weighted": elif strategy == "weighted":
total = sum(n["weight"] for n in nodes) return self._weighted_select(nodes)
if total == 0:
return nodes
import random
r = random.uniform(0, total)
cumulative = 0
for n in nodes:
cumulative += n["weight"]
if r <= cumulative:
return [n]
return [nodes[-1]]
elif strategy == "least-conn": elif strategy == "least-conn":
sorted_nodes = sorted(nodes, key=lambda n: self._conn_counts.get(n["id"], 0)) return self._least_conn_select(nodes)
return [sorted_nodes[0]] else:
return nodes return self._weighted_select(nodes)
def _find_subnet_match(self, client_ip: str, nodes: list[dict]):
try:
ip = ipaddress.ip_address(client_ip)
except ValueError:
return None
best = None
best_prefix = -1
for node in nodes:
if not self.is_online(node["id"]):
continue
subnet_str = node.get("subnet", "0.0.0.0/0")
if not subnet_str:
continue
try:
subnet = ipaddress.ip_network(subnet_str, strict=False)
except ValueError:
continue
if ip in subnet:
if subnet.prefixlen > best_prefix:
best_prefix = subnet.prefixlen
best = node
return best
def _weighted_select(self, nodes: list[dict]):
online = [n for n in nodes if self.is_online(n["id"])]
if not online:
online = nodes
total = sum(n["weight"] for n in online)
if total == 0:
return online[0] if online else None
import random
r = random.uniform(0, total)
cumulative = 0
for n in online:
cumulative += n["weight"]
if r <= cumulative:
return n
return online[-1]
def _least_conn_select(self, nodes: list[dict]):
online = [n for n in nodes if self.is_online(n["id"])]
if not online:
online = nodes
sorted_nodes = sorted(online, key=lambda n: self._conn_counts.get(n["id"], 0))
return sorted_nodes[0] if sorted_nodes else None
def report_health(self, node_id: str, metrics: dict):
self._node_health[node_id] = {
"last_seen": time.time(),
"online": True,
**metrics,
}
def is_online(self, node_id: str) -> bool:
h = self._node_health.get(node_id)
if h is None:
if any(n["id"] == node_id and n.get("is_origin") for n in settings.edge_nodes):
return True
return False
if time.time() - h.get("last_seen", 0) > 30:
return False
return h.get("online", False)
def get_health_summary(self):
result = {}
for node in settings.edge_nodes:
nid = node["id"]
h = self._node_health.get(nid)
result[nid] = {
"name": node["name"],
"host": node["host"],
"online": self.is_online(nid),
"weight": node["weight"],
"enabled": node.get("enabled", True),
"subnet": node.get("subnet"),
"is_origin": node.get("is_origin", False),
"last_seen": h.get("last_seen") if h else None,
"metrics": {
"cpu_percent": h.get("cpu_percent"),
"memory_percent": h.get("memory_percent"),
"bandwidth_mbps": h.get("bandwidth_mbps"),
"active_connections": h.get("active_connections"),
"hls_readers": h.get("hls_readers"),
} if h else None,
}
return result
def increment_connection(self, node_id: str): def increment_connection(self, node_id: str):
self._conn_counts[node_id] = self._conn_counts.get(node_id, 0) + 1 self._conn_counts[node_id] = self._conn_counts.get(node_id, 0) + 1
+12 -16
View File
@@ -69,12 +69,6 @@ class StreamService:
status=StreamStatus.live, status=StreamStatus.live,
started_at=datetime.utcnow(), started_at=datetime.utcnow(),
) )
assigned = [n["id"] for n in lb.get_enabled_nodes()]
await self.update_stream(
stream.id,
assigned_nodes=json.dumps(assigned),
multicast_enabled=multicast_detector.is_available(),
)
rtmp_url = media_service.get_rtmp_url(stream_key) rtmp_url = media_service.get_rtmp_url(stream_key)
transcode_mgr.start_transcode(stream_key, rtmp_url) transcode_mgr.start_transcode(stream_key, rtmp_url)
@@ -89,20 +83,22 @@ class StreamService:
) )
transcode_mgr.stop_transcode(stream_key) transcode_mgr.stop_transcode(stream_key)
async def get_watch_urls(self, stream_key: str): async def get_watch_urls(self, stream_key: str, client_ip: str | None = None):
nodes = lb.get_enabled_nodes() best_node = lb.select_best_node(client_ip)
if not best_node:
best_node = lb.get_enabled_nodes()[0] if lb.get_enabled_nodes() else None
lines = [] lines = []
for node in nodes: if best_node:
base_hls = f"/hls/{stream_key}"
line_info = { line_info = {
"node_id": node["id"], "node_id": best_node["id"],
"node_name": node["name"], "node_name": best_node["name"],
"weight": node["weight"], "weight": best_node["weight"],
"type": "hls",
"url": f"{base_hls}/index.m3u8",
"qualities": [], "qualities": [],
} }
base_hls = f"/hls/{stream_key}"
line_info["type"] = "hls"
line_info["url"] = f"{base_hls}/index.m3u8"
for quality in settings.qualities: for quality in settings.qualities:
label = quality["label"] label = quality["label"]
line_info["qualities"].append({ line_info["qualities"].append({
+77 -33
View File
@@ -1,12 +1,20 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { import {
Card, Table, InputNumber, Switch, Typography, message, Space, Tag, Statistic, Row, Col, Button, Card, Table, InputNumber, Switch, Typography, message, Space, Tag, Statistic, Row, Col, Button, Tooltip, Progress,
} from 'antd' } from 'antd'
import { CheckCircleOutlined, CloseCircleOutlined, ReloadOutlined } from '@ant-design/icons' import { CheckCircleOutlined, CloseCircleOutlined, ReloadOutlined, DashboardOutlined } from '@ant-design/icons'
import api from '../../api/client' import api from '../../api/client'
const { Title, Text } = Typography const { Title, Text } = Typography
interface NodeMetrics {
cpu_percent: number | null
memory_percent: number | null
bandwidth_mbps: number | null
active_connections: number | null
hls_readers: number | null
}
interface NodeInfo { interface NodeInfo {
id: string id: string
name: string name: string
@@ -15,6 +23,10 @@ interface NodeInfo {
api_port: number api_port: number
weight: number weight: number
enabled: boolean enabled: boolean
subnet: string
is_origin: boolean
online: boolean
metrics: NodeMetrics | null
} }
interface StrategyInfo { interface StrategyInfo {
@@ -24,6 +36,7 @@ interface StrategyInfo {
} }
const strategyLabels: Record<string, string> = { const strategyLabels: Record<string, string> = {
'subnet': 'Subnet-Aware',
'round-robin': 'Round-Robin', 'round-robin': 'Round-Robin',
'weighted': 'Weighted', 'weighted': 'Weighted',
'least-conn': 'Least-Conn', 'least-conn': 'Least-Conn',
@@ -34,7 +47,7 @@ export default function NodeManager() {
const [strategy, setStrategy] = useState<StrategyInfo | null>(null) const [strategy, setStrategy] = useState<StrategyInfo | null>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
useEffect(() => { loadData() }, []) useEffect(() => { loadData(); const t = setInterval(loadData, 10000); return () => clearInterval(t) }, [])
async function loadData() { async function loadData() {
setLoading(true) setLoading(true)
@@ -66,42 +79,72 @@ export default function NodeManager() {
} catch { message.error('Failed') } } catch { message.error('Failed') }
} }
const onlineNodes = nodes.filter((n) => n.online).length
const columns = [ const columns = [
{
title: 'Status', dataIndex: 'online', key: 'online', width: 80,
render: (online: boolean) => (
<Tooltip title={online ? 'Online' : 'Offline'}>
<Tag icon={online ? <CheckCircleOutlined /> : <CloseCircleOutlined />}
color={online ? 'success' : 'error'} />
</Tooltip>
),
},
{ title: 'Name', dataIndex: 'name', key: 'name' }, { title: 'Name', dataIndex: 'name', key: 'name' },
{ title: 'Host', dataIndex: 'host', key: 'host' }, { title: 'Host', dataIndex: 'host', key: 'host', width: 140 },
{ title: 'HLS Port', dataIndex: 'hls_port', key: 'hls_port', width: 100 }, {
{ title: 'API Port', dataIndex: 'api_port', key: 'api_port', width: 100 }, title: 'Subnet', dataIndex: 'subnet', key: 'subnet', width: 140,
render: (v: string) => <Text code style={{ fontSize: 12 }}>{v}</Text>,
},
{
title: 'CPU', key: 'cpu', width: 80,
render: (_: any, r: NodeInfo) => r.metrics?.cpu_percent != null
? <Progress size="small" percent={Math.round(r.metrics.cpu_percent)} strokeColor="#1E3A5F" showInfo={false} />
: <Text type="secondary">--</Text>,
},
{
title: 'RAM', key: 'ram', width: 80,
render: (_: any, r: NodeInfo) => r.metrics?.memory_percent != null
? <Progress size="small" percent={Math.round(r.metrics.memory_percent)} strokeColor="#1E3A5F" showInfo={false} />
: <Text type="secondary">--</Text>,
},
{
title: 'BW', key: 'bw', width: 70,
render: (_: any, r: NodeInfo) => r.metrics?.bandwidth_mbps != null
? <Text style={{ fontSize: 12 }}>{r.metrics.bandwidth_mbps.toFixed(1)} Mbps</Text>
: <Text type="secondary">--</Text>,
},
{
title: 'Conn', dataIndex: 'connections', key: 'conn', width: 60,
render: (_: any, r: NodeInfo) => r.metrics?.active_connections != null
? r.metrics.active_connections
: <Text type="secondary">--</Text>,
},
{ {
title: 'Weight', dataIndex: 'weight', key: 'weight', width: 140, title: 'Weight', dataIndex: 'weight', key: 'weight', width: 140,
render: (_: number, record: NodeInfo) => ( render: (_: number, record: NodeInfo) => (
<InputNumber <InputNumber
min={0} min={0} max={1000} value={record.weight}
max={1000}
value={record.weight}
onChange={(val) => { onChange={(val) => {
const v = val || 0 const v = val || 0
setNodes((prev) => prev.map((n) => (n.id === record.id ? { ...n, weight: v } : n))) setNodes((prev) => prev.map((n) => (n.id === record.id ? { ...n, weight: v } : n)))
updateWeight(record.id, v) updateWeight(record.id, v)
}} }}
size="small" size="small" style={{ width: 80 }}
style={{ width: 90 }} disabled={record.is_origin}
/> />
), ),
}, },
{ {
title: 'Enabled', dataIndex: 'enabled', key: 'enabled', width: 80, title: 'Enabled', dataIndex: 'enabled', key: 'enabled', width: 70,
render: (_: any, record: NodeInfo) => ( render: (_: any, record: NodeInfo) => (
<Switch <Switch
checked={record.enabled} checked={record.enabled} onChange={(val) => toggleNode(record.id, val)}
onChange={(val) => toggleNode(record.id, val)} size="small" disabled={record.is_origin}
size="small"
/> />
), ),
}, },
{
title: 'Health', key: 'health', width: 100,
render: () => <Tag icon={<CheckCircleOutlined />} color="success">Online</Tag>,
},
] ]
return ( return (
@@ -110,26 +153,26 @@ export default function NodeManager() {
{strategy && ( {strategy && (
<Row gutter={[24, 24]} style={{ marginBottom: 24 }}> <Row gutter={[24, 24]} style={{ marginBottom: 24 }}>
<Col xs={24} sm={8}> <Col xs={24} sm={6}>
<Card style={{ borderRadius: 12 }}> <Card style={{ borderRadius: 12 }}>
<Statistic <Statistic title="Strategy" value={strategyLabels[strategy.strategy] || strategy.strategy} />
title="Strategy"
value={strategyLabels[strategy.strategy] || strategy.strategy}
/>
</Card> </Card>
</Col> </Col>
<Col xs={24} sm={8}> <Col xs={24} sm={6}>
<Card style={{ borderRadius: 12 }}> <Card style={{ borderRadius: 12 }}>
<Statistic <Statistic title="Multicast" value={strategy.multicast_available ? 'Available' : 'N/A'}
title="Multicast" valueStyle={{ color: strategy.multicast_available ? '#2E7D32' : '#999' }} />
value={strategy.multicast_available ? 'Available' : 'N/A'}
valueStyle={{ color: strategy.multicast_available ? '#2E7D32' : '#999' }}
/>
</Card> </Card>
</Col> </Col>
<Col xs={24} sm={8}> <Col xs={24} sm={6}>
<Card style={{ borderRadius: 12 }}> <Card style={{ borderRadius: 12 }}>
<Statistic title="Nodes" value={nodes.length} /> <Statistic title="Total Nodes" value={nodes.length} />
</Card>
</Col>
<Col xs={24} sm={6}>
<Card style={{ borderRadius: 12 }}>
<Statistic title="Online" value={onlineNodes}
valueStyle={{ color: onlineNodes > 0 ? '#2E7D32' : '#C62828' }} />
</Card> </Card>
</Col> </Col>
</Row> </Row>
@@ -146,7 +189,8 @@ export default function NodeManager() {
rowKey="id" rowKey="id"
loading={loading} loading={loading}
pagination={false} pagination={false}
size="middle" size="small"
scroll={{ x: 1100 }}
/> />
</Card> </Card>
</div> </div>
+2 -1
View File
@@ -7,8 +7,9 @@ import api from '../../api/client'
const { Title } = Typography const { Title } = Typography
const strategies = [ const strategies = [
{ value: 'round-robin', label: 'Round-Robin' }, { value: 'subnet', label: 'Subnet-Aware (Recommended)' },
{ value: 'weighted', label: 'Weighted' }, { value: 'weighted', label: 'Weighted' },
{ value: 'round-robin', label: 'Round-Robin' },
{ value: 'least-conn', label: 'Least Connections' }, { value: 'least-conn', label: 'Least Connections' },
] ]