Repository navigation
Conversation
A router-proxy replica can be running while the service mesh that captures its traffic has not set it up, so /health keeps it in the Service endpoints and requests sent to it fail. The repeatable --readiness-required-listener flag names ports that must have a LISTEN socket in the pod's network namespace for GET /readyz to answer 200; otherwise it answers 503 naming the missing ports. With no ports required /readyz always answers 200. Detection reads /proc/net/tcp and /proc/net/tcp6 because the image has no shell. It fails open: when a table cannot be read or has an unrecognised header, ports not found in the readable tables are not reported missing, and the proxy logs once per transition. A /proc problem must not take every replica out of service. An absent tcp6 is an IPv6-disabled kernel and counts as an empty table. Refs defilantech#2442 Signed-off-by: Christian Nuss <cnuss@users.noreply.github.com>
Lets a ModelRouter require local listeners, such as Istio ambient's HBONE port 15008, before a proxy replica is Ready. When set, the readiness probe moves to /readyz and each port is passed to the proxy as --readiness-required-listener. Liveness stays on /health so an unready replica is not restarted. Unset or empty renders the same Deployment as before, so an operator upgrade does not roll existing proxies; the routerproxy goldens are unchanged. The CRD bounds each port to 1-65535 and the list to 16 unique entries, so an invalid port cannot reach the proxy flag and crash-loop the pod. Refs defilantech#2442 Signed-off-by: Christian Nuss <cnuss@users.noreply.github.com>
Covers what the field gates, that liveness stays on /health, the fail-open behavior when /proc cannot be read, the image requirement when spec.proxy.image is pinned, and the posture of /readyz. Refs defilantech#2442 Signed-off-by: Christian Nuss <cnuss@users.noreply.github.com>
|
One question for you: when |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Reviewed against what #2442 settled, not just the PR body. All four items hold:
- Unset is byte-identical.
routerproxy-deployment.goldenandrouterproxy-deployment-proxy-options.goldenpinreadinessProbe.httpGet.path: /healthand are untouched, and the empty-list case is pinned against the nil-proxy case in the builder test. So an operator upgrade does not roll a router that did not opt in. - Both tables,
0Aonly. The fixtures cover LISTEN in tcp only, LISTEN in tcp6 only, and the established row on 15008 not counting. The established fixture carries the port on both sides, local and remote, which is the version I'd want. - Fail open on an unreadable table, logged once per transition, with recovery logged.
- CRD bounds 1 to 65535, 16 unique entries, both the kustomize base and the chart copy, deepcopy regenerated.
Verified rather than taken on trust: the header and column parsing is right for both tables, local_address at field 1 and st at field 3 on tcp and tcp6, port as the hex suffix after the last colon, so the 32 hex char tcp6 address parses. Absent tcp6 as an empty table is the correct call for ipv6.disable=1. Set and clear both reach the live Deployment because the update path copies the desired Template.Spec. Router-proxy pods are never hostNetwork in this repo, only multinode.go sets it and that is InferenceService, so the read really is the pod's own netns.
Three things I'd like, none of them blocking:
readiness.gologs the read failure and then, in the same call,required listeners present; reporting ready, because the missing set is forced empty before the transition key is computed. Say "not enforced" rather than "present" for that case./readyzrescans both tables per request under a mutex on the unauthenticated inference port. Cost scales with the socket count, and this proxy holds a lot of them to model pods. A short TTL cache, a second or so, bounds what a caller can make us do.- Docs: fail-open covers unreadable tables only. A port that never listens in this container's netns is not fail-open, it is every replica unready at once. One clause saying that is worth it, since the whole page leads with a single port number a reader will copy.
One question, and it decides how useful the Istio example is for other users. In your environment, does the LISTEN on 15008 land inside the workload container's netns? The port itself is right, ztunnel binds [::]:15008 as its inbound, but istio makes the DaemonSet's hostNetwork conditional on ambient.shareHostNetworkNamespace, so I cannot establish from upstream where the socket lives by default. Your #2442 note about the ss -Hltn 'sport = :15008' probe in the model pods reads like yes, and you have the environment to confirm it on 1.30.7. If it holds only for some topologies then the docs need the netns stated as a requirement rather than implied by the example.
Minor: the binary test's 503-before-listen assertion assumes the free port it picked stayed free on the runner. Fine to leave, worth knowing it can flake.
|
Two follow-ups to my review. The 15008 question: I had that wrong, so there's nothing for you to check. The I also checked it on the deployment behind #2442, on 1.30.7. The model pods' Your pinned image question: warn, don't refuse. The controller can't tell what a pinned tag or digest contains, so refusing would block pins that work. The failure is also contained: the proxy exits with Timing: as I said on #2442, this lands after 0.10.17, so please hold the merge until that's cut. I'll let you know when it is. |
When a socket table could not be read and a required port was not found in the others, the checker reported ready and logged "required listeners present", which claims the port was seen. That state now logs "required listeners not enforced; reporting ready" with the ports and the unreadable tables, and "present" is kept for ports that were found. Present, missing and not enforced each log once per transition. Refs defilantech#2442 Signed-off-by: Christian Nuss <cnuss@users.noreply.github.com>
/readyz is reachable by any pod that can reach the proxy, and every call read both socket tables under a mutex, so callers could drive a scan per request and queue behind each other. The result is now cached for about a second, and callers that arrive while a check runs wait for it instead of starting their own, so the tables are read at most about once per second. The scan runs outside the mutex, which now only guards the cached result and the logging state. Refs defilantech#2442 Signed-off-by: Christian Nuss <cnuss@users.noreply.github.com>
The test picked a free port, closed it and assumed nothing else would listen on it before the 503 check. It now binds the port without listening, which keeps other processes off it while /readyz must report it missing, and calls listen on that same socket for the 200 check. Refs defilantech#2442 Signed-off-by: Christian Nuss <cnuss@users.noreply.github.com>
…edListeners The listener has to exist in the router pod's own network namespace, which for Istio ambient means in-pod mode, and a runtime class with its own network stack hides it. Fail-open covers unreadable tables only: a port that never listens takes every replica out and stalls a rollout. Also documents the new log line and the one-second /readyz cache. Refs defilantech#2442 Signed-off-by: Christian Nuss <cnuss@users.noreply.github.com>
…steners The mesh binds 15008 within the pod's network namespace in every ambient topology, so the example does not depend on how ztunnel is deployed and the note only added a condition that does not exist. The sandboxed-runtime caveat is shortened to one sentence. Refs defilantech#2442 Signed-off-by: Christian Nuss <cnuss@users.noreply.github.com>
|
Thanks for the review. All three are addressed in new commits on the branch:
On the test flake: the binary test now binds the required port without listening on it. Nothing else can take the port during the 503 check, and the test then listens on that same socket for the 200 check. Thanks for retracting the netns question. It matches what we saw on Istio 1.30.7: the listener shows up as |
Defilan
left a comment
There was a problem hiding this comment.
I read the code and docs in the commits since my approval (1026fe771 through b55c07d82). The three-state logging, the cache and the shared in-flight check look right, and the docs now state the limits clearly. My approval stands, and thanks for addressing all of that so quickly.
One optional hardening, not blocking: if a scan ever panicked, inflight would never be cleared, and every later /readyz would block on it. Clearing and closing it in a defer would rule that out.
v0.10.17 is cut, so the hold from my last comment is over. Tell me if you want to add the defer first; otherwise this is good to merge.
What
Adds opt-in router readiness that requires local TCP listeners.
ModelRouter.spec.proxy.readinessRequiredListeners(for example[15008], Istio ambient's HBONE inbound port) moves the router-proxy's readiness probe to a newGET /readyz. It answers 503 while any listed port has no LISTEN socket in the pod's network namespace. Liveness stays on/health.Why
A router-proxy replica can be running while the service mesh that captures its traffic has not set it up.
/healthkeeps that replica in the Service endpoints, and requests sent to it fail. Gating readiness on the mesh's listener takes such a replica out of rotation without restarting it.Fixes #2442
How
--readiness-required-listener=<port>flag, andGET /readyzon the existing listener next to/healthand/healthz, which are unchanged. Detection is pure Go and reads/proc/net/tcpand/proc/net/tcp6. A port counts only when an entry's local port matches and its state is0A(LISTEN)./readyzreturns 503 with{"status":"not ready","missingListeners":[...]}, otherwise 200. With no ports required it always returns 200.required listeners present; reporting ready(every port seen),required listeners missing; reporting not ready, andrequired listeners not enforced; reporting ready(a table couldn't be read, with the ports and the unreadable tables). Each table also logscannot read TCP socket tableonce, and once more when it can be read again. A missing/proc/net/tcp6, as on a kernel booted with IPv6 disabled, counts as an empty table rather than a read failure. Fail-open covers unreadable tables only: a port that never listens leaves every replica with the field unready./procis read at most about once per second however many callers there are. The scans run outside the mutex, which only guards the cached result and the logging state./readyz(same port and timings), and the args get one--readiness-required-listener <port>per entry. Unset or empty renders the same Deployment as before, so upgrading the operator does not roll existing routers. The existingrouterproxy-*.goldenfiles are unchanged and still pass.readinessRequiredListeners []int32with item bounds 1 to 65535,MaxItems=16andlistType=set. CRDs and chart CRDs regenerated.docs/site/concepts/model-router.mdcovers the field, liveness staying on/health, the fail-open behavior and the log lines. It also says fail-open covers unreadable tables only, so a port that never listens takes every replica out and stalls a rollout. It says not to combine the field with a runtime class that has its own network stack (gVisor), and covers the image requirement whenspec.proxy.imageis pinned, the ~1s cache, and the posture of/readyz./readyzis unauthenticated on the inference port, like/health. It reveals only which configured ports have no listener./proc/net/tcpandtcp6fixtures in the kernel's format: LISTEN in tcp only, LISTEN in tcp6 only, ESTABLISHED on the port, a different port listening, unreadable, missing or malformed tables, and a port found while the other table is unreadable. Each case asserts which of the three state lines is logged, and that it is logged once.-race)./readyzis tested through the mux.waitForReadyin the router-proxy integration tests lost its timeout parameter, because every caller passed 10s andunparamflagged it.Checklist
make testpasses locally (the full suite ran throughmake gate, which also runs fmt, vet,lint-all, regeneration drift and copyright checks)make lintpasses locally (make lint-all, darwin and linux)git commit -s) per DCOAssisted-by: Claude Code (Claude Opus 5.5) generated the code, tests and this description; I reviewed the diff and the description and ran the checks above.