Docker is native on linux, run via virtualisation elsewhere. Docker on mac is, and always will be, slow. File access from host mounted volumes is slow, CPU performance takes a significant hit. Docker Desktop is an application for MacOS and Windows machines for the building and sharing of containerized applications and microservices. Docker Desktop delivers the speed, choice and security you need for designing and delivering containerized applications on your desktop. Jul 12, 2019. Unfortunately, Mac OS and Windows cannot provide this. Therefore, there is a client on Mac OS to run Docker. In addition to this, there is an abstraction layer between Mac OS kernel and applications (Docker containers) and the filesystems are not the same. Because of that, Docker runs on Mac OS slowly.
Docker For Mac Slow Download SafariKey Features and CapabilitiesThe fastest way to design and deliver containerized applications and microservices on the desktop and cloud.Simple Setup for Docker and Kubernetes
No need to fiddle with VMs or add a bunch of extra components; simply install from a single package and have your first containers running in minutes. You get certified Kubernetes and Docker, for developers of all levels of container expertise.
Certified Kubernetes
Setup a fully functional Kubernetes environment on your desktop with a single click and start developing and testing modern applications in minutes.
Application Templates and App Designer
Customize and share multi-service applications and service templates that are tailored to your organization. Pre-defined and customizable application templates adhere to corporate standards and automate configuration, eliminating error-prone manual setup. Intuitive Application Designer facilitates the packaging, installing, and managing of multi-service applications as a shareable package.
Docker is a helpful tool for both developers and ops. It can simplify both the development of an application as well as deployment and management of it. In this post we are going to explore a common pitfall related to developing an application in Docker on the Mac and see what we can do to mitigate the issue and work as productively as possible.
Developing in Docker
Developing in Docker has a number of advantages over developing directly on your Mac. Before we begin, let’s remind ourselves of a few reasons why we might be developing in Docker:
While there are many more reasons to use Docker, especially in production, these are a few of the benefits you can gain from using it for development, even if its use stops there!
So What’s the Problem?
When developing in Docker, there are a couple of steps that need to take place in order to get an application running:
So what happens when you make a change to your code? In order to see that change, you need to rebuild your image and start a new container. Often this is satisfactory, especially when working with compiled languages, as Docker will cache unchanged parts of an image and rebuild only those that have changed. This may mean a simple recompile of the application binary and we are off to the races.
What about when working with an interpreted language like Javascript, Python, or Ruby? With Ruby on Rails, for example, we are used to the concept of a “hot reload” in which we simply make a change to the source code and see that change reflected upon page refresh. By default with Docker, this is not possible. We would have to rebuild the image to see each change reflected. This is a slow process that hinders our productivity.
In order to work around this, developers will often create a bind mount. This means that we specify a folder on the host machine (commonly the application working directory) and instruct Docker to keep that directory in sync with a directory in the container. This way when we make a change to a source file on the host, that change is propagated to the container without rebuilding our image, thus keeping the “hot reload” intact. Problem solved, right?
Not exactly.
Docker works its magic by leveraging features of the linux kernel, notably namespaces (for isolation) and control groups (or cgroups—for resource management). On your Mac, these resources do not exist. Therefore, in order for Docker Desktop for Mac to function, it runs a linux virtual machine. Along with the VM comes a filesystem sharing utility called “osxfs” which is in charge of keeping the filesystem native to your Mac in sync with the linux-based filesystem of your docker containers.
This sync process comes at a cost. While Docker has made great strides in improving sync performance, the process is still much slower than running natively without syncing. The issue is compounded when you have applications that make changes to large amounts of small files, as each change made on the host needs to be detected and propagated to the container, and vice-versa.
Getting Started
In order to evaluate the performance of different sync strategies, we need to execute a repeatable task that results in heavy IO load. One such task is the installation of Rails. This is due to the large amount of dependencies required for installation. If we tell Docker to bind mount the gem installation directory, it will ensure any files that are created in the container during gem installation are copied to the host filesystem. Note that this is a somewhat contrived example, but it is an easy way to demonstrate how the sync process can affect the speed of IO in Docker on the Mac and consequently, the speed at which your application executes.
First, let’s create a new directory and enter it:
Next, create a simple Dockerfile within that directory:
This produces a Dockerfile that looks like this:
Now, build the image:
Great. We now have an image that can be run to perform our speed tests.
Establishing a Baseline
First, let's see how long it takes to install Rails without any filesystem syncing. This will establish a baseline that we can use for comparison.
This will drop us into a bash shell as specified in in the CMD portion of the Dockerfile. As for the flags?
Now, let’s install Rails and establish that baseline!
On my machine, looking at the “real” time elapsed, it took about 54 seconds.
We can now exit the container.
Bind Mount: Consistent
Bind mounts have three different types in Docker: consistent, delegated, and cached. By default when a bind mount is created it is of type consistent. This means that whenever a write occurs, it is immediately reflected to the other end of the mount. Since this is the default, it is what most developers will be using when they mount their working directory. So let’s see what kind of effect this has on performance. Using the docker image from before, let’s again log into the container, only this time we will bind mount the gem home to a local directory.
First, create a local gem directory for mounting:
Now log into the container:
Now, we time the Rails install again:
In this case, the installation took about 2 minutes and 55 seconds! This is an increase of 2 minutes, or about 3x slower. Ouch!
Again, although this example is contrived, you can see how this could significantly slow down execution of a dockerized application. When working with a Rails project, there are lots of small file writes taking place all the time, and when you are syncing your working directory this will slow down your application significantly. The same can be said for any other application which performs a lot of IO.
Bind Mount: Cached
As mentioned earlier, one of the options that Docker Desktop for Mac allows is setting a bind mount as type cached. What this means is that Docker will view the macOS host as the authoritative source of truth, and there could be delays before updates are visible within the container. Typically, these delays are within a second or two—not enough to matter in most cases, but as we will see it can gain us some speed increases.
Clear the local gem directory:
Log into the container with a cached bind mount:
Now, running the same speed tests as before, I get 2 minutes and 4 seconds. This is certainly faster than a consistent mount, but it’s still significantly slower than our baseline.
Bind Mount: Delegated
This is similar to the cached type, but in this case the container’s filesystem has the authoritative view and updates on the host may be delayed. Running the same test as we did for the cached type, I get a result of 2 minutes and 13 seconds.
Docker-Sync
Docker Sync is a ruby gem which enables you to keep your code base in sync with the container while allowing the application to perform nearly at full speed. In short, the way it achieves this is by creating a docker volume that your app can write to at full speed. This volume is then connected to a special container which syncs that volume with the host in an asynchronous fashion. For more details, see this page.
So how does this strategy perform? Let’s take a look.
First install the gem:
Install Docker On Mac Os
Now, create a YAML file which defines a simple docker-sync configuration:
You should end up with a file that looks something like this:
Now start up your docker container:
Notice that the volume source we specify is that which we declared in the docker-sync.yml file. For more information on why we set the nocopy option, see here.
Timing the Rails install, I get about 1 minute and 1 second. This is very close to our baseline of 53 seconds!
In this case, the slow sync via 'osxfs' is hidden from the application, which sees only a fast docker volume.
Mutagen
Mutagen is self-described as a “fast, continuous, multidirectional file synchronization tool”. Of the supported synchronization types, the one that we are interested in is its support for Docker containers. Once we start the mutagen daemon, we'll simply tell it to create a synchronization session between our local code and a remote path on the docker container. Mutagen will then seamlessly copy an agent binary into the container which will communicate with the host to keep things in sync. You can learn more about how mutagen works here with Docker-specific information available here.
Without further ado, let’s get things running and see how it performs.
First we will need to install the agent binary:
(Note: this steps assumes you have the homebrew package manager installed. If not, see here.)
Next, start the mutagen daemon:
Now we’ll need to start our container. This step is simple. Like in our baseline step, there is no need to mount any volumes. The only difference is that we will give a name to the container so that we can reference it later.
Now, we tell mutagen to keep things in sync:
That’s it! We can now time our Rails install as we did in previous steps. I get about 55 seconds. Taking into account margin for error, this is about the same as our baseline!
For reference, there are a few other mutagen commands worth knowing.
mutagen list will list all sync sessions and their statuses, mutagen monitor shows a dynamic status display for a single session, allowing you see if things are working, and mutagen terminate will permanently stop synchronization. Instead of terminating, you can pause and resume as well. Lastly, it’s worth mentioning the -i flag of mutagen create . With it, you can tell mutagen to ignore sync on certain directories. If you are running a Rails app, for example, it might be a good idea to specify something like the following:
as these are directories that are often written to but have little value in syncing.
The Results
We’ve talked a bit about the results of the different sync methods we have tried, but let’s take a closer look. Here we can see the execution time of each strategy:
It’s clear that a normal bind mount makes a significant dent in IO performance. By instructing Docker to favor host or container consistency we can easily gain some speed. Introducing a third-party tool to the mix allows us to significantly improve performance on top of that.
Let’s take a quick look at the pros and cons of each approach:
Bind Mount (Cached & Delegated)Pros:
Cons:
Notes:
Docker-SyncPros:![]()
Cons:
MutagenPros:
Cons:
Final Thoughts
Docker is a great tool for developing applications. While using Docker, it often makes sense to create a bind mount to ensure that changes to your local codebase are immediately reflected into your application container. By default, however, doing so can create significant application performance issues.
Adding a simple flag to your volume mounts is an easy way to help mitigate the issue. For the fastest possible speeds, look towards a third-party tool such as docker-sync or mutagen. In many—but not all!—cases the small effort required to implement one of these solutions will pay off greatly with faster application performance and, as a result, increased developer productivity.
Hopefully this guide helps you to choose the best option for your application. Personally, I prefer Mutagen for its speed and flexibility of use cases. The great thing is that all of the available options are easy to implement and switch between, so if one solution doesn’t work out it’s easy to try another!
Comments are closed.
|
AuthorWrite something about yourself. No need to be fancy, just an overview. Archives
December 2020
Categories |