TT Lab
Blog

한국어 English 日本語 中文

Get started
Blog

Blog

Hybrid GPU Kubernetes with Cilium and Istio Networking

Status: Draft (2026-10-05), partially reflecting actual measurements. The sections "Measured: The Cost of the Istio Sidecar," "mTLS Between Services: Configuration and Actual Behavior," and "Measured: kube-proxy and Cilium When Services Grow to Ten Thousand" contain values directly measured by the author on a VM created in the experimental environment, and the "Reinforced Measurements" (2026-10-09) in those sections were measured on a kind cluster on the same host. The remaining descriptions are verified through official documentation and have not been measured yet. Please read "Verified Versions and Dates" and "Unverified Items" at the end of the article first. Writing Rule: Measured data is the default, and items that could not be measured are marked as "Unmeasured."

Table of Contents

The reasons commonly cited for choosing a hybrid approach are on-premises GPU unit costs, data location, and cloud supply and elasticity. Using them together requires designing cluster boundaries, paths for node joining, service discovery, and failure scopes entirely by yourself. This article summarizes the choices and constraints based on official documentation, while separately addressing long-lived connections and streaming, which are particularly problematic in LLM serving.

What You Can Explain in an Interview After Reading This Article

  1. You can compare whether to combine or split clusters based on etcd latency, failure domains, and operational overhead, and explain where EKS Hybrid Nodes fit in.
  2. You can explain the role boundaries between Cilium (Container Network Interface (CNI), policies, Gateway API, ClusterMesh) and Istio (mutual TLS (mTLS), L7 routing) and the configurations required when using them together.
  3. You can explain with examples how streaming LLM responses break down due to proxy layer idle timeouts, HTTP/2 multiplexing, and shallow health checks.

1. Why Hybrid

There are three reasons. The first is cost. The logic that steady-state loads are handled by owned equipment and peaks are offloaded to the cloud is common, but this article has not verified cost figures. The second is GPU supply, and the third is data location. An AWS blog post (2025-03-19) lists retrieval-augmented generation (RAG) inference close to data, cloud utilization during peaks, and edge inference as use cases for Hybrid Nodes.

2. Cluster Configuration Choices

Choices Advantages Costs
Split clusters by environment and connect them using a mesh or Multi-Cluster Services (MCS) Failure and security boundaries are separated Service discovery, policies, and deployments must be managed redundantly per environment
Join on-premises nodes to a cloud-managed control plane (EKS Hybrid Nodes) Offload control plane operations to AWS If connectivity drops, you are disconnected from the control plane
Extend a single control plane over WAN It is a single cluster etcd consensus is sensitive to latency

etcd documentation guides setting the heartbeat interval to 0.5–1.5 times the round-trip time (RTT) between members and the election timeout to at least 10 times the RTT. The defaults are 100ms and 1000ms, respectively. Kubernetes documentation states that if availability is critical, control plane components should be replicated across at least 3 failure domains.

The core conditions in the EKS Hybrid Nodes documentation are as follows:

For GCP, we have only verified GKE Multi-cluster Services (for sharing services viaServiceExport) and multi-cluster Gateways (where a single configuration-only cluster manages Gateway API resources).

3. Node Joining and Network Paths

flowchart LR
  subgraph ONP["온프레미스"]
    G1["GPU 노드 A"] --- R["라우터·방화벽"]
    G2["GPU 노드 B"] --- R
  end
  R -- "Site-to-Site VPN 또는 Direct Connect" --> TGW["Transit Gateway 또는 가상 사설 게이트웨이"]
  TGW --> VPC["VPC 서브넷"]
  VPC --> CP["EKS 제어 평면"]
  VPC --> CN["클라우드 노드"]

AWS Site-to-Site VPN provides up to 1.25Gbps per tunnel, with a bandwidth-optimized option offering 5Gbps, aggregated via Equal-Cost Multi-Path (ECMP) when using dynamic routing on a Transit Gateway. The MTU is 1446 bytes and does not support Path MTU Discovery (PMTUD). Because overlay encapsulation further reduces pod MTU, fragmentation must be verified through measurement. Direct Connect is a private dedicated line, and MACsec is an optional feature on select dedicated connections. Rather than pulling large files like model weights across the WAN repeatedly (explanatory example: downloading 140GB at 1Gbps theoretically takes about 19 minutes), it is better to keep them in on-premises registries and caches.

The ports Cilium requires between nodes are VXLAN UDP 8472, WireGuard UDP 51871, and health check TCP 4240. In hybrid environments, intermediate firewalls frequently block these ports (see "Common Pitfalls" below).

4. The Role of Cilium

5. The Role of Istio

Istio handles service-to-service mTLS identities (SPIFFE-based certificates) and L7 routing (traffic splitting, retries, and timeouts viaVirtualService's weight).

Item Sidecar Mode Ambient Mode (GA since 1.24)
Data Plane Envoy per pod ztunnel (L4) per node, waypoint (L7 Envoy) when needed
mTLS Between sidecars HBONE tunnel between ztunnels (port 15008)
L7 Features Included by default Only when waypoint is deployed

Multi-cluster Ambient has beta support for multi-network configurations, and the undocumented or strict constraints include: not recommended for existing sidecar production, single-network untested, support for only multiple primary clusters, identical waypoint names across all clusters, and manual configuration synchronization. Version 1.31 (2026-08-31) added zone-aware load balancing (zoneAwareLbSetting) and waypoint canary.

Measured: The Cost of the Istio Sidecar

We quantified what the documentation refers to when it states that "sidecars add latency."

Environment. A single KubeVirt VM (8 vCPU, 7.9GiB, guest kernel 6.8.0-139), k3s v1.35.8, Istio 1.31.0 (profile=default, native sidecar), load testing tool fortio 1.75.3. Within the same VM, client and server pods were deployed to alternately measure plain(no injection) and mesh(sidecars on both sides, mTLS). Test conditions were 16 connections, a fixed 500 QPS for 25 seconds (5 runs), and saturation for 20 seconds (3 runs).

Metric No Mesh Sidecar mTLS Difference
Average Latency (500 QPS, Median) 0.241 ms 2.450 ms +2.2 ms (approx. 10x)
p50 / p90 / p99 0.232 / 0.349 / 0.538 ms 2.474 / 3.068 / 3.726 ms +2.2 / +2.7 / +3.2 ms
Saturation Throughput 217,476 QPS 16,246 QPS 7.5% of no mesh

Variance across the 5 runs was within ±3% of the average latency, and there were no failed calls.

What these numbers mean. A single request passing through two sidecars (client-side and server-side) added about 2.2ms of latency in this environment. For LLM serving requests that take hundreds of milliseconds to the first token, this cost is minimal, but for responses streaming token by token or internal microservices where latency is around 1ms, it becomes significant. The drop in saturation throughput to 7.5% occurs because the proxies share the same CPU, which means that for sidecars, you must allocate a separate CPU budget in addition to latency .

What these numbers do not show.

Lessons learned from the measurement. The initial measurements were off due to fortio's default histogram resolution (1ms). When latency is under 1ms, percentiles are recorded as positions within the bucket (0.5, 0.9, 0.99ms) rather than actual values. I realized this after seeing a mismatch between the average latency of the same run and Little's Law (concurrent connections / throughput). This is a case where you must doubt the tool itself and cross-check the two metricsbefore trusting benchmark results, which will be covered again in Part 5.

mTLS Between Services: Configuration and Actual Behavior

These are the results of actually applying and verifying "how to establish mTLS between solutions" in Istio 1.31.0.

First, two layers must be distinguished. Cilium WireGuard encrypts pod traffic traveling between nodes using node-level keys (by default, this only covers traffic between pods managed by Cilium; node-to-host traffic requires enabling a beta option, and traffic within the same node is not encrypted), but it does not verify workload identities. Istio mTLS allows both sides to authenticate each otherusing certificates issued by service accounts (SPIFFE identity), and access can be allowed or denied based on that identity. If service-to-service authentication is the goal, Istio is the right tool.

The configuration consists of three pieces. All YAMLs below were applied to test VMs to verify their behavior. The API version is security.istio.io/v1 .

  1. Label the namespace with istio-injection=enabled , and already running pods must be restarted for the sidecar to be injected.
  2. Use PeerAuthentication to determine whether the server side will accept non-mTLS connections. Since the default when no policy exists is PERMISSIVE, the safe approach is to observe first and then elevate to STRICT.
  3. Since identities can only be trusted once mTLS is established, on top of that, use AuthorizationPolicy to restrict which service accounts are allowed to call.
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata: {name: default, namespace: <네임스페이스>}
spec: {mtls: {mode: STRICT}}
---
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata: {name: only-good, namespace: <네임스페이스>}
spec:
  selector: {matchLabels: {app: <서버 앱>}}
  action: ALLOW
  rules:
  - from: [{source: {principals: ["cluster.local/ns/<네임스페이스>/sa/good"]}}]

The caller's DestinationRule(tls.mode: ISTIO_MUTUAL) is not mandatory because automatic mTLS is applied between meshes according to the documentation. In the experiment, it was specified to explicitly turn encryption on and off.

Actual Behavior. The client is a temporary curl pod, and the results are shown as the status code and curl exit code.

Scenario Result
No policy (PERMISSIVE), plaintext client outside the mesh 200
STRICT, plaintext client outside the mesh 000 exit=56 (Connection dropped)
STRICT, client inside the mesh 200
AuthorizationPolicy, allowed service account 200
Same policy, disallowed account and default account 403

There are two important points in operations. First, the way STRICT rejects requests is a connection drop, not an HTTP error. Searching for 403 in server logs yields nothing, and on the client side, it only manifests as Connection reset or curl: (56) . When a failure occurs, your first suspicion should be: "Is it STRICT, but the peer is outside the mesh?" Second, authorization rejection is 403 . Because the mTLS connection was successfully established and the identity was simply not on the allowlist, the two symptoms differ, allowing you to distinguish the cause.

Separating the Costs of Encryption and Proxy Hops. This is a comparison with two sidecars left intact while only mTLS was toggled on and off (same VM, average latency is the median of 5 runs at 500 QPS, saturation throughput is the median of 3 runs).

Configuration Average Latency Saturation Throughput
No mesh 0.241 ms 217,476 QPS
Sidecar, mTLS off 2.252 ms 18,339 QPS
Sidecar, mTLS on 2.437 ms 16,511 QPS

Of the approximately 2.2ms added above, the two proxy hops account for about 2.0ms (approx. 92%) and encryption accounts for about 0.19ms (approx. 8%) . Contrary to the common belief that "mTLS is slow," in this environment most of the cost came from the proxy's HTTP processing rather than encryption. However, since this VM's CPU exposed AES hardware acceleration (aes, vaes), we cannot claim that the same ratio holds for CPUs without acceleration, large bodies, or long streaming sessions (unmeasured).

Additional test separating HTTP parsing from proxy hops. The same server was exposed via a port named http- (L7 parsing) and a port named tcp- (L4 byte relay) to compare sidecars (same VM, 500 QPS for 20 seconds, 5 cross runs, median average latency).

Configuration Average Latency Relative to No Mesh
No mesh 0.238 ms Baseline
Sidecar, L4(tcp-) 1.585 ms +1.35 ms
Sidecar, L7(http-) 2.459 ms +2.22 ms

Of the 2.2ms added, HTTP parsing accounted for about 0.87ms (approx. 39%), while simply routing through L4 without parsing left +1.35ms (approx. 61%) incurred solely by traversing two sidecars. Therefore, "two proxy hops account for ~92%" is the sum of non-encryption costs, within which HTTP parsing's share is around 40%. External research ("Dissecting Service Mesh Overheads") reports that protocol parsing constitutes 60-70% of proxy costs, which is smaller in view of this environment's L4 versus L7 difference (we do not make definitive claims as conditions differ).

gRPC, Large Bodies, Long Streaming, and Plaintext Exception Ports.

Test No Mesh Sidecar (mTLS)
gRPC unary call average latency (500 QPS, 3 runs) 0.397 ms (0.394~0.408) 3.834 ms (3.054~3.961), approx. +3.4ms
Downloading a 100MiB response once (3 runs) 5,363~8,107 MB/s, 0.01~0.02s 1,517~1,895 MB/s, 0.06~0.07s
200 SSE events (server sends every 20ms, 3 runs) Interval median 20.1ms, p99 20.3ms Interval median 20.1ms, p99 20.4ms (max 22.0ms)
Plaintext client outside mesh under STRICT 000 exit=56(Connection dropped)
Setting port 8080 to PERMISSIVE with portLevelMtls under STRICT 200

Precautions when applying to actual service clusters are inferences that have not yet been tested (unmeasured). Plaintext traffic coming from outside the mesh (e.g., Cilium Gateway) hits the connection drop mentioned above when reaching STRICT services, so the approach used is to set only the relevant port to PERMISSIVE using portLevelMtls (behavior verified in the table above). Databases, DaemonSets, and Jobs should be migrated last, and for Jobs, you must separately check for the issue where sidecars do not terminate.

Measured: kube-proxy and Cilium When Services Scale to Ten Thousand

We verified with numbers the claim that "if Cilium replaces kube-proxy, things remain fast even as services scale."

Environment. Two KubeVirt VMs (8 vCPUs each, 7.9GiB, guest kernel 6.8.0-139, k3s v1.36.5). One runs k3s built-in kube-proxy (iptables, nf_tables backend) and flannel, while the other runs Cilium 1.20.1 (KubeProxyReplacement=True, VXLAN). The two VMs ran on different physical hosts. Measurements were taken while scaling services to 0, 100, 1,000, 5,000, and 10,000.

Measurement kube-proxy (iptables) Cilium (eBPF)
Dataplane metrics at 10,000 services 110,296 NAT rules (~11 lines per service) 40,104 BPF LB entries (~4 entries per service)
Time until a new Service connects via ClusterIP, 0 services Median approx. 0.96s Not remeasured (median 0.24s in kind on the same host, augmented below)
Same latency, 10,000 services Median approx. 2.3s Median 0.31s
New connection average latency increase (from 0 to 10,000 services) Approx. +0.05ms (approx. 15%) Almost none

How to Read.

What These Numbers Don't Tell You.

When the conntrack table fills up. On disposable VMs, nf_conntrack_max was lowered, and 4,000 concurrent loopback connections were opened simultaneously (connection timeout of 5 seconds, waiting before the test until existing entries dropped below 500).

Limit Entries before start Success Failure (failed to establish within 5s) Notes
262,144 (Default) 223 4,000 0 p99 14.5ms
2,048 140 1,913 2,087 Max 3,068ms (Connections that succeeded late due to SYN retransmissions)
1,024 59 969 3,031

Supplemental Practical Measurement: Four dataplaes in kind on the same host. The blanks left by the VM experiment above were filled using a kind cluster (Kubernetes v1.36.1, 1 control plane + 1 worker) inside Docker Desktop on a Mac (Apple M1 Max). We measured kube-proxy's iptables, ipvs, and nftables modes, as well as Cilium 1.20.1 (replacing kube-proxy, with VXLAN), in separate new clusters using the same script. Here, "between nodes" refers to between kind node containers on the same host, so physical NIC and line latency are not included. New connection latency is the median of 6,000 measurements per condition for a new TCP connection → HTTP request → one response. Since the client is written in Python, you should look at the differences between conditions rather than the absolute values.

Measurement iptables ipvs nftables Cilium
New connection, same-node backend (Median / p99) 0.190 / 0.440ms 0.202 / 0.477ms 0.186 / 0.800ms 0.165 / 0.312ms
New connection, different-node backend 0.213 / 0.449ms 0.237 / 0.693ms 0.210 / 0.585ms 0.227 / 0.426ms
New connection median when backends scale from 10 → 1,000 0.281 → 0.307ms 0.269 → 0.292ms 0.290 → 0.350ms 0.317 → 0.300ms
Dataplane entries when there are 1,000 backends 4,073 NAT rules Uncounted Uncounted 1,016 BPF LB entries
Time from 0 services to new Service connection (median of 10 runs) 1.04s 0.12s 1.03s 0.24s
Endpoint change propagation (1 backend, paused for 2s then changed, 5 runs) 7–9ms 31–36ms 6–8ms First run 82ms, rest 474–477ms
Propagation of the revert change immediately after (5 runs) 1.02–1.03s 60–83ms 1.01–1.02s 0.50–0.51s

Raw data and scripts: docs/experiments/2026-10-09-dataplane-scale/.

Lessons Learned from Measurement. In the initial measurements, the connection attempt limit (0.2s) caused Cilium's values to lock into two fixed figures: 206ms and 411ms. Any table where the same values repeat should raise suspicions about the tool's resolution. We reduced the attempt limit to 20ms and re-measured.

6. Considerations When Combining Tools

According to Cilium documentation, if you want to use Istio while Cilium replaces kube-proxy, you need socketLB.hostNamespaceOnly=trueand cni.exclusive=false. Keeping kube-proxy is recommended as the configuration with the fewest changes. For L7 HTTP policies, it is advised to use either Cilium or Istio, but not both. Ambient mTLS traffic is tunneled to port 15008, preventing Cilium policies from seeing the source and L7 information. In this case, Istio handles L4 and higher policies within the mesh.

7. Key Considerations for LLM Serving

클라이언트 -> LB/Gateway -> (Envoy 사이드카 or waypoint) -> 모델 서버(vLLM 등)
 각 홉마다: 요청 타임아웃 / 스트림 유휴 타임아웃 / 연결 유휴 타임아웃 / 드레인

8. Service Discovery, Failure Domains, and Security

Failure domains should be evaluated by dividing them into clusters, network paths (VPN), and control planes. In mixed clusters, use Service trafficDistribution(PreferSameZone) to keep traffic within the same zone and place CoreDNS replicas on both sides (recommended by EKS documentation). For security, do not rely solely on perimeter firewalls; instead, establish inter-service mTLS, cluster-specific intermediate CAs under a common root, and automated certificate rotation tools like cert-manager or SPIRE. On-premise nodes authenticate to EKS using temporary credentials issued by SSM Hybrid Activations or IAM Roles Anywhere instead of long-term keys (EKS documentation). Because ClusterMesh and Istio multi-cluster merge trust domains the moment they connect, a compromise on one side can spread to the other.

Common Pitfalls

The author's experimental environment resides within a single private LAN, meaning there is no WAN, cloud, ClusterMesh, or ambient mesh. The items below summarize experiences from that environment as general cases.

Try It Yourself

All examples below are read-only or executed within temporary namespaces. The commands were not executed and verified while writing this post.

# 읽기 전용: Cilium 설정 확인
kubectl -n kube-system get cm cilium-config -o yaml | grep -E "routing-mode|tunnel-protocol|enable-wireguard|kube-proxy-replacement|cni-exclusive|bpf-lb-sock-hostns-only"
kubectl get gateway,httproute -A

# 임시: 노드 사이 지연과 스트리밍 간격 (정리 포함)
kubectl create ns lab-net
kubectl -n lab-net run a --image=nicolaka/netshoot --overrides='{"spec":{"nodeName":"NODE_A"}}' -- sleep 3600
kubectl -n lab-net run b --image=nicolaka/netshoot --overrides='{"spec":{"nodeName":"NODE_B"}}' -- sleep 3600
kubectl -n lab-net exec a -- ping -c 10 $(kubectl -n lab-net get pod b -o jsonpath='{.status.podIP}')
kubectl delete ns lab-net

For ping results, check the maximum and standard deviation rather than just the average. In the same way, launch an SSE server in a temporary pod to send chunks at 10-second intervals and curl -Nreceive them to see if the service path cuts off the stream. Experiments to change the gateway timeouts.requesttouch the actual service route, so perform them only on a separate test gateway.

Likely Interview Questions

  1. Should we use a single cluster or split them? Split them if you need to isolate blast radii and security boundaries; join nodes into a single control plane, like EKS Hybrid Nodes, if unifying management is crucial. Extending the control plane over a WAN requires matching the etcd timeout to the RTT and designing the failure domain for disconnects.
  2. What network is required for EKS Hybrid Nodes? A private connection to the VPC via VPN or Direct Connect is needed. The node and pod CIDRs must not overlap with each other, the VPC, or the service CIDRs, and the pod CIDR must be routable for webhooks and direct node-to-node communication.
  3. What happens when Cilium and Istio are used together? Cilium handles the CNI, L3/L4 policies, and ingress points, while Istio handles mTLS identities and L7 routing. In kube-proxy replacement mode, socket LB must be restricted to the host namespace and CNI exclusivity should be relaxed. L7 policies should be managed in only one place.
  4. What if LLM streaming cuts off mid-way? Check request, stream idle, and connection idle timeouts at every hop, and identify the layers where logs are recorded. Separate timeouts along the streaming path, and match retries, draining, and health checks to the response length.
  5. What about multi-cluster failures and security? Keep normal traffic local using region affinity, failing over to remote clusters only when all local backends are unhealthy. Because trust domains are merged, connect only clusters with equivalent security levels. Policies and configurations must be deployed per cluster to maintain consistency via GitOps.

Common Misconceptions

References

External Evidence

For claims that could not be measured directly or where measurements were insufficient, the following materials were reviewed:

Claim Source Confirmed Findings
Most sidecar overhead comes from HTTP parsing Zhu et al., Dissecting Service Mesh Overheads (arXiv 2207.00592, 2022, preprint) In Istio 1.13, HTTP mode overhead is about 4x latency and 3x CPU compared to TCP mode, with the majority (62–73%) spent on protocol parsing. TLS encryption costs were not measured separately.
Proxies are more expensive than mTLS encryption Bremler-Barr et al., Performance Comparison of Service Mesh Frameworks: the MTLS Test Case (arXiv 2411.02267, 2024, technical report) Baseline with only mTLS enabled without a mesh shows up to a 3% increase in p99. Istio sidecar throughput remained similar even with mTLS disabled, and TCP mode (which also disables HTTP parsing) was about 5x. Ambient mode showed a much smaller p99 increase. Since the test injected a 200ms delay into the server, the absolute values include saturation effects.
Magnitude of additional latency per request Istio Official Performance Documentation Version 1.16.2 documentation shows two proxies increasing p90 by ~1.7ms and p99 by ~2.7ms (1,000 QPS, 16 connections, mTLS). Version 1.20 documentation shows p90 at ~0.228ms. The +2.2ms in this post is similar to the former value and about 10x larger than the latter. Whether the documentation hardware changed could not be verified, and CPU contention—due to the client, server, and proxy co-locating on a single VM in this post—may be a contributing factor, though it was not validated.
The claim that Ambient is lighter Second paper above, Istio 1.24 documentation At 3,200 QPS with enforced mTLS, p99 latency increase was Ambient +0.03s vs. sidecar +0.37s. ztunnel used ~0.06 vCPU and 12MB vs. sidecar's ~0.20 vCPU and 60MB (at 1,000 QPS). This post did not test Ambient.
kube-proxy synchronization interval Kubernetes Documentation, kube-proxy minSyncPeriod Default is 1 second Matches the ~0.96 seconds with 0 services in this post.
Scale dependency of iptables first-packet latency Huawei 2017 presentation (rule traversal cost based on service count), Kubernetes 1.28 partial updates Because rules are traversed linearly, services later in the chain experience slower first packets. IPVS mode has been deprecated since 1.35, with nftables as the alternative.
WireGuard encryption scope and cost Cilium Documentation (WireGuard Transparent Encryption), WireGuard Paper (NDSS 2017) Default covers only pod traffic managed by Cilium; node traffic is a beta option, and traffic within the same node is unencrypted. Tunnel routing modes incur double encapsulation. The paper reports 1,011 Mbit/s over a 1GbE link (author comparison, link-limited).
Hybrid constraints AWS EKS Hybrid Nodes documentation, Site-to-Site VPN limits documentation 1.25 Gbps limit per tunnel; Cilium is the supported CNI for hybrid nodes, and hybrid nodes incur hourly vCPU billing.
Behavior of conntrack saturation Test in this post (above) External literature was not consulted; replaced by direct measurement.

Verified Versions and Dates

As of 2026-10-05: Kubernetes 1.37.1 (2026-09-23), Cilium 1.20.2 (2026-09-16, 1.21.0-pre.3 exists), Istio 1.31.1 (2026-09-21, 1.31 supports Kubernetes 1.32–1.36), Gateway API v1.6.1 (per Cilium documentation). The actual versions used for measurements were Cilium 1.20.1 and Istio 1.31.0.

Unverified Items: Specific figures in the EKS Hybrid Nodes pricing table, latest supported Cilium version (AWS docs only mention 1.17 and 1.18), products for attaching on-premises nodes to GKE, default encryption policies for Direct Connect, exact behavior during ClusterMesh remote cluster disconnections, the impact of WAN latency on Ambient, and the actual output of the commands above.

Sign in to like

Comments

No comments yet.

Sign in to leave a comment