- 可观测性
- 后端
- 前端
- 运维
【免费下载链接】kener
Stunning status pages, batteries included!
Kener 是一个"SvelteKit 应用 + Express 服务器 + 后台调度器 + BullMQ 队列"的完整状态页(status page)系统,生产环境所有组件收敛在同一个 Node.js 进程中运行。本文基于仓库中的 架构文档/docs/content/v4/architecture.md) 并结合scripts/、src/lib/server/下的实际源码,逐层拆解 Kener 的启动流程、定时调度器、队列处理管线、数据库访问模式与两段式构建系统,帮助你在部署、排障或二次开发 Kener 时准确理解各组件的边界与数据流向。
总体架构概览
从源码结构看,Kener 的运行时可以概括为一张"单进程、双平面"的图:HTTP 请求平面由 Express 挂载 SvelteKit handler 处理;后台作业平面由一组 BullMQ 调度器驱动,经队列管线写入数据库并向订阅者扇出通知。两个平面共享同一套 Knex 数据库层与 Redis 连接,但通过独立的连接池隔离(后文详述)。
┌─────────────────────────────────────────────────────────┐ │ Node.js Process │ │ │ │ ┌───────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Express │───▶│ SvelteKit │ │ Schedulers │ │ │ │ Server │ │ Handler │ │ (BullMQ) │ │ │ └───────────┘ └──────────────┘ └──────┬───────┘ │ │ │ │ │ ┌──────▼───────┐ │ │ │ Queues │ │ │ │ (BullMQ) │ │ │ └──────┬───────┘ │ │ │ │ │ ┌──────────────────────────────────────────▼────────┐ │ │ │ Database (Knex.js) │ │ │ │ SQLite │ PostgreSQL │ MySQL │ │ │ └───────────────────────────────────────────────────┘ │ │ │ │ ┌───────────────────────────────────────────────────┐ │ │ │ Redis (BullMQ backend) │ │ │ └───────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘Express 服务器负责 HTTP 请求,所有页面渲染与 API 路由都委托给 SvelteKit handler;后台调度器按 cron 表达式轮询各个 monitor(监控目标),执行结果经过一条多级队列管线完成处理、入库与告警。
生产入口:scripts/main.ts 的启动序列
生产服务器定义在 scripts/main.ts,构建时被打包为build/main.js。阅读 main.ts 可以看到完整的启动顺序:
- 动态
import("../build/handler.js")加载 SvelteKit handler(注释解释了为什么用动态导入:确保BODY_SIZE_LIMIT等.env变量在 handler 模块顶层读取前已生效); - 创建 Express 应用,并额外创建一份独立的 Knex 实例
db用于启动期操作; - 挂载
/healthcheck端点(受KENER_BASE_PATH前缀影响); app.use(handler)将其余全部路由交给 SvelteKit;app.listen(PORT, ...)监听端口,PORT默认 3000;- 监听回调内依次执行:
runMigrations()(先修正knex_migrations表中历史.js记录为.ts,再db.migrate.latest())→runSeed()(db.seed.run())→db.destroy()销毁这份启动期实例 → 调用Startup()启动后台调度器; - 注册
SIGTERM/SIGINT优雅退出处理器。
/healthcheck 的设计细节
main.ts 中的健康检查有几个值得注意的工程决策:
- 每次探测有 2 秒超时(
Promise.race),防止某个挂死的依赖拖垮健康端点; - 默认始终返回 200,响应体形如
{ "status": "ok" | "degraded", "db": true, "redis": true }。源码注释解释了原因:依赖宕机时重启应用无法救活数据库,若让探活返回 5xx,基于健康检查的自动重启机制反而会把应用弹射(bounce);如需严格模式可传?strict=1,此时任一组件异常返回 503; - Redis 探测前先检查
redis.status === "ready",因为共享的 ioredis 客户端设置了maxRetriesPerRequest: null,断线期间直接发 PING 会让命令无限排队并在多次探测中不断累积。
Startup()本身定义在 src/lib/server/startup.ts),它先把进程时区固定为 UTC(process.env.TZ = "UTC"),安装代理环境,然后依次启动mainScheduler(应用级调度)、maintenanceScheduler与dailyCleanupScheduler,最后打印版本号横幅。
开发模式与生产模式的差异
package.json 中的 scripts 字段印证了两种运行形态:
| 方面 | 开发 | 生产 |
|---|---|---|
| SvelteKit | vite dev(development),带 HMR | Express 托管预构建的build/handler.js |
| 后台作业 | 独立进程:devschedule=vite-node src/lib/server/startup.ts | 同一进程,打包进build/main.js |
| TypeScript | JIT 编译 | esbuild 预编译为 JavaScript |
| 启动命令 | npm run dev(npm-run-all --parallel devschedule development并行起两个进程) | npm start→node build/main.js |
这种"开发拆进程、生产合进程"的划分解释了 startup.ts 末尾的判断逻辑:当模块作为主模块被tsx或vite-node直接运行时才调用Startup()——生产环境下Startup()是由main.ts显式调用的,避免调度器被启动两次。
路由分组(Route Groups)
SvelteKit 路由位于src/routes/下,使用带括号的 route group 目录组织:
| 路由分组 | 用途 | 示例路由 |
|---|---|---|
(kener)/ | 公开状态页 | /、/[page_path]、/badge/、/incidents/ |
(manage)/ | 管理后台(需登录) | /manage/app/monitors/、/manage/app/incidents/ |
(api)/ | 供外部消费方的 REST API | /api/monitors/、/api/incidents/、/api/pages/ |
(embed)/ | 可嵌入的状态小组件 | /embed/monitor-[tag]/、/embed/latency-[tag]/ |
(docs)/ | 基于 Markdown 的文档站 | /docs/、/docs/v4/architecture |
(account)/ | 认证页面 | /account/signin/、/account/logout/ |
(assets)/ | 上传资源的静态服务 | /assets/images/ |
(ext)/ | 扩展端点(如 heartbeat 推送) | /ext/heartbeat/ |
此外还有(embed)/+layout.ts、(kener)/+layout.ts这类布局级逻辑,各分组之间通过括号目录共享布局但互不影响 URL 结构。
服务器架构
监控执行服务(Services)
监控执行逻辑位于src/lib/server/services/,每种 monitor 类型有独立的服务类。service.ts 中的Service工厂类根据 monitor 的monitor_type字段分发到具体实现:
| 服务文件 | Monitor 类型 | 说明 |
|---|---|---|
| apiCall.ts | API | HTTP/HTTPS 端点监控 |
| pingCall.ts | PING | ICMP ping 检测 |
| tcpCall.ts | TCP | TCP 端口连通性 |
| dnsCall.ts | DNS | DNS 记录解析 |
| sslCall.ts | SSL | SSL 证书有效期/到期检查 |
| sqlCall.ts | SQL | 数据库查询监控 |
| heartbeatCall.ts | HEARTBEAT | 被动式推送监控(uptime-kuma 风格) |
| gamedigCall.ts | GAMEDIG | 游戏服务器监控 |
| dockerCall.ts | DOCKER | Docker 容器/守护进程健康 |
| groupCall.ts | GROUP | 聚合子 monitor 的状态 |
| grpcCall.ts | GRPC | gRPC 端点监控 |
| prometheusCall.ts | PROMETHEUS | Prometheus 指标监控 |
| noneCall.ts | (兜底) | 未知类型回退 |
注意从源码结构看,当前仓库的工厂类实际支持GRPC、PROMETHEUS两种文档表格未列出的类型,且对无法识别的monitor_type会回退到NoneCall,保证调度链路不因新增类型而崩溃。
调度器(Schedulers)
调度器位于src/lib/server/schedulers/,全部基于 BullMQ 的 repeatable job(upsertJobScheduler)实现:
| 调度器 | 触发节奏 | 职责 |
|---|---|---|
| appScheduler.ts | 每 10 秒 | 对账(reconcile)各 monitor 的调度器:新增、移除或更新 |
| monitorSchedulers.ts | 按 monitor 的 cron 表达式 | 按每个 monitor 配置的 cron 触发其执行 |
| maintenanceScheduler.ts | 周期轮询(源码中every: 3600000,即每小时,启动时immediately: true) | 依据 RRULE 为未来 7 天生成计划内的 maintenance 事件 |
| dailyCleanup.ts | cron0 0 * * *(UTC 午夜) | 每日清理任务(文档表格未提及,见源码) |
appScheduler 的对账机制值得展开。appScheduler.ts 的 worker 每 10 秒执行一次:
- 拉取所有
ACTIVE状态的 monitor,为每个 monitor 计算身份哈希monitor.tag + "::" + HashString(JSON.stringify(monitor))——把 tag 和完整配置序列化后一起哈希,意味着 monitor 配置一旦变更,旧调度器会被判定为不匹配而被移除、新调度器被重建,从而让新的 cron 或类型参数即时生效; - 列出
monitorScheduleQueue中现有的所有 job scheduler,删除"不在活跃 monitor 集合中"的(即已删除或改名的 monitor); - 为"活跃但没有对应 scheduler"的 monitor 调用
addJobToSchedulerQueue新建; - 顺带调用
UpdateMaintenanceEventStatuses()刷新维护事件状态。
monitorSchedulers 的 cron 触发:monitorSchedulers.ts 中,addJobToSchedulerQueue以 monitor 身份哈希为 scheduler id,{ pattern: monitor.cron, immediately: true }注册——immediately: true表示注册后立即先跑一次。worker 的并发度concurrency被设置为Math.max(activeMonitors.length, 1),保证多 monitor 并发触发时互不排队。
被触发的 job 进入 cron-minute.ts 的Minuter:先校验monitor_type合法性,取当前分钟的 UTC 整分时间戳作为统一的作业时间键(ts),对GROUP类型附加 1 秒(可由type_data.executionDelay覆盖)延迟——这是为了等待子 monitor 先完成执行、避免 GROUP 聚合读到过期状态——最后推入monitorExecuteQueue。
队列管线(Queues)
队列位于src/lib/server/queues/,由 Redis 通过 BullMQ 支撑,形成一条"执行 → 存储 → 告警 → 通知"的管线:
Scheduler (cron 触发) │ ▼ monitorExecuteQueue ─── 运行 monitor 服务(API、Ping 等) │ ▼ monitorResponseQueue ── 结果入库、更新缓存 │ ▼ alertingQueue ────────── 评估告警规则,创建/解决 incident │ ▼ subscriberQueue ──────── 向订阅者扇出通知 │ ▼ emailQueue ───────────── 发送单封邮件统一的基础设施在 q.ts 中:
- 所有队列共享 Redis 连接,key 前缀统一为
kener; - 默认作业策略:
attempts: 3(重试 3 次),backoff为指数退避、基准延迟 5000ms,removeOnComplete: true、removeOnFail: false——即失败的作业保留现场便于排障; - worker 默认并发 5(
concurrency: 5),且每个函数型 processor 会被db.runInWorkerContext(...)包裹,把所有BullMQ worker 的数据库访问路由到独立 worker 连接池,与 Web 请求池隔离(这是防止后台任务突发耗尽页面请求连接的关键设计)。
各队列的具体职责与去重策略:
- monitorExecuteQueue.ts:核心执行器。worker 内
new Service(monitor).execute(ts)完成探测后,还会做几件文档未展开的事:在探测之后读取该分钟是否有手动 incident / maintenance 覆盖(overlay 优先生效);对配置了confirmation_threshold > 1的 monitor 执行"确认阈值"阻尼(连续 N 次才翻转状态,缓解抖动误报);最后按default → realtime → incident → maintenance的优先级合并出最终MonitoringResult,并逐分钟推给monitorResponseQueue。去重 id 为${monitor.tag}-${ts},保证同一 monitor 同一分钟不会重复执行。 - monitorResponseQueue.ts:调用
InsertMonitoringData把结果写入monitoring_data表,通过SetLastMonitoringValue更新"最近一次探测值"缓存,再向alertingQueue推送{ monitorTag, ts, status }。 - alertingQueue.ts:评估 monitor 的告警规则(v2 告警配置、trigger 条件),创建或解决 incident,并通过
sendEmail/sendWebhook/sendSlack/sendDiscord等通知函数产出告警。 - subscriberQueue.ts 与 emailQueue.ts:前者向订阅者扇出,后者发送单封邮件;两者都在
push时设置了 deduplication id,防止重复通知。
每个队列模块都导出自己的shutdown(),统一由 queues/shutdown.ts 按 execute → response → alerting → subscriber → email 的顺序依次关闭 worker。
中间件:SvelteKit handle 钩子
src/hooks.server.ts 的handle钩子对每个请求生效,职责包括:
- API 鉴权:所有
/api/*路由校验 Bearer token(VerifyAPIKey),仅PUBLIC_API_PATHS = ["/api/status"]免鉴权; - 资源预加载:通过正则从路径中提取
monitor_tag、incident_id、maintenance_id、page_path(同时兼容/api/v{n}/...带版本前缀的写法),从数据库取对应资源挂到event.locals,供下游 handler 直接消费; - 环境代理:启动时调用
InstallEnvProxy()安装代理环境变量。
数据库层
Kener 使用Knex.js作为查询构建器,knexfile.ts 根据DATABASE_URL的前缀选择后端:
| 数据库 | 连接前缀 | 驱动 | 备注 |
|---|---|---|---|
| SQLite | sqlite:// | better-sqlite3 | 默认值sqlite://./database/kener.sqlite.db,无独立池,复用单连接 |
| PostgreSQL | postgresql:// | pg | 开启keepAlive(DATABASE_KEEPALIVE !== "false"),防止云端网络静默丢弃空闲连接 |
| MySQL | mysql:// | mysql2 | enableKeepAlive: true,初始延迟 10s |
双连接池设计(Postgres/MySQL 才适用):knexfile.ts 构建两套池——WEB 池(默认DATABASE_POOL_MAX=10)服务 SvelteKit 请求,WORKER 池(默认DATABASE_WORKER_POOL_MAX=5)服务 BullMQ 后台任务;min默认为 0(源码注释指出 knex 默认的 min 2 连接从不回收,恰恰是最先变"死"的),acquireConnectionTimeout默认 15 秒,目的是数据库故障时快速失败而不是挂起 60 秒。预算约束:副本数 × (web + worker) < 数据库 max_connections。
Repository 模式
数据库访问遵循仓库模式:
- src/lib/server/db/repositories/ —— 按领域划分的仓库类,仓库中实际包含
monitors、incidents、monitoring、maintenances、alerts、users、pages、monitorAlertConfig、emailTemplateConfig、subscriptionSystem、images、site-data、base等; - src/lib/server/db/dbimpl.ts —— 门面(facade),组合全部仓库并暴露统一接口,构造时接收
knexOb与workerKnexOb两个配置实例; - src/lib/server/db/db.ts —— 导出单例
DbImpl实例,全应用统一通过import db from "$lib/server/db/db"访问数据库; - src/lib/server/db/poolContext.ts —— 请求上下文/worker 上下文的连接池路由实现,与
q.ts中的runInWorkerContext配合。
迁移(Migrations)
迁移文件存放在仓库根目录 migrations/,启动时由db.migrate.latest()自动执行到最新状态,覆盖 monitors、incidents、subscriptions、maintenances、告警体系、RBAC、OIDC 等历次 schema 变更;种子数据在 seeds/ 目录(roles、permissions、monitors、pages、site_data 等),由db.seed.run()在迁移后执行。
构建系统
构建分两个阶段(见 package.json 的 scripts):
阶段 1:SvelteKit 构建
npm run build:sveltekit(即vite build)把 SvelteKit 应用编译到build/:
build/handler.js—— SvelteKit 请求处理器,生产运行时由 Express 动态加载;build/server/—— SSR 产物;build/client/—— 静态客户端资源。
阶段 2:服务端构建
npm run build:server运行 scripts/build-server.js,用esbuild把scripts/main.ts及其全部服务端依赖打包为单一 ESM 文件build/main.js。源码可确认的关键选项:
entryPoints: ["scripts/main.ts"],platform: "node",target: "node20",format: "esm";- external 化全部
node_modules(运行时再解析),但CJS_PACKAGES_TO_BUNDLE = ["rrule"]例外——该 CJS 包在 ESM 命名导入下会出问题,必须打包进产物; alias将 SvelteKit 的$lib映射到./src/lib,使服务端代码脱离 Vite 上下文后导入路径依然正确;define在构建时注入import.meta.env.PACKAGE_VERSION = pkg.version,让 src/lib/version.ts 无需运行期插件即可取到版本;- 自定义插件
rewrite-build-imports把产物中的../build/前缀重写为./,因为main.js与handler.js同在build/下,否则 handler 的相对导入会指向错误位置。
构建命令速查
| 命令 | 作用 |
|---|---|
npm run build | 完整生产构建(build-sveltekit.js+build-server.js) |
npm run build:sveltekit | 仅 SvelteKit 构建 |
npm run build:server | 仅服务端构建 |
npm start | 运行生产服务器(node build/main.js) |
优雅退出(Graceful Shutdown)
main.ts 在SIGTERM/SIGINT时执行gracefulShutdown,顺序为:
shutdownSchedulers()(schedulers/shutdown.ts):依次关闭 appScheduler、monitorSchedulers、maintenanceScheduler、dailyCleanupScheduler 的 worker;shutdownQueues()(queues/shutdown.ts):依次关闭 execute、response、alerting、subscriber、email 五个队列的 worker;dbInstance.close()关闭数据库连接;process.exit(0)(任一步失败则以退出码 1 结束)。
先停调度器、再排空队列、最后关库的顺序,保证停机时进行中的探测作业能跑完并落库,部署与重启不丢数据。
延伸阅读
以下文档位于仓库src/routes/(docs)/docs/content/v4/下,可与本文配套阅读:
- Environment Variables/docs/content/v4/setup/environment-variables.md) —— 必需与可选配置项;
- Database Setup/docs/content/v4/setup/database-setup.md) —— 数据库选择与配置;
- Redis Setup/docs/content/v4/setup/redis-setup.md) —— 为 BullMQ 队列配置 Redis;
- Monitors Overview/docs/content/v4/monitors/overview.md) —— 各 monitor 类型与配置说明;
- Getting Started/docs/content/v4/getting-started.md) 与 Installation/docs/content/v4/installation.md) —— 安装与首次部署。
- 可观测性
- 后端
- 前端
- 运维
【免费下载链接】kener
Stunning status pages, batteries included!
相关推荐
Kener 工作原理深度解析:从 SvelteKit 前端到调度器、队列与数据存储
Kener 工作原理深度解析:从 SvelteKit 前端到调度器、队列与数据存储 本篇技术指南以 Kener 官方文档《How it works》为核心脉络,
可观测性后端前端运维终极指南:Stack-on-a-budget如何构建免费开发者服务数据库
终极指南:Stack on a budget如何构建免费开发者服务数据库 Stack on a budget(简称st)是一个专为预算有限的开发者打造的免费服务
教程开发工具Kener 开源状态页系统深度解读:SvelteKit + Node.js 架构、部署与核心能力全景
Kener 开源状态页系统深度解读:SvelteKit + Node.js 架构、部署与核心能力全景 Kener 是一个基于 SvelteKit + Node.
可观测性后端前端运维
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考