Engineering
Sep 20, 2023
Engineering
High sky, plump horses, and Container Dieting
Mario (Manseok) Cho
Solution Architect / Consultant
Sep 20, 2023
Engineering
High sky, plump horses, and Container Dieting
Mario (Manseok) Cho
Solution Architect / Consultant
Introduction
Most Linux distributions, such as Ubuntu, RedHat, and CentOS, use glibc as the system's standard C library. When you install a library package, such as OpenSSL, with apt on Ubuntu or rpm (yum) on the RedHat line, it is dynamically linked with glibc by default.
GNU (Gnu) is an operating system and includes a wide range of computer software. GNU is open source, developed and maintained by the Free Software Foundation (FSF). Examples of things created by GNU include compilers and development tools such as GCC, G++, and Make. GNU uses glibc as its standard C library. glibc uses the GNU Lesser General Public License.
musl is a Linux standard C library distributed under the MIT license. Its developer is Rich Felker, and while glibc uses dynamic linking, musl aims to implement a standard C library that conforms to POSIX standards using static linking. It also implements non-standard features of Linux, BSD, and glibc.
Differences between glibc and musl in the Linux environment
When you install a package on Linux, it uses glibc by default. If you've ever built a C/C++ program using gcc, you've most likely done a glibc-based dynamic link build. However, in addition to this common glibc dynamic build, you can also do a MUSL-based dynamic/static build.
There are the following differences between *-linux-gnu
and *-linux-musl
.
Build targets | Standard C libraries | Linking method |
---|---|---|
*-linux-gnu | glibc | dynamic linking |
*-linux-musl | musl | dynamic/static linking |
Consider the case of building an executable with Rust.
When you install Rust on a Linux environment using rustup
, *-linux-gnu
is selected as the default target.
If you don't specify any other options, Rust will build the binary with the *-linux-gnu
target and dynamically link it with glibc.
To run a binary built in this way, you must have glibc installed in your Linux environment for it to work.
If the binary relies on external libraries such as OpenSSL (if it is dynamically linked), you will also need to install those libraries via a package manager such as apt.
If you want to run these dynamically linked binaries as a regular user, you can bundle them into a package like a DEB or RPM that describes the dependencies on external libraries.
The package manager will then automatically find and install the appropriate dependent libraries.
However, if you're using a library that isn't registered with the package manager, or even the same library, there are subtle compatibility issues between the installed version and the version you used to develop it, there's a chance that the binary you build won't run as intended.
If you specify the *-linux-musl
target, Rust will statically link with musl when building the binary.
If you rely on external libraries like OpenSSL, it will also statically link those as well, embedding them all into the binary.
This means that you end up with all of these libraries inside a single binary file in Rust.
This static binary can run on any Linux environment, as long as it matches the CPU architecture and the set of system calls provided by the Linux kernel.
This makes it easier to distribute binaries because you only need to pass a single binary to run it, rather than using a package like a DEB or RPM.
If this makes deploying binaries so easy, why isn't the *-linux-musl
target the default for Linux environments?
The reason is that using MUSL makes build preparation somewhat more complicated.
This is because if a developer-created binary package uses *-linux-musl
and also relies on external libraries, those external libraries must also be statically linked with musl instead of dynamically linked with glibc.
This means that all dependent libraries, as well as the main body of the program you want to build using the compiler for musl, must be built as static links from source code.
Fortunately, you don't have to build everything from scratch if it's a commonly used external library in Rust.
By utilizing a Docker image that bundles frequently used libraries with the Rust compiler/gcc, you can easily create a musl-based static build.
(In the command examples that follow, I'll arbitrarily use the <distro>#
prompt to distinguish the container environment for each Linux distribution).
$ docker run -it --name ubuntu ubuntu:22.04 bash
ubuntu# apt update && apt install -y curl gcc vim
Let's configure a dynamic link, glibc, and a static link, musl, in the Rust language environment, which is commonly used for development. First, install Rust on your Ubuntu environment.
ubuntu# curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
ubuntu# source $HOME/.cargo/env
Let's compare dynamic and static linking using Rust's default example, "Hello World" output.
First, let's build "Hello World" using glibc.
ubuntu# cd ubuntu# cargo new --bin hello && cd $_ Created binary (application) `hello` package ubuntu# cargo build --release Compiling hello v0.1.0 (/root/hello) Finished release [optimized] target(s) in 0.35s
Let's use the ldd
command to verify that the library is configured as a dynamic link in the glibc environment.
We can see that linux-vdso
, libgcc_s
, libc
, etc. are configured as dynamic links.
ubuntu# ldd target/release/hello
linux-vdso.so.1 (0x00007fffe87df000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007fdce9c3f000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fdce9a17000)
/lib64/ld-linux-x86-64.so.2 (0x00007fdce9cc2000)
So let's change the RUST target configuration with a MUSL static link.
ubuntu# rustup target add x86_64-unknown-linux-musl
info: downloading component 'rust-std' for 'x86_64-unknown-linux-musl'
info: installing component 'rust-std' for 'x86_64-unknown-linux-musl'
34.7 MiB / 34.7 MiB (100%) 8.6 MiB/s in 4s ETA: 0s
ubuntu# rustup show
Default host: x86_64-unknown-linux-gnu
rustup home: /root/.rustup
installed targets for active toolchain
--------------------------------------
x86_64-unknown-linux-gnu
x86_64-unknown-linux-musl
active toolchain
----------------
stable-x86_64-unknown-linux-gnu (default)
rustc 1.72.0 (5680fa18f 2023-08-23)
ubuntu#
Let's build "Hello World" to verify that static links are configured correctly.
ubuntu# cargo build --release --target=x86_64-unknown-linux-musl
Compiling hello v0.1.0 (/root/hello)
Finished release [optimized] target(s) in 0.37s
ubuntu# ldd target/x86_64-unknown-linux-musl/release/hello
statically linked
You can see that "Hello World" is configured as a static link using the musl environment.
Now let's run "Hello World" built with both dynamic and static links by copying the binaries on CentOS and Alpine environments. CentOS 8 uses glibc dynamic linking and Alpine Linux uses musl static linking.
CentOS Container Environment
$ docker run -it --name centos centos:centos8 bash
centos#
Alpine Container Environment
The Alpine distribution uses musl by default rather than glic.
$ docker run -it --rm alpine:3.18
alpine#
Let's copy 'Hello World' into a glibc environment and a musl environment to see the behavior.
$ docker cp ubuntu:/root/hello/target/x86_64-unknown-linux-musl/release/hello .
$ docker cp hello centos:/root/
$ docker cp hello alpine:/root/
Let's check the behavior on centOS.
centos# ./hello Hello, world!
Let's check the behavior on alpine.
alpine# ./hello Hello, world!
Comparing glibc and musl using the Rust application 'slice'
Let's take the Rust application 'slice' and compare the container images created with glibc and musl.
The Rust implementation of 'slice', like Python's 'slice', is publicly available on the GitHub repository https://github.com/ChanTsune/slice. 'slice' is a tool that prints the contents of a file from the front or back, like 'head' or 'tail'. For example, the command below will print lines 10 through 20 from 'file.txt'.
$ slice 10:20 file.txt
When you build 'slice' in a Rust environment and create a container to use it, you can use it like this
$ docker run -i --rm -v `pwd`:`pwd` -w `pwd` slice
Let's build a container using glibc in the Ubuntu 22.04 environment.
FROM rust:latest as builder
WORKDIR /work
RUN git clone https://github.com/ChanTsune/slice /work/.
RUN cargo build --release
RUN strip /work/target/release/slice -o /slice
FROM ubuntu:22.04
COPY --from=builder /slice /usr/local/bin/
ENTRYPOINT ["slice"]
This time, we'll create a container image based on Ubuntu 22.04 using musl static links.
FROM rust:latest as builder
RUN rustup target add "$(uname -m)"-unknown-linux-musl
WORKDIR /work
RUN git clone https://github.com/ChanTsune/slice /work/.
RUN cargo build --release --target "$(uname -m)"-unknown-linux-musl
RUN strip /work/target/"$(uname -m)"-unknown-linux-musl/release/slice -o /slice
FROM ubuntu:22.04
COPY --from=builder /slice /usr/local/bin/
ENTRYPOINT ["slice"]
Let's create a container image based on theAlpine distribution using a musl static link.
FROM rust:latest as builder RUN rustup target add "$(uname -m)"-unknown-linux-musl WORKDIR /work RUN git clone https://github.com/ChanTsune/slice /work/. RUN cargo build --release --target "$(uname -m)"-unknown-linux-musl RUN strip /work/target/"$(uname -m)"-unknown-linux-musl/release/slice -o /slice FROM alpine COPY --from=builder /slice / ENTRYPOINT ["slice"]
If we compare the size of a glibc container image and a musl container image on Ubuntu 22.04 and a musl container image on Alpine, we can see that the container image with musl is smaller.
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
slice distroless-musl d38a74f8568a 11 seconds ago 3.52MB
slice alpine-musl e3abb5f0aace 39 seconds ago 8.4MB
slice ubuntu22.04-musl 467edd130e79 About a minute ago 78.9MB
slice ubuntu22.04-glibc 09fe5ad40d56 3 minutes ago 78.8MB
In the Ubuntu environment, using glibc or musl doesn't make much difference in the size of the container image, but in the Alpine distribution, you can see that the container image size is reduced by about a tenth. This shows that by utilizing Alpine Linux with static builds, we can make our container images lightweight and reduce deployment time.
Conclusion
Using static links in programs that use standard C libraries can simplify the process of deploying Linux binaries. It also reduces the size of the container image compared to dynamic links, and makes deployment convenient regardless of the distribution. When you replace glibc with musl, you benefit not only from the difference in container image size, but also from features newly supported by musl, such as mDNS (a multicast-DNS-based zero config system) and NUMA clusters. Furthermore, if you use distroless, which is distributed by Google to better utilize musl, as your default container image, you can deploy and take advantage of smaller container images.
This post is automatically translated from Korean