摘要:本文面向负责 DolphinDB 生产环境运维的工程师,系统讲解如何利用 DolphinDB 内置脚本能力构建监控、维护、部署、告警一体化的自动化运维工具链。文章基于 DolphinDB 2.0.x 版本,从"运维工具"与"自动化运维脚本"两个核心概念切入,给出可直接复用的 5 组脚本示例、3 类 Mermaid 图示、2 张对照表和 3 张配图提示,并重点说明版本兼容、变更风险和回滚策略,帮助读者把重复运维动作沉淀为可调度、可观测、可回滚的标准化流程。
文章目录
-
- 一、引言:为什么运维脚本必须自动化
- 二、概念拆解:运维工具与自动化运维脚本
-
- [2.1 什么是 DolphinDB 运维工具](#2.1 什么是 DolphinDB 运维工具)
- [2.2 自动化运维脚本解决什么问题](#2.2 自动化运维脚本解决什么问题)
- [2.3 运维脚本分层体系](#2.3 运维脚本分层体系)
- 三、监控类脚本:从被动救火到主动发现
- 四、维护类脚本:定时任务与健康管理
- 五、部署与升级脚本:标准化变更流程
- 六、告警通知与事件闭环
- 七、运维脚本分类对照与告警阈值参考
- 八、实战演练:完整运维工具链落地
- 九、适用边界与风险提示
- 十、总结与延伸思考
- 参考链接
一、引言:为什么运维脚本必须自动化
在生产环境中,DolphinDB 集群往往承载着高频写入、实时计算和复杂查询任务。随着节点数增加、业务场景复杂化,依赖人工登录每台机器执行巡检、清理、备份、升级的日子已经难以为继。手动操作不仅效率低,还容易出现"这台机器改了、那台机器漏改"的配置漂移,或者在紧急故障时因为步骤不熟悉而二次扩大影响面。
自动化运维脚本解决的核心问题,是把"人记住流程"变成"脚本执行流程"。它的价值不只是省时间,更在于可重复、可审计、可回滚。当所有关键操作都被脚本化后,团队可以通过调度器定时执行、通过日志追溯每一次变更、通过版本控制管理脚本本身的演进。这对于需要 7×24 小时稳定运行的金融、物联网、工业大数据场景尤为重要。
从成熟度来看,运维自动化通常经历三个阶段:第一阶段是"adhoc 脚本",哪里有问题写哪里;第二阶段是"定时任务",把重复动作交给调度器;第三阶段是"平台化运维",通过统一入口、版本控制和可观测体系把脚本变成组织能力。本文介绍的方案覆盖了第二到第三阶段的关键能力,读者可以根据自身团队规模选择逐步落地。
本文基于 DolphinDB 2.0.x 进行讲解,涉及的 submitJob、scheduleJob、getClusterPerf 等函数和脚本组织思路均为经典运维自动化原理的实践,不依赖特定小版本。如果你使用的是更早的 1.x 版本,部分函数签名和集群管理接口可能存在差异,建议参考对应版本的官方文档作为替代方案。接下来会先拆解两个关键概念,再按监控、维护、部署、告警的链路给出完整实现与边界说明。
二、概念拆解:运维工具与自动化运维脚本
2.1 什么是 DolphinDB 运维工具
运维工具并不是某一款单一产品,而是围绕集群生命周期管理的一整套能力集合。在 DolphinDB 的语境下,它既包括官方提供的 Web 管理界面、命令行客户端、集群管理接口,也包括用户基于 DolphinDB 脚本语言(DolphinDB Script)自行封装的函数、任务和流程。
常见的运维工具覆盖五大领域:监控(采集 CPU、内存、连接数、查询耗时)、维护(数据清理、索引重建、健康检查)、部署(单机/集群安装、参数初始化)、备份恢复(全量/增量备份、灾难恢复)、告警通知(阈值触发、多渠道通知)。这五大领域并不是孤立存在的,而是构成了"采集→分析→决策→执行→通知"的完整闭环。
对于中小规模集群,官方 Web 管理界面和命令行可能已经足够;但当节点数量超过 10 个、业务对 RTO/RPO 有明确要求时,就需要把官方能力进一步封装成脚本,并接入企业内部的告警、CMDB、发布平台。此时,"运维工具"就不再是单一产品,而是一个与组织流程深度结合的工程体系。
2.2 自动化运维脚本解决什么问题
自动化运维脚本的本质是用可执行的代码替代人工操作步骤 。它通常具备三个特征:第一,可调度,能够通过 scheduleJob 按固定周期运行,也能通过 submitJob 立即触发;第二,可观测,执行结果写入流表或持久化到文件,方便后续审计;第三,可组合,多个脚本可以组合成更大的运维工作流,例如"备份→升级→验证→回滚"。
没有脚本化时,团队容易陷入三种困境:第一是响应慢 ,深夜告警需要值班人员手动登录排查;第二是不一致 ,不同人员执行同一操作的步骤存在偏差;第三是难追溯,事故发生后无法快速还原当时做了什么。脚本化并不能消除故障,但能显著降低人为因素带来的风险。
此外,脚本化还有三个容易被忽视的好处:第一是可测试 ,脚本可以在测试环境反复跑,直到稳定后再上生产;第二是可交接 ,新员工通过阅读脚本就能理解运维流程,而不是依赖老员工的口头传授;第三是可优化,当发现某个步骤耗时过长时,可以通过 profiling 脚本本身找到瓶颈,而不是模糊地"再观察观察"。
2.3 运维脚本分层体系
按照职责不同,可以把 DolphinDB 运维脚本分为三层:采集层 负责持续收集系统和数据库指标;决策层 负责根据阈值判断是否需要干预;执行层负责实际完成清理、备份、部署、通知等动作。三层之间通过流表、日志和调度任务解耦,任何一层升级都不应影响其他层的稳定运行。
#mermaid-svg-XvsMebdd3s2IPDuH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XvsMebdd3s2IPDuH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XvsMebdd3s2IPDuH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XvsMebdd3s2IPDuH .error-icon{fill:#552222;}#mermaid-svg-XvsMebdd3s2IPDuH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XvsMebdd3s2IPDuH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XvsMebdd3s2IPDuH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XvsMebdd3s2IPDuH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XvsMebdd3s2IPDuH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XvsMebdd3s2IPDuH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XvsMebdd3s2IPDuH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XvsMebdd3s2IPDuH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XvsMebdd3s2IPDuH .marker.cross{stroke:#333333;}#mermaid-svg-XvsMebdd3s2IPDuH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XvsMebdd3s2IPDuH p{margin:0;}#mermaid-svg-XvsMebdd3s2IPDuH .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XvsMebdd3s2IPDuH .cluster-label text{fill:#333;}#mermaid-svg-XvsMebdd3s2IPDuH .cluster-label span{color:#333;}#mermaid-svg-XvsMebdd3s2IPDuH .cluster-label span p{background-color:transparent;}#mermaid-svg-XvsMebdd3s2IPDuH .label text,#mermaid-svg-XvsMebdd3s2IPDuH span{fill:#333;color:#333;}#mermaid-svg-XvsMebdd3s2IPDuH .node rect,#mermaid-svg-XvsMebdd3s2IPDuH .node circle,#mermaid-svg-XvsMebdd3s2IPDuH .node ellipse,#mermaid-svg-XvsMebdd3s2IPDuH .node polygon,#mermaid-svg-XvsMebdd3s2IPDuH .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XvsMebdd3s2IPDuH .rough-node .label text,#mermaid-svg-XvsMebdd3s2IPDuH .node .label text,#mermaid-svg-XvsMebdd3s2IPDuH .image-shape .label,#mermaid-svg-XvsMebdd3s2IPDuH .icon-shape .label{text-anchor:middle;}#mermaid-svg-XvsMebdd3s2IPDuH .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XvsMebdd3s2IPDuH .rough-node .label,#mermaid-svg-XvsMebdd3s2IPDuH .node .label,#mermaid-svg-XvsMebdd3s2IPDuH .image-shape .label,#mermaid-svg-XvsMebdd3s2IPDuH .icon-shape .label{text-align:center;}#mermaid-svg-XvsMebdd3s2IPDuH .node.clickable{cursor:pointer;}#mermaid-svg-XvsMebdd3s2IPDuH .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XvsMebdd3s2IPDuH .arrowheadPath{fill:#333333;}#mermaid-svg-XvsMebdd3s2IPDuH .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XvsMebdd3s2IPDuH .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XvsMebdd3s2IPDuH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XvsMebdd3s2IPDuH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XvsMebdd3s2IPDuH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XvsMebdd3s2IPDuH .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XvsMebdd3s2IPDuH .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XvsMebdd3s2IPDuH .cluster text{fill:#333;}#mermaid-svg-XvsMebdd3s2IPDuH .cluster span{color:#333;}#mermaid-svg-XvsMebdd3s2IPDuH div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XvsMebdd3s2IPDuH .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XvsMebdd3s2IPDuH rect.text{fill:none;stroke-width:0;}#mermaid-svg-XvsMebdd3s2IPDuH .icon-shape,#mermaid-svg-XvsMebdd3s2IPDuH .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XvsMebdd3s2IPDuH .icon-shape p,#mermaid-svg-XvsMebdd3s2IPDuH .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XvsMebdd3s2IPDuH .icon-shape .label rect,#mermaid-svg-XvsMebdd3s2IPDuH .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XvsMebdd3s2IPDuH .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XvsMebdd3s2IPDuH .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XvsMebdd3s2IPDuH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 执行层
决策层
采集层
是
是
否
系统监控脚本
指标流表
数据库监控脚本
集群监控脚本
阈值判断
是否触发
维护脚本
告警通知脚本
继续采集
部署/升级脚本
图示说明:运维脚本按采集层、决策层、执行层分层解耦,指标统一写入流表后再做阈值判断与动作执行。

三、监控类脚本:从被动救火到主动发现
监控是运维自动化的眼睛。没有监控,所有的故障处理都是被动救火;有了监控,才能在问题萌芽阶段就触发干预。DolphinDB 的监控脚本通常需要覆盖三个层面:系统层(CPU、内存、磁盘、网络)、数据库层(连接数、查询量、写入量、缓存命中率)、集群层(节点状态、节点级资源使用)。
下面这段脚本把三类监控任务合并到一个模块中,通过不同的 submitJob 名称区分职责。系统监控每分钟采集一次主机指标并写入 monitor_metrics 流表;数据库监控关注连接数、查询量和缓存命中率;集群监控则遍历所有节点,把每个节点的状态写入 cluster_metrics。
dolphindb
// 监控类脚本合集:系统 + 数据库 + 集群
def initMonitoringTables() {
share streamTable(100000:0,
`timestamp`cpu_usage`mem_usage`disk_usage`net_rx`net_tx,
[TIMESTAMP, DOUBLE, DOUBLE, DOUBLE, LONG, LONG]) as monitor_metrics
share streamTable(100000:0,
`timestamp`connections`queries`writes`memory`cache_hit_rate,
[TIMESTAMP, INT, INT, INT, DOUBLE, DOUBLE]) as db_metrics
share streamTable(100000:0,
`timestamp`node_id`status`cpu_usage`mem_usage`connections,
[TIMESTAMP, STRING, STRING, DOUBLE, DOUBLE, INT]) as cluster_metrics
}
def systemMonitorTask() {
cpuInfo = cpu()
memInfo = mem()
diskInfo = disk()
netInfo = net()
insert into monitor_metrics values(
now(), cpuInfo.usage,
memInfo.used * 100.0 / memInfo.total,
diskInfo.used * 100.0 / diskInfo.total,
netInfo.rxBytes, netInfo.txBytes)
if (cpuInfo.usage > 80) sendAlert("CPU使用率过高: " + cpuInfo.usage + "%")
}
def dbMonitorTask() {
insert into db_metrics values(
now(), getConnectionCount(), getQueryCount(),
getWriteCount(), getMemoryUsage(), getCacheHitRate())
}
def clusterMonitorTask() {
nodes = getClusterPerf()
for (node in nodes) {
insert into cluster_metrics values(
now(), node.node, node.status,
node.cpuUsage, node.memUsage, node.connectionCount)
}
}
def startMonitoring() {
initMonitoringTables()
submitJob("sys_monitor", "系统监控", systemMonitorTask, 60000, 0)
submitJob("db_monitor", "数据库监控", dbMonitorTask, 60000, 0)
submitJob("cluster_monitor", "集群监控", clusterMonitorTask, 60000, 0)
print("监控脚本已启动")
}
预期输出:
startMonitoring()执行后,控制台打印"监控脚本已启动",三张流表monitor_metrics、db_metrics、cluster_metrics开始以 60 秒为周期接收数据。submitJob最后一个参数为 0 表示无限次执行,直到主动取消或节点重启。
这里把三类监控合并写在一个模块里,是因为它们共享相同的"采集→写入流表"模式。实际生产环境中,如果监控点非常多,建议把每个监控任务拆成独立脚本文件,并通过统一的命名规范管理。另外,监控频率不要盲目设得太高,60 秒足以覆盖大多数场景;如果设成 1 秒,一方面会占用大量 CPU 和磁盘 IO,另一方面流表数据膨胀会加快清理压力。
在存储设计上,monitor_metrics 这类流表建议设置合理的 retention 策略。如果无限制地保留所有历史监控数据,几个月后可能占用上百 GB 磁盘。常见的做法是保留 30 天原始明细和 1 年聚合指标,原始明细用于故障定位,聚合指标用于容量规划。
四、维护类脚本:定时任务与健康管理
维护类脚本的核心目标是让数据库长期保持健康状态 ,而不是等到磁盘满了、查询慢了才临时处理。常见的维护动作包括:清理过期数据释放磁盘、重建碎片化索引提升查询性能、执行健康检查并生成报告。DolphinDB 通过 scheduleJob 可以方便地把这些动作变成定时任务。
下面的脚本把数据清理、索引维护和健康检查封装成三个函数,并提供统一的启动入口。数据清理每天凌晨 3 点执行,清理 90 天前的过期分区和超过 1 天的临时文件;索引维护每周日凌晨 2 点执行,重建碎片化率超过 30% 的索引;健康检查每小时执行一次,输出 JSON 格式的检查报告。
dolphindb
// 维护类脚本合集:清理 + 索引 + 健康检查
def cleanupExpiredData() {
cutoffDate = date(now() - 90 * 86400000)
delete from sensor_data where date(timestamp) < cutoffDate
print("过期数据清理完成")
}
def cleanupTempFiles(tempDir) {
files = listFiles(tempDir)
for (file in files) {
if (file.age > 86400000) deleteFile(file.path)
}
print("临时文件清理完成")
}
def rebuildFragmentedIndexes() {
indexes = getIndexFragmentation()
for (idx in indexes) {
if (idx.fragmentation > 30) {
rebuildIndex(idx.name)
print("重建索引: " + idx.name)
}
}
}
def checkSystemHealth() {
issues = array(STRING, 0)
if (cpu().usage > 80) issues.append!("CPU使用率高")
if (mem().used * 100.0 / mem().total > 85) issues.append!("内存使用率高")
if (disk().used * 100.0 / disk().total > 90) issues.append!("磁盘使用率高")
return dict(STRING, ANY, [
["status", iif(issues.size()==0, "healthy", "warning")],
["issues", issues]])
}
def generateHealthReport(report) {
path = "/data/ddb/reports/health_" + format(now(), "yyyyMMddHHmmss") + ".json"
saveJson(report, path)
print("健康报告已保存: " + path)
}
def startMaintenance() {
scheduleJob("data_cleanup", "数据清理",
def(){ cleanupExpiredData(); cleanupTempFiles("/data/ddb/temp/") },
03:00m, true, 2024.01.01, 2099.12.31, 'D')
scheduleJob("index_maintenance", "索引维护",
rebuildFragmentedIndexes, 02:00m, true, 2024.01.01, 2099.12.31, 'W', 0)
scheduleJob("health_check", "健康检查",
def(){ generateHealthReport(dict(STRING, ANY,
[["system", checkSystemHealth()], ["database", checkDatabaseHealth()],
["cluster", checkClusterHealth()]])) },
00:00m, true, 2024.01.01, 2099.12.31, 'H')
print("维护脚本已启动")
}
预期输出:
startMaintenance()执行后,三个定时任务被注册到调度器。每天凌晨 3 点会清理过期数据,每周日凌晨 2 点重建碎片化索引,每小时生成一份健康报告到/data/ddb/reports/。scheduleJob的'D'、'W'、'H'分别代表按天、按周、按小时调度。
这里有一个设计要点:维护任务尽量放在业务低峰期执行。如果在交易高峰期做索引重建或大规模数据删除,可能会抢占 CPU 和 IO 资源,导致查询延迟飙升。另外,delete from 清理分区后,建议配合 gc() 或 clearCache() 释放底层资源,但这两个操作也有性能开销,需要根据集群负载情况决定是否启用。
对于健康检查脚本,建议把报告格式统一为 JSON,并接入企业内部的监控大盘或告警系统。JSON 报告里除了 status 和 issues,还可以增加 "suggest" 字段,给出下一步操作建议。这样即使值班人员不熟悉 DolphinDB,也能根据报告快速做出判断,缩短 MTTR。
五、部署与升级脚本:标准化变更流程
部署和升级是运维工作中风险最高的两类操作。一次不规范的升级可能导致集群不可用、数据不一致甚至业务中断。因此,部署脚本和升级脚本不仅要"能跑通",还要"能验证"和"能回退"。
下面的脚本包含两部分:第一部分是单机自动部署,依次完成环境检查、依赖校验、配置生成、服务启动和连接验证;第二部分是在线升级,依次完成数据备份、服务停止、旧版本备份、新版本安装、配置迁移、服务启动和升级验证。两者都强调"先备份再变更"的原则。
dolphindb
def checkEnvironment() {
os = getOSVersion()
print("操作系统: " + os)
print("CPU核数: " + string(getCPUCores()))
print("内存大小: " + string(getPhysicalMemory()/1024/1024/1024) + "GB")
if (isPortInUse(8848)) throw "端口8848已被占用"
if (!checkLibrary("glibc", "2.17")) throw "glibc版本过低"
}
def configureSystem(config) {
saveText(generateConfigFile(config), "/opt/dolphindb/dolphindb.cfg")
createDirectory("/data/ddb/log")
createDirectory("/data/ddb/dfsMeta")
createDirectory("/data/ddb/chunkMeta")
}
def startAndVerifyService() {
execCommand("./dolphindb -config dolphindb.cfg -mode single")
sleep(5000)
if (!checkServiceStatus()) throw "服务启动失败"
conn = xdb("localhost", 8848, "admin", "123456")
conn.run("1+1")
print("连接测试成功")
}
def autoDeploy(config) {
print("开始自动部署...")
checkEnvironment()
configureSystem(config)
startAndVerifyService()
print("自动部署完成")
}
def backupBeforeUpgrade(dbPath, backupDir) {
executeFullBackup(dbPath, backupDir + "/pre_upgrade")
execCommand("cp -r /opt/dolphindb /opt/dolphindb.bak")
print("备份完成")
}
def installNewVersion(version) {
url = "https://www.dolphindb.cn/downloads/DolphinDB_Linux64_" + version + ".zip"
execCommand("wget " + url)
execCommand("unzip DolphinDB_Linux64_" + version + ".zip -d /opt/dolphindb")
execCommand("cp /opt/dolphindb.bak/dolphindb.cfg /opt/dolphindb/")
}
def upgradeDolphinDB(version, dbPath, backupDir) {
print("开始升级到: " + version)
backupBeforeUpgrade(dbPath, backupDir)
stopService()
installNewVersion(version)
startService()
print("当前版本: " + getVersion())
verifyFunctionality()
print("升级完成")
}
预期输出:
autoDeploy(cfg)执行后,完成环境检查、配置写入和服务启动,最后通过xdb连接验证1+1是否返回 2。upgradeDolphinDB("2.00.10", "dfs://industrial_iot", "/data/backup")会先备份数据和旧版本安装目录,再执行升级,并在升级后打印当前版本号。
这段脚本的关键在于变更前的双重备份:数据备份保证业务数据不丢,安装目录备份保证可以回退到旧版本。实际生产环境中,升级前还应该做一次灰度验证,比如先在测试集群跑一遍完整流程,确认无异常后再在生产环境执行。另外,脚本中的密码、下载地址等敏感信息建议通过环境变量或配置中心注入,不要硬编码在脚本文件里。
升级脚本还应包含回退逻辑。例如,当 verifyFunctionality() 检测到关键功能失败时,应自动停止服务、恢复旧版本安装目录、启动旧版本服务,并通知值班人员。这种"自动回滚"能力在夜间无人值守升级时尤为重要,可以避免小问题演变成长时间不可用。
六、告警通知与事件闭环
监控发现问题只是第一步,更重要的是把问题通知到责任人并推动事件闭环。一个好的告警通知脚本需要解决三个问题:合并同类告警避免消息轰炸、按严重程度选择通知渠道、保留告警历史便于复盘。
下面的脚本实现了系统、数据库、集群三类告警的合并,并通过邮件、钉钉、短信三种渠道分级通知。普通告警走邮件和钉钉,严重告警(critical)额外触发短信。所有告警最终写入 active_alerts 表,方便后续统计和复盘。
告警日志表 通知渠道 告警合并器 决策规则 监控脚本 告警日志表 通知渠道 告警合并器 决策规则 监控脚本 #mermaid-svg-1BQJBmi7EeYHuyKo{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1BQJBmi7EeYHuyKo .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1BQJBmi7EeYHuyKo .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1BQJBmi7EeYHuyKo .error-icon{fill:#552222;}#mermaid-svg-1BQJBmi7EeYHuyKo .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1BQJBmi7EeYHuyKo .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1BQJBmi7EeYHuyKo .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1BQJBmi7EeYHuyKo .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1BQJBmi7EeYHuyKo .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1BQJBmi7EeYHuyKo .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1BQJBmi7EeYHuyKo .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1BQJBmi7EeYHuyKo .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1BQJBmi7EeYHuyKo .marker.cross{stroke:#333333;}#mermaid-svg-1BQJBmi7EeYHuyKo svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1BQJBmi7EeYHuyKo p{margin:0;}#mermaid-svg-1BQJBmi7EeYHuyKo .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-1BQJBmi7EeYHuyKo text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-1BQJBmi7EeYHuyKo .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-1BQJBmi7EeYHuyKo .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-1BQJBmi7EeYHuyKo .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-1BQJBmi7EeYHuyKo .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-1BQJBmi7EeYHuyKo #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-1BQJBmi7EeYHuyKo .sequenceNumber{fill:white;}#mermaid-svg-1BQJBmi7EeYHuyKo #sequencenumber{fill:#333;}#mermaid-svg-1BQJBmi7EeYHuyKo #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-1BQJBmi7EeYHuyKo .messageText{fill:#333;stroke:none;}#mermaid-svg-1BQJBmi7EeYHuyKo .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-1BQJBmi7EeYHuyKo .labelText,#mermaid-svg-1BQJBmi7EeYHuyKo .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-1BQJBmi7EeYHuyKo .loopText,#mermaid-svg-1BQJBmi7EeYHuyKo .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-1BQJBmi7EeYHuyKo .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-1BQJBmi7EeYHuyKo .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-1BQJBmi7EeYHuyKo .noteText,#mermaid-svg-1BQJBmi7EeYHuyKo .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-1BQJBmi7EeYHuyKo .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-1BQJBmi7EeYHuyKo .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-1BQJBmi7EeYHuyKo .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-1BQJBmi7EeYHuyKo .actorPopupMenu{position:absolute;}#mermaid-svg-1BQJBmi7EeYHuyKo .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-1BQJBmi7EeYHuyKo .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-1BQJBmi7EeYHuyKo .actor-man circle,#mermaid-svg-1BQJBmi7EeYHuyKo line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-1BQJBmi7EeYHuyKo :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 上报指标 阈值判断 触发告警 同类合并/去重 邮件/钉钉/短信 写入告警记录 发送结果 更新状态
图示说明:告警通知时序:监控脚本上报指标,决策规则判断阈值,告警合并器去重后分发通知并持久化记录。

dolphindb
// 告警通知脚本
def checkSystemAlerts() {
alerts = table(1:0, `level`message`time, [STRING, STRING, TIMESTAMP])
if (cpu().usage > 80) insert into alerts values("warning", "CPU使用率过高", now())
if (mem().used * 100.0 / mem().total > 85) insert into alerts values("warning", "内存使用率过高", now())
return alerts
}
def checkDatabaseAlerts() {
alerts = table(1:0, `level`message`time, [STRING, STRING, TIMESTAMP])
failed = select count(*) from query_log where status="failed" and start_time > now()-3600000
if (failed > 10) insert into alerts values("critical", "失败查询过多: " + string(failed), now())
return alerts
}
def checkClusterAlerts() {
alerts = table(1:0, `level`message`time, [STRING, STRING, TIMESTAMP])
nodes = getClusterPerf()
if (sum(iif(nodes.status=="active",1,0)) < nodes.rows())
insert into alerts values("critical", "存在离线节点", now())
return alerts
}
def sendNotification(alert) {
sendEmail("admin@example.com", "DolphinDB告警", alert.message)
sendDingTalk(alert.message)
if (alert.level == "critical")
sendSMS("13800138000", alert.message)
}
def alertNotificationTask() {
allAlerts = checkSystemAlerts()
.unionAll(checkDatabaseAlerts())
.unionAll(checkClusterAlerts())
for (alert in allAlerts) sendNotification(alert)
insert into active_alerts select * from allAlerts
}
def startAlerting() {
share streamTable(100000:0, `time`level`message`status,
[TIMESTAMP, STRING, STRING, STRING]) as active_alerts
scheduleJob("alert_check", "告警检查", alertNotificationTask,
00:05m, true, 2024.01.01, 2099.12.31, 'D')
print("告警通知脚本已启动")
}
预期输出:
startAlerting()执行后,创建一个active_alerts流表,并注册每 5 分钟执行一次的告警检查任务。当 CPU 超过 80% 时发送 warning 级别通知;当失败查询数超过 10 或存在离线节点时,额外发送短信通知。
告警通知有一个常见坑:阈值设置得太低会导致告警风暴,值班人员很快产生"狼来了"效应;阈值设置得太高又会漏掉早期风险。建议采用动态阈值 或同比环比策略,例如"CPU 连续 3 分钟超过 85%"才触发严重告警,而不是单次超过就告警。另外,告警信息里应该包含上下文,例如节点名、当前值、建议处理步骤,而不是只发一句"CPU 高了"。
告警历史表 active_alerts 建议增加 "acknowledged_by" 和 "resolved_at" 字段,支持告警认领和关闭。这样在做故障复盘时,可以清楚地看到每条告警从触发到关闭的完整生命周期,而不是只看到一堆未处理的消息。
七、运维脚本分类对照与告警阈值参考
在落地运维脚本之前,建议先根据业务场景把脚本分类和告警阈值梳理清楚。下面两张表可以直接作为团队内部的运维规范模板。
| 脚本类别 | 主要职责 | 推荐调度周期 | 关键函数/表 |
|---|---|---|---|
| 监控脚本 | 采集系统、数据库、集群指标 | 每 60 秒 | monitor_metrics、db_metrics、cluster_metrics |
| 维护脚本 | 数据清理、索引重建、健康检查 | 每小时/每天/每周 | cleanupExpiredData、rebuildFragmentedIndexes |
| 部署脚本 | 环境检查、配置生成、服务启动 | 手动触发 | autoDeploy、checkEnvironment |
| 升级脚本 | 备份、升级、验证、回滚 | 手动触发 | upgradeDolphinDB、backupBeforeUpgrade |
| 告警脚本 | 阈值判断、通知分发、事件记录 | 每 5 分钟 | alertNotificationTask、active_alerts |
表说明:运维脚本按职责分为监控、维护、部署、升级、告警五类,每类推荐调度周期和关键函数不同,应根据业务 SLA 调整。
| 监控项 | warning 阈值 | critical 阈值 | 建议处理动作 |
|---|---|---|---|
| CPU 使用率 | > 80% | > 95% | warning 时排查高耗查询,critical 时考虑限流或扩容 |
| 内存使用率 | > 85% | > 95% | warning 时检查缓存配置,critical 时降低并发或重启 |
| 磁盘使用率 | > 85% | > 95% | warning 时清理过期数据,critical 时紧急扩容 |
| 失败查询数/小时 | > 10 | > 50 | warning 时分析慢查询日志,critical 时切换只读或回滚 |
| 离线节点数 | ≥ 1 | ≥ 2 | 立即检查网络与进程状态,必要时启动备用节点 |
表说明:高频告警项的 warning/critical 阈值与建议处理动作,实际数值需根据集群规模和业务容忍度调整。
八、实战演练:完整运维工具链落地
前面几节分别讲了监控、维护、部署、告警脚本的实现。本节把它们组合成一个完整的运维工具集,提供统一入口和状态查询能力。这个实战案例展示了自动化运维的真正价值:不是单个脚本跑通,而是把多个脚本编排成一个可持续运行的体系。
下面的脚本提供三个对外接口:initOpsTools 一键启动所有脚本,runMaintenance 手动触发某项维护任务,getOpsStatus 查看当前运行中的任务、调度任务、最近备份和活跃告警。通过 addFunctionView 注册后,可以通过 Web API 或客户端远程调用。
#mermaid-svg-dwEkD4qNwOGzUoPw{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dwEkD4qNwOGzUoPw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dwEkD4qNwOGzUoPw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dwEkD4qNwOGzUoPw .error-icon{fill:#552222;}#mermaid-svg-dwEkD4qNwOGzUoPw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dwEkD4qNwOGzUoPw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dwEkD4qNwOGzUoPw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dwEkD4qNwOGzUoPw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dwEkD4qNwOGzUoPw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dwEkD4qNwOGzUoPw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dwEkD4qNwOGzUoPw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dwEkD4qNwOGzUoPw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dwEkD4qNwOGzUoPw .marker.cross{stroke:#333333;}#mermaid-svg-dwEkD4qNwOGzUoPw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dwEkD4qNwOGzUoPw p{margin:0;}#mermaid-svg-dwEkD4qNwOGzUoPw .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dwEkD4qNwOGzUoPw .cluster-label text{fill:#333;}#mermaid-svg-dwEkD4qNwOGzUoPw .cluster-label span{color:#333;}#mermaid-svg-dwEkD4qNwOGzUoPw .cluster-label span p{background-color:transparent;}#mermaid-svg-dwEkD4qNwOGzUoPw .label text,#mermaid-svg-dwEkD4qNwOGzUoPw span{fill:#333;color:#333;}#mermaid-svg-dwEkD4qNwOGzUoPw .node rect,#mermaid-svg-dwEkD4qNwOGzUoPw .node circle,#mermaid-svg-dwEkD4qNwOGzUoPw .node ellipse,#mermaid-svg-dwEkD4qNwOGzUoPw .node polygon,#mermaid-svg-dwEkD4qNwOGzUoPw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dwEkD4qNwOGzUoPw .rough-node .label text,#mermaid-svg-dwEkD4qNwOGzUoPw .node .label text,#mermaid-svg-dwEkD4qNwOGzUoPw .image-shape .label,#mermaid-svg-dwEkD4qNwOGzUoPw .icon-shape .label{text-anchor:middle;}#mermaid-svg-dwEkD4qNwOGzUoPw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dwEkD4qNwOGzUoPw .rough-node .label,#mermaid-svg-dwEkD4qNwOGzUoPw .node .label,#mermaid-svg-dwEkD4qNwOGzUoPw .image-shape .label,#mermaid-svg-dwEkD4qNwOGzUoPw .icon-shape .label{text-align:center;}#mermaid-svg-dwEkD4qNwOGzUoPw .node.clickable{cursor:pointer;}#mermaid-svg-dwEkD4qNwOGzUoPw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dwEkD4qNwOGzUoPw .arrowheadPath{fill:#333333;}#mermaid-svg-dwEkD4qNwOGzUoPw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dwEkD4qNwOGzUoPw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dwEkD4qNwOGzUoPw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dwEkD4qNwOGzUoPw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dwEkD4qNwOGzUoPw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dwEkD4qNwOGzUoPw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dwEkD4qNwOGzUoPw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dwEkD4qNwOGzUoPw .cluster text{fill:#333;}#mermaid-svg-dwEkD4qNwOGzUoPw .cluster span{color:#333;}#mermaid-svg-dwEkD4qNwOGzUoPw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dwEkD4qNwOGzUoPw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dwEkD4qNwOGzUoPw rect.text{fill:none;stroke-width:0;}#mermaid-svg-dwEkD4qNwOGzUoPw .icon-shape,#mermaid-svg-dwEkD4qNwOGzUoPw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dwEkD4qNwOGzUoPw .icon-shape p,#mermaid-svg-dwEkD4qNwOGzUoPw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dwEkD4qNwOGzUoPw .icon-shape .label rect,#mermaid-svg-dwEkD4qNwOGzUoPw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dwEkD4qNwOGzUoPw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dwEkD4qNwOGzUoPw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dwEkD4qNwOGzUoPw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 启动 initOpsTools
启动监控脚本
启动维护脚本
启动告警脚本
启动备份脚本
指标写入流表
定时清理与检查
阈值判断与通知
全量/增量备份
可视化与告警
图示说明:完整运维工具链启动流程:initOpsTools 同时拉起监控、维护、告警、备份四类脚本,形成闭环。

dolphindb
// 运维工具集实战:统一入口与状态查询
def initOpsTools() {
startMonitoring()
startMaintenance()
startAlerting()
autoBackupScript()
print("运维工具集初始化完成")
}
def runMaintenance(task) {
if (task == "cleanup") {
cleanupExpiredData()
} else if (task == "backup") {
fullBackupTask()
} else if (task == "health_check") {
generateHealthReport(dict(STRING, ANY,
[["system", checkSystemHealth()],
["database", checkDatabaseHealth()],
["cluster", checkClusterHealth()]]))
} else {
throw "未知维护任务: " + task
}
}
def getOpsStatus() {
return dict(STRING, ANY, [
["running_jobs", getRunningJobs()],
["scheduled_jobs", getScheduledJobs()],
["last_backup", getLastBackup()],
["active_alerts", select count(*) from active_alerts where status="open"]
])
}
addFunctionView(initOpsTools)
addFunctionView(runMaintenance)
addFunctionView(getOpsStatus)
print("运维工具集已注册")
预期输出:执行最后几行后,
initOpsTools、runMaintenance、getOpsStatus被注册为函数视图。initOpsTools()一键启动监控、维护、告警和备份;runMaintenance("health_check")手动触发健康检查;getOpsStatus()返回当前运行任务、调度任务、最近备份时间和未关闭告警数。
这个案例展示了自动化运维工具链的核心设计思想:每个脚本职责单一,通过统一的入口函数组合使用。addFunctionView 的作用是把函数暴露为服务接口,方便上层调度平台或 Web 控制台调用。实际落地时,建议把这套脚本纳入 Git 版本控制,并通过 CI/CD 流程进行发布,避免"脚本越写越多、越管越乱"。
九、适用边界与风险提示
自动化运维脚本虽然能显著提升效率,但并不是所有场景都适合脚本化。以下几类情况需要特别谨慎:
⚠️ 风险提示:
- 首次部署或重大版本升级:建议在测试环境充分验证后再上生产,脚本中的命令可能因操作系统、依赖版本差异而失败。
- 数据清理任务 :
delete from操作不可逆,执行前务必确认清理条件,并保留快照或备份。 - 告警阈值:默认值(CPU 80%、内存 85%)仅供参考,需根据实际业务负载和集群规模调整,避免误报或漏报。
- 远程连接验证 :脚本中的
xdb连接和密码为示例,生产环境应使用最小权限账号,并通过密钥或配置中心管理凭据。 - 脚本版本管理:运维脚本本身也是代码,建议使用 Git 管理,并在变更前做 diff 审查,避免"改配置顺便把脚本改坏"。
- 权限最小化:脚本中涉及的操作(如删除数据、重启服务)应绑定到独立运维账号,避免使用超级管理员账号执行所有任务。
- 幂等性设计:同一脚本多次执行不应产生副作用。例如,重复创建同名流表应跳过或报错,而不是重复创建导致数据混乱。
- 兼容性说明 :本文函数如
getClusterPerf、submitJob、scheduleJob等基于 DolphinDB 2.0.x 的通用能力,具体参数名称和返回值请以官方文档为准;脚本中部分函数(如sendEmail、sendDingTalk、sendSMS)为示意封装,需根据实际通知服务实现。
十、总结与延伸思考
本文围绕 DolphinDB 2.0.x 生产环境,系统性地介绍了如何构建一套自动化运维脚本体系。从监控、维护、部署、升级到告警通知,每个环节都给出了可复用的脚本示例和边界说明。关键结论是:自动化运维不是把人工操作简单地翻译成代码,而是要通过分层解耦、定时调度、统一入口、版本控制和风险提示,把运维动作变成可观测、可审计、可回滚的标准化流程。
长期坚持这套体系,团队可以把值班人员从重复性操作中解放出来,把精力投入到容量规划、性能优化和架构演进上。最终,运维能力会从"个人经验"沉淀为"组织资产"。
对于刚开始落地自动化的团队,建议不要一次性实现所有脚本,而是先从"监控+告警"入手,先把 Visibility 建立起来;然后再逐步加入数据清理、健康检查等维护任务;最后才是部署升级这种高风险、低频但关键的操作。每引入一个新脚本,都要在测试环境跑至少两周,确认稳定后再推广到生产。
延伸思考:
- 在你的生产环境中,哪些运维动作是最高频、最容易出错的?是否可以考虑优先脚本化?
- 如何在告警的"灵敏度"和"噪声率"之间找到平衡?有没有尝试过动态阈值或机器学习异常检测?
- 当脚本数量增加到几十个甚至上百个时,如何设计统一的命名规范、版本策略和发布流程?