K8s 集群部署中间件 - yaml 版本(二)
K8s 集群部署中间件 - yaml 版本(二)
文章目录
- K8s 集群部署中间件 - yaml 版本(二)
- 1. 部署 mysql
- 1. ConfigMap
- 2. Secret
- 3. StatefulSet
- 4. Service
- 2. 部署 Nacos
- 1 ConfigMap
- 2. StatefulSet
- 3. Service
- 疑问与总结
- 1. 单独创建 PVC 与 通过 volumeClaimTemplates 创建 PVC 的区别?
- 2. 独立创建 PVC 可以被共享,为什么不适合有状态服务?
- 不同的应用,需要采取不同的部署方式:
deployment:无状态应用部署,比如微服务,提供多副本等功能。statefulSet:有状态应用部署,比如 redis,提供稳定的存储、网络等功能。- 稳定的网络身份:StatefulSet 为每个 Pod 分配了一个持久且唯一的网络标识符,这对于 MySQL 这类需要固定主机名或网络地址以维持主从复制关系的数据库服务至关重要。
- 持久化存储:StatefulSet 易于与持久化存储卷结合使用,确保数据库数据的持久保存,即便是在 Pod 重启或重新调度后。
- 适合有状态应用:StatefulSet 是为有状态应用设计的,如数据库和消息队列,它提供了必要的支持来维护这些应用的状态。
DaemonSet:守护型应用部署,比如日志收集组件,在每个机器都运行一份。Job/CronJob:定时任务部署,比如垃圾清理组件,可以在指定时间运行。
1. 部署 mysql
- k8s 部署 mysql 我们需要编写的文件以4下个:
ConfigMap:用于存储 mysql 相关配置。kind:ConfigMap。Secret:用于存储 mysql 用户密码。kind:Secret。StatefulSet:构建 mysql 服务。kind:StatefulSet。Service:向集群内提供 mysql 服务访问入口。由于我们需要使用工具连接该 mysql,所以除了向集群内提供服务,还要向集群外提供连接服务。- 向 k8s 集群内部提供服务的
service,kind:service,ClusterIP:None,type: ClusterIP(默认值,可省略)。提供 DNS 解析与服务发现。让集群内部组件能直接发现所有 MySQL 节点,k8s 会为无头服务自动创建 DNS 记录(格式:服务名.命名空间.svc.cluster.local),当其他 Pod(如应用服务、MySQL 从节点)解析该 DNS 时,会返回 所有匹配该服务标签选择器的 MySQL Pod 的真实 IP 列表(而非单一虚拟 IP)。 向集群外提供连接服务的 service,kind:service,type:NodePort。
- 向 k8s 集群内部提供服务的
- Ingress:向集群外提供服务访问入口。Ingress 具体分为:(mysql 不配置 ingerss)
- 通常不建议给 MySQL 这类数据库服务配置 Ingress,核心原因是 Ingress 本质是为 HTTP/HTTPS 流量设计的 “路由网关”,而 MySQL 基于 TCP 协议通信,两者适配性差,且存在安全和性能隐患。由于我需要通过工具连接 mysql ,所以上面用的 type: NodePort。无需给 mysql 提供 ingress 向外提供服务。该模式在集群节点开放一个高位端口(30000-32767),仅允许特定 IP(如运维电脑、跳板机)通过 “节点 IP:NodePort” 访问,配合防火墙限制 IP 白名单。
Ingress:是 Kubernetes 中的一种资源对象,它定义了外部访问集群内服务的规则。可以将其理解为一个智能的 “流量路由器”,根据接收到的 HTTP/HTTPS 请求的不同规则,将流量转发到集群内不同的服务上。kind:Ingress。Ingress-Controller:是实际负责执行 Ingress 资源中定义的路由规则的组件。它是一个运行在 Kubernetes 集群中的服务,通常以 Pod 的形式存在,不断监听 Ingress 资源的变化,并根据最新的规则来配置和更新负载均衡器或代理服务器。kind:Service。
1. ConfigMap
-
我们在启动 mysql 服务时(通过容器启动),通常需要创建 my.cnf 文件,根据我们的需求设置一些参数,现在将这些配置抽取到 ConfigMap。
-
my.cnf 文件:
[client] #设置客户端默认字符集utf8mb4 default-character-set=utf8mb4 [mysql] #设置服务器默认字符集为utf8mb4 default-character-set=utf8mb4 [mysqld] #配置服务器的服务号,具备日后需要集群做准备 server-id = 1 #开启MySQL数据库的二进制日志,用于记录用户对数据库的操作SQL语句,具备日后需要集群做准备 log-bin=mysql-bin #设置清理超过30天的日志,以免日志堆积造过多成服务器内存爆满。2592000秒等于30天的秒数 binlog_expire_logs_seconds = 2592000 #解决MySQL8.0版本GROUP BY问题 sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION' #允许最大的连接数 max_connections=1000 # 禁用符号链接以防止各种安全风险 symbolic-links=0 # 设置东八区时区 default-time_zone = '+8:00' -
抽取成 ConfigMap:
mysql-configMap.yamlapiVersion: v1 kind: ConfigMap metadata:# ConfigMap 名称,用于挂载引用name: mysql-config # 与 MySQL 部署同命名空间namespace: mortal-system data:# 键为配置文件名,值为完整的配置内容(包含 client、mysql、mysqld 三个部分)my.cnf: |[client]# 设置客户端默认字符集为 utf8mb4(支持 emoji 等特殊字符)default-character-set=utf8mb4[mysql]# 设置 MySQL 命令行客户端默认字符集为 utf8mb4default-character-set=utf8mb4[mysqld]# 配置服务器的服务号,为日后集群做准备server-id = 1# 开启 MySQL 数据库的二进制日志,记录用户操作 SQL 语句,为日后集群做准备log-bin=mysql-bin# 设置清理超过 30 天的日志,避免日志堆积导致服务器内存爆满(2592000 秒 = 30 天)binlog_expire_logs_seconds = 2592000# 解决 MySQL8.0 版本 GROUP BY 问题sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'# 允许最大的连接数max_connections=1000# 禁用符号链接以防止各种安全风险symbolic-links=0# 设置东八区时区default-time_zone = '+8:00'# 服务器端默认字符集(与客户端保持一致,避免乱码)character-set-server=utf8mb4# 数据库字符集对应的排序规则collation-server=utf8mb4_unicode_ci -
创建 ConfigMap 资源:
kubectl apply -f mysql-configMap.yaml -
查看:

2. Secret
-
我们创建一个 Secret 用来存储 MySQL root 用户的密码:mysql-secret.yaml
apiVersion: v1 kind: Secret metadata:# Secret 名称,用于部署时引用name: mysql-secret# 与 MySQL 部署在同一命名空间namespace: mortal-system # 通用类型,用于存储任意键值对 type: Opaque data:# root 用户密码(需要 base64 编码)# 生成方式:echo -n "你的root密码" | base64# 示例:明文为 "root",编码后为 cm9vdA==root-password: cm9vdA== -
创建 Secret 资源:mysql-secret.yaml
kubectl apply -f mysql-secret.yaml -
查看创建的服务:

-
查看解码后的密码:

3. StatefulSet
-
部署mysql 服务:mysql-k8s.yaml,采用 NFS 进行存储。
# 持久化存储声明模板(PVC Template):为每个副本自动创建独立存储 apiVersion: apps/v1 kind: StatefulSet metadata:name: mysqlnamespace: mortal-system spec:serviceName: mysql-headless # 关联 service 上的服务名称,要一致# 单副本(扩展集群时修改此处)replicas: 1selector:matchLabels:app: mysqltemplate:metadata:labels:app: mysqlspec:imagePullSecrets: - name: harbor-credscontainers:- name: mysql# 替换为你的 MySQL 镜像image: mortal.harbor.com/mortal-system/mysql:8.3.0ports:- containerPort: 3306env:- name: MYSQL_ROOT_PASSWORDvalueFrom:secretKeyRef:name: mysql-secretkey: root-password- name: TZvalue: "Asia/Shanghai"# 配置文件挂载(ConfigMap)volumeMounts:- name: mysql-configmountPath: /etc/mysql/conf.d/my.cnfsubPath: my.cnf# 数据存储卷(与下面的 volumeClaimTemplates 对应)- name: mysql-data mountPath: /var/lib/mysql# 健康检查livenessProbe:exec:command: ["mysqladmin", "ping", "-uroot", "-p$(MYSQL_ROOT_PASSWORD)"]initialDelaySeconds: 30periodSeconds: 10readinessProbe:exec:command: ["mysqladmin", "ping", "-uroot", "-p$(MYSQL_ROOT_PASSWORD)"]initialDelaySeconds: 5periodSeconds: 5volumes:# 配置文件卷(引用 ConfigMap)- name: mysql-configconfigMap:name: mysql-config# 存储声明模板:为每个副本自动创建 PVC(名称格式:mysql-data-<statefulset-name>-<ordinal>)volumeClaimTemplates:- metadata:name: mysql-dataspec:accessModes: [ "ReadWriteOnce" ]resources:requests:storage: 10Gi# 使用我们创建的sc nfs-scstorageClassName: "nfs-sc" -
部署并查看启动情况:
kubectl apply -f mysql-k8s.yaml kubectl get pod -A
4. Service
-
部署 Service:mysql-headless.yaml
apiVersion: v1 kind: Service metadata:# 服务名称,集群内可通过此名称访问name: mysql-headless# 与 MySQL 实例同命名空间namespace: mortal-system spec:clusterIP: None # 无头服务核心标识selector:# 匹配标签为 app=mysql 的 Pod(与 StatefulSet 保持一致)app: mysqlports:# 集群内访问端口(其他服务通过此端口连接)- port: 3306# 指向 Pod 内部的 MySQL 端口(容器暴露的端口)targetPort: 3306# 仅集群内部可访问(默认类型,可省略)type: ClusterIP--- # 外部访问 Service(NodePort 供应用连接,如 Navicat) apiVersion: v1 kind: Service metadata:name: mysql-servicenamespace: mortal-system spec:type: NodePortports:- port: 3306targetPort: 3306nodePort: 30306selector:app: mysql -
部署并查看启动情况:
kubectl apply -f mysql-headless.yaml kubectl get service -n mortal-system
-
测试连接:

2. 部署 Nacos
- k8s 部署 nacos 我们需要编写的文件以4下个:我们采用是 mysql 作为 nacos 的存储。
ConfigMap:用于存储 nacos 连接 mysql 相关配置。kind:ConfigMap。StatefulSet:构建 nacos 服务。kind:StatefulSet。Service:向集群内提供 nacos 服务访问入口。由于我们需要连接该 nacos 控制台,所以除了向集群内提供服务,还要向集群外提供连接服务。- 向 k8s 集群内部提供服务的
service,kind:service,ClusterIP:None,type: ClusterIP(默认值,可省略)。提供 DNS 解析与服务发现。为 Nacos 集群节点提供固定 DNS 解析(nacos-0.xxx),支持集群内部 Leader 选举、数据同步。 向集群外提供连接服务的 service,kind:service,type:NodePort。把 Nacos 控制台 / API 暴露到 K8s 集群外部,提供负载均衡(外部请求转发到任意 Nacos 节点)。
- 向 k8s 集群内部提供服务的
1 ConfigMap
-
部署 Nacos 的 ConfigMap ,配置 Nacos 连接 mysql 进行存储的一些配置,以及 Nacos 的一些安全配置。
apiVersion: v1 kind: ConfigMap metadata:name: nacos-confignamespace: mortal-system data:application.properties: |spring.datasource.platform=mysqldb.num=1# 替换为你的 MySQL 服务名、数据库名、用户名、密码db.url.0=jdbc:mysql://mysql-headless:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTCdb.user=rootdb.password=root# 生产环境建议改为 true 并配置密钥nacos.core.auth.enabled=falsenacos.server.ip=0.0.0.0nacos.naming.data.warmup=true# 禁用单节点模式(集群必须设为 false)nacos.core.singleton=false# 指定 raft 协议数据目录(需持久化)nacos.raft.dataDir=/home/nacos/data/raft# 显式指定集群名称nacos.naming.cluster.name=mortal-cluster# 显式指定集群通信端口nacos.server.rpc.port=9849nacos.inetutils.prefer-hostname-over-ip=true# 显式指定集群内节点的通信地址(域名形式)nacos.naming.serverAddr=nacos-0.nacos-headless.mortal-system.svc.cluster.local:8848,nacos-1.nacos-headless.mortal-system.svc.cluster.local:8848,nacos-2.nacos-headless.mortal-system.svc.cluster.local:8848- 还需要对 Nacos 数据库进行初始化,创建 12 张表,根据ConfigMap配置,数据库名为 nacos_config。具体 sql 之前 的文章有写到,或者查看官方文档。
2. StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:name: nacosnamespace: mortal-system
spec:serviceName: nacos-headlessreplicas: 3selector:matchLabels:app: nacostemplate:metadata:labels:app: nacosspec:containers:- name: nacosimage: mortal.harbor.com/mortal/nacos:2.1.1ports:- containerPort: 8848name: http- containerPort: 9848name: client-rpc- containerPort: 9849name: server-rpcenv:- name: NACOS_REPLICASvalue: "3"- name: NACOS_SERVERSvalue: "nacos-0.nacos-headless.mortal-system.svc.cluster.local:8848 nacos-1.nacos-headless.mortal-system.svc.cluster.local:8848 nacos-2.nacos-headless.mortal-system.svc.cluster.local:8848"- name: POD_NAMESPACEvalueFrom:fieldRef:fieldPath: metadata.namespace- name: POD_NAMEvalueFrom:fieldRef:fieldPath: metadata.name- name: NACOS_SERVER_PORTvalue: "8848"volumeMounts:- name: nacos-configmountPath: /home/nacos/conf/application.propertiessubPath: application.properties- name: nacos-datamountPath: /home/nacos/data# 挂载 ConfigMap 配置volumes:- name: nacos-configconfigMap:name: nacos-config # 与前面 ConfigMap 名称一致# PVC 模板:自动创建 3 个 PVC,关联 nfs-sc 存储类volumeClaimTemplates:- metadata:name: nacos-data # 与上面 volumeMounts.name 对应spec:accessModes: [ "ReadWriteMany" ] # NFS 支持多节点读写(适配集群)storageClassName: "nfs-sc" # 关键:指定已存在的 NFS 存储类名resources:requests:storage: 5Gi # 每个节点请求 10Gi 存储(3节点共 30Gi,SC 需支持)
-
NACOS_SERVERS:为必填项,供集群选举、数据同步使用。如果替换成 IP 需要替换成 物理机IP (不建议替换成 IP)。建议使用如上的 保留无头服务域名:nacos-0.nacos-headless.mortal-system.svc.cluster.local:8848。- nacos-0:StatefulSet 实例的名称(固定格式:StatefulSet名称-序号)。
- nacos-headless:绑定的无头服务(Headless Service)名称(你前面定义的 Service 名)。
- mortal-system:命名空间(Nacos 部署所在的 K8s 命名空间)。
- svc:K8s 服务的 DNS 子域(所有 Service 都会注册到这个子域下)。
- cluster.local:K8s 集群的默认域名(可通过集群配置修改,默认就是这个)。
- :8848:Nacos 容器的通信端口(和 StatefulSet 中定义的 containerPort: 8848 对应)。
-
为什么要这么写?核心依据是 K8s 的两个核心特性:
- StatefulSet 的「固定实例命名」特性。普通 Deployment 部署的 Pod 名称是随机的(比如 nacos-7f98d765c4-2xqzk),重启后名称和 IP 都会变;而 StatefulSet 会给每个实例分配 固定的名称和序号:
- 当你设置 replicas: 3 时,K8s 会自动创建 3 个 Pod,名称固定为 nacos-0、nacos-1、nacos-2(序号从 0 开始);
- 即使 Pod 重启、重建(比如节点故障迁移),实例名称依然是 nacos-0,不会变。
- 无头服务(Headless Service)的「DNS 解析」特性:你前面定义的 nacos-headless 是「无头服务」(clusterIP: None),它和普通 Service 的区别是:
- 普通 Service 会分配一个集群 IP,通过负载均衡转发请求到 Pod;
- 无头服务 不分配集群 IP,而是直接在 K8s DNS 中为每个关联的 StatefulSet Pod 注册一条 DNS 记录:{Pod名称}.{无头服务名}.{命名空间}.svc.cluster.local;
- 这条 DNS 记录会自动解析到对应 Pod 的 实际 IP(不管 Pod 重启后 IP 怎么变,DNS 都会动态更新解析结果)。
- StatefulSet 的「固定实例命名」特性。普通 Deployment 部署的 Pod 名称是随机的(比如 nacos-7f98d765c4-2xqzk),重启后名称和 IP 都会变;而 StatefulSet 会给每个实例分配 固定的名称和序号:
3. Service
# 无头服务(供 Nacos 集群内部通信,固定 DNS 解析)
apiVersion: v1
kind: Service
metadata:name: nacos-headlessnamespace: mortal-system
spec:clusterIP: None # 无头服务核心标识(无集群 IP,生成 Pod 独立 DNS)ports:- port: 8848targetPort: 8848name: http # 控制台/API 端口- port: 9848targetPort: 9848name: client-rpc # 客户端通信端口- port: 9849targetPort: 9849name: server-rpc # 集群节点通信端口selector:app: nacos---
# 供外部访问
apiVersion: v1
kind: Service
metadata:name: nacos-servicenamespace: mortal-system
spec:type: NodePort # 暴露到 K8s 节点 IP,外部可访问ports:- port: 8848targetPort: 8848nodePort: 30848 # 外部访问端口(范围 30000-32767,可自定义)selector:app: nacos # 关联 Nacos Pod
-
部署效果图如下:


-
Naocs控制台集群节点状态:

-
微服务成功注册进 Nacos:

疑问与总结
1. 单独创建 PVC 与 通过 volumeClaimTemplates 创建 PVC 的区别?
- 创建 PVC 有两种 方式:PVC 只会和 一个 PV 进行绑定。
- 使用 kind:PersistentVolumeClaim 来单独创建 PVC。
-
与 pod / Deployment 等通过 vloumes.persistenVolumeClaim.claimName:[PVC name] 进行关联。
-
该 PVC 是独立的 k8s 资源,其生命周期与使用它的 pod / Deployment 等解耦。即时 pod 被删除,PVC 与其绑定的 PV 仍会保留,除非手动删除。
-
当单独创建 PVC 设置 accessModes:ReadWriteMany, 和单独创建 PV 设置 accessModes:ReadWriteMany 进行绑定时,创建的资源可以被多个 pod 共享。注意:如果 accessModes 不一致则PVC 会一直处于 Pending 状态,无法与 PV 绑定。或者通过单独创建 PVC 设置 accessModes:ReadWriteMany 和 通过 StorageClass 自动创建 PV,也可实现多个 pod 共享。
访问模式 缩写 含义 适用场景 ReadWriteMany RWX 多 Pod 可同时读写 共享静态资源(如前端代码)、日志收集 ReadWriteOnce RWO 仅一个节点上的 Pod 可读写(单节点独占) 数据库、有状态服务(独立存储) ReadOnlyMany ROX 多 Pod 可同时只读 共享只读数据(如配置文件、镜像) - 适合无状态服务。
-
- 使用 kind:PersistentVolumeClaim 来单独创建 PVC。
- 部署服务时,如kind:StatefulSet,通过 volumeClaimTemplates 来创建 PVC。
- 通过 volumeClaimTemplates 定义 “存储模板”,Kubernetes 会自动为每个 StatefulSet 副本创建对应的 PVC 和 PV。
- PVC 的生命周期与 StatefulSet 的副本强绑定。当 StatefulSet 扩容时,会基于模板创建新的 PVC 和 PV。当缩容时,对应的 PV 和 PVC 不会自动删除(默认保留,防止数据丢失)。当 StatefulSet 被删除时, PVC 和 PV 也会保留,需手动清除。
- 适用于有状态服务,如数据库,分布式中间件(Nacos等),需要每个实例有独立且持久的存储。
2. 独立创建 PVC 可以被共享,为什么不适合有状态服务?
- 资源数据可以被共享,不是更能体现数据的一致性吗,为什么却不适合 有状态服务呢?
- 有状态服务的 “状态” 本质:并非简单 “数据一致”,而是 “实例身份与数据的强绑定”,比如说:
- MySQL 主从集群中,主库和从库的数据虽然最终一致,但各自有独立的写入路径、日志(binlog)和缓存,不能直接共享同一份存储(否则主从同步会失效,甚至产生数据冲突)。
- Nacos 集群中,每个节点需要存储本地元数据、raft 协议日志等,这些数据是节点私有的,不能共享(否则集群共识机制会混乱)。
- 共享存储只能保证 “数据文件相同”,但无法解决分布式服务的 “实例独立性”(如节点身份、本地状态、读写冲突等)。
- 如果有状态服务共享的 PVC 和 PV,可能带来的风险有:
- 并发写入冲突:多个实例同时读写同一份数据文件(如数据库表、配置文件),会导致数据损坏(如 MySQL 的 InnoDB 引擎无法在共享存储上安全运行多实例)。
- 锁机制失效:分布式服务的本地锁(如文件锁)在共享存储中可能失效,引发数据一致性问题。
- 而在无状态服务中,实例之间完全对等,没有 “身份” 差异(删除任意实例不影响服务)。不需要持久化本地状态(或状态可通过外部服务恢复,如从 Redis 拉取配置)。所以 独立创建 PVC 更适合 无状态服务。
- 独立创建 PVC 也适合 只读场景(如共享静态资源)。
- 所以有状态服务的 “状态” 不仅是数据本身,还包括实例的身份和本地逻辑,这些必须与独立存储绑定才能正常工作。所以不适合有状态服务。
- 有状态服务的 “状态” 本质:并非简单 “数据一致”,而是 “实例身份与数据的强绑定”,比如说:
其他中间件持续更新中…
