news 2026/11/2 1:06:27

自建平台状态雷达:监控系统架构与动态基线异常检测实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
自建平台状态雷达:监控系统架构与动态基线异常检测实践

PLFM_RADAR这个名字听起来多少有点装,但说白了,它就是一个平台状态雷达。做这个东西的原因特别朴素:我手上要负责盯着的平台页面越来越多,光靠人工点开去看,既不可能也没效率。PLFM_RADAR做的事情就是替我盯住平台上那些需要关注的板块,采集数据、比对变化、发现异常、推送提醒,把原来的“定时人工巡检”变成“实时自动感知”。这篇文章就把这个系统的完整设计思路、核心实现和部署运维过程掰开讲一讲,适合想把监控能力落地到自己项目里的开发同学参考。

1. 为什么需要平台雷达:核心需求拆解

1.1 人工巡检的三个死穴

最初我并没有打算搞一个系统,因为量少的时候人工巡检是可行的。平台页面就那么几个,每天早上打开看一眼结构,下午再看一眼有没有异常,一天也就结束了。但等板块数量突破两位数之后,这个模式立刻撑不住了。

第一个死穴是覆盖面不够。如果平台有12个需要关注的板块,每个板块点开检查一遍至少需要40分钟,一上午就这么耗掉了。更致命的是“检查”这个动作是纯肉眼的,页面布局没变、图表颜色正常,不代表背后数据没有问题。

第二个死穴是无法感知历史趋势。人工巡检只能回答“现在是不是正常”,回答不了“相比过去一段时间是不是恢复得太慢了”或者“某些指标从什么时候开始缓慢恶化”。这些慢性的、渐进的异常,光靠人工看,几乎完全抓不到。

第三个死穴是反应速度太慢。从异常发生到被人工发现,这个时间窗口往往是以小时计算的。等发现问题再汇报,影响已经造成了。真正需要的不是“出了问题能查到”,而是“问题还没被用户发现的时候我就已经知道”。

这些死穴叠加在一起,结论就变得很明确:必须用程序代替人工去做高频、持续、可量化的平台状态感知。PLFM_RADAR这个名字,就是在这样的背景下定下来的——它做的事情就是雷达的工作:持续发射探测信号,接收反馈,分析回波,发现目标,标记异常。

1.2 平台雷达要解决的四个核心问题

在设计这个系统之前,我把需求拆成了四个核心问题,也是所有监控系统都必须回答的问题。

第一个问题是完整性。平台的信息是不是都被覆盖到了?有没有哪些板块是盲区?这要求系统有一个清晰的探测模块清单,每一条都对应实际的平台页面和具体的检查项,不留死角。

第二个问题是时效性。信息从平台变化到系统感知到这个变化,中间隔了多久?间隔是秒级、分钟级还是小时级?这决定了这个系统是“雷达”还是“定时打卡器”。我给自己定的目标是核心板块的感知延迟不超过5分钟。

第三个问题是准确性。系统发现的“异常”里面,有多少是真的异常,有多少是平台正常的动态变化被误判了?误报率太高的监控系统会让人失去信任,最终连真实告警也会被忽略。这是监控系统的生死线。

第四个问题是可解释性。告警不能只是说“xxx页面异常”,得说清楚“哪个页面、哪个元素、和之前相比发生了什么变化、可能的原因是什么”。一个说不清楚原因的告警,基本等于没有告警。

这四个问题听起来像是通用监控系统的标准问题,但真正落地的时候,每一个都会踩到具体的坑。下面从架构设计开始逐步展开。

2. 整体架构与核心设计

2.1 四大模块的划分逻辑

PLFM_RADAR的整体架构没有搞得太复杂,主要分成了四个模块:数据采集模块、规则引擎模块、数据存储模块、告警触达模块。这个划分方式经过实际验证是合理的,因为每个模块的生命周期和演进速度都不一样,拆开之后维护成本低很多。

数据采集模块负责从平台页面获取原始数据。这里我采用了“主动探测为主、被动接收为辅”的策略。主动探测就是按照预设的调度策略去请求平台的页面接口或解析页面内容;被动接收则是如果平台本身提供了回调或者消息推送机制,就直接接收平台发送的状态数据。主因是主动探测实现简单、可控性强,但会给平台侧带来额外的请求压力;被动接收对平台压力为零,但依赖平台是否提供这样的能力。两者结合,核心高频板块用被动接收,中低频板块用主动探测,兼顾压力与时效。

规则引擎模块是整个系统的大脑。从采集模块拿到原始数据之后,规则引擎要完成三件事:清洗数据、对照历史基线、判断是否触发异常。我把规则从代码里抽离出来,做成JSON配置的形式,这样不需要重新部署代码就能调整判定逻辑。规则引擎是系统迭代最频繁的部分,因为随着对业务理解的深入,异常判定条件会不断地修改和细化。这里的经验是,规则配置一定要考虑版本管理,否则改坏了连回滚都做不到。

数据存储模块承担两件事:保存原始数据用于回放追溯,保存时间序列数据用于趋势分析。我的设计是,采集到的所有原始数据先落到MongoDB里做全量保存,同时把关键指标写入时序数据库用于后续的业务分析和看板展示。全量保存的好处是出了任何可疑问题都可以回查“当时到底发生了什么”,这是人工巡检完全做不到的。

告警触达模块负责把异常事件推送到相关人员。我接入了企业微信机器人、邮件和短信三种渠道,按照异常级别分级分发。这个模块的逻辑很简单,但有一个重要的设计原则:同一事件在某个时间窗口内只能触发一次告警,不能因为每轮探测都发现异常就反复轰炸。后面在实践环节会详细讲这个去重机制。

2.2 关键指标的设计思路

PLFM_RADAR监控哪些指标?这是这个系统最核心的设计问题。如果指标选错了,后面的所有工作都等于在做无用功。

我从四个角度来定义指标:

可用性类指标,比如页面是否能够正常返回、响应时间是否在容忍范围之内、关键接口的HTTP状态码是否正常。这类指标是最基础的,类似于雷达的“扫描线是否还在正常工作”。

内容一致性指标,比如某个板块的数据更新时间是否晚于平台承诺的更新频率、关键字段的数值波动是否超过正常范围、页面结构是否发生了变化。这类指标用于回答“平台上的信息是否准确”。

时效性指标,比如核心板块从平台发布到系统感知到的时间差、告警从触发到被确认的时间差。这类指标不是用来发现业务异常的,而是用来发现“监控系统自己是否有问题的”,没有这类指标,监控系统本身会变成一个盲区。

样本质量指标,比如数据采集的成功率、数据解析的成功率、告警的误报率。监控系统运营一段时间后,这些指标能真实反映出系统自身的健康状态。我见过太多监控系统变成了摆设,就是因为从来不看自己采集的数据质量。

在设计指标时有一个特别重要的经验:宁可少而精,不要多而杂。指标如果太多,维护成本会失控,而且指标之间会出现大量的噪音干扰。我一开始列了40多个指标,后来砍到了17个核心指标,监控效果不但没有下降,反而因为注意力集中而变得更好。

3. 核心模块实现与实操细节

3.1 数据采集层的三种采集方式

数据采集是PLFM_RADAR的地基,这一层如果做不好,上层所有分析和告警都是空中楼阁。我根据不同的场景采用了三种采集方式。

第一种是直接请求接口。大部分平台页面背后都有JSON接口,直接请求接口比解析HTML稳定得多。我会先用浏览器的开发者工具找到页面数据对应的接口,分析请求参数和响应结构,然后封装成标准的数据源。这种方式最大的优点是数据干净、解析成本低、稳定性强。实战中需要处理的是接口鉴权问题——有些接口需要登录态,这就需要维护一套cookie或token的获取和刷新机制。

第二种是解析HTML页面。有些关键信息没有对应的JSON接口,只能从渲染后的页面里提取。这时候就需要用解析库来处理HTML。XPath和CSS选择器是主要的提取手段。我踩过最大的坑是:某些页面元素是通过JavaScript异步加载的,普通HTTP请求拿到的是空壳HTML,直接解析什么都得不到。解决思路是先用无头浏览器加载完整页面再解析,但无头浏览器的性能和资源占用是一个需要评估的问题。我的方案是能用接口就用接口,只有确认某个信息只能从HTML里拿到时才用无头浏览器,而且严格控制这部分的使用范围。

第三种是事件驱动采集。平台有变更时通过回调通知,或者通过监听平台消息推送来触发采集。这种方式最节省资源,但前提是平台侧提供了这样的机制。我在做PLFM_RADAR时,给平台方提了接入推送的需求,推动了一段时间才落地。如果有条件和平台方协同,优先用这种方式,既能让监控系统更实时,又能大幅降低对平台的请求压力。

采集频率的设置是另一个需要仔细考虑的问题。频率太高,会对平台造成不必要的压力,甚至可能被平台的反滥用机制盯上;频率太低,又无法满足时效要求。我按板块的重要性分了三个档位:核心板块60秒探测一次,重要板块5分钟一次,普通板块30分钟一次。这个频率组合经过实际运行验证,既保证了核心信息5分钟内的感知延迟,又把整体的请求量控制在了合理范围。

3.2 异常检测与预警机制

异常检测是PLFM_RADAR的核心技术难点。最开始我以为“数值超过阈值就告警”就够了,但真正跑起来之后发现完全不是这么回事。平台数据天然是有波动的,比如某些指标在工作日和周末、在白天和深夜就是不一样,用一个固定阈值去套所有的数据,结果就是白天疯狂误报、深夜真实异常反而被漏掉。

我的解决思路是引入动态基线的概念。系统不是拿当前数值去和一个人为设定值比较,而是和一个基于历史数据计算出来的动态区间比较。这个区间会在不同的时间段有不同的宽度和位置。好比雷达不会用同一个灵敏度去扫描所有空域,它需要根据背景噪声自动调整探测灵敏度。动态基线的实现我用了时间窗口滑动平均加上标准差偏移的方式,这样能兼顾趋势和波动。

具体实现上,我用了一个相对轻量的算法。对每个核心指标,维护过去7天同时段的历史数据,计算均值与标准差,然后设定一个动态阈值区间,当前值落在这个区间之外就认为是潜在异常。这个方案的优点是计算成本低,不需要复杂的离线训练流程,对于中小规模的监控场景完全够用。

但动态基线也带来一个新的问题——首次覆盖与漂移。某个板块刚接入监控系统时,没有足够的历史数据来建立基线,这个时间段只能先记录数据不做判定;而当平台业务发生结构性变化时,历史基线的参考价值会下降,需要自动调整窗口。后面在问题排查部分会详细展开这两个坑。

预警机制方面,我设计了两级告警:预警和告警。预警表示“指标有异常趋势,但还不需要立即介入”,通过企业微信机器人推送,目标是一线值班同学,方便提前关注;告警表示“指标已经明显偏离正常范围,影响已经或即将发生”,推送范围扩大到相关负责同学,并且升级为邮件和短信。这个分级极大地提高了告警的可用性。

告警去重是这个机制的润滑剂。我实现了一个简单的去重器:以“指标ID+异常类型”为维度,在一个时间窗口内只允许推送一次。窗口的长度默认设置为30分钟,如果异常持续存在,每30分钟重新推送一次,避免刚开始就连续施压导致没人看消息。这个机制上线后,消息从每天几十条降到了每天几条,而真正的异常一条都没漏。

3.3 可视化与触达渠道的取舍

PLFM_RADAR建立初期,我把大量精力花在配置告警推送渠道上,后来发现只有推送是不够的。告警能让人知道“出事了”,但让人理解“事情有多严重、趋势是怎么演变的”,还需要一个可视化的页面。

可视化我选了Grafana作为前端展示层,数据源接的时序数据库。从指标维度来看,看板分为三层。最上面一层是核心指标卡片区,一眼能看出各个板块当前的状态值;中间一层是关键指标的趋势图,展示过去24小时或7天的走势;最下面一层是异常事件的列表与状态流转,用于追踪每一个告警的处理进展。

在触达渠道的选型上,我的实际体验是:企业微信机器人是目前触达效率和质量综合性价比最高的方式,因为配置成本低、消息格式灵活,可以直接发文本、Markdown和图文卡片;邮件适合作为沉淀和归档渠道,因为它不适合快速响应,但适合事后的追溯和复盘;短信的成本较高,我目前只把短信用在最高级别的告警上,比如核心板块不可用持续超过10分钟。

这里有一个值得分享的取舍经验:不要为了“显得专业”而把所有触达渠道全部接上。渠道越多,维护成本越高,噪音越大。图片和消息发到多个渠道,最终的结果是每个渠道的人都在等别人先响应,反而降低了处理效率。我最后收敛成了“企业微信机器人为主、邮件归档为辅”的组合,短信只保留用于值班电话场景,整体的响应速度和满意度都得到了明显提升。

3.4 数据存储的选型实践

存储层选型也走过弯路。最初我把所有数据全部放在关系型数据库里,没过多久就发现写入瓶颈了——高频采集产生的时序数据让单表膨胀得很快,查询时性能也在明显下降。后来我将存储分离:原始数据落到MongoDB,时序指标落到专门的时序数据库。

这个拆分有几个务实的理由。MongoDB面对结构不固定的数据有天然优势,因为不同板块采集到的原始数据字段可能完全不同,用关系表要不断改表结构,用文档型存储则不需要。另一方面,时序数据如果继续放在普通数据库里,等时间跨度拉长之后,数据量会变得非常恐怖,而时序数据库自带压缩与自动降采样,查询效率和管理成本都要好得多。

数据留存策略也要提前定好。原始数据我保留30天,超过30天直接清理,因为异常排查一般不会追溯到30天以前;时序数据保留更长时间,用来做季度趋势分析。定期清理这个动作看似简单,但如果不做,存储会几何级数增长,最终变成监控系统自己不健康。

4. 部署上线与运行参数配置

4.1 部署架构与资源评估

PLFM_RADAR部署采用了两台Linux服务器,一台负责采集与规则引擎,一台负责存储和可视化。整个系统的组件全部容器化,通过Docker Compose管理。之所以最早选择Docker Compose而不是Kubernetes,是因为这套系统的规模和团队运维能力决定的。对于核心采集服务这种无状态组件,Compose的scale参数已经足够用,Kubernetes反而会让小型团队付出太多维护成本。当采集任务量级再上一个台阶之后,再考虑迁移到更强大的编排平台不迟。

资源评估的经验教训是:监控系统的资源消耗高得比你预想中多。采集端主要消耗在请求处理和解析上;存储端的时间线索引和查询计算同样吃资源。我给采集服务分配了2核4G,存储节点给了4核8G,初期足够运行。如果你要复现这套方案,建议不要一上来就搞最小配置,至少留出50%的余量,否则后续调整会处处受制。

部署的核心环节是做好健康检查。我在编排配置里给每个服务都设置了健康检查接口,一旦服务自身出问题会自动重启。这套机制解决了监控系统一个很尴尬的问题:监控系统本身挂了,所有人都不知道。自愈能力对于无人值守的监控系统来说是刚需能力。

4.2 核心配置参数与调度策略

调度策略是整个系统的节拍器。我的调度器采用“槽位更新”的方式:按探测周期划分时间片,每个时间片扫描一次任务清单,把到期的任务提交到采集队列。核心板块每60秒跑一轮,重要板块每5分钟跑一轮,普通板块每30分钟跑一轮。

接下来是请求超时与重试配置。请求超时时间我统一设置为10秒,超过10秒不返回就标记为本次探测失败,不立即告警,而是进入重试队列。重试采用指数退避的策略,第一次重试在10秒后,第二次在30秒后,第三次在90秒后。三次重试都失败才认定为一次真正的采集失败,再上报异常。这个策略的关键作用是降低瞬时网络抖动造成的误报。实际跑下来效果很好,网络上轻微的波动不会吓到人。

规则引擎的执行周期和其他采集周期不一样,我设置为每15秒扫描一次实时指标缓冲池,而不是跟着任务粒度来跑。这样可以保证高优先级指标在多个探测源都上报异常后,能够立刻被规则引擎捕捉到,不用干等一个周期循环。实时性要求高的场景下,这种“探测异步-检测同步”的设计比“探测和检测绑在同一条链路上”要稳健得多。

5. 常见问题与排查技巧实录

5.1 高频异常问题对照表

运行这段时间下来,我把高频问题整理成了以下表格,供有类似需求的朋友直接对照排查:

问题现象可能原因排查思路
采集成功率持续下降平台接口结构变动或请求被限制查看响应状态码分布,对比历史成功率趋势,对照接口文档检查
同一条告警反复触发告警去重窗口设置过短延长去重窗口,或者增加首报冷却时间
异常判定过于灵敏动态基线窗口数据不足检查历史数据的覆盖时间跨度,增加基线训练期
页面字段解析为空页面结构改版或异步渲染用无头浏览器抓取完整页面,重新定位元素节点
历史数据回查太慢存储层未建索引或数据量过大为时间字段加索引,或调整降采样策略
告警推送没人看渠道消息过于频繁检查分级策略是否正确,是否大量预警级消息压过告警级消息
系统自身长时间未产生数据调度器异常或服务进程退出检查健康检查与自动重启机制是否生效,查看调度器日志

5.2 三个让我印象深刻的坑

第一个坑是关于首次覆盖的。接入一个新的监控板块时,因为缺少历史数据,动态基线是空的。我当时简单粗暴地把空基线当成“没有基线”,直接跳过判定。结果这个板块在接入后的第三天出现了真实异常,但因为基线还没建立,系统完全瞎了。后来我补了一个逻辑:新接入的板块自动进入“学习模式”,学习期间不产生告警,但会输出学习进度日报,让值班同学知道还有多久可以启用正式判定。学习期的时间窗口根据数据粒度和周期性来设置,一般是1到2个完整周期。

第二个坑是平台结构变更引发的批量误报。平台进行了一次页面改版,所有板块的HTML元素都变了,导致采集模块大面积解析失败。那天的告警消息几乎刷屏了。这次教训让我养成了一个习惯:做好结构指纹。不管是指标还是元素定位,都要有一个版本标记,一旦同一时间大量指标同时告警,第一件事先怀疑是不是结构性变化,而不是业务出了问题。这个习惯帮我后来省了特别多精力。

第三个坑是系统自身的监控盲区。PLFM_RADAR上线运行一段时间之后,有一天企业微信消息突然安静了很久。我打开看板一查,发现规则引擎服务因为内存溢出挂掉了,整个监控系统停摆了将近40分钟。那次经历让我意识到监控自己的健康是必须的。我后来加了一个独立的小看门狗进程,不做复杂的事情,就探测规则引擎和采集服务的存活状态。它单独部署在另一台机器上,一旦发现主服务异常就直接告警,避免“监控系统死了却没人知道”的情况再次发生。

6. 配置文件的工程化管理

刚开始的时候,所有规则和参数都是环境变量加硬编码,改一个判断条件要改代码重新部署。这种模式在系统小的时候还能忍,等指标多了、规则复杂了,就彻底失控了。我把所有规则配置抽成了统一的配置文件,并引入了版本管理。

每个指标对应一套配置,包含采集源、解析规则、动态基线的参数、告警等级、去重时间窗口等。修改规则不需要重新构建镜像,只需要把新的配置文件挂载进去,触发一次配置重载即可。我设计了一套简化的配置热加载机制,优点是规则变更的生效时间从小时级缩短到了分钟级。

配置管理还要注意加密问题。采集过程中需要用到一些敏感的凭证信息,这些不能明文写在配置文件里。我的做法是通过环境变量注入,使用容器编排系统自带的密钥管理能力,避免把密钥提交到代码仓库。这个习惯在当时给我避免了不少安全隐患,也建议读者一开始就采用。

规则配置的版本管理是很多人的盲区。我见过太多团队把配置放在服务器上直接改,改完也没有回溯能力。配置一旦出错,想要知道“改之前是什么样子”往往已成奢望。因此我坚持所有配置都走Git管理,每次变更都留下提交记录。这个习惯在排查异常时价值极大。

7. 从0到1的搭建运行手记

如果你准备复刻一套PLFM_RADAR,可以参考以下步骤来搭建最小可用版本。

第一步,确定监控范围和指标清单。不要贪多,先明确最关心的板块和指标,控制在10个以内,跑通后再逐步扩展。第二步,搭建数据采集层,优先对接有JSON接口的板块,跑通数据采集和落库,确认数据在后端能正常写入。第三步,实现基线与异常判定逻辑,先用离线历史数据校准基线参数,别着急接告警推送。第四步,配置告警渠道与去重规则,建议只接一个触达渠道,验证告警消息的完整性和可读性。第五步,部署可视化看板,把核心指标与告警事件整合到一起。最后,根据实际运行效果迭代规则与参数。

关于调度和检测的关系,有一个设计细节值得反复强调:采集调度和规则检测一定要解耦。采集只管拿数据、清洗数据、按指标维度写入缓冲池,规则检测则独立消费这份数据。这样做的好处是,哪怕某一个采集源出现延迟,其他指标的检测不会受影响;哪天要单独扩展检测规则,也不会牵连采集链路。这个解耦设计在系统长期运行过程中的价值远超预期。

另外我建议在早期就给系统加入“事件流”能力。每一条原始数据、每一次规则判定、每一次告警触发,都按时间顺序记录成一个事件。这个事件流在出现诡异案例时,是最可靠的诊断依据。没有事件流,“发生了什么”这个问题很容易变成一笔糊涂账。

PLFM_RADAR做到现在,功能上其实已经超出了当初的设想。唯一没变的是核心思路:用程序持续感知平台状态,把异常发现的时间窗口从小时级缩短到分钟级。整个项目最花精力的部分并不是写代码,而是梳理到底哪些信息值得监控、什么算异常、告警该发给谁。这些问题想清楚之后,代码是水到渠成的。我自己的体会是,监控系统的设计没有终点,它会随着业务的变化不断调整和生长,但保持“结构简单、配置可追溯、异常可解释”这三条原则,后面怎么改都不会走偏。

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

隔离SPI链路设计实战:MAX14431与STM32F401的工业通信方案

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

作者头像 李华
网站建设 2026/11/2 1:05:40

工业SPI隔离方案:MAX14431+STM32F107VCT6根治地环路干扰

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

作者头像 李华
网站建设 2026/11/2 1:05:26

PLFM雷达FPGA实时信号处理全链路实现

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

作者头像 李华
网站建设 2026/11/2 1:05:10

工业SPI隔离方案:数字隔离器选型与PIC18F8722电路设计实战

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

作者头像 李华
网站建设 2026/11/2 1:04:49

工业SPI隔离设计:物理断开与四线同步的工程实践

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

作者头像 李华
网站建设 2026/11/2 1:04:29

轻量级统一监控告警系统:PLFM_RADAR探针设计与实践

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

作者头像 李华