拓冰建站拓冰建站
首页 / 资讯中心 / 正文

自建状态监控面板Status Deck:技术选型与全栈实施记录

我手里那台小服务器上跑的东西越来越多两台树莓派、一个NAS、三个定时爬虫脚本还有Nginx、MariaDB、RSS订阅服务。以前每次想确认服务是否正常都是挨个SSH登上去看日志来回跑一圈小半个小时没了。所以我决定自己做一个状态监控面板把全屋设备和在线服务统一放到一个页面上随时一屏看清。这个项目我取名叫Status Deck这是系列文章的第二篇上一回讲了整体规划和原型验证这一篇直接聊技术栈选型是怎么定下来的以及项目实施阶段怎么一步步落到可运行的状态。熟悉我的读者应该知道我写技术文章不太喜欢直接甩结论更愿意把“为什么这么选”讲清楚。Status Deck这类个人全栈项目技术选型的好坏直接决定后面的开发效率和长期维护成本。选对了越做越顺手选错了后期重构的代价比重新做一个还高。所以这篇文章我会重点拆解每一步决策背后的原因也会把实施过程中踩过的坑和排查思路原样记录下来。如果你也在做类似的个人监控项目、或者正准备从零搭一个全栈应用那这篇应该能帮你在选型阶段少走不少弯路。1. 项目定位与需求拆解1.1 我需要解决什么在做技术选型之前我先把Status Deck要解决的问题写清楚这个步骤非常关键。简单来说我需要一个“一屏状态总览”所有被监控的服务和设备的运行状态、响应时间、历史趋势都要在一个页面里完成展示不需要再分别登录到各个设备上去看日志。于是我把需求拆成了几条核心功能一页展示所有服务与设备的在线状态、延迟、最近一次检查时间保留历史数据支持查看最近1小时、24小时、7天的趋势曲线允许自定义数据源上报比如树莓派的温度传感器、下载机的磁盘占用率都可以主动推送到后端能手机浏览器直接访问最好可以“加到桌面”当轻量App用整个系统部署在一台1核1G的小VPS上内存占用必须低重启不丢数据备份迁移简单这个清单列完以后我对项目的边界就非常清楚了这不是要做一个企业级监控平台撑死算一个“个人状态控制台”。也因此很多企业级技术方案在候选清单里直接被划掉比如K8s、微服务、独立时序数据库。1.2 为什么没有直接套现成监控平台肯定有人会问Prometheus加Grafana、或者Uptime Kuma这类开源工具已经很成熟为什么还要从零自造这个问题我在动手前也认真想过。现成方案确实能解决“监控需求”但它们解决不了“我对信息展示的掌控欲”。Grafana的Dashboard确实漂亮但定制一块符合自己审美和信息密度的面板需要学一堆插件和配置语法Uptime Kuma更轻但监控纬度偏Web探活我想监控设备CPU温度、脚本运行时长、磁盘占用等自定义指标扩展起来反而要写不少插件代码。自己造的好处有三个。第一完全按自己的信息消费习惯来组织界面哪些指标放第一屏、哪些放二级页都是自己说了算。第二整个数据链路从采集、存储到展示全部可控不依赖第三方SaaS也不怕某天服务商改版或者收费。第三这个项目本身就是一个全栈学习载体从前后端到部署运维每一个环节都亲手淌一遍对能力的提升远大于配置一个现成面板。1.3 需求清单固定后技术选型豁然开朗很多人选型纠结本质上是需求没定清楚。需求清单一旦写成上文那种分类条目技术选型就变成了“找工具去满足需求”的匹配题而不是“哪家方案更酷”的比拼。我当时的选型原则就三条第一单机能跑资源占用要低第二部署和备份要简单不能依赖复杂运维第三技术栈要让自己开发起来顺手毕竟晚上和周末的时间都投在这里兴趣不能变成负担。顺着这三条原则前端、后端、数据库、部署方式的人选其实很快就浮出水面。2. 技术栈选型背后的考量2.1 前端Vue 3 Vite TypeScript前端这块我没有过多犹豫直接定了Vue 3配合Vite构建和TypeScript。理由不是“Vue比React好”而是这个项目的具体场景里Vue的响应式心智模型非常契合“状态面板”这种高频刷新数据的界面。Vue 3的组合式API可以把轮询逻辑、数据处理、组件生命周期管理拆得很干净。比如我需要一个全局的轮询定时器用setup语法只要在布局组件里写onMounted和onUnmounted就行每个页面组件只负责展示数据不用各管各的刷新逻辑。这比我在某些项目里看到的“每个组件各自setInterval”清爽太多了。TypeScript在这个项目里的价值比在业务系统里更明显。监控系统的核心就是数据展示前后端要约定一堆指标字段。有了TS我可以在前端用接口类型直接约束后端返回的数据结构联调时字段拼错会立刻编译报错这种体验比裸JavaScript强太多。组件库和图表库我选了Naive UI和ECharts。Naive UI走的是极简风格不重ECharts做折线图和饼图非常成熟配置项灵活状态历史曲线这种基础场景完全够用。实际上状态面板里的图表大多是折线图ECharts的dataZoom和tooltip开箱即用省了不少手工实现图表交互的时间。2.2 后端Go Gin后端选Go理由很直接编译出来是一个静态二进制文件直接扔到服务器就能跑不需要像Node或者Python那样在目标机器上搭运行时环境。哪怕要部署到树莓派也是交叉编译完scp上去就行非常舒服。Go的并发模型也适合这种“要同时去探活多个目标”的场景。我每个被监控的服务可以看作一个拉取任务用goroutine并行发HTTP请求和传统多线程编程相比代码写起来简单得多也不容易出资源泄漏问题。Gin是我选的HTTP框架它很轻自带路由分组、中间件和JSON绑定。Status Deck的API规模不大总共就十来个端点Gin这种重量级正好不需要引入更重的东西。可能有人会质疑既然只用HTTP标准库不也够吗确实够但Gin的路由和参数绑定确实能少写不少样板代码这点便利没必要排斥。后端架构上我坚持了一个铁律保持单进程所有模块按功能拆包。采集、存储、API、调度各干各的靠内部接口通信。个人项目最怕架构上给自己挖坑单进程好部署、好调试、好备份等到真需要水平扩展的时候再说。2.3 数据层SQLite为何够用数据库选型我一度纠结过因为网上都在说时序数据就应该上InfluxDB或者TimescaleDB。但后来我算了一笔账直接把自己劝退了。按每10秒记录一轮所有服务状态、每轮产生50个指标点来算一天大概是43万条记录。这个数量听起来多实际上每条记录就是时间戳、目标名、指标名、数值这几个字段单条不超过几十字节。SQLite单文件数据库对这种量级的数据完全能支撑最关键的是它零运维——不用装服务、不用配账号、不用管端口数据就是一个文件备份直接复制走人。我只需要注意一点高频写入不能傻写。同一时刻只允许一个写者如果采集器每个指标都立刻往SQLite里插一条很快就会出现锁冲突。所以我在存储层做了批量写入设计这个细节会在后面第3节详细展开。如果未来数据量真的涨到SQLite扛不住迁移路径也不难。写入层和查询层我留了接口到时候把store实现替换成Postgres或者真正的时序库API层完全不用动。2.4 部署层Docker Compose systemd部署方案我直接选了Docker Compose用systemd做守护。后端容器、前端静态资源容器、反向代理容器放在同一个compose项目里彼此通过内网通信。个人项目不碰K8s理由非常简单Compose的配置一次写完基本不用动K8s那一套YAML和运维成本在一个只有三四个容器的项目里纯属浪费。systemd的作用是保证“重启后一切自动恢复”。我需要的是一个稳定跑在我小VPS上的服务而不是一个需要我每天记着手动拉起的玩具。所以在compose之外加了一个unit文件把docker compose up和down包成系统服务设置好Restartalways配合开机自启整台机器重启我也不用操心。部署层的最后一个组件是Caddy负责TLS证书和反向代理。Caddy相对于Nginx最爽的地方是自动申请和续期HTTPS证书配置几行就能搞定省去手动维护证书的麻烦。3. 项目实施核心模块从零到可运行3.1 仓库结构与初始化项目采用monorepo结构一个Git仓库管理所有代码发布时打一个tag。这样做的好处是前后端改动可以同步看到代码评审和版本追溯都简单。我整理了一下最终目录结构你参考的时候可以按自己的情况增删status-deck/ ├── backend/ │ ├── cmd/server/main.go │ ├── internal/ │ │ ├── collector/ // 采集器 │ │ ├── store/ // 存储层 │ │ ├── api/ // HTTP API │ │ └── scheduler/ // 调度器 │ └── go.mod ├── frontend/ │ ├── src/ │ │ ├── api/ │ │ ├── stores/ │ │ ├── views/ │ │ └── components/ │ └── package.json ├── deploy/ │ ├── docker-compose.yml │ └── Caddyfile └── docs/前端构建完的静态文件由nginx容器托管后端只暴露API不直接伺服前端静态资源。这个决策能让静态资源走独立的缓存策略API和页面分离将来如果前端要加PWA或者换框架后端一行不用改。3.2 后端三大核心模块采集、存储、API后端最核心的模块是采集器。我定义了一个Target结构体用来描述任何一个被监控对象type Target struct { Name string json:name Endpoint string json:endpoint Interval time.Duration json:interval Timeout time.Duration json:timeout Type string json:type // http / tcp / ping / push }对HTTP类型的Target采集器按设定的Interval发起GET请求记录响应状态码和耗时对TCP类型的则直接尝试建立连接对push类型的Target它不主动拉取而是等外部脚本通过API上报数据。采集结果统一打包成Metric结构体交给存储层。存储层我做了一个批量写入机制。每个采集周期产生的指标先进内存buffer等攒够100条或者过了5秒才开一个事务批量插入SQLite。这样SQLite的写入频率从“每10秒写几十次”降到了“每5秒写一次”锁冲突的概率大幅下降。如果你直接上手做一个类似的存储层这块建议直接照这个思路抄作业。API层我设计了三个核心端点GET /api/v1/overview返回所有目标的最新状态首页一进来就请求这一个接口不用分别加载每个卡片GET /api/v1/history?targetxxxrange1h返回某个目标在一段时间内的时序数据供趋势图表使用POST /api/v1/push接收外部脚本主动上报的自定义指标请求头带token校验身份3.3 前端页面与状态管理前端页面整体分三块顶部是全局状态栏和手动刷新按钮中间是服务卡片区每个目标一张卡片显示在线状态、响应延迟和最近更新时间底部是趋势区点击任意卡片后显示对应目标的历史折线图。状态管理用Pinia存的不是一大堆组件各自维护的临时数据而是全局唯一的Targets列表、最新指标集和加载状态。轮询逻辑放在布局组件里统一处理每10秒拉一次overview接口然后更新Pinia内的状态。这里有一个实战细节值得单独说轮询定时器一定要在onUnmounted里清理否则组件切走了定时器还在跑白白增加后端压力。我第一版就踩过这个坑切了几个页面之后浏览器卡得不行打开DevTools一看全是定时器。卡片区我用了一个Grid网格布局每个卡片显示四类信息在线状态的点色、服务名称、最近一次响应耗时、最后检查时间。点击卡片后触发趋势区加载该目标的历史数据用ECharts渲染出来。整个页面只依赖overview和history两个接口数据交互模型非常清晰。3.4 对接自定义数据源Status Deck和常规监控工具最大的差异点是它不只能主动探活还能被动接收外部脚本上报数据。我的树莓派传感器、下载机磁盘脚本都是通过这种方式接入的。接入方式非常简单后端提供了一个push端点curl -X POST https://status.example.com/api/v1/push \ -H Authorization: Bearer YOUR_TOKEN \ -d { target: raspi-sensor, metrics: [ {name: temperature, value: 36.5, unit: c}, {name: humidity, value: 42.1, unit: %} ] }树莓派上跑一个cron脚本每5分钟执行一次传感器读取把结果POST上来。后端拿到数据后直接存储不关心数据是怎么产生的。这样的接口设计让系统扩展性非常强以后想接入任何设备只要写一个几行的脚本就能搞定。4. 部署方案与日常运维4.1 Compose编排与镜像体积控制部署编排我用了一个三服务的compose文件backend、frontend、caddy。我精简一下给你看关键结构version: 3.8 services: backend: build: context: ../backend dockerfile: Dockerfile restart: always volumes: - ./data:/app/data environment: - DB_PATH/app/data/status.db expose: - 8080 frontend: build: context: ../frontend dockerfile: Dockerfile restart: always expose: - 80 caddy: image: caddy:2-alpine restart: always ports: - 80:80 - 443:443 volumes: - ./Caddyfile:/etc/caddy/Caddyfile - caddy_data:/data depends_on: - backend - frontend volumes: caddy_data:镜像体积这块我专门优化过。后端用多阶段构建编译阶段用golang:1.22镜像运行阶段用alpine最终镜像只有30MB左右。前端用nginx:alpine托管构建出来的静态资源顺便开了gzip。镜像小上传、拉取、启动都快在低配VPS上尤其明显。4.2 systemd守护与自动重启Docker Compose本身有restart: always策略但那是Docker守护进程层面的策略。我更进一步用systemd来守护“docker compose up”这个整体操作这样不管是compose里哪个容器挂了还是整台服务器重启了都能自动恢复。systemd unit文件我放在了/etc/systemd/system/statusdeck.service[Unit] DescriptionStatus Deck Service Afterdocker.service Requiresdocker.service [Service] WorkingDirectory/opt/status-deck ExecStart/usr/bin/docker compose up ExecStop/usr/bin/docker compose down Restartalways RestartSec10 [Install] WantedBymulti-user.target设置好以后执行systemctl enable statusdeck --now服务就托管给了systemd。日常升级的流程也很顺畅改代码后重新构建镜像然后systemctl restart statusdeck几个容器一起重启所有服务无缝切换。4.3 安全加固与备份个人项目暴露到公网之后安全那根弦不能松。我的做法分三层。第一层Caddy自动申请TLS证书所有访问都是HTTPS中间人攻击的问题直接免掉。第二层状态面板本身加了一层Basic Auth虽然这个项目的敏感度不高但不想随便什么人都能看到我家里设备的运行状态。第三层push接口必须带Bearer Tokentoken在后端环境变量里配置不写死在代码仓库。备份策略我坚持“一个命令能恢复”的原则。SQLite的数据文件在deploy/data目录下我写了一个cron脚本每天凌晨3点用sqlite3 .backup导出一致性快照再tar压缩一份放到备份目录同时通过rclone同步到对象存储。整个备份过程不到一分钟跑了大半年了一次没出过问题。5. 踩坑记录与排查思路5.1 SQLite并发写导致服务间歇性无响应第一个大坑是SQLite的database is locked。现象很好判断API偶发超时后端日志里不断刷锁错误。排查后发现原因是我犯了“多连接高频写”的错误采集器每10秒写一轮前端历史查询也在同时读两个连接撞在一起SQLite默认的写锁策略扛不住。解决方法是三件套。第一开启WAL模式让读写并发度更高第二把busy_timeout设为5000毫秒遇到锁时等待而不是立刻报错第三写入统一走一个单写者goroutine所有指标先进buffer由它负责批量落盘。这三点改完以后database is locked再也没出现过。你如果用SQLite做类似的存储这几个PRAGMA建议直接加上PRAGMA journal_modeWAL; PRAGMA busy_timeout5000; PRAGMA synchronousNORMAL;5.2 Go交叉编译到ARM的CGO坑我的树莓派是ARM架构最初想直接在上面跑后端程序于是在开发机上执行GOOSlinux GOARCHarm64 go build。结果编译出来的二进制一运行就崩查了一圈发现是SQLite驱动的问题。我一开始用的驱动是mattn/go-sqlite3这个库依赖CGO交叉编译时CGO_ENABLED被默认关闭链接就断了。解决办法是换用纯Go实现的SQLite驱动modernc.org/sqlite这个驱动不依赖CGO交叉编译非常干净。换完驱动后CGO_ENABLED0交叉编译ARM64版本一切正常。如果你也跑树莓派相关的Go项目这个坑大概率会碰到提前知道能省大半天排查时间。5.3 Vue响应式失效与定时器泄漏前端也遇到过两个典型问题。第一个是响应式失效后端返回新的服务列表后页面上的卡片状态要么不更新要么更新了一部分。排查后发现是我直接改数组下标导致的Vue 3的响应式代理在这种写法下不是总能触发视图更新。解决方法是改用整体替换的方式每次接口返回数据后重新生成一个新的对象数组再一次性赋值给响应式状态。这个方法粗暴但有效从根源上避免了响应式追踪的边界问题。第二个问题是定时器泄漏。最初我做轮询时把定时器放在了每个卡片组件内部结果开了十个卡片就有十个定时器在轮询而且组件销毁时定时器没有清理导致泄漏。后来我把轮询提升到根布局组件全局只保留一个定时器组件卸载时统一clear前端资源占用立刻降了下来。5.4 Nginx反向代理下SSE被缓冲第一版的前端更新靠轮询10秒刷一次。后来我做了个优化想把更新频率降到秒级于是引入了SSEServer-Sent Events。后端用SSE推送服务状态变化前端通过EventSource监听实时刷新。效果很好数据延迟降到了1秒以内但上线后发现页面还是半天才收到一次新数据。排查到最后锁定到反向代理层的缓存问题。Nginx默认会缓冲代理响应SSE这种流式响应卡在缓冲里前端自然等不到新数据。解决办法是在Nginx的location里加proxy_buffering off;同时在后端响应头带上X-Accel-Buffering: no。Caddy也类似需要在reverse_proxy里配置flush_interval -1让流式响应即时转发。这个坑属于“本地永远复现不了、一上公网就出问题”的典型比较隐蔽特别值得记一笔。6. 调试与优化的一些实战经验6.1 给自己的系统加指标端点一个做监控的系统自己居然没有一个“健康检查”入口这事儿我一开始还真忘了。后来有一次前端报错我登录服务器看日志发现后端也是正常跑着的顿时觉得不对劲——系统本身的状态我居然没有观测手段。于是我在后端加了一个/metrics端点用最简单的JSON格式返回几组关键信息goroutine数量、SQLite连接数、最近一次采集耗时、成功率、总内存占用。前端只在调试模式下调这个接口平时我用curl看。这个设计虽然简单但遇到“系统今天好像卡了”这类问题先curl一下数据类型立刻就有不用靠猜。6.2 数据降采样与存储精简SQLite虽然扛得住43万条一天但时间长了文件还是会膨胀。我加了一个每日执行的降采样任务逻辑是1小时前的数据按10分钟粒度归档24小时前的按1小时粒度归档7天前只保留每天的最大、最小、平均值。这样历史数据保留完整趋势但文件体积被压住现在运行了大半年数据库文件稳定在几十MB以内。降采样任务用Go的定时器实现每天凌晨跑一次跑完会往日志里写一行统计。这个任务虽然不复杂但对长期运行的监控类项目来说很有必要不加的话SQLite文件总有一天会变成一个难以备份的大块头。6.3 后续方向告警通道与多端体验Status Deck目前已经稳定跑了大半年接下来的扩展方向也基本想好了。第一个是告警通道如果某个Target连续三次超时就通过Webhook推送到飞书或者Telegram这样不用随时盯着页面也能第一时间知道异常。第二个是AI辅助分析现在历史数据都攒在SQLite里未来可以接一个大模型做自然语言总结比如让它分析“过去一周CPU趋势为什么每天凌晨都有一个尖峰”这种能力很适合配合现有的监控数据实现。多端体验方面我现在用PWA已经把面板“加到桌面”当轻量App用了后续如果需求更重可以用uniapp套一层壳打包成真正的移动App。接口层面都留好了前端框架换掉也不会影响后端。我个人做这个项目最大的体会是技术选型没有绝对好坏只有合适不合适。Status Deck的整个技术栈——Vue 3、Go、SQLite、Docker Compose——都不是什么新潮东西但组合在一起在一台1G内存的小VPS上跑得非常稳。你如果也想自造一个类似的状态面板建议先像我这样把需求清单写到纸面上再对着清单选技术最后动手实施时会发现每一步都有据可依而不是凭感觉拍脑袋。另外一个小建议是从第一天就加上指标端点和备份脚本这两件事后期补的成本远高于一开始就做。希望这篇选型与实施的记录对你有点帮助。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门