使用容器进行 Go 开发
先决条件
完成将您的镜像作为容器运行模块中的步骤,了解如何管理容器的生命周期。
引言
在本模块中,您将学习如何在容器中运行数据库引擎,并将其连接到示例应用程序的扩展版本。您将看到一些用于保持持久数据以及连接容器使其相互通信的选项。最后,您将学习如何使用 Docker Compose 有效地管理这种多容器本地开发环境。
本地数据库与容器
您将使用的数据库引擎称为 CockroachDB。它是一个现代、云原生、分布式 SQL 数据库。
您将使用CockroachDB 的 Docker 镜像并在容器中运行它,而不是从源代码编译 CockroachDB 或使用操作系统本地包管理器安装 CockroachDB。
CockroachDB 在很大程度上兼容 PostgreSQL,并与其共享许多约定,特别是环境变量的默认名称。因此,如果您熟悉 Postgres,看到一些熟悉的环境变量名称时不要感到惊讶。与 Postgres 兼容的 Go 模块,例如 pgx、pq、GORM 和 upper/db 也与 CockroachDB 一起使用。
有关 Go 和 CockroachDB 之间关系的更多信息,请参阅CockroachDB 文档,尽管这对于继续本指南并非必要。
存储
数据库的意义在于拥有持久化的数据存储。卷(Volumes)是持久化 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 网络的信息。
为卷和网络选择好的名称
俗话说,计算机科学中最难的两件事是:缓存失效和命名。以及差一错误。
在为网络或托管卷选择名称时,最好选择一个能指示其预期用途的名称。本指南力求简洁,因此使用了简短、通用的名称。
启动数据库引擎
现在内务工作已完成,您可以运行 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 initialize 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 http://localhost/ 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 http://localhost/send \
--header 'content-type: application/json' \
--data '{"value": "Hello, Docker!"}'
应用程序回复了消息内容,这意味着它已保存到数据库中:
{ "value": "Hello, Docker!" }
再发送一条消息:
$ curl --request POST \
--url http://localhost/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
在您的应用程序目录中,创建一个名为 compose.yaml
的新文本文件,内容如下。
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
文件
如果存在 .env
文件,Docker Compose 将自动读取其中的环境变量。由于您的 Compose 文件要求设置 PGPASSWORD
,请将以下内容添加到 .env
文件中:
PGPASSWORD=whatever
在此示例中,具体值并不重要,因为您在非安全模式下运行 CockroachDB。请确保将变量设置为某个值,以免出现错误。
合并 Compose 文件
如果未提供 -f
标志,docker compose
命令将识别默认文件名为 compose.yaml
。这意味着如果您的环境有这样的要求,您可以有多个 Docker Compose 文件。此外,Docker Compose 文件是可组合的(双关语),因此可以在命令行上指定多个文件以将配置的各个部分合并在一起。以下列表只是此类功能非常有用的一些场景示例:
- 在本地开发时使用绑定挂载(bind mount)来挂载源代码,但在运行 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 读取 compose.yaml
文件,将其解析到内存中的数据结构,在可能的情况下进行验证,然后从其内部表示中打印出该配置文件的重构。如果由于错误而无法实现,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 命令,如配置数据库引擎中所述。完成这些操作后(并且示例应用程序容器自动重启),rest-service
将停止失败并重启,控制台将恢复安静。
本可以连接您之前使用的卷,但为了本示例的目的,这样做带来的麻烦多于好处,而且它也提供了一个机会,通过 restart_policy
Compose 文件特性来演示如何为部署引入弹性。
$ docker exec -it roach ./cockroach sql --insecure
(此处承接 i=151)
(此处承接 i=152)
测试应用程序
现在,测试您的 API 端点。在新的终端中,运行以下命令:
$ curl http://localhost/
您应该会收到以下响应:
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 中运行功能测试的一种可能方法。