Runnable demonstration of envoyproxy/envoy#46159. It shows that Envoy's existing gRPC client and cluster hash policies can keep requests with the same downstream session key on the same external processor endpoint.
The demo runs stock Envoy with two Go ext_proc servers. Each server returns an immediate response identifying itself and the gRPC initial metadata it received, making endpoint selection visible.
Envoy
+----------------------------------+
| |
client -- x-session-id | :10080 ext_proc_header cluster |
| RING_HASH(header) |
| |
client -- session_id | :10081 ext_proc_cookie cluster |
cookie | RING_HASH(cookie) |
| |
+----------------------------------+
|
| ring-hash selects one endpoint
|
+---------> +----------------------+
| | processor-a :50051 |
| +----------------------+
|
+---------> +----------------------+
| processor-b :50051 |
+----------------------+
Both listeners send request headers to the ext_proc filter. Both clusters contain the same two Go gRPC processors; they differ only in the hash policy used to select one processor.
The header-affinity listener on port 10080 copies x-session-id into the ext_proc gRPC initial
metadata. The Envoy gRPC client creates the ext_proc request with that metadata, and the
ext_proc_header cluster hashes its x-session-id header with RING_HASH.
The cookie-affinity listener on port 10081 copies the downstream cookie header. Its cluster
uses the passive session_id cookie hash policy. Envoy does not generate or return this cookie; the
downstream client supplies it.
downstream session key
-> GrpcService.initial_metadata
-> gRPC request header or cookie
-> cluster HttpProtocolOptions.hash_policy
-> ring-hash endpoint selection
-> processor-a or processor-b
-> HTTP 200 immediate response containing processor=<selected ID>
For a fixed key and fixed healthy endpoint set, each request should hash to the same processor. A
different key may select either processor. The processors return an immediate response so the
selected endpoint is directly observable; the route's fallback 204 response is not the behavior
under test.
Warning
docker compose up --build downloads and executes container images from the internet. Review the
Dockerfile, Compose configuration, image tags, and Go dependencies first, and run this demo only
in an isolated, non-production environment.
Start Envoy and both processors:
make upIn another shell, show how several session keys map to the two processors:
make observeRepeated requests with one key must remain on exactly one processor:
make check SESSION=session-1Measure both affinity paths across multiple sessions:
make measure SESSIONS=32 REQUESTS_PER_SESSION=5For each affinity mode, the measurement performs these steps:
- Generate
SESSIONSdistinct keys:session-000,session-001, and so on. - Send
REQUESTS_PER_SESSIONsequential requests for each key. - Require every response to be HTTP
200and contain a non-emptyprocessor=<ID>field. - Build the set of observed processor IDs for each session.
- Count the session as an
affinity_violationwhen that set contains more than one ID. - Require zero affinity violations and exactly
EXPECTED_PROCESSORSdistinct processors overall.
The second assertion ensures this two-processor demo does not accidentally pass while only one processor is serving traffic. The per-processor request and distinct-session counts expose the distribution without requiring a perfectly even split.
A successful run looks like this (the distribution varies):
mode=header sessions=32 requests_per_session=5 total_requests=160 processors_observed=2 affinity_violations=0
processor=processor-a requests=85 sessions=17
processor=processor-b requests=75 sessions=15
mode=cookie sessions=32 requests_per_session=5 total_requests=160 processors_observed=2 affinity_violations=0
processor=processor-a requests=85 sessions=17
processor=processor-b requests=75 sessions=15
Try the individual paths:
make header SESSION=session-1
make cookie SESSION=session-1
make missingThe exact processor selected for a key is not important. The invariant is that repeated requests with the same key select the same processor while both endpoints are healthy and the cluster membership is unchanged. The measurement deliberately sends requests sequentially; it measures endpoint affinity and endpoint participation, not throughput, latency, failover, or remapping after membership changes.
Stop the demo with Ctrl-C, then remove its containers and network:
make downThe prepared Envoy branch adds documentation and integration coverage without changing production behavior:
https://github.com/envoyproxy/envoy/compare/main...dio:envoy:ext-proc-session-affinity
It covers header affinity, passive-cookie affinity, and requests without an affinity key. The cluster-level approach applies to the Envoy gRPC client, not the native Google gRPC client.