用Docker打包Python執行環境

orion 發表於 2022-05-13

雖然Docker作為部署環境打包映象的工具,和我的科研並沒有直接的關係。但我覺得在專案中運用Docker來打包環境依賴也可以大大提高工作效率,於是準備專門學習一下Docker。

1. Docker基礎

1.1 Docker架構

Docker使用客戶端伺服器架構。Docker客戶端與Docker守護程式會話,後者複雜構建、執行和分發Docker容器的繁重工作。Docker客戶端和守護程式可以在同一系統執行,也可以將Docker客戶端連線到遠端Docker守護程式。Docker客戶端和守護程式通過REST API(採用一種簡潔的URL風格規範)通訊,其底層基於UNIX套接字或網路介面。其架構示意圖如下:

NLP多工學習

其中,Docker 守護程式 (dockerd) 監聽Docker API 請求並管理Docker物件,例如映象、容器、網路和磁碟分卷。守護程式還可以與其他守護程式通訊以管理Docker服務。而Docker 客戶端 ( docker) 是使用者與 Docker 互動的主要方式。當我們使用諸如docker run之類的命令時,客戶端會將這些命令傳送到dockerd執行它們。docker命令使用 Docker API。Docker 客戶端可以與多個守護程式通訊。

Docker登錄檔儲存Docker映象(你可以類比為Maven的repo)。Docker Hub 是一個任何人都可以使用的公共登錄檔,並且 Docker 預設配置為在Docker Hub上查詢映象。我們也可以執行自己的私有登錄檔。我們可以呼叫docker pull從登錄檔中拉取映象。當我們docker run命令時,系統會從先從本地尋找映象,如果本地找不到,則會從Docker Hub拉取。當我們使用docker push命令時,映象會被推送到我們配置的登錄檔中。可以看出,Docker映象版本控制和Git類似。

1.2 Docker物件

當我們在使用Docker時,我們就正在建立和使用映象、容器、網路、磁碟分卷、外掛和其他物件了。下面簡要介紹一下其中的映象和容器物件。

  • 映象 映象可視為一個只讀模板,其中包含建立 Docker 容器的指令。通常,一個映象基於另一個映象,並帶有一些額外的自定義。例如可以基於現有的ubuntu映象,來構建安裝有其它應用程式的映象。要構建我們自己的映象,需要使用簡單的語法建立一個Dockerfile ,用於定義建立和執行映象所需的步驟。

  • 容器
    容器是映象的可執行例項(類似於程式和程式的關係)。我們可以使用 Docker API 或 CLI 建立、啟動、停止、移動或刪除容器。我們可以將容器連線到一個或多個網路。

2. 啟動Docker程式並執行映象

2.1 啟動Docker守護程式

Linux

Linux上的docker同時包括客戶端和守護程式兩部分,故安裝好docker後,只需要用以下命令即可執行docker守護程式:

$ sudo service docker start # Ubuntu/Debian

如果您是RedHat/Centos,則需要執行:

$ sudo systemctl start docker

MacOS

然而,在Mac上docker二進位制僅僅是client部分(因為docker守護程式使用了一些Linux核心的特點),我們不能使用它來執行docker守護程式。所以,我們還需要安裝docker-machine來建立一個虛擬機器並將守護程式執行在上面。如果你的Mac上已經有brew,可以直接執行以下命令安裝:

brew install docker-machine

然後啟動docker-machine:

(base) [email protected] ~ % brew services start docker-machine
==> Successfully started `docker-machine` (label: homebrew.mxcl.docker-machine)

2.2 執行映象

之後我們就可以嘗試執行Docker映象了。比如我們下面用docker run命令執行docker/getting-started映象:

(base) [email protected] ~ % docker run -d -p 80:80 docker/getting-started 
Unable to find image 'docker/getting-started:latest' locally
latest: Pulling from docker/getting-started
9981e73032c8: Pull complete 
e5f90f35b4bc: Pull complete 
ab1af07f990a: Pull complete 
bd5777bb8f79: Pull complete 
a47abff02990: Pull complete 
d4b8ebd00804: Pull complete 
6bec3724f233: Pull complete 
b95ca5a62dfb: Pull complete 
Digest: sha256:b558be874169471bd4e65bd6eac8c303b271a7ee8553ba47481b73b2bf597aae
Status: Downloaded newer image for docker/getting-started:latest
cc167092ff76941a25fe51da25fbbfe6a0a70cc07171fa5f56707f3bf7383e6a

可以看到由於沒有在本地找到docker/getting-started:latest映象,Docker從遠處Docker Hub登錄檔上pull下來。

我們用docker ps檢視目前在執行的映象例項(即容器):

(base) [email protected] ~ % docker ps       
CONTAINER ID   IMAGE                    COMMAND                  CREATED          STATUS          PORTS                NAMES
cc167092ff76   docker/getting-started   "/docker-entrypoint.…"   29 minutes ago   Up 29 minutes   0.0.0.0:80->80/tcp   epic_lehmann

可以用docker stop終止映象執行:

(base) [email protected] ~ % docker stop cc167092ff76
cc167092ff76
(base) [email protected] ~ % docker ps   
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

docker images檢視有哪些本地映象:

(base) [email protected] ~ % docker images                                 
REPOSITORY               TAG       IMAGE ID       CREATED       SIZE
docker/getting-started   latest    157095baba98   4 weeks ago   27.4MB

3. 用Docker打包Python環境

接下來我們看如何用Docker打包一個Python環境。
首先,我們編寫一個Python小Demo:

import numpy as np
import matplotlib.pyplot as plt
x = np.arange(-10, 10, 0.01)
y = x**2
plt.plot(x, y)
plt.savefig("/out/quad.png") 
# 此處的/out為容器內的絕對路徑,無需手動建立,
# 後面我們會設定掛載引數自動生成該目錄

然後我們編輯好requirements.txt

numpy==1.21.3
matplotlib==3.4.3  

再編輯好Dockerfile:

# syntax=docker/dockerfile:1

FROM python:3.9-slim-buster

WORKDIR /draw_quad

COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt 

COPY . .

CMD [ "python3", "draw_quad.py"]

我們來細細看Dockerfile每一部分。

首先,# syntax是指解析器指令。這裡使用docker/dockerfile:1,即始終指向版本1語法的最新版本。

之後,我們需要告訴Docker我們在應用中使用什麼基礎映象。由於Docker映象可以從其它映象繼承,因此我們並不構建自己的基礎映象,而是使用官方的Python映象,即FROM python:3.9-slim-buster

然後我們建立一個工作目錄/draw_quad,即後續命令的預設執行路徑。這樣我們後面就不必輸入完整的檔案路徑,而是可以使用基於工作目錄的相對路徑。如COPY requirements.txt requirements.txt其實是將requirements(第一個引數)複製到到工作目錄中(第二個引數)。

接著,我們將requirements.txt 放入映象後,就可以使用RUN命令來執行pip3 install了,這和我們在本地安裝的經驗完全相同,不過這次是將模組安裝到映象中。

此時,我們有了一個基於Python 3.9的映象,並且已經按照了我們的依賴項。下一步我們繼續用COPY
命令將原始碼新增到映象中,即DockerFile中的COPY . .

之後,我們還需要Docker當我們的映象在容器中執行時我們想要執行什麼命令,即CMD [ "python3", "draw_quad.py"]

最終的專案目錄如下:

draw
|____ draw_quad.py
|____ requirements.txt
|____ Dockerfile

然後我們就可以構建docker映象了(用--tag引數指定映象名稱):

(base) [email protected] draw % docker build --tag draw .                 
[+] Building 9.1s (14/14) FINISHED                                                                                             
 => [internal] load build definition from Dockerfile                                                                      0.0s
 => => transferring dockerfile: 37B                                                                                       0.0s
 => [internal] load .dockerignore                                                                                         0.0s
 => => transferring context: 2B                                                                                           0.0s
 => resolve image config for docker.io/docker/dockerfile:1                                                                4.9s
 => CACHED docker-image://docker.io/docker/dockerfile:[email protected]:443aab4ca21183e069e7d8b2dc68006594f40bddf1b15bbd83f5137bd  0.0s
 => [internal] load build definition from Dockerfile                                                                      0.0s
 => [internal] load .dockerignore                                                                                         0.0s
 => [internal] load metadata for docker.io/library/python:3.9-slim-buster                                                 3.9s
 => [1/5] FROM docker.io/library/python:[email protected]:830e161433edfe047a23ebc99c12ee0eb1dc0a50e6b5f1c98e869ac27  0.0s
 => [internal] load build context                                                                                         0.0s
 => => transferring context: 594B                                                                                         0.0s
 => CACHED [2/5] WORKDIR /draw_quad                                                                                       0.0s
 => CACHED [3/5] COPY requirements.txt requirements.txt                                                                   0.0s
 => CACHED [4/5] RUN pip3 install -r requirements.txt                                                                     0.0s
 => [5/5] COPY . .                                                                                                        0.0s
 => exporting to image                                                                                                    0.0s
 => => exporting layers                                                                                                   0.0s
 => => writing image sha256:18f3a254f4ce46faa17142ece6bfd442e9157e79510ca60a789ab4d4b1a12498                              0.0s
 => => naming to docker.io/library/draw                                        0.0s

我們輸入docker images命令可以看到名稱為draw的映象已經構建成功。

(base) [email protected] Draw % docker images
REPOSITORY               TAG       IMAGE ID       CREATED          SIZE
draw                     latest    f1fc30becc34   46 seconds ago   251MB

然後就可以執行映象了(包含檔案系統掛載操作):

(base) [email protected] draw % docker run -d -v ${PWD}/out:/out draw
0e04d81d254fcd963924ee2492b82a6c895789525f09943b43ce0b46ac0d63a9

注意,${PWD}/out為宿主機的目錄,意思為當前目錄下的out資料夾,如果不存在則會自動為我們建立。/out為該容器中的絕對路徑,在容器啟動會自動建立/out目錄。

我們可以看到,quad.png成功在宿主機當前目錄下的out檔案中生成:

(base) [email protected] draw % ls out           
quad.png

參考