New FAMILUG

The PyMiers

Saturday 7 November 2020

Kiến trúc Docker, phỏng vấn, best practice

Bài này không giới thiệu về docker, "lúc nào rảnh" chắc sẽ có bài đó.

Bài này nói về kiến trúc của docker, các công nghệ liên quan phía dưới mà docker sử dụng - thường có tác dụng lớn khi 1) chém gió lên mặt 2) phỏng vấn.

Ngoài ra có kèm theo một số best-practice khi build docker image để có size nhỏ/build nhanh hơn.

Photo by Andy Li on Unsplash

Kiến trúc của Docker

Docker là một phần mềm viết bằng Golang, theo kiến trúc client-server.

Nghe hình thức thì vậy, mô hình này nham nhảm khắp nơi khi dùng các database: mysql sẽ có mysqld và mysql client (cli, GUI) ... redis có redis-server và redis-cli...

Docker có dockerd và docker-cli.

docker-cli tương tác với dockerd (docker daemon) qua network socket, khi chạy trên cùng 1 máy, nó dùng UNIX socket để giao tiếp 

# ps xau | grep docker[d]
root      2640  0.0  4.4 937168 91652 ?        Ssl  12:14   0:01 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
# file /run/containerd/containerd.sock
/run/containerd/containerd.sock: socket
root@buster:~# docker run -d alpine sh -c "sleep 100"
c9ebf8e668e4bc354244d5289c480821f6182564dbbb85bde57b216f6889ef66
root@buster:~# strace -e connect docker ps
connect(3, {sa_family=AF_UNIX, sun_path="/var/run/nscd/socket"}, 110) = -1 ENOENT (No such file or directory)
connect(3, {sa_family=AF_UNIX, sun_path="/var/run/nscd/socket"}, 110) = -1 ENOENT (No such file or directory)
connect(3, {sa_family=AF_UNIX, sun_path="/var/run/docker.sock"}, 23) = 0
CONTAINER ID        IMAGE               COMMAND               CREATED             STATUS              PORTS               NAMES
c9ebf8e668e4        alpine              "sh -c 'sleep 100'"   17 seconds ago      Up 16 seconds                           compassionate_noyce
+++ exited with 0 +++


Ở đây dùng strace để thấy docker-cli (câu lệnh docker ps), kết nối vào socket để giao tiếp với dockerd.

Mô hình này cũng đồng nghĩa với việc, docker-cli chỉ là bộ điều khiển, ra lệnh, còn dockerd sẽ thực hiện hầu hết các công việc (pull image, build image, run container...). docker-cli có thể kết nối đến các dockerd khác qua network, option -H để chọn địa chỉ IP để connect tới: 
# docker -H 127.0.0.1 ps
Cannot connect to the Docker daemon at tcp://127.0.0.1:2375. Is the docker daemon running? 

Port 2375 là port mặc định của dockerd khi listen trên network (thay vì localhost).

Các công nghệ bên dưới docker

Docker phụ thuộc vào nhiều tính năng của Linux Kernel. Tính tới thời điểm này (COVID năm thứ 2 - 2020), docker vẫn chỉ chạy trên Linux.

Khi cài docker trên MacOS/Windows, thực ra bộ cài sẽ cài 1 cái máy ảo linux rồi chạy docker trên đó, vậy nên gặp rất nhiều vấn đề, hiệu năng kém hơn nhiều so với chạy trên Linux-based OS như Ubuntu/Debian/Fedora/RHEL...Câu chuyện cũng dần sang trang mới cho Windows khi có WSL2, khiến chạy docker trên Windows sẽ dần ngon như trên Linux-based OS https://docs.docker.com/docker-for-windows/wsl/

 Namespaces

Namespaces là tính năng của Linux kernel phiên bản mới (3.x trở đi), nó cho phép tạo ra các "namespace" riêng biệt. Ví dụ trước đây, chỉ có thể có 1 PID 1 trên mỗi OS đang chạy, thì giờ có thể có PID 1 ở mỗi namespace khác nhau. 

  • The pid namespace: Process isolation (PID: Process ID).
  • The net namespace: Managing network interfaces (NET: Networking).
  • The ipc namespace: Managing access to IPC resources (IPC: InterProcess Communication).
  • The mnt namespace: Managing filesystem mount points (MNT: Mount).
  • The uts namespace: Isolating kernel and version identifiers. (UTS: Unix Timesharing System).

mỗi container sẽ dùng namespace của riêng mình, khiến chúng hoàn toàn độc lập và không bị lẫn với các container khác.

Control groups (cgroups)

Theo man 7 cgroups

       Control  cgroups,  usually  referred  to as cgroups, are a Linux kernel feature which allow processes to be organized into hierarchical
       groups whose usage of various types of resources can then be limited and monitored.  The kernel's cgroup interface is provided  through
       a  pseudo-filesystem  called  cgroupfs.  Grouping is implemented in the core cgroup kernel code, while resource tracking and limits are
       implemented in a set of per-resource-type subsystems (memory, CPU, and so on).


cgroup là 1 tính năng của Linux Kernel, mỗi cgroup sẽ là một bộ các process được monitor và gán giới hạn về tài nguyên (CPU/Memory...). 

Nhờ tính năng này, người dùng có thể giới hạn mỗi container dùng bao nhiêu CPU/RAM.

Union Filesystem (UnionFS/OverlayFS)

Một loại filesystem có khả năng tạo các layer, và merge các layer lại với nhau. Khi layer1 chứa file a và b, layer 2 chứa file b và c, thì khi merge lại, ta sẽ thấy 3 file a b c. Đây là tính năng giúp tạo các docker layer.

Các filesystem có tính năng của UnionFS: AUFS, btrfs, vfs, and DeviceMapper.

Lý thuyết chém gió/ phỏng vấn chỉ có vậy, vì thường cũng chỉ thuộc từ khóa đề hù nhau, chứ hỏi thêm nữa nó hỏi lại lại không biết nói gì :))

Về mặt thuần công nghệ, docker không được đánh giá cao khi mới xuất hiện, do nó chỉ tạo ra 1 sản phẩm dựa trên các công nghệ có sẵn nói trên, hay nói cách khác, 1 sysadmin hoàn toàn có thể làm được điều tương tự qua các câu lệnh mà không cần dùng "docker". Nó cũng không mới, khi trên FreeBSD tồn tại một công nghệ có tên jails từ rất lâu rồi.

Sự thành công của docker nằm ở chỗ nó chuẩn hóa công việc trên, tạo 1 hệ sinh thái (ecosystem) mà các lập trình viên có thể chia sẻ các image (docker hub), tạo 1 bộ sản phẩm giúp chạy code như nhau trên 3 hệ điều hành phổ biến Ubuntu/Windows/MacOS. Docker giải quyết được 1 vấn đề kinh điển trong ngành phần mềm: "code này chạy trên máy tôi mà" (nhưng mang sang máy khác thì lại không chạy).

Nghe giống như máy ảo virtualbox/VMWare, nhưng docker nhẹ hơn nhiều do không thực hiện giả lập phần cứng như các hệ thống ảo hóa này.

Tuy không ấn tượng về mặt công nghệ, nhưng docker lại rất thành công về mặt phổ biến, khiến ngày nay các hệ thống mới đều mặc định là phải dùng container/kubernetes. Một điều chua cay đáng chú ý, là công ty Docker, thì lại thất bại về mặt tài chính, do không tìm ra được cách kiếm NHIỀU tiền dựa trên docker. Ván bài chính là docker-swarm đã thất bại thảm hại trước Kubernetes khi các ông lớn cloud lần lượt nhảy vào làm GKE, EKS, AKS.

Docker objects

Docker objects là tên gọi chung cho các khái niệm trong docker: images, containers, networks, volumes, plugins,...

image

Mỗi image là 1 read-only template với các câu lệnh để chạy container. Thông thường các image sẽ base trên các image khác, ví dụ python:ubuntu image được tạo ra bằng cách chạy thêm các câu lệnh cài đặt trên image ubuntu.

Để tạo 1 image mới, người dùng tạo 1 file tên là Dockerfile rồi viết vào đó các câu lệnh:

# cat Dockerfile
FROM ubuntu
RUN apt-get update && apt-get install -y --no-install-recommends python3 && rm -rf /var/lib/apt

Chạy lệnh sau để build image

root@buster:~# cat Dockerfile  | docker build -t mypython -
Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM ubuntu
 ---> d70eaf7277ea
Step 2/2 : RUN apt-get update && apt-get install -y --no-install-recommends python3 && rm -rf /var/lib/apt
 ---> Using cache
 ---> 4b5492751a0a
Successfully built 4b5492751a0a
Successfully tagged mypython:latest

Nhìn chung, các "step" khi build sẽ tạo ra 1 layer mới, chồng lên các layer trước, và việc tạo layer mới này chỉ thực hiện khi có thay đổi, khiến cho quy trình build image trở nên nhanh/nhẹ hơn nhiều so với build 1 máy ảo truyền thống (VMWare/Virtualbox).

Container

Khi mang image đi chạy, ta có 1 container, mọi thứ sinh ra khi chạy container sẽ bị mất đi khi tắt container, trừ khi ta lưu dữ liệu thay đổi lên các "volume" hay database bên ngoài/ hoặc gõ docker commit để tạo image mới từ container đang dùng.

Các object khác nằm ngoài phạm vi bài viết này, tham khảo tại doc của docker và các bài viết khác trên trang này

Các best-practice khi build image nhanh/nhẹ

Gộp nhiều câu lệnh làm một

Mỗi câu lệnh RUN/COPY/ADD trong Dockerfile sẽ tạo ra 1 layer mới, vậy nên cần chú ý gộp các câu lệnh thành 1 câu (dùng && trong shell) để thu được ít layer nhất.

Ví dụ trích từ file Dockefile chính thức của Python trên Debian buster:


RUN apt-get update && apt-get install -y --no-install-recommends \
libbluetooth-dev \
tk-dev \
uuid-dev \

&& rm -rf /var/lib/apt/lists/*

Tận dụng tính năng cache để tránh build lại

Docker sẽ không build lại layer nếu câu lệnh không thay đổi. Sau khi 1 layer phải build lại do có thay đổi, tất cả các câu lệnh theo sau nó sẽ phải build lại hết.

Hai câu lệnh COPY và ADD hay thay đổi nhất. Vì vậy, hãy đặt COPY hay ADD ở phần cuối cùng của file.

COPY/ADD dùng để copy 1 file/thư mục từ máy vào docker image, nó dựa trên checksum của các file để tính xem có thay đổi gì không. Nếu không có file nào thay đổi, 2 lệnh này cũng sẽ không gây build lại layer.

Khi có 1 file thay đổi hay xuất hiện file mới trong thư mục, chúng sẽ build lại layer.

Tách riêng file ít khi thay đổi ra 1 câu lệnh COPY/ADD

Trong code Python, khi file requirements.txt thay đổi, ta sẽ muốn chạy lại bước pip install -r requirements.txt để update dependency mới. Nhưng file này không thay đổi hàng ngày, nên tránh việc phải chạy pip install mỗi lần build bằng cách tách riêng file này ra 1 dòng:

COPY requirements.txt /app/requirements.txt
RUN pip install -r requirements.txt
COPY . /app

COPY hay ADD

COPY

ADD ngoài tính năng như COPY thì còn có 1 số tính năng khác như: giải nén file nén, tải URL. Vậy nên khi muốn COPY thì dùng COPY, và nếu muốn giải nén/ tải file hãy dùng ADD.

COPY, ADD và RUN sẽ tạo layer

nên dùng ít các câu lệnh này để giảm kích thước image.

Tận dụng cache

COPY, ADD sẽ dựa trên nội dung (dùng checksum, không tính last-accessed time, last-modifed time) các file/thư mục để xem có thực hiện lại lệnh COPY/ADD hay dùng lại cache của lần build trước.

Các câu lệnh khác chỉ chạy lại khi có thay đổi, kể cả RUN, chỉ dựa vào string của dòng đó.

Sử dụng .dockerignore 

để Docker bỏ qua các file nhất định khi tính checksum

Ví dụ 

**/*.pyc

sẽ bỏ qua các file .pyc trong thư mục hiện tại (và cả các thư mục con).


Dùng Multistage builds

Tính năng mới từ Docker 17, để build với 1 Dockerfile thay vì 1 file để build, 1 file để chạy

https://docs.docker.com/develop/develop-images/multistage-build/

Dùng FROM scratch

để tối ưu về dung lượng và bảo mật - đặc biệt hữu ích với static-binary như Golang.

https://docs.docker.com/develop/develop-images/baseimages/

Đề phòng với alpine

alpine là một Linux distro rất nhỏ, tối ưu về kích thước cho các container, thường chỉ nặng 5-7MB so với Ubuntu 70MB

# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
mypython            latest              4b5492751a0a        38 minutes ago      106MB
ubuntu              latest              d70eaf7277ea        2 weeks ago         72.9MB
alpine              latest              d6e46aa2470d        2 weeks ago         5.57MB

 Nhưng nhắm mắt dùng alpine sẽ nhiều khi dẫn tới thảm họa.

Alpine dùng musl libc thay vì glibc lâu đời của Linux, thư viện này chưa được "thực chiến" nhiều nên nhiều khi gặp các bug đau đầu để xử lý.

Riêng với Python, dùng Alpine khiến tốc độ cài pip install thường chậm hơn, do các thư viện Python có sẵn wheel cho libc, chỉ việc tải về, thì hầu như không có wheel cho musl, khiến phải compile mỗi lần cài đặt.

Tham khảo

https://pythonspeed.com/articles/base-image-python-docker-images/

https://docs.docker.com/develop/develop-images/dockerfile_best-practices/

https://docs.docker.com/get-started/overview/

https://jvns.ca/blog/2019/11/18/how-containers-work--overlayfs/

https://www.redhat.com/sysadmin/cgroups-part-one

HVN at "học python tại PyMi" https://pymi.vn and https://www.familug.org  

1 comment: