17使用 Go 运行与部署
简介
到了最后, 测试和文档都已经完成了, 只剩下部署了.
平常测试的时候可以直接使用 go run
运行, 但到了部署阶段, 对于编译型语言来说,
肯定是要使用 go build
生成二进制文件的.
在 docker 中构建
因为整个系统都是基于 docker-compose 的, 所以需要写一个 Dockerfile,
将整个项目在 docker 中构建为一个镜像.
这样, 就可以直接在 docker 中运行了. 每次的本地构建生成二进制文件的过程,
就转变为了重新构建 docker 镜像.
Dockerfile 如下:
FROM golang:1.13 as build
ENV GOPROXY="https://goproxy.io"
# https://stackoverflow.com/questions/36279253/go-compiled-binary-wont-run-in-an-alpine-docker-container-on-ubuntu-host
# build for static link
ENV CGO_ENABLED=0
WORKDIR /app
COPY . /app
RUN make build
# production stage
FROM alpine as production
WORKDIR /app
COPY ./conf/ /app/conf
COPY --from=build /app/web /app
EXPOSE 8081
ENTRYPOINT ["/app/web"]
CMD [ "-c", "./conf/config_docker.yaml" ]
构建的时候用到了二阶段构建, 首先在普通的 golang 镜像中构建二进制文件,
然后复制到 alpine 镜像中使用, 以减少构建完成后的镜像体积.
构建的时候需要设置环境变量 CGO_ENABLED=0
, 以禁止使用 CGO 动态链接,
具体请参考 stackoverflow.
集成在 docker-compose 中
当 Dockerfile 写完后, 可以直接构建镜像, 并运行一下测试是否正常.
docker build -t go_web .
docker run -p 8081:8081 go_web
一切顺利之后, 就可以将它集成到 docker-compose.yaml
中, 命名为一个服务了.
app:
build:
context: .
depends_on:
- mysql
有个依赖, 毕竟 mysql 要先启动. 至于为什么没有暴露端口, 是因为要使用 nginx 反向代理.
使用 nginx 反向代理
docker-compose 可以将一个 SERVICE 手动缩放至多个实例.
Usage: up [options] [--scale SERVICE=NUM...] [SERVICE...]
虽然在 kubernetes 出来之后, docker-compose scale 已经不再流行了, 但还是实现一下.
这里只关注 app 的扩容, 即当前项目, 而不管其他的依赖, 比如数据库等.
修改 API
首先, 改造一下 /check/health
API, 返回 hostname, 以便可以观察到效果.
var hostname string
func init() {
name, err := os.Hostname()
if err != nil {
name = "unknow"
}
hostname = name
}
// HealthCheck 返回心跳响应
func HealthCheck(ctx *gin.Context) {
message := fmt.Sprintf("OK from %s", hostname)
ctx.String(http.StatusOK, message)
}
修改完成后, 注意重新构建镜像, 以便改动生效.
创建 nginx service
在 docker-compose 中配置 nginx.
nginx:
image: nginx:stable-alpine
ports:
- 80:80
depends_on:
- app
volumes:
- ./conf/nginx_web.conf:/etc/nginx/conf.d/default.conf
command: nginx -g 'daemon off;'
然后是编写 nginx 的配置文件:
upstream web {
server app:8081;
}
server {
listen 80;
server_name localhost;
location / {
# https://stackoverflow.com/questions/42720618/docker-nginx-stopped-emerg-11-host-not-found-in-upstream
resolver 127.0.0.1;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
client_max_body_size 5m;
proxy_pass http://web;
}
}
这里设置了反向代理, 将所有的请求都转向了 app:8081
, 就是应用服务器暴露的端口,
注意, 设置了 resolver 127.0.0.1;
, 否则 nginx 会在一开始无法连通 app:8081
时直接崩溃.
那么, 为什么不再准备好的时候才启动 nginx 呢? 这是因为 docker-compose 中的 depends_on
只是保证了启动顺序, 而无法确认是否已经准备好了.
更新数据库
数据库也是同理, 我们需要设置一定的重试机制来保证数据库是否已经启动完成了.
func openDB(username, password, addr, name string) *gorm.DB {
config := fmt.Sprintf(
"%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=%t&loc=%s&timeout=10s",
username,
password,
addr,
name,
true,
// "Asia%2FShanghai", // 必须是 url.QueryEscape 的
"Local",
)
var db *gorm.DB
var err error
for i := 0; i < 10; i++ {
db, err = gorm.Open("mysql", config)
if err == nil {
break
}
time.Sleep(time.Second * 3)
}
if db == nil {
logrus.Fatalf("数据库连接失败. 数据库名字: %s. 错误信息: %s", name, err)
}
logrus.Infof("数据库连接成功, 数据库名字: %s", name)
setupDB(db)
return db
}
另外, 在数据库启动的时候, 设置一个初始化脚本, 这样就不用手动创建数据库了.
mysql:
image: mysql:8
command: --default-authentication-plugin=mysql_native_password --init-file /data/application/init.sql
environment:
MYSQL_ROOT_PASSWORD: "1234"
ports:
- 3306:3306
volumes:
- ./script/db.sql:/data/application/init.sql
数据库初始化脚本很简单, 只是检查特定数据库是否存在, 不存在就先创建.
CREATE DATABASE IF NOT EXISTS `db_apiserver`;
启动
当一切改动都完成之后, 就可以启动并尝试了.
docker-compose up --scale app=3 nginx
这会启动三个 app 的实例.
如果你不断访问 http://127.0.0.1:80/v1/check/health
, 应该会得到三个结果,
类似于下面这样:
OK from 5f8a835b6797
OK from b6dbb50cecd5
OK from 87e98121950d
前几次访问可能返回 502 错误, 这是因为 app 还在连接数据库中, 没有启动起来.
然后, 你可以修改 nginx 的配置, 体验 nginx 内置的各种负载均衡机制.
建议配合前面的 使用 Go 添加 Nginx 代理
一起使用.
总结
Go 部署方式有很多, 选择适合的就好. 如果有需求, 还可以交叉编译各平台的二进制文件.
当前部分的代码
作为版本 v0.17.0