n9e-modify-task-tpl

star 13.1k

帮助用户生成、修改或排障夜莺(n9e)告警自愈脚本(task_tpl / ibex 脚本)。当用户要求"写一个磁盘清理/重启服务/清理日志/dump 进程/reload nginx"等自愈脚本,或问"自愈脚本怎么拿告警传过来的参数"、"stdin 是什么格式"、"timeout 应该填多少"、"为什么 is_recovered 永远 false"、"为什么自愈脚本拿不到 k8s namespace"、"脚本一直 running 怎么办"时使用。本技能专注**脚本正文层**——若用户要改告警规则、接收人或通知模板,引导到对应 skill。

ccfos By ccfos schedule Updated 6/3/2026

name: n9e-modify-task-tpl description: 帮助用户生成、修改或排障夜莺(n9e)告警自愈脚本(task_tpl / ibex 脚本)。当用户要求"写一个磁盘清理/重启服务/清理日志/dump 进程/reload nginx"等自愈脚本,或问"自愈脚本怎么拿告警传过来的参数"、"stdin 是什么格式"、"timeout 应该填多少"、"为什么 is_recovered 永远 false"、"为什么自愈脚本拿不到 k8s namespace"、"脚本一直 running 怎么办"时使用。本技能专注脚本正文层——若用户要改告警规则、接收人或通知模板,引导到对应 skill。 tags: - internal

夜莺(n9e) 告警自愈脚本(task_tpl)生成

夜莺告警自愈是 ibex 子系统:告警规则的 callbacks 字段写成 ${ibex}/<task_tpl_id> 时,告警事件触发后会按当前 event 的 TargetIdent 拉起对应 task_tpl 的脚本,在那台机器的 categraf 上执行。脚本通过 stdin 拿到本次告警的标签。

本技能专注写/改 task_tpl.script 字段本身——不涉及创建告警规则、配置接收人或编辑通知模板。


1. 适用范围:先确定用户在改哪一层

夜莺告警链路分四层,每层走不同的 skill:

实体 关键文件 本 skill 是否管
自愈脚本 task_tpl task_tpl models/task_tpl.goalert/sender/ibex.go
告警规则 alert_rule models/alert_rule.go 否(用 n9e-create-alert-rule
通知模板 notify_tpl models/notify_tpl.go 否(用 n9e-generate-message-template
通知通道 notify_channel models/notify_channel.go 否(用 n9e-notify-channel-copilot

判断口径:用户原话出现"脚本/shell/bash/python/jq/解析/执行/超时"——本 skill;出现"PromQL/阈值/触发条件"——告警规则;出现"模板/正文/字段渲染"——消息模板;出现"URL/Webhook/签名"——通知通道。


2. 数据模型(用户能填什么、夜莺真用哪些)

TaskTplmodels/task_tpl.go:17-35

字段 类型 含义
id int64 主键,告警规则 callbacks 里 ${ibex}/<id> 引用的就是它
group_id int64 业务组(权限边界,CanDoIbex 会校验)
title string 模板名,执行时会拼成 <title> FH: <hostname> 写到任务标题
script string 本技能主要操作的字段
args string 命令行参数。告警触发执行时如果调用者没传 args,用这里的默认值
tags string 空格分隔的标签,模板列表筛选/分类用,不影响执行
account string 在目标机器上以哪个用户身份运行(如 root
batch int 每批并发主机数。自愈通常只跑触发那台,0 即可
tolerance int 批内允许失败数。单机自愈场景留 0
timeout int 。0 → 默认 30;> 5 天 → 拒绝
pause string 批次间暂停时间表(cron 风格)。自愈基本不用

TaskFormmodels/task_tpl.go:351-365)—— 真正下发时的载体

告警触发自愈走 alert/sender/ibex.go CallIbex → 构造 TaskFormTaskAdd 写入 ibex:

  • AlertTriggered: true —— 边缘机房会走 Redis 直发 categraf,不依赖中心 DB(ibex.go:244-276
  • Stdin: <JSON 字符串> —— 本技能最关心,下一节详解
  • Hosts: []string{event.TargetIdent} —— 只在告警事件命中的那台机器执行
  • Title: tpl.Title + " FH: " + host —— 执行历史里好认(FH = From Host)

CleanFields 硬约束(models/task_tpl.go:137-189

保存或下发前会做这些校验,违反会被直接拒绝:

  • timeout == 0 → 自动设为 30
  • timeout > 3600*24*5 → 报错"longer than five days"
  • title / args / pause / tagsstr.Dangerous 的字符(`$()&& 等)→ 报错
  • script 自动 \r\n → \n(解掉 Windows 编辑器拷贝的 CRLF 问题)
  • script == "" → 报错"script is required"

3. ⚠️ stdin 的真相(最容易踩的坑,必读

3.1 stdin 是 map[string]string 的扁平 JSON,不是 $event 对象

很多用户从消息模板文档抄了 {{$event.RuleName}} 这种语法过来,完全不能用$eventnotify_tpl 模板的上下文,ibex 自愈脚本里没有 template 渲染过程——它拿到的就是一段纯 JSON。

stdin 的构造逻辑alert/sender/ibex.go:118-142):

tagsMap := make(map[string]string)
for _, pair := range event.TagsJSON {        // event 标签一条条展平
    k, v := splitOnce(pair, "=")
    tagsMap[k] = v
}
// 注入 3 个内置 key
tagsMap["alert_severity"]      = strconv.Itoa(event.Severity)
tagsMap["alert_trigger_value"] = event.TriggerValue
tagsMap["is_recovered"]        = strconv.FormatBool(event.IsRecovered)

tags, _ := json.Marshal(tagsMap)             // 整体序列化为 string
in.Stdin = string(tags)

真实 stdin payload 示例

{
  "ident": "host01",
  "instance": "host01:9100",
  "job": "categraf",
  "alert_severity": "2",
  "alert_trigger_value": "92.5",
  "is_recovered": "false"
}

注意:所有值都是字符串(alert_severity"2" 不是 2),脚本里要 parse 成数字得自己转。

3.2 三条"想拿但拿不到"的字段及解法

想拿的字段 为什么拿不到 解法
rule_name / rule_id / trigger_time / severity 中文名 stdin 只装 labels,没装 event 元数据 PromQL 里用 label_replace(..., "rule_name", "...", "...", "...") 把信息注入到标签;或者改用 callback 媒介(HTTP 传整个 event JSON)
接收人 / 通知组 自愈与通知规则是两条独立通道,self_heal 只看 alert_rule.callbacks,不看 notify_rule 想用接收人 → 走 callback 媒介,stdin 不是这条路
被 PromQL 聚合掉的标签(如 k8s namespace/deployment sum(...) 不带 by(...) 会丢标签 告警规则里 PromQL 要写成 sum by (instance, namespace, deployment)(...),被保留的标签才会进 stdin

3.3 三语言读 stdin 模板(生成脚本时必须包含)

shell + jq(最常见)

#!/bin/bash
set -euo pipefail

PAYLOAD=$(cat)                    # 一次性读完 stdin,避免阻塞
[ -z "$PAYLOAD" ] && { echo "FATAL: empty stdin payload"; exit 2; }

IDENT=$(echo "$PAYLOAD" | jq -r '.ident          // empty')
SEV=$(echo   "$PAYLOAD" | jq -r '.alert_severity // "3"')
VAL=$(echo   "$PAYLOAD" | jq -r '.alert_trigger_value // "0"')

[ -z "$IDENT" ] && { echo "FATAL: no ident in stdin"; exit 2; }
echo "[$(date -Iseconds)] ident=$IDENT severity=$SEV value=$VAL"

shell 无 jq 兜底(用 grep+sed,适合最小化容器):

PAYLOAD=$(cat)
IDENT=$(echo "$PAYLOAD" | grep -oE '"ident"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"\([^"]*\)"$/\1/')

python(处理逻辑复杂时):

#!/usr/bin/env python3
import sys, json, time

try:
    data = json.load(sys.stdin)
except json.JSONDecodeError as e:
    print(f"FATAL: bad stdin json: {e}", file=sys.stderr)
    sys.exit(2)

ident = data.get("ident", "")
sev   = int(data.get("alert_severity", "3"))
val   = float(data.get("alert_trigger_value", "0"))

if not ident:
    print("FATAL: no ident", file=sys.stderr)
    sys.exit(2)

print(f"[{time.strftime('%FT%T')}] ident={ident} sev={sev} val={val:.2f}")

go(极少用,给最小骨架):

package main

import (
    "encoding/json"
    "fmt"
    "os"
)

func main() {
    var d map[string]string
    if err := json.NewDecoder(os.Stdin).Decode(&d); err != nil {
        fmt.Fprintln(os.Stderr, "FATAL:", err)
        os.Exit(2)
    }
    fmt.Println("ident:", d["ident"])
}

4. ⚠️ is_recovered 的真相(别写死代码

ibex 自愈不会在恢复事件触发——alert/sender/ibex.go:39-42

func (c *IbexCallBacker) CallBack(ctx CallBackContext) {
    ...
    if event.IsRecovered {
        logger.Infof("event_callback_ibex: event is recovered, event: %s", event.Hash)
        return                       // ← 直接返回,根本不调 handleIbex
    }
    ...
}

所以 stdin 里的 is_recovered 永远是字符串 "false"if [ "$IS_RECOVERED" = "true" ]; then ... 是死代码。

正确路径

  • 想"告警恢复时也跑个动作(发邮件 / 关工单 / 通知 IM)" → 用 notify_rule + callback 媒介,在 notify_rule 里勾选"恢复也通知",callback URL 收到事件 JSON 后再处理。
  • 想"恢复事件触发另一段脚本" → 同上,不要试图复用 task_tpl。

一个核心痛点是"恢复也想触发",而"加 is_recovered 判断"这个常见思路其实是无效解——本质上需要架构层改 ibex 接受恢复事件,或者用户改走 callback 通道。


5. timeout / batch / tolerance / pause —— 数字字段语义

5.1 timeout(秒)

  • 默认 30 秒(CleanFields 自动填)
  • 最长 5 天(更长直接报错;设计上自愈就是短周期任务)
  • 超时后进程会被 SIGKILL,stdout/stderr 在被 kill 之前已写到 task_host 表
  • 与告警评估 interval 没有直接关系——评估周期 60s 不意味着脚本只有 60s 可跑

典型值参考

场景 建议 timeout
reload 配置 / systemctl restart 30 ~ 60
清理日志 / 镜像缓存(数 GB 级) 120 ~ 300
jstack/jmap 取 heap dump 300 ~ 600
yum/apt 安装包 600 ~ 1800

5.2 区分三个"超时类"根因

现象 真根因 解法
docker-compose 自愈脚本超时 误把 telegraf 当 ibex-agent,通道根本没建立 装 categraf(不是 telegraf),看 categraf -test 输出
通知脚本 timeout=0 立即被 kill fe 表单 DefaultTimeout=0 BUG 后端 CleanFields 已修(0 → 30)
自愈脚本执行超时 脚本本身慢(远程 yum / 大文件拷贝) 调大 timeout;考虑改用 p2p 工具传文件(不要拿 ibex 当文件分发)

不要混为一谈——用户说"超时"先问场景。

5.3 batch / tolerance / pause

  • batch:每批并发主机数。告警触发的单机自愈用不上,留 0 即可。手动批量任务(运维场景)才用。
  • tolerance:批内允许失败数。同 batch,自愈场景留 0。
  • pause:批次间暂停(如 00:00-08:00,17:00-23:59 表示这些时间段暂停)。自愈基本用不到——告警随时可能触发,没法靠 pause 做工作时段限制。真的要限制工作时段应该在 alert_rule 的 enable_in_bg / enable_stime 字段做,或者用 notify_rule 的时间窗口。

6. 危险命令清单(生成时必须规避或加护栏)

6.1 黑名单(直接拒绝生成,即使用户明确要求)

命令 风险
rm -rf / / rm -rf /* / rm -rf $UNSET_VAR/ 整盘删除
mkfs.*dd of=/dev/sdashred /dev/sda 文件系统破坏
shutdownrebootinit 0init 6haltpoweroff 整机停机——自愈脚本不应有此权限
iptables -F / ufw disable / firewall-cmd --reload 无备份 网络/安全策略丢失
chmod -R 777 /chown -R nobody:nobody / 权限破坏
curl <非白名单 URL> | shwget ... -O - | bash 远程代码注入
base64/zip/gzip 编码的内嵌 shell 静态审查规避
kubectl delete nodekubectl drain 无 PDB 检查 集群级影响

6.2 灰名单(生成但必须加护栏

命令 必须配套的护栏
systemctl restart <svc> lock file 防止重入;记录 PID before/after
kill -9 kill -TERM 等 10s;再 -9;记录进程信息
docker rm / docker rmi docker ps -a 过滤,禁止删 running 容器
find -delete 先 dry-run(`find ... -print
truncate / > /var/log/xxx 备份到 .bak.<ts>;不动 active log(先 logrotate)
iptables -A 写入 /etc/iptables/rules.v4.bak.<ts> 备份

6.3 通用护栏模板

#!/bin/bash
set -euo pipefail

# (1) 单实例锁,防止并发触发
LOCK=/var/run/$(basename "$0").lock
exec 9>"$LOCK"
flock -n 9 || { echo "another instance running, skip"; exit 0; }

# (2) dry-run 开关(args 里传 --dry-run 即不真做)
DRY_RUN=0
[[ "${1:-}" == "--dry-run" ]] && DRY_RUN=1

# (3) before 状态
echo "=== BEFORE ==="
df -h /var/log

# (4) 主体(按 DRY_RUN 切换)
if [[ $DRY_RUN -eq 1 ]]; then
    find /var/log -name "*.log" -mtime +7 -print | head -20
else
    find /var/log -name "*.log" -mtime +7 -delete
fi

# (5) after 状态
echo "=== AFTER ==="
df -h /var/log

7. 内置场景库(命中"想不到好场景")

每个场景给:典型告警规则名 → 脚本骨架 → timeout 建议 → 风险点。

7.1 磁盘空间类

S1:清理 /var/log 下 7 天前的日志

  • 触发:disk_used_percent{mountpoint="/"} > 90
  • timeout: 120
#!/bin/bash
set -euo pipefail
PAYLOAD=$(cat)
echo "before: $(df -h /var/log | tail -1)"
find /var/log -type f -name "*.log.*" -mtime +7 -print -delete | wc -l
find /var/log -type f -name "*.gz"    -mtime +7 -print -delete | wc -l
echo "after:  $(df -h /var/log | tail -1)"

S2:清理 docker 镜像层缓存

  • 触发:磁盘满 + 主机有 docker
  • timeout: 300
#!/bin/bash
set -euo pipefail
command -v docker >/dev/null || { echo "no docker, skip"; exit 0; }
echo "before:"; df -h /var/lib/docker
docker image prune -af --filter "until=168h"     # 7 天前的镜像
docker builder prune -af --filter "unused-for=168h"
echo "after:"; df -h /var/lib/docker

S3:清理 /tmp 30 天前的文件

  • timeout: 60
#!/bin/bash
set -euo pipefail
find /tmp -xdev -type f -mtime +30 -print -delete | wc -l
find /tmp -xdev -type d -empty -mtime +30 -delete 2>/dev/null || true

S4:journalctl 缩容到 500MB

  • timeout: 60
#!/bin/bash
set -euo pipefail
journalctl --disk-usage
journalctl --vacuum-size=500M
journalctl --disk-usage

7.2 内存 / 进程类

S5:找出 top5 内存进程并 dump(不自动 kill)

  • 触发:mem_used_percent > 90
  • timeout: 60
  • ⚠️ 故意不杀进程——内存高的根因是业务问题,自动 kill 会扩大故障
#!/bin/bash
set -euo pipefail
TS=$(date +%Y%m%d-%H%M%S)
OUT=/var/log/n9e-mem-top5.$TS.txt
{
    echo "=== top5 mem proc @$TS ==="
    ps -eo pid,user,%mem,%cpu,etime,cmd --sort=-%mem | head -6
    echo; free -m
} > "$OUT"
echo "dumped to $OUT"

S6:JVM OOM 自动 dump + 重启

  • 触发:process_jvm_heap_used_percent > 95
  • timeout: 600
  • ⚠️ 需要 stdin 里有 service 标签(PromQL by 里加上)
#!/bin/bash
set -euo pipefail
PAYLOAD=$(cat)
SVC=$(echo "$PAYLOAD" | jq -r '.service // empty')
[ -z "$SVC" ] && { echo "no service tag"; exit 2; }

PID=$(systemctl show -p MainPID --value "$SVC")
[ "$PID" = "0" ] && { echo "service not running"; exit 0; }

TS=$(date +%Y%m%d-%H%M%S)
DIR=/var/log/jvm-dump
mkdir -p "$DIR"

jstack "$PID" > "$DIR/jstack.$SVC.$TS.txt" || true
jmap -dump:live,format=b,file="$DIR/heap.$SVC.$TS.hprof" "$PID" || true

# lock file 防止重入
LOCK=/var/run/n9e-restart-$SVC.lock
exec 9>"$LOCK"; flock -n 9 || { echo "already restarting"; exit 0; }

systemctl restart "$SVC"
sleep 5
systemctl is-active "$SVC"

S7:僵尸进程清理

  • 触发:zombie_process_count > 5
  • timeout: 30
#!/bin/bash
set -euo pipefail
ps -eo stat,ppid,pid,cmd | awk '$1 ~ /^Z/ {print $2}' | sort -u | while read ppid; do
    echo "send SIGCHLD to ppid $ppid"
    kill -SIGCHLD "$ppid" 2>/dev/null || true
done

7.3 服务可用性类

S8:服务端口 down → systemctl restart(带冷却)

  • 触发:net_tcp_connection_count{port="3306"} < 1
  • timeout: 120
#!/bin/bash
set -euo pipefail
PAYLOAD=$(cat)
SVC=$(echo "$PAYLOAD" | jq -r '.service // empty')
[ -z "$SVC" ] && { echo "no service tag"; exit 2; }

# 冷却:5 分钟内已重启过则跳过
COOLDOWN=/var/run/n9e-restart-$SVC.cooldown
if [ -f "$COOLDOWN" ] && [ $(($(date +%s) - $(stat -c%Y "$COOLDOWN"))) -lt 300 ]; then
    echo "in cooldown, skip"
    exit 0
fi

systemctl restart "$SVC"
touch "$COOLDOWN"
sleep 5
systemctl is-active "$SVC"

S9:nginx 配置变更后 reload

  • 触发:手动触发 / file_change_count{path="/etc/nginx"} > 0
  • timeout: 30
#!/bin/bash
set -euo pipefail
nginx -t                          # 验证配置
nginx -s reload
sleep 1
pgrep -x nginx >/dev/null || { echo "nginx died after reload"; exit 1; }
echo "reloaded ok"

S10:crashloop 容器只诊断不重启(k8s)

  • 触发:kube_pod_container_status_restarts_total > 5
  • timeout: 60
  • ⚠️ k8s 已经会自动重启,自愈只做诊断 + 通知
#!/bin/bash
set -euo pipefail
PAYLOAD=$(cat)
NS=$(echo "$PAYLOAD" | jq -r '.namespace // "default"')
POD=$(echo "$PAYLOAD" | jq -r '.pod // empty')
[ -z "$POD" ] && { echo "no pod tag (check PromQL by clause)"; exit 2; }

TS=$(date +%Y%m%d-%H%M%S)
OUT=/var/log/n9e-crash-$NS-$POD.$TS.log
{
    kubectl -n "$NS" describe pod "$POD"
    echo "==="
    kubectl -n "$NS" logs "$POD" --tail=200 --previous || true
} > "$OUT"
echo "diagnosed → $OUT"

7.4 网络 / 系统类

S11:网卡 down → ip link set up(白名单)

  • 触发:net_interface_up{iface=~"eth.+|ens.+"} == 0
  • timeout: 30
#!/bin/bash
set -euo pipefail
PAYLOAD=$(cat)
IFACE=$(echo "$PAYLOAD" | jq -r '.iface // empty')
[ -z "$IFACE" ] && { echo "no iface"; exit 2; }

# 白名单:只允许 eth* / ens* / eno*,避免动到容器/VPN 网卡
[[ "$IFACE" =~ ^(eth|ens|eno) ]] || { echo "iface $IFACE not in whitelist"; exit 0; }

ip link show "$IFACE"
ip link set "$IFACE" up
sleep 2
ip link show "$IFACE"

S12:NTP 偏移 → 重启 chronyd

  • 触发:system_clock_offset_seconds > 60
  • timeout: 30
#!/bin/bash
set -euo pipefail
chronyc tracking | grep -E "System time|Last offset"
systemctl restart chronyd
sleep 5
chronyc makestep
chronyc tracking | grep -E "System time|Last offset"

S13:DNS 解析失败 → 重启 systemd-resolved + 验证

  • timeout: 60
#!/bin/bash
set -euo pipefail
echo "before: $(dig +short @127.0.0.53 example.com || true)"
systemctl restart systemd-resolved
sleep 2
echo "after:  $(dig +short @127.0.0.53 example.com)"

S14:sysctl 参数漂移恢复

  • 触发:node_net_somaxconn != 65535
  • timeout: 10
#!/bin/bash
set -euo pipefail
sysctl -w net.core.somaxconn=65535
grep -q "net.core.somaxconn=65535" /etc/sysctl.d/99-n9e.conf || \
    echo "net.core.somaxconn=65535" >> /etc/sysctl.d/99-n9e.conf

7.5 Windows 场景(PowerShell)

S15:Windows 服务挂掉 → Restart-Service

  • 触发:windows_service_state{state!="Running"} == 1
  • timeout: 120
  • 在 task_tpl 里把 account 改为有权限的用户;脚本类型 script_type: powershell(categraf-win 支持)
$payload = [Console]::In.ReadToEnd() | ConvertFrom-Json
$svc = $payload.service
if (-not $svc) { Write-Error "no service tag"; exit 2 }

Get-Service $svc | Format-Table
Restart-Service $svc -Force
Start-Sleep -Seconds 5
$st = (Get-Service $svc).Status
Write-Output "after restart: $st"
if ($st -ne "Running") { exit 1 }

S16:Windows 磁盘清理(temp + Windows.old)

  • timeout: 300
$before = (Get-PSDrive C).Free / 1GB
Get-ChildItem C:\Windows\Temp -Recurse -Force -ErrorAction SilentlyContinue |
    Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-7) } |
    Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
$after = (Get-PSDrive C).Free / 1GB
Write-Output ("freed {0:N2} GB" -f ($after - $before))

7.6 容器内场景

S17:通过 kubectl exec 在 pod 内重启进程

  • 在中心机房一台带 kubeconfig 的"运维机"上注册 categraf,task_tpl 选这台执行
  • timeout: 120
#!/bin/bash
set -euo pipefail
PAYLOAD=$(cat)
NS=$(echo "$PAYLOAD" | jq -r '.namespace // empty')
POD=$(echo "$PAYLOAD" | jq -r '.pod // empty')
CTN=$(echo "$PAYLOAD" | jq -r '.container // empty')

[ -z "$NS" ] || [ -z "$POD" ] || [ -z "$CTN" ] && { echo "need namespace/pod/container in labels"; exit 2; }

kubectl -n "$NS" exec "$POD" -c "$CTN" -- supervisorctl restart app || \
kubectl -n "$NS" delete pod "$POD"     # 兜底重建

S18:通过 docker exec 在容器内执行

  • timeout: 60
#!/bin/bash
set -euo pipefail
PAYLOAD=$(cat)
CNAME=$(echo "$PAYLOAD" | jq -r '.container_name // empty')
[ -z "$CNAME" ] && { echo "no container_name"; exit 2; }
docker inspect "$CNAME" >/dev/null || { echo "no such container"; exit 2; }
docker exec "$CNAME" sh -c 'kill -HUP 1'    # 让 PID 1 自己 reload

7.7 中间件场景

S19:MySQL 长事务 kill(带白名单 user)

  • 触发:mysql_long_transaction_seconds > 600
  • timeout: 60
#!/bin/bash
set -euo pipefail
PAYLOAD=$(cat)
THRESH=600

# 只 kill 用户连接,绕开系统用户
mysql -N -e "
SELECT trx_mysql_thread_id
FROM information_schema.innodb_trx t
JOIN information_schema.processlist p ON p.ID=t.trx_mysql_thread_id
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > $THRESH
  AND p.USER NOT IN ('root','repl','backup','event_scheduler')
" | while read tid; do
    [ -n "$tid" ] && mysql -e "KILL $tid"
done

S20:Redis 内存超阈值 → 强制 BGREWRITEAOF + 提示扩容

  • timeout: 120
#!/bin/bash
set -euo pipefail
USED=$(redis-cli INFO memory | awk -F: '/^used_memory_rss:/{gsub(/\r/,""); print $2}')
echo "rss: $USED bytes"
redis-cli BGREWRITEAOF
echo "BGREWRITEAOF triggered, NOT auto-restarting redis; please扩容 maxmemory or scale out"

8. 调试与排错

8.1 "脚本一直 running"

按时间顺序对照 issue:

现象 根因 状态
stdout 不刷新就一直 running ibex < v8.3.0 的反馈不闭环 bug 已修复,升级到 v8.3.0+
脚本里有 while true; sleep(自己不退出) 写法问题 加 timeout、加最大循环次数
任务超时但 categraf 没收到 kill 网络抖动 等 task_host 表 status 自动转 failed(依赖心跳)

8.2 "stdin 拿不到"

排查顺序:

  1. 打开手动任务测试:在"任务中心"手动建一次任务,stdin 输入 {"ident":"host01"},看脚本能不能 echo 出来
  2. 看 PromQL by() 子句:自愈缺标签 90% 是这条;告警事件 detail 页里能看到 event.TagsJSON 真实有什么
  3. 看脚本读法read VAR 只读一行;要用 catpython json.load(sys.stdin) 一次读完

8.3 "exit code 0 但服务没真的恢复"

  • 强制写 set -e(任何子命令失败立即退出)
  • 关键步骤后加显式验证:systemctl is-active "$SVC" || exit 1
  • 在 stdout 输出 before/after,让 task_record 可观察

8.4 查执行历史的 SQL(用户问"任务跑没跑成功")

-- 看某个事件触发的所有自愈任务
SELECT id, title, create_at, FROM_UNIXTIME(create_at) AS ts
FROM task_record
WHERE event_id = <event_id>
ORDER BY id DESC;

-- task_host_x 是分片表(按 task_id mod 26)
-- 假设 task_id=12345, 12345%26=21
SELECT id, host, status, stdout, stderr
FROM task_host_21
WHERE id = 12345;

任务历史设计为不可删除——别教用户删,告诉他们这是审计要求。


9. 输出风格

用户问"帮我写一个 X 的自愈脚本"时按这个套路:

  1. 一句话点假设场景("假设触发条件是 disk_used_percent > 90,机器装了 jq")
  2. fenced 脚本块——必含 stdin 解析 + set -euo pipefail + before/after echo + 主体;对应灰名单命令必含护栏
  3. stdin 字段说明:只列脚本里真用到的 key,标明哪些是 PromQL 标签、哪些是 ibex 注入
  4. 运行参数建议timeout=120 / batch=0 / tolerance=0 / pause=空 / account=root,并给理由
  5. 风险与回滚:destructive 动作必给回滚方法或"不可逆"提示

用户问"为什么 X" 时:直接答,不要丢一整段脚本——尤其 is_recovered 类问题先纠概念再说替代方案。

用户要求 destructive 命令:陈述风险 + 给安全替代,绝不直接生成。

Install via CLI
npx skills add https://github.com/ccfos/nightingale --skill n9e-modify-task-tpl
Repository Details
star Stars 13,092
call_split Forks 1,726
navigation Branch main
article Path SKILL.md
More from Creator