玩玩微服务1(持更)

忍不住

一直看人家面试要求 K8s + docker,然而这个经验对于一般的公司来说是十分宝贵的。
(能有大场景的公司能有几个啊?捂脸)
但是好奇心还是趋势我去实现一个最最最最简单的 k8s + docker 提供服务的场景。
所以这篇文章就要从 0 开始搭建这么一套服务。

准备

准备工作的安装环节,我们只提供教程地址,点击即可。

关于 Docker

大家有时候会把 docker 看做是虚拟机,这个没有问题,但是我想说的是:一个“容器”,实际上是一个由 Linux Namespace、Linux Cgroups 和 rootfs 三种技术构建出来的进程的隔离环境。严格意义上,docker 并不等同于 VM。

关于 kubernetes

在安装 kubernetes 的时候,请使用阿里云的镜像安装。什么?你有 VPN?
没用的朋友,安装过程中会在容器中再行进行镜像的拉取,除非你修改安装脚本设置 http proxy,否则还是会被墙的。

说实话,关于 k8s 的概念完全可以另开一篇文章了。
但是我尽可能用最少的语言白话一下关键的概念点。

首先你要明白,k8s 最终操作的是 docker。为了让好多好多 docker 管理起来更加方便,也易于伸缩。
把 k8s 想象成一个大箱子,里面会有不同大小的篮子,有哪几种篮子,颗粒度分别是怎样的呢?

Pod

对于 k8s 来说,他看得见的最小单位就是 pod。

Deployment

Service

试验代码

大家都是Hello World老手了,我们简单过一下:
目录结构如下

1
2
3
├── hello
   ├── Dockerfile
   └── main.go

main.go 实现一个简单的 http server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
"net/http"
)

func main() {
http.HandleFunc("/", func(r http.ResponseWriter, r2 *http.Request){
r.Write([]byte("Hello World"))
})

fmt.Println("Start listening...")
http.ListenAndServe(":8080", nil)
}

我们还需要 go mod init 一下,因为我们没有什么需要依赖的第三方库,所以不会生成 go.sum,我们手动创建一个空文件。

写Dockerfile

关于 Dockerfile 需要的看【这里】

普通的go程序已经对我们没有吸引力了,我们现在要将他封装成一个 container。
为了保证 container 的精简,我们最后肯定是只在原始镜像中放一个可执行文件,以及运行该程序的最小配置环境。
可别动不动就用上百 MB 的全镜像做基镜像啊,除非你家开硬盘厂的(捂脸)。

感谢Pierre Prinetti的文章,给我们展示了一个完整的基础镜像的构建过程。
来上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 第一阶段的 image ===========
# 设定 Go 的版本
ARG GO_VERSION=1.11

# 创建一个可执行镜像,基础为 alpine, alpine自带包管理
# 并明命名为 builder
FROM golang:${GO_VERSION}-alpine AS builder

# 创建一个没有特权的用户和用户组,这个很重要
# 否则程序会没有权限执行

RUN mkdir /user && \
echo 'nobody:x:65534:65534:nobody:/:' > /user/passwd && \
echo 'nobody:x:65534:' > /user/group

# 通过 alpine 的包管理工具 apk
# 添加 ca-certificates 工具,用来做 https 通讯的, 以及 git
RUN apk add --no-cache ca-certificates git

# 设定接下来的 RUN 的执行目录
WORKDIR /src

# 先获取需要的依赖,可以加速之后的构建速度
COPY ./go.mod ./go.sum ./
RUN go mod download

# 复制代码哟
COPY ./ ./

# 静态连接方式编译程序
RUN CGO_ENABLED=0 go build \
-installsuffix 'static' \
-o /main .

# 第二阶段的 image ===========
# scratch 比 alpine 更轻
# 第二阶段一般不支持 用 RUN,因为镜像很小没有需要的一些命令
FROM scratch AS final

# 从第一个 image 导入用户和组设置
COPY --from=builder /user/group /user/passwd /etc/

# 将证书文件也拷贝过来
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# 直接将可执行文件也拷贝过来
COPY --from=builder /main /main

# 暴露我们需要的 8080 端口
EXPOSE 8080

# 以 nobody 的名义执行下面的指令
USER nobody:nobody

# main 走起
ENTRYPOINT ["/main"]

准备好了 Dockerfile 后,我们需要 build 一下

go build -t hello-go .
千万不要忘记了 最后的 .

我们将最终的镜像取名为 hello-go
完后发现整个过程没有报异常并且 build 成功。
我们使用docker images命令查看,会发现多了两个镜像,
一个是没有名称的,也就是我们在第一构建阶段的镜像。还有一个是我们的 hello-go。

我们需要运行这个容器来验证一下:

docker run –name hello-go-1 -p 8080:8080 -d hello-go

docker ps 查看已经运行起来了,我们访问 127.0.0.1:8080,发现页面已经输出了 Hello World。
到此,我们构建一个 go应用容器成功。

不过等等

实际情况真有这么简单吗?应用什么也不需要,直接裸跑?
别做梦了!

现实是,一个应用不光会使用很多的第三方代码库,还会和其他服务一起协同,比如数据库啦,缓存啦,日志啦,其他服务啦,blablablah…

那好,我们就来改进下我们的场景:

除了输出内容外,我们还需要将接受到的数据存入到 Redis 队列中。
既然我们做成了容器,那就是为了能够更好的伸缩,所以我们的这个新应用会多个容器同时运行。

如下图:

所以问题就聚焦在了如何灵活使用配置。这里有两个方案:

  1. 设置在 Dockerfile 中,添加 ENV xxx xxx
  2. 在容器运行的时候,添加 -e 参数

我选择了后者,因为更灵活。
假设我们的 redis 服务运行在宿主机上,那么我们需要容器活期宿主机的地址,可以用这个变量 -e xxx=host.docker.internal

同时,我们的开发机,打包机,和生产机是要做完全区分的。
而一般的开发发布流程应该是:

  1. 开发机提交代码到 git 仓库
  2. 触发打包机 pull 代码并进行 在线的 build
  3. 打包机生成镜像并 push 到私有的 docker 仓库
  4. 生产机 pull 最新的镜像并 run 一个容器

其中,私有的 docker 仓库可以使用阿里云,免费的。
再发布的时候我们是需要打 tag的,docker tag 的作用就是区分镜像的版本。

*参考