1.Hyperf 生产环境部署实践:动态更新策略探讨
在基于 Hyperf 框架进行生产环境开发时,如何高效、安全地更新应用代码是一个普遍面临的关键问题。Hyperf 作为构建在 Swoole 之上的高性能框架,其常驻内存的特性意味着:当代码文件发生变更后,必须重启 Worker 进程才能使修改生效,从而加载最新代码到内存中。针对这一核心挑战,业界普遍存在以下几种解决方案:
-
服务进程重启 (Brute-force Restart)
- 操作方式: 直接停止(Kill)现有 Hyperf 服务进程,然后重新启动。
- 优点: 实现极其简单直观,无需额外工具或配置。
- 缺点: 属于"暴力"重启,会导致当前正在处理的请求丢失,服务出现短暂中断,不适用于对可用性要求高的生产环境。
-
开发环境热更新机制 (Hyperf Watcher - Development Only)
- 操作方式: 利用 Hyperf 提供的热更新组件(如
hyperf/watcher
),在检测到代码文件变动时自动触发服务重启。 - 优点: 开发体验便捷,显著提升开发效率。
- 缺点: 仅适用于本地开发或测试环境。 其本质仍是重启整个 Worker 进程组,在生产环境中同样会造成请求中断。强烈不建议用于生产环境。
- 操作方式: 利用 Hyperf 提供的热更新组件(如
-
基于 Docker 与 Nginx Upstream 的蓝绿/滚动发布
-
核心思路: 为更新启动一个包含新代码的新容器实例(新服务)。待新实例完全就绪并通过健康检查后,通过动态修改 Nginx
upstream
配置,用新容器的实际 IP 地址和端口替换旧容器实例的条目,从而将流量平滑切换到新版本服务。 -
主要技术挑战:
- 新容器实例需动态分配端口(避免与旧实例端口冲突)。
- 如何自动发现新启动的服务实例(服务发现)。
- 如何实时、可靠地更新 Nginx
upstream
配置(配置动态更新)。
-
优点: 相较于前两种方案,能实现更平滑的流量切换,避免瞬间中断,用户体验更佳。
-
缺点: 需自行解决端口管理、服务发现和配置动态更新等复杂性问题。
-
-
Kubernetes (K8s) 滚动更新 (Rolling Update)
- 核心思路(类似方案3,但自动化): 在 K8s 集群中,通过 Deployment 等资源定义更新时,会启动包含新代码的 Pod(新服务实例)。当新 Pod 就绪并通过探针检查后,Kubernetes 控制平面会自动将其添加至 Service 的 Endpoints(接收外部流量)。同时,系统将旧 Pod 从 Endpoints 中移除(停止接收新流量)。可配置优雅终止(
preStop
hook)确保旧 Pod 处理完现有请求后被安全删除。 - 技术挑战: 需具备 Kubernetes 基础知识和运维能力。
- 优点: 平台自动化处理滚动更新、健康检查(存活/就绪探针)、自动扩缩容、服务发现、负载均衡及优雅终止,是实现生产环境无缝更新的最佳实践之一。
- 核心思路(类似方案3,但自动化): 在 K8s 集群中,通过 Deployment 等资源定义更新时,会启动包含新代码的 Pod(新服务实例)。当新 Pod 就绪并通过探针检查后,Kubernetes 控制平面会自动将其添加至 Service 的 Endpoints(接收外部流量)。同时,系统将旧 Pod 从 Endpoints 中移除(停止接收新流量)。可配置优雅终止(
本文重点聚焦于 Hyperf 应用在生产环境中的部署与更新策略。 文中内容的探讨基于读者已熟悉 Hyperf 框架原理,并对 Kubernetes 核心概念(如 Deployment, Pod, Service, Ingress 等)有基本了解的前提展开。 关于k8s的一些入门教程:
部署流程图:
1、开发者提交代码到master分支后,执行构建脚本.sh:
- 拉取master分支最新代码
- 生成版本号
- 创建临时容器,用于代码的编译
- 进入容器执行composer install、生成proxy文件
- 将代码打包,减小镜像的大小
- 根据版本号创建镜像,将打包好的代码COPY到镜像内
- 将镜像推送至 镜像仓库
- k8s更新镜像:kubectl set image deployment/<Deployment的名称> <容器名称>=<新镜像地址:新版本标签>
2、k8s接收到命令后,进行镜像的更新
这构建流程不仅适用于php hyperf, golang项目在部署时也同样适用,构建脚本.sh 后期也可替换成Jenkins,流程大同小异,关键点在于 构建脚本.sh 的编写,与k8s的部署与配置。
所需的软件和环境
框架 :mineAdmin3.0,一个基于hyperf框架的企业级后台管理系统。本文中使用该框架做例子。当然也可以是其他项目或框架,比如:gin项目。CICD流程都差不多。
系统 :使用虚拟机进行安装CentOS-7.9,,虚拟机和系统搭建,请看这篇文章
k8s :Kubernetes 是一个可移植、可扩展的开源平台,用于管理容器化的工作负载和服务,方便进行声明式配置和自动化。Kubernetes 拥有一个庞大且快速增长的生态系统,其服务、支持和工具的使用范围广泛。
k3s :K3s 是一个轻量级的 Kubernetes 发行版,它针对边缘计算、物联网等场景进行了高度优化。K3s 适用于以下场景:边缘计算-Edge、物联网-IoT、CI、Development、ARM、嵌入 K8s
kuboard :kuboard 一个Kubernetes 多集群管理界面,可视化的管理k8s/k3s的集群
初始化
准备好2台CentOS-7.9的服务器
名称 | ip | 配置 | |
---|---|---|---|
k3s-server | 192.168.119.131 | 2核4G | |
k3s-agent | 192.168.119.132 | 2核4G |
csharp
#在server节点上执行,需要重启服务器
sudo hostnamectl set-hostname k3s-server
#在agent节点上执行,需要重启服务器
sudo hostnamectl set-hostname k3s-agent
sudo systemctl stop firewalld # 立即停止防火墙
sudo systemctl disable firewalld # 禁止开机启动
安装docker
arduino
sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine
arduino
sudo curl -o /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-7.repo
sudo yum clean all
sudo yum makecache
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
sudo yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
sudo yum makecache fast
bash
# 查看可用版本列表
sudo yum list docker-ce --showduplicates | sort -r
# 安装指定版本(26.1.4)
sudo yum install -y docker-ce-26.1.4 docker-ce-cli-26.1.4 containerd.io
bash
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": [
"https://registry.cn-hangzhou.aliyuncs.com",
"https://docker.mirrors.ustc.edu.cn",
"https://docker.xuanyuan.run"
]
}
EOF
sudo systemctl enable docker
sudo systemctl start docker
#验证docker
[root@localhost ~]# docker -v
Docker version 26.1.4, build 5650f9b
遇到镜像拉取不下来时,建议使用轩辕镜像,方便后续操作。
server 和 agent 服务都需要安装docker
安装k3s
使用离线安装方式进行安装,下载离线包server、agent上都分别下载
bash
#下载安装脚本
wget https://rancher-mirror.rancher.cn/k3s/k3s-install.sh
chmod +x k3s-install.sh
# 下载 K3s 二进制和镜像(替换版本号)下载太慢的话,可以在宿主机上下载,再上传到虚拟机
wget -4 https://github.com/k3s-io/k3s/releases/download/v1.33.4-rc1%2Bk3s1/k3s
sudo chmod +x k3s && sudo mv k3s /usr/local/bin/
wget -4 https://github.com/k3s-io/k3s/releases/download/v1.33.4-rc1%2Bk3s1/k3s-airgap-images-amd64.tar
sudo mkdir -p /var/lib/rancher/k3s/agent/images/
sudo cp k3s-airgap-images-amd64.tar /var/lib/rancher/k3s/agent/images/
添加镜像加速: server、agent节点分别添加
bash
sudo tee /etc/rancher/k3s/registries.yaml > /dev/null <<'EOF'
mirrors:
"docker.io":
endpoint:
- "https://这里填自己的docker加速地址"
"k8s.gcr.io":
endpoint:
- "https://registry.aliyuncs.com/google_containers"
- "https://gcr.mirrors.ustc.edu.cn"
EOF
server节点安装
perl
INSTALL_K3S_SKIP_DOWNLOAD=true ./k3s-install.sh
#安装结果:
[root@k3s_server ~]# INSTALL_K3S_SKIP_DOWNLOAD=true ./k3s-install.sh
[INFO] Skipping k3s download and verify
[INFO] Skipping installation of SELinux RPM
[INFO] Creating /usr/local/bin/kubectl symlink to k3s
[INFO] Creating /usr/local/bin/crictl symlink to k3s
[INFO] Skipping /usr/local/bin/ctr symlink to k3s, command exists in PATH at /usr/bin/ctr
[INFO] Creating killall script /usr/local/bin/k3s-killall.sh
[INFO] Creating uninstall script /usr/local/bin/k3s-uninstall.sh
[INFO] env: Creating environment file /etc/systemd/system/k3s.service.env
[INFO] systemd: Creating service file /etc/systemd/system/k3s.service
[INFO] systemd: Enabling k3s unit
Created symlink from /etc/systemd/system/multi-user.target.wants/k3s.service to /etc/systemd/system/k3s.service.
[INFO] systemd: Starting k3s
#查看一下状态,是否正确启动了
[root@k3s-server ~]# k3s kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
k3s-server Ready control-plane,master 11m v1.33.4-rc1+k3s1 192.168.119.131 <none> CentOS Linux 7 (Core) 3.10.0-1160.el7.x86_64 containerd://2.0.5-k3s2
接着再server节点查看url和token:
csharp
[root@k3s_server ~]# sudo cat /etc/rancher/k3s/k3s.yaml | grep server
server: https://127.0.0.1:6443
[root@k3s_server ~]# sudo cat /var/lib/rancher/k3s/server/node-token
K104c4194aac85b338608773eda1fea40b817ced6ef6f4b6879b11ab9c9423ee8b3::server:0e1f5e91b653c171e42aef9d8c853bfa
如果安装失败了,进行卸载再重新操作一遍:
bash
/usr/local/bin/k3s-uninstall.sh
agent节点安装
K3S_URL:替换成自己的server节点ip地址
K3S_TOEKN:由server获取
ini
INSTALL_K3S_SKIP_DOWNLOAD=true K3S_URL=https://192.168.119.131:6443 K3S_TOKEN=K104c4194aac85b338608773eda1fea40b817ced6ef6f4b6879b11ab9c9423ee8b3::server:0e1f5e91b653c171e42aef9d8c853bfa ./k3s-install.sh
perl
[root@k3s_agent ~]# INSTALL_K3S_SKIP_DOWNLOAD=true K3S_URL=https://192.168.119.131:6443 K3S_TOKEN=K104c4194aac85b338608773eda1fea40b817ced6ef6f4b6879b11ab9c9423ee8b3::server:0e1f5e91b653c171e42aef9d8c853bfa ./k3s-install.sh
[INFO] Skipping k3s download and verify
[INFO] Skipping installation of SELinux RPM
[INFO] Creating /usr/local/bin/kubectl symlink to k3s
[INFO] Creating /usr/local/bin/crictl symlink to k3s
[INFO] Skipping /usr/local/bin/ctr symlink to k3s, command exists in PATH at /usr/bin/ctr
[INFO] Creating killall script /usr/local/bin/k3s-killall.sh
[INFO] Creating uninstall script /usr/local/bin/k3s-agent-uninstall.sh
[INFO] env: Creating environment file /etc/systemd/system/k3s-agent.service.env
[INFO] systemd: Creating service file /etc/systemd/system/k3s-agent.service
[INFO] systemd: Enabling k3s-agent unit
Created symlink from /etc/systemd/system/multi-user.target.wants/k3s-agent.service to /etc/systemd/system/k3s-agent.service.
[INFO] systemd: Starting k3s-agent
Job for k3s-agent.service failed because the control process exited with error code. See "systemctl status k3s-agent.service" and "journalctl -xe" for details.
perl
#查看一下状态,是否正确启动了,再server执行
[root@k3s-server ~]# k3s kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
k3s-agent Ready <none> 7s v1.33.4-rc1+k3s1 192.168.119.132 <none> CentOS Linux 7 (Core) 3.10.0-1160.el7.x86_64 containerd://2.0.5-k3s2
k3s-server Ready control-plane,master 15m v1.33.4-rc1+k3s1 192.168.119.131 <none> CentOS Linux 7 (Core) 3.10.0-1160.el7.x86_64 containerd://2.0.5-k3s2
如果安装失败了,进行卸载再重新操作一遍:
bash
/usr/local/bin/k3s-agent-uninstall.sh
安装kuboard
在server节点上执行
ini
sudo docker run -d \
--restart=unless-stopped \
--name=kuboard \
-p 8081:80/tcp \
-p 10081:10081/tcp \
-e KUBOARD_ENDPOINT="http://{server的ip地址}:80" \
-e KUBOARD_AGENT_SERVER_TCP_PORT="10081" \
-v /root/kuboard-data:/data \
eipwork/kuboard:v3
进入管理后台: http://192.168.119.131:8081/ admin Kuboard123




集群导入成功,到此 kuboard 的安装就完成了
安装mineadmin
关于mineAdmin的部署与启动这里就不再进行说明。要确保服务能正常启动.
csharp
docker run -it \
--name mineadmin \
-p 9501:9501 \
-v $(pwd):/opt/www \
mineadmin:latest
#先在.env文件配置好数据库和redis新增
#进入容器
dokcer exec -it mineadmin /bin/bash
# 安装依赖包(开发环境)
composer install -vvv
# 创建数据库(可选,也可以手动创建)
mysql -u root -p -e "CREATE DATABASE mineadmin CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
# 执行数据库迁移
php bin/hyperf.php migrate
# 填充初始数据
php bin/hyperf.php db:seed
#启动程序
/opt/www # php bin/hyperf.php start
[INFO] Worker#1 started.
[INFO] Worker#0 started.
[INFO] Worker#2 started.
[INFO] Worker#3 started.
[INFO] HTTP Server listening at 0.0.0.0:9503
[INFO] HTTP Server listening at 0.0.0.0:9501
[INFO] Worker#6 started.
[INFO] Worker#4 started.
[INFO] Worker#7 started.
[INFO] Worker#5 started.
创建镜像仓库
镜像仓库可以自己搭建、者使用阿里云的镜像仓库,这里以阿里云镜像仓库为例。




在server、agent节点上进行登入
ini
docker login --username=xxxx registry.cn-shenzhen.aliyuncs.com
到此,阿里云镜像仓库创建完毕,基础镜像和项目镜像都会上传到这个仓库里。
构建镜像
为了加快镜像的构建速度,将镜像分为:基础镜像 和项目镜像,基础镜像是php、swoole等一些配置。项目镜像包含了项目的代码。
基础镜像
./deploy/base.dockerfile
基础镜像只需要构建一次。后面就不需要了,除非修改了php配置或者增加了php扩展等
bash
FROM hyperf/hyperf:8.1-alpine-v3.19-swoole-slim
LABEL maintainer="MineManage Developers <group@stye.cn>" version="1.0" license="MIT" app.name="MineManage"
##
# ---------- env settings ----------·
##
# --build-arg timezone=Asia/Shanghai
ARG timezone
ENV TIMEZONE=${timezone:-"Asia/Shanghai"} \
APP_ENV=prod \
SCAN_CACHEABLE=(true)
# update
RUN set -ex \
# show php version and extensions
&& php -v \
&& php -m \
&& php --ri swoole \
# ---------- some config ----------
&& cd /etc/php* \
# - config PHP
&& { \
echo "upload_max_filesize=128M"; \
echo "post_max_size=128M"; \
echo "memory_limit=1G"; \
echo "date.timezone=${TIMEZONE}"; \
} | tee conf.d/99_overrides.ini \
# - config timezone
&& ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \
&& echo "${TIMEZONE}" > /etc/timezone \
# ---------- clear works ----------
&& rm -rf /var/cache/apk/* /tmp/* /usr/share/man \
&& echo -e "\033[42;37m Build Completed :).\033[0m\n"
# update
RUN set -ex \
# ---------- some config ----------
&& cd /etc/php81 \
# - config timezone
&& ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \
&& echo "${TIMEZONE}" > /etc/timezone \
&& echo -e "\033[42;37m Build Completed :).\033[0m\n"
RUN set -ex && \
sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk update \
&& apk add --no-cache libstdc++ openssl git bash autoconf pcre2-dev zlib-dev re2c gcc g++ make \
php81-pear php81-dev php81-tokenizer php81-fileinfo php81-simplexml php81-xmlwriter \
&& apk add --no-cache --virtual .build-deps $PHPIZE_DEPS zlib-dev libaio-dev openssl-dev curl-dev c-ares-dev \
&& pecl81 channel-update pecl.php.net \
&& pecl81 install --configureoptions 'enable-reader="yes"' xlswriter \
&& echo "extension=xlswriter.so" >> /etc/php81/conf.d/60-xlswriter.ini \
&& php -m \
&& php -v \
&& php --ri swoole \
&& mkdir -p /app-src \
# ---------- clear works ----------
&& apk del .build-deps \
&& rm -rf /var/cache/apk/* /tmp/* /usr/share/man /usr/local/bin/php* \
&& echo -e "\033[42;37m Build Completed :).\033[0m\n"
ENV LD_PRELOAD /usr/lib/preloadable_libiconv.so
WORKDIR /opt/www
csharp
# 项目根目录执行
docker build -f ./deploy/base.dockerfile -t mineadmin:base .
[root@k3s-server deploy]# docker images | grep mineadmin
mineadmin base 905322f09690 About a minute ago 383MB
mineadmin latest 331df88ac067 19 hours ago 118MB
#打标签
docker tag 905322f09690 registry.cn-shenzhen.aliyuncs.com/byd_dev/mineadmin:base
#推送镜像
docker push registry.cn-shenzhen.aliyuncs.com/byd_dev/mineadmin:base
如果遇到镜像拉取不下来,请更换其他源。或者提前把 hyperf/hyperf:8.1-alpine-v3.19-swoole-slim pull 下来
项目镜像
./deploy.dockerfile.tlp
bash
# 这里要替换成自己的基础镜像地址
FROM registry.cn-shenzhen.aliyuncs.com/byd_dev/mineadmin:base
COPY {package_name} /opt/www/{project_tar_gz}
RUN rm -rf /opt/www/{package_name}
EXPOSE 9501 9502 9503
修改.dockerignore 文件
diff
**
!app/
!bin/
!config/
!databases/
!plugin/
!storage/
!composer.*
!.env.example
!*.tar.gz
编写build.sh
./deploy/build.sh
bash
#!/bin/bash
# --- Color Definitions ---
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# --- Script Configuration ---
#镜像仓库地址
REGISTRY_URL="registry.cn-shenzhen.aliyuncs.com/byd_dev/mineadmin"
#构建容器名称
CONTAINER_NAME="mineadmin"
#打包名称(项目名称)
PROJECT_NAME="mineadmin"
#k8s命名空间
K8S_NAMESPACE="mineadmin"
PROJECT_NAME_TAR_GZ="${PROJECT_NAME}.tar.gz"
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
WEB_PATH=$(dirname "$SCRIPT_DIR")
# --- Main Script ---
cd "$WEB_PATH" || exit
# 1. Parse branch argument
BRANCH=${1:-master}
if [ "$BRANCH" != "master" ] && [ "$BRANCH" != "dev" ]; then
echo -e "${RED}❌ 错误:无效的分支名称。请使用 'master' 或 'dev'。${NC}"
exit 1
fi
echo -e "${CYAN}===================================================${NC}"
echo -e "${CYAN}🚀 开始为分支 '${YELLOW}$BRANCH${CYAN}' 执行构建...${NC}"
echo -e "${CYAN}===================================================${NC}"
# 2. Pull latest code from Git
echo -e "\n${BLUE}Step 1: 从 Git 拉取最新代码...${NC}"
git checkout $BRANCH > /dev/null 2>&1
git pull origin $BRANCH > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo -e "${RED}❌ 错误:从 Git 拉取代码失败。${NC}"
exit 1
fi
echo -e "${GREEN}✔️ Git 代码拉取成功。${NC}"
# Step 2: Get version from timestamp1
echo -e "\n${BLUE}Step 2: 正在生成时间戳版本号...${NC}"
VERSION=$(date +%y%m%d%H%M%S)
if [ -z "$VERSION" ]; then
echo -e "${RED}❌ 错误:生成时间戳版本号失败。${NC}"
exit 1
fi
echo -e "${GREEN}✔️ 使用时间戳作为版本号:${YELLOW}$VERSION${NC}"
# Step 3: Prepare configuration file
# 如果使用了k8s的configMap功能,就不需要copy配置文件了
echo -e "\n${BLUE}Step 3: 准备配置文件...${NC}"
ENV_FILE=".env.$BRANCH"
if [ ! -f "$ENV_FILE" ]; then
echo -e "${RED}❌ 错误:配置文件 '$ENV_FILE' 不存在。${NC}"
exit 1
fi
cp "$ENV_FILE" .env
echo -e "${GREEN}✔️ 已将 '$ENV_FILE' 复制到 '.env'。${NC}"
# Check container status
echo -e "\n${BLUE}Step 4: 检查容器状态...${NC}"
CONTAINER_ID=$(docker ps -qf "name=^${CONTAINER_NAME}$")
if [ -z "$CONTAINER_ID" ]; then
echo -e "${RED}❌ 错误:找不到名为 '$CONTAINER_NAME' 的正在运行的容器。${NC}"
echo -e "${YELLOW}请确保容器已根据环境要求提前启动。${NC}"
exit 1
fi
echo -e "${GREEN}✔️ 容器 '$CONTAINER_NAME' 正在运行。${NC}"
# Execute commands inside the container11
echo -e "\n${BLUE}Step 5: 在容器内执行构建命令...${NC}"
echo -e " - 正在执行 'composer install'..."
docker exec $CONTAINER_NAME composer install --no-dev --optimize-autoloader > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo -e "${RED} - ❌ 错误:'composer install' 执行失败${NC}"
exit 1
fi
echo -e " - 正在执行 'composer dump-autoload -o'..."
docker exec $CONTAINER_NAME composer dump-autoload -o > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo -e "${RED} - ❌ 错误:'composer dump-autoload -o' 执行失败。${NC}"
exit 1
fi
echo -e " - 正在生成 proxy 代理文件..."
docker exec $CONTAINER_NAME php /opt/www/bin/hyperf.php > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo -e "${RED} - ❌ 错误:生成代理文件命令执行失败。${NC}"
exit 1
fi
docker exec $CONTAINER_NAME test -d /opt/www/runtime/container
if [ $? -ne 0 ]; then
echo -e "${RED} - ❌ 错误:Hyperf 代理文件目录 /opt/www/runtime/container 未找到,生成失败。${NC}"
exit 1
fi
echo -e "${GREEN}✔️ 容器内构建命令执行成功。${NC}"
# 5. Package the code1
PACKAGE_NAME="${PROJECT_NAME}-${VERSION}.tar.gz"
echo -e "\n${BLUE}Step 6: 📦 将代码打包到 '${YELLOW}$PACKAGE_NAME${BLUE}'...${NC}"
tar --exclude=.dockerignore \
--exclude='*.log' \
--exclude='.idea' \
--exclude='.devcontainer' \
--exclude='.git' \
--exclude='runtime' \
--exclude='*.tar.gz' \
-zcvf $PACKAGE_NAME ./* > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo -e "${RED}❌ 错误:打包代码失败。${NC}"
exit 1
fi
echo -e "${GREEN}✔️ 代码打包成功。${NC}"
# 6. Build Docker image
IMAGE_TAG="$VERSION"
FULL_IMAGE_NAME="${REGISTRY_URL}:${IMAGE_TAG}"
TEMPLATE_FILE="deploy/deploy.dockerfile.tlp"
DEPLOY_DOCKERFILE="deploy/deploy.dockerfile"
echo -e "\n${BLUE}Step 7: 🐳 构建 Docker 镜像...${NC}"
echo -e " - 镜像名称: ${YELLOW}$FULL_IMAGE_NAME${NC}"
if [ ! -f "$TEMPLATE_FILE" ]; then
echo -e "${RED}❌ 错误:模板文件 '$TEMPLATE_FILE' 不存在。${NC}"
exit 1
fi
sed -e "s|{package_name}|${PACKAGE_NAME}|g" -e "s|{project_tar_gz}|${PROJECT_NAME_TAR_GZ}|g" "$TEMPLATE_FILE" > "$DEPLOY_DOCKERFILE"
if [ $? -ne 0 ]; then
echo -e "${RED}❌ 错误:创建 '$DEPLOY_DOCKERFILE' 失败。${NC}"
exit 1
fi
docker build -f "$DEPLOY_DOCKERFILE" -t "$FULL_IMAGE_NAME" . > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo -e "${RED}❌ 错误:构建 Docker 镜像失败。${NC}"
rm "$DEPLOY_DOCKERFILE"
exit 1
fi
echo -e "${GREEN}✔️ Docker 镜像构建成功。${NC}"
# Clean up temporary Dockerfile
rm "$DEPLOY_DOCKERFILE"
# 7. Push image to registry
echo -e "\n${BLUE}Step 8: 📤 上传镜像到仓库...${NC}"
docker push $FULL_IMAGE_NAME > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo -e "${RED}❌ 错误:上传镜像失败。${NC}"
exit 1
fi
echo -e "${GREEN}✔️ 镜像上传成功。${NC}"
echo -e "\n${GREEN}===================================================${NC}"
echo -e "${GREEN}✨ 构建和上传成功完成! ✨${NC}"
echo -e "${GREEN}===================================================${NC}"
echo -e "镜像名称:${YELLOW}$FULL_IMAGE_NAME${NC}\n"
# 8. 更新k8s
echo -e "\n${BLUE}Step 9: 🚀 更新 Kubernetes Deployment...${NC}"
# 设置mineadmin 进行镜像更新
kubectl set image deployment/${PROJECT_NAME} ${PROJECT_NAME}=${FULL_IMAGE_NAME}
if [ $? -ne 0 ]; then
echo -e "${RED}❌ 错误:触发 Kubernetes Deployment 更新失败。${NC}"
echo -e "${RED}请检查:${NC}"
echo -e "${RED} 1. 是否有权限访问 Kubernetes 集群 (kubectl config)。${NC}"
echo -e "${RED} 2. Deployment 名称 '${PROJECT_NAME}' 和容器名称 '${PROJECT_NAME}' 是否正确。${NC}"
exit 1
fi
echo -e "${GREEN}✔️ Deployment 更新已成功触发。${NC}"
# 9. (可选但强烈推荐) 检查更新状态
echo -e "\n${BLUE}Step 10: 🔍 监控滚动更新状态...${NC}"
kubectl rollout status deployment/${PROJECT_NAME}
if [ $? -ne 0 ]; then
echo -e "${RED}❌ 错误:Deployment 滚动更新失败。请使用 'kubectl describe deployment/${PROJECT_NAME}' 查看详情。${NC}"
exit 1
fi
echo -e "\n${GREEN}===================================================${NC}"
echo -e "${GREEN}✅ CI/CD 流程全部成功完成!应用已更新。 ✨${NC}"
echo -e "${GREEN}===================================================${NC}"
连接k3s集群
在执行build.sh脚本之前,需要对k8s集群有访问权限。如果是在k3s-server 节点上执行构建脚本的话,就不需要做此操作。在其他服务器上构建时,需要做以下操作:
bash
# 下载最新版本的 kubectl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
# 赋予执行权限
chmod +x ./kubectl
# 将其移动到您的PATH路径下,例如 /usr/local/bin
sudo mv ./kubectl /usr/local/bin/kubectl
# 创建 .kube 目录 : kubectl 默认会在用户的主目录下的 .kube 文件夹中寻找名为 config 的文件。
mkdir -p $HOME/.kube
#将 k8s-service节点上的配置文件复制过来:sudo cat /etc/rancher/k3s/k3s.yaml,修改server:https://192.168.119.131:6443
vim $HOME/.kube/config

csharp
# 在构建节点上执行,看是否能获取到集群信息
[root@localhost ~]# kubectl get nodes
NAME STATUS ROLES AGE VERSION
k3s-agent Ready <none> 5d19h v1.33.4-rc1+k3s1
k3s-server Ready control-plane,master 5d19h v1.33.4-rc1+k3s1
执行build.sh
在项目根目录执行 sh deploy/build.sh
看是否能够正常:
- 打包出来的tag.gz文件,是否正常
- 是否能push到镜像仓库
- 对刚创建出来的 镜像,进行测试。根据镜像 创建一个临时容器,进入容器,启动项目,看是否能启动成功
到此完成了项目的拉取、编译、构建镜像功能。下一步就是在k8s(k3s)上使用镜像部署项目
k3s部署项目
创建命名空间


配置ConfigMap
ConfigMap 是 Kubernetes 中用于存储非敏感配置数据的资源对象。它可以将配置信息(如环境变量、配置文件、命令行参数等)与应用程序代码分离,从而实现配置的集中管理和动态更新。

也可通过yaml文件创建:deploy/k8s/ConfigMap.yaml
configMap会以环境变量的方式,写入pod的容器内,在代码里还是使用
env('APP_NAME', 'MineAdmin'),
方式获取配置文件信息
创建Deployment

创建镜像仓库账号密码
配置启动命令

bash
/bin/sh -c 'tar -zxf mineadmin.tar.gz . && php bin/hyperf.php start'
配置服务端口
项目启动了哪些服务就填写对应的端口

配置探针
启动探针,用于判断容器是否启动成功。
存活探针
就绪探针
每个探针都有自己所属的功能和应用场景,根据实际情况来选择。一般配置存活探针、就绪探针就可以保证服务的正常运行
配置文件存储
将容器内的logs目录挂载到宿主机的目录下,方便后续的日志采集或者查看日志
创建service

启动deployment 启动成功:

配置Ingress
由于我们是在本地环境下部署的,所有得在本机hosts上添加域名。 C:\Windows\System32\drivers\etc\hosts


在生成环境中,前面还有一层高可用的 LBS做负载均衡。流量路径:用户请求->DNS->lbs->ingress->service->pod->服务
遇到的问题
在创建deployment过程中,会遇到各种各样的问题,以下是一些解决思路:
1、首先保证项目镜像是正常,能正常启动的。可以在agent节点,把镜像拉取下来,创建一个临时容器,看项目是否能够正确启动。
bash
docker -it --namne test1 registry.cn-shenzhen.aliyuncs.com/byd_dev/mineadmin:250820150754
#创建临时容器
docker run -it --namne test1 registry.cn-shenzhen.aliyuncs.com/byd_dev/mineadmin:250820150754
#解压启动
tar -zxf mineadmin.tar.gz . && php bin/hyperf.php start
项目的镜像正常了,到了k8s不正常,怎么排查?
初始化容器时,先不启动服务,先执行sleep 3600。 这样子容器就启动成功了,然后可以手动的进入容器,检查是什么原因导致服务启动失败的

bash
#k8s使用的是containerd,命令跟docker一致,只不过docker 换成了 crictl
#有可能 mineadmin-f6dcd85c9 部署在server 或 agent 两边都查一下
crictl ps -a | grep mineadmin-f6dcd85c9
#进入容器
crictl exec -it {CONTAINER} /bin/bash
进入容器之后,首先查看
- 代码是否打包成功、解压出来的文件是否有缺失
- configMap 是否写入容器的环境变量里。执行
env
查看容器环境变量 - 然后尝试启动服务看服务是否正常启动。
php bin/hyperf.php start
启动成功之后,退出容器,回到管理后台,更新回正常的启动命令,然后更新Deployment


拉取镜像失败
分别在 server agent 节点 创建 /etc/rancher/k3s/registries.yaml 文件
bash
sudo tee /etc/rancher/k3s/registries.yaml > /dev/null <<'EOF'
mirrors:
"docker.io":
endpoint:
- "https://这里填自己的docker加速地址"
"k8s.gcr.io":
endpoint:
- "https://registry.aliyuncs.com/google_containers"
- "https://gcr.mirrors.ustc.edu.cn"
EOF
arduino
#agent节点 重启k3s-agent
sudo systemctl restart k3s-agent
#server节点 重启k3s
sudo systemctl restart k3s
总结
到此完成了代码镜像的构建、k8s的安装与部署,以及代码的发布。该方案还有许多完善的地方,比如:
- 构建脚本.sh 可以更换成 Jenkins ,实现CICD自动发布
- 多个节点下服务日志的收集。需要使用ELK进行收集与查询
生成环境下,建议使用各个云产商的k8s服务,毕竟开发人员不是专业的运维人员,k8s的运维还是比较复杂的。比如阿里云的ACK功能,后面有时间的话,会把Jenkins加上。