1import * as _ from 'lodash-es' 2import { CoreV1Api } from '@kubernetes/client-node' 16type CliActionProps = { cluster_name: string, args: string[], kubeConfigPath?: string } 17type CliAction = { (props: CliActionProps): Promise<void>, cliDescript: string } 19const iperfImage = 'networkstatic/iperf3:latest' 21const iperfDurationSec = 30 22const podReadyTimeoutMs = 120_000 23const imageFailReasons = ['ErrImagePull', 'ImagePullBackOff'] 25const parseIperfGbps = (output: string): number => { 26 const match = output.match(/([\d.]+)\s+(Gbits|Mbits)\/sec.*sender/) 28 const valStr = match[1], unit = match[2] 29 const bitsPerSec = parseFloat(valStr) * (unit === 'Mbits' ? mbit : gbit) 30 return bitsPerSec / gbit 33export const netbench: CliAction = async ({cluster_name}) => { 34 const runId = Date.now().toString(36) + crypto.randomUUID().slice(0, 4) 37 const api = kubeConfig.makeApiClient(CoreV1Api) 38 const {items: nodes} = await api.listNode() 39 const nodeNames = _.chain(nodes) 40 .filter(node => node.status?.conditions?.some(c => c.type === 'Ready' && c.status === 'True') === true) 41 .map(node => node.metadata?.name) 44 throwIf(() => nodeNames.length < 2, {cluster_name}) 46 const mkNetbenchJob = async ({name, node, taskCmd}: {name: string, node: string, taskCmd: string}) => 48 jobType: {kind: 'Job', backoffLimit: 0, nodeSelector: {'kubernetes.io/hostname': node}}, 49 name, cluster_name, image: iperfImage, taskCmd, envLocal: {}, git_sha: runId, 52 const waitForPodRunning = async ({name}: {name: string}) => { 54 predicate: async () => { 57 if (status?.waitingReason && imageFailReasons.includes(status.waitingReason)) throwDebugH({name, waitingReason: status.waitingReason}) 58 return !status?.running 60 timeoutLabel: `netbench pod ${name}`, 61 timeoutMs: podReadyTimeoutMs, 67 const podName = pod.metadata?.name 69 const podIp = pod.status?.podIP 71 return {podName, podIp} 74 const cleanupPods = async ({name}: {name: string}) => { 76 if (!pods.length) return 80 const measureDirection = async ({source, target}: {source: string, target: string}): Promise<number> => { 81 const serverName = `netbench-${runId}-${target}-srv` 82 const clientName = `netbench-${runId}-${source}-cli` 83 const serverJob = await mkNetbenchJob({name: serverName, node: target, taskCmd: `while true; do iperf3 -s -p ${iperfPort}; done`}) 84 const clientJob = await mkNetbenchJob({name: clientName, node: source, taskCmd: 'sleep 3600'}) 86 await resourcesActionLite({resources: [serverJob, clientJob], action: 'apply', cluster_name, isQuiet: true}) 87 const {podIp: serverIp} = await waitForPodRunning({name: serverName}) 88 const {podName: clientPodName} = await waitForPodRunning({name: clientName}) 90 podName: clientPodName, containerName: clientName, isQuiet: true, 91 cmdA: ['iperf3', '-c', serverIp, '-p', String(iperfPort), '-t', String(iperfDurationSec), '-f', 'g', '--connect-timeout', '5000'], 93 return parseIperfGbps(output) 95 await resourcesActionLite({resources: [serverJob, clientJob], action: 'delete', cluster_name, isQuiet: true}) 96 await cleanupPods({name: serverName}) 97 await cleanupPods({name: clientName}) 101 const uniqPairs: [string, string][] = [] 102 for (let i = 0; i < nodeNames.length; i++) { 103 for (let j = i + 1; j < nodeNames.length; j++) { 104 uniqPairs.push([nodeNames[i], nodeNames[j]]) 108 console.log(`netbench: ${uniqPairs.length} pairs across ${nodeNames.length} nodes (${iperfDurationSec}s each)`) 109 const failures: string[] = [] 110 for (const [a, b] of uniqPairs) { 111 const results: string[] = [] 112 for (const [source, target] of [[a, b], [b, a]] as [string, string][]) { 114 const gbps = await measureDirection({source, target}) 115 results.push(`${source} -> ${target} ${gbps.toFixed(1)} Gbps`) 117 const msg = err instanceof Error ? err.message : String(err) 118 results.push(`${source} -> ${target} FAILED: ${msg}`) 119 failures.push(`${source} -> ${target}: ${msg}`) 122 console.log(results.join(', ')) 126netbench.cliDescript = 'measure pod-network bandwidth between all node pairs'