Tag : Container

  • High sky and plump horses, and Container Dieting

    By Mario (Manseok) Cho

    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

    20 September 2023

We're here for you!

Complete the form and we'll be in touch soon

Contact Us

Headquarter & HPC Lab

Namyoung Bldg. 4F/5F, 34, Seolleung-ro 100-gil, Gangnam-gu, Seoul, Republic of Korea

© Lablup Inc. All rights reserved.