使用容器進行 Go 開發
先決條件
按照將您的映象作為容器執行模組中的步驟進行操作,以瞭解如何管理容器的生命週期。
簡介
在本模組中,您將瞭解如何在容器中執行資料庫引擎並將其連線到示例應用程式的擴充套件版本。您將看到一些用於儲存持久資料和將容器連線在一起的選項。最後,您將學習如何使用 Docker Compose 有效地管理此類多容器本地開發環境。
本地資料庫和容器
您將使用的資料庫引擎稱為CockroachDB。它是一個現代的雲原生分散式 SQL 資料庫。
您將使用CockroachDB 的 Docker 映象在容器中執行,而不是從原始碼編譯 CockroachDB 或使用作業系統的本地包管理器安裝 CockroachDB。
CockroachDB 在很大程度上與 PostgreSQL 相容,並與後者共享許多約定,特別是環境變數的預設名稱。因此,如果您熟悉 Postgres,請不要對看到一些熟悉的環境變數名稱感到驚訝。與 Postgres 一起使用的 Go 模組(例如 pgx、pq、GORM和 upper/db也可以與 CockroachDB 一起使用。
有關 Go 和 CockroachDB 之間關係的更多資訊,請參閱CockroachDB 文件,儘管這對於繼續本指南不是必需的。
儲存
資料庫的目的是擁有持久的資料儲存。卷是持久化 Docker 容器生成和使用的資料的首選機制。因此,在您啟動 CockroachDB 之前,請為其建立卷。
要建立託管卷,請執行
$ docker volume create roach
roach
您可以使用以下命令檢視 Docker 例項中所有託管卷的列表
$ docker volume list
DRIVER VOLUME NAME
local roach
網路
示例應用程式和資料庫引擎將透過網路相互通訊。有不同型別的網路配置,您將使用所謂的使用者定義的橋接網路。它將為您提供 DNS 查詢服務,以便您可以透過其主機名引用資料庫引擎容器。
以下命令建立一個名為 mynet
的新橋接網路
$ docker network create -d bridge mynet
51344edd6430b5acd121822cacc99f8bc39be63dd125a3b3cd517b6485ab7709
與託管卷一樣,有一個命令可以列出 Docker 例項中設定的所有網路
$ docker network list
NETWORK ID NAME DRIVER SCOPE
0ac2b1819fa4 bridge bridge local
51344edd6430 mynet bridge local
daed20bbecce host host local
6aee44f40a39 none null local
您的橋接網路 mynet
已成功建立。另外三個名為 bridge
、host
和 none
的網路是預設網路,由 Docker 本身建立。雖然這與本指南無關,但您可以在網路概述部分中瞭解有關 Docker 網路的更多資訊。
為卷和網路選擇好的名稱
正如俗話說,計算機科學中只有兩件難事:快取失效和命名。以及 off-by-one 錯誤。
在為網路或託管卷選擇名稱時,最好選擇一個反映預期用途的名稱。本指南旨在簡潔,因此使用簡短的通用名稱。
啟動資料庫引擎
現在已經完成了家務事,您可以將 CockroachDB 執行在容器中,並將其附加到您剛剛建立的卷和網路。當您執行以下命令時,Docker 將從 Docker Hub 拉取映象並在本地為您執行它
$ docker run -d \
--name roach \
--hostname db \
--network mynet \
-p 26257:26257 \
-p 8080:8080 \
-v roach:/cockroach/cockroach-data \
cockroachdb/cockroach:latest-v20.1 start-single-node \
--insecure
# ... output omitted ...
注意巧妙地使用標籤 latest-v20.1
來確保您拉取 20.1 的最新修補版本。可用標籤的多樣性取決於映象維護者。在這裡,您的目標是擁有 CockroachDB 的最新修補版本,但隨著時間的推移不要偏離已知的正常工作版本太遠。要檢視 CockroachDB 映象的可用標籤,您可以訪問 Docker Hub 上的 CockroachDB 頁面。
配置資料庫引擎
現在資料庫引擎已啟動,在您的應用程式可以使用它之前需要進行一些配置。幸運的是,並不多。您必須
- 建立一個空資料庫。
- 註冊一個新的使用者帳戶到資料庫引擎。
- 授予該新使用者訪問資料庫的許可權。
您可以使用 CockroachDB 內建的 SQL shell 完成此操作。要在執行資料庫引擎的同一容器中啟動 SQL shell,請鍵入
$ docker exec -it roach ./cockroach sql --insecure
在 SQL shell 中,建立示例應用程式將使用的資料庫。
CREATE DATABASE mydb;
註冊一個新的 SQL 使用者帳戶到資料庫引擎。使用使用者名稱
totoro
。CREATE USER totoro;
為新使用者授予必要的許可權。
GRANT ALL ON DATABASE mydb TO totoro;
鍵入
quit
退出 shell。
以下是一個與 SQL shell 互動的示例。
$ sudo docker exec -it roach ./cockroach sql --insecure
#
# Welcome to the CockroachDB SQL shell.
# All statements must be terminated by a semicolon.
# To exit, type: \q.
#
# Server version: CockroachDB CCL v20.1.15 (x86_64-unknown-linux-gnu, built 2021/04/26 16:11:58, go1.13.9) (same version as client)
# Cluster ID: 7f43a490-ccd6-4c2a-9534-21f393ca80ce
#
# Enter \? for a brief introduction.
#
root@:26257/defaultdb> CREATE DATABASE mydb;
CREATE DATABASE
Time: 22.985478ms
root@:26257/defaultdb> CREATE USER totoro;
CREATE ROLE
Time: 13.921659ms
root@:26257/defaultdb> GRANT ALL ON DATABASE mydb TO totoro;
GRANT
Time: 14.217559ms
root@:26257/defaultdb> quit
oliver@hki:~$
瞭解示例應用程式
現在您已經啟動並配置了資料庫引擎,您可以將注意力轉向應用程式。
本模組的示例應用程式是您在之前模組中使用過的 docker-gs-ping
應用程式的擴充套件版本。您有兩個選擇
- 您可以更新您本地版本的
docker-gs-ping
以匹配本章中介紹的新擴充套件版本;或者 - 您可以克隆 docker/docker-gs-ping-dev 儲存庫。建議使用後一種方法。
要簽出示例應用程式,請執行
$ git clone https://github.com/docker/docker-gs-ping-dev.git
# ... output omitted ...
應用程式的 main.go
現在包含資料庫初始化程式碼,以及實現新業務需求的程式碼。
- 傳送到
/send
的 HTTPPOST
請求包含{ "value" : string }
JSON 必須將該值儲存到資料庫。
您還對另一個業務需求進行了更新。需求是
- 應用程式在對
/
的請求中使用包含心形符號(“<3
”)的文字訊息進行響應。
現在將變成
應用程式將以包含儲存在資料庫中的訊息計數的字串(括號內)進行響應。
示例輸出:
Hello, Docker! (7)
以下是 main.go
的完整原始碼清單。
package main
import (
"context"
"database/sql"
"fmt"
"log"
"net/http"
"os"
"github.com/cenkalti/backoff/v4"
"github.com/cockroachdb/cockroach-go/v2/crdb"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
db, err := initStore()
if err != nil {
log.Fatalf("failed to initialise the store: %s", err)
}
defer db.Close()
e.GET("/", func(c echo.Context) error {
return rootHandler(db, c)
})
e.GET("/ping", func(c echo.Context) error {
return c.JSON(http.StatusOK, struct{ Status string }{Status: "OK"})
})
e.POST("/send", func(c echo.Context) error {
return sendHandler(db, c)
})
httpPort := os.Getenv("HTTP_PORT")
if httpPort == "" {
httpPort = "8080"
}
e.Logger.Fatal(e.Start(":" + httpPort))
}
type Message struct {
Value string `json:"value"`
}
func initStore() (*sql.DB, error) {
pgConnString := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s sslmode=disable",
os.Getenv("PGHOST"),
os.Getenv("PGPORT"),
os.Getenv("PGDATABASE"),
os.Getenv("PGUSER"),
os.Getenv("PGPASSWORD"),
)
var (
db *sql.DB
err error
)
openDB := func() error {
db, err = sql.Open("postgres", pgConnString)
return err
}
err = backoff.Retry(openDB, backoff.NewExponentialBackOff())
if err != nil {
return nil, err
}
if _, err := db.Exec(
"CREATE TABLE IF NOT EXISTS message (value TEXT PRIMARY KEY)"); err != nil {
return nil, err
}
return db, nil
}
func rootHandler(db *sql.DB, c echo.Context) error {
r, err := countRecords(db)
if err != nil {
return c.HTML(http.StatusInternalServerError, err.Error())
}
return c.HTML(http.StatusOK, fmt.Sprintf("Hello, Docker! (%d)\n", r))
}
func sendHandler(db *sql.DB, c echo.Context) error {
m := &Message{}
if err := c.Bind(m); err != nil {
return c.JSON(http.StatusInternalServerError, err)
}
err := crdb.ExecuteTx(context.Background(), db, nil,
func(tx *sql.Tx) error {
_, err := tx.Exec(
"INSERT INTO message (value) VALUES ($1) ON CONFLICT (value) DO UPDATE SET value = excluded.value",
m.Value,
)
if err != nil {
return c.JSON(http.StatusInternalServerError, err)
}
return nil
})
if err != nil {
return c.JSON(http.StatusInternalServerError, err)
}
return c.JSON(http.StatusOK, m)
}
func countRecords(db *sql.DB) (int, error) {
rows, err := db.Query("SELECT COUNT(*) FROM message")
if err != nil {
return 0, err
}
defer rows.Close()
count := 0
for rows.Next() {
if err := rows.Scan(&count); err != nil {
return 0, err
}
rows.Close()
}
return count, nil
}
儲存庫還包括 Dockerfile
,它與之前模組中介紹的多階段 Dockerfile
幾乎完全相同。它使用官方 Docker Go 映象來構建應用程式,然後透過將編譯後的二進位制檔案放入更精簡的 distroless 映象中來構建最終映象。
無論您是更新了舊的示例應用程式還是簽出了新的示例應用程式,都需要構建這個新的 Docker 映象來反映應用程式原始碼的更改。
構建應用程式
您可以使用熟悉的 build
命令構建映象
$ docker build --tag docker-gs-ping-roach .
執行應用程式
現在,執行您的容器。這次您需要設定一些環境變數,以便您的應用程式知道如何訪問資料庫。現在,您將在 docker run
命令中直接執行此操作。稍後您將看到使用 Docker Compose 的更便捷方法。
注意
由於您在不安全模式下執行 CockroachDB 叢集,因此密碼的值可以是任何內容。
在生產環境中,請勿在不安全模式下執行。
$ docker run -it --rm -d \
--network mynet \
--name rest-server \
-p 80:8080 \
-e PGUSER=totoro \
-e PGPASSWORD=myfriend \
-e PGHOST=db \
-e PGPORT=26257 \
-e PGDATABASE=mydb \
docker-gs-ping-roach
關於此命令,需要注意幾點。
這次您將容器埠
8080
對映到主機埠80
。因此,對於GET
請求,您可以直接使用curl localhost
$ curl localhost Hello, Docker! (0)
或者,如果您願意,一個完整的 URL 也同樣有效
$ curl https:/// Hello, Docker! (0)
目前儲存的訊息總數為
0
。這是正常的,因為您尚未向您的應用程式釋出任何內容。您透過其主機名(即
db
)引用資料庫容器。這就是您在啟動資料庫容器時使用--hostname db
的原因。實際密碼並不重要,但必須設定為某個值,以避免混淆示例應用程式。
您剛剛執行的容器名為
rest-server
。這些名稱對於管理容器生命週期很有用。# Don't do this just yet, it's only an example: $ docker container rm --force rest-server
測試應用程式
在上一節中,您已經使用 GET
測試了查詢應用程式,它返回了儲存的訊息計數器為零。現在,向它釋出一些訊息。
$ curl --request POST \
--url https:///send \
--header 'content-type: application/json' \
--data '{"value": "Hello, Docker!"}'
應用程式使用訊息內容進行響應,這意味著它已儲存到資料庫中。
{"value":"Hello, Docker!"}
傳送另一條訊息。
$ curl --request POST \
--url https:///send \
--header 'content-type: application/json' \
--data '{"value": "Hello, Oliver!"}'
同樣,您會收到訊息的值。
{"value":"Hello, Oliver!"}
執行 curl 並檢視訊息計數器顯示什麼。
$ curl localhost
Hello, Docker! (2)
在本示例中,您傳送了兩條訊息,資料庫將它們儲存下來。或者它儲存了嗎?停止並刪除所有容器,但不刪除卷,然後重試。
首先,停止容器。
$ docker container stop rest-server roach
rest-server
roach
然後,刪除它們。
$ docker container rm rest-server roach
rest-server
roach
驗證它們是否已消失。
$ docker container list --all
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
然後再次啟動它們,先啟動資料庫。
$ docker run -d \
--name roach \
--hostname db \
--network mynet \
-p 26257:26257 \
-p 8080:8080 \
-v roach:/cockroach/cockroach-data \
cockroachdb/cockroach:latest-v20.1 start-single-node \
--insecure
然後啟動服務。
$ docker run -it --rm -d \
--network mynet \
--name rest-server \
-p 80:8080 \
-e PGUSER=totoro \
-e PGPASSWORD=myfriend \
-e PGHOST=db \
-e PGPORT=26257 \
-e PGDATABASE=mydb \
docker-gs-ping-roach
最後,查詢您的服務。
$ curl localhost
Hello, Docker! (2)
太好了!資料庫中的記錄計數是正確的,儘管您不僅停止了容器,還在啟動新例項之前刪除了它們。區別在於 CockroachDB 的託管卷,您可以重複使用它。新的 CockroachDB 容器已從磁碟讀取資料庫檔案,就像它在容器外部執行時一樣。
關閉所有內容
請記住,您正在不安全模式下執行 CockroachDB。現在您已經構建並測試了應用程式,是時候在繼續之前關閉所有內容了。您可以使用 list
命令列出正在執行的容器。
$ docker container list
現在您知道了容器 ID,您可以使用 docker container stop
和 docker container rm
,如之前模組中所示。
在繼續之前,請停止 CockroachDB 和 docker-gs-ping-roach
容器。
使用 Docker Compose 提高生產力
此時,您可能想知道是否有一種方法可以避免處理 docker
命令的冗長引數列表。在本系列中使用的玩具示例需要五個環境變數來定義與資料庫的連線。一個真實的應用程式可能需要更多、更多的環境變數。然後還有依賴關係的問題。理想情況下,您希望確保在執行應用程式之前啟動資料庫。而啟動資料庫例項可能需要另一個具有許多選項的 Docker 命令。但有一種更好的方法可以協調這些部署以用於本地開發目的。
在本節中,您將建立一個 Docker Compose 檔案,使用單個命令啟動 docker-gs-ping-roach
應用程式和 CockroachDB 資料庫引擎。
配置 Docker Compose
在應用程式的目錄中,建立一個名為 docker-compose.yml
的新文字檔案,其中包含以下內容。
version: '3.8'
services:
docker-gs-ping-roach:
depends_on:
- roach
build:
context: .
container_name: rest-server
hostname: rest-server
networks:
- mynet
ports:
- 80:8080
environment:
- PGUSER=${PGUSER:-totoro}
- PGPASSWORD=${PGPASSWORD:?database password not set}
- PGHOST=${PGHOST:-db}
- PGPORT=${PGPORT:-26257}
- PGDATABASE=${PGDATABASE:-mydb}
deploy:
restart_policy:
condition: on-failure
roach:
image: cockroachdb/cockroach:latest-v20.1
container_name: roach
hostname: db
networks:
- mynet
ports:
- 26257:26257
- 8080:8080
volumes:
- roach:/cockroach/cockroach-data
command: start-single-node --insecure
volumes:
roach:
networks:
mynet:
driver: bridge
此 Docker Compose 配置非常方便,因為您不必鍵入要傳遞給 docker run
命令的所有引數。您可以在 Docker Compose 檔案中宣告性地執行此操作。 Docker Compose 文件頁面 內容非常豐富,包括 Docker Compose 檔案格式的完整參考。
.env
檔案
如果可用,Docker Compose 會自動從 .env
檔案中讀取環境變數。由於您的 Compose 檔案需要設定 PGPASSWORD
,因此請將以下內容新增到 .env
檔案中。
PGPASSWORD=whatever
對於本示例,確切的值並不重要,因為您在不安全模式下執行 CockroachDB。請確保將變數設定為某個值,以避免出現錯誤。
合併 Compose 檔案
檔名 docker-compose.yml
是預設檔名,如果未提供 -f
標誌,docker compose
命令將識別該檔名。這意味著如果您的環境有此類需求,您可以擁有多個 Docker Compose 檔案。此外,Docker Compose 檔案是……可組合的(雙關語),因此可以在命令列中指定多個檔案以將配置的不同部分合並在一起。以下列表只是在以下情況下可以使用此功能的一些示例
- 使用繫結掛載用於本地開發的原始碼,但在執行 CI 測試時不使用它;
- 在使用預構建映象用於某些 API 應用程式的前端與建立原始碼的繫結掛載之間進行切換;
- 新增用於整合測試的額外服務;
- 等等……
此處將不會介紹任何這些高階用例。
Docker Compose 中的變數替換
Docker Compose 的一個非常酷的功能是 變數替換。您可以在 Compose 檔案的 environment
部分中看到一些示例。舉個例子
PGUSER=${PGUSER:-totoro}
表示在容器內部,環境變數PGUSER
將被設定為與執行 Docker Compose 的主機上的值相同。如果主機上沒有名為該名稱的環境變數,則容器內部的變數將獲得預設值totoro
。PGPASSWORD=${PGPASSWORD:?database password not set}
表示如果主機上未設定環境變數PGPASSWORD
,則 Docker Compose 將顯示錯誤訊息。這是可以的,因為您不想為密碼硬編碼預設值。您在.env
檔案中設定密碼值,該檔案是本地機器上的檔案。最好將.env
新增到.gitignore
中,以防止將機密資訊檢入版本控制系統。
Docker 文件的 變數替換 部分介紹了處理未定義或空值的其他方法。
驗證 Docker Compose 配置
在應用對 Compose 配置檔案的更改之前,您有機會使用以下命令驗證配置檔案的內容。
$ docker compose config
執行此命令時,Docker Compose 會讀取 docker-compose.yml
檔案,將其解析為記憶體中的資料結構,並在可能的情況下進行驗證,然後列印回從其內部表示形式重建的配置檔案。如果由於錯誤而無法執行此操作,則 Docker 會列印錯誤訊息。
使用 Docker Compose 構建並執行應用程式
啟動應用程式並確認它正在執行。
$ docker compose up --build
您傳遞了 --build
標誌,因此 Docker 將編譯您的映象,然後啟動它。
注意
Docker Compose 是一個有用的工具,但它也有自己的怪癖。例如,除非提供了
--build
標誌,否則在更新原始碼時不會觸發重建。一個非常常見的陷阱是編輯原始碼,然後在執行docker compose up
時忘記使用--build
標誌。
由於您的設定現在由 Docker Compose 執行,因此它為其分配了一個專案名稱,因此您獲得了 CockroachDB 例項的新卷。這意味著您的應用程式將無法連線到資料庫,因為資料庫不存在於這個新卷中。終端顯示資料庫的身份驗證錯誤。
# ... omitted output ...
rest-server | 2021/05/10 00:54:25 failed to initialise the store: pq: password authentication failed for user totoro
roach | *
roach | * INFO: Replication was disabled for this cluster.
roach | * When/if adding nodes in the future, update zone configurations to increase the replication factor.
roach | *
roach | CockroachDB node starting at 2021-05-10 00:54:26.398177 +0000 UTC (took 3.0s)
roach | build: CCL v20.1.15 @ 2021/04/26 16:11:58 (go1.13.9)
roach | webui: http://db:8080
roach | sql: postgresql://root@db:26257?sslmode=disable
roach | RPC client flags: /cockroach/cockroach <client cmd> --host=db:26257 --insecure
roach | logs: /cockroach/cockroach-data/logs
roach | temp dir: /cockroach/cockroach-data/cockroach-temp349434348
roach | external I/O path: /cockroach/cockroach-data/extern
roach | store[0]: path=/cockroach/cockroach-data
roach | storage engine: rocksdb
roach | status: initialized new cluster
roach | clusterID: b7b1cb93-558f-4058-b77e-8a4ddb329a88
roach | nodeID: 1
rest-server exited with code 0
rest-server | 2021/05/10 00:54:25 failed to initialise the store: pq: password authentication failed for user totoro
rest-server | 2021/05/10 00:54:26 failed to initialise the store: pq: password authentication failed for user totoro
rest-server | 2021/05/10 00:54:29 failed to initialise the store: pq: password authentication failed for user totoro
rest-server | 2021/05/10 00:54:25 failed to initialise the store: pq: password authentication failed for user totoro
rest-server | 2021/05/10 00:54:26 failed to initialise the store: pq: password authentication failed for user totoro
rest-server | 2021/05/10 00:54:29 failed to initialise the store: pq: password authentication failed for user totoro
rest-server exited with code 1
# ... omitted output ...
由於您使用 restart_policy
設定了部署方式,因此失敗的容器每 20 秒重新啟動一次。因此,為了解決問題,您需要登入到資料庫引擎並建立使用者。您之前已經在 配置資料庫引擎 中執行過此操作。
這不是什麼大問題。您只需連線到 CockroachDB 例項並執行三個 SQL 命令來建立資料庫和使用者,如 配置資料庫引擎 中所述。
因此,從另一個終端登入到資料庫引擎。
$ docker exec -it roach ./cockroach sql --insecure
並執行與之前相同的命令來建立資料庫 mydb
、使用者 totoro
以及向該使用者授予必要的許可權。完成此操作後(示例應用程式容器會自動重新啟動),rest-service
將停止失敗並重新啟動,控制檯將保持靜默。
您可以連線之前使用的卷,但出於本例的目的,這樣做會比較麻煩,而且也無法展示如何使用restart_policy
Compose 檔案功能在部署中引入彈性。
測試應用程式
現在,測試您的 API 端點。在新終端中,執行以下命令
$ curl https:///
您應該收到以下響應
Hello, Docker! (0)
關閉
要停止由 Docker Compose 啟動的容器,請在執行docker compose up
的終端中按ctrl+c
。要刪除已停止的容器,請執行docker compose down
。
分離模式
您可以使用-d
標誌以分離模式執行由docker compose
命令啟動的容器,就像使用docker
命令一樣。
要啟動由 Compose 檔案定義的堆疊,請在分離模式下執行
$ docker compose up --build -d
然後,您可以使用docker compose stop
停止容器,並使用docker compose down
刪除它們。
進一步探索
您可以執行docker compose
來檢視其他可用命令。
總結
本章刻意省略了一些與主題相關的有趣內容。對於更具冒險精神的讀者,本節提供了一些進一步學習的建議。
持久化儲存
託管卷不是為容器提供持久儲存的唯一方法。強烈建議您熟悉可用的儲存選項及其用例,這些內容在管理 Docker 中的資料中介紹。
CockroachDB 叢集
您運行了一個 CockroachDB 例項,這對於本例來說已經足夠了。但是,您可以執行一個 CockroachDB 叢集,它由多個 CockroachDB 例項組成,每個例項都在自己的容器中執行。由於 CockroachDB 引擎是按設計分散式的,因此您只需進行很少的更改就可以執行具有多個節點的叢集。
這種分散式設定提供了一些有趣的可能性,例如應用混沌工程技術來模擬叢集部分發生故障並評估您的應用程式應對這種故障的能力。
如果您有興趣嘗試使用 CockroachDB 叢集,請檢視
- 在 Docker 中啟動 CockroachDB 叢集 文章;以及
- 有關 Docker Compose 關鍵字
deploy
和replicas
的文件。
其他資料庫
由於您沒有執行 CockroachDB 例項的叢集,您可能想知道是否可以使用非分散式資料庫引擎。答案是“可以”,如果您要選擇更傳統的 SQL 資料庫,例如PostgreSQL,本章描述的過程將非常相似。
下一步
在本模組中,您使用應用程式和資料庫引擎在不同容器中執行,設定了容器化的開發環境。您還編寫了一個 Docker Compose 檔案,該檔案將這兩個容器連結在一起,並提供了一種簡單的方法來啟動和拆卸開發環境。
在下一個模組中,您將瞭解在 Docker 中執行功能測試的一種可能方法。