containerized-data-importer: CDI import from URL is significantly slower than a manual wget + virtctl image-upload
What happened: There is a massive performance variance between CDI image import methods which makes the declarative workflow for PVCs/DataVolumes unusable. I’m REALLY trying to make Kubevirt work as a replacement for qemu wrapped in bash but the slowness of drive creation is making it impossible to achieve buy-in from others.
You can find my full setup here: https://github.com/cloudymax/argocd-nvidia-lab/tree/main/kubevirt
Disk configs are specifically here: https://github.com/small-hack/argocd/tree/main/kubevirt/disks
CDI configuration is here: https://github.com/small-hack/argocd/tree/main/kubevirt/cdi
This has been tested across multiple hardware types:
-
Hetzner AX102 Instance with R97950X3D + NVME (Raid0)
-
Dell XPS with i7-11700 + NVME (no raid)
-
Hetzner instance with i7 7700 + SATA6 SSDs (Raid0)
-
Has been tested on K3s (bare-metal and in a KVM VPS)
-
Has been tested with local-path and Longhorn storage classes
-
The CDI has been tested with nodePort and LoadBalancer type
-
I have increased the resources available to the CDI importer but it had no effect, the process will not consume more resources.
Method: wget + virtctl Storage Class: local-path Time: 22 seconds
Method: wget + virtctl Storage Class: longhorn Time: 56 seconds
Command used:
export VOLUME_NAME=debian12-pvc
export NAMESPACE="default"
export STORAGE_CLASS="longhorn"
export ACCESS_MODE="ReadWriteMany"
export IMAGE_URL="https://cloud.debian.org/images/cloud/bookworm/daily/latest/debian-12-generic-amd64-daily.qcow2"
export IMAGE_PATH=debian-12-generic-amd64-daily.qcow2
export VOLUME_TYPE=pvc
export SIZE=120Gi
export PROXY_ADDRESS=$(kubectl get svc cdi-uploadproxy-loadbalancer -n cdi -o json | jq --raw-output '.spec.clusterIP')
# $(kubectl get svc cdi-uploadproxy -n cdi -o json | jq --raw-output
time wget -O $IMAGE_PATH $IMAGE_URL && \
time virtctl image-upload $VOLUME_TYPE $VOLUME_NAME \
--size=$SIZE \
--image-path=$IMAGE_PATH \
--uploadproxy-url=https://$PROXY_ADDRESS:443 \
--namespace=$NAMESPACE \
--storage-class=$STORAGE_CLASS \
--access-mode=$ACCESS_MODE \
--insecure --force-bind
When using the declarative method the process takes over 20 minutes
Logs attached: Explore-logs-2023-07-15 10 36 08.txt
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: "debian"
labels:
app: containerized-data-importer
annotations:
cdi.kubevirt.io/storage.bind.immediate.requested: "true"
cdi.kubevirt.io/storage.import.endpoint: "https://cloud.debian.org/images/cloud/bookworm/daily/latest/debian-12-generic-amd64-daily.qcow2"
spec:
storageClassName: local-path
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 120Gi
What you expected to happen:
I would expect this process to be MUCH faster. For example, when performing this task using just plain QEMU + Bash it takes only a couple seconds, and the VM is booted and ready to login in ~30 seconds total on the same hardware mentioned above.
Source: https://github.com/cloudymax/Scrap-Metal/blob/main/virtual-machines/vm.sh
> bash vm.sh create-cloud-vm test
[2023-07-15 10:55:18] 📞 Setting networking options.
[2023-07-15 10:55:18] - Static IP selected.
bridge exists
tap0 exists.
[2023-07-15 10:55:18] j🖥 Set graphics options based on gpu presence.
[2023-07-15 10:55:18] - GPU attached
[2023-07-15 10:55:18] 📂 Creating VM directory.
[2023-07-15 10:55:18] - Done!
[2023-07-15 10:55:18] 🔐 Create an SSH key for the VM admin user
[2023-07-15 10:55:20] - Done.
[2023-07-15 10:55:20] ⬇️ Downloading cloud image...
debian-12-generic-amd64 100%[===============================>] 361.08M 67.0MB/s in 8.3s
[2023-07-15 10:55:29] 📈 Expanding image
[2023-07-15 10:55:29] - Done!
[2023-07-15 10:55:29] 👤 Generating user data
[2023-07-15 08:55:29] 🔎 Checking for required utilities...
[2023-07-15 08:55:29] - All required utilities are installed.
[2023-07-15 08:55:29] 📝 Creating user-data file
[2023-07-15 08:55:29] 📝 Checking against the cloud-inint schema...
[2023-07-15 08:55:29] Valid cloud-config: user-data.yaml
[2023-07-15 08:55:29] - Done.
[2023-07-15 10:55:30] 🌱 Generating seed iso containing user-data
[2023-07-15 10:55:30] - Done!
[2023-07-15 10:55:30] 🌥 Creating cloud-image based VM
[2023-07-15 10:55:30] Watching progress:
[2023-07-15 10:55:54] - Cloud-init complete.. 22.4 -----+ 3:ce:10:8c:52:c8:eb:9
download_cloud_image(){
log "⬇️ Downloading cloud image..."
wget -c -O "$CLOUD_IMAGE_NAME" "$CLOUD_IMAGE_URL" -q --show-progress
}
# Expand the size of the disk image
expand_cloud_image(){
log "📈 Expanding image"
export CLOUD_IMAGE_FILE_TYPE=$(echo "${CLOUD_IMAGE_NAME#*.}")
case $CLOUD_IMAGE_FILE_TYPE in
"img")
echo "img"
qemu-img create -b ${CLOUD_IMAGE_NAME} -f qcow2 \
-F qcow2 disk.qcow2 \
"$DISK_SIZE" 1> /dev/null
;;
"qcow2")
echo "qcow2"
qemu-img create -b ${CLOUD_IMAGE_NAME} -f qcow2 \
-F qcow2 disk.qcow2 \
"$DISK_SIZE" 1> /dev/null
;;
*)
echo "error"
exit
esac
log " - Done!"
}
...
# start the cloud-init backed VM
create_ubuntu_cloud_vm(){
log "🌥 Creating cloud-image based VM"
if tmux has-session -t "${VM_NAME}_session" 2>/dev/null; then
echo "session exists"
else
tmux new-session -d -s "${VM_NAME}_session"
tmux send-keys -t "${VM_NAME}_session" "sudo qemu-system-x86_64 \
-machine accel=kvm,type=q35 \
-cpu host,kvm="off",hv_vendor_id="null" \
-smp $SMP,sockets="$SOCKETS",cores="$PHYSICAL_CORES",threads="$THREADS",maxcpus=$SMP \
-m "$MEMORY" \
$VGA_OPT
$PCI_GPU
$NETDEV
$DEVICE
-object iothread,id=io1 \
-device virtio-blk-pci,drive=disk0,iothread=io1 \
-drive if=none,id=disk0,cache=none,format=qcow2,aio=threads,file=disk.qcow2 \
-drive if=virtio,format=raw,file=seed.img,index=0,media=disk \
-bios /usr/share/ovmf/OVMF.fd \
-usbdevice tablet \
-vnc $HOST_ADDRESS:$VNC_PORT \
$@" ENTER
watch_progress
fi
}
Environment:
- CDI version (use
kubectl get deployments cdi-deployment -o yaml): v1.54.3 - Kubernetes version (use
kubectl version): v1.27.3+k3s1 - DV specification: loacal-path and Longhorn
- Cloud provider or hardware configuration: None - bare metal
- OS (e.g. from /etc/os-release): Debian12 and Ubuntu 22.04
- Kernel (e.g.
uname -a): 6.1 and 5.15 - Install tools: N/A
- Others: N/A
About this issue
- Original URL
- State: closed
- Created a year ago
- Comments: 52 (14 by maintainers)
@cloudymax Just released 1.57.0 which should not require the ‘cert’ hack shown above. It should be similar speed.
nbdkit changes are upstream in 1.35.8, mainly this commit: https://gitlab.com/nbdkit/nbdkit/-/commit/a74b289ee15a7c75dceb8a96403f1fa8ebd72e88
I did some tests with the URLs under discussion and we are better than wget for the Debian and Ubuntu cases. The Windows URL is weird, more below.
https://cloud-images.ubuntu.com/lunar/current/lunar-server-cloudimg-amd64.img (HTTP/1.1)
nbdkit before change: 180s nbdkit after change, connections=16: 42.4s nbdkit after change, connections=32: 21.6s wget: 20.2s nbdkit after change, connections=64: 17.2s
https://gemmei.ftp.acc.umu.se/images/cloud/bookworm/daily/latest/debian-12-generic-amd64-daily.qcow2 (HTTP/1.1)
nbdkit before change: 100.6s nbdkit after change, connections=16: 23.7s wget: 20.9s nbdkit after change, connections=32: 14.0s nbdkit after change, connections=64: 9.1s
https://www.itechtics.com/?dl_id=173 This URL redirects twice, one to windowstan.com which is proxied by cloudflare, and a second time to software.download.prss.microsoft.com. Unfortunately the second redirect is not an actual HTTP redirect, but a message in the body of the 502 error message that cloudflare frequently sends if you attempt any automated download. So I did these tests using the microsoft.com URL directly. The microsoft site uses HTTP/2.
nbdkit before change if you use the windowstan.com URL: over 80 minutes, and very unreliable nbdkit before change if you use the microsoft.com URL: 3m03 nbdkit after change, connections=1: 3m24 nbdkit after change, connections=2: 3m05 nbdkit after change, connections=16: 3m04 wget: 1m04
Some tips:
ipresolve=v4to force IPv4 which will save a bit of time.connectionsparameter controls the number of HTTP connections to the remote server. Reducing will put less load on / more friendly to the server, increasing will make things faster. Default is 16.Oh and we found and fixed an actual bug in curl: https://github.com/curl/curl/pull/11557
I reimplemented the plugin using curl “multi” handle support, and the results are way better: https://listman.redhat.com/archives/libguestfs/2023-July/032167.html (see patch 2 for some test timings)
Quick question: Does the container environment have broken IPv6? The Ubuntu URL resolves to IPv6 then IPv4, and curl tries the IPv6 addresses first. In the Beaker test environment IPv6 is broken and this causes a delay. I added a feature upstream to force IPv4 which improves performance.
Anyway, I’m on holiday today, will take another look at this tomorrow.
I eliminated nbdkit from the equation and I am running the go http client to directly get the data from inside the pod, and it was still slow until I increased the amount of memory available to the pod, so I used this yaml to create the DataVolume
And I increased the amount of memory allocated to the CDI importer pod by modifying the CDI CR to look like this
In particular note the
podResourceRequirementswhich increased the amount of memory and cpu available. I went a little overboard, it doesn’t require that much, but apparently the defaults are just not enough. This allowed me to import using the http go client into the scratch space, and convert from there. It took maybe a minute if that on my local machine which a 1G fiber connection. TheemptyCertis to trick CDI into thinking it cannot use nbdkit and fall back to the scratch space method which is a straight http download and then convert.CDI runs inside a container on the host. So I just double checked, it looks like 1.57 is running 1.34 nbdkit, and 1.56 is running 1.30. And I think we need to add a plugin to 1.34 that is not needed in 1.30.
@cloudymax could you try 1.56 instead of 1.57-rc1 to see if it works better?
nbdkit is even faster this morning …
I should note that I’m running a slightly modified nbdkit. The curl plugin uses a parallel thread model which seems to make it a bit faster. And I gave in and wrote a qcow2 filter.
The two issues are overlapping but slightly distinct. In BZ 2013000 / #1981, the problem was with Fedora redirects being flaky, sometimes resolving to a 404 mirror. We decided in the end to solve that by using the nbdkit-retry-request-filter which basically retries single requests until we get a good mirror. In that case pre-resolving the URL was thought to be bad (because we might pre-resolve it to a 404, and then it wouldn’t work at all) and so #1981 was not merged. In this case the Debian mirrors all appear to be good (ie they don’t resolve to 404s that I’ve seen), but it’s the issue that we’re doing the resolves on every block (curl internally doesn’t cache them) and they happen to be especially slow on the central Debian server, so pre-resolving would be a good thing.
I’ve finally worked out what’s going on here. About 3/4 of the time is spent doing 302 redirects from the orignal URL located on
cloud.debian.orgto the mirror (which for me happens to besaimei.ftp.acc.umu.se, but I guess that depends where you are located in the world).The way curl works is it doesn’t cache 302 redirects, so basically on every single block that we request we’re doing a whole new redirect.
If you “manually” resolve the redirect first then for me it is ~ 4 times faster, and near the speed of plain wget.
It turns out we’ve actually hit this bug before in a slightly different context:
https://bugzilla.redhat.com/show_bug.cgi?id=2013000
I proposed a patch for that bug, adding an
effective-url=trueflag which would do the pre-resolving once inside the curl plugin. There were various problems with that patch which you can read about here:https://listman.redhat.com/archives/libguestfs/2021-October/026882.html
I wonder if CDI can do a simple HEAD request to the site that is submitted in order to resolve redirects, and then use the resolved URI when opening nbdkit? It should be fairly trivial if CDI has access to the curl library. I can suggest some code for this if you like. It would also be useful to give better errors for things like 404s.
The fundamental problem is still
qemu-img convertlacking support for NBD multi-conn which is not something we can fix in nbdkit.But anyway a while back I had this patch to nbdkit to enable parallel curl plugin. It had some unfortunate effects because of fairness which I describe in the thread there. I looked at it again and I think we should give it another go, so I pushed this commit:
https://gitlab.com/nbdkit/nbdkit/-/commit/f2163754860d041c4cb12dace90591c280eccae8
With this patch and upping the default number of connections (
connections=8) it’s goes from 20 mins to about 4 mins, which while still not great (see above about qemu) is probably the best we can do until qemu gets fixed.Hey! Thanks for reporting this
This sounds similar to previous issues we had like https://github.com/kubevirt/containerized-data-importer/issues/2358, but the changes that tackle those did not get backported to your CDI version (
v1.54.3). The author of the PR that fixes this (https://github.com/kubevirt/containerized-data-importer/pull/2701) mentions:If you want to try that change, you can install CDI 1.57 RC