frank-medium: fail over to the other replica instead of returning 502
The proxy trusted its health table, which the watcher refreshes every 10 s. A replica that restarts — and a deploy restarts both — therefore produced a window in which requests routed to the dead card got a 502 back, even though the other card was serving the whole time. A failed connection now demotes that replica immediately and retries the request on another live one, before anything is written to the client. vLLM's OpenAI endpoints are stateless, so re-sending a request that never reached an engine is safe; a failure after streaming has begun is still not retried. The failover is logged, because a silent one hides a card that is down. Measured with card 1 stopped mid-flight: six requests, six 200s, one failover line in the log, no client-visible error. Before this commit that burst returned 502s.
This commit is contained in:
@@ -80,19 +80,43 @@ async def proxy(request: web.Request) -> web.StreamResponse:
|
||||
return await health(session)
|
||||
|
||||
body = await request.read()
|
||||
idx = routing.route(body, request.remote, _UP)
|
||||
if idx < 0:
|
||||
return web.Response(status=503, text="proxy: no healthy frank-medium replica\n")
|
||||
target = UPSTREAMS[idx] + request.path_qs
|
||||
headers = {k: v for k, v in request.headers.items() if k.lower() not in HOP_BY_HOP}
|
||||
|
||||
try:
|
||||
upstream = await session.request(
|
||||
request.method, target, headers=headers, data=body or None,
|
||||
timeout=ClientTimeout(total=None, sock_connect=10, sock_read=3600),
|
||||
)
|
||||
except Exception as exc: # upstream refused or unreachable
|
||||
return web.Response(status=502, text=f"proxy: {target} unreachable: {exc}\n")
|
||||
# Fail over before anything is written to the client. A replica that
|
||||
# restarts is invisible to the health watcher for up to HEALTH_TTL, and a
|
||||
# deploy restarts both replicas — without this retry a 10 s window of
|
||||
# requests would 502 on a service that has a live card the whole time.
|
||||
# vLLM's OpenAI endpoints are stateless per request, so re-sending a
|
||||
# request that never reached an engine is safe; a request that failed
|
||||
# mid-stream is not retried (that path is below).
|
||||
tried: set[int] = set()
|
||||
upstream = None
|
||||
last_error = "no healthy frank-medium replica"
|
||||
while len(tried) < len(UPSTREAMS):
|
||||
idx = routing.route(body, request.remote, _UP)
|
||||
if idx < 0 or idx in tried:
|
||||
break
|
||||
tried.add(idx)
|
||||
target = UPSTREAMS[idx] + request.path_qs
|
||||
try:
|
||||
upstream = await session.request(
|
||||
request.method, target, headers=headers, data=body or None,
|
||||
timeout=ClientTimeout(total=None, sock_connect=10, sock_read=3600),
|
||||
)
|
||||
break
|
||||
except Exception as exc:
|
||||
# Demote at once: the watcher will confirm, but every request until
|
||||
# then should skip this replica rather than pay the same timeout.
|
||||
# Logged because a silent failover hides a card that is down.
|
||||
_UP[idx] = False
|
||||
last_error = f"{target} unreachable: {exc}"
|
||||
print(f"proxy: failover — {target} unreachable ({exc}); retrying "
|
||||
f"another replica", flush=True)
|
||||
if upstream is None:
|
||||
# 503 when no replica was even reachable through the routing table,
|
||||
# 502 when replicas were tried and all of them failed.
|
||||
status = 502 if tried else 503
|
||||
return web.Response(status=status, text=f"proxy: {last_error}\n")
|
||||
|
||||
out = web.StreamResponse(status=upstream.status, reason=upstream.reason)
|
||||
for k, v in upstream.headers.items():
|
||||
|
||||
Reference in New Issue
Block a user