Problem – Milvus ReplicaSet Scaling Failure Under High‑Concurrency Benchmark
During a distributed performance benchmark of Milvus 2.x, the team attempted to increase the replicaCount of the QueryNode (and optionally the Proxy) to meet a target query throughput of ~10 k QPS. The scaling operation consistently failed:
- New QueryNode pods remained in
Pendingor enteredCrashLoopBackOff. - Existing pods began emitting gRPC errors such as:
rpc error: code = Unavailable desc = connection refused etcdserver: request timed out Failed to sync replica set: context deadline exceeded panic: runtime error: invalid memory address or nil pointer dereference - Overall benchmark throughput dropped sharply, and latency spikes appeared.
This article walks through the root cause analysis, systematic debugging steps, a concrete resolution, validation procedures, and preventive measures.
Root Cause – Interaction of Resource Limits, etcd quorum, and Proxy gRPC Saturation
Milvus consists of three core components that are each deployed as a Kubernetes Deployment with a ReplicaSet:
- Proxy – front‑end gRPC gateway that multiplexes client connections.
- QueryNode – executes vector search; each replica loads the full index into memory.
- DataNode – handles data ingestion and persistence.
According to the Milvus 2.x Kubernetes Deployment Guide, scaling a component requires:
- Adjusting
replicaCountin the Helm chart. - Ensuring sufficient
resources.limits(CPU, memory) for the workload. - Maintaining etcd quorum (default 3 members) for metadata coordination.
Three intertwined failures were observed in the benchmark environment (see the “Real incidents” evidence):
- Insufficient memory limits – Each QueryNode loads a large IVF‑PQ index (~12 GB). The default
resources.limits.memoryof 8 GiB caused OOM kills when the replica count doubled, leading toKubelet failed to start container for milvus‑querynode: OOMKilled. - etcd leader election stalls – Adding eight QueryNode pods increased the number of simultaneous writes to etcd (metadata updates for node registration). With the default etcd pod CPU request (100 m) and no dedicated etcd node pool, the etcd leader could not keep up, producing
etcdserver: request timed outacross all services. - Proxy gRPC connection pool exhaustion – The Proxy’s
grpc.maxConcurrentStreamsdefault (100) and its internal connection pool were sized for a modest number of back‑ends. When new QueryNode pods appeared, the Proxy attempted to open new streams for each, saturating the pool and returningrpc error: code = Unavailable desc = connection refusedfor inbound client requests.
These conditions collectively prevented the ReplicaSet controller from reaching the desired replica count, manifesting as the “Failed to sync replica set: context deadline exceeded” error.
Debug – Systematic Investigation Steps
1. Verify Helm values and pod status
helm get values milvus -n milvus | grep replicaCount
kubectl get pods -n milvus -l app=milvus-querynode
kubectl describe pod <new‑querynode‑pod> -n milvus
Typical output showing pending state:
Name: milvus-querynode-6f7c9b8d5c-abcde
Namespace: milvus
Status: Pending
Reason: Unschedulable
Message: 0/10 nodes are available: 10 Insufficient memory.
2. Inspect OOM events and container logs
kubectl logs milvus-querynode-6f7c9b8d5c-abcde -n milvus --previous
kubectl get events -n milvus --field-selector reason=OOMKilled
Sample log snippet:
2024-03-12T10:15:32Z FATAL: out of memory: kill process 12345 (milvus) score 987 or sacrifice child
3. Check etcd health
kubectl exec -it milvus-etcd-0 -n milvus -- etcdctl endpoint health
kubectl exec -it milvus-etcd-0 -n milvus -- etcdctl endpoint status --write-out=table
Output indicating timeouts:
https://milvus-etcd-0.milvus.svc:2379 is unhealthy: request timed out
4. Examine Proxy gRPC metrics
Milvus exposes Prometheus metrics at /metrics. Query the relevant series:
curl -s http://milvus-proxy:9091/metrics | grep grpc_server_handled_total
curl -s http://milvus-proxy:9091/metrics | grep grpc_server_streams_active
Metrics showed a spike in active streams reaching the configured limit:
# HELP grpc_server_streams_active Number of active gRPC streams.
grpc_server_streams_active{service="proxy"} 102
5. Review Helm chart defaults (official docs)
The Milvus 2.x Kubernetes Deployment Guide lists the default values:
| Component | Default replicaCount | Default memory limit |
|---|---|---|
| QueryNode | 3 | 8Gi |
| Proxy | 2 | 4Gi |
| etcd | 3 | 2Gi |
Solution – Adjust Resources, Etcd Sizing, and Proxy gRPC Settings
1. Increase QueryNode memory limits to accommodate the full index
# values.yaml (before)
queryNode:
resources:
limits:
memory: 8Gi
requests:
memory: 6Gi
# values.yaml (after)
queryNode:
resources:
limits:
memory: 16Gi # enough for 12 Gi index + overhead
requests:
memory: 12Gi
2. Expand etcd CPU and enable a dedicated node pool
# values.yaml (etcd section before)
etcd:
resources:
limits:
cpu: "0.5"
requests:
cpu: "0.2"
# after – allocate more CPU and pin to etcd‑node pool
etcd:
nodeSelector:
nodepool: etcd
resources:
limits:
cpu: "2"
requests:
cpu: "1"
Deploy a separate node pool with SSD‑backed storage to avoid I/O contention.
3. Raise Proxy gRPC concurrency limits
# values.yaml (proxy section before)
proxy:
grpc:
maxConcurrentStreams: 100
# after – increase to match expected back‑end count
proxy:
grpc:
maxConcurrentStreams: 500
resources:
limits:
memory: 8Gi
requests:
memory: 6Gi
4. Enforce pod anti‑affinity for QueryNode replicas
# values.yaml (queryNode anti‑affinity)
queryNode:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values: ["milvus-querynode"]
topologyKey: "kubernetes.io/hostname"
This prevents the scheduler from placing multiple QueryNode pods on the same physical host, avoiding network saturation and “context deadline exceeded” errors.
5. Apply the updated Helm chart
helm upgrade milvus milvus/milvus -n milvus -f values.yaml
Watch the rollout:
kubectl rollout status deployment/milvus-querynode -n milvus
kubectl get pods -n milvus -l app=milvus-querynode
Verify – Confirm That Scaling Now Succeeds
- Replica count:
kubectl get rs -n milvus -l app=milvus-querynodeshould show the desiredDESIREDreplicas matchingAVAILABLE. - Memory usage:
kubectl top pod -n milvus -l app=milvus-querynodeshould report < 80 % of the 16 Gi limit. - etcd health: Re‑run
etcdctl endpoint health– all members return “healthy”. - Proxy gRPC streams: Verify
grpc_server_streams_activestays well below the new limit (e.g., 200 vs 500). - Benchmark throughput: Rerun the query load (10 k QPS) and observe latency < 5 ms and error rate < 0.1 %.
Prevent – Operational Guardrails for Future Scaling
- Capacity planning: Use the Milvus Performance Tuning Documentation to size CPU/memory per index size. Allocate at least 1.5× the index memory per QueryNode.
- Etcd sizing checklist: Ensure etcd CPU requests ≥ 1 core per member and enable dedicated node pools for high‑throughput workloads.
- Proxy tuning: Set
grpc.maxConcurrentStreamstoreplicaCount × 50as a rule of thumb, and monitor thegrpc_server_streams_activemetric. - Pod anti‑affinity policies: Enforce host‑level anti‑affinity for all Milvus back‑end components to avoid single‑point network bottlenecks.
- Automated health checks: Add liveness/readiness probes that verify gRPC health (e.g.,
grpc_health_probe) and etcd health endpoints; configure alerts on OOMKilled events and etcd request timeouts.
FAQ – Common Follow‑Up Questions
- Why does scaling work in dev but fail in the benchmark cluster?
Because the dev cluster runs with a small index (≈2 Gi) and ample resources, while the benchmark loads a 12 Gi index on each QueryNode. The default memory limits and etcd CPU are insufficient for the larger workload. - Can I keep the default
grpc.maxConcurrentStreamsand still scale?
Only if you also increase the Proxy replica count proportionally. The limit is per Proxy instance, so adding more Proxy pods spreads the stream load. - Do I need to restart the entire Milvus deployment after changing etcd resources?
No. Updating the etcd Deployment with a rolling restart (viahelm upgrade) is sufficient; the cluster will re‑elect a leader without full downtime. - How do I know the appropriate memory limit for a QueryNode?
Measure the size of the loaded index (e.g., viadu -sh /var/lib/milvus/indexes) and add 30‑40 % headroom for working set, cache, and Go runtime overhead. - What alert thresholds should I set for etcd timeouts?
Trigger an alert whenetcd_server_leader_changes_seen_totalincrements rapidly or whenetcd_server_failed_requests_totalexceeds 5 per minute.
Related Topic Hub: Vector Databases Troubleshooting Hub