Docker 容器化技術能有效確保開發環境一致性,但實際操作中,容器生命週期管理不當會破壞一致性。容器映像是容器的基礎,Docker 會先檢查本地倉函式庫是否存在所需映像,若無則會提取或建立。然而,即使遠端倉函式庫的 latest 標籤更新,本地已有的 latest 映像檔並不會自動更新,這可能導致版本不一致。為確保一致性,需定期清理舊容器和映像檔,並養成更新本地映像檔的習慣。清理指令包含 docker stopdocker rmdocker image prunedocker system prune 等。此外,使用 Docker Compose 能簡化多容器應用程式的管理,透過定義服務、網路和卷,確保開發和生產環境的一致性。同時,使用特定版本標籤、docker pull 更新映像檔、docker build --no-cache 重建映像檔,以及在 docker-compose 中設定 pull_policy: always 也是確保一致性的有效方法。除了過時映像檔,本地卷差異、不同硬體和複雜的容器組合也可能造成環境不一致,需要特別注意。

容器化技術

使用容器化技術是確保開發環境一致性和可重複性的有效方法。容器將程式碼及其依賴項封裝成一個單元,能夠在任何地方執行。這意味著無論是在本地開發還是在生產環境中執行,程式碼的執行環境都保持一致,從而減少了因環境差異而導致的問題。

容器化技術的優勢

容器化技術因 Docker 的工作而變得流行,包括其產品供應以及對開放容器倡議(Open Container Initiative)的貢獻,該倡議致力於制定容器化的標準。本章將使用 Docker 的範例來說明如何使用容器。

例項解析

假設你正在開發一個執行在 Spark 上的資料轉換任務。如果沒有容器化技術,你可能會在本地安裝 Spark 及其依賴項。然而,當另一個開發者加入並安裝了新版本的 Spark 時,生產環境可能因為某些資料消費者執行的 Spark 查詢所需的依賴項而被固定在特定的 Spark 版本上。這樣一來,開發和生產環境中就存在三個不同版本的依賴項,可能導致程式碼在不同環境中的行為差異,且難以重現和調查差異的根本原因。

曾經有過這樣的案例:開發者在使用某個資料函式庫的新功能時,因為開發環境與生產環境版本不一致,導致開發的功能在生產環境中不可用。最終,這項工作不得不被廢棄。

容器生命週期管理

雖然容器化技術有助於建立一致且可重複的開發流程,但容器生命週期的實際情況可能會在實踐中破壞這種一致性。Docker 容器和映像檔可能會像不請自來的房客一樣,除非強制移除,否則不會離開。實際上,這通常是容器化環境之間不比對的主要原因。

讓我們來看看容器的生命週期。容器是從映像檔建立的。映像檔可以指定為倉函式庫中預建映像檔的連結,或透過指定 Dockerfile 來建立映像檔。在這兩種情況下,Docker 在建立容器時首先會檢查本地倉函式庫中是否存在所需的映像檔。如果不存在,它會提取或建立映像檔,並將其新增到本地倉函式庫。

圖示說明:容器生命週期

此圖示顯示了執行帶有 latest 標籤的 Postgres 映像檔的容器生命週期。

內容解密:

  • 此圖示呈現了帶有 latest 標籤的 Postgres 映像檔如何隨時間更新,但本地倉函式庫中的映像檔不會自動更新。
  • 在 T0 時,建立一個使用 postgres:latest 的容器,由於本地沒有對應映像檔,因此會從官方倉函式庫提取。
  • 在 T1 之後,即使官方倉函式庫中的 latest 標籤更新為新的映像檔,本地已經提取的映像檔不會自動更新。
  • 在 T2,容器繼續執行,直到 T3 時被銷毀。
  • 圖示強調了即使用 latest 標籤,本地實際執行的映像檔版本可能與遠端倉函式庫最新的版本不一致,因為舊映像檔仍保留在本地倉函式庫中。

管理容器生命週期以確保一致性

為瞭解決上述問題,需要定期清理舊的或未使用的 Docker 映像檔和容器,以避免版本不一致的問題。此外,也應該養成定期更新本地映像檔的習慣,以確保開發環境與生產環境的一致性。

程式碼範例:清理 Docker 容器和映像檔

# 列出所有正在執行的容器
docker ps

# 停止並刪除所有正在執行的容器
docker stop $(docker ps -aq)
docker rm $(docker ps -aq)

# 刪除未使用的映像檔
docker image prune -af

# 清理未使用的 Docker 資源
docker system prune -af

內容解密:

  1. docker ps:列出所有正在執行的容器,用於檢查目前有哪些容器正在執行。
  2. docker stop $(docker ps -aq):停止所有正在執行的容器,docker ps -aq 用於取得所有容器的 ID。
  3. docker rm $(docker ps -aq):刪除所有停止的容器,同樣使用 docker ps -aq 取得所有容器的 ID。
  4. docker image prune -af:刪除所有未被使用的映像檔,-af 表示強制刪除所有未使用的映像檔,無需確認。
  5. docker system prune -af:清理未使用的 Docker 資源,包括停止的容器、未使用的網路和未使用的映像檔等,同樣是強制執行無需確認。

使用Docker容器開發的挑戰與最佳實踐

在軟體開發過程中,Docker容器提供了一種方便且一致的環境來佈署和執行應用程式。然而,如果不正確地管理容器和映像檔,就可能會遇到一些問題。

保持容器和映像檔更新

當您使用Docker容器時,很容易忘記映像檔可能已經過時。假設您在T0時間下載了postgres:latest映像檔並建立了一個容器。稍後,在T1時間,您再次執行相同的命令來執行Postgres,因為postgres:latest映像檔已經存在於您的本地倉函式庫中,Docker將根據該映像檔建立一個新的容器。然而,這個映像檔可能已經不是最新的,因為Postgres Docker倉函式庫中的postgres:latest映像檔已經被更新了兩次。

為瞭解決這個問題,您需要養成定期刪除和重新建立映像檔和容器的習慣。就像您可能會將程式函式庫或模組固定到特定版本以防止“最新”版本變更造成意外中斷一樣,使用特定版本的Docker映像檔也可以達到同樣的效果。

最佳實踐:

  • 使用docker pull下載新的映像檔,然後重新建立容器。
  • 如果您使用Dockerfile來建立映像檔,請使用docker build --no-cache來重建映像檔,而不依賴任何現有的層。
  • 如果您使用docker-compose,可以在服務定義中指定pull_policy: always來總是取得最新的映像檔。

檢查映像檔和容器的建立時間戳

如果您不確定您的容器是否正在執行最新的映像檔,可以檢查映像檔和容器的建立時間戳。例如,要檢視本地有哪些Postgres映像檔,可以執行:

$ docker images
REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
postgres     latest    f8dd270e5152   8 days ago    376MB
postgres     11.1      5a02f920193b   3 years ago   312MB

要檢視容器正在執行的映像檔,可以先列出容器,使用-a來顯示已停止的容器:

$ docker container ls -a
CONTAINER ID   IMAGE          CREATED          STATUS
2007866a1f6c   postgres       48 minutes ago   Up 48 minutes
7eea49adaae4   postgres:11.1  2 hours ago      Exited (0) 2 hours ago

然後,您可以使用docker inspect來取得有關容器所使用的映像檔的詳細資訊。

其他容器相關問題

除了過時的容器和映像檔之外,還有一些其他原因可能導致“它在我的機器上可以運作”的問題:

本地卷差異

當您掛載本地捲到容器中時,卷中的檔案可能會與容器中的檔案發生衝突。例如,如果您在本地目錄中安裝了Python函式庫,但該函式庫的版本與容器中的版本不同,則可能會導致問題。

不同硬體

如果開發人員使用不同的硬體,例如Linux和Mac,或者Intel和M1晶片的Mac,則可能會遇到不同的Docker問題。

容器組合

當您需要為資料管線開發設定多個服務時,您可能需要使用Docker Compose來組合多個容器。

使用Docker Compose

Docker Compose提供了一種方便的方式來定義和執行多個容器。例如,對於圖5-6所示的流式管線,您可以建立一個Docker Compose檔案,其中包含Kafka、Postgres和資料轉換服務。每個服務條目都會建立一個新的容器。

# transform-docker-compose.yml
version: '3'
services:
  kafka:
    image: confluentinc/cp-kafka:latest
    ports:
      - "9092:9092"
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092

  postgres:
    image: postgres:latest
    environment:
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mypass
      POSTGRES_DB: mydb

  transformation:
    build: .
    depends_on:
      - kafka
      - postgres

內容解密:

此Docker Compose檔案定義了三個服務:Kafka、Postgres和資料轉換。Kafka服務使用confluentinc/cp-kafka:latest映像檔,並將容器的9092埠對映到主機的9092埠。Postgres服務使用postgres:latest映像檔,並設定了環境變數來建立資料函式庫和使用者。資料轉換服務則是根據當前目錄中的Dockerfile建立的,並依賴於Kafka和Postgres服務。

在Docker Compose中建立有效的本地開發環境

在現代軟體開發中,容器化技術已成為主流。Docker Compose作為一種強大的工具,能夠簡化多容器應用程式的開發、測試和佈署流程。本文將探討如何使用Docker Compose建立有效的本地開發環境,涵蓋服務組態、環境變數管理、分享組態等多個關鍵導向。

Docker Compose基礎組態

首先,讓我們看看一個基本的Docker Compose檔案,它定義了多個服務之間的關係和組態。

services:
  kafka:
    image: confluentinc/cp-kafka:7.2.1
    # ... 其他組態
  postgres:
    image: postgres:latest
    # ... 其他組態
  transform:
    image: image/repo/transform_image:latest
    # ... 其他組態
  networks:
    migration:

內容解密:

  • services定義了多個服務,包括kafkapostgrestransform,這些服務共同構成了應用程式的基礎。
  • 每個服務都指定了所使用的Docker映像,例如confluentinc/cp-kafka:7.2.1postgres:latest
  • networks部分定義了服務之間的網路連線,確保transform可以發布訊息到kafka,而postgres可以從kafka讀取資料。

原生程式碼測試與生產環境依賴

在本地開發環境中,我們經常需要在生產級別的依賴下測試原生程式碼變更。以下是一個典型的transform容器定義:

transform:
  image: container/repo/transform_image:latest
  container_name: transform
  environment:
    KAFKA_TOPIC_READ: migration_data
    KAFKA_TOPIC_WRITE: transformed_data
  volumes:
    - ./code_root/transform:/container_code/path
  networks:
    migration:

內容解密:

  • image指定了用於transform服務的Docker映像,這與生產環境中使用的映像相同。
  • environment部分定義了環境變數,例如KAFKA_TOPIC_READKAFKA_TOPIC_WRITE,這些變數被Spark作業用來決定讀取和寫入哪些Kafka主題。
  • volumes掛載了本地的轉換程式碼目錄到容器內的指定路徑,從而實作了原生程式碼變更的實時測試。

使用Dockerfile進行開發

如果需要升級或新增函式庫,可以建立一個Dockerfile來新增依賴項到現有的映像中:

FROM container/repo/transform_image:latest
WORKDIR /
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt

內容解密:

  • FROM指令指定了基礎映像,即生產環境中使用的映像。
  • COPY指令將本地的requirements.txt檔案複製到容器內。
  • RUN指令安裝了指定的Python依賴項。

環境變數管理

環境變數的使用鼓勵良好的實踐,例如避免將憑據資訊硬編碼到Compose檔案中。以postgres容器定義為例:

postgres:
  image: postgres
  container_name: postgres
  environment:
    POSTGRES_USER: ${PG_USER}
    POSTGRES_PASSWORD: ${PG_PASS}
  volumes:
    - pg_data:/var/lib/postgresql/data
  networks:
    - migration

內容解密:

  • environment部分使用${PG_USER}${PG_PASS}等環境變數來設定Postgres使用者名稱和密碼。
  • 這種做法避免了敏感資訊被直接寫入Compose檔案。

分享組態

當有多個服務在不同的Compose檔案中重複使用時,可以將分享組態提取到單獨的檔案中。例如:

# dev-docker-compose.yml
services:
  postgres:
    image: postgres
    environment:
      POSTGRES_USER: ${PG_USER}
      POSTGRES_PASSWORD: ${PG_PASS}
    volumes:
      - pg_data:/var/lib/postgresql/data
    networks:
      - migration
  volumes:
    pg_data:
      name: postgres
  networks:
    migration:

內容解密:

  • 將分享的Postgres服務組態和網路組態提取到dev-docker-compose.yml檔案中。
  • 這種做法有助於保持不同團隊和專案之間的組態一致性。

共用Docker Compose組態以簡化開發環境

在團隊開發中,共用Docker Compose組態是一種重要的實踐,可以確保團隊成員使用相同的環境設定。以下是一個範例,展示如何共用Compose組態以簡化開發環境。

共用Compose檔案

首先,建立一個共用的Compose檔案dev-compose-file.yml,其中包含多個服務共用的設定:

version: '3'
services:
  postgres:
    image: postgres
    environment:
      POSTGRES_USER: ${PG_USER}
      POSTGRES_PASSWORD: ${PG_PASS}
    networks:
      - migration

  api:
    environment:
      POSTGRES_USER: ${PG_USER}
      POSTGRES_PASSWORD: ${PG_PASS}
      POSTGRES_PORT: ${PG_PORT}
      POSTGRES_HOST: ${PG_HOST}
    networks:
      - migration
    depends_on:
      - postgres

內容解密:

此共用Compose檔案定義了兩個服務:postgresapipostgres服務使用官方Postgres映像,並設定了環境變數。api服務則依賴於postgres服務,並共用了部分環境變數。

個別服務的Compose檔案

接下來,為不同的團隊建立個別的Compose檔案。例如,api-docker-compose.yml僅包含與API服務相關的設定:

version: '3'
services:
  api:
    image: container/repo/api_image:dev_tag
    container_name: api
    volumes:
      - ./code_root/api:/container_code/path

內容解密:

此API Compose檔案定義了api服務的映像標籤和容器名稱,並掛載了原生程式碼到容器中。由於共用Compose檔案中已定義了api服務對postgres的依賴,因此無需在此檔案中重複定義。

同樣地,transform-docker-compose.yml包含與轉換服務相關的設定:

version: '3'
services:
  kafka:
    # kafka服務的設定
  transform:
    # transform服務的設定
  api:
    image: container/repo/api_image:latest
    container_name: api

內容解密:

此轉換Compose檔案定義了kafkatransformapi服務。其中,api服務使用了不同的映像標籤,並且沒有掛載原生程式碼。

啟動容器

要啟動容器,可以使用以下命令:

docker compose -f transform-docker-compose.yml -f dev-compose-file.yml up
docker compose -f api-docker-compose.yml -f dev-compose-file.yml up

內容解密:

在這些命令中,-f選項用於指定要使用的Compose檔案。最後指定的檔案會覆寫前面檔案中相同的設定。這使得團隊可以根據需要自定義服務的設定。

使用擴充套件欄位共用變數

為了進一步減少重複,可以使用擴充套件欄位共用變數。例如,在dev-docker-compose.yml中:

x-environ: &def-common
  environment:
    &common-env
      POSTGRES_USER: ${PG_USER}
      POSTGRES_PASSWORD: ${PG_PASS}

services:
  postgres:
    image: postgres
    environment:
      <<: *common-env

  api:
    environment:
      <<: *common-env
      POSTGRES_PORT: ${PG_PORT}
      POSTGRES_HOST: ${PG_HOST}

內容解密:

此範例中,x-environ是一個擴充套件欄位,定義了共用的環境變數。postgresapi服務都使用了這些共用變數,從而避免了重複定義。

減少外部依賴

透過使用Docker和共用Compose組態,可以簡化開發環境並減少外部依賴。這有助於降低成本並加快開發速度。圖5-8展示了一個短暫的開發環境設定,其中部分服務執行在本地,部分執行在雲端。

@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title Docker容器生命週期管理與最佳實踐

package "Docker 架構" {
    actor "開發者" as dev

    package "Docker Engine" {
        component [Docker Daemon] as daemon
        component [Docker CLI] as cli
        component [REST API] as api
    }

    package "容器運行時" {
        component [containerd] as containerd
        component [runc] as runc
    }

    package "儲存" {
        database [Images] as images
        database [Volumes] as volumes
        database [Networks] as networks
    }

    cloud "Registry" as registry
}

dev --> cli : 命令操作
cli --> api : API 呼叫
api --> daemon : 處理請求
daemon --> containerd : 容器管理
containerd --> runc : 執行容器
daemon --> images : 映像檔管理
daemon --> registry : 拉取/推送
daemon --> volumes : 資料持久化
daemon --> networks : 網路配置

@enduml

此圖示展示了開發環境的不同部分如何協同工作。透過減少外部依賴,可以提高開發效率並降低成本。