☰
Kener 技术架构解析:Express + SvelteKit + BullMQ 单进程状态页服务如何组织调度、队列与数据库层
2026/10/13 23:54:00 网站建设 项目流程
  • 可观测性
  • 后端
  • 前端
  • 运维

【免费下载链接】kener

Stunning status pages, batteries included!

项目地址:https://gitcode.com/gh_mirrors/ke/kener
点击查看免费下载

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 可以看到完整的启动顺序:

  1. 动态import("../build/handler.js")加载 SvelteKit handler(注释解释了为什么用动态导入:确保BODY_SIZE_LIMIT等.env变量在 handler 模块顶层读取前已生效);
  2. 创建 Express 应用,并额外创建一份独立的 Knex 实例db用于启动期操作;
  3. 挂载/healthcheck端点(受KENER_BASE_PATH前缀影响);
  4. app.use(handler)将其余全部路由交给 SvelteKit;
  5. app.listen(PORT, ...)监听端口,PORT默认 3000;
  6. 监听回调内依次执行:runMigrations()(先修正knex_migrations表中历史.js记录为.ts,再db.migrate.latest())→runSeed()(db.seed.run())→db.destroy()销毁这份启动期实例 → 调用Startup()启动后台调度器;
  7. 注册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 字段印证了两种运行形态:

方面开发生产
SvelteKitvite dev(development),带 HMRExpress 托管预构建的build/handler.js
后台作业独立进程:devschedule=vite-node src/lib/server/startup.ts同一进程,打包进build/main.js
TypeScriptJIT 编译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.tsAPIHTTP/HTTPS 端点监控
pingCall.tsPINGICMP ping 检测
tcpCall.tsTCPTCP 端口连通性
dnsCall.tsDNSDNS 记录解析
sslCall.tsSSLSSL 证书有效期/到期检查
sqlCall.tsSQL数据库查询监控
heartbeatCall.tsHEARTBEAT被动式推送监控(uptime-kuma 风格)
gamedigCall.tsGAMEDIG游戏服务器监控
dockerCall.tsDOCKERDocker 容器/守护进程健康
groupCall.tsGROUP聚合子 monitor 的状态
grpcCall.tsGRPCgRPC 端点监控
prometheusCall.tsPROMETHEUSPrometheus 指标监控
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.tscron0 0 * * *(UTC 午夜)每日清理任务(文档表格未提及,见源码)

appScheduler 的对账机制值得展开。appScheduler.ts 的 worker 每 10 秒执行一次:

  1. 拉取所有ACTIVE状态的 monitor,为每个 monitor 计算身份哈希monitor.tag + "::" + HashString(JSON.stringify(monitor))——把 tag 和完整配置序列化后一起哈希,意味着 monitor 配置一旦变更,旧调度器会被判定为不匹配而被移除、新调度器被重建,从而让新的 cron 或类型参数即时生效;
  2. 列出monitorScheduleQueue中现有的所有 job scheduler,删除"不在活跃 monitor 集合中"的(即已删除或改名的 monitor);
  3. 为"活跃但没有对应 scheduler"的 monitor 调用addJobToSchedulerQueue新建;
  4. 顺带调用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钩子对每个请求生效,职责包括:

  1. API 鉴权:所有/api/*路由校验 Bearer token(VerifyAPIKey),仅PUBLIC_API_PATHS = ["/api/status"]免鉴权;
  2. 资源预加载:通过正则从路径中提取monitor_tag、incident_id、maintenance_id、page_path(同时兼容/api/v{n}/...带版本前缀的写法),从数据库取对应资源挂到event.locals,供下游 handler 直接消费;
  3. 环境代理:启动时调用InstallEnvProxy()安装代理环境变量。

数据库层

Kener 使用Knex.js作为查询构建器,knexfile.ts 根据DATABASE_URL的前缀选择后端:

数据库连接前缀驱动备注
SQLitesqlite://better-sqlite3默认值sqlite://./database/kener.sqlite.db,无独立池,复用单连接
PostgreSQLpostgresql://pg开启keepAlive(DATABASE_KEEPALIVE !== "false"),防止云端网络静默丢弃空闲连接
MySQLmysql://mysql2enableKeepAlive: 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,顺序为:

  1. shutdownSchedulers()(schedulers/shutdown.ts):依次关闭 appScheduler、monitorSchedulers、maintenanceScheduler、dailyCleanupScheduler 的 worker;
  2. shutdownQueues()(queues/shutdown.ts):依次关闭 execute、response、alerting、subscriber、email 五个队列的 worker;
  3. dbInstance.close()关闭数据库连接;
  4. 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!

项目地址:https://gitcode.com/gh_mirrors/ke/kener
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询