使用容器进行 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 网络的更多信息。
为卷和网络选择合适的名称
俗话说,计算机科学中只有两件难事:缓存失效和命名事物。还有差一错误。
为网络或托管卷选择名称时,最好选择一个能表明其预期目的的名称。本指南旨在简洁,因此使用了简短、通用的名称。
启动数据库引擎
现在杂务已处理完毕,您可以在容器中运行 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 内置的 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发送包含{ "value" : string }JSON 的 HTTPPOST请求必须将该值保存到数据库中。
您还更新了另一个业务需求。该需求是
- 应用程序通过对
/的请求回复包含心形符号("<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 镜像来构建应用程序,然后通过将编译后的二进制文件放入更精简、无发行版的镜像中来构建最终镜像。
无论您是更新了旧的示例应用程序,还是签出了新的应用程序,都必须构建这个新的 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
在您的应用程序目录中,创建一个名为 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 文件
文件名 compose.yaml 是 docker compose 命令在未提供 -f 标志时识别的默认文件名。这意味着如果您的环境有此要求,您可以拥有多个 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 会读取文件 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 命令来创建数据库和用户,如配置数据库引擎中所述。
因此,从另一个终端登录到数据库引擎
$ 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 中运行功能测试的一种可能方法。