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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容