Network Policy Scalability: eBPF, Compilation, Cardinality
Network Policy at Large Scale
Network policy provides microsegmentation: control traffic between Pods based on labels.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all
spec:
podSelector: {}
ingress: [] # deny all incoming
egress: [] # deny all outgoingAt 1000+ nodes:
- 100K+ Pods, each subject to policies
- 100+ NetworkPolicy objects
- Egress rules to external networks (CIDR blocks)
Bottleneck: eBPF program generation, policy evaluation per packet.
eBPF-Based Enforcement (Dataplane V2)
Before eBPF (Legacy)
Network policy enforced via iptables rules on each node:
iptables -t filter -A FORWARD \
-s 10.4.0.0/14 \ # Pod CIDR
-d 10.4.0.0/14 \
-j ACCEPTComplexity: Manyiptables rules (1 rule per policy × selector complexity).
Performance: Linear scan through rules (O(rules)) per packet.
With eBPF (Dataplane V2)
Network policy compiled to eBPF bytecode (loaded into kernel):
// Simplified eBPF pseudocode
int policy_ingress(struct __sk_buff *skb) {
struct iphdr *ip = load_header(skb);
struct pod_id *pod = lookup_pod(ip->daddr);
if (pod->labels[app] == "web") {
if (allowed_sources.contains(ip->saddr)) {
return ALLOW;
}
}
return DROP;
}eBPF program JIT-compiled to machine code → O(1) lookups (hash map).
Performance: ~100x faster than iptables.
eBPF Map Limits
Endpoint Count Limit (260K)
Dataplane V2 uses eBPF map (hash map in kernel):
Key: (protocol, service_ip, port, backend_pod_ip)
Value: (backend_pod_ip, backend_port)Max entries: 260K across all services/endpoints.
Implication:
1000 services, avg 260 backends per service = 260K endpoints (max capacity)
If average 100 backends/service, can support 2600 services
If average 500 backends/service, can support 520 servicesAt large scale (100K Pods):
- Few services with many backends (high fan-out) → consume limit fast
- Many services with few backends (low fan-out) → fit within limit
Policy Rules Map Limit
NetworkPolicy rules also map-backed:
Key: (pod_ip, policy_name, rule_index)
Value: (action, target_selectors)Limit: Also 260K-ish per cluster (shared with endpoint limit).
Policy Compilation Overhead
When new NetworkPolicy created/updated:
1. API server receives policy
2. etcd persists
3. Controller (Cilium operator) detects change
4. Compiles policy to eBPF bytecode
5. Loads bytecode into kernel
6. Programs attached to network interfacesCompilation time: 100-500ms per policy (depends on rule complexity).
At scale (100+ policies, frequent changes):
- Compile queue builds up
- Policy update latency increases
- Eventual consistency: new policy visible 1-2 seconds after apply
Example:
kubectl apply networkpolicy rule-1.yaml # t=0
... (policy compiles, loads to eBPF) ... # t=100-500ms
Traffic matching rule-1 blocked (or allowed) # t=500msReal-World Scenario: Policy Explosion
Case: 1000-node cluster, developing microservices. Team creates NetworkPolicy per service (100 services = 100 policies).
Topology:
service-a ↔ service-b ↔ service-c ↔ ... ↔ service-zEach service has egress to 10 others (100 × 10 = 1000 rules).
Timeline:
- Create 100 NetworkPolicy objects (sequential apply)
- Each policy compiled: 1000 × 100ms = 100 seconds
- During compilation, traffic not fully enforced
- After 100 seconds: all policies loaded
- Traffic now microsegmented
Cost:
- CPU on Dataplane (Cilium operator): 20% during compilation
- Memory: policies + eBPF programs = 100-500MB
- Latency: new policies visible after ~500ms
Optimization: Policy Aggregation
Instead of 1 policy per service:
# Before (100 policies)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: service-a-ingress
spec:
podSelector:
matchLabels:
app: service-a
ingress:
- from:
- podSelector:
matchLabels:
app: service-b
- from:
- podSelector:
matchLabels:
app: service-c
# ... 8 more sources# After (1 policy)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: core-services
spec:
podSelector:
matchLabels:
tier: core
ingress:
- from:
- podSelector:
matchLabels:
tier: coreResult: Fewer policies → fewer compilation cycles → less overhead.
Egress Policy Complexity
Egress to external (non-cluster) networks adds complexity:
spec:
egress:
- to:
- namespaceSelector: {}
- podSelector: {}
ports:
- protocol: TCP
port: 443
- to:
- ipBlock:
cidr: 0.0.0.0/0 # any IP
except:
- 169.254.169.254/32 # block metadata service
ports:
- protocol: TCP
port: 443Compilation: Multiple CIDR blocks = multiple eBPF rules (O(CIDR count)).
Best practice: Aggregate CIDR blocks, use fewest rules.
Diagnosis: Network Policy Bottleneck
Metrics:
cilium_policy_regeneration_total # policy compile count
cilium_policy_regeneration_time_bucket # compile latencyWarning signs:
- Policy compile latency >1 second
- Cilium operator CPU saturated
- New policies not visible 10+ seconds after apply
Remediation:
- Simplify policies (fewer rules, aggregate selectors)
- Batch policy updates (apply multiple policies, single compile cycle)
- Scale Cilium operator (more replicas)
- Review policy necessity (might not need all policies)