1. The Problem
Project details:
- File size 5.6 GB
- File count 529352
Dockerfile
| |
The build command and its output are as follows:
| |
The time-consuming parts are:
- 10s, load build context
- 26s, executing the COPY operation
- 67s, exporting the image, image size 5.79GB
The following sections follow this same line of thinking, checking each item one by one and validating with tests, in search of the IO bottleneck during builds.
2. A Custom Go Client Submitting Directly to Dockerd Performs Poorly
The project https://github.com/shaowenchen/demo/tree/master/buidl-cli implements exactly this: it submits the local Dockerfile and context to Dockerd for building, so as to test whether the Docker CLI has a bottleneck when submitting files.
2.1 Compile the Binary
| |
2.2 Submit a Build Job with the Custom Binary
| |
Using the CLI written in Go to submit the build context to Dockerd for building, the duration increases sharply; meanwhile, the load on the build machine spikes.
There may be other optimization points that need slow debugging. And the Docker CLI actually also has relevant parameters that can be used to reduce IO time.
3. The compress and stream Build Parameters Have Little Optimizing Effect
compress compresses the context into gzip format for transmission, while stream transmits the context as a stream.
3.1 Optimizing with compress
| |
3.2 Optimizing with stream
| |
Neither of these two parameters has much effect on shortening the build time. But note that the test project’s files are large and numerous; if the test case changes, the effect may differ. Next, let us look together at how file count and file size affect Dockerd’s image building.
4. File Count Affects COPY Far Less Than File Size
4.1 Preparing Test Files
| |
A 119MB file was placed in the data directory, and the build context size was increased by copying that file repeatedly.
4.2 Test Dockerfile
| |
4.3 Build Command
| |
4.4 File Size Has a Clear Effect on COPY
| File Size | Build Duration | File Count |
|---|---|---|
| 119M | 0.3s | 1 |
| 237M | 0.4s | 2 |
| 355M | 0.5s | 3 |
| 473M | 0.6s | 4 |
| 1.3G | 3.7s | 11 |
| 2.6G | 9.0s | 22 |
File size has a clear effect on COPY, growing nearly linearly.
4.5 File Count Has Almost No Effect on COPY
| File Size | Build Duration | File Count |
|---|---|---|
| 2.9G | 13.8s | 264724 |
| 5.6G | 37.1s | 529341 |
File count has little effect on COPY. This is because when the Docker CLI sends the build context to Dockerd, it tars up the context rather than transferring files one at a time.
4.6 The Build Concurrency Bottleneck Is Disk IO
5.6G, 529341 files
| Concurrency | Build Duration |
|---|---|
| 1 | 37.1s |
| 2 | 46s |
| 3 | 81s |
Using iotop you can observe the disk write speed in real time; it reaches 200MB/s at its fastest, closest to the file system’s 4K random write speed.
| |
Because a single Dockerd is shared, the Dockerd throughput becomes a bottleneck under concurrency, and the system’s disk IO also becomes a bottleneck.
5. Not Cleaning the Buildkit Cache Has Almost No Effect on New Builds
If it reports that docker build cannot be found, then EXPERIMENTAL needs to be enabled, or there is no buildx and docker-buildx needs to be downloaded to the /usr/libexec/docker/cli-plugins/ directory.
- View the build cache
| |
- Clean all build cache
| |
Build cache is only produced when BuildKit is enabled. In the production environment the cache size reached 1.408TB, but comparing before and after cleaning, no obvious change in build speed was found for new projects; for old projects, if nothing changed, hitting the cache is very fast. The likely reason is that although the cache is large, there are not many entries, so the cost of querying whether a cache exists is very small.
But cleaning the cache periodically helps prevent the risk of the disk filling up.
- Periodically clean old build cache
Clean cache older than 72h
| |
6. Builds Do Not Limit CPU but IO Is Very Slow
6.1 Testing CPU Limits
Dockerfile
| |
| |
The build machine has 40C; during the build the machine’s CPU load reaches 95%, which shows that Dockerd does not limit CPU consumption by default during builds. In the production environment there have been cases where npm run build consumed more than a dozen GB of memory, so I judge that Dockerd does not limit memory consumption by default either.
6.2 Testing IO in the Dockerfile
Dockerfile
| |
| |
6.3 Testing IO in a Container
| |
| |
6.4 Testing IO in a Container Storage Volume
| |
| |
6.5 Testing IO on the Host
| |
When Dockerd builds a Dockerfile and encounters a Run command, it starts a container to run it and then commits the image. From the test results, you can see that the IO speed in the Dockerfile is far from the host’s, matching the IO speed in a container; the IO speed of a host storage volume matches the host’s IO speed.
7. Building Directly with buildkitd Performs Poorly
Although BuildKit builds can be enabled with DOCKER_BUILDKIT=1, if using buildkitd directly worked well, it would be a good choice to replace Dockerd for building.
7.1 Installing buildkit
| |
7.2 Deploying buildkitd
| |
| |
Just check that buildkitd is running normally.
7.3 Testing buildctl Build Submission
| |
Submitting to buildkitd for building with buildctl takes more time, reaching 4min, double the previous figure.
8. There Is a Bottleneck in Reading and Writing Images Under the Current Storage Driver
8.1 Looking at Dockerd’s Processing Logic
The logic for handling Dockerfiles can be found in the code at https://github.com/moby/moby/blob/8d193d81af9cbbe800475d4bb8c529d67a6d8f14/builder/dockerfile/dispatchers.go.
1, Both Add and Copy call the performCopy function
2, performCopy calls NewRWLayer() to create a new layer, and calls exportImage to write the data
So the suspicion is that Dockerd is slow at writing image layers.
8.2 Testing Image Layer Write Speed
Prepare an image, 16GB in size, 18 layers in total.
- Import the image
| |
- Save the image
| |
docker load and docker save are about the same speed, and the processing speed for image layers is roughly 100 MB/s. That is nearly 30% less than the disk’s 4K random write speed. In my view, if it is for personal use that is barely acceptable; if it is used in a platform product that provides build services externally, this disk is clearly unsuitable.
8.3 How to Choose a Storage Driver
Below is a comparison table compiled from https://docs.docker.com/storage/storagedriver/select-storage-driver/:
| Storage Driver | Filesystem Requirements | High-Frequency Write Performance | Stability | Notes |
|---|---|---|---|---|
| overlay2 | xfs, ext4 | Poor | Good | Current first choice |
| fuse-overlayfs | Unrestricted | - | - | For rootless scenarios |
| btrfs | btrfs | Good | - | - |
| zfs | zfs | Good | - | - |
| vfs | Unrestricted | - | - | Not recommended for production |
| aufs | xfs, ext4 | - | Good | First choice for Docker 18.06 and earlier, unmaintained |
| devicemapper | direct-lvm | Good | Good | Unmaintained |
| overlay | xfs, ext4 | Poor, but better than overlay2 | - | Unmaintained |
Excluding the unmaintained and non-production-suitable ones, there really are not many options left. It happens that there is a machine whose disk was formatted as Btrfs when it was initialized some time ago, which can be used for testing. The zfs storage driver is recommended for high-density PaaS systems.
8.4 Testing the Btrfs Storage Driver
- On the host
| |
- Test command under the container
Run the container
| |
Run the test
| |
- Testing the overlay2 storage driver
| |
| |
- Testing the btrfs storage driver
| |
| |
You can clearly see that the btrfs storage driver is faster than overlay2.
9. Summary
This article mainly records the process of troubleshooting a slow Dockerfile build IO problem encountered in the production environment.
Designing various test cases to investigate the problem and validating each factor one by one requires a great deal of patience, and it is very easy to go in the wrong direction and reach a wrong conclusion.
The main points of this article are as follows:
- The compress and stream parameters are not necessarily effective for build speed
- Reducing the build context size helps relieve build IO pressure
- Buildkit’s cache does not need to be cleaned frequently
- When building a Dockerfile and executing commands, CPU and memory are not limited, but IO is slow
- Building with buildkitd is slower than Dockerd with DOCKER_BUILDKIT enabled
- Using Btrfs storage helps achieve better IO speed
But the simplest thing is still to use a disk with fast 4K random read/write. Before taking a new environment into production, be sure to test it first, and only proceed with subsequent plans when it meets the requirements.
