面向數據科學家的 Docker 最佳實踐

面向數據科學家的 Docker 最佳實踐

作者 | Thushan Ganegedara

頭圖 | CSDN 下載自東方 IC

作為一名數據科學家,每天我都要與 Docker 打交道。創建鏡像、更新內容和編寫 Python 腳本已經成為了我的日常工作。在這個過程中,我常常感嘆:“多麼希望我以前就能知道這些方法。”

因此,我將在本文中探討一些數據科學項目中會用到的 Docker 最佳實踐。雖然未能做到詳盡,但我涵蓋了數據科學家大部分的工作。

本文假定你對 Docker 的基礎知識有一定的瞭解。例如,你知道 Docker 的用途,而且能夠輕鬆地編寫 Dockerfile,並且瞭解 Docker 命令(如 RUN、CMD 等)。如果你缺乏對這些基礎知識的瞭解,那麼請先閱讀 Docker 官方網站上的文檔。

面向数据科学家的 Docker 最佳实践

為什麼選擇 Docker?

自發布以來,Docker 已席捲全球。在 Docker 出現之前的日子裡,虛擬機填補了這一領域的空白。然而,Docker 提供的功能遠超過了虛擬機。

面向数据科学家的 Docker 最佳实践

Docker 的優勢

  • 隔離:不管基礎操作系統/基礎架構、安裝好的軟件、更新等如何變化,Docker 都能提供隔離的環境。

  • 輕量級:共享 OS 內核,不需要為每個容器分配 OS 內核。

  • 性能:輕巧,可以在同一操作系統上同時運行多個容器。

面向数据科学家的 Docker 最佳实践

Docker 入門

Docker 包含了三個重要的概念。

鏡像:這是由可運行的庫和二進制文件組成的集合,代表一個開發/生產/測試所需的環境。你可以通過以下方式下載/創建鏡像。

  • 從鏡像倉庫中提取,例如:docker pull alpine。這個命令將在計算機的本地查找一個名為 alpine 的鏡像,如果找不到,就去 Dockerhub 中查找。

  • 使用 Dockerfile 在本地構建鏡像,例如:docker build . -t <image>:<image>。這個命令不是下載/拉取鏡像,而是構建自己的鏡像。這種說法也不完全,因為 Dockerfile 包含以 FROM <base-image>開頭的行,該行會查找以其開頭的基本鏡像,如果找不到,則從 Dockerhub 中提取。/<base-image>/<image>/<image>

容器:容器是鏡像的運行實例。你可以通過如下命令建立一個容器:`docker container run <arguments> <image> <command> 。例如,根據 alpine 創建一個容器的命令為:docker container run -it alpine /bin/bash。/<command>/<image>/<arguments>

卷:我們利用捲來存儲容器使用的數據(例如日誌、下載的數據等)。此外,卷可以在多個容器之間共享。你可以通過以下幾種方式使用卷。

  • 創建卷:卷的創建命令為:docker volume create <volume>。請注意,如果刪除卷,則存儲在其中的信息會丟失。/<volume>

  • 綁定掛載卷:將宿主上現有的卷綁定掛載到容器的命令為:-v <source>:<target>。例如,如果你需要將卷/my_data 掛載到容器,並作為卷/data,則可以執行以下命令:docker container run -it -v /my_data:/data alpine /bin/bash。你在掛載的時候所做的更改將反映在宿主上。/<target>/<source>

一、創建鏡像

1. 鏡像不宜過大,應避免緩存

構建鏡像時,首先你必須做兩件事:

  • 安裝 Linux 軟件包

  • 安裝 Python 庫

在安裝這些軟件包和庫時,軟件包管理器將緩存數據,以便你再次安裝時能夠使用本地的數據。但這會增加鏡像的大小,完全沒有必要。Docker 鏡像應該儘可能保持輕量級。

在安裝 Linux 軟件包時,請記住在 apt-get installl 命令中添加一行,就可以刪除所有的緩存數據。

<code>RUN apt-get update && apt-get install tini && \\
rm -rf /var/lib/apt/lists/*/<code>

在安裝 Python 軟件包時,為避免緩存,請按照以下操作:

<code>RUN pip3 install <library-1> <library-2> --no-cache-dir`/<library-2>/<library-1>/<code>

2. 在 requirements.txt 中單獨指定 Python 庫

最好在 requirements.txt 中單獨指定 Python 庫,然後通過以下命令安裝庫。

<code>RUN pip3 install -r requirements.txt --no-cache-dir/<code>

這種做法可以很好地區分 Dockerfile 和 Python 的庫。此外,如果你有多個 Dockerfile(例如用於生產/開發/測試),而且希望它們都安裝相同的庫,則可以輕鬆地重用這個命令。requirements.txt 文件中只包含了一堆庫名。

<code>numpy==1.18.0
scikit-learn==0.20.2
pandas==0.25.0/<code>

3. 修改庫版本

請注意,你需要在 requirements.txt 中凍結要安裝的版本,這非常重要。因為否則,每次構建 Docker 鏡像時,你都有可能安裝不同的版本,那麼就會面臨“依賴性地獄”。

二、容器的運行

1. 多使用非 root 用戶

在運行容器時,如果你不指定運行的用戶身份,則會默認使用 root 用戶。我曾經很天真,特別喜歡使用 sudo 或 root。然而,我遭到了慘痛的教訓,擁有不必要的特權會引發不必要的麻煩。

如果你想以非 root 用戶身份運行容器,只需運行如下命令:

<code>docker run -it -u <user-id>:<group-id> <image-name> <command>/<image-name>/<group-id>/<user-id>/<code>

如果你想進入現有的容器中,則可以運行如下命令:

<code>docker exec -it -u <user-id>:<group-id> <container-id> <command>/<container-id>/<group-id>/<user-id>/<code>

例如,你可以通過將<user-id>指定為$(id -u),<group-id>指定為$(id -g),使容器內的用戶 ID 和組 ID 與宿主匹配。/<group-id>/<user-id>

注意,不同的操作系統分配用戶 ID 和組 ID 的方式也不同。例如,MacOS 上的用戶 ID/組 ID 可能是 Ubuntu 容器內預先分配/保留的用戶 ID /組 ID。

2. 創建一個非特權用戶

我們能夠以非 root 用戶身份從宿主登錄到容器。但是,如果你以這種方式登錄,那麼你在容器中沒有用戶名。因為,很明顯容器不知道該用戶 ID 來自何處。而且,每次你想啟動或 exec 容器的時候,都需要記住並輸入這些用戶 ID 和組 ID。為了避免這些麻煩,你可以在 Dockerfile 中創建用戶/組。

<code>ARG UID=1000
ARG GID=1000/<code>
  • 首先將 ARG UID=1000 和 ARG GID=1000 添加到 Dockerfile 中。UID 和 GID 是容器中的環境變量,你可以在 docker build 階段將值傳遞給該容器(默認為 1000)。

  • 然後通過 RUN groupadd -g $GID john-group,在鏡像中添加帶有組 ID GID 的 Linux 組。

  • 接下來,通過 useradd -N -l -u $UID -g john-group -G sudo john,在鏡像中添加帶有用戶 ID UID 的 Linux 用戶。在此,我們將 john 添加到 sudo 組中。但這是可選的。如果你百分百確定你不需要 sudo 權限,則可以忽略這一步。

然後,在鏡像構建期間,你可以為這些參數傳遞值,例如:

<code>docker build <build> -t <image>:<image> --build-arg UID=<uid-value> --build-arg GID=<gid-value>/<uid-value>/<image>/<image>/<build>/<code>

舉個例子:

<code>docker build . -t docker-tut:latest --build-arg UID=$(id -u) --build-arg GID=$(id -g)
/<code>

擁有一個非特權用戶在運行不需要 root 權限的進程時非常有用。例如,如果你的 Python 腳本只是從目錄中讀寫數據,那麼完全沒必要用 root 身份運行。另外,還有一個好處,如果容器內的用戶 ID 和組 ID 完全與宿主匹配,則你創建的所有文件都將擁有宿主用戶的所有權。因此,如果你掛載這些文件(或創建新文件),那麼就像在宿主上創建的一樣。

三、創建卷

1. 利用卷做分離

作為數據科學家,你需要處理各種數據、模型和代碼。你可以將代碼放在一個卷中(比如/app),將數據放在另一個卷中(比如/data)。這樣你的 Docker 鏡像就能擁有一個很好的結構,且可以擺脫所有宿主級別的依賴關係。

我所說的這種依賴是什麼意思?假設你將代碼放在/home/<user>/code/src 中。如果你將/home/<user>/code/src 複製/掛載到卷/app 中,而/home/<user>/code/data 掛載到卷/data 中,那麼即便宿主上的代碼和數據位置發生變化,也沒有關係。一旦掛載到 Docker 鏡像後,它們的位置就不會發生變化。因此,你可以按照如下步驟在 Python 腳本中定義這些路徑。/<user>/<user>/<user>

<code>data_dir = "/data"
model_dir = "/models"
src_dir = "/app"/<code>

你可以通過以下方法,將必要的代碼和數據複製到鏡像中:

<code>COPY test-data /data
COPY test-code /app/<code>

請注意,test-data 和 test-code 都是宿主上的目錄。

2. 在開發過程中掛載目錄

掛載的好處在於,容器中的變化都會反映到宿主上。在開發和調試項目時,這種做法非常方便。我們來看一個例子。

假設你通過以下命令創建了 docker 鏡像:

<code>docker build <build-dir> <image-name>:<image-version>/<image-name>/<build-dir>/<code>

那麼,現在可以使用以下命令建立鏡像的一個容器:

<code>docker run -it <image-name>:<image-version> -v /home/<user>/my_code:/code/<user>/<image-version>/<image-name>/<code>

然後,你就可以在容器內運行代碼,同時還能進行調試,且代碼的變更都會反映到宿主上。而這主要得益於在容器中使用相同的宿主用戶 ID 和組 ID。你所做的所有改動都好似來自宿主上的這個用戶。

3. 永遠不要掛載宿主的關鍵目錄

我曾做過一件好笑的事,我將機器的主目錄掛載到 Docker 容器中,而且還想修改主目錄的權限。不用說,我無法再登陸系統了,並花了好幾個小時才修復。因此,你應該只掛載所需的內容。

例如,假設開發期間你有三個想要掛載的目錄:

<code>/home/<user>/my_data
/home/<user>/my_code
/home/<user>/my_model/<user>/<user>/<user>/<code>

你可能很想用一行代碼直接掛載/home/<user>目錄,但是編寫三行代碼來分別掛載這些單獨的子目錄,這絕對值得,因為可以為你節省數小時的艱辛工作(甚至是幾天)。/<user>

四、其他小貼士

1. 瞭解 ADD 和 COPY 之間的區別

你可能知道 Docker 有兩個命令叫做 ADD 和 COPY,那麼二者究竟有何不同?

  • ADD:你可以使用 ADD 從 URL 下載文件。

  • ADD:當某個壓縮文件(例如 tar.gz)會從指定的位置提取文件時,請使用 ADD。

  • COPY:將某個文件/文件夾複製到容器的指定位置。

2. ENTRYPOINT 和 CMD 之間的區別

我打個比方,你可以把 ENTRYPOINT 當成一輛車,而 CMD 就是車上的控制器(例如加速器、制動器、方向盤)。ENTRYPOINT 本身不會執行任何操作,它只是容納你在容器中執行的操作的盒子。它不會處理你推送到容器的任何命令。

命令 CMD 是容器內實際執行的命令。例如,bash 會在容器內創建一個 shell,那麼你就可以像在 Ubuntu 的普通終端上一樣使用該容器。

3. 將文件複製到現有容器

我們經常犯這樣一個錯誤:創建好容器後,卻忘記將文件添加到鏡像中。構建鏡像需要花費很長時間。有什麼辦法可以避免這個問題,自動將文件添加到現有容器中呢?

你可以使用 docker cp 命令執行該操作。很簡單:

<code>docker cp  <container>:<dest>/<container>/<code>

下次進入容器時,你就可以看到複製的文件躺在<dest>中。但是,請不要忘記修改 Dockerfile,以便能在下次構建時複製必要的文件。/<dest>

英文:Docker Best Practices for Data Scientists

鏈接:https://towardsdatascience.com/docker-best-practices-for-data-scientists-2ed7f6876dff

作者:Thushan Ganegedara,數學科學家。

本文為 CSDN 翻譯,轉載請註明來源出處。


分享到:


相關文章: