Skip to content

NS-3 Script

kns-3-sim.cc is an NS-3 network simulation that models the communication performance of a Kubernetes cluster. For a candidate pod placement node, it simulates how a newly placed pod (the client) would reach all existing services running on other nodes (the servers). Each node is scored based on the simulated network metrics, and the results are returned to the KNS-3 scheduler to guide its placement decision.

  • Directorykns-3-sim/
    • kns-3-sim.cc Main simulation logic, entry point
    • kns-3-sim.h Struct definitions
    • Directorylib/
      • json.h JSON parsing / serialisation helpers
      • json.cc
      • Directorynlohmann/
        • json.hpp nlohmann/json single-header library

Represents a single Kubernetes worker node. Populated from the JSON input provided by the FastAPI service.

Field Type Description
nodeName string Kubernetes node hostname
latency double Round-trip latency in milliseconds
packetLoss double Packet loss ratio (0.0 – 1.0)
bandwidth double Raw interface capacity in bytes/sec
congestion double Current transmit rate in bytes/sec

Controls how the four metrics contribute to the final placement score. All fields are required in the JSON if weights is present; otherwise defaults are used:

Weight Default Description
latency 0.40 Higher weight = latency matters more
throughput 0.25
packetLoss 0.20
utilization 0.15

Weights are normalised at scoring time so they do not need to sum to exactly 1.0.

Controls the normalisation range for each metric. Values outside their bound are clamped to 0 or 100. Defaults:

Field Default Description
maxLatency 500.0 Latency (ms) at which latency score reaches 0
maxThroughput 100.0 Throughput (Mbps) at which throughput score is 100
maxPacketLoss 0.10 Packet loss ratio at which packet loss score is 0
maxUtilization 0.50 Utilisation ratio at which utilisation score is 0

Top-level input parsed from JSON:

Field Required Default Description
nodes Yes - List of NodeMetrics (maximum 30)
simulationTime No 10 Simulation duration in seconds
podPlacementNode No "" If set, only this node is evaluated; otherwise all nodes are tested
weights No see above Scoring weights (ScoringWeights)
bounds No see above Normalisation bounds (ScoringBounds)

Produced per evaluated node:

Field Description
nodeName Node hostname
avgLatency Mean per-packet latency in ms
minLatency Minimum observed latency in ms
maxLatency Maximum observed latency in ms
packetLossRate Fraction of transmitted packets that were lost
throughput Throughput in Mbps
avgUtilization Mean link utilisation across all paths
packetsSent Total packets transmitted
packetsReceived Total packets received
score Composite placement score (0 – 100)
success Whether the simulation completed without error
error Error message if success is false

The simulation uses a full-mesh point-to-point topology. Every node has a direct link to every other node. This results in O(N²) links but provides the most realistic representation of intra-cluster pod-to-pod reachability.

Each link is configured independently using the metrics of the two connected nodes:

Link Parameter Derivation
Delay Average of both nodes’ latency values (converted to ms)
Data Rate Minimum bandwidth of the two nodes × available fraction
Packet Loss Maximum packet loss of the two nodes (conservative)
Queue Size Bandwidth-delay product / 1500 bytes, minimum 50 packets, scaled up under congestion
utilization = min(congestion / bandwidth, 0.95)
availableBandwidth = bandwidth × (1 − utilization)

A 5% floor is enforced (0.05 × bandwidth) so links do not become completely saturated in the model.

Subnets are allocated sequentially in the 10.x.y.0/24 space, one per link. Routing tables are populated using Ipv4GlobalRoutingHelper::PopulateRoutingTables().

Pod Placement Node → (client) → all other nodes
All other nodes → (server) → receives traffic

The placement candidate acts as the client (new pod), and every other node acts as a server (existing service, database, API, cache, etc.). This models whether a newly placed pod can efficiently reach all services already running in the cluster.

  • Protocol: UDP echo (UdpEchoServer / UdpEchoClient)
  • Packet size: 1024 bytes
  • Send interval: 10 ms (100 packets/second per server)
  • Per-server start time: staggered by 2.0 + i × 0.1 seconds to avoid a simultaneous burst
  • Max packets: computed from runTime / interval, floored at 100

An ns3::RateErrorModel is attached to both ends of each link when the computed packetLoss > 0. The error unit is per-packet, so the error rate maps directly to the Prometheus-derived packet loss ratio.

NS-3’s FlowMonitor is installed across all nodes and polled after Simulator::Run() completes.

Aggregation over all flows:

avgLatency = sum(flow.delaySum × flow.rxPackets) / totalRxPackets [ms]
packetLossRate = totalLostPackets / totalTxPackets
throughput = (totalRxPackets × 1024 × 8) / (simulationTime × 1e6) [Mbps]
avgUtilization = mean(congestion_i / bandwidth_ij) across all server paths

CalculatePlacementScore() converts raw metrics into a single 0–100 score. The normalisation ranges are configurable via ScoringBounds (defaults shown):

Metric Direction Default range Formula
Latency Lower better 0 – 500 ms 100 × max(0, 1 − latency / maxLatency)
Throughput Higher better 0 – 100 Mbps min(100, throughput / maxThroughput × 100)
Packet Loss Lower better 0 – 10% 100 × max(0, 1 − lossRate / maxPacketLoss)
Utilization Lower better 0 – 50% 100 × max(0, 1 − utilization / maxUtilization)
score = (latencyScore × w_latency)
+ (throughputScore × w_throughput)
+ (packetLossScore × w_packetLoss)
+ (utilizationScore × w_utilization)

Weights are normalised before use:

w_latency = latency / (latency + throughput + packetLoss + utilization)

The binary accepts the following command-line arguments:

Parameter Default Description
--jsonInput required JSON input string, or - to read from stdin
--jsonOutput true Emit results as JSON (set to false for human-readable output)
--seed 1 RNG seed for reproducible packet loss simulation

The FastAPI service always passes --jsonInput=- and pipes the request JSON to stdin.

main()
└── Set RNG seed (--seed)
└── ParseJsonInput() # Deserialise JSON from stdin or --jsonInput arg
└── RunSimulation() # Loop over candidate nodes (max 30)
└── RunSimulationForPlacement() # Full NS-3 sim for one placement
├── Create NodeContainer
├── Install InternetStack
├── Build full-mesh P2P topology
├── Apply RateErrorModel per link
├── Assign 10.x.y.0/24 subnets
├── PopulateRoutingTables
├── Install UdpEchoServer on all non-placement nodes
├── Install UdpEchoClient on placement node
├── Install FlowMonitor
├── Simulator::Run()
└── Collect & aggregate FlowMonitor stats
└── CalculatePlacementScore()
└── Sort results by score (descending)
└── GenerateJsonOutput() # Emit JSON to stdout
{
"nodes": [
{
"name": "worker-node-1",
"latency": 10.0,
"packetLoss": 0.001,
"bandwidth": 1250000000,
"congestion": 48000000
},
{
"name": "worker-node-2",
"latency": 15.0,
"packetLoss": 0.002,
"bandwidth": 1250000000,
"congestion": 60000000
}
],
"simulationTime": 20,
"podPlacementNode": "worker-node-1",
"weights": {
"latency": 0.4,
"throughput": 0.25,
"packetLoss": 0.2,
"utilization": 0.15
},
"bounds": {
"maxLatency": 500,
"maxThroughput": 100,
"maxPacketLoss": 0.1,
"maxUtilization": 0.5
}
}
{
"success": true,
"performance": {
"simulationDuration": 1.42,
"nodesEvaluated": 2,
"weights": {
"latency": 0.4,
"throughput": 0.25,
"packetLoss": 0.2,
"utilization": 0.15
},
"bounds": {
"maxLatency": 500.0,
"maxThroughput": 100.0,
"maxPacketLoss": 0.1,
"maxUtilization": 0.5
}
},
"results": [
{
"nodeName": "worker-node-1",
"metrics": {
"avgLatency": 12.3,
"minLatency": 8.5,
"maxLatency": 19.4,
"packetLossRate": 0.002,
"throughput": 43.1,
"avgUtilization": 0.038,
"packetsSent": 950,
"packetsReceived": 948,
"score": 84.7
}
}
]
}

Results are ordered by score descending. The FastAPI service deserialises this output directly into a SimulationResponse.

The simulation is built with CMake against an NS-3 release pinned, with its checksum, in simulator/ns3.env. From simulator/:

Terminal window
make build # fetch + verify + configure ns-3 once, then build kns-3-sim
export NS3_SCRIPT=$(make -s ns3-script)

The binary is installed to simulator/bin/kns-3-sim. The Docker build runs the same scripts/ns3.sh with the same configure flags, and installs the binary at /code/kns-3-sim inside the container.