Build a Service Mesh (Envoy / Istio style) (13 scenes)
Scene 10 · Control plane and data plane — config over gRPC
Sidecars (data plane) handle traffic; the control plane (Istiod) streams listener/route/cluster/cert config via xDS. Kill the control plane and traffic keeps flowing.
Previously
Certs came from a central CA, route tables came from a central operator — there has to be one process responsible for fanning all of that out to every sidecar live, and that role splits the mesh into two planes.
Scene 11
Control plane and data plane — config over gRPC
Diagram
Top: the **control plane** (Istiod) — one process that watches the cluster, computes config, and is NOT on the request path. Bottom row: the **data plane** — eight **sidecar** envoys, each with a cached route table cell (the listener/route/cluster config from scenes 4-5). Between them: long-lived **xDS** gRPC streams (sub-resources LDS/RDS/CDS/EDS — listeners/routes/clusters/endpoints — multiplexed on one connection). Below the sidecars: request traffic, the bytes that actually flow between services — this animation never pauses regardless of control-plane health.
↑ control plane — config issuer, NOT on the request path
long-lived streaming gRPC ↓
← data plane — sidecars handling traffic
data-plane traffic never pauses
One control plane up top streams config to every sidecar over a long-lived xDS gRPC stream. The sidecars are the data plane — they hold cached config and move bytes for every request. Watch the request traffic below the sidecar row.
Implementation
Sidecar.subscribeXds
long-lived gRPC stream — server pushes, sidecar acks
1stream = grpc.stream(2 CONTROL_PLANE,3 "/envoy.service.discovery.v3"4 ".AggregatedDiscoveryService/StreamAggregatedResources",5)6stream.send(DiscoveryRequest(7 resource_names=["mesh-listener", "checkout-routes", ...],8))9loop:10 resp = stream.recv() # server pushes on change11 apply(resp.resources) # update cached LDS/RDS/CDS/EDS12 stream.send(ack(resp.nonce)) # ack the version_info
ControlPlane.onYamlChange
operator edits a VirtualService — Istiod fans out new routes
1def on_yaml_change(vs: VirtualService):2 new_routes = compile_routes(vs)3 version = bump_version()4 for sidecar in registry.affected_sidecars(vs):5 sidecar.xds_stream.send(DiscoveryResponse(6 resources=[new_routes],7 type_url=RDS_TYPE_URL,8 version_info=version,9 nonce=fresh_nonce(),10 ))
DataPlane.whenControlPlaneDies
why traffic survives the loss of Istiod
1# Sidecars KEEP serving — they hold the last applied2# LDS/RDS/CDS/EDS in memory and consult THAT cache3# on every request, never the control plane.4for req in inbound_traffic:5 route = cached_rds.match(req) # in-memory, authoritative6 forward(req, route.cluster)78# New operator edits stall — no Istiod to compile them.9# Cert rotation continues from the CA pool until SVIDs10# expire (~24h Istio default).11# Therefore: control plane is on the CONFIG path,12# NOT the REQUEST path.