news 2026/10/5 6:13:29

Bonree Ants流式引擎:面向监控告警的轻量级可观测性管道

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Bonree Ants流式引擎:面向监控告警的轻量级可观测性管道

简介:Bonree Ants流式大数据处理引擎是一套面向Windows平台开发者的轻量级时序数据流式计算框架,专为解决企业级大数据项目中重复造轮子、架构不统一、运维成本高等痛点而设计,适用于中高级Java工程师及大数据平台开发者快速构建准实时指标计算、动态基线预警等能力。资源包共136个文件,含110个核心Java类(如GranuleCalcBolt、AntsConfig、CalcServer等)、13个XML配置文件、6个Shell脚本及1个说明文档(Bonree Ants大数据计算引擎.docx),整体仅397KB,结构紧凑、开箱即用。已有44人学习下载,体现了其在中小规模流式场景中的实用价值。读者可直接获取完整可运行的引擎骨架,涵盖原始数据预处理、自定义算子扩展、多粒度批量计算、容错机制及报警条件判断等关键模块实现,代码组织清晰,便于理解流式计算核心流程与二次开发。

1. Bonree Ants流式大数据处理引擎:不是“又一个实时计算框架”,而是面向监控告警场景的轻量级流式管道中枢

你手头有一套微服务集群,每天产生数百万条日志、指标、链路追踪 Span,想做实时异常检测、动态阈值告警、业务黄金指标秒级聚合——但 Spark Streaming 启动太重、Flink 部署运维成本高、Kafka Streams 又缺开箱即用的告警 DSL。这时,Bonree Ants 流式大数据处理引擎.zip 就不是个普通压缩包,而是一套专为可观测性(Observability)场景打磨的流式处理“工具链集合”:它不追求通用 SQL 引擎或状态一致性语义,而是把“从采集端接流 → 做轻量清洗/转换 → 按规则触发告警 → 写入下游存储”这整条链路,封装成可解压即用、配置驱动、无 Java 环境依赖的二进制管道。它常见于 APM 厂商私有化部署包中,也作为 Bonree 自家监控 SaaS 的边缘侧流式预处理模块存在。如果你正在做监控系统二次开发、需要在资源受限节点(如边缘网关、容器 Pod 内)跑实时规则引擎,或想绕过 Flink/Kafka 复杂拓扑快速验证流式逻辑,Ants 就是那个“能跑通、能调参、能看日志、能立刻上线”的务实选择。它不替代 Flink,但能让你在 30 分钟内,用一个 config.yaml 和一个 zip 包,把 Prometheus Pushgateway 的指标流,变成钉钉/企业微信的实时告警消息。


2. 解压与启动:用最小依赖跑通第一个流式管道

Bonree Ants 引擎本质是一个 Go 编写的单体二进制程序(antsd),其.zip包结构高度标准化。解压后你会看到:bin/antsd(主程序)、conf/(含config.yaml和rules/子目录)、logs/(空目录)、plugins/(预留插件目录)。整个运行不依赖 JVM、Python 或 Node.js,仅需 Linux x86_64 或 macOS ARM64 环境(Windows 不支持,官方明确标注)。启动前必须完成三件事:确认配置文件路径、设置数据源接入点、赋予二进制执行权限。下面以最简场景——从本地文件模拟指标流,触发一条 CPU 使用率超限告警——为例,走通全流程。

2.1 解压与目录结构校验

# 下载得到 bonree-ants-v2.4.1.zip(版本号依实际包名而定) unzip bonree-ants-v2.4.1.zip -d ants-root cd ants-root # 校验关键目录是否存在(缺失任一即无法启动) ls -l bin/ conf/ logs/ plugins/ # 正常输出应包含: # bin/antsd # conf/config.yaml # conf/rules/default.rule # logs/ (空)

提示:conf/rules/下的.rule文件是 Ants 的核心 DSL,非 JSON/YAML,而是类 PromQL + Groovy 表达式的混合语法。不要试图用 VS Code JSON 插件校验它——会报错,这是设计使然。

2.2 修改 config.yaml:绑定输入源与输出目标

打开conf/config.yaml,重点修改三处(其余字段保持默认即可):

# conf/config.yaml 关键片段 input: type: "file" # 支持 file / kafka / http / prometheus file: path: "/tmp/metrics.log" # 注意:必须是绝对路径,且 ants 进程有读权限 tail: true # 实时追加读取,类似 tail -f output: type: "console" # 先用 console 验证,后续可换为 webhook / kafka / elasticsearch console: level: "debug" # 输出原始事件和匹配结果 rules: dir: "./conf/rules/" # 规则目录路径,相对 conf/ 目录 reload_interval: "30s" # 热加载规则,无需重启

这里input.type: file是最轻量的调试入口。你只需提前创建/tmp/metrics.log并写入符合 Ants 默认解析格式的模拟数据(见下一节),就能绕过 Kafka 集群搭建,直接验证流式逻辑。

2.3 准备模拟数据流:按 Ants 默认格式生成指标行

Ants 默认解析key=value键值对格式,每行一条事件,时间戳由@timestamp字段或首字段自动识别。以下命令生成 5 条 CPU 指标(单位 %),其中第 3 条超限(>80%):

cat > /tmp/metrics.log << 'EOF' @timestamp=1717023456000 cpu_usage=72.3 host=web01 service=api @timestamp=1717023457000 cpu_usage=68.1 host=web01 service=api @timestamp=1717023458000 cpu_usage=85.7 host=web01 service=api @timestamp=1717023459000 cpu_usage=79.2 host=web01 service=api @timestamp=1717023460000 cpu_usage=71.5 host=web01 service=api EOF

参数说明:@timestamp必须是毫秒级 Unix 时间戳(13 位),cpu_usage是数值型字段,host/service是标签字段。Ants 会自动将cpu_usage解析为 float,其他字段为 string。若字段含空格或特殊字符,需用双引号包裹(如service="order-api-v2")。

2.4 启动并观察实时日志流

# 赋予执行权限(Linux/macOS) chmod +x bin/antsd # 启动,指定配置路径(必须是绝对路径!) ./bin/antsd --config ./conf/config.yaml # 正常启动日志示例: # INFO[0000] Ants engine started, version=v2.4.1, pid=12345 # INFO[0000] Input plugin 'file' initialized, watching /tmp/metrics.log # INFO[0000] Rule loader watching ./conf/rules/ with interval 30s # DEBUG[0001] Event received: map[@timestamp:1717023456000 cpu_usage:72.3 host:web01 service:api] # DEBUG[0001] Rule 'default.rule' matched event, triggering action...

此时,你已在本地跑通了 Ants 的最小可行流式管道:文件输入 → 规则匹配 → 控制台输出。下一步就是让规则真正“动起来”。


3. 规则编写与热加载:用 DSL 实现动态阈值告警

Ants 的规则引擎不是靠写 Java UDF 或 SQL,而是用一套精简 DSL(Domain Specific Language)描述“什么条件下触发什么动作”。它的语法设计明显偏向运维人员——没有复杂 JOIN,不支持窗口函数,但对标签过滤、数值比较、时间衰减、告警抑制等监控刚需功能做了深度优化。规则文件(.rule)本质是 YAML 结构,但嵌入了表达式块(expr:),这些表达式使用 Ants 自研的轻量解释器执行,性能接近原生 Go 代码。

3.1 default.rule 解析:理解基础语法结构

打开conf/rules/default.rule,你会看到如下结构:

# conf/rules/default.rule name: "cpu-high-usage-alert" description: "当 CPU 使用率连续 3 分钟超过 80%,且 host 标签为 web01 时告警" enabled: true trigger: expr: | cpu_usage > 80 && host == "web01" duration: "3m" # 持续满足条件的时间窗口 group_by: ["host", "service"] # 告警分组键,相同键的事件合并为一条告警 action: type: "log" # 可选 log / webhook / email / kafka log: level: "warn" message: "CPU usage high: {{ .cpu_usage }}% on {{ .host }}/{{ .service }}"

逻辑说明:trigger.expr是核心判断逻辑,支持&&||!运算符、==!=><>=<=比较符,以及in(用于数组包含判断)。duration: "3m"表示该表达式需在连续 3 分钟内每条事件都为 true 才触发,而非单次命中。group_by决定了告警去重粒度——若host=web01且service=api的事件持续超限,只会发出一条告警,避免刷屏。

3.2 扩展规则:添加动态阈值与告警抑制

硬编码80显然不适应所有业务。Ants 支持基于历史数据的动态基线计算,通过baseline()函数实现。同时,为避免凌晨低峰期误报,可加入时间窗抑制:

# conf/rules/cpu-dynamic.rule name: "cpu-dynamic-baseline" enabled: true trigger: expr: | cpu_usage > baseline("cpu_usage", "1h", "p95") * 1.5 duration: "2m" group_by: ["host"] suppress: - expr: "hour(@timestamp) >= 2 && hour(@timestamp) <= 5" # 凌晨 2-5 点不告警 action: type: "webhook" webhook: url: "https://oapi.dingtalk.com/robot/send?access_token=xxx" method: "POST" body: | { "msgtype": "text", "text": { "content": "【动态基线告警】{{ .host }} CPU 使用率 {{ .cpu_usage }}%,超出 1 小时 P95 基线 {{ .baseline_value | printf \"%.1f\" }}% 的 1.5 倍" } }

参数说明:baseline("cpu_usage", "1h", "p95")表示:对cpu_usage字段,在过去 1 小时窗口内计算 P95 分位值。suppress是可选数组,每个expr为 true 时,当前告警被抑制。{{ .baseline_value }}是baseline()函数自动注入的上下文变量,无需手动定义。

3.3 热加载验证:改规则不用重启

Ants 在启动时已监听conf/rules/目录。你只需修改.rule文件并保存(如 vim 保存、或echo >> default.rule),30 秒内(由rules.reload_interval控制)控制台就会打印:

INFO[0032] Rule 'default.rule' reloaded, 1 rules active DEBUG[0032] Rule 'cpu-dynamic-baseline' loaded, trigger expr: cpu_usage > baseline("cpu_usage", "1h", "p95") * 1.5

此时新规则立即生效。你可以用curl -X POST http://localhost:8080/debug/rules(Ants 默认开启 debug 接口)查看当前加载的规则列表及最后更新时间,这是线上排查规则是否生效的第一手证据。


4. 输入源对接实战:从 Kafka 到 Prometheus Pushgateway

Ants 的价值在真实生产环境才真正体现——它不孤立运行,而是作为可观测数据流水线的“中间层”,衔接上游采集器与下游告警/存储系统。本节聚焦两个最常用输入源:Kafka(日志/链路流)和 Prometheus Pushgateway(指标流),给出零踩坑的对接配置与验证方法。

4.1 Kafka 输入:消费 topic 并按 partition 均衡分发

Ants 的 Kafka 输入插件基于sarama库,支持 SASL/SSL 认证,但不支持自动创建 topic(需提前建好)。关键配置项:

# conf/config.yaml input: type: "kafka" kafka: brokers: ["kafka-broker1:9092", "kafka-broker2:9092"] topic: "metrics-topic" group_id: "ants-consumer-group" offset: "latest" # 可选 earliest / latest sasl: enabled: true username: "ants-user" password: "xxx" ssl: enabled: true ca_file: "/etc/ants/kafka-ca.pem"

避坑点:group_id必须全局唯一,否则多个 Ants 实例会争抢分区。若需水平扩展,启动 N 个 Ants 实例,它们会自动按 Kafka partition 数量均衡分配(如 6 个 partition,3 个实例,则每个实例消费 2 个 partition)。offset: "latest"是生产环境推荐值,避免重启后重放历史数据导致告警风暴。

验证 Kafka 连通性:

# 在 Ants 启动后,检查日志是否有: # INFO[0000] Kafka consumer created, brokers=[kafka-broker1:9092], topic=metrics-topic # INFO[0000] Assigned partitions: [0 1 2] for group ants-consumer-group # 手动发送测试消息(确保 topic 存在) echo '{"@timestamp":1717023456000,"cpu_usage":85.7,"host":"web01"}' | \ kafkacat -P -b kafka-broker1:9092 -t metrics-topic # 若 Ants 控制台立即打印 DEBUG 日志,则 Kafka 对接成功

4.2 Prometheus Pushgateway 输入:拉取指标并转为事件流

Pushgateway 是 Prometheus 生态中接收短期任务指标的组件。Ants 通过 HTTP 定期拉取其/metrics接口,并将文本格式指标解析为事件。配置要点:

# conf/config.yaml input: type: "prometheus" prometheus: url: "http://pushgateway:9091/metrics" interval: "15s" # 拉取频率,建议 ≥10s 避免压垮 Pushgateway timeout: "5s" # 可选:只拉取特定 job 的指标 params: "match[]": 'job="batch-job"'

参数说明:interval: "15s"是心跳间隔,Ants 会每 15 秒发起一次 HTTP GET 请求。params用于传递 Prometheus 查询参数,match[]支持正则(如job=~"batch.*")。Ants 内置解析器能识别# TYPE注释行,自动将cpu_usage{job="batch-job",instance="host1"} 78.2转为map[cpu_usage:78.2 job:"batch-job" instance:"host1"]。

验证 Pushgateway 对接:

# 确保 Pushgateway 中有指标(例如用 curl 模拟推送) curl -X POST -H "Content-Type: text/plain" --data-binary " cpu_usage{job=\"batch-job\",instance=\"host1\"} 78.2 memory_usage{job=\"batch-job\",instance=\"host1\"} 65.1 " http://pushgateway:9091/metrics/job/batch-job/instance/host1 # 查看 Ants 日志,应出现: # DEBUG[0015] Prometheus scrape success, got 2 metrics from http://pushgateway:9091/metrics # DEBUG[0015] Event parsed: map[cpu_usage:78.2 job:batch-job instance:host1]

4.3 多输入源共存:同一引擎并行处理不同数据流

Ants 支持input配置为数组,即一个antsd实例可同时消费 Kafka、文件、HTTP 等多种源。这极大降低运维复杂度——你无需为日志、指标、链路各起一套 Flink 作业:

# conf/config.yaml input: - type: "kafka" kafka: { ... } # 如前配置 - type: "file" file: { path: "/var/log/app.log", tail: true } - type: "http" http: port: 8081 # 开启内置 HTTP server,接收 POST /event # 支持 JSON 或 key=value 格式

注意:多输入源下,所有事件统一进入同一个规则引擎。trigger.expr中可通过source字段区分来源(Ants 自动注入source: "kafka"/"file"/"http"),例如:source == "kafka" && cpu_usage > 90。


5. 避坑指南:5 个让工程师深夜重启服务的真实问题

Ants 的易用性背后藏着几个极易触发的“玄学”故障点。这些不是文档里写的 Bug,而是大量私有化部署中反复出现的血泪经验。以下每一条都来自真实翻车现场,附带现象、根因和可立即执行的解决命令。

5.1 现象:启动时报错failed to copy spatial iop zip,进程退出

  • 原因:这不是 Ants 本身的错误,而是.zip包在 Windows 下下载/解压时被杀毒软件(尤其是国内某款“安全卫士”)劫持,篡改了plugins/目录下的spatial-iop.zip文件头,导致 Ants 校验失败。该插件用于地理围栏计算,非核心功能,但校验逻辑强制要求。
  • 解决:
    # 删除被污染的插件(不影响基础流式功能) rm -f plugins/spatial-iop.zip # 或从干净环境重新下载完整 zip 包,用 sha256sum 校验 sha256sum bonree-ants-v2.4.1.zip # 对比官网公布的 checksum

5.2 现象:Kafka 输入无日志,Assigned partitions不出现

  • 原因:Kafka broker 的advertised.listeners配置未正确指向外部可访问地址,导致 Ants 消费者连接到内网 IP(如10.0.1.100:9092),而该 IP 在 Ants 所在宿主机不可达。
  • 解决:
    # 在 Kafka broker 机器上执行,确认 advertised.listeners 是否为公网/容器网络 IP docker exec kafka cat /opt/kafka/config/server.properties | grep advertised.listeners # 若为内网 IP,需修改并重启 Kafka # 或临时在 Ants 机器 hosts 中映射(仅测试用) echo "10.0.1.100 kafka-broker1" | sudo tee -a /etc/hosts

5.3 现象:Prometheus 输入拉取成功,但cpu_usage字段始终为 0

  • 原因:Pushgateway 返回的指标文本中,cpu_usage行前面有# HELP或# TYPE注释行,而 Ants 的解析器默认跳过所有以#开头的行。若指标定义顺序错误(如# TYPE cpu_usage gauge在cpu_usage{...} 78.2之后),则解析失败。
  • 解决:
    # 手动请求 Pushgateway,检查返回顺序 curl http://pushgateway:9091/metrics | head -20 # 正确顺序应为: # HELP cpu_usage CPU usage percent # TYPE cpu_usage gauge # cpu_usage{job="batch-job",instance="host1"} 78.2 # 若顺序错乱,需检查推送客户端(如 Python client)是否调用了 set() 之前未调用 describe()

5.4 现象:规则duration: "3m"不生效,单次命中就告警

  • 原因:duration依赖事件时间戳(@timestamp)的连续性。若上游数据源时间戳乱序(如日志采集延迟、设备时钟漂移),Ants 会认为“3 分钟窗口”被中断,从而降级为单次触发。
  • 解决:
    # 在 config.yaml 中启用时间戳矫正(默认关闭) input: type: "kafka" kafka: { ... } # 新增以下配置 timestamp_correction: enabled: true max_drift: "5m" # 允许最大时钟漂移 5 分钟

    提示:启用后,Ants 会维护一个滑动窗口,对乱序事件进行缓冲重排,但会增加内存占用。

5.5 现象:Webhook 输出钉钉失败,日志显示Post https://oapi.dingtalk.com/...: net/http: request canceled (Client.Timeout exceeded while awaiting headers)

  • 原因:Ants 默认 HTTP 客户端超时时间为 5 秒,而钉钉机器人在高并发时响应可能超过此值,导致请求被 cancel。
  • 解决:
    # conf/config.yaml 中为 webhook 单独配置超时 output: type: "webhook" webhook: url: "https://oapi.dingtalk.com/..." timeout: "15s" # 覆盖全局默认值

6. 进阶技巧:用 Ants 做流式数据质量门禁与低代码告警编排

Ants 最被低估的能力,是它能把“数据质量校验”和“告警路由”这两件事,用配置而非代码完成。这在 CI/CD 流水线集成、多租户 SaaS 告警隔离等场景中,价值远超单纯做阈值告警。我一般会在项目交付时,用以下两个技巧把 Ants 从“告警工具”升级为“数据治理网关”。

6.1 数据质量门禁:拦截脏数据,防止污染下游

在数据接入层,我们常遇到字段缺失、类型错乱、时间戳非法等问题。Ants 的filter动作可在事件进入规则引擎前丢弃不合格数据,且支持自定义拒绝原因:

# conf/rules/data-quality.rule name: "data-quality-gate" enabled: true # 注意:此规则无 trigger.expr,而是 always true,作用于所有事件 always: true action: type: "filter" filter: condition: | # 必填字段检查 !has(@timestamp) || !has(cpu_usage) || !has(host) # 类型检查:cpu_usage 必须是数字 || !is_number(cpu_usage) # 时间戳合理性:不能早于 1 小时前,不能晚于 10 分钟后 || @timestamp < now() - 3600000 || @timestamp > now() + 600000 reason: "missing_field_or_invalid_type"

效果:所有不满足条件的事件,会被静默丢弃,并记录到logs/filter.log(需在 config.yaml 中启用log_filter: true)。下游规则引擎完全看不到脏数据,避免因cpu_usage为空导致cpu_usage > 80报错中断。

6.2 低代码告警编排:用 rule chain 实现 if-else 路由

Ants 支持规则链(Rule Chain),即一个规则的输出可作为下一个规则的输入。这让我们能用纯配置实现“先判断严重等级,再决定通知渠道”的逻辑,无需写任何代码:

# conf/rules/severity-chain.rule name: "severity-router" enabled: true trigger: expr: "cpu_usage > 90" duration: "1m" action: type: "chain" chain: - name: "critical-webhook" # 高危:立即电话+钉钉 condition: "true" output: "webhook-critical" - name: "high-webhook" # 高:仅钉钉 condition: "cpu_usage > 80 && cpu_usage <= 90" output: "webhook-high" - name: "medium-log" # 中:仅记录 condition: "cpu_usage > 70 && cpu_usage <= 80" output: "log-medium" # conf/rules/webhook-critical.rule(独立规则文件) name: "webhook-critical" enabled: true # 此规则由 chain 触发,事件已带 context action: type: "webhook" webhook: url: "https://phone-api.example.com/alert" # ... 其他配置

参数说明:action.type: "chain"表示此规则不执行最终动作,而是根据condition匹配结果,将事件转发给对应name的规则。output字段指定目标规则名(对应.rule文件的name字段)。这种模式让告警策略变得像 Excel 表格一样可维护——运营同学改condition就能调整路由逻辑。

6.3 验证与灰度:用 debug 接口做规则效果预演

上线新规则前,我必做的一步是:用 Ants 内置的/debug/eval接口,传入真实事件 JSON,预演规则匹配结果。这比改完配置再等 30 秒热加载快得多:

# 构造测试事件(注意:必须包含 @timestamp) curl -X POST http://localhost:8080/debug/eval \ -H "Content-Type: application/json" \ -d '{ "@timestamp": 1717023458000, "cpu_usage": 85.7, "host": "web01", "service": "api" }' # 返回示例: # { # "matched_rules": ["cpu-high-usage-alert", "severity-router"], # "evaluated_expressions": [ # {"rule": "cpu-high-usage-alert", "expr": "cpu_usage > 80 && host == \"web01\"", "result": true}, # {"rule": "severity-router", "expr": "cpu_usage > 90", "result": false} # ] # }

这个接口是 Ants 最实用的“后悔药”——它让你在规则生效前,就看到它会对哪些事件触发、触发哪个分支。线上出问题?第一时间 curl 这个接口,传入告警时刻的原始事件,5 秒定位是规则写错,还是数据本身异常。

我习惯把这套流程固化为团队 SOP:新规则必须经过/debug/eval验证 + 本地文件流测试 + Kafka 灰度流量(用group_id隔离)三步,才允许上生产。不是因为 Ants 不可靠,而是因为人写规则时,永远比机器更可能漏掉一个括号或一个空格。希望帮到你。

本文还有配套的精品资源,点击获取

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

C++贪吃蛇源码与讲解视频:游戏循环、STL容器选型一次说透

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

作者头像 李华
网站建设 2026/10/5 6:12:17

SpringBoot应用迁移到BES 9.5.5信创中间件完整改造指南

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

作者头像 李华
网站建设 2026/10/5 6:11:53

STM32计价电子秤设计:HX711称重与OLED交互全解析

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

作者头像 李华
网站建设 2026/10/5 6:11:50

STM32F030RC 驱动 MR25H40CDF SPI MRAM 实战指南

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

作者头像 李华
网站建设 2026/10/5 6:11:33

从复位向量到RTOS第一个任务:STM32上电启动全流程拆解

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

作者头像 李华
网站建设 2026/10/5 6:11:04

基于PIC24与MRAM的工业数据存储方案:SPI驱动与掉电保护设计

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

作者头像 李华