gortiz opened a new pull request, #19383:
URL: https://github.com/apache/pinot/pull/19383
## Problem
Mailbox channels (server↔server, and server→broker) are cached per `(host,
port)` and were built with
no liveness policy: `ChannelManager` configured only `idleTimeout`, which is
disabled by default
(`pinot.query.runner.channel.idle.timeout.seconds=-1`) and never fires on a
cluster that keeps serving
queries anyway.
So a peer that stops answering **without closing its socket** — a host whose
kernel is hung, a one-way
partition, a pod IP that no longer exists anywhere — is invisible. No `RST`
arrives, gRPC keeps the
channel in `READY`, and every mailbox send parks until the query deadline.
Worse, since gRPC only
re-resolves DNS when a transport is dropped, the cached channel keeps
pointing at the dead address for
every *later* query too, so a peer that has since come back at a new address
is never reached again.
We have hit this in production: several nodes went `NotReady`, their pods
were force-deleted and
rescheduled, and the servers that did **not** restart each kept mailbox
channels to pod IPs that no
longer existed. Every multi-stage query then timed out — while the leaf
stages completed in
milliseconds — for hours, until the dead sockets were killed by hand. A peer
whose process actually
dies was never the problem: that sends a `RST`, which gRPC has always
handled.
## Changes
**Client** — keep-alive on every mailbox channel:
| Config | Default |
|---|---|
| `pinot.query.runner.channel.keep.alive.time.ms` | `300000` |
| `pinot.query.runner.channel.keep.alive.timeout.ms` | `30000` |
| `pinot.query.runner.channel.keep.alive.without.calls` | `false` |
**Server** — the mailbox server's ping-rate enforcement becomes tunable, so
lowering the client
interval does not get the channel torn down with `GOAWAY(ENHANCE_YOUR_CALM)`:
| Config | Default |
|---|---|
| `pinot.query.runner.mailbox.server.permit.keep.alive.time.ms` | `300000` |
| `pinot.query.runner.mailbox.server.permit.keep.alive.without.calls` |
`false` |
Both defaults match Netty's own gRPC server defaults, so a peer still
running an older version —
which enforces exactly those values and has no knob to relax them — is never
punished during a rolling
upgrade. This mirrors what `QueryServer` already does for the broker
dispatch channel (#18268).
`DispatchClient.KeepAliveConfig` is promoted to
`org.apache.pinot.query.grpc.GrpcKeepAliveConfig`
rather than duplicated, so both MSE channel types share one holder and one
place where "disabled"
means the settings are never handed to gRPC (which rejects a non-positive
`keepAliveTime`).
## What the defaults buy, and what they don't
With `withoutCalls=false` the ping deadline is anchored to the last data
*received* on the transport
(`KeepAliveManager` only advances it in `onDataReceived`), **not** to stream
boundaries — so a channel
whose streams keep dying at their query deadline still accumulates it; the
ping is deferred until a
stream is open again and then fires at once. The dead transport is therefore
torn down by the next
query to open a stream, and that one query fails. Turning `without.calls` on
moves the tear-down into
the background between queries, so no query sees the dead channel at all.
Queries inside the detection window still fail. What changes is that the
cluster recovers on its own
instead of staying broken until someone intervenes.
## Release notes
Five new configuration options (see the tables above) add gRPC keep-alive to
the multi-stage engine's
mailbox channels. Keep-alive is **on by default** at a 5-minute interval
with a 30-second timeout,
which is deliberately conservative: it is the only setting that is safe
while any peer in the cluster
still runs a version without the matching server-side permit knobs. No
action is required to get the
default protection.
To detect a silent peer faster than ~5.5 minutes, lower the interval in
phases. The rule is per
direction: for every channel A→B, A's `channel.keep.alive.time.ms` must be
**≥** B's
`mailbox.server.permit.keep.alive.time.ms`. Both brokers and servers run a
mailbox server, so every
phase applies to both roles. All of these keys are read at startup, so each
phase is a rolling restart.
1. **Prerequisite.** Every broker and server must already run a version
containing these options.
Before that, a peer enforces Netty's 5-minute permit with no way to relax
it, and a faster ping is
answered with `GOAWAY(ENHANCE_YOUR_CALM)`. Do not tune inside a
mixed-version window.
2. **Widen the permits first** — this cannot break anything, because it only
*accepts* faster pings
while nobody is sending them yet. On brokers and servers set
`pinot.query.runner.mailbox.server.permit.keep.alive.time.ms=15000` (and
`...permit.keep.alive.without.calls=true` if you intend step 4). Roll,
then confirm from the
startup log: `Starting GrpcMailboxServer with ...
permitKeepAliveTimeMs=15000`.
3. **Lower the client interval.** Set
`pinot.query.runner.channel.keep.alive.time.ms=30000`, leaving
the timeout at its default. Roll, then confirm from the startup log:
`Initialized MailboxService with ... channel keepAlive[timeMs=30000,
...]`. Detection is now ~60s.
4. **Optional.** Set
`pinot.query.runner.channel.keep.alive.without.calls=true` so a dead transport
is
dropped in the background rather than by failing the next query.
Choosing values:
- **The floor is 10 seconds.** gRPC-Java silently clamps a smaller client
interval up to 10s, so a
lower value in your config has no effect.
- **Shrink the interval, not the timeout.** The 30s timeout is sized to
survive a long stop-the-world
pause; a ping unanswered for a few seconds is a GC pause, not a dead host,
and dropping the
transport there fails healthy in-flight queries. For ~40s detection prefer
interval 10s + timeout
30s over interval 30s + timeout 10s.
- Keep the permit at roughly half the client interval, so jitter cannot
produce a strike.
- `too_many_pings` / `ENHANCE_YOUR_CALM` in the logs means the ordering
above was inverted: roll back
the client interval, not the permit. `UNAVAILABLE: Keepalive failed`
against peers that were
actually alive means the timeout is too tight.
The broker dispatch channel added in #18268 has the same defaults and the
same procedure, via
`pinot.query.multistage.dispatch.channel.keep.alive.time.ms` and
`pinot.query.multistage.query.server.permit.keep.alive.time.ms`.
## Testing
- `MailboxChannelKeepAliveTest` reproduces the failure end to end: two
channels through a TCP relay
that stops forwarding while holding every socket open (killing the peer
instead would send the `RST`
gRPC already handles). The keep-alive channel must fail `UNAVAILABLE`,
while an identically
black-holed channel with keep-alive off must still believe the peer is
healthy — so the test cannot
pass for the wrong reason. Verified by ablation: removing the wiring in
`ChannelManager#decorate`
makes it fail.
- `MailboxServiceKeepAliveConfigTest`, `MailboxServerPermitKeepAliveTest`,
`GrpcKeepAliveConfigTest`,
and additions to `ChannelManagerTest` cover config parsing, the defaults,
the mixed-version bound
(client interval ≥ the permit peers enforce), and that a disabled policy
is not handed to gRPC.
- `QueryServerTest`, `QueryDispatcherTest`, `MailboxServiceTest`,
`GrpcSendingMailboxTest`,
`GrpcSenderBackpressure*Test`, `GrpcMailboxServerValidationTest` all pass,
covering the
`GrpcKeepAliveConfig` extraction.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]