This issue relates to the PR #266 - I should have opened the issue first instead of the other way around.
Real world impact
We run workloads on Apple Silicon hosts with vfkit VMs. F.ex. BuildKit's cache client inside the guest downloads large cache objects over a shared HTTP/2 connection, the server's PING acks toward the guest get stuck behind the stalled queue, the client's health check times out, and the connection dies with "http2: client connection lost". In our case it always seems to happen during large concurrent downloads. Recovering happens once the guest catches up. It seems that any protocol with a heartbeat shorter than the stall (gRPC keepalives, TLS handshake timeouts, DNS) is affected the same way.
Reproduction
stall-repro.sh
#!/bin/bash
set -eu
vm="${1:?Usage: $0 VM_NAME [STALL_SECONDS]}"
stall="${2:-6}"
pings=$((stall + 6))
ip=$(dscacheutil -q host -a name "$vm-vmnet-helper.local" | awk '/^ip_address/ {print $2; exit}')
helper_log="$HOME/.vmnet-helper/vms/$vm/vmnet-helper.log"
# Virtualization.framework spawns a com.apple.Virtualization.VirtualMachine
# XPC process per VM, and that process holds the vm socket.
# Find ours by its open VM disk.
vz_pid=""
for pid in $(pgrep -f com.apple.Virtualization.VirtualMachine); do
if lsof -p "$pid" 2>/dev/null | grep -q "vms/$vm/disk.img"; then
vz_pid="$pid"
break
fi
done
[ -n "$vz_pid" ] || { echo "no VirtualMachine process for $vm"; exit 1; }
echo "vm=$vm ip=$ip vz=$vz_pid stall=${stall}s"
echo
echo "== ping during a ${stall}s client stall (1 ping per second) =="
ping -i 1 -c "$pings" "$ip" > /tmp/stall-ping.txt &
ping_pid=$!
# Bulk host->vm traffic (~11 Mbit/s UDP) so the stall overflows the vm
# socket receive buffer and write_to_vm() hits ENOBUFS.
python3 - "$ip" $((stall + 4)) <<'EOF' &
import socket, sys, time
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
payload = b"x" * 1400
end = time.time() + float(sys.argv[2])
while time.time() < end:
s.sendto(payload, (sys.argv[1], 9))
time.sleep(0.001)
EOF
blast_pid=$!
sleep 2
echo "-- SIGSTOP VirtualMachine process --"
kill -STOP "$vz_pid"
sleep "$stall"
echo "-- SIGCONT VirtualMachine process --"
kill -CONT "$vz_pid"
wait "$ping_pid" || true
wait "$blast_pid" || true
grep -E "icmp_seq|packet loss|round-trip" /tmp/stall-ping.txt
echo
echo "== helper log during the stall =="
tail -5 "$helper_log"
The script uses SIGSTOP to emulate what we see in our workloads.
In terminal 1
In terminal 2
Output in the broken case
vm=test ip=192.168.64.14 vz=61214 stall=6s
== ping during a 6s client stall (1 ping per second) ==
-- SIGSTOP VirtualMachine process --
-- SIGCONT VirtualMachine process --
64 bytes from 192.168.64.14: icmp_seq=0 ttl=64 time=0.295 ms
64 bytes from 192.168.64.14: icmp_seq=1 ttl=64 time=0.129 ms
Request timeout for icmp_seq 2
Request timeout for icmp_seq 3
Request timeout for icmp_seq 4
Request timeout for icmp_seq 5
Request timeout for icmp_seq 6
64 bytes from 192.168.64.14: icmp_seq=2 ttl=64 time=6012.425 ms
64 bytes from 192.168.64.14: icmp_seq=3 ttl=64 time=5011.270 ms
64 bytes from 192.168.64.14: icmp_seq=4 ttl=64 time=4011.324 ms
64 bytes from 192.168.64.14: icmp_seq=5 ttl=64 time=3006.248 ms
64 bytes from 192.168.64.14: icmp_seq=6 ttl=64 time=2001.181 ms
64 bytes from 192.168.64.14: icmp_seq=7 ttl=64 time=996.461 ms
64 bytes from 192.168.64.14: icmp_seq=8 ttl=64 time=0.192 ms
64 bytes from 192.168.64.14: icmp_seq=9 ttl=64 time=0.366 ms
64 bytes from 192.168.64.14: icmp_seq=10 ttl=64 time=0.287 ms
64 bytes from 192.168.64.14: icmp_seq=11 ttl=64 time=0.778 ms
12 packets transmitted, 12 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 0.129/1753.413/6012.425/2131.011 ms
== helper log during the stall ==
INFO [main] using bulk_forwarding: true
INFO [main] started vmnet interface
INFO [main] started host forwarding
INFO [main] started vm forwarding
INFO [main] waiting for termination
The network looks lossless, but only by having unbounded latency.
Output in the "fixed" case
vm=test ip=192.168.64.14 vz=61174 stall=6s
== ping during a 6s client stall (1 ping per second) ==
-- SIGSTOP VirtualMachine process --
-- SIGCONT VirtualMachine process --
64 bytes from 192.168.64.14: icmp_seq=0 ttl=64 time=0.261 ms
64 bytes from 192.168.64.14: icmp_seq=1 ttl=64 time=0.311 ms
64 bytes from 192.168.64.14: icmp_seq=2 ttl=64 time=0.368 ms
Request timeout for icmp_seq 3
Request timeout for icmp_seq 4
Request timeout for icmp_seq 5
Request timeout for icmp_seq 6
64 bytes from 192.168.64.14: icmp_seq=3 ttl=64 time=5009.535 ms
64 bytes from 192.168.64.14: icmp_seq=4 ttl=64 time=4007.919 ms
64 bytes from 192.168.64.14: icmp_seq=5 ttl=64 time=3004.290 ms
64 bytes from 192.168.64.14: icmp_seq=6 ttl=64 time=1999.251 ms
64 bytes from 192.168.64.14: icmp_seq=7 ttl=64 time=996.027 ms
64 bytes from 192.168.64.14: icmp_seq=8 ttl=64 time=0.204 ms
64 bytes from 192.168.64.14: icmp_seq=9 ttl=64 time=0.211 ms
64 bytes from 192.168.64.14: icmp_seq=10 ttl=64 time=0.258 ms
64 bytes from 192.168.64.14: icmp_seq=11 ttl=64 time=0.624 ms
12 packets transmitted, 12 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 0.204/1251.605/5009.535/1741.061 ms
== helper log during the stall ==
INFO [main] started vm forwarding
INFO [main] waiting for termination
WARN [host->vm] dropped 1 packets (1 total): peer is not reading from the socket
WARN [host->vm] dropped 6 packets (698 total): peer is not reading from the socket
WARN [host->vm] dropped 6 packets (1523 total): peer is not reading from the socket
In the fixed case we can observe the dropped packet count in the logs. Dropping of the packets should cause the sending endpoints to retransmit or maybe even slow down. This is for TCP, I guess for UDP it's up to the application to decide.
"Real world counterparts"
I'm no expert in either hardware networking gear or linux networking, but I guess we could come up with some example "write timeout" or "packet buffer" values from these.
Here's some documentation from Broadcom https://docs.broadcom.com/doc/BC-0503EN
| Chip |
Ports |
Packet buffer (whole chip) |
Time to drain the full buffer at port speed |
| BCM53101 |
5x 100 Mbit |
64 KB |
~5 ms at 100 Mbit/s |
| BCM53125 |
7x 1 GbE |
128 KB |
~1 ms at 1 Gbit/s |
| BCM53128 |
8x 1 GbE |
192 KB |
~1.5 ms |
| BCM5396 |
16x 1 GbE |
256 KB |
~2 ms |
| BCM53158 |
8x 1 GbE + 2x 10 GbE |
1 MB |
~0.8 ms at 10 G, ~8 ms at 1 G |
Linux has
This issue relates to the PR #266 - I should have opened the issue first instead of the other way around.
Real world impact
We run workloads on Apple Silicon hosts with vfkit VMs. F.ex. BuildKit's cache client inside the guest downloads large cache objects over a shared HTTP/2 connection, the server's PING acks toward the guest get stuck behind the stalled queue, the client's health check times out, and the connection dies with "http2: client connection lost". In our case it always seems to happen during large concurrent downloads. Recovering happens once the guest catches up. It seems that any protocol with a heartbeat shorter than the stall (gRPC keepalives, TLS handshake timeouts, DNS) is affected the same way.
Reproduction
stall-repro.shThe script uses SIGSTOP to emulate what we see in our workloads.
In terminal 1
./run testIn terminal 2
./stall-repro.sh test 6Output in the broken case
The network looks lossless, but only by having unbounded latency.
Output in the "fixed" case
In the fixed case we can observe the dropped packet count in the logs. Dropping of the packets should cause the sending endpoints to retransmit or maybe even slow down. This is for TCP, I guess for UDP it's up to the application to decide.
"Real world counterparts"
I'm no expert in either hardware networking gear or linux networking, but I guess we could come up with some example "write timeout" or "packet buffer" values from these.
Here's some documentation from Broadcom https://docs.broadcom.com/doc/BC-0503EN
Linux has
params->target = MS2TIME(5)