GO学习笔记 | 第九章节 JWT、Redis 入门与 K8s 部署实战| K8s 部署 web服务、MySQL、Redis 与 Ingress

代码仓库地址:Darling-123456/go_learning: go学习过程记录

核心内容:kubectl 日志调试、K8s部署web服务、K8s 部署 MySQL、K8s 部署 Redis、Ingress 七层路由、环境变量配置、K8s 面试要点
前置知识:K8s Deployment/Service 基本概念、Docker 镜像


一、kubectl 常用调试命令

1.1 查看 Pod 日志

1
2
3
4
5
6
7
8
9
# 查看指定 Pod 的日志
kubectl logs <pod-name>

# 实时跟踪(follow 模式,有一条输出一条)
kubectl logs -f <pod-name>

# 先列出所有 Pod
kubectl get pods
kubectl logs -f webook-deployment-xxx-yyy

-f 相当于 tail -f——应用有新日志就实时显示,排查问题非常实用。

1.2 查看 Pod 详细信息

1
2
# 查看 Pod 的状态、事件、配置等
kubectl describe pod <pod-name>

当 Pod 起不来时,describe 可以看到具体原因(镜像拉不下来?端口冲突?资源不够?)。

1.3 常用命令速查

命令 作用
kubectl get pods 列出所有 Pod
kubectl get services 列出所有 Service
kubectl get deployments 列出所有 Deployment
kubectl logs <pod> 查看 Pod 日志
kubectl logs -f <pod> 实时跟踪日志
kubectl describe pod <pod> 查看 Pod 详细状态
kubectl apply -f xxx.yaml 应用配置
kubectl delete -f xxx.yaml 删除配置

二、Kubernetes 部署web

image-20260628173937095

2.1 K8s 是什么?

image-20260628174031221

核心三件套:

概念 一句话
Pod 最小的部署单元,一个 Pod 跑一个实例
Service 逻辑上的服务入口,对 Pod 做负载均衡
Deployment 管理 Pod 的控制器,保证 Pod 数量正确

一个pod里面可以跑很多个容器。deployment管理:如果它管3个pod,其中有一个崩了,那它就帮你启动一个pod,如果多了一个pod变成4个了,那就帮你删掉一个

外卖餐厅类比(最通俗易懂)

  • Pod → 【后厨的工位 / 厨师团队】
    • 一个工位可以有 1 个主厨(主业务容器),也可以配几个帮厨(Sidecar 辅助容器,如给主厨递调料/做记录的)。主厨挂了,整个工位完蛋。
  • Service → 【外卖平台的固定商家电话/门店入口】
    • 不管后厨哪个厨师在掌勺,甚至厨师换了一拨人(Pod 漂移),顾客(客户端)始终只需要拨打这个固定的电话(Service 的 ClusterIP),或者点击这个店铺入口。电话会自动把订单分配给此时此刻正在上班的厨师(负载均衡)。
  • Deployment → 【餐厅店长】
    • 店长定下了后厨必须要有 3 个工位(Replicas=3)。如果有工位塌了(Pod 挂掉),店长立刻指挥搭建一个新工位顶上去(自动自愈)。换新菜单时,店长保证逐批更新工位,避免同时停摆(滚动更新)。

微服务/数据中心架构类比(更符合 Go 后端工程师直觉)

  • Pod → 【一台物理机上的微服务实例(进程)】(对应 IP 属性)
    • 它有自身的 IP 地址,如果这个进程崩溃了,机器重启后得到一个新的 IP。
  • Service → 【负载均衡器(Nginx 上的 Upstream 或云厂商的 SLB)】(对应访问入口)
    • 不管背后的微服务实例 IP 怎么变,客户端访问的 VIP(虚 IP)永远是固定的,SLB 会自动把请求转发给背后存活的实例。
  • Deployment → 【运维自动化脚本(或 Ansible 机器人)】(对应调度者)
    • 它负责启动进程、监测进程存活数、如果进程挂了就重新启动。它是整个服务生命周期的管家。

2.2 Docker 启用 K8s 支持 && 安装kubectl

学习阶段直接用 Docker Desktop 内置的 K8s,在设置里勾选 Enable Kubernetes 即可。

image-20260628180043064

image-20260628181022736

image-20260628180924943

在windows下用管理员运行

1
winget install Kubernetes.kubectl

安装完成后,重新打开一个新的cmd,输入下面的命令

1
kubectl version --client
1
2
3
4
#成功之后的
C:\Users\xxx>kubectl version --client
Client Version: v1.34.1
Kustomize Version: v5.7.1

如果输出系统无法执行制定程序的话,输入这个命令

1
where kubectl

它会给你打印出 Windows 到底是从哪个文件夹里找的 kubectl.exe

  • 如果是类似 C:\Users\你的用户名\kubectl.exe,那绝对就是那个 2KB 的坏文件。找到它之后,去那个文件夹里,把这个 kubectl.exe 删掉!

然后重新输入,看看是不是输出版本号就行了

2.3 用Kubernetes部署web服务器

image-20260630161343692

部署方案

image-20260630161404072

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//main.go
func main() {
//初始化DB
//db := initDB()
//初始化Server
//server := initWebServer()

//初始化User
//u := initUser(db)
//注册路由
//u.RegisterRoutes(server)

//启动
server := gin.Default()
server.GET("/hello", func(ctx *gin.Context) {
ctx.String(http.StatusOK, "你好,你来了")
})
server.Run(":9090")
}

K8s 部署 MySQL 和 Redis 比较复杂(涉及 PV/PVC/ConfigMap),第一次部署时可以先去掉代码里对 MySQL 和 Redis 的依赖,只保证 Web 服务能启动、监听端口即可。后续再慢慢加回来。

image-20260630163014940

准备K8s容器镜像

image-20260630163107168

dockerfile文件:打包

在webook目录下

核心作用:把你本地编译好的 Go 可执行文件打包进一个 Ubuntu 系统镜像里,让它能在 Docker 容器中运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 1. 指定基础镜像:使用 Ubuntu 20.04 LTS 作为容器运行环境。
# 所有后续的操作(如复制文件、执行命令)都会在这个精简版的 Linux 系统中进行。
FROM ubuntu:20.04
# 2. 复制二进制文件:将宿主机(你电脑)当前目录下的 "webook" 可执行文件,
# 复制到容器内部的 "/app" 目录下,并保留文件名 "webook"。
# 这是最关键的一步,相当于把编译好的程序“扔”进镜像里。
COPY webook /app/webook
# 3. 设定工作目录:容器启动后,默认的终端路径会切换到这里(/app)。
# 后续的 ENTRYPOINT 命令和相对路径操作都会以此为基准,方便程序读取配置或日志。
WORKDIR /app
# 4. 定义程序入口:容器启动时,会自动执行 "/app/webook" 这个程序。
# 使用中括号(Exec 格式)表示不会创建 Shell 子进程,信号传递更直接,性能也更好。
# 这相当于告诉 Docker:“开机就运行这个 Go 服务”。
ENTRYPOINT ["/app/webook"]

# 5. [已注释] 添加作者元数据:这行命令不影响程序运行,只是给镜像打个标签(类似备注)。
# 如果取消注释,可以在 docker inspect 中看到作者信息 "darling-ge"。
#LABEL authors="darling-ge"

# 6. [已注释] 测试/调试用的入口点:原本用来覆盖上面的 ENTRYPOINT,用于调试容器环境。
# 如果取消注释并将上面的 ENTRYPOINT 注释掉,容器启动后会执行 "top -b" 命令,
# 用来持续查看容器内的进程列表(常用于验证镜像是否构建成功)。
#ENTRYPOINT ["top", "-b"]

无注释版本

1
2
3
4
FROM ubuntu:20.04
COPY webook /app/webook
WORKDIR /app
ENTRYPOINT ["/app/webook"]

image-20260630163254093

交叉编译:为 Linux 编译 Go 程序

为什么需要?

开发用 Mac/Windows,服务器跑 Linux。Go 天生支持交叉编译

1
2
#在webook目录下输入这个命令
GOOS=linux GOARCH=arm go build -o vbook .
环境变量 含义 常见值
GOOS 目标操作系统 linux, darwin, windows
GOARCH 目标 CPU 架构 amd64, arm64

为什么不在 Docker 里编译?——Docker 里编译很慢。正常是 CI(持续集成)过程编译好,再打包进镜像。

1
2
3
4
#windows下输入以下命令
$env:GOOS="linux"
$env:GOARCH="arm"
go build -o webook .
1
2
3
4
5
PS E:\go_learning\webook_project\webook> $env:GOOS="linux"
PS E:\go_learning\webook_project\webook> $env:GOARCH="arm"
PS E:\go_learning\webook_project\webook> go build -o webook .
# runtime/cgo
gcc: error: unrecognized command-line option '-marm'; did you mean '-mabm'?

这个报错是因为尝试编译 ARM 架构 的 Linux 程序,而 Windows 系统上的 gcc 编译器不支持生成 ARM 代码

Go 在编译时如果代码中用到了 cgo(即调用了 C 语言库),就需要调用宿主机的 C 编译器(gcc)来编译 C 部分。而 Windows 上的 gcc(通常是 MinGW)是为 x86/x64 设计的,不支持 ARM 的 -marm 选项,所以报错。

解决方案:禁用 cgo

如果不需要 C 语言依赖(绝大多数 Go 项目都不用),可以直接禁用 cgo,让 Go 使用纯 Go 编译,这样就绕过了 gcc。

1
2
3
4
$env:CGO_ENABLED=0
$env:GOOS="linux"
$env:GOARCH="arm"
go build -o webook .

这样应该就能成功生成 ARM 版本的 Linux 可执行文件。

打包镜像

在webook目录下执行以下命令:

1
docker build -t darling/webook:v0.0.1 .

得到如下图所示的最后一行的darling/webook,这个darling是标签,可以自己输

image-20260701104530238

Makefile:一键构建

1
2
3
4
5
6
7
8
9
.PHONY: docker
docker:
@rm webook || true
@$env:CGO_ENABLED=0
@$env:GOOS="linux"
@$env:GOARCH="arm"
@go build -o webook .
@docker rmi -f darling/webook:v0.0.1 .
@docker build -t darling/webook:v0.0.1 .

这是第一次写的,还是windows版本,这根本运行不了

1
2
3
4
5
6
.PHONY: docker
docker:
@rm webook || true
@CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -o webook .
@docker rmi -f darling/webook:v0.0.1
@docker build --no-cache -t darling/webook:v0.0.1 .

这是linux版本的makefile

执行make docker命令,就可以删除原来的webook,重新构建一个webook

要注意:

1.在windows下执行的时候,golang ide里面的命令行powershell是不行的,得去webook目录下打开git bash,是模拟的linux环境,然后执行make docker

2.要记得写CGO_ENABLED=0,原因和上面交叉编译的原因一样的

2.4 编写Deployment

1.编写deployment.yaml文件

image-20260701112717266

image-20260701115628067

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
# API 版本:apps/v1 是 Kubernetes 中 Deployment 资源的稳定版本
apiVersion: apps/v1
# 资源类型:Deployment 负责管理 Pod 的副本数、滚动更新和回滚
kind: Deployment

# 元数据:该 Deployment 自身的名字
metadata:
name: webook

# spec 是 Deployment 的期望状态描述
spec:
# 副本数量:期望运行 3 个 Pod 实例
replicas: 3

# selector(标签选择器):用于匹配该 Deployment 管理的所有 Pod
# 关键作用:Deployment 通过这个选择器来识别哪些 Pod 属于自己,
# 并控制它们的数量、更新等。该选择器必须与 template.metadata.labels 中定义的标签一致。
selector:
matchLabels:
app: webook # 必须和下面的 labels 完全一致

# template 是 Pod 的模板,描述每次创建 Pod 时的具体配置
template:
metadata:
name: webook # Pod 的名称(实际运行时会被加上随机后缀)
labels:
app: webook # 这个标签必须与 selector.matchLabels 一致,
# 否则 Deployment 无法识别自己管理的 Pod,创建会失败。
spec:
containers:
- name: webook # 容器名称
image: darling/webook:v0.0.1 # 容器镜像
ports:
- containerPort: 9090 # 声明容器内应用程序监听的端口
# 这个端口必须与你的 Go 代码中 server.Run(":9090") 的端口一致
# 作用:方便 Service 或 Ingress 通过该端口访问容器内服务,
# 同时也是一个文档化信息,提醒运维人员该服务监听的端口。

无注释版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: apps/v1
kind: Deployment
metadata:
name: webook
spec:
#副本数量
replicas: 3
selector:
matchLabels:
app: webook
# template描述的是你的POD是什么样子的
template:
metadata:
name: webook
labels:
app: webook
spec:
containers:
- name: webook
image: darling/webook:v0.0.1
ports:
- containerPort: 9090

注意:

1.第八行selector的 app: webook一定要和第16行的webook是对应的

selectortemplate.metadata.labels 必须匹配

  • Kubernetes 的工作机制:Deployment 本身不直接管理 Pod,而是通过标签选择器(selector)来“认领”带特定标签的 Pod。当 Deployment 创建或更新时,它会根据 selector 查找所有带有 app: webook 标签的 Pod,并将它们视为自己的“手下”,负责维护它们的副本数量、执行滚动更新等。
  • 如果不匹配:Deployment 创建时,API Server 会校验 selector 是否匹配 template 中的 labels,如果不匹配则直接报错(selector does not match template labels)。即使强行忽略,Deployment 也找不到自己该管理哪些 Pod,导致无法工作。

2.ports的9090要和main函数里面的server.Run(“:9090”)这个要对应上

  • 这是一个声明性配置containerPort 并不会实际改变容器内的端口绑定,它只是告诉 Kubernetes“这个容器会监听 9090 端口”。这个信息主要用于:
    • 当创建 Service 时,可以通过 targetPort: 9090 将流量转发到容器的正确端口。
    • 便于其他开发者或运维人员了解该服务提供的端口号。
  • 如果不一致:虽然容器仍能正常运行(因为程序监听的是 9090,而 containerPort 只是一个声明),但如果你配置 Service 转发到 9090 而程序实际监听 8080,那么流量就会转错,导致服务不可达。因此,保持一致是最佳实践,避免后续配置错误。

2.验证yaml文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1. 根据 YAML 文件创建或更新 Kubernetes 资源(这里是 Deployment)
# -f 指定文件路径,kubectl 会读取该文件并发送给 API Server
# 如果资源已存在则更新,不存在则创建
kubectl apply -f k8s-webook-deployment.yaml

# 2. 查看当前命名空间下所有 Pod 的运行状态
# 输出列:NAME(Pod名)、READY(就绪容器数/总容器数)、STATUS(状态)、RESTARTS(重启次数)、AGE(运行时间)
# 常用状态:Running(正常)、Pending(调度中)、ImagePullBackOff(镜像拉取失败)、CrashLoopBackOff(反复崩溃)
kubectl get pods

# 3. 查看当前命名空间下所有 Deployment 的部署状态
# 输出列:NAME(部署名)、READY(就绪副本数/期望副本数)、UP-TO-DATE(最新版本副本数)、AVAILABLE(可用副本数)、AGE
# 若 READY 和 AVAILABLE 等于期望副本数,说明应用已成功滚动更新并稳定运行
kubectl get deployments
1
2
3
kubectl apply -f k8s-webook-deployment.yaml
kubectl get pods
kubectl get deployments
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
darling123456 MINGW64 /e/go_learning/webook_project/webook (week3)
$ kubectl apply -f k8s-webook-deployment.yaml
deployment.apps/webook created

darling123456 MINGW64 /e/go_learning/webook_project/webook (week3)
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
webook-754bdfb74f-4tttd 1/1 Running 0 83s
webook-754bdfb74f-h6997 1/1 Running 0 83s
webook-754bdfb74f-zh899 1/1 Running 0 83s

@darling123456 MINGW64 /e/go_learning/webook_project/webook (week3)
$ kubectl get deployments
NAME READY UP-TO-DATE AVAILABLE AGE
webook 3/3 3 3 3m59s

执行上面这3个命令出现差不多的结果就是这个yaml没写错

3.K8s 的核心设计:声明式驱动

image-20260701120534239

apiVersion 的作用:告诉 K8s “这份配置该用什么版本的规范来解读”。不同 apiVersion 对应不同的字段结构和行为。写 YAML 时 GoLand 会自动提示正确的 apiVersion

  • “声明式” vs “命令式”
    • 命令式(你日常操作):你告诉电脑“怎么做”。比如:go build -o webook .(编译)→ docker build ...(打包)→ kubectl apply ...(部署)。每一步你都要亲自指挥。
    • 声明式(K8s 的做法):你只告诉 K8s“最终要什么”。比如你在 YAML 里写 replicas: 3,K8s 看了之后,自己去判断现在有几个 Pod,少了就创建,多了就删除,你不需要告诉它怎么创建、怎么删除
  • 配置驱动/元数据驱动:一切皆以 YAML 配置为准。K8s 的“大脑”(控制器)就像一个永不停歇的监控器,不停地扫描这些配置,确保现实状态无限趋近于配置里的期望状态。

4.各个字段详细含义

image-20260701120714145

image-20260701120834227

image-20260701121000901

image-20260701121059362

2.5 编写service

1.编写service.yaml

image-20260701121120403

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
# API 版本:v1 是 Kubernetes 核心 API 组,用于 Service 资源
apiVersion: v1

# 资源类型:Service,用于将 Pod 的网络服务暴露给外部或内部客户端
kind: Service

# 元数据:该 Service 自身的名称,用于在集群内标识
metadata:
name: webook

# spec 是 Service 的期望状态描述
spec:
# 服务类型:LoadBalancer
# 在 Docker Desktop 或 Minikube 等本地环境中,会自动将 Service 的端口映射到宿主机 localhost,
# 使得可以通过 localhost 直接访问服务,无需关心 NodePort 的随机端口。
type: LoadBalancer

# 标签选择器:用于选择该 Service 将流量转发到哪些 Pod
# 必须与 Deployment 中 Pod 模板的 labels 一致(app: webook)
selector:
app: webook

# 端口映射列表
ports:
# 协议类型:TCP(HTTP/HTTPS 均基于 TCP)
- protocol: TCP

# 端口名称(可选),便于标识,尤其在多端口 Service 中很有用
name: http

# Service 对外暴露的端口(宿主机访问的端口)
# 设置为 80,则浏览器中直接访问 http://localhost 即可访问服务
port: 80

# 目标端口:流量将被转发到 Pod 内容器的此端口
# 必须与你的 Go 程序监听的端口一致(代码中 server.Run(":9090"))
targetPort: 9090
1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
name: webook
spec:
type: LoadBalancer
selector:
app: webook
ports:
- protocol: TCP
name: http
port: 80
targetPort: 9090

注:targetport要和deployment.yaml里面的containerPort要对应上

2.启动服务(验证)

image-20260701130706555

1
2
3
4
5
6
7
8
9
darling123456 MINGW64 /e/go_learning/webook_project/webook (week3)
$ kubectl apply -f k8s-webook-service.yaml
service/webook created

darling123456 MINGW64 /e/go_learning/webook_project/webook (week3)
$ kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 2d18h
webook LoadBalancer 10.96.102.175 172.19.0.5 80:32332/TCP 12s

执行后访问如下网站

1
http://localhost:80/hello

image-20260701131144395

至此就算配置成功了

3.可能遇到的问题

访问http://localhost:80/hello这个网址的时候没有成功

那你要看看,spec的type一定要是LoadBalancer,而不能是NodePort,不然就会访问失败

4. Service 的三种类型

Service 主要有三种类型,区别在对外暴露的方式

类型 访问方式 使用场景
ClusterIP 只能在集群内部访问 服务间互相调用(如 Web 调 MySQL)
NodePort 通过节点 IP + 端口从外部访问 开发调试,简单暴露服务
LoadBalancer 云厂商分配公网 IP,自动负载均衡 生产环境对外暴露

Docker Desktop 内置的 K8s 支持 LoadBalancer,会自动分配 localhost 可访问的端口。

Service 怎么找到 Pod? 通过 selector 标签匹配。Service 持续监听带匹配标签的 Pod,自动负载均衡——Pod 挂了、重建了、IP 变了,Service 都能自动感知。

2.6 总结

1. Go 源码 → 二进制文件

  • 写了 main.go,监听 9090 端口,注册 /hello 路由。
  • 执行 make docker → 在 Windows 上交叉编译出 Linux ARM 可执行文件 webook
  • CGO_ENABLED=0 → 纯静态编译,不依赖 C 库,能直接在 Ubuntu 容器里跑。

2. 二进制 → Docker 镜像

  • Dockerfile 做了 3 件事:
    1. 拉取 ubuntu:20.04 作为基础镜像;
    2. webook 复制到 /app/webook
    3. 容器启动时自动执行 /app/webook
  • 最终打成一个镜像:darling/webook:v0.0.1,存在本地 Docker 仓库里。

3. 镜像 → Kubernetes Pod

  • 执行 kubectl apply -f k8s-webook-deployment.yaml,提交 Deployment 给 K8s。
  • K8s 拉取本地镜像,创建 3 个 Pod 副本,每个 Pod 里跑着你的 Go 服务(监听 9090)。
  • kubectl get pods 显示 Running → 服务已正常启动。

4. Service 暴露 Pod(关键!)

  • 执行 kubectl apply -f k8s-webook-service.yaml,创建 Service。
  • 关键配置:
    • type: LoadBalancer → Docker Desktop 会把 Service 的端口直接绑定到宿主机 localhost
    • port: 80 → 浏览器访问 http://localhost 时直接命中,不需要带端口号
    • targetPort: 9090 → 流量转发到 Pod 内 Go 程序监听的端口。
  • Endpoints 控制器通过 selector: app: webook 找到 3 个 Pod,绑定它们的 IP 和 9090 端口。

5. 最终访问成功

  • 在浏览器输入 http://localhost/hello
  • 流量直达 Service 的 80 端口 → 负载均衡到任意一个 Pod 的 9090 端口 → Go 程序处理请求 → 返回 "你好,你来了"

2.7 概念速查

概念 一句话
交叉编译 GOOS/GOARCH 指定目标平台,编译 Linux 可执行文件
K8s 声明式驱动 用户写 YAML 声明”我要什么”,K8s 自动执行”怎么做到”
Pod K8s 最小部署单元,一个 Pod 跑一个实例
Deployment 管理 Pod 的控制器,保证 Pod 数量正确
Service Pod 的固定入口,selector 标签匹配 + 负载均衡
apiVersion 告诉 K8s 用哪个版本的 API 规范解读 YAML
selector.matchLabels Deployment 通过标签找到它管理的 Pod
ClusterIP / NodePort / LoadBalancer Service 三种暴露类型:内网 / 节点端口 / 公网 LB

三、K8s 部署 MySQL

image-20260701150312125

3.1 核心问题:数据不能丢

image-20260701150347693

Pod 是临时的——挂了重建之后,里面的数据全没了。MySQL 必须用 PersistentVolume(PV) 把数据持久化到宿主机。

3.2 mysql-deployment.yaml 和 service.yaml 基础版本

image-20260701152053860

1
kubectl apply -f k8s-webook-deployment.yaml
1
kubectl apply -f k8s-webook-service.yaml
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
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: webook-mysql
labels:
app: webook-mysql
spec:
replicas: 1
selector:
matchLabels:
app: webook-mysql
template:
metadata:
name: webook-mysql
labels:
app: webook-mysql
spec:
containers:
- name: webook-mysql
image: mysql:8.0
env:
- name: MYSQL_ROOT_PASSWORD
value: 040725ge
imagePullPolicy: IfNotPresent
ports:
- containerPort: 3306
protocol: TCP
restartPolicy: Always

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#service.yaml
apiVersion: v1
kind: Service
metadata:
name: webook-mysql
spec:
selector:
app: webook-mysql
ports:
- protocol: TCP
#这是咱们外部访问用的端口
port: 11309
targetPort: 3306
#如果下面的type是nodeport,那就得有下面这行nodeport
#nodePort: 30001
type: LoadBalancer

1
2
3
//注:这是在docker-compose.yaml中的环境变量,也就是root用户密码要在上面的deployment.yaml中配置
environment:
MYSQL_ROOT_PASSWORD: 040725ge

理解这几个端口号:

image-20260701153310612

golang中数据库配置

image-20260701153911331

进入数据库之后,再自己创建一个webook的数据库然后修改属性

image-20260701155005700

当前还没有持久化,如果你直接down了,那下次还得换成mysql这个数据库而不是webook这个数据库,因为down之后所有的资源都没有了,包括创建的webook数据库。

3.3 加入持久化内容

image-20260701154909019

image-20260701161648699

image-20260701162542187

mysql storage、PV、PVC关系

image-20260701163442900

组件关系详解(完整版)
第一个问题:这五个东西到底是什么?

为了彻底记住,用“搬家”来类比:

组件 搬家类比 在 K8s 里是什么
/var/lib/mysql 你家的卧室:MySQL 这个“人”只住这间房,只把东西放这里 MySQL 容器内部的数据目录,MySQL 进程写死的路径,改不了
mysql-storage 卧室门口的标签:“这是卧室A”,方便工人(Pod)认 Deployment 里起的一个名字,用来在 Pod 内部标识这个存储槽位
PVC 搬家合同:写明了“我要一个 1GB 的柜子,要能读写” 存储申请单,描述了需求(多大、什么类型、什么速度)
PV 仓库里的柜子:已经存在、贴上标签“1GB、可读写”的实体柜子 集群里已准备好的存储资源,管理员提前创建的
宿主机目录(/mnt/live 仓库本身:柜子摆放在这个仓库里,数据最终存在这里 物理存储位置,在你的 WSL2 虚拟机硬盘上
第二个问题:这五层是怎么连接起来的?(数据流向)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
MySQL 进程

│ 第1步:MySQL 固定往这个目录写数据

/var/lib/mysql(容器内的目录)

│ 第2步:通过 volumeMounts,把这个目录“绑”到下面的存储槽位

mysql-storage(Pod 内的卷名,一个"插槽")

│ 第3步:这个插槽通过 claimName 指向下面的 PVC

PVC(webook-mysql-claim,存储申请单)

│ 第4步:PVC 通过 storageClassName + accessModes + 容量,匹配到下面的 PV

PV(my-local-pv,集群里的存储资源"现房")

│ 第5步:PV 的 hostPath 字段,指向真正的物理位置

宿主机目录(/mnt/live,最终存数据的地方)

每一层只认识“上一层”和“下一层”,但不认识隔层的东西:

  • MySQL 不知道有 mysql-storage,它只管往 /var/lib/mysql 写。
  • mysql-storage 不知道有 PV,它只知道 claimName 指向 PVC。
  • PVC 不知道有宿主机目录,它只管匹配 PV。
  • PV 知道有宿主机目录,因为 hostPath 是它自己写的。
第三个问题:YAML 里怎么体现这些关系?

关系①:/var/lib/mysqlmysql-storage 的连接(在 Deployment 里)

1
2
3
4
5
containers:
- name: webook-mysql
volumeMounts:
- mountPath: /var/lib/mysql # ← 容器内的目录
name: mysql-storage # ← 连接到 Pod 里的这个卷

解读:把容器内的 /var/lib/mysql“接到” Pod 里名叫 mysql-storage 的卷上。

关系②:mysql-storage 和 PVC 的连接(也在 Deployment 里)

1
2
3
4
volumes:
- name: mysql-storage # ← 就是上面那个名字
persistentVolumeClaim:
claimName: webook-mysql-claim # ← 指向这个 PVC

解读mysql-storage 这个插槽,接的是名叫 webook-mysql-claim 的 PVC。

关系③:PVC 和 PV 的连接(靠“标签+容量”匹配)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# PVC
spec:
storageClassName: suibian # ← 我要这种类型的
accessModes:
- ReadWriteOnce # ← 我要这个访问模式
resources:
requests:
storage: 1Gi # ← 我要 1GB

# PV
spec:
storageClassName: suibian # ← 我提供这种类型
accessModes:
- ReadWriteOnce # ← 我支持这个访问模式
capacity:
storage: 1Gi # ← 我有 1GB
hostPath:
path: "/mnt/live" # ← 数据实际放在这里

解读:K8s 自动匹配,找到这三个条件都符合的 PV,然后 PVC 和 PV 绑定。

注意:PVC 里没有claimName 指向 PV,因为它们是自动匹配绑定的,而不是手动指定名字。你在 kubectl get pvc 里看到的 VOLUME 列,是绑定成功后 K8s 自动填上去的。

第四个问题:为什么需要这么多层?直接让 Pod 用 PV 不行吗?

不行。原因有两点:

  1. 解耦应用和管理员

    • 开发者(写 Deployment 的人)只需要写 PVC,说“我要 1GB”。
    • 管理员(管集群的人)提前准备好 PV,说“我这里有一堆 1GB、2GB、5GB 的存储”。
    • 双方不用商量具体名字,K8s 自动撮合。
  2. Pod 重建时数据不丢

    • Pod 可以被删掉重建(IP、名字都变了),但 PVC 还在。
    • 新 Pod 只要用同一个 PVC 名字,就能找到原来绑定的 PV,数据接着用。
一句话总结

/var/lib/mysql 是 MySQL 写数据的窗口mysql-storage 是 Pod 里接这个窗口的插槽,PVC 是申请单,PV 是集群里的现房,宿主机 /mnt/live物理位置。数据从 MySQL 出发,一路经过这五个站,最终存到你的硬盘上。Pod 重启后,走同样的路,就能找回原来的数据。

对应关系速查表(写 YAML 时对着检查)
要检查的匹配 位置 A 位置 B 不匹配的后果
PVC 和 PV 的 storageClassName pvc.spec.storageClassName pv.spec.storageClassName PVC 一直 Pending,找不到 PV
PVC 和 PV 的 accessModes pvc.spec.accessModes pv.spec.accessModes 绑定失败
PVC 申请的容量 ≤ PV 的容量 pvc.spec.resources.requests.storage pv.spec.capacity.storage 绑定失败(容量不够)
Deployment 的 claimName volumes.persistentVolumeClaim.claimName PVC 的 metadata.name Pod 找不到 PVC,启动失败
Deployment 的 volumeMounts.name volumeMounts.name volumes.name 容器目录找不到对应的卷,挂载失败
Deployment 和 Service 的 selector spec.selector.matchLabels spec.template.metadata.labels Deployment 管不了 Pod,Service 找不到 Pod

具体的配置文件

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# ============================================================
# 1. PersistentVolume (PV) - 定义物理存储资源
# 相当于“房东手里的现房”:一块提前准备好的存储空间
# ============================================================
apiVersion: v1
kind: PersistentVolume
metadata:
name: my-local-pv # PV 的名称,集群内唯一
labels:
role: my-local-pv # 标签,可用于筛选和匹配
spec:
# 存储类名称:用于和 PVC 绑定
# 注意:使用 hostPath 时,这个名称可以自定义(如 "suibian")
# 但在云环境(如阿里云)中,需要改成云厂商提供的 StorageClass 名称
storageClassName: suibian

# 存储容量:1GB
capacity:
storage: 1Gi

# 访问模式:ReadWriteOnce 表示该卷只能被单个节点以读写方式挂载
# 其他模式:ReadOnlyMany(多节点只读)、ReadWriteMany(多节点读写)
accessModes:
- ReadWriteOnce

# hostPath:将宿主机(你的电脑/Docker 虚拟机)上的目录挂载到容器
# 注意:在 Docker Desktop / WSL2 环境中,需要确保该路径存在
# 生产环境建议使用云盘或 NFS 等更可靠的存储
hostPath:
path: "/mnt/live" # 宿主机上的目录路径


# ============================================================
# 2. PersistentVolumeClaim (PVC) - 申请存储资源
# 相当于“租客的租房申请单”:声明我需要多大的存储
# ============================================================
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: webook-mysql-claim # PVC 名称,Deployment 中通过此名称引用
labels:
app: webook-mysql-claim # 标签,用于标识
spec:
# 存储类名称:必须和 PV 中的 storageClassName 一致
# 如果一致,K8s 会将此 PVC 与对应的 PV 绑定
storageClassName: suibian

# 访问模式:必须和 PV 中的 accessModes 匹配
accessModes:
- ReadWriteOnce

# 资源请求:声明需要的存储大小
resources:
requests:
storage: 1Gi # 申请 1GB 空间


# ============================================================
# 3. Deployment - MySQL 应用部署
# 相当于“租客入住”:将 MySQL 容器运行起来,并挂载存储
# ============================================================
apiVersion: apps/v1
kind: Deployment
metadata:
name: webook-mysql # Deployment 名称
labels:
app: webook-mysql # 标签,用于 Service 选择
spec:
# 副本数:只运行 1 个 MySQL 实例
replicas: 1

# 标签选择器:Deployment 通过它找到自己要管理的 Pod
selector:
matchLabels:
app: webook-mysql

# Pod 模板:描述 Pod 的详细配置
template:
metadata:
name: webook-mysql # Pod 名称(实际运行时会被加上随机后缀)
labels:
app: webook-mysql # Pod 的标签,必须与 selector 匹配

spec:
containers:
- name: webook-mysql # 容器名称
image: mysql:8.0 # MySQL 镜像
env:
- name: MYSQL_ROOT_PASSWORD
value: 040725ge # MySQL root 密码(生产环境建议用 Secret)
imagePullPolicy: IfNotPresent # 镜像拉取策略:本地有则用本地,没有则拉取

# 卷挂载:将 PVC 挂载到容器的指定目录
volumeMounts:
- mountPath: /var/lib/mysql # 容器内的挂载路径(MySQL 数据存放目录)
name: mysql-storage # 对应下面 volumes 中的 name

# 容器端口
ports:
- containerPort: 3306
protocol: TCP

# Pod 重启策略:总是重启(默认值)
restartPolicy: Always

# 卷定义:声明 Pod 需要使用的存储资源
volumes:
- name: mysql-storage # 卷名称,与 volumeMounts 中的 name 对应
persistentVolumeClaim:
claimName: webook-mysql-claim # 引用前面定义的 PVC 名称


# ============================================================
# 4. Service - 暴露 MySQL 服务
# 相当于“门牌号”:让外部(如你的 Go 应用)能访问到 MySQL
# ============================================================
apiVersion: v1
kind: Service
metadata:
name: webook-mysql # Service 名称
spec:
# 标签选择器:Service 通过它找到要负载均衡的 Pod
selector:
app: webook-mysql # 必须匹配 Pod 的 labels

ports:
- protocol: TCP
# 外部访问端口:你的 Go 应用通过此端口连接 MySQL
# 访问地址:webook-mysql.default.svc.cluster.local:11309
port: 11309

# 目标端口:容器内部 MySQL 监听的端口
targetPort: 3306

# 如果 type 是 NodePort,可以指定 nodePort(范围 30000-32767)
# 如果是 LoadBalancer,则不需要手动指定 nodePort
# nodePort: 30001

# 服务类型:LoadBalancer
# - ClusterIP(默认):仅集群内部访问
# - NodePort:通过节点 IP + 端口从外部访问
# - LoadBalancer:云厂商分配公网 IP(本地环境映射到 localhost)
type: LoadBalancer

不带注释版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# k8s-mysql-pv.yaml
apiVersion: v1
# 这个指的是我k8s有哪些value
kind: PersistentVolume
metadata:
name: my-local-pv
labels:
role: my-local-pv
spec:
storageClassName: suibian
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
hostPath:
path: "/mnt/live"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# k8s-mysql-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
#这个是指mysql要用的东西
name: webook-mysql-claim
labels:
app: webook-mysql-claim
spec:
storageClassName: suibian
accessModes:
- ReadWriteOnce
resources:
requests:
#1GB
storage: 1Gi
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
# k8s-mysql-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: webook-mysql
labels:
app: webook-mysql
spec:
replicas: 1
selector:
matchLabels:
app: webook-mysql
template:
metadata:
name: webook-mysql
labels:
app: webook-mysql
spec:
containers:
- name: webook-mysql
image: mysql:8.0
env:
- name: MYSQL_ROOT_PASSWORD
value: 040725ge
imagePullPolicy: IfNotPresent
volumeMounts:
- mountPath: /var/lib/mysql
#我POD里面有那么多volumes,要用哪个
name: mysql-storage
ports:
- containerPort: 3306
protocol: TCP
restartPolicy: Always
#我整个POD有哪些 就是这个name和上面那个name要对应上
volumes:
- name: mysql-storage
persistentVolumeClaim:
claimName: webook-mysql-claim
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# k8s-mysql-service.yaml
apiVersion: v1
kind: Service
metadata:
name: webook-mysql
spec:
selector:
app: webook-mysql
ports:
- protocol: TCP
#这是咱们外部访问用的端口
port: 11309
targetPort: 3306
#如果下面的type是nodeport,那就得有下面这行nodeport
#nodePort: 30001
type: LoadBalancer

对比:

序号 匹配点 具体位置(文件及字段) 说明
PVC 引用 PV pvc.spec.storageClassName: suibianpv.spec.storageClassName: suibian 必须完全一致,否则 PVC 找不到对应的 PV,会一直 Pending
存储容量匹配 pvc.spec.resources.requests.storage: 1Gipv.spec.capacity.storage: 1Gi PVC 申请的大小不能超过 PV 的容量,否则绑定失败。
访问模式匹配 pvc.spec.accessModes: ReadWriteOncepv.spec.accessModes: ReadWriteOnce 必须完全一致,否则无法绑定。
Deployment 引用 PVC deployment.spec.template.spec.volumes.persistentVolumeClaim.claimName: webook-mysql-claimpvc.metadata.name: webook-mysql-claim 名字必须完全一致,Pod 才能找到对应的 PVC。
Volume 名称对应 deployment.spec.template.spec.containers.volumeMounts.name: mysql-storagedeployment.spec.template.spec.volumes.name: mysql-storage 必须在同一个 Deployment 内部匹配,把容器内的挂载点和 Pod 的存储声明连接起来。
Service 选择 Pod service.spec.selector.app: webook-mysqldeployment.spec.template.metadata.labels.app: webook-mysql 必须匹配,否则 Service 找不到 Pod,流量进不来。
Deployment 选择 Pod deployment.spec.selector.matchLabels.app: webook-mysqldeployment.spec.template.metadata.labels.app: webook-mysql 必须匹配,否则 Deployment 不知道自己该管哪些 Pod。

使用流程(执行顺序)

1
2
3
4
5
6
7
8
9
10
11
12
# 1. 先创建存储资源(PV 和 PVC)
kubectl apply -f k8s-mysql-pv.yaml
kubectl apply -f k8s-mysql-pvc.yaml

# 2. 再部署 MySQL 应用
kubectl apply -f k8s-mysql-deployment.yaml

# 3. 最后暴露服务
kubectl apply -f k8s-mysql-service.yaml

# 4. 查看状态
kubectl get pv, pvc, pods, svc

3.4 验证

1
2
3
4
kubectl apply -f k8s-mysql-deployment.yaml
kubectl apply -f k8s-mysql-pv.yaml
kubectl apply -f k8s-mysql-pvc.yaml
kubectl apply -f k8s-mysql-service.yaml

执行上述命令,而后去ide中连接数据,创建webook数据库并在webook数据库中建表插入数据

1
kubectl delete deployment webook-mysql

而后执行上面这个命令,删除配置,验证是否有持久化

1
kubectl apply -f k8s-mysql-deployment.yaml

再次执行命令之后,去webook数据库看看数据是不是还在,还在的话,那就说明持久化成功了

  1. PV 和 PVC 不需要重复 apply
    • 你每次重建 Deployment 时,PV 和 PVC 如果已经存在且状态正常,是不需要重新 apply 的。直接执行 kubectl apply -f k8s-mysql-deployment.yaml 就够了。
    • 但如果你担心配置有变化,重新 apply 也无害(K8s 会显示 “unchanged”)。
  2. 持久化验证的关键文件
    • 确保你的 PVC 的 accessModes 和 PV 匹配(都是 ReadWriteOnce),否则绑定会失败。
    • 你的 hostPath: /mnt/live 如果是在 WSL2 环境中,确认这个目录存在且 MySQL 有写入权限。

3.5 验证过程中遇到的bug

好的,我把这个Bug的来龙去脉、解决步骤和底层原理,整理成一份结构清晰的“Bug分析报告”,你可以直接复制到笔记里。

Bug 描述(问题是什么?)

现象:
按照标准的 PV、PVC、Deployment 部署了 MySQL,但每次删除 Pod 或重建 Deployment 后,之前写入的数据都会丢失,持久化完全没有生效。

表面症状:

  • kubectl get pvkubectl get pvc 都显示 Bound(绑定成功),状态正常。
  • Pod 能正常启动,kubectl exec 进入 MySQL 也能写入数据。
  • 但是,一旦 kubectl delete pod,新 Pod 启动后,SHOW DATABASES; 发现只剩下默认的系统库,之前建的库和表全没了。

最迷惑的地方:
PV 和 PVC 都显示绑定成功了,看起来一切正常,但数据就是存不住。

怎么解决的?(修复操作)

只修改了 k8s-mysql-deployment.yaml 文件中的一处配置:

修改前(错误):

1
2
volumeMounts:
- mountPath: /mysql # ❌ 随便起的一个路径

修改后(正确):

1
2
volumeMounts:
- mountPath: /var/lib/mysql # ✅ MySQL 真正的数据目录

改完之后,重新 kubectl apply -f k8s-mysql-deployment.yaml,再删 Pod 重建,数据就完好无损了。

解决的原理(为什么这样就能行?)

要理解这个原理,需要理清三个关键点:

  1. MySQL 容器内部有固定的“数据存放点”
  • MySQL 官方 Docker 镜像在启动时,其进程(mysqld)被写死为:所有数据文件(表、索引、日志等)默认写入容器内的 /var/lib/mysql 目录
  • 无论你挂载什么卷,MySQL 进程本身只认这一个路径,它不可能把数据写到 /mysql/data 里。
  1. 挂载(mountPath)的本质是“目录覆盖”
  • 当你在 Deployment 中设置 mountPath: /mysql 时,K8s 确实把存储卷挂载到了容器的 /mysql 目录。
  • 这意味着:/mysql 里写数据,会进入存储卷(PV);往 /var/lib/mysql 里写数据,依然写在容器的临时存储里。
  • 因为 MySQL 进程只往 /var/lib/mysql 写,所以存储卷(PV)里始终是空的。数据全躺在容器的“草稿纸”上。
  1. Pod 删除时,容器的“草稿纸”会被清空
  • Pod 被删除时,容器内的临时文件系统(/var/lib/mysql 默认就在这上面)会被彻底销毁。
  • 而挂载点(PV)因为对应的是宿主机硬盘(或远程存储),独立于 Pod 生命周期,所以能持久保存。
  • 修复的本质:把存储卷的挂载点(/var/lib/mysql精确对齐到应用程序(MySQL)实际写入数据的目录,让所有写入操作“无缝滑入”持久化存储中。

一句话总结(笔记版)

Bug 根因: mountPath 没有指向应用程序的实际数据目录,导致卷虽然挂载了,但 MySQL 并不往里面写数据。

核心解决原则: 卷挂载的 mountPath 必须等于 容器内应用程序默认的数据存储路径,而非随意指定的路径。

常见中间件数据目录速查表(补充到笔记)

中间件 容器内默认数据目录
MySQL /var/lib/mysql
PostgreSQL /var/lib/postgresql/data
MongoDB /data/db
Redis (持久化) /data
Elasticsearch /usr/share/elasticsearch/data

部署任何带持久化需求的服务时,第一步先去查官方镜像文档,确认“数据写在哪”,再把 mountPath 精准指向那里

3.6 其他各个字段含义

image-20260701180315748

image-20260701180945072

一样的话就是我这个pvc需要的你这个pv可以支持,就是匹配上了

image-20260701181119207

这里的一个是指一个pod,而一个pod里面可以有多个线程

四、K8s 部署 Redis

image-20260701181252132

image-20260701190557341

image-20260701190642666

4.1 redis-deployment.yaml

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
apiVersion: apps/v1
kind: Deployment
metadata:
name: webook-redis
labels:
app: webook-redis
spec:
replicas: 1
selector:
matchLabels:
app: webook-redis
template:
metadata:
name: webook-redis
labels:
app: webook-redis
spec:
containers:
- name: webook-redis
image: redis:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 6379
protocol: TCP
restartPolicy: Always

Redis 结构和 MySQL 类似——单实例 + ClusterIP。如果需要持久化也可以挂 Volume。

4.2 redis-service.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Service
metadata:
name: webook-redis
spec:
selector:
app: webook-redis
ports:
- protocol: TCP
port: 11479
targetPort: 6379
#nodePort: 30003
type: LoadBalancer
#type: NodePort

4.3 验证

输入以下命令得到以下结果就算成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
darling123456 MINGW64 /e/go_learning/webook_project/webook (week3)
$ kubectl apply -f k8s-redis-deployment.yaml
deployment.apps/webook-redis configured

darling123456 MINGW64 /e/go_learning/webook_project/webook (week3)
$ kubectl apply -f k8s-redis-service.yaml
service/webook-redis unchanged

darling123456 MINGW64 /e/go_learning/webook_project/webook (week3)
$ redis-cli -h localhost -p 11479
set b c
OK
get b
c

4.4 port、nodeport、targetport

image-20260701190747788

image-20260701191349809

portnodePorttargetPort 的区别

端口类型 是谁的端口 在哪里用 Redis 配置值
port Service 自己的端口 K8s 集群内部访问时使用(比如一个 Pod 访问另一个 Service) 11479
nodePort K8s 节点(虚拟机/宿主机)的端口 K8s 集群外部访问时使用(比如你电脑上的 redis-cli 30003
targetPort Pod 内容器监听的端口 Service 把流量转给 Pod 时,发到 Pod 的这个端口 6379

流量走向图解(你图里的意思)

1
2
3
4
5
6
7
8
9
10
11
12
13
外部客户端(redis-cli)


访问 nodePort: 30003(或 LoadBalancer 的 port: 11479)


Service(webook-redis)收到请求


通过 targetPort: 6379 转发到 Pod


Redis 容器监听 6379,处理请求

4.5 NodePort和 LoadBalancer

1. NodePortLoadBalancer 的区别

这是 Service 的两种暴露类型,区别在于“外部怎么访问”。

对比维度 NodePort LoadBalancer
访问地址 http://<节点IP>:<nodePort> 云厂商分配一个公网 IP(或 Docker Desktop 映射到 localhost)
访问路径 你的电脑 → K8s 节点 IP + 端口 → Service → Pod 你的电脑 → 负载均衡器 IP → Service → Pod
在 Docker Desktop + WSL2 中的表现 ❌ 不自动映射到 localhost,连不上 ✅ Docker Desktop 把端口自动映射到 localhost,能直接访问
适用场景 本地调试(有公网/内网 IP 的情况)、小型集群 云服务生产环境、Docker Desktop 本地开发
端口范围 固定范围 30000-32767(K8s 限制) 端口由你指定(如 6379、11479)

2. 为什么在windows下把nodeport改 LoadBalancer 就成功了?

NodePort 方式
redis-cli -h localhost -p 30003 → 请求到达 Windows 宿主机 → Windows 防火墙 + WSL2 网络隔离 → 请求被挡住,到不了 WSL2 虚拟机内的 NodePort。

LoadBalancer 方式(Docker Desktop 特殊支持)
redis-cli -h localhost -p 11479 → 请求到达 Windows 宿主机 → Docker Desktop 的负载均衡器在 Windows 的 127.0.0.1:11479 上监听 → 直接接收请求 → 内部转发给 K8s Service → 到达 Pod。

本质:LoadBalancer 在 Docker Desktop 中绕过了 WSL2 的网络隔离,直接在 Windows 宿主机上开了“门”。

port 是 Service 对内的门牌号,targetPort 是 Pod 的门牌号,nodePort 是外部门牌号但受防火墙限制。
在 Docker Desktop 下,LoadBalancer 相当于把门直接开在 Windows 的 localhost 上,所以能直接访问。NodePort 的门开在 WSL2 虚拟机里,Windows 过不去。


五、K8s 部署 nginx

image-20260701192220299

5.1 为什么需要 Ingress?

image-20260701192158670

Service(LoadBalancer)是四层负载均衡。所谓”四层”,就是它只工作在传输层,能看懂 IP 地址和端口号,但看不懂 HTTP 请求的具体内容(比如路径 /api/hello)。所以它转发流量时是”盲转”——来了什么就转什么,不做路由判断。这就导致一个 Service 只能对应一个端口/IP,如果你有十个服务需要暴露,就得配十个 LoadBalancer,每个占用一个端口或公网 IP,浪费资源且难以管理。

Ingress 是七层路由。”七层”指的是应用层,它能看懂 HTTP 请求里的域名(live.webook.com)和路径(/api/admin),并据此做智能路由。因此多个 Service 可以共用一个统一入口,由 Ingress 根据请求内容分发给不同的后端。

1
2
3
4
用户 → Ingress(一个入口)
├── /api/* → webook-api-service
├── /admin/* → webook-admin-service
└── / → webook-frontend-service

补充:Ingress 定义的只是路由规则,真正执行转发的是 Ingress Controller( ingress-nginx)。规则 + 执行者,缺一不可。

5.2 Ingress 依赖 Ingress Controller

image-20260701192735458

其实就是 ingress是制定规则(定好路由规则),ingress controller是执行规则(按照这些路由规则去分发数据),比如ingress规定了A发的就要往B发,C发的就要往D走,那么A真的过来了,就是ingress controller根据这个规则把A发的发到B去了

5.3 安装ingress并验证

image-20260701193230859

命令都需要在git bash中执行,并且需要科学上网

1
2
3
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 700 get_helm.sh
./get_helm.sh

据笔者个人尝试,windows下没办法弄第一条命令,只能手动安装

直接打开https://github.com/helm/helm/releases这个网址,下载好之后解压把它放到自己想要的目录,比如我就是放到g/heml下了,然后在环境变量中添加heml.exe所在目录

然后去git bash中输入

1
helm version

得到以下输出代表成功安装

1
version.BuildInfo{Version:"v3.21.2", GitCommit:"125963406833fe0525be91f46c8b5b0f22fb9e32", GitTreeState:"clean", GoVersion:"go1.26.4"}

执行下面的命令

1
2
3
helm upgrade --install ingress-nginx ingress-nginx \
--repo https://kubernetes.github.io/ingress-nginx \
--namespace ingress-nginx --create-namespace

得到以下输出算是成功

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
Release "ingress-nginx" has been upgraded. Happy Helming!
NAME: ingress-nginx
LAST DEPLOYED: Thu Jul 2 10:10:27 2026
NAMESPACE: ingress-nginx
STATUS: deployed
REVISION: 2
TEST SUITE: None
NOTES:
The ingress-nginx controller has been installed.
It may take a few minutes for the load balancer IP to be available.
You can watch the status by running 'kubectl get service --namespace ingress-nginx ingress-nginx-controller --output wide --watch'

An example Ingress that makes use of the controller:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example
namespace: foo
spec:
ingressClassName: nginx
rules:
- host: www.example.com
http:
paths:
- pathType: Prefix
backend:
service:
name: exampleService
port:
number: 80
path: /
# This section is only required if TLS is to be enabled for the Ingress
tls:
- hosts:
- www.example.com
secretName: example-tls

If TLS is enabled for the Ingress, a Secret containing the certificate and key must also be provided:

apiVersion: v1
kind: Secret
metadata:
name: example-tls
namespace: foo
data:
tls.crt: <base64 encoded cert>
tls.key: <base64 encoded key>
type: kubernetes.io/tls

可以通过如下命令验证

1
2
3
4
5
6
7
8
9
10
11
#看 Pod 的状态(最重要)
darling123456 MINGW64 /e/go_learning/webook_project/webook (week3)
$ kubectl get pods -n ingress-nginx -w
NAME READY STATUS RESTARTS AGE
ingress-nginx-controller-5cd9869bf8-qb7kz 1/1 Running 0 59s
# 看 Service 是否分配了外部 IP
darling123456 MINGW64 /e/go_learning/webook_project/webook (week3)
$ kubectl get svc -n ingress-nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ingress-nginx-controller LoadBalancer 10.96.12.6 <pending> 80:31668/TCP,443:30342/TCP 83s
ingress-nginx-controller-admission ClusterIP 10.96.103.174 <none> 443/TCP 83s

看 Pod 的状态

1
kubectl get pods -n ingress-nginx -w
  • 耐心看几行,如果状态最终变为 Running(且 READY 变成 1/1),说明容器跑起来了。
  • 如果长时间 PendingContainerCreating,说明在拉取镜像,多等一会儿。

看 Service 是否分配了外部 IP

1
kubectl get svc -n ingress-nginx

会看到一个叫 ingress-nginx-controller 的 Service,类型是 LoadBalancer。它的 EXTERNAL-IP 列:

  • 一开始显示 <pending>,这是正常的。
  • 等一两分钟后,如果变成 localhost127.0.0.1说明大功告成了,你现在可以通过 localhost 访问它来做路由转发。

ingress-nginx

ingress-nginx 这个控制器里,主要包含 两大组件:一个控制器(Controller)Nginx 本身。也就是说ingress-nginx和nginx是包含关系

我们可以把它理解为一个紧密协作的“大脑”和“执行者”的组合:

  • 控制器 (Controller):这是整个控制器的“大脑”。它是一个用 Go 语言编写的程序,职责是与 Kubernetes API 服务器通信,实时监控集群中 IngressService 等资源的变化。一旦发现你创建或修改了 Ingress 规则,它就会立刻生成一份新的 Nginx 配置文件(nginx.conf)。
  • Nginx:这是实际处理流量的“执行者”。它是一个高性能的 Web 服务器和反向代理。它的工作就是根据控制器生成的 nginx.conf 配置文件,接收外部请求并将其转发到你定义的后端服务上。

它们俩就像一对配合默契的搭档,共同在同一个 Pod 里工作:

  1. 感知变化:控制器(大脑)时刻盯着 Kubernetes 的 API 服务器。
  2. 生成配置:当你创建或更新 Ingress 规则时,大脑立刻行动,根据规则生成一份新的 Nginx 配置文件。
  3. 更新执行:大脑将新配置传递给 Nginx(执行者),并让它优雅地重新加载配置。这个过程保证了服务不中断。
  4. 处理请求:从此以后,所有进入集群的外部请求,都会由这个时刻准备着的 Nginx 执行者,按照最新配置进行路由和转发。

5.4 ingress.yaml

image-20260702101920710

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: webook-ingress
spec:
ingressClassName: nginx
rules:
# host是live.webook.com的时候 命中这条
- host: live.webook.com
http:
paths:
- backend:
service:
# 与ks8-webook-service.yaml里面的名称和端口对应
# 请求路径前缀是/的时候
#将流量转发过去后面的webook服务上
#端口是80
name: webook
port:
number: 80
pathType: Prefix
path: /

注意service部分的name和port要和ks8-webook-service.yaml里面的对应上

5.5 验证

1.配置hosts文件

  1. Win + S,搜索 “记事本”

  2. 右键点击“记事本”,选择 “以管理员身份运行”

  3. 在记事本里,点击 文件 → 打开

  4. 导航到 C:\Windows\System32\drivers\etc\ 目录。

  5. 右下角文件类型从 .txt 改成 “所有文件”

  6. 选择 hosts 文件,点击打开。

  7. 在文件末尾另起一行,添加:

    1
    127.0.0.1 live.webook.com
  8. 保存文件(Ctrl + S),然后关闭记事本

加这行 127.0.0.1 live.webook.com,本质上是在“本地 DNS 解析”这一步做了个手脚,绕过了公网 DNS 服务器。因为 live.webook.com 这个域名是虚构的,并没有在阿里云、Cloudflare 等公网 DNS 服务商那里注册过

如果不加这一行,浏览器访问 http://live.webook.com 时会发生:

  1. 浏览器问 DNS 服务器:“live.webook.com 这个网站的 IP 是多少?”
  2. DNS 服务器在全球查了一圈,发现:这个域名不存在,没注册过
  3. 浏览器找不到这个网站! 直接报 ERR_NAME_NOT_RESOLVED连电脑的网卡都出不去

结果就是:根本碰不到 ingress-nginx,更别提什么路由规则了。

加了 127.0.0.1 live.webook.com 之后发生了什么?

  1. 浏览器问 DNS 之前,先查本地的 hosts 文件。它发现:live.webook.com 对应 127.0.0.1
  2. 浏览器直接告诉你:“这网站就在你自己电脑上(127.0.0.1)。”
  3. 请求直接打到本机的 80 端口,正好被 ingress-nginx Controller 接住(它已经在本地 80 端口监听了)。
  4. ingress-nginx 收到请求,看到请求头里的 Host: live.webook.com,和你写的 Ingress 规则里的 host: live.webook.com 匹配上了,于是按规则转发。

2. 访问对应网站出现以下结果说明配置成功

image-20260702104928485

image-20260702104949575

1
2
3
4
5
6
7
8
9
10
11
浏览器访问 http://live.webook.com/hello

Ingress Controller(ingress-nginx)收到请求

检查请求的 Host 头:live.webook.com ✅ 匹配规则

根据 backend 配置,把请求转发给 Service: webook (port 80)

Service webook 负载均衡到 Pod (targetPort 9090)

Go 程序返回 "你好,你来了"

六、集成mysql和redis

image-20260702111342598

1.修改代码

把之前注释的代码都打开

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
//初始化DB
db := initDB()
//初始化Server
server := initWebServer()

//初始化User
u := initUser(db)
//注册路由
u.RegisterRoutes(server)
server.Run(":9090")
}
1
2
3
4
//main.go的initDB函数的的下面这行
db, err := gorm.Open(mysql.Open("root:040725ge@tcp(localhost:13316)/webook"))
//改成下面这行,要记得localhost对应webook-mysql,端口号是11309,这个是k8s-mysql-service里面对应的
db, err := gorm.Open(mysql.Open("root:040725ge@tcp(webook-mysql:11309)/webook"))
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
//同理对redis进行修改 第5行和第21行,记得都要改,只改一个不行
func initWebServer() *gin.Engine {
server := gin.Default()
redisClient := rateLimitredis.NewClient(&rateLimitredis.Options{
Addr: "webook-redis:11479",
})
/*没有部署k8s之前的
redisClient := rateLimitredis.NewClient(&rateLimitredis.Options{
Addr: "localhost:6379",
})*/
server.Use(ratelimit.NewBuilder(redisClient, time.Second, 100).Build())
server.Use(cors.New(cors.Config{
AllowOrigins: []string{"http://localhost:3000"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
AllowCredentials: true,
ExposeHeaders: []string{"x-jwt-token"},
MaxAge: 12 * time.Hour,
}))
store, err := redis.NewStore(16, "tcp",
"webook-redis:11479", "root", "",
[]byte("qOYZLAuWmwkxAKG6bijwru9ghNNS9rHc"),
[]byte("8Mv11Olt6x3DX97rUE1exp9XISEMSZJl"))
if err != nil {
panic(err)
}
server.Use(sessions.Sessions("mysession", store))
server.Use(middleware.NewLoginJWTMiddlewareBuilder().
IgnorePaths("/users/signup").
IgnorePaths("/users/login").Build())
return server
}

2.验证

1
2
3
4
5
make docker
#删掉旧的镜像
kubectl delete deployment webook
#用新的
kubectl apply -f k8s-webook-deployment.yaml

如果你发现下面两个不能访问,但是live.webook.com/hello还可以访问,那只能说明还是旧的镜像

image-20260702124402342

image-20260702124415296

3.小技巧

image-20260702132855819

image-20260702132908402

image-20260702132010107

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//dev.go
//go:build !k8s

//本地连接 不使用k8s这个编译标签

package config

var Config = config{
DB: DBConfig{
DSN: "localhost:11316",
},
Redis: RedisConfig{
Addr: "localhost:6379",
},
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//k8s.go
//go:build k8s

//使用k8s这个编译标签

package config

var Config = config{
DB: DBConfig{
DSN: "root:040725ge@tcp(webook-mysql:11309)/webook",
},
Redis: RedisConfig{
Addr: "webook-redis:11479",
},
}

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

type config struct {
DB DBConfig
Redis RedisConfig
}

type DBConfig struct {
DSN string
}

type RedisConfig struct {
Addr string
}

1
2
3
4
5
6
7
8
9
10
11
//main.go 中的修改 一共修改下面三个地方,替换成上面配置文件中的东西即可
func initDB() *gorm.DB {
db, err := gorm.Open(mysql.Open(config.Config.DB.DSN))

redisClient := rateLimitredis.NewClient(&rateLimitredis.Options{
Addr: config.Config.Redis.Addr,
})
store, err := redis.NewStore(16, "tcp",
config.Config.Redis.Addr, "root", "",
[]byte("qOYZLAuWmwkxAKG6bijwru9ghNNS9rHc"),
[]byte("8Mv11Olt6x3DX97rUE1exp9XISEMSZJl"))
1
2
//makefile修改 下面这行加入tags参数
@CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -tags=k8s -o webook .

之后还是执行这三条命令,然后去postman访问那两个页面看看是不是成功了

1
2
3
make docker
kubectl delete deployment webook
kubectl apply -f k8s-webook-deployment.yaml

4.在浏览器中访问

image-20260702132919523

1
2
3
4
5
6
const instance = axios.create({
// 这边记得修改你对应的配置文件
baseURL: "http://localhost:80",
//baseURL: "http://localhost:9090",
withCredentials: true
})

把9090换成80后,去webook-fe目录下输入npm run dev然后就可以正常访问登录注册页面和功能了

最后去mysql-k8s数据库中查一下是不是已经有了具体的数据,有了就代表全都通顺了

注:

浏览器直接访问 live.webook.com/users/signup

浏览器地址栏发的是 GET 请求,但后端 user.go 里注册的是:

1
server.POST("/users/signup", u.SignUp)   // ← 只有 POST

Gin 找不到 GET /users/signup,返回 404

而通过前端 localhost:3000/users/signup 注册登录

前端的 axios.ts 里:

1
baseURL: "http://localhost:80",   // ← 直接连 K8s LoadBalancer Service

表单提交时 axios 发的是 POST 请求,路由匹配 POST /users/signup,一切正常。而且前端走的是 localhost:80(Docker Desktop 直接把 LoadBalancer 映射到了本地),根本没经过 Ingress

总结

live.webook.com/users/signup localhost:3000 前端
请求方式 GET(浏览器地址栏) POST(axios)
后端匹配 ❌ 无 GET 路由 → 404 POST /users/signup → 成功
流量路径 Ingress → Service 直接 LoadBalancer:80

**live.webook.com 是后端 API,不是给人”看”的。要使用它只能用 Postman(发 POST)或者让 axios 的 baseURL 指向它。**浏览器直接访问它没有意义——就好比你用浏览器访问 https://api.github.com/users 能看到 JSON,但你访问 live.webook.com 没有根路由就什么都没有。

七、作业

image-20260702133726017