Skip to content

Commit 16254d4

Browse files
committed
Remove relay-only mesh config
1 parent 0007a17 commit 16254d4

9 files changed

Lines changed: 72 additions & 66 deletions

File tree

consul-postgres-ha/ARCHITECTURE.md

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,10 @@ recovery, congestion control, and an idle-timeout-driven liveness
307307
check. Crucially, it does not assume a reliable underlay — *unlike*
308308
yamux, which we tried first and gave up on. The earlier yamux build
309309
sustained ~3 KB on the dstack hairpin path before its keepalive /
310-
recv-window invariants tripped on dropped packets. QUIC sustains
311-
~25 MB/s on the same path.
310+
recv-window invariants tripped on dropped packets. QUIC gives us the
311+
loss recovery and congestion control needed for this underlay; see the
312+
real-env service mesh benchmark below for the current measured data
313+
point.
312314

313315
### Client / server roles
314316

@@ -372,6 +374,46 @@ per ICE conn**, plus a 3-byte header per stream and a 2-byte length
372374
prefix per UDP datagram. That is the entirety of mesh-conn's wire
373375
format.
374376

377+
### Real-env service mesh bandwidth
378+
379+
On 2026-05-17 we measured a direct worker-to-worker service-mesh path
380+
on Phala Cloud `US-WEST-1` with:
381+
382+
- 1 coordinator CVM + 2 worker CVMs, all `tdx.small`.
383+
- Default ICE candidate gathering, so direct candidates were available.
384+
- Worker-1 client -> local service VIP `127.10.0.13:5201` -> local
385+
Envoy -> worker-2 peer VIP `127.50.0.3:21002` -> worker-2 Envoy ->
386+
nginx bound on `127.0.0.1:5201`.
387+
- A Consul service-resolver pinned the benchmark service to
388+
`peer=worker-2`, so the measured route did not use a local backend.
389+
390+
The selected worker-to-worker ICE pair was direct UDP, not TURN relay:
391+
392+
```text
393+
worker-1: host 10.0.2.10:55650 <-> srflx 66.220.6.105:50015 (proto=udp)
394+
worker-2: srflx 66.220.6.105:50015 <-> prflx 66.220.6.105:48294 (proto=udp)
395+
```
396+
397+
Five 512 MiB HTTP downloads through the full Consul Connect path:
398+
399+
| run | seconds | MiB/s | Mbps |
400+
| --- | ---: | ---: | ---: |
401+
| 1 | 8.499705 | 60.24 | 505.31 |
402+
| 2 | 7.780371 | 65.81 | 552.03 |
403+
| 3 | 7.347274 | 69.69 | 584.57 |
404+
| 4 | 7.439566 | 68.82 | 577.31 |
405+
| 5 | 8.585113 | 59.64 | 500.28 |
406+
407+
Average: **64.84 MiB/s / 543.90 Mbps**. Median:
408+
**65.81 MiB/s / 552.03 Mbps**.
409+
410+
The first benchmark attempt used `iperf3`, but Consul's generic TCP
411+
health check opens a plain TCP connection to the service port. That is
412+
valid for most TCP services, but `iperf3` treats the probe as a broken
413+
control session. For arbitrary TCP services whose accept path has
414+
protocol side effects, use a TTL check or a separate health endpoint
415+
instead of a generic TCP connect check.
416+
375417
## Trust boundaries
376418

377419
- **App → Envoy** is plaintext on loopback. Same CVM, same TEE.

consul-postgres-ha/FAILOVER.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,9 @@ detection (which sees worker-4's whole agent disappear, not just the
166166
Patroni lock) lines up with the Patroni leader-key TTL on this run,
167167
so neither signal extends the RTO.
168168

169-
The post-restart rejoin path on dstack-worker pairs is occasionally
170-
flaky (the documented `MESH_CONN_RELAY_ONLY=1` escape hatch in
171-
`compose/worker.yaml` is exactly this case — flip it on if your
172-
deployment hits a wedged ICE re-handshake). The mesh-conn binary
173-
behavior is unchanged by the single-sidecar consolidation.
169+
The post-restart rejoin path on dstack-worker pairs used to be
170+
occasionally flaky when direct ICE re-handshake wedged. The mesh-conn
171+
binary behavior is unchanged by the single-sidecar consolidation.
174172

175173
### Things confirmed by the hard-kill that the soft-kill didn't exercise
176174

@@ -213,9 +211,10 @@ T_streaming 17:35:43 streaming WAL on timeline 4, lag=0 +82s tot
213211
```
214212

215213
A few-MB pgdata transferred in ~18 seconds end-to-end. The dataset
216-
is small enough that handshake/startup overhead dominates — for a
217-
realistic throughput number, see the soft-kill section's pg_basebackup
218-
trace at ~25 MB/s sustained on the QUIC path.
214+
is small enough that handshake/startup overhead dominates, so this
215+
trace should be read as a recovery-path check rather than a bandwidth
216+
benchmark. For a current full service-mesh datapoint, see
217+
`ARCHITECTURE.md`.
219218

220219
The path itself is the proof point: Patroni correctly detects empty
221220
pgdata, picks `bootstrap from leader` (not WAL replay), pulls the full

consul-postgres-ha/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ the platform plumbing untouched.
293293
| In-place env updates | Yes — change image tags or env values, `terraform apply`, CVMs update without losing pgdata. Requires provider `phala-network/phala 0.2.0-beta.3+`. |
294294
| Failover RTO | ~24s soft-kill, ~33s hard-kill (default Patroni `ttl=30`). See [`FAILOVER.md`](FAILOVER.md). |
295295
| Cheap rejoin | Yes — a recovered ex-leader replays local WAL and rejoins as a streaming replica without pg_basebackup. |
296-
| Disk-loss rejoin | Yes — Patroni detects empty pgdata, runs full pg_basebackup over the QUIC overlay (~25 MB/s sustained between dstack workers). |
296+
| Disk-loss rejoin | Yes — Patroni detects empty pgdata and runs full pg_basebackup over the QUIC overlay. |
297297
| Build provenance | Sigstore-attested via GitHub Build Provenance on every published image. Verify with `gh attestation verify oci://... --repo Dstack-TEE/dstack-examples`. |
298298

299299
## Known limitations

consul-postgres-ha/ROBUSTNESS.md

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -162,13 +162,11 @@ whose direct path goes down can't fail over to TURN until it's back".
162162
sustained-forge variant gated on the `onAttemptStarted` hook,
163163
which guarantees the forge lands inside the in-flight window.
164164

165-
- **pion DisconnectedTimeout / FailedTimeout headroom for the
166-
TURN-relay path.** With pion's defaults (5 s + 25 s = 30 s of no
167-
inbound from the selected pair before the state callback fires
168-
Failed), the relay-only path on `MESH_CONN_RELAY_ONLY=1` clusters
169-
flapped on its own roughly every couple of minutes: brief
170-
jitter / refresh-window noise at coturn was enough to accumulate
171-
a 30 s consent-freshness gap, pion declared Failed,
165+
- **pion DisconnectedTimeout / FailedTimeout headroom.** With pion's
166+
defaults (5 s + 25 s = 30 s of no inbound from the selected pair
167+
before the state callback fires Failed), lossy or high-jitter selected
168+
pairs can flap on their own: brief jitter / refresh-window noise is
169+
enough to accumulate a 30 s consent-freshness gap, pion declares Failed,
172170
`OnConnectionStateChange` fired `abortAttempt`, QUIC's
173171
`accept`/pumps came back with `context canceled`, and `runPeerLink`
174172
re-established the link 2–5 s later. The link-pair recovered every
@@ -194,9 +192,9 @@ whose direct path goes down can't fail over to TURN until it's back".
194192
Regression net: `mesh-conn/main_test.go::TestSteadyStateNoFlap`
195193
loopback two-peer steady-state observation. Default window 5 min;
196194
`STEADY_STATE_OBSERVE_SECONDS=N` overrides for the CI 50-trial
197-
sweep at 30 s each (passes 50/50). Live verification: 6-CVM
198-
relay-only cluster observed for 15+ min post-rollout with zero
199-
`ice state: Failed` events on any peer's sidecar log.
195+
sweep at 30 s each (passes 50/50). Live verification: a 6-CVM
196+
cluster was observed for 15+ min post-rollout with zero `ice state:
197+
Failed` events on any peer's sidecar log.
200198

201199
## Layer 2 — mesh-conn forwarder
202200

consul-postgres-ha/cluster-example/cluster.tf

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -107,15 +107,6 @@ variable "external_turn_secret" {
107107
sensitive = true
108108
}
109109

110-
# Force ICE to gather Relay candidates only — routes all peer traffic
111-
# through coturn instead of attempting NAT-hairpin direct paths. Set
112-
# this when worker↔worker direct-pair ICE handshakes are unstable
113-
# (the dstack provider NAT path is known-flaky for these pairs).
114-
variable "mesh_conn_relay_only" {
115-
type = string
116-
default = ""
117-
}
118-
119110
# Set to "1" to enable pion's verbose ICE debug logs (connectivity
120111
# checks, STUN attribute parsing). Useful when debugging hot-patch
121112
# / re-handshake flakiness; off by default because it's very chatty.
@@ -328,7 +319,6 @@ locals {
328319
GOSSIP_KEY = random_bytes.gossip_key.base64
329320
PATRONI_SUPERUSER_PW = random_bytes.patroni_superuser_pw.hex
330321
PATRONI_REPLICATION_PW = random_bytes.patroni_replication_pw.hex
331-
MESH_CONN_RELAY_ONLY = var.mesh_conn_relay_only
332322
MESH_CONN_DEBUG_ICE = var.mesh_conn_debug_ice
333323
ADMISSION_BROKER_ENABLE = var.enable_attestation_admission ? "1" : ""
334324
CONSUL_MANAGEMENT_TOKEN = var.consul_management_token
@@ -349,7 +339,6 @@ locals {
349339
MESH_SIDECAR_IMAGE = var.mesh_sidecar_image
350340
DSTACK_VERIFIER_IMAGE = var.dstack_verifier_image
351341
GOSSIP_KEY = random_bytes.gossip_key.base64
352-
MESH_CONN_RELAY_ONLY = var.mesh_conn_relay_only
353342
MESH_CONN_DEBUG_ICE = var.mesh_conn_debug_ice
354343
ADMISSION_BROKER_ENABLE = var.enable_attestation_admission ? "1" : ""
355344
# The env key is part of the compose allowlist, but the policy

consul-postgres-ha/compose/coordinator.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,6 @@ services:
6969
# WORKAROUND: gossip key generated in Terraform and broadcast
7070
# identically to every CVM. See cluster.tf.
7171
- GOSSIP_KEY=${GOSSIP_KEY}
72-
# See worker.yaml for the rationale on MESH_CONN_RELAY_ONLY.
73-
- MESH_CONN_RELAY_ONLY=${MESH_CONN_RELAY_ONLY:-}
7472
- MESH_CONN_DEBUG_ICE=${MESH_CONN_DEBUG_ICE:-}
7573
- ADMISSION_BROKER_ENABLE=${ADMISSION_BROKER_ENABLE:-}
7674
- ADMISSION_POLICY_JSON=${ADMISSION_POLICY_JSON:-}

consul-postgres-ha/compose/worker.yaml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,6 @@ services:
5252
- GOSSIP_KEY=${GOSSIP_KEY}
5353
- ADMISSION_BROKER_ENABLE=${ADMISSION_BROKER_ENABLE:-}
5454
- CONSUL_MANAGEMENT_TOKEN=${CONSUL_MANAGEMENT_TOKEN:-}
55-
# MESH_CONN_RELAY_ONLY=1 forces ICE to gather only Relay
56-
# candidates, routing all peer traffic through the coturn server.
57-
# Default off because direct candidates work; flip on if a
58-
# deployment hits worker↔worker direct-pair instability.
59-
- MESH_CONN_RELAY_ONLY=${MESH_CONN_RELAY_ONLY:-}
6055
- MESH_CONN_DEBUG_ICE=${MESH_CONN_DEBUG_ICE:-}
6156
volumes:
6257
- /var/run/dstack.sock:/var/run/dstack.sock:ro

consul-postgres-ha/mesh-conn/main.go

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@
5555
// the root cause is dropped packets. QUIC has built-in loss recovery,
5656
// congestion control, and stream-multiplexing — it's exactly what a
5757
// lossy UDP underlay needs. The previous yamux build died at 3KB-260KB
58-
// depending on path; the QUIC build sustains 25-28 MB/s on the same
59-
// hairpin.
58+
// depending on path; current service-mesh bandwidth is documented in
59+
// ARCHITECTURE.md from real-env benchmarks.
6060

6161
package main
6262

@@ -1115,37 +1115,26 @@ func (m *Mesh) dialICE(remoteID string, sess *peerSession, attemptCtx context.Co
11151115
}
11161116
}
11171117

1118-
// MESH_CONN_RELAY_ONLY=1 restricts candidate gathering to Relay only.
1119-
// Use when direct (host/srflx/prflx) connectivity is unreliable — e.g.
1120-
// dstack worker-to-worker pairs where pion's connectivity check fails
1121-
// for every direct pair and the agent never gets to relay before
1122-
// timing out. Trades latency for guaranteed reachability via coturn.
11231118
candidateTypes := []ice.CandidateType{
11241119
ice.CandidateTypeHost,
11251120
ice.CandidateTypeServerReflexive,
11261121
ice.CandidateTypePeerReflexive,
11271122
ice.CandidateTypeRelay,
11281123
}
1129-
if os.Getenv("MESH_CONN_RELAY_ONLY") == "1" {
1130-
candidateTypes = []ice.CandidateType{ice.CandidateTypeRelay}
1131-
}
11321124
// MESH_CONN_DEBUG_ICE=1 turns on pion's verbose ICE-level logging
11331125
// (connectivity-check requests/responses, STUN attribute parsing,
11341126
// candidate-pair scoring). Off by default because it's chatty.
11351127
//
11361128
// Disconnected/Failed timeouts: pion defaults to 5 s + 25 s (= 30 s
11371129
// of no inbound from the selected pair before the state callback
11381130
// fires Failed and our outer code aborts the attempt). That is too
1139-
// tight for the TURN-relay path. The relay leg adds ~150 ms RTT,
1140-
// the periodic permission/allocation refreshes can briefly
1131+
// tight for high-jitter selected pairs. TURN relay candidates add
1132+
// RTT, the periodic permission/allocation refreshes can briefly
11411133
// reorder packet flow at coturn, and pion's own 2 s consent
1142-
// freshness has no retry budget — a sub-second jitter window
1143-
// where 1 STUN binding gets lost and another arrives late is
1144-
// enough to start the disconnected clock, and consecutive jitter
1145-
// events accumulate against the 30 s budget. We've observed the
1146-
// resulting "Connected → Disconnected → Failed → Checking →
1147-
// Connected" cycle every couple of minutes on freshly-deployed
1148-
// relay-only clusters, with no actual peer death behind it.
1134+
// freshness has no retry budget — a sub-second jitter window where
1135+
// 1 STUN binding gets lost and another arrives late is enough to
1136+
// start the disconnected clock, and consecutive jitter events
1137+
// accumulate against the 30 s budget.
11491138
//
11501139
// The fix is to give pion enough headroom that only a genuine
11511140
// relay-path outage (rather than refresh / jitter noise) trips

consul-postgres-ha/quic-on-ice/main.go

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,12 @@ func main() {
5858
turnHost := flag.String("turn-host", "", "TURN host (e.g. 155.138.146.255)")
5959
turnSecret := flag.String("turn-secret", "", "TURN HMAC shared secret")
6060
mb := flag.Int("mb", 10, "MB to transfer")
61-
relayOnly := flag.Bool("relay-only", false, "force ICE relay-only candidates")
6261
flag.Parse()
6362
if *role == "" || *peerID == "" || *signal == "" {
6463
log.Fatalf("need -role, -peer, -signal")
6564
}
6665

67-
conn, err := dialICE(*role, *peerID, *signal, *turnHost, *turnSecret, *relayOnly)
66+
conn, err := dialICE(*role, *peerID, *signal, *turnHost, *turnSecret)
6867
if err != nil {
6968
log.Fatalf("ice dial: %v", err)
7069
}
@@ -125,8 +124,8 @@ func runQUICClient(pkt net.PacketConn, peerID string, mb int) {
125124
// of "where to send" matches what the PacketConn does anyway.
126125
remote := pkt.LocalAddr() // any addr — pkt.WriteTo ignores it
127126
conn, err := quic.Dial(ctx, pkt, remote, tlsConf, &quic.Config{
128-
KeepAlivePeriod: 5 * time.Second,
129-
MaxIdleTimeout: 60 * time.Second,
127+
KeepAlivePeriod: 5 * time.Second,
128+
MaxIdleTimeout: 60 * time.Second,
130129
InitialStreamReceiveWindow: 4 << 20,
131130
MaxStreamReceiveWindow: 16 << 20,
132131
InitialConnectionReceiveWindow: 8 << 20,
@@ -264,7 +263,7 @@ var (
264263
sessions = map[string]*peerSession{}
265264
)
266265

267-
func dialICE(self, remote, signalURL, turnHost, turnSecret string, relayOnly bool) (*ice.Conn, error) {
266+
func dialICE(self, remote, signalURL, turnHost, turnSecret string) (*ice.Conn, error) {
268267
var urls []*stun.URI
269268
if turnHost != "" {
270269
user, pass := turnCreds(turnSecret, time.Hour)
@@ -280,9 +279,6 @@ func dialICE(self, remote, signalURL, turnHost, turnSecret string, relayOnly boo
280279
ice.CandidateTypePeerReflexive,
281280
ice.CandidateTypeRelay,
282281
}
283-
if relayOnly {
284-
candTypes = []ice.CandidateType{ice.CandidateTypeRelay}
285-
}
286282
agent, err := ice.NewAgent(&ice.AgentConfig{
287283
Urls: urls,
288284
NetworkTypes: []ice.NetworkType{ice.NetworkTypeUDP4, ice.NetworkTypeTCP4},

0 commit comments

Comments
 (0)