从零到一:在单机K3S上搭建一个耐造的全栈微服务集群

从零到一:在单机K3S上搭建一个耐造的全栈微服务集群

    • [1. 引言:为什么是K3S单机集群?](#1. 引言:为什么是K3S单机集群?)
    • [2. 前置准备:磨刀不误砍柴工](#2. 前置准备:磨刀不误砍柴工)
      • [2.1 操作系统基础优化](#2.1 操作系统基础优化)
      • [✅ 检查点1](#✅ 检查点1)
      • [2.2 防火墙/安全组策略配置](#2.2 防火墙/安全组策略配置)
      • [2.3 用户与用户组规划](#2.3 用户与用户组规划)
      • [2.4 容器运行时选择:containerd还是Docker?](#2.4 容器运行时选择:containerd还是Docker?)
    • [3. K3S安装:单机集群的起点](#3. K3S安装:单机集群的起点)
      • [3.1 资源自适应计算公式](#3.1 资源自适应计算公式)
      • [3.2 在线安装(网络通畅环境)](#3.2 在线安装(网络通畅环境))
      • [3.3 离线安装(内网环境)](#3.3 离线安装(内网环境))
      • [✅ 检查点2](#✅ 检查点2)
    • [4. 存储与网络配置](#4. 存储与网络配置)
      • [4.1 StorageClass:让PVC能找到"家"](#4.1 StorageClass:让PVC能找到“家”)
      • [4.2 本地镜像仓库策略](#4.2 本地镜像仓库策略)
    • [5. 中间件部署:摆好棋盘,放上棋子](#5. 中间件部署:摆好棋盘,放上棋子)
      • [5.1 MySQL:跑在K8S上的数据库](#5.1 MySQL:跑在K8S上的数据库)
      • [5.2 Redis:缓存层](#5.2 Redis:缓存层)
      • [5.3 Kafka:消息队列](#5.3 Kafka:消息队列)
      • [5.4 Elasticsearch:搜索引擎](#5.4 Elasticsearch:搜索引擎)
      • [5.5 Nacos:注册中心/配置中心](#5.5 Nacos:注册中心/配置中心)
      • [5.6 MinIO:对象存储](#5.6 MinIO:对象存储)
      • [5.7 Nginx:前端网关/反向代理](#5.7 Nginx:前端网关/反向代理)
      • [✅ 检查点3](#✅ 检查点3)
    • [6. 业务微服务部署:让应用跑起来](#6. 业务微服务部署:让应用跑起来)
      • [6.1 统一部署模板与环境变量注入](#6.1 统一部署模板与环境变量注入)
      • [6.2 Secrets管理](#6.2 Secrets管理)
      • [6.3 批量部署](#6.3 批量部署)
    • [7. 验证与测试:链路灯亮没亮?](#7. 验证与测试:链路灯亮没亮?)
      • [7.1 检查所有Pod状态](#7.1 检查所有Pod状态)
      • [7.2 核心链路测试](#7.2 核心链路测试)
      • [7.3 K3S自带监控检查](#7.3 K3S自带监控检查)
      • [7.4 健康检查探针验证](#7.4 健康检查探针验证)
    • [8. 常见问题FAQ](#8. 常见问题FAQ)
    • [9. 结语](#9. 结语)

⏱️ 预计耗时 :90-120分钟(含镜像下载时间,视网络状况而定)

✅ 前置条件清单

  • 一台物理机或虚拟机,系统为 Ubuntu 20.04/22.04 LTSCentOS 7.9/8.x(本文以Ubuntu 22.04为例)
  • 硬件配置为 16核32G / 16核64G / 32核64G 中的一种
  • 拥有 root 或具有 sudo 权限的账号
  • 一个可以访问互联网的环境(在线安装方案),或已准备好离线安装包(离线安装方案)
  • 一颗愿意折腾的心和一杯咖啡

1. 引言:为什么是K3S单机集群?

"单机"和"集群"放在一起,听着就像"一个人的狂欢"。但现实场景中------无论是边缘计算节点、研发团队的公共测试床,还是个人云工作站------我们确实需要在 一台机器上跑出一整套微服务

K8S太"重",Minikube太"玩具",MicroK8S的snap包在离线环境下能把人逼疯......挑来选去,K3S 这个Rancher出品的轻量级Kubernetes发行版,是那个"刚刚好"的选择。

咱们今天的目标很明确:在一台机器上,用K3S把Redis、Kafka、ES、MySQL、Nacos、Nginx、MinIO这些常见的中间件都安排上,再部署一套典型的业务微服务(网关、认证、系统、文件、定时任务、订单、库存、小程序后端),让它们互相协作,变成一个五脏俱全的小生态。

架构速览

整个栈分为三层:

  • 基础设施层:K3S + containerd + Local Path Provisioner
  • 中间件层:MySQL, Redis, Kafka, ES, Nacos, MinIO, Nginx
  • 业务层:8个Spring Boot微服务,通过Nacos注册发现,由Gateway统一入口,Nginx做前端代理

别担心配置复杂,所有的YAML我都准备好了,你只需要 复制-粘贴-微调参数-回车。文中的内存/CPU配置我会给出动态计算公式,无论你的机器是32G还是64G,都能自适应匹配。


2. 前置准备:磨刀不误砍柴工

2.1 操作系统基础优化

很多新手关掉SWAP就以为万事大吉了,但K3S的 kubelet 对内存压力特别敏感。咱们今天要部署的中间件加起来能吃几十G内存,所以必须做一套"生产级"的内核调优。

bash 复制代码
# ========== 1. 关闭SWAP(但保留swapiness=1,理由见下文) ==========
# 先看看当前swap状态
sudo swapon --show
# 预期输出:如果有swap设备,会显示类似 '/swapfile file 8G 0B -2'

# 临时关闭
sudo swapoff -a
# 永久关闭:注释掉 /etc/fstab 中的swap行
sudo sed -i '/swap/s/^/#/' /etc/fstab

# 设置swappiness=1,告诉内核"尽量别用swap,但别完全禁用"
# 完全禁用(swappiness=0)在内存真耗尽时会触发OOM Killer,可能杀掉关键进程
echo 'vm.swappiness=1' | sudo tee -a /etc/sysctl.d/99-k3s.conf
bash 复制代码
# ========== 2. 调整文件句柄数和inotify限制 ==========
# K3S和containerd会打开大量文件,默认的1024根本不够
cat <<EOF | sudo tee -a /etc/security/limits.conf
* soft nofile 65536
* hard nofile 65536
* soft nproc 65536
* hard nproc 65536
EOF

# inotify限制,ES和Kafka尤其依赖这个
cat <<EOF | sudo tee -a /etc/sysctl.d/99-k3s.conf
fs.inotify.max_user_instances=8192
fs.inotify.max_user_watches=524288
EOF
bash 复制代码
# ========== 3. 内核网络参数调优 ==========
cat <<EOF | sudo tee -a /etc/sysctl.d/99-k3s.conf
# 允许IP转发,K3S网络插件必备
net.ipv4.ip_forward=1
net.bridge.bridge-nf-call-iptables=1
net.bridge.bridge-nf-call-ip6tables=1

# TCP调优,微服务间高并发连接
net.core.somaxconn=32768
net.ipv4.tcp_max_syn_backlog=8096
net.ipv4.tcp_tw_reuse=1
EOF

# 让所有sysctl配置生效
sudo sysctl --system
# 预期输出:逐一打印每个参数的设置值,无error

⚠️ 避坑指南

  1. sysctl: cannot stat /proc/sys/net/bridge/bridge-nf-call-iptables :说明没加载 br_netfilter 模块,执行 sudo modprobe br_netfilter && echo 'br_netfilter' | sudo tee -a /etc/modules-load.d/k3s.conf 即可。
  2. 文件句柄数重启后失效 :检查 /etc/ssh/sshd_config 中是否有 UsePAM yes,PAM模块会覆盖 limits.conf,需额外在 /etc/systemd/system.conf 中设置 DefaultLimitNOFILE=65536
  3. swapoff后重启又出现 :如果用的是swap文件,除了注释fstab,还要 sudo rm -f /swapfile;如果是swap分区,用 sudo systemctl mask dev-sdXN.swap 禁用对应systemd单元。

✅ 检查点1

执行 sudo sysctl net.ipv4.ip_forward,应返回 net.ipv4.ip_forward = 1;执行 ulimit -n,应返回 65536


2.2 防火墙/安全组策略配置

先理清楚咱们要用哪些端口,一股脑儿全放开,等跑通了再按需收紧。

bash 复制代码
# ========== K3S集群内部通信端口 ==========
# 6443: K3S API Server
# 10250: kubelet
# 8472: Flannel VXLAN (如果用flannel网络)
sudo ufw allow 6443/tcp
sudo ufw allow 10250/tcp
sudo ufw allow 8472/udp

# ========== 中间件暴露端口 ==========
sudo ufw allow 30000:32767/tcp   # NodePort范围,我们的服务都通过NodePort暴露
sudo ufw allow 80/tcp             # Nginx HTTP入口
sudo ufw allow 443/tcp            # Nginx HTTPS入口
sudo ufw allow 3306/tcp           # MySQL (如果外部需要直连)
sudo ufw allow 6379/tcp           # Redis
sudo ufw allow 9092/tcp           # Kafka
sudo ufw allow 9200/tcp           # ES HTTP
sudo ufw allow 9300/tcp           # ES Transport
sudo ufw allow 8848/tcp           # Nacos
sudo ufw allow 9000/tcp           # MinIO API
sudo ufw allow 9001/tcp           # MinIO Console

# 启用防火墙(如果之前没开过)
sudo ufw enable
# 预期输出:Firewall is active and enabled on system startup
sudo ufw status verbose

⚠️ 避坑指南

  1. 云服务器别忘了安全组:ufw配了但外部还是不通?先去云控制台的安全组里把上述端口也放行,ufw和安全组是两层过滤。
  2. Flannel在部分云厂商下不通:如果用的是Calico或Cilium替代Flannel,端口会不同,本文用K3S默认的Flannel,VXLAN走UDP 8472。
  3. NodePort默认范围是30000-32767,咱们的服务都用这个范围暴露,别去改它,省得跟K8S社区实践打架。

2.3 用户与用户组规划

我们创建一个专用运维账号 k3sadmin,避免日常操作直接用root。

bash 复制代码
# 创建用户,-m 创建家目录,-s 指定shell
sudo useradd -m -s /bin/bash k3sadmin
# 设置密码
sudo passwd k3sadmin
# 预期输出:提示输入新密码并确认

# 加入sudo组,并配置免密sudo(生产环境慎用,测试环境方便)
echo 'k3sadmin ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/k3sadmin
sudo chmod 0440 /etc/sudoers.d/k3sadmin

# 顺便把docker/k3s组也加上,后面可能用到
sudo usermod -aG docker k3sadmin 2>/dev/null || true

老司机私货 :我习惯在这个阶段顺便配好SSH免密登录,省得后面来回输密码打断节奏。ssh-copy-id k3sadmin@localhost 一步搞定。


2.4 容器运行时选择:containerd还是Docker?

我的建议:直接用K3S自带的containerd。

理由很直白:

  • K3S默认内置了 containerd,你不需要额外装任何东西。
  • Docker从K8S 1.24开始被移除了原生支持,K3S虽然还能用 --docker 参数,但那多了一层 cri-dockerd 的适配层,增加了故障点。
  • 离线环境下,containerd的镜像导入比docker简单,一个 ctr image import 搞定。

如果你非要怀旧用Docker(比如有些CI工具必须挂载Docker socket),那就在安装K3S时加 --docker 参数。本文默认走containerd路线,省心。


3. K3S安装:单机集群的起点

3.1 资源自适应计算公式

在动手安装之前,我们先定义一套 内存分配策略 ,避免后面配置中间件时拍脑袋。变量用 $(nproc)$(free -g) 动态获取:

bash 复制代码
# 获取CPU核数和总内存GB数,存为变量
export TOTAL_CORES=$(nproc)
export TOTAL_MEM_GB=$(free -g | awk '/^Mem:/{print $2}')
echo "CPU核数: $TOTAL_CORES, 总内存: ${TOTAL_MEM_GB}G"

推荐资源分配对照表(单位:GB内存 / 核数):

组件 16核32G配比 16核64G配比 32核64G配比 动态公式(粗粒度)
OS + K3S基础 4GB / 2核 4GB / 2核 4GB / 2核 固定预留
MySQL 4GB / 2核 8GB / 2核 8GB / 2核 MEM×12% (2GB~8GB)
Redis 2GB / 1核 4GB / 1核 4GB / 1核 MEM×6% (1GB~4GB)
Kafka 2GB / 2核 4GB / 2核 4GB / 2核 MEM×6% (2GB~4GB)
ES 4GB / 2核 8GB / 2核 8GB / 4核 MEM×12% (2GB~8GB)
Nacos 2GB / 1核 2GB / 1核 2GB / 1核 固定2GB/1核
MinIO 2GB / 1核 2GB / 1核 2GB / 1核 固定2GB/1核
Nginx 0.5GB / 0.5核 0.5GB / 0.5核 0.5GB / 0.5核 固定0.5GB/0.5核
业务微服务(8个) 8GB / 4核(各1GB) 16GB / 8核(各2GB) 16GB / 8核(各2GB) (剩余/8) 每个服务1~2GB
预留缓冲 5.5GB / 1.5核 15.5GB / 2.5核 15.5GB / 11.5核 总内存 - 上述分配总和

看到没,32G内存其实挺吃紧的,ES和MySQL稍微多分点,留给业务服务的内存就不多了。如果是生产环境,建议64G起步。


3.2 在线安装(网络通畅环境)

bash 复制代码
# 以k3sadmin身份执行(或者sudo)
curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION=v1.28.5+k3s1 \
  INSTALL_K3S_EXEC="--write-kubeconfig-mode 644 \
  --disable traefik \
  --disable servicelb \
  --kubelet-arg 'eviction-hard=memory.available<500Mi'" \
  sh -

# 预期输出:一大段日志后,最后显示
# [INFO] systemd: Starting k3s
# 等待30秒左右

参数详解:

  • --write-kubeconfig-mode 644:让普通用户也能读kubectl配置,不用每次sudo
  • --disable traefik:我们后面自己装Nginx做Ingress,traefik用不上
  • --disable servicelb:单机环境没有外部LB,关掉免得报一堆pending
  • --kubelet-arg 'eviction-hard=memory.available<500Mi':内存低于500Mi才开始驱逐Pod,避免正常的中件间被误杀
bash 复制代码
# 安装完成后,配置kubectl
mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $(id -u):$(id -g) ~/.kube/config

# 验证集群状态
kubectl get nodes
# 预期输出:
# NAME      STATUS   ROLES                  AGE   VERSION
# hostname  Ready    control-plane,master   30s   v1.28.5+k3s1

kubectl get pods -A
# 预期输出:kube-system命名空间下所有Pod状态为Running

3.3 离线安装(内网环境)

如果你的环境不能直接访问外网,需要先在另一台有网的机器上下载安装包和镜像。

步骤1:在联网机器上准备离线资源

bash 复制代码
# 下载K3S二进制文件和安装脚本
export K3S_VERSION=v1.28.5+k3s1
wget https://github.com/k3s-io/k3s/releases/download/${K3S_VERSION}/k3s -O /tmp/k3s
wget https://get.k3s.io -O /tmp/install.sh
wget https://github.com/k3s-io/k3s/releases/download/${K3S_VERSION}/k3s-airgap-images-amd64.tar.gz -O /tmp/k3s-images.tar.gz

# 打包
tar -czf k3s-offline.tar.gz -C /tmp k3s install.sh k3s-images.tar.gz
# 把这个包传到目标服务器

步骤2:在目标服务器上安装

bash 复制代码
# 解压
tar -xzf k3s-offline.tar.gz -C /tmp

# 安装二进制
sudo cp /tmp/k3s /usr/local/bin/
sudo chmod +x /usr/local/bin/k3s

# 导入镜像到containerd
sudo mkdir -p /var/lib/rancher/k3s/agent/images/
sudo cp /tmp/k3s-images.tar.gz /var/lib/rancher/k3s/agent/images/

# 运行安装脚本(会自动识别离线镜像)
sudo INSTALL_K3S_SKIP_DOWNLOAD=true INSTALL_K3S_EXEC="--write-kubeconfig-mode 644 --disable traefik --disable servicelb" /tmp/install.sh

# 预期输出:顺利启动,无网络错误

⚠️ 避坑指南

  1. 离线安装时kube-system的Pod都是ImagePullBackOff :说明镜像没导入成功,检查 /var/lib/rancher/k3s/agent/images/ 下文件是否存在,或者手动用 sudo k3s ctr images import /tmp/k3s-images.tar.gz 再导入一次。
  2. /tmp 分区太小导致解压失败 :换个有大空间的目录,比如 /opt
  3. K3S安装后kubectl get nodes返回NotReady :等一两分钟,如果还是NotReady,用 sudo systemctl status k3s 查看日志,大概率是cgroup驱动不兼容。

✅ 检查点2

执行 kubectl cluster-info,返回Kubernetes control plane和CoreDNS的地址;kubectl get cs,scheduler和controller-manager显示Healthy(注:高版本K3S中该命令已废弃,直接看Pod状态即可)。


4. 存储与网络配置

4.1 StorageClass:让PVC能找到"家"

K3S默认会创建 local-path 这个StorageClass,它会在节点上的 /opt/local-path-provisioner 目录下创建PV。这个路径默认在系统盘,而咱们的MySQL、ES数据量大,我们需要把它指向一块大容量的数据盘(如果有的话)。

bash 复制代码
# 查看默认SC
kubectl get sc
# 预期输出:NAME                   PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE
#          local-path (default)   rancher.io/local-path   Delete          WaitForFirstConsumer

# 如果数据盘挂载在 /data,修改local-path的配置
kubectl edit configmap -n kube-system local-path-config
# 找到 paths: 部分,把 /opt/local-path-provisioner 改成 /data/local-path-provisioner
# 同时修改下面这个Deployment的挂载
kubectl edit deployment -n kube-system local-path-provisioner
# 把 hostPath 的 path 也改为 /data/local-path-provisioner

如果你没有数据盘,想玩玩NFS或Longhorn,可以自定义StorageClass,但本文为了简单,直接用默认的 local-path,设它为默认SC就好。

4.2 本地镜像仓库策略

内网环境拉不到外网镜像?两个办法:

方案A:搭建私有Registry(推荐)

bash 复制代码
# 用docker-compose或K3S自身跑一个Registry
kubectl create deployment registry --image=registry:2 --port=5000 -n kube-system
kubectl expose deployment registry --type=NodePort --port=5000 --target-port=5000 -n kube-system
# 然后配置每个节点/etc/rancher/k3s/registries.yaml,让K3S信任这个私有仓库

方案B:手动导入镜像(简单暴力)

把需要的镜像先下载到一台跳板机,打包成tar,再到目标机器上导入:

bash 复制代码
# 在联网跳板机上
docker pull registry.cn-hangzhou.aliyuncs.com/your-ns/your-service:latest
docker save registry.cn-hangzhou.aliyuncs.com/your-ns/your-service:latest -o your-service.tar

# 传到目标机,用containerd的ctr导入
sudo k3s ctr images import your-service.tar
# 预期输出:unpacking docker.io/... 进度条

两种方案混着用最灵活:基础中间件镜像用导入的方式(反正版本固定),业务镜像推到私有Registry。


5. 中间件部署:摆好棋盘,放上棋子

咱们所有中间件都用 Helm Chart 或原生 YAML Manifest 部署。为了统一管理,先建一个命名空间 middleware

bash 复制代码
kubectl create ns middleware

5.1 MySQL:跑在K8S上的数据库

我这里用 bitnami/mysql 的Helm Chart,因为它对PVC、资源限制这些K8S特性支持得很好。

bash 复制代码
# 先添加bitnami的chart仓库
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update

# 动态计算JVM/内存参数
MYSQL_MEM_LIMIT=$(( TOTAL_MEM_GB * 12 / 100 ))
[[ $MYSQL_MEM_LIMIT -lt 2 ]] && MYSQL_MEM_LIMIT=2
[[ $MYSQL_MEM_LIMIT -gt 8 ]] && MYSQL_MEM_LIMIT=8  # 单机MySQL没必要超大内存
echo "MySQL内存限制: ${MYSQL_MEM_LIMIT}G"

创建 mysql-values.yaml

yaml 复制代码
# mysql-values.yaml
auth:
  rootPassword: "YourStrong!Passw0rd"  # 改掉!
  database: "microservice_db"
  username: "ms_user"
  password: "ms_pass_123"

primary:
  persistence:
    size: 20Gi  # 根据实际磁盘调整
    storageClass: "local-path"
  resources:
    requests:
      memory: "2Gi"
      cpu: "1000m"
    limits:
      memory: "${MYSQL_MEM_LIMIT}Gi"  # 这里手动替换成上面计算的值,或保留用envsubst
      cpu: "2000m"
  service:
    type: NodePort
    nodePorts:
      mysql: 30006  # 可选,不指定则随机分配
bash 复制代码
# 部署
envsubst < mysql-values.yaml | helm install mysql bitnami/mysql -n middleware -f -
# 预期输出:NAME: mysql, STATUS: deployed

# 验证
kubectl get pods -n middleware -l app.kubernetes.io/name=mysql
# 预期输出:mysql-0  1/1  Running

关键配置项含义

  • auth.rootPassword:root密码,生产务必改掉。
  • primary.persistence.size:PVC申请大小,local-path provisioner会在节点上创建对应大小的目录。
  • service.type: NodePort:通过节点IP+端口暴露,方便外部Navicat等工具直连调试。

5.2 Redis:缓存层

bash 复制代码
# 计算内存
REDIS_MEM=$(( TOTAL_MEM_GB * 6 / 100 ))
[[ $REDIS_MEM -lt 1 ]] && REDIS_MEM=1
[[ $REDIS_MEM -gt 4 ]] && REDIS_MEM=4

redis-values.yaml

yaml 复制代码
architecture: standalone  # 单机就别搞集群了
auth:
  password: "Redis!Strong2024"
master:
  persistence:
    size: 8Gi
    storageClass: "local-path"
  resources:
    requests:
      memory: "512Mi"
      cpu: "500m"
    limits:
      memory: "${REDIS_MEM}Gi"
      cpu: "1000m"
  service:
    type: NodePort
    nodePorts:
      redis: 30079
bash 复制代码
helm install redis bitnami/redis -n middleware -f redis-values.yaml
# 验证
kubectl exec -it redis-master-0 -n middleware -- redis-cli -a "Redis!Strong2024" PING
# 预期输出:PONG

5.3 Kafka:消息队列

bash 复制代码
KAFKA_MEM=$(( TOTAL_MEM_GB * 6 / 100 ))
[[ $KAFKA_MEM -lt 2 ]] && KAFKA_MEM=2
[[ $KAFKA_MEM -gt 4 ]] && KAFKA_MEM=4

kafka-values.yaml

yaml 复制代码
replicaCount: 1
controller:
  replicaCount: 0  # 单节点不需要controller
listeners:
  client:
    protocol: PLAINTEXT
  interbroker:
    protocol: PLAINTEXT
persistence:
  size: 10Gi
  storageClass: "local-path"
resources:
  requests:
    memory: "1Gi"
    cpu: "500m"
  limits:
    memory: "${KAFKA_MEM}Gi"
    cpu: "2000m"
heapOpts: "-Xmx${KAFKA_MEM}G -Xms${KAFKA_MEM}G"
service:
  type: NodePort
  nodePorts:
    client: 30092
bash 复制代码
helm install kafka bitnami/kafka -n middleware -f kafka-values.yaml
# 验证:创建一个测试topic
kubectl exec -it kafka-0 -n middleware -- kafka-topics.sh --create --topic test --bootstrap-server localhost:9092
# 预期输出:Created topic test.

5.4 Elasticsearch:搜索引擎

ES对内存很贪婪,给太多了JVM GC压力大,给太少了性能差。ES_HEAP_SIZE建议设为可用内存的一半,但不超过31GB(指针压缩上限)

bash 复制代码
ES_MEM=$(( TOTAL_MEM_GB * 12 / 100 ))
[[ $ES_MEM -lt 2 ]] && ES_MEM=2
[[ $ES_MEM -gt 8 ]] && ES_MEM=8
ES_HEAP=$(( ES_MEM / 2 ))

es-values.yaml

yaml 复制代码
replicas: 1
minimumMasterNodes: 1
clusterHealthCheckParams: "wait_for_status=yellow&timeout=1s"
esJavaOpts: "-Xms${ES_HEAP}g -Xmx${ES_HEAP}g"
resources:
  requests:
    memory: "2Gi"
    cpu: "1000m"
  limits:
    memory: "${ES_MEM}Gi"
    cpu: "2000m"
volumeClaimTemplate:
  accessModes: [ "ReadWriteOnce" ]
  storageClassName: "local-path"
  resources:
    requests:
      storage: 20Gi
service:
  type: NodePort
  nodePort: 30020
bash 复制代码
helm install elasticsearch bitnami/elasticsearch -n middleware -f es-values.yaml
# 验证
curl http://localhost:30020
# 预期输出:{ "name" : "elasticsearch-master-0", "cluster_name" : "elasticsearch", ... }

5.5 Nacos:注册中心/配置中心

Nacos需要MySQL来存数据,我们先在MySQL里创建数据库(通过Navicat直连30006,或者用下面命令):

bash 复制代码
kubectl exec -it mysql-0 -n middleware -- mysql -u root -pYourStrong\!Passw0rd -e "CREATE DATABASE IF NOT EXISTS nacos DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"

nacos-values.yaml

yaml 复制代码
replicaCount: 1
mysql:
  enabled: false  # 我们不使用子chart的mysql,用外部已有的
externalDatabase:
  host: "mysql.middleware.svc.cluster.local"  # K8S内部DNS
  port: 3306
  db: "nacos"
  user: "ms_user"
  password: "ms_pass_123"
resources:
  requests:
    memory: "1Gi"
    cpu: "500m"
  limits:
    memory: "2Gi"
    cpu: "1000m"
service:
  type: NodePort
  nodePort: 30848
bash 复制代码
helm install nacos nacos/nacos -n middleware -f nacos-values.yaml
# 注:需要先 helm repo add nacos https://nacos-group.github.io/nacos-helm-chart
# 预期输出:nacos-0  1/1  Running

5.6 MinIO:对象存储

bash 复制代码
MINIO_MEM=2  # 固定2G,小规模够用

minio-values.yaml

yaml 复制代码
mode: standalone
rootUser: "minioadmin"
rootPassword: "minioadmin2024"
persistence:
  size: 20Gi
  storageClass: "local-path"
resources:
  requests:
    memory: "1Gi"
    cpu: "500m"
  limits:
    memory: "${MINIO_MEM}Gi"
    cpu: "1000m"
service:
  type: NodePort
  nodePorts:
    api: "30900"
    console: "30901"
bash 复制代码
helm install minio bitnami/minio -n middleware -f minio-values.yaml
# 验证
curl http://localhost:30900/minio/health/live
# 预期输出:OK

5.7 Nginx:前端网关/反向代理

我们用Nginx做整个集群的统一入口,部署一个简单的Pod,把80/443映射出去。

yaml 复制代码
# nginx-deploy.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-config
  namespace: middleware
data:
  default.conf: |
    server {
        listen 80;
        server_name _;
        location / {
            proxy_pass http://gateway-svc:8080;  # 后面业务网关的Service
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }
    }
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  namespace: middleware
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.25-alpine
        ports:
        - containerPort: 80
        volumeMounts:
        - name: config
          mountPath: /etc/nginx/conf.d
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "512Mi"
            cpu: "500m"
      volumes:
      - name: config
        configMap:
          name: nginx-config
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-svc
  namespace: middleware
spec:
  type: NodePort
  selector:
    app: nginx
  ports:
  - port: 80
    targetPort: 80
    nodePort: 30080  # 外部统一入口
bash 复制代码
kubectl apply -f nginx-deploy.yaml
curl http://localhost:30080
# 此时后端gateway还没部署,预期返回502 Bad Gateway,说明Nginx本身没问题

✅ 检查点3

执行 kubectl get pods -n middleware,所有中间件Pod状态均为 Runningkubectl get svc -n middleware 能看到每个服务的NodePort。


6. 业务微服务部署:让应用跑起来

6.1 统一部署模板与环境变量注入

我们的8个微服务(gateway, auth, system, file, job, order, stock, mini)结构相似,都是Spring Boot应用,通过环境变量注入配置。这里以 gateway 为例,其他服务改个名字和端口即可。

部署模板 gateway-deploy.yaml

yaml 复制代码
apiVersion: v1
kind: ConfigMap
metadata:
  name: gateway-config
  namespace: app  # 先创建 kubectl create ns app
data:
  application.yml: |-
    # 这里放业务配置,也可以不用,完全走Nacos配置中心
    spring:
      cloud:
        nacos:
          discovery:
            server-addr: nacos.middleware.svc.cluster.local:8848
            namespace: public
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: gateway
  namespace: app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: gateway
  template:
    metadata:
      labels:
        app: gateway
    spec:
      containers:
      - name: gateway
        image: registry.cn-hangzhou.aliyuncs.com/your-ns/gateway:v1.0.0
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 8080
        env:
        - name: NACOS_ADDR
          value: "nacos.middleware.svc.cluster.local:8848"
        - name: DB_HOST
          value: "mysql.middleware.svc.cluster.local"
        - name: DB_PORT
          value: "3306"
        - name: DB_USER
          valueFrom:
            secretKeyRef:
              name: ms-secret
              key: db-user
        - name: DB_PASS
          valueFrom:
            secretKeyRef:
              name: ms-secret
              key: db-pass
        - name: REDIS_HOST
          value: "redis-master.middleware.svc.cluster.local"
        - name: REDIS_PORT
          value: "6379"
        - name: REDIS_PASS
          valueFrom:
            secretKeyRef:
              name: ms-secret
              key: redis-pass
        - name: JVM_OPTS
          value: "-Xms512m -Xmx1024m"  # 根据资源分配表调整
        livenessProbe:
          httpGet:
            path: /actuator/health
            port: 8080
          initialDelaySeconds: 60
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /actuator/health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 5
        resources:
          requests:
            memory: "512Mi"
            cpu: "250m"
          limits:
            memory: "1Gi"
            cpu: "1000m"
---
apiVersion: v1
kind: Service
metadata:
  name: gateway-svc
  namespace: app
spec:
  selector:
    app: gateway
  ports:
  - port: 8080
    targetPort: 8080

镜像构建策略

所有业务服务统一用Jib或Dockerfile构建,推送到私有Registry或离线导入。

dockerfile 复制代码
# 通用Dockerfile
FROM openjdk:11-jre-slim
COPY target/app.jar /app.jar
ENV JVM_OPTS="-Xms512m -Xmx1024m"
ENTRYPOINT ["sh", "-c", "java $JVM_OPTS -jar /app.jar"]
bash 复制代码
# 构建并推送
docker build -t registry.cn-hangzhou.aliyuncs.com/your-ns/gateway:v1.0.0 .
docker push registry.cn-hangzhou.aliyuncs.com/your-ns/gateway:v1.0.0

6.2 Secrets管理

敏感信息(数据库密码等)统一用Secret:

bash 复制代码
kubectl create secret generic ms-secret -n app \
  --from-literal=db-user='ms_user' \
  --from-literal=db-pass='ms_pass_123' \
  --from-literal=redis-pass='Redis!Strong2024'

6.3 批量部署

其他7个服务(auth, system, file, job, order, stock, mini)都按 gateway 的模板来,替换以下几个地方:

  • 所有 gateway 字样 → 对应服务名
  • containerPort → 对应服务的端口
  • image → 对应服务镜像地址
  • Service名称

偷懒技巧 :写一个简单的shell脚本,用 sed 批量替换模板生成每个服务的yaml,然后 kubectl apply -f 一把梭。

bash 复制代码
for svc in auth system file job order stock mini; do
  sed "s/gateway/${svc}/g" gateway-deploy.yaml > ${svc}-deploy.yaml
  kubectl apply -f ${svc}-deploy.yaml
done

7. 验证与测试:链路灯亮没亮?

7.1 检查所有Pod状态

bash 复制代码
kubectl get pods -n app
# 预期输出:8个微服务全部 Running
kubectl get pods -n middleware
# 预期输出:7个中间件全部 Running

7.2 核心链路测试

测试链路:Nginx → Gateway → Auth → MySQL/Redis

bash 复制代码
# 1. 访问Nginx入口,看是否转发到Gateway
curl -v http://localhost:30080/auth/login
# 预期:返回401或400,而不是502/504(说明Gateway已响应)

# 2. 注册/登录测试(根据你实际的API)
curl -X POST http://localhost:30080/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"admin123"}'
# 预期:返回JWT token

# 3. 用拿到的token访问一个受保护的接口
curl -H "Authorization: Bearer <token>" http://localhost:30080/system/user/info
# 预期:返回用户信息JSON

7.3 K3S自带监控检查

bash 复制代码
# 查看节点资源使用
kubectl top nodes
# 预期输出:CPU%和MEMORY%的实际使用率

# 查看Pod资源使用
kubectl top pods -n app
kubectl top pods -n middleware

# K3S自带的systemd服务状态
sudo systemctl status k3s
# 预期输出:active (running)

7.4 健康检查探针验证

bash 复制代码
# 描述一个Pod,查看探针状态
kubectl describe pod gateway-xxx -n app
# 在Events部分能看到:
#   Liveness probe succeeded
#   Readiness probe succeeded

如果探针失败 ,大概率是 /actuator/health 路径不对或者Spring Boot没加actuator依赖。检查 pom.xml 是否有 spring-boot-starter-actuator


8. 常见问题FAQ

Q1: 某个Pod一直是ContainerCreating状态?

A1: 大概率是PVC没绑定上。kubectl describe pvc -n <namespace> 看Events,如果提示 no persistent volumes available,检查StorageClass配置和节点磁盘空间。

Q2: MySQL启动失败,日志显示 InnoDB: mmap(xxx bytes) failed; errno 12

A2: 内存不足。减一下MySQL的 limits.memory,或者把其他不用的Pod停掉。

Q3: Nacos服务注册成功,但服务间调用不通?

A3: 检查Nacos里的服务地址是否正确,应该是K8S Service名(如 gateway-svc.app.svc.cluster.local:8080),而不是Pod IP。Spring Cloud默认会取宿主机IP,需要加配置:

yaml 复制代码
spring.cloud.nacos.discovery.ip: ${POD_NAME}.${SERVICE_NAME}.${NAMESPACE}.svc.cluster.local

或者用 spring.cloud.nacos.discovery.metadata.preserved.heart.beat.interval=1000

Q4: ES集群健康状态一直是red?

A4: 单节点ES,分片没有副本,状态yellow是正常的。如果是red,检查磁盘空间是否 > 90%导致索引只读:curl -XPUT 'localhost:30900/_cluster/settings' -H 'Content-Type: application/json' -d '{"transient":{"cluster.routing.allocation.disk.watermark.low":"90%"}}'

Q5: 重启机器后,所有服务都起不来了?

A5: K3S的local-path provisioner在主机重启后,如果Pod漂移(虽然单节点不会漂,但重建时),数据会丢失。生产环境建议用外置存储或Longhorn做复制。开发环境就先这样,记得定时备份。

Q6: 镜像拉取超时 ImagePullBackOff

A6: 先 kubectl describe pod xxx 看具体错误。如果是超时,可能是私有仓库没配置 imagePullSecrets。创建一个Secret:kubectl create secret docker-registry regcred --docker-server=your-registry --docker-username=xxx --docker-password=xxx -n app,然后在Deployment里加上 imagePullSecrets: - name: regcred


9. 结语

好了,到这儿,你已经在一台机器上成功搭建了一套完整的微服务基础设施。来回顾下咱们干了什么:

  • 一个单节点K3S集群,containerd做运行时
  • 7个核心中间件(MySQL, Redis, Kafka, ES, Nacos, MinIO, Nginx)通过Helm Chart管理
  • 8个业务微服务通过统一的部署模板跑起来,配置通过环境变量和Nacos下发
  • 所有服务通过NodePort暴露,Nginx做统一入口
  • 资源分配根据物理机配置自适应,不会因为硬编码导致OOM

但它还不是生产就绪的:少了日志收集(ELK/Loki)、少了Prometheus监控告警、少了CI/CD流水线、数据备份也没自动化......这些我会在下一篇文章《给K3S单机集群穿上盔甲》里补上。

最后一句老司机私货 :今天这套东西,最适合的场景是 研发自测环境边缘小型节点。真要上生产,请至少做三台机器的集群,数据做异地备份,中间件用外部托管服务。单机集群是瑞士军刀,不是屠龙刀------用对地方,爽得飞起;用错地方,半夜报警电话会让你怀疑人生。

如果这篇文章帮你省了10小时折腾时间,欢迎点个赞、转给同样在坑里的同事。有任何问题,评论区见,我基本每条都会回复。