从 0 到 1 构建一体化研发运维平台:架构、核心实现与踩坑实录

作者:青云 发布时间: 2026-11-07 阅读量:29 评论数:0

技术栈:Spring Boot 3.5 + Vue 3 + MySQL + Redis + Docker + Nginx 关键词:多环境发布、蓝绿零停机、SSH 远程执行、RBAC 数据权限、实时监控、审计日志


一、项目背景

团队里服务器、项目、环境、发布、监控、权限、审计分散在多个工具里:服务器靠 Excel 记录,发布靠手工 scp + docker run,出问题靠人肉登机器看日志。目标很明确——用一个平台把这些动作收敛起来:

  • 服务器管理:统一登记、SSH 连通性检测、Web 终端

  • 项目与环境管理:一个项目多套环境(DEV/TEST/PROD),每套环境绑定服务器、端口、实例数

  • 发布中心:从 Git 拉代码 → 构建镜像 → 多实例部署 → 负载均衡 → 一键回滚

  • 监控大盘:实例状态、CPU/内存、告警、容器日志

  • 权限与审计:角色 + 数据范围,所有敏感操作留痕

下面按「技术选型 → 架构 → 核心实现 → 踩坑」的顺序展开。


二、技术选型

层次

技术

选择理由

后端框架

Spring Boot 3.5.14 / Java 17

团队熟悉,生态完整,JDK 17 长期支持

ORM

MyBatis-Plus 3.5.5

单表 CRUD 零 SQL,复杂查询仍可写 XML

数据库迁移

Flyway

版本化 DDL,换环境不用手工导库

缓存/锁

Redis 7

登录失败计数、发布环境互斥锁

认证

JWT + 自定义过滤器

前后端分离,无状态

远程执行

JSch

纯 Java SSH,可复用密码/私钥认证

接口文档

SpringDoc + Knife4j

注解即文档,联调成本低

前端框架

Vue 3.5 + Vite 6

组合式 API,构建快

UI

Element Plus 2.9(按需引入)

后台系统组件齐全

状态

Pinia + persistedstate

主题、用户信息持久化

终端

xterm.js 6 + addon-fit

浏览器里直接开 SSH 会话

样式

SCSS + CSS 变量双主题

默认浅色 + 深色赛博朋克


三、整体架构

┌────────────────────────────┐        ┌─────────────────────────────┐
│  Vue 3 SPA (Vite)          │  REST  │  Spring Boot 3 (pl-platform)│
│  Element Plus / Pinia      │ <────> │  Controller → Service → Mapper
│  xterm.js 终端             │  WS    │  Security / JWT / 审计切面   │
└────────────────────────────┘        └───────────┬─────────────────┘
                                                  │ SSH(JSch) / WS
                                    ┌─────────────▼─────────────┐
                                    │  MySQL 3307   Redis 6380  │
                                    └───────────────────────────┘
                                                  │
                        ┌─────────────────────────▼────────────────────────┐
                        │ 目标服务器 node1 / node2(Docker + Nginx)          │
                        │ 应用容器 ×N  +  负载均衡容器 ×1                     │
                        └──────────────────────────────────────────────────┘

后端按 controller / service / service.impl / mapper / entity / dto / config / security 分层,共 9 个 Controller、15 个核心 Service 实现;数据库通过 10 个 Flyway 脚本从零建到 V10。


四、核心实现

4.1 RBAC + 数据范围

权限模型是「用户-角色-权限点」三层,角色额外带一个 数据范围:

  • ALL:全部数据

  • PROJECT:本人负责的项目及其发布单

  • SELF:仅本人创建的数据

落地方式是 DataScopeService 统一给出 Scope 与可见 projectIds,各业务 Service 在拼查询条件时统一调用,避免每个模块各写一套判断:

DataScopeService.Scope scope = dataScopeService.scope(userId);
if (scope == Scope.SELF) {
    query.eq(SysRelease::getOperatorId, userId);
} else if (scope == Scope.PROJECT) {
    query.in(SysRelease::getProjectId, dataScopeService.projectIds(userId));
}

同时做了几条硬约束:

  1. 超级管理员不可编辑、不可删除、不可停用、不可重置密码、不可移除超管角色;

  2. 非超管给他人授权时,只能授予自己已拥有的权限点,否则 403;

  3. 登录失败 5 次锁定 15 分钟,用 Redis 计数,Redis 异常时降级放行,避免缓存故障导致全员登不上。

4.2 SSH 凭据加密与 Web 终端

服务器的 SSH 密码/私钥不能明文入库,落地为 AES-GCM 加密列(CryptoService),并遵循两条规则:

  • 列表接口只返回「是否已配置」,不回显密文;

  • 编辑弹窗需要回显时,走单独接口 GET /api/servers/{id}/credentials,且要求 server:update 权限并写审计日志。

前端用统一的 SensitiveField 组件:默认显示 *,点「查看」后切换明文并可编辑,避免敏感信息长期暴露在屏幕上。

Web 终端基于 WebSocket + JSch:前端 xterm.js 负责渲染与尺寸同步(addon-fit),后端 SshTerminalHandler 把 PTY 输出流式推回,输入直接写进 channel。

4.3 发布引擎:蓝绿切换做到零停机

这是整个项目最复杂的一块。最初版本是「删旧容器 → 起新容器」,实测会有几秒 502;改造后流程如下:

阶段1  逐台服务器:git 拉取 → docker build
阶段2  起新实例:换一组端口(基础端口 +100/+1000/+2000 动态选空闲槽),
       容器打标签 pl.project / pl.env / pl.release,等端口就绪
阶段3  切负载均衡:重写 nginx upstream → nginx -t → nginx -s reload(复用 LB 容器)
阶段4  排空 10 秒后:按标签清理非当前 release 的旧容器

几个关键设计点:

① 端口选择不能写死。 新旧实例要同时存活,必须让新实例先起在另一组端口。用 docker ps --format '{{.Ports}}' 采集实际占用,再选 0 / +1000 / +2000 里第一个在所有目标服务器上都空闲的槽位:

for (int offset : new int[]{0, PORT_SLOT_OFFSET, 2 * PORT_SLOT_OFFSET}) {
    // 逐个端口检查是否被 ":{port}->" 占用
}

② 就绪判定用 TCP 探测而非 sleep。 端口能连上才进入下一阶段,起不来就直接失败并打日志,旧实例继续服务:

for i in $(seq 1 90); do (echo > /dev/tcp/127.0.0.1/$PORT) && { READY=1; break; }; sleep 2; done

③ 容器用标签做代次识别。 实例名带上发布单号(pl-<code>-<env>-<releaseId>-<n>),清理时按 pl.release 标签保留当前代次,避免靠名字正则误删(早期用子串匹配 name=pl-xxx-dev-,把 -lb 负载均衡容器一起删了)。

④ 失败要能收拾残局。 切换 LB 之前任何一步失败,只删「本次发布单」启动的容器(按 pl.release 标签),旧实例不受影响;切换成功后才允许进清理阶段。

⑤ 并发发布互斥。 两个人同时点发布,两套流程会互相清理对方的容器,最后可能把 LB 指向的实例删掉。因此在「查是否有进行中发布 + 落库」外面套了 Redis SETNX 环境锁(TTL 30s),Redis 不可用时只告警不阻塞。

⑥ 旧实例留 10 秒排空。 nginx -s reload 后旧 worker 还在处理在途请求,立刻 rm -f 会有瞬时 5xx,所以统一等待 10 秒再清理。

发布日志按秒落库,前端 1 秒轮询,抽屉打开即自动滚到底部。

4.4 监控采集:从「假在线」到真实探测

监控大盘最初两个指标是“糊”的:

  • 「在线服务器」其实是 status=1 的启用数量,SSH 连不上也照样算在线;

  • 实例状态按「环境实例数 + 命名规则」推导,发布改造后实例名带了发布单号,导致真实容器被判成 MISSING。

修正方案:

  1. sys_server 增加 online、last_check_at(Flyway V10),每轮采集先对启用服务器执行 docker info 做真实连通探测;

  2. 实例发现改为按 Docker 标签查询(pl.project + pl.env),LB 容器用 -lb 后缀识别;

  3. 采集结果写 sys_monitor_instance,状态从 RUNNING 变为异常时生成告警;不再存在的实例记录自动清理;

  4. 页面展示「实例数据采集于 xx · 服务器连通探测于 xx」,把 30 秒采集周期摆到明面上,避免用户误判为实时。

前端侧还有两个体验问题顺手修了:

  • 轮询请求全部带上 hideLoading,否则全局 loading 遮罩每 2 秒闪一次,看起来像“一直加载中”;

  • 日志抽屉只在首次加载显示局部 loading,之后静默刷新并自动滚到底部。

4.5 审计日志

所有敏感操作(改密码、查看凭据、发布、回滚、导出)统一走审计切面,记录操作人、IP、参数、耗时、结果。两个细节:

  • 参数中的密码/令牌统一替换为 ***;

  • IPv6 回环地址统一展示为 127.0.0.1,便于检索。

查询支持多条件组合与 CSV 导出(导出单独走 audit:export 权限点)。


五、踩坑实录

5.1 菜单点击后自动收起

原因:侧边栏用了嵌套 el-menu,子菜单被重新创建导致展开态丢失。改法是只保留一个 el-menu,用递归组件 MenuTree.vue 渲染层级。

5.2 切换菜单整页刷新

一开始怀疑 Vite 依赖预构建,实际根因是 App.vue 顶层的 <router-view :key="route.path">——key 变化导致整个布局销毁重建。把 key 下移到布局内部的 <router-view>,只重建内容区;同时在 vite.config.js 的 optimizeDeps 里预声明所有 Element Plus 组件,避免开发态依赖重载。

5.3 切换主题后表格斑马纹出现白底残影

Element Plus 的斑马纹用的是半透明背景色,Chromium 切主题时不重绘单元格背景。方案是新增不透明主题变量 --app-table-stripe,在 element-overrides.scss 统一覆盖。

5.4 服务器是 CentOS 7 + git 1.8.3,私有仓库拉不下来

1.8.3 不支持 http.extraHeader 认证,git clone 直接报 could not read Username。改用 GIT_ASKPASS 临时脚本(mktemp + chmod 700 + trap 退出删除),令牌不出现在命令行和日志里。

5.5 Docker Hub 与 Maven 中央仓库太慢

  • Docker:写 /etc/docker/daemon.json 配置国内 registry mirror 并重启;

  • Maven:构建镜像时注入阿里云 settings.xml,依赖下载从 ~10KB/s 提到 ~1MB/s。

5.6 代码明明推了,发布却一直是旧版本(最隐蔽的一个)

发布日志里 git rev-parse HEAD 始终停在仓库最初提交,但 git ls-remote origin master 能看到新提交。根因是服务器上缓存的仓库缺少 remote.origin.fetch refspec,git fetch --all 只写 FETCH_HEAD,origin/master 永远不更新,checkout -B master origin/master 每次都回到旧提交。

修复:

git fetch origin --tags --prune '+refs/heads/*:refs/remotes/origin/*'

并在发布日志里常驻打印「当前 HEAD / 远端 master」,以后代码没更新能一眼看出。

5.7 编译通过却运行报 Unresolved compilation problems

mvn compile | tail -8 拿到的退出码是 tail 的而不是 mvn 的,导致误判成功,实际用 IDE 生成的错误 class 在跑,抛 java.lang.Error,catch (Exception) 又抓不到,发布单卡在 RUNNING。教训是校验构建结果必须重定向到文件并显式取 $?。


六、部署与验证

本地/测试环境用 Docker 跑 MySQL(3307)、Redis(6380),后端 8080,前端 Vite dev 5173 或构建为静态资源由 Nginx 托管。Flyway 在启动时自动把库迁到最新版本,无需人工干预。

线上验证方式值得一提:单独做了一个 pl-demo 项目作为“被发布对象”,提供 5 个开放接口:

  • GET /api/demo/ping:连通性探测

  • GET /api/demo/info:返回 instanceId、hostIp、容器 IP,用来验证多实例与负载均衡

  • GET /api/demo/time、POST /api/demo/echo、GET /api/demo/divide

平台侧再把 PL_HOST_IP、PL_INSTANCE_ID 注入容器,配合 /api/demo/info 的调用日志,就能直观验证「请求落到哪台服务器哪个实例」以及监控日志的实时刷新。

一次 1.0.12 发布实测:两实例(node1/node2)、LB 轮询正常,切换期间以 0.3 秒间隔连续探测 278 次全部返回 200,旧容器按标签清理干净。


七、总结

这个平台的价值不在“功能堆了多少”,而在于把几件容易出错的事做成了确定性流程:

  1. 发布可回滚:镜像按版本打 tag,回滚复用已有镜像,不做重复构建;

  2. 切换无中断:端口槽位 + 就绪探测 + upstream reload + 排空等待;

  3. 状态可自证:实例名、Docker 标签、instanceId、hostIp 都能反查请求落在哪;

  4. 权限收得住:数据范围 + 敏感字段加密 + 审计留痕。

后续可以继续做的方向:把发布流水线拆成可编排阶段(支持自定义构建命令)、监控指标落历史表做趋势图、以及把单机 SSH 执行替换为 Agent 模式,减少对 SSH 稳定性的依赖。

评论