D27 kubernetes 通过Service实现蓝绿发布
1.蓝绿发布的基本原理
蓝绿发布(Blue-Green-Deployment)是一种应用升级发布方式,这种方式可以瞬间切换到新版本,也可以瞬间回退到老版本。可以做到服务不中断的回滚,提升用户的使用体验。
- 下面以在kubernetes中升级为例来说明蓝绿发布的过程
为了在k8s中支持蓝绿发布,需要给应用pod添加新的标签也就是version,service中的Selector也要添加version
1.abc服务v1.0运行:version v1.0的abc服务正在接受用户的请求,也就是绿色的pod
2.abc服务v1.1发布:现在新发布了一个version v1.1的abc服务接受用户的请求。也就是蓝色的pod
3.abc服务v1.1接受用户请求:修改service中的version标签为v1.1,这样用户的请求就能转发到新的pod中,实现蓝绿发布。
4.abc服务v1.0下线: 如果用户请求没有问题就可以将version 为v1.0的abc服务删除
5.紧急回退:如果有问题也可以通过修改service中的version标签的值为v1.0进行快速回退(Rollback)
传统的配置中通过配置nginx的配置文件也可以实现蓝绿发布。不过过程比较繁琐也容易出错。也可以使用apisix,通过调用apisix进行动态切换服务ip,相比nginx少了reload和修改配置文件的操作
k8s中的Deployment也支持滚动更新的策略
2.基于service/selector/lablel实现abc服务的蓝绿发布
2.1 准备go代码
package main
import (
"flag"
"github.com/gin-gonic/gin"
"net/http"
"os"
)
var version = flag.String("v", "v1", "v1")
func main() {
router := gin.Default()
router.GET("", func(c *gin.Context) {
flag.Parse()
hostname, _ := os.Hostname()
c.String(http.StatusOK, "This is version:%s running in pod %s", *version, hostname)
})
router.Run(":8080")
}
2.2 启动程序,访问测试
-
页面打开 127.0.0.1:8080
-
可以看到已经有请求进来
2.3 基于Dockerfile制作镜像
2.3.1写一个Dockerfile,与main.go同级目录
suyajun@MacBook-Pro-2 abc % ls -l
total 40
-rw-r--r--@ 1 suyajun staff 1372 Dec 5 18:59 Dockerfile
-rw-r--r--@ 1 suyajun staff 1369 Dec 5 16:01 go.mod
-rw-r--r--@ 1 suyajun staff 7028 Dec 5 16:01 go.sum
-rw-r--r--@ 1 suyajun staff 361 Dec 5 18:03 main.go
suyajun@MacBook-Pro-2 abc % cat Dockerfile
###################1.构建阶段###################
# 使用 Golang 1.23.4 镜像作为构建环境,这里的go版本要和本地开发环境的版本号一致
FROM uhub.service.ucloud.cn/librarys/golang:1.23.4 AS build
# 构建阶段的工作目录
WORKDIR /go/src/test/
# 将当前目录下的所有文件复制到 Docker 容器的 /go/src/test/ 目录中
COPY . /go/src/test/
# 设置 Go 的代理,使用国内的 goproxy.cn 以提高下载速度
RUN go env -w GOPROXY=https://goproxy.cn,direct
# 使用 GOARCH=amd64 和 GOOS=linux 标志编译 Go 应用为 Linux 下的可执行文件。
# CGO_ENABLED=0 禁用 CGO(C 语言调用 Go 语言),确保静态链接。
# go build -v -o main . 会在工作目录中生成名为 main 的二进制文件。
RUN CGO_ENABLE=0 GOARCH=amd64 GOOS=linux go build -v -o main .
###################2.运行阶段###################
# 使用 Ubuntu 最新版本的镜像作为运行时环境
FROM uhub.service.ucloud.cn/librarys/ubuntu:latest AS api
# 在容器内创建一个 /app 目录,用于存放构建好的应用
RUN mkdir /app
# 将从构建阶段复制过来的 main 文件复制到容器内的 /app 目录
COPY --from=build /go/src/test/main /app
# 设置容器的工作目录为 /app,之后所有命令将在该目录下执行
WORKDIR /app
# 定义容器启动时的默认执行命令:执行 main 程序并传入参数 "-v" 和 "1.0"
ENTRYPOINT ["./main", "-v", "1.0"]
2.3.2 制作镜像测试程序是否能正常运行
docker build -t abc:v1.0 . && docker run -p 8080:8080 abc:v1.0
[+] Building 42.3s (15/15) FINISHED docker:desktop-linux
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 1.45kB 0.0s
=> [internal] load metadata for uhub.service.ucloud.cn/librarys/ubuntu:latest 0.4s
=> [internal] load metadata for uhub.service.ucloud.cn/librarys/golang:1.23.4 0.4s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [build 1/5] FROM uhub.service.ucloud.cn/librarys/golang:1.23.4@sha256:0ace92a91fb174f5ec759b39ce66ba365237b1b5ee8b35ff311e46659f05ef68 0.0s
=> [api 1/4] FROM uhub.service.ucloud.cn/librarys/ubuntu:latest@sha256:6e75a10070b0fcb0bead763c5118a369bc7cc30dfc1b0749c491bbb21f15c3c7 0.0s
=> [internal] load build context 0.1s
=> => transferring context: 614B 0.1s
=> CACHED [build 2/5] WORKDIR /go/src/test/ 0.0s
=> [build 3/5] COPY . /go/src/test/ 0.0s
=> [build 4/5] RUN go env -w GOPROXY=https://goproxy.cn,direct 0.2s
=> [build 5/5] RUN CGO_ENABLE=0 GOARCH=amd64 GOOS=linux go build -v -o main . 41.4s
=> CACHED [api 2/4] RUN mkdir /app 0.0s
=> CACHED [api 3/4] COPY --from=build /go/src/test/main /app 0.0s
=> CACHED [api 4/4] WORKDIR /app 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:ccab35f1327e70fdb24e66d5bcf28380bce2278336690be3ad9c6b907b0f41dd 0.0s
=> => naming to docker.io/library/abc:v1.0 0.0s
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET / --> main.main.func1 (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :8080
可以看到服务已经起来了
2.3.3 制作v1.0版本的镜像并推送到hub仓库
- **v1.0 的 dockerfile,v1.0和v1.1的Dockerfile的区别是: **
v1.0: ENTRYPOINT ["./main", "-v", "1.0"]
v1.1: ENTRYPOINT ["./main", "-v", "1.1"]
suyajun@MacBook-Pro-2 abc % ls -l
total 40
-rw-r--r--@ 1 suyajun staff 1372 Dec 5 18:59 Dockerfile
-rw-r--r--@ 1 suyajun staff 1369 Dec 5 16:01 go.mod
-rw-r--r--@ 1 suyajun staff 7028 Dec 5 16:01 go.sum
-rw-r--r--@ 1 suyajun staff 361 Dec 5 18:03 main.go
suyajun@MacBook-Pro-2 abc % cat Dockerfile
###################1.构建阶段###################
# 使用 Golang 1.23.4 镜像作为构建环境,这里的go版本要和本地开发环境的版本号一致
FROM uhub.service.ucloud.cn/librarys/golang:1.23.4 AS build
# 构建阶段的工作目录
WORKDIR /go/src/test/
# 将当前目录下的所有文件复制到 Docker 容器的 /go/src/test/ 目录中
COPY . /go/src/test/
# 设置 Go 的代理,使用国内的 goproxy.cn 以提高下载速度
RUN go env -w GOPROXY=https://goproxy.cn,direct
# 使用 GOARCH=amd64 和 GOOS=linux 标志编译 Go 应用为 Linux 下的可执行文件。
# CGO_ENABLED=0 禁用 CGO(C 语言调用 Go 语言),确保静态链接。
# go build -v -o main . 会在工作目录中生成名为 main 的二进制文件。
RUN CGO_ENABLE=0 GOARCH=amd64 GOOS=linux go build -v -o main .
###################2.运行阶段###################
# 使用 Ubuntu 最新版本的镜像作为运行时环境
FROM uhub.service.ucloud.cn/librarys/ubuntu:latest AS api
# 在容器内创建一个 /app 目录,用于存放构建好的应用
RUN mkdir /app
# 将从构建阶段复制过来的 main 文件复制到容器内的 /app 目录
COPY --from=build /go/src/test/main /app
# 设置容器的工作目录为 /app,之后所有命令将在该目录下执行
WORKDIR /app
# 定义容器启动时的默认执行命令:执行 main 程序并传入参数 "-v" 和 "1.0"
ENTRYPOINT ["./main", "-v", "1.0"]
- 制作v1.0版本的镜像并推送到仓库
docker build -t uhub.service.ucloud.cn/librarys/abc:v1.0 .
docker push uhub.service.ucloud.cn/librarys/abc:v1.0
2.3.4 制作v1.1版本的镜像并推送到hub仓库
- **v1.0 的 dockerfile,v1.0和v1.1的Dockerfile的区别是: **
v1.0: ENTRYPOINT ["./main", "-v", "1.0"]
v1.1: ENTRYPOINT ["./main", "-v", "1.1"]
suyajun@MacBook-Pro-2 abc % ls -l
total 40
-rw-r--r--@ 1 suyajun staff 1372 Dec 5 18:59 Dockerfile
-rw-r--r--@ 1 suyajun staff 1369 Dec 5 16:01 go.mod
-rw-r--r--@ 1 suyajun staff 7028 Dec 5 16:01 go.sum
-rw-r--r--@ 1 suyajun staff 361 Dec 5 18:03 main.go
suyajun@MacBook-Pro-2 abc % cat Dockerfile
###################1.构建阶段###################
# 使用 Golang 1.23.4 镜像作为构建环境,这里的go版本要和本地开发环境的版本号一致
FROM uhub.service.ucloud.cn/librarys/golang:1.23.4 AS build
# 构建阶段的工作目录
WORKDIR /go/src/test/
# 将当前目录下的所有文件复制到 Docker 容器的 /go/src/test/ 目录中
COPY . /go/src/test/
# 设置 Go 的代理,使用国内的 goproxy.cn 以提高下载速度
RUN go env -w GOPROXY=https://goproxy.cn,direct
# 使用 GOARCH=amd64 和 GOOS=linux 标志编译 Go 应用为 Linux 下的可执行文件。
# CGO_ENABLED=0 禁用 CGO(C 语言调用 Go 语言),确保静态链接。
# go build -v -o main . 会在工作目录中生成名为 main 的二进制文件。
RUN CGO_ENABLE=0 GOARCH=amd64 GOOS=linux go build -v -o main .
###################2.运行阶段###################
# 使用 Ubuntu 最新版本的镜像作为运行时环境
FROM uhub.service.ucloud.cn/librarys/ubuntu:latest AS api
# 在容器内创建一个 /app 目录,用于存放构建好的应用
RUN mkdir /app
# 将从构建阶段复制过来的 main 文件复制到容器内的 /app 目录
COPY --from=build /go/src/test/main /app
# 设置容器的工作目录为 /app,之后所有命令将在该目录下执行
WORKDIR /app
# 定义容器启动时的默认执行命令:执行 main 程序并传入参数 "-v" 和 "1.0"
ENTRYPOINT ["./main", "-v", "1.1"]
- 制作v1.1版本的镜像并推送到仓库
docker build -t uhub.service.ucloud.cn/librarys/abc:v1.1 .
docker push uhub.service.ucloud.cn/librarys/abc:v1.1
2.4 进行蓝绿发布
2.4.1 将abc发布到k8s集群中
cat abc-pod-v1.0.yaml
apiVersion: v1
kind: Pod
metadata:
name: abc-v1-0
labels:
app: abc
version: v1.0
spec:
containers:
- name: abc
image: uhub.service.ucloud.cn/librarys/abc:v1.0
cat abc-pod-v1.1.yaml
apiVersion: v1
kind: Pod
metadata:
name: abc-v1-1
labels:
app: abc
version: v1.1
spec:
containers:
- name: abc
image: uhub.service.ucloud.cn/librarys/abc:v1.1
cat abc-service.yaml
apiVersion: v1
kind: Service
metadata:
name: abc
spec:
selector:
app: abc
version: v1.0
ports:
- name: http
port: 8080
targetPort: 8080
nodePort: 31080
type: NodePort
# 查看资源清单文件
[root@k8s-master abc]# ls -l
total 12
-rw-r--r--. 1 root root 182 Dec 6 09:52 abc-pod-v1.0.yaml
-rw-r--r--. 1 root root 182 Dec 6 09:52 abc-pod-v1.1.yaml
-rw-r--r--. 1 root root 204 Dec 6 09:52 abc-service.yaml
# 创建资源
[root@k8s-master abc]# kubectl apply -f .
pod/abc-v1-0 created
pod/abc-v1-1 created
service/abc created
# 查看创建的资源
[root@k8s-master abc]# kubectl get all|grep abc
pod/abc-v1-0 1/1 Running 0 3s
pod/abc-v1-1 1/1 Running 0 3s
service/abc NodePort 10.111.158.99 <none> 8080:31080/TCP 3s
# 查看pod标签
[root@k8s-master abc]# kubectl get pod --show-labels
NAME READY STATUS RESTARTS AGE LABELS
abc-v1-0 1/1 Running 0 5m13s app=abc,version=v1.0
abc-v1-1 1/1 Running 0 5m13s app=abc,version=v1.1
# 可以看到service中Selector选中的是v1.0
[root@k8s-master abc]# kubectl describe svc abc
Name: abc
Namespace: default
Labels: <none>
Annotations: <none>
Selector: app=abc,version=v1.0
Type: NodePort
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.111.158.99
IPs: 10.111.158.99
Port: http 8080/TCP
TargetPort: 8080/TCP
NodePort: http 31080/TCP
Endpoints: 10.244.85.202:8080
Session Affinity: None
External Traffic Policy: Cluster
2.4.2 切换流量到v1.1
- 模拟用户访问
while sleep 1;do curl localhost:31080;echo ;done
- 修改abc-service.yaml中的version 为v1.1
[root@k8s-master abc]# cat abc-service.yaml
apiVersion: v1
kind: Service
metadata:
name: abc
spec:
selector:
app: abc
version: v1.1
ports:
- name: http
port: 8080
targetPort: 8080
nodePort: 31080
type: NodePort
kubectl apply -f abc-service.yaml
- 验证查看
[root@k8s-master abc]# kubectl describe svc abc|grep Selector
Selector: app=abc,version=v1.1
注意:如果是通过浏览器验证可能客户端会有缓存不会立刻生效。用苹果的 safari浏览器 10秒左右会生效
发版的时候如何实现:
1、如果service最开始的时候是指向v1.0
2、发布的时候先发布v1.1
3、service的标签指向v1.1
4、观察有问题将service的标签指向v1.0做回退,观察没问题 删除v1.0的pod
如果有平台的发布可以实现灰度发布(这种的需要单独的运维平台)
1、发布的时候通过页面选择其中两个节点进行发布,发布完的不允许再点发布。
2、下线nacos
3、从apisix中将服务摘除
4、删除pod
5、apply 下线的两个节点
在这个基础上,也可以实现金丝雀的发布。比如我们有10个节点(通过更新pod的比例),我们只发布其中4个节点,然后会有40%的流量到新发布的服务上面,60%的流量还在旧的服务上。