news 2026/9/26 1:13:33

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
自建状态监控面板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包成系统服务,设置好Restart=always,配合开机自启,整台机器重启我也不用操心。

部署层的最后一个组件是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?target=xxx&range=1h:返回某个目标在一段时间内的时序数据,供趋势图表使用
  • 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] Description=Status Deck Service After=docker.service Requires=docker.service [Service] WorkingDirectory=/opt/status-deck ExecStart=/usr/bin/docker compose up ExecStop=/usr/bin/docker compose down Restart=always RestartSec=10 [Install] WantedBy=multi-user.target

设置好以后执行systemctl enable statusdeck --now,服务就托管给了systemd。日常升级的流程也很顺畅:改代码后重新构建镜像,然后systemctl restart statusdeck,几个容器一起重启,所有服务无缝切换。

4.3 安全加固与备份

个人项目暴露到公网之后,安全那根弦不能松。我的做法分三层。

第一层,Caddy自动申请TLS证书,所有访问都是HTTPS,中间人攻击的问题直接免掉。第二层,状态面板本身加了一层Basic Auth,虽然这个项目的敏感度不高,但不想随便什么人都能看到我家里设备的运行状态。第三层,push接口必须带Bearer Token,token在后端环境变量里配置,不写死在代码仓库。

备份策略我坚持“一个命令能恢复”的原则。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_mode=WAL; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;

5.2 Go交叉编译到ARM的CGO坑

我的树莓派是ARM架构,最初想直接在上面跑后端程序,于是在开发机上执行GOOS=linux GOARCH=arm64 go build。结果编译出来的二进制一运行就崩,查了一圈发现是SQLite驱动的问题。

我一开始用的驱动是mattn/go-sqlite3,这个库依赖CGO,交叉编译时CGO_ENABLED被默认关闭,链接就断了。解决办法是换用纯Go实现的SQLite驱动modernc.org/sqlite,这个驱动不依赖CGO,交叉编译非常干净。换完驱动后,CGO_ENABLED=0交叉编译ARM64版本一切正常。如果你也跑树莓派相关的Go项目,这个坑大概率会碰到,提前知道能省大半天排查时间。

5.3 Vue响应式失效与定时器泄漏

前端也遇到过两个典型问题。第一个是响应式失效:后端返回新的服务列表后,页面上的卡片状态要么不更新,要么更新了一部分。排查后发现是我直接改数组下标导致的,Vue 3的响应式代理在这种写法下不是总能触发视图更新。解决方法是改用整体替换的方式,每次接口返回数据后重新生成一个新的对象数组,再一次性赋值给响应式状态。这个方法粗暴但有效,从根源上避免了响应式追踪的边界问题。

第二个问题是定时器泄漏。最初我做轮询时,把定时器放在了每个卡片组件内部,结果开了十个卡片就有十个定时器在轮询,而且组件销毁时定时器没有清理,导致泄漏。后来我把轮询提升到根布局组件,全局只保留一个定时器,组件卸载时统一clear,前端资源占用立刻降了下来。

5.4 Nginx反向代理下SSE被缓冲

第一版的前端更新靠轮询,10秒刷一次。后来我做了个优化,想把更新频率降到秒级,于是引入了SSE(Server-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上跑得非常稳。你如果也想自造一个类似的状态面板,建议先像我这样把需求清单写到纸面上,再对着清单选技术,最后动手实施时会发现每一步都有据可依,而不是凭感觉拍脑袋。另外一个小建议是,从第一天就加上指标端点和备份脚本,这两件事后期补的成本远高于一开始就做。希望这篇选型与实施的记录对你有点帮助。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 1:12:04

服务端HTML转PDF方案:无头浏览器解决中文与表格分页

简介:这是一份面向前端开发者的网页转 PDF 完整源码方案,基于 jsPDF 与 html2canvas 实现,无需安装任何浏览器插件,即可将任意网页对象以所见即所得的矢量方式输出为 PDF,并完整支持中文、图片与表格。资源共 10 个文件…

作者头像 李华
网站建设 2026/9/26 1:10:58

基于xxxwww的电商采集管道:会话保持与反爬实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 1:10:46

洗碗机水泵EMC整改:高集成方案如何从源头解决辐射超标

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 1:10:33

Proteus 8.17 SP2安装故障深度解析与系统级调优指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 1:10:07

多重假设检验校正:FDR、q值与Bonferroni原理及Python/R实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 1:09:17

STM32 SBUS解码:DMA循环接收+IDLE中断,稳定不丢帧

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华