visit
pi@raspberrypi:~$ cat /proc/cpuinfo
processor : 0
model name : ARMv6-compatible processor rev 7 (v6l)
BogoMIPS : 577.53
Features : half thumb fastmult vfp edsp java tls
CPU implementer : 0x41
CPU architecture: 7
CPU variant : 0x0
CPU part : 0xb76
CPU revision : 7
Hardware : ARM-Versatile (Device Tree Support)
Revision : 0000
Serial : 0000000000000000
pi@raspberrypi:~ $ cat /proc/cpuinfo
processor : 0
model name : ARMv6-compatible processor rev 7 (v6l)
BogoMIPS : 697.95
Features : half thumb fastmult vfp edsp java tls
CPU implementer : 0x41
CPU architecture: 7
CPU variant : 0x0
CPU part : 0xb76
CPU revision : 7
Hardware : BCM2835
Revision : 000d
Serial : 000000003d9a54c5
What is QEMU?
is a processor emulator. It supports a number of different processors, but we're only interested in something that can run Raspberry Pi images natively without a lot of difficulties. In this case, we're going to be using QEMU 4.2.0, which supports an ARM11 instruction set that's compatible with the Broadcom BCM2835 (ARM1176JZFS) chip found on the Raspberry Pi 1 and Zero. We will use ARM1176 support on QEMU, which will allow us to more or less emulate a Raspberry Pi 1. I say more or less because we will still need to use a in order to boot on the emulated hardware. QEMU support for the Pi is still in development, so our approach to getting it to work here is just a clever hack that will by no means be optimal or efficient in terms of CPU utilization.QEMU Features
QEMU supports many of the same features found in Docker, however, it can run full software emulation without a host kernel driver. This means that it can run inside Docker, or any other virtual machine, without host virtualization support. The QEMU feature list is extensive, and the learning curve is steep. However, the primary feature that we will be focused on for this build is host port forwarding so that data can be passed to the host.Dockerized QEMU
One of Docker's strengths is that it doesn't handle full-fledged virtualization, but instead relies on the architecture of the host system. Since our host system will be running an Intel processor, we can't expect Docker to handle ARM operations on its own. So, we will be placing QEMU inside a Docker container. Since Docker is designed to run software at near-native performance, the operational efficiency challenge will be with QEMU itself. QEMU, on the other hand, supports the emulation of a machine's architecture completely with software. The advantage here is that it can run inside any virtualized system or container, independent of its system architecture. If patient, we could even run a Dockerized Raspberry Pi container inside another Dockerized Raspberry Pi container. The drawback to QEMU is that it has comparatively poor performance compared to other types of virtualization. But we can benefit from the best of both worlds by leveraging QEMU's ARM emulation while depending on Docker for everything else.Raspbian
Based on Debian, is a popular and well-supported operating system for the Raspberry Pi. It's one of the most often recommended for the platform and the is active and well managed.Physical Raspberry Pi Speed Comparison
The following tests are intended as a baseline for comparing our virtualized systems. Since we will be emulating a single-core, these tests are only single-core, single thread, regardless of how many physical cores are incorporated into the architecture.Raspberry Pi 1 2011,12
Test execution summary:
total time: 330.5514s
total number of events: 10000
total time taken by event execution: 330.5002
per-request statistics:
min: 32.92ms
avg: 33.05ms
max: 40.94ms
approx. 95 percentile: 33.24ms
Threads fairness:
events (avg/stddev): 10000.0000/0.00
execution time (avg/stddev): 330.5002/0.00
Raspberry Pi 1 A+ V1.1 2014
Test execution summary:
total time: 328.7505s
total number of events: 10000
total time taken by event execution: 328.6931
per-request statistics:
min: 32.71ms
avg: 32.87ms
max: 78.93ms
approx. 95 percentile: 33.03ms
Threads fairness:
events (avg/stddev): 10000.0000/0.00
execution time (avg/stddev): 328.6931/0.00
Raspberry Pi Zero W v1.1 2017
Test execution summary:
total time: 228.2025s
total number of events: 10000
total time taken by event execution: 228.1688
per-request statistics:
min: 22.76ms
avg: 22.82ms
max: 35.29ms
approx. 95 percentile: 22.94ms
Threads fairness:
events (avg/stddev): 10000.0000/0.00
execution time (avg/stddev): 228.1688/0.00
Raspberry Pi 2 Model B v1.1 2014
Test execution summary:
total time: 224.9052s
total number of events: 10000
total time taken by event execution: 224.8738
per-request statistics:
min: 22.20ms
avg: 22.49ms
max: 32.85ms
approx. 95 percentile: 22.81ms
Threads fairness:
events (avg/stddev): 10000.0000/0.00
execution time (avg/stddev): 224.8738/0.00
Raspberry Pi 3 Model B v1.2 2015
Test execution summary:
total time: 139.6140s
total number of events: 10000
total time taken by event execution: 139.6087
per-request statistics:
min: 13.94ms
avg: 13.96ms
max: 34.06ms
approx. 95 percentile: 13.96ms
Threads fairness:
events (avg/stddev): 10000.0000/0.00
execution time (avg/stddev): 139.6087/0.00
Raspberry Pi 4 B 2018
Test execution summary:
total time: 92.6405s
total number of events: 10000
total time taken by event execution: 92.6338
per-request statistics:
min: 9.22ms
avg: 9.26ms
max: 23.50ms
approx. 95 percentile: 9.27ms
Threads fairness:
events (avg/stddev): 10000.0000/0.00
execution time (avg/stddev): 92.6338/0.00
Single Host Specifications
Historically, QEMU has been single-threaded, emulating all cores of a system's architecture on a single CPU. While that's no longer the case, we are still going to be emulating a single core Raspberry Pi. We will do some benchmarks later to compare how different CPU limits on each node impacts performance. But for now, we will use one CPU per single-core node. Since QEMU has the potential to use a lot of CPU resources due to its inherent inefficiency, our initial three-node cluster will start with a baseline of at least one CPU per node, leaving one CPU dedicated to the host to avoid performance problems. The VM specs selected for this task are as follows.Cloud Provider: Google Cloud Platform
Instance Type: n1-standard-4
CPUs: 4
Memory: 15GB
Disk: 100GB
Operating System: Ubuntu 18.04 LTS
Docker
Installed on the host, we're also using the default version of Docker that is available on the default apt repository for Ubuntu 18.04 LTS.# docker -v
Docker version 18.09.7, build 2d0083d
Docker Compose
# docker-compose -v
docker-compose version 1.25.0, build 0a186604
Docker Hub Ubuntu Image
18.04, bionic-20200112, bionic, latest
QEMU
Installed inside the Docker container, we will be using the following version of QEMU for ARM:# qemu-system-arm --version
QEMU emulator version 4.2.0
Copyright (c) 2003-2019 Fabrice Bellard and the QEMU Project developers
QEMU Customized Kernel for Raspbian
Loaded from QEMU inside Docker, we will use 's for Raspbian, which has been modified to be usable with QEMU.Raspbian Lite Image
Also booted from QEMU, we will use an unmodified version of from 9/30/2019.Expect (Tcl/Tk)
Installed on the Docker container is the following version of Expect:# expect -v
expect version 5.45.4
ssh
/sshd
sshd
will need to be enable on each Raspbian node, and ssh
should be enabled on the host.Ansible
The following version of Ansible is also being used, along with its other dependencies:# ansible --version
ansible 2.5.1
config file = /etc/ansible/ansible.cfg
configured module search path = [u'/root/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python2.7/dist-packages/ansible
executable location = /usr/bin/ansible
python version = 2.7.17 (default, Nov 7 2019, 10:07:09) [GCC 7.4.0]
QEMU Build Container
We will compile QEMU 4.2.0 from source. It will need all the supporting build tools, so to keep our app container as small as possible, we will create a separate build container for the QEMU build using a minimal version of Ubuntu 18.04 from Docker Hub.QEMU App Container
Once the QEMU is compiled from source, we will transfer it to the app container. Also from Docker Hub, we will use the same minimal version of Ubuntu 18.04 to host the QEMU binary.Docker Configuration
The
Dockerfile
We will be using the following Dockerfile, which may also be found updated in this guide's accompanying repository on Github. Each code snippet below makes up a segment of the Dockerfile. Thanks goes to 's for his work on .Build stage for
qemu-system-arm
:FROM ubuntu AS qemu-system-arm-builder
ARG QEMU_VERSION=4.2.0
ENV QEMU_TARBALL="qemu-${QEMU_VERSION}.tar.xz"
WORKDIR /qemu
RUN apt-get update && \
apt-get -y install \
wget \
gpg \
pkg-config \
python \
build-essential \
libglib2.0-dev \
libpixman-1-dev \
libfdt-dev \
zlib1g-dev \
flex \
bison
RUN wget "//download.qemu.org/${QEMU_TARBALL}"
RUN # Verify signatures...
RUN wget "//download.qemu.org/${QEMU_TARBALL}.sig"
RUN gpg --keyserver keyserver.ubuntu.com --recv-keys CEACC9E15534EBABB82D3FA03353C9CEF108B584
RUN gpg --verify "${QEMU_TARBALL}.sig" "${QEMU_TARBALL}"
RUN tar xvf "${QEMU_TARBALL}"
RUN "qemu-${QEMU_VERSION}/configure" --static --target-list=arm-softmmu
RUN make -j$(nproc)
RUN strip "arm-softmmu/qemu-system-arm"
Build the intermediary
pidoc
VM app image.FROM ubuntu as pidoc-vm
ARG RPI_KERNEL_URL="//github.com/dhruvvyas90/qemu-rpi-kernel/archive/afe411f2c9b04730bcc6b2168cdc9adca224227c.zip"
ARG RPI_KERNEL_CHECKSUM="295a22f1cd49ab51b9e7192103ee7c917624b063cc5ca2eaad5f4"
COPY --from=qemu-system-arm-builder /qemu/arm-softmmu/qemu-system-arm /usr/local/bin/qemu-system-arm
ADD $RPI_KERNEL_URL /tmp/qemu-rpi-kernel.zip
RUN apt-get update && \
apt-get -y install \
unzip \
expect
RUN cd /tmp && \
echo "$RPI_KERNEL_CHECKSUM qemu-rpi-kernel.zip" | sha256sum -c && \
unzip qemu-rpi-kernel.zip && \
mkdir -p /root/qemu-rpi-kernel && \
cp qemu-rpi-kernel-*/kernel-qemu-4.19.50-buster /root/qemu-rpi-kernel/ && \
cp qemu-rpi-kernel-*/versatile-pb.dtb /root/qemu-rpi-kernel/ && \
rm -rf /tmp/*
VOLUME /sdcard
ADD ./entrypoint.sh /entrypoint.sh
ENTRYPOINT ["./entrypoint.sh"]
Build the final app
pidoc
image with the Raspbian Lite filesystem loaded.FROM pidoc-vm as pidoc
ARG FILESYSTEM_IMAGE_URL="//downloads.raspberrypi.org/raspbian_lite/images/raspbian_lite-2019-09-30/2019-09-26-raspbian-buster-lite.zip"
ARG FILESYSTEM_IMAGE_CHECKSUM="a50237c2f718bd8d806b96df5b9d2174ce8b789eda1f03434ed2213bbca6c6ff"
ADD $FILESYSTEM_IMAGE_URL /filesystem.zip
ADD pi_ssh_enable.exp /pi_ssh_enable.exp
RUN echo "$FILESYSTEM_IMAGE_CHECKSUM /filesystem.zip" | sha256sum -c
The
entrypoint.sh
FileFirst, the script determines if the filesystem has been downloaded or not, and if not, it downloads and decompresses it.#!/bin/sh
raspi_fs_init() {
image_path="/sdcard/filesystem.img"
zip_path="/filesystem.zip"
if [ ! -e $image_path ]; then
echo "No filesystem detected at ${image_path}!"
if [ -e $zip_path ]; then
echo "Extracting fresh filesystem..."
unzip $zip_path
mv *.img $image_path
rm $zip_path
else
exit 1
fi
fi
}
The script then checks for an empty
raspi-init
file, which serves as a marker to determine if Expect has been launched previously to enable ssh on Raspbian.if [ ! -e /raspi-init ]; then
touch /raspi-init
raspi_fs_init
echo "Initiating Expect..."
/usr/bin/expect /pi_ssh_enable.exp `hostname -I`
echo "Expect Ended..."
If Expect has already been previously enabled, then we only need to launch QEMU, without Expect. Note that we are forwarding port
22
on Raspbian to port 2222
inside the Docker container.else
/usr/local/bin/qemu-system-arm \
--machine versatilepb \
--cpu arm1176 \
--m 256M \
--hda /sdcard/filesystem.img \
--net nic \
--net user,hostfwd=tcp:`hostname -I`:2222-:22 \
--dtb /root/qemu-rpi-kernel/versatile-pb.dtb \
--kernel /root/qemu-rpi-kernel/kernel-qemu-4.19.50-buster \
--append "root=/dev/sda2 panic=1" \
--no-reboot \
--display none \
--serial mon:stdio
fi
QEMU doesn't have a straightforward method for running configuration scripts on boot. And because Raspbian doesn't come with SSH enabled by default, we will have to turn it on ourselves. Our options are to do it manually or to use some sort of scripting tool that can interact with
stdio
. Another option is to customize the Raspbian image before installation. This would have to be done on the host, however, as Docker restricts the mounting of new filesystems. In any case, to make this build the most portable and host independent, the most straightforward for our purposes will be to use an script, and have it copied into our Docker image on build.The
pi_ssh_enable.exp
FileSince an unmodified Raspbian image has no accessible ports by default, we will use Expect to interface with
stdio
in QEMU, log in with a default username and password, and enable the sshd
listener.#!/usr/bin/expect -f
set ipaddr [lindex $argv 0]
set timeout -1
spawn /usr/local/bin/qemu-system-arm \
--machine versatilepb \
--cpu arm1176 \
--m 256M \
--hda /sdcard/filesystem.img \
--net nic \
--net user,hostfwd=tcp:$ipaddr:2222-:22 \
--dtb /root/qemu-rpi-kernel/versatile-pb.dtb \
--kernel /root/qemu-rpi-kernel/kernel-qemu-4.19.50-buster \
--append "root=/dev/sda2 panic=1" \
--no-reboot \
--display none \
--serial mon:stdio
expect "raspberrypi login:"
send -- "pi\r"
expect "Password:"
send -- "raspberry\r"
expect "pi@raspberrypi:"
send -- "sudo systemctl enable ssh\r"
expect "pi@raspberrypi:"
send -- "sudo systemctl start ssh\r"
expect "pi@raspberrypi:"
expect eof
Build Image
In the folder with the
Dockerfile
, we will be building our two containers. The first will be our build container that includes all the dependencies for compiling QEMU, and the other will be our app container for running QEMU.docker build -t pidoc .
docker run -itd --name testnode pidoc
docker logs testnode -f
Once Raspbian is fully booted, Expect should automatically enable
sshd
. Log into the docker container and test that SSH is reachable from inside the container on port 2222
.# docker exec -it testnode bash
root@d4abc2f655e6:/# hostname -I
172.17.0.3
root@d4abc2f655e6:/# cat < /dev/tcp/172.17.0.3/2222
SSH-2.0-OpenSSH_7.9p1 Raspbian-10
root@d4abc2f655e6:/# exit
exit
# docker kill testnode
testnode
# docker container rm testnode
testnode
Start/Test Container
We will need to start the container for testing. This is primarily to gain some understanding of the performance of QEMU so that we can better make design decisions regarding our cluster. The system should come up clean with maybe a few benign warnings related to differences between the somewhat more generalized emulated hardware and the expected physical raspberry Pi hardware. I found it necessary to make sure port forwarding was working properly between QEMU and the Docker image so that I could further verify that port forwarding between the Docker image and host was working properly. Our first goal is to double forward SSH so that QEMU is accessible directly from the host.docker run -itd -p 127.0.0.1:2222:2222 --name testnode pidoc
docker logs testnode -f
Once the system again comes online, test for
sshd
on port 2222
of the host by using ssh
to log into Raspbian:# ssh pi@localhost -p 2222
The authenticity of host '[localhost]:2222 ([127.0.0.1]:2222)' can't be established.
ECDSA key fingerprint is SHA256:N0oRF23lpDOFjlgYAbml+4v2xnYdyrTmBgaNUjpxnFM.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '[localhost]:2222' (ECDSA) to the list of known hosts.
pi@localhost's password:
Linux raspberrypi 4.19.50+ #1 Tue Nov 26 01:49:16 CET 2019 armv6l
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Tue Jan 21 12:24:59 2020
SSH is enabled and the default password for the 'pi' user has not been changed.
This is a security risk - please login as the 'pi' user and type 'passwd' to set a new password.
pi@raspberrypi:~ $
Testing Fractional CPU Utilization
To run this cluster, we're using a GCP n1-standard-4 instance (a 4x15) running Ubuntu 18.04 LTS. But we now notice how inefficient QEMU is once Raspbian begins doing anything. Multiple Raspberry Pi instances might stack fine if their idle, but if we want to keep the system viable, we will need to restrict CPU utilization on each instance, or else the system could be rendered unusable once more than a few nodes are put under load. Fortunately, Docker can handle this for us. We have 15GB of ram on this instance, so let's see what happens if we are slightly more ambitious and squeezed 6 Raspberry Pi containers onto our VM. We will have a whole core left for the host to manage other tasks without much risk of a failure. We can scale this at some point later with Docker Compose.We will run two test containers at 50% and 100% for benchmark testing.docker run -itd --cpus="0.50" -p 127.0.0.1:2250:2222 --name pidoc_50_test pidoc
docker run -itd --cpus="1.00" -p 127.0.0.1:2200:2222 --name pidoc_00_test pidoc
Performance
While a full core allocation performs at near Physical Raspberry Pi speeds, an instance running at 50% runs rightly at half that speed. This might be manageable under certain circumstances, but it's not the most desirable. The overall efficiency of the cluster may increase, depending on the task at hand. But for now, we will continue with our original full core allocation of 3 nodes, and then later it tests with 6 nodes.Single Thread Benchmarks
Testing can be done by using the following simple benchmark tests.CPU Prime Test
sysbench --test=cpu --cpu-max-prime=9999 run
CPU Integer Test
time $(i=0; while ((i<9999999)); do ((i++)); done)
HDD Read Test
dd bs=16K count=102400 iflag=direct if=test_data of=/dev/null
HDD Write Test
dd bs=16k count=102400 oflag=direct if=/dev/zero of=test_data
Results (Single Thread)
For this guide, we will only focus on the CPU Prime Test using
sysbench
Host
General statistics:
total time: 10.0009s
total number of events: 9417
Latency (ms):
min: 1.04
avg: 1.06
max: 1.63
95th percentile: 1.10
sum: 9992.36
Threads fairness:
events (avg/stddev): 9417.0000/0.00
execution time (avg/stddev): 9.9924/0.00
Virtual Raspberry Pi - Limit: 100%
Test execution summary:
total time: 397.8781s
total number of events: 10000
total time taken by event execution: 397.4056
per-request statistics:
min: 38.61ms
avg: 39.74ms
max: 57.15ms
approx. 95 percentile: 40.92ms
Threads fairness:
events (avg/stddev): 10000.0000/0.00
execution time (avg/stddev): 397.4056/0.00
Virtual Raspberry Pi - Limit: 50%
Test execution summary:
total time: 823.8272s
total number of events: 10000
total time taken by event execution: 822.9329
per-request statistics:
min: 38.68ms
avg: 82.29ms
max: 184.02ms
approx. 95 percentile: 94.65ms
Threads fairness:
events (avg/stddev): 10000.0000/0.00
execution time (avg/stddev): 822.9329/0.00
Create
docker-compose.yml
FileWe will use Docker Compose for cluster creation. Initially, we will keep this at three nodes to keep it easy to manage. Once we have a proof of concept cluster, we can then scale it out. The most straightforward way to handle this is to map separate ports to localhost for each container. We can specify a range of ports to be used in the
docker-compose.yml
file, as noted below.version: '3'
services:
node:
image: pidoc
ports:
- "2201-2203:2222"
Bring Up Cluster
To bring up three nodes with
docker-compose
, use the --scale
option.docker-compose up --scale node=3
Ansible Configuration
Now that we have all the infrastructure in place for a cluster, we need to manage it. We could use Docker to double attach to the QEMU monitor, but ssh is much more robust. Since we are using
ssh
, we can use Ansible. A few basic operations are provided here: update
, upgrade
, reboot
, and shutdown
. These can be expanded as needed to develop a more robust system.hosts
FilePlease note of the ports we specified in the
docker-compose.yml
file earlier, and edit your hosts
inventory accordingly.[all:vars]
ansible_user=pi
ansible_ssh_pass=raspberry
ansible_ssh_extra_args='-o StrictHostKeyChecking=no'
[pidoc-cluster]
node_1.localhost:2201
node_2.localhost:2202
node_3.localhost:2203
update.yml
File---
- name: Apt update Pi...
hosts: pidoc-cluster
tasks:
- name: Update apt cache...
become: yes
apt:
update_cache=yes
Usage:
ansible-playbook playbooks/update.yml -i hosts
upgrade.yml
File---
- name: Upgrade Pi...
hosts: pidoc-cluster
gather_facts: no
tasks:
- name: Update and upgrade apt packages...
become: true
apt:
upgrade: yes
update_cache: yes
cache_valid_time: 86400
Usage:
ansible-playbook playbooks/upgrade.yml -i hosts
reboot.yml
File---
- name: Reboot Pi...
hosts: pidoc-cluster
gather_facts: no
tasks:
- name: Reboot Pi...
shell: shutdown -r now
async: 0
poll: 0
ignore_errors: true
become: true
- name: Wait for reboot...
local_action: wait_for host={{ ansible_host }}state=started delay=10
become: false
Usage:
ansible-playbook playbooks/reboot.yml -i hosts
shutdown.yml
File---
- name: Shutdown Pi...
hosts: pidoc-cluster
gather_facts: no
tasks:
- name: 'Shutdown Pi'
shell: shutdown -h now
async: 0
poll: 0
ignore_errors: true
become: true
- name: "Wait for shutdown..."
local_action: wait_for host={{ ansible_host }} state=stopped
become: false
Usage:
ansible-playbook playbooks/shutdown.yml -i hosts
Scaling Up
Docker Compose makes scaling Raspberry Pi containers on the same host near trivial. By using Ansible for cluster management, it also becomes incredibly easy to scale horizontally to other hosts by changing the port binding from localhost to an IP address that's routable. Here is our example with 6 nodes instead of 3.docker-compose.yml
Fileversion: '3'
services:
node:
image: pidoc
ports:
- "2201-2212:2222"
deploy:
resources:
limits:
cpus: "0.5"
Bring Up Cluster
We should stop containers from our previous cluster, and prune all volumes before scaling up our revised cluster. To bring up all 6 nodes with
docker-compose
, use the --scale
option again.docker-compose up --scale node=5
This article was originally published on .