descheduler PodLifeTime 插件实战:基于 Pod 生命周期与状态迁移的精细驱逐策略
云原生运维【免费下载链接】deschedulerDescheduler for Kubernetes项目地址https://gitcode.com/gh_mirrors/de/descheduler点击查看免费下载导读PodLifeTime 是 Kubernetes descheduler 框架中一个高度灵活的 Deschedule 插件它不依赖节点资源利用率而是完全以 Pod 自身的年龄 状态 状态迁移时间为判断依据决定哪些 Pod 该被驱逐。本文以插件官方文档pkg/framework/plugins/podlifetime/README.md为主体结合仓库源码、示例配置与单元/E2E 测试完整讲解其过滤链组合逻辑、全部参数语义、经典使用场景与可直接复用的 DeschedulerPolicy 配置帮助你在集群中实现到期轮换、残留清理、故障回收三类最常见的 Pod 生命周期治理。插件定位在 Descheduler 框架中的角色PodLifeTime 被注册为Deschedule 扩展点插件。在 pkg/descheduler/setupplugins.go 中插件以podlifetime.PluginName即PodLifeTime注册进默认插件注册表并绑定了参数类型PodLifeTimeArgs、校验函数ValidatePodLifeTimeArgs与默认值函数SetDefaults_PodLifeTimeArgspluginregistry.Register(podlifetime.PluginName, podlifetime.New, podlifetime.PodLifeTime{}, podlifetime.PodLifeTimeArgs{}, podlifetime.ValidatePodLifeTimeArgs, podlifetime.SetDefaults_PodLifeTimeArgs, registry)因此在使用时PodLifeTime 必须出现在策略文件的plugins.deschedule.enabled列表中而不是balance或filter等其他扩展点。其核心实现位于 pkg/framework/plugins/podlifetime/pod_lifetime.go参数类型定义在 pkg/framework/plugins/podlifetime/types.go。工作原理过滤链的 AND/OR 组合语义PodLifeTime 的判定逻辑可以概括为一句话所有非空的过滤类别之间是 AND 关系Pod 必须满足每一个已配置的过滤条件才会被驱逐每个过滤类别内部是 OR 关系命中其中任意一项即可满足该类别。源码中 New() 通过podutil.WrapFilterFuncs把各个条件逐一串成一条过滤链命名空间过滤include/exclude与labelSelector是插件级的通用前置过滤与驱逐器的Filter/PreEvictionFilter一并组合maxPodLifeTimeSecondsPod 年龄大于该秒数才通过states任一匹配即通过ORownerKinds按 OwnerReference 的 Kind 做 include/excludeconditions任一条件过滤器命中即通过ORexitCodes任一容器终止退出码匹配即通过OR。最终所有过滤器叠加后Pod 必须全部通过才能进入驱逐候选列表。该语义与项目根文档 README.md 中All non-empty filter categories are ANDed…Within each category, items are ORed的描述完全一致。排序与驱逐最老优先、限额即止候选 Pod 确定后插件调用 SortPodsBasedOnAgepkg/descheduler/pod/pods.go按CreationTimestamp升序原地排序保证最老的 Pod 先被驱逐随后在 Deschedule() 中逐个调用handle.Evictor().Evict()执行驱逐。驱逐循环对两种限额错误做了专门处理pkg/framework/plugins/podlifetime/pod_lifetime.go#L179-L193EvictionNodeLimitError单节点驱逐上限已满continue跳过当前节点继续尝试其他节点上的 PodEvictionTotalLimitError全局总驱逐上限已满直接停止整个插件执行。这些限额maxPodsToEvictPerNode、maxPodsToEvictPerNamespace、maxPodsToEvictTotal由驱逐器在 pkg/descheduler/evictions/evictions.go 中统一强制total 与 per-node 上限在EvictPod内检查并返回对应错误类型PodLifeTime 只需消费它们即可无需自己实现限额逻辑。注Pod 的 PDBPod Disruption Budget约束同样由驱逐器层负责PodLifeTime 不绕过 PDB驱逐请求会正常受 PDB 保护。参数总览下表完整列出了插件支持的配置参数字段名、语义与 types.go 一致参数说明类型必填默认maxPodLifeTimeSeconds年龄超过该秒数的 Pod 才会被驱逐uint否*nilstates按 Pod phase、Pod status reason、容器 waiting/terminated reason 过滤命中任一即匹配[]string否nilconditions仅驱逐满足状态条件见 PodConditionFilter的 Pod[]PodConditionFilter否nilexitCodes仅驱逐存在匹配容器终止退出码的 Pod[]int32否nilownerKinds按 OwnerReference 的 Kind 做 include/excludeOwnerKinds否nilnamespaces限定驱逐的命名空间include 或 excludeNamespaces否nillabelSelector仅驱逐匹配这些标签的 Podmetav1.LabelSelector否nilincludingInitContainers将 state/exitCode 过滤扩展到 init 容器bool否falseincludingEphemeralContainers将 state 过滤扩展到 ephemeral 容器bool否false* 至少需要指定一个过滤条件maxPodLifeTimeSeconds、states、conditions或exitCodes之一否则参数校验失败。这条至少一个过滤条件的硬性校验在 validation.go 中实现同时校验还包括namespaces 与 ownerKinds 的 include/exclude 互斥、labelSelector 合法性、states 取值白名单、conditions 每条至少设置一个字段。defaults.gopkg/framework/plugins/podlifetime/defaults.go目前保持各字段默认 nil/false 不变即所有过滤维度默认关闭未配置即不参与判定。states 字段详解四类状态的 OR 匹配states是 PodLifeTime 最核心的过滤维度。它按OR语义同时匹配四类状态任一命中即通过见 pod_lifetime.go类别取值示例Pod phaseRunning、Pending、Succeeded、Failed、UnknownPod status reasonNodeAffinity、NodeLost、Shutdown、UnexpectedAdmissionError容器 waiting reasonCrashLoopBackOff、ImagePullBackOff、ErrImagePull、CreateContainerConfigError、CreateContainerError、InvalidImageName、PodInitializing、ContainerCreating容器 terminated reasonOOMKilled、Error、Completed、DeadlineExceeded、Evicted、ContainerCannotRun、StartError上述全部取值由 validation.go 中的podLifeTimeAllowedStates白名单强制约束states 中不允许出现白名单之外的值否则校验直接报错。底层匹配由 pkg/descheduler/pod/pods.go 中的HasMatchingContainerWaitingState/HasMatchingContainerTerminatedState辅助函数完成逐条检查容器的State.Waiting.Reason与State.Terminated.Reason是否命中集合。当includingInitContainers为true时InitContainerStatuses的 waiting/terminated reason 也会参与匹配当includingEphemeralContainers为true时EphemeralContainerStatuses同样参与。单元测试 pod_lifetime_test.go 验证了未开启这两个开关时init/ephemeral 容器的CreateContainerError不会被匹配预期驱逐数为 0开启后则能被驱逐预期驱逐数为 1。conditions按状态条件与迁移时间精细过滤conditions允许你针对pod.status.conditions[]做精确匹配非常适合清理已 Succeeded 但残留过久的场景。PodConditionFilter 字段单个条件过滤器内的字段级匹配是AND所有已设置字段必须同时命中未设置的字段不参与检查多个条件过滤器之间是OR——Pod 的任一 condition 满足任意一个过滤器即被驱逐实现见 pod_lifetime.go 的matchesAnyPodConditionFilter与matchesConditionFields字段说明type条件类型如Ready、Initialized、ContainersReadystatus条件状态True、False、Unknownreason条件原因如PodCompletedminTimeSinceLastTransitionSeconds要求匹配条件的lastTransitionTime距今至少该秒数校验规则validation.go每条过滤器至少设置type/status/reason/minTimeSinceLastTransitionSeconds之一否则校验失败。迁移时间语义重点当设置了minTimeSinceLastTransitionSeconds时Pod 的条件必须同时满足 type/status/reason 字段匹配且其lastTransitionTime必须足够久远若该 condition 没有lastTransitionTime零值则视为不匹配。源码逻辑pod_lifetime.goif f.MinTimeSinceLastTransitionSeconds ! nil { if cond.LastTransitionTime.IsZero() { continue } idle : metav1.Now().Sub(cond.LastTransitionTime.Time) if idle 0 || uint(idle.Seconds()) *f.MinTimeSinceLastTransitionSeconds { continue } } return true测试 TestTransitionTimeFiltering 覆盖了三个关键行为2 小时前的旧迁移时间可驱逐、1 分钟前的新迁移时间不可驱逐、迁移时间检查只作用于匹配字段命中的那条 condition另一条 reason 不匹配的 condition 即便迁移时间很新也不影响判定。ownerKinds按控制器类型定向驱逐ownerKinds通过 Pod 的 OwnerReference 的Kind字段做 include/exclude见 pod_lifetime.go字段说明include仅驱逐由这些 Kind 拥有的 Podexclude不驱逐由这些 Kind 拥有的 Podinclude与exclude最多只能设置一个validation.go 强制。测试 TestOwnerKindsFiltering 验证exclude: [Job]时 Job 拥有的 Failed Pod 不被驱逐、非 Job Pod 正常驱逐include: [Job]时只有 Job Pod 被驱逐。典型价值Job 控制器自身会清理已结束的 Pod因此驱逐器通常应该排除 Job 拥有的 Pod避免与 Job 的清理逻辑重复而 ReplicaSet/DaemonSet 等长期运行的控制器则希望被驱逐后由控制器重建。典型使用场景可直接复用以下场景配置全部来自官方 README字段语义与前述参数一一对应。场景一清理闲置过久的 Succeeded Podargs: states: [Succeeded] conditions: - reason: PodCompleted status: True minTimeSinceLastTransitionSeconds: 14400 # 4 hours仅当 Pod 处于Succeeded阶段且存在reasonPodCompleted、statusTrue的 condition且该 condition 的迁移时间距今超过 4 小时才会被驱逐。场景二驱逐 Failed Pod 但排除 Job 拥有的args: states: [Failed] exitCodes: [1] ownerKinds: exclude: [Job] maxPodLifeTimeSeconds: 3600 includingInitContainers: trueFailed Pod 同时满足存在退出码为 1 的已终止容器含 init 容器且年龄超过 1 小时且不属于 Job才被驱逐。exitCodes匹配实现在 pod_lifetime.go测试 TestExitCodesFiltering 验证了命中与未命中两种情形。场景三资源泄漏缓解——定期重启长驻 Podargs: maxPodLifeTimeSeconds: 604800 # 7 days states: [Running]长期运行的 Pod 可能积累内存泄漏本配置让运行超过 7 天的 Running Pod 被驱逐并由控制器重建实现到期轮换。场景四清理 CrashLoopBackOff / ImagePullBackOff 卡死 Podargs: states: [CrashLoopBackOff, ImagePullBackOff]states列表内部是 OR 语义命中任一 waiting reason 即可被驱逐用于快速回收陷入镜像拉取/启动循环的故障 Pod。场景五仅驱逐特定控制器拥有的 Podargs: states: [Succeeded, Failed] ownerKinds: include: [Job] maxPodLifeTimeSeconds: 600只对 Job 拥有的 Succeeded/Failed Pod 生效且要求其存活超过 600 秒适合对已完成但未及时清理的 Job Pod 做兜底回收。完整 DeschedulerPolicy 配置示例示例 A按年龄 状态过滤驱逐1 天轮换apiVersion: descheduler/v1alpha2 kind: DeschedulerPolicy profiles: - name: default plugins: deschedule: enabled: - name: PodLifeTime pluginConfig: - name: PodLifeTime args: maxPodLifeTimeSeconds: 86400 # 1 day states: - Running namespaces: include: - default示例 B基于状态迁移时间的精细驱逐Succeeded 残留 4 小时apiVersion: descheduler/v1alpha2 kind: DeschedulerPolicy profiles: - name: default plugins: deschedule: enabled: - name: PodLifeTime pluginConfig: - name: PodLifeTime args: states: - Succeeded conditions: - reason: PodCompleted status: True minTimeSinceLastTransitionSeconds: 14400 namespaces: include: - default该配置的效果default命名空间中处于Succeeded阶段、带有PodCompletedTrue条件、且该条件最后迁移时间距今超过 4 小时的 Pod 会被驱逐。仓库还提供了两个可直接套用的独立示例文件examples/pod-life-time.yml7 天年龄 Pending/PodInitializing 状态与 examples/pod-life-time-transition.ymlSucceeded PodCompleted 4 小时迁移阈值均以descheduler/v1alpha2API 版本编写可作为--policy-config-file的输入。命令行运行与验证按 docs/user-guide.md 的Balance Cluster By Pod Age用例可通过 CLI 直接加载策略文件运行descheduler -v3 --evict-local-storage-pods --policy-config-filepod-life-time.yml该命令以-v3开启详细日志--evict-local-storage-pods允许驱逐挂载本地存储的 Pod并加载包含maxPodLifeTimeSeconds: 604800的PodLifeTime策略文件即上述7 天轮换配置。文档同时建议为每个业务应用配置 Pod Disruption BudgetPDB以保证驱逐不会导致应用可用性受损。测试与可靠性依据单元测试pkg/framework/plugins/podlifetime/pod_lifetime_test.go约 1367 行覆盖年龄阈值边界605 秒驱逐 / 595 秒不驱逐见TestPodLifeTime_AgeThreshold、各 phase 状态、全部 waiting reason、Pod status reason、init/ephemeral 容器开关、条件过滤、迁移时间过滤、ownerKinds、exitCodes、组合过滤以及驱逐限额per-node / per-namespace / total下的最老优先行为。参数校验测试validation_test.go 验证了无任何过滤条件时报错非法 state 报错并列出完整白名单include/exclude 互斥空 condition 过滤器报错等规则。默认值测试defaults_test.go 确认空参数时各字段保持 nil。E2E 测试test/e2e/e2e_podlifetime_test.go 在真实集群中用 Job 制造 Failed/Succeeded Pod验证States: [Failed]可驱逐、OwnerKinds.Exclude: [Job]不驱逐 Job Pod、Conditions命中与不命中两种结果。这些测试共同锁定了本文所述的所有参数语义与边界行为可作为你自行验证配置时的参照。小结PodLifeTime 的价值在于把时间这一维度引入了驱逐决策从简单的按年龄驱逐到精确到某个状态条件已迁移超过 N 秒的细粒度回收再到按控制器类型、命名空间、标签、退出码的组合筛选它都能在不触碰运行良好 Pod 的前提下完成集群的新陈代谢。配置时请牢记三条准则至少指定一个过滤条件、类别间 AND / 类别内 OR、为关键应用配置 PDB。赞分享云原生运维【免费下载链接】deschedulerDescheduler for Kubernetes项目地址https://gitcode.com/gh_mirrors/de/descheduler点击查看免费下载相关推荐iTerm2-Color-Schemes 完整指南605 款终端配色一键导入与切换教程iTerm2 Color Schemes 完整指南605 款终端配色一键导入与切换教程 刚拿到一台 Mac、装好 iTerm2 的开发者最常见的动作就是去搜开发工具Android Activity 生命周期全解析基于 android-training-course-in-chinese 的回调机制、状态迁移与实例状态保存实战Android Activity 生命周期全解析基于 android training course in chinese 的回调机制、状态迁移与实例状态保存文档教程移动开发Kubernetes Pod 状态与生命周期管理从构成、相位到探针与重启策略的完整实战指南Kubernetes Pod 状态与生命周期管理从构成、相位到探针与重启策略的完整实战指南 Pod 是 Kubernetes 中最小的调度与部署单元理解它的教程云原生容器编排上一篇Thornvigil 战斗动画实战参考运动文件布局、回归测试套件与实机测量工具链下一篇原神数据本地化管理胡桃工具箱 Snap.Hutao 完整使用指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

ant-design-blazor 多彩标签(Colorful Tag)完全指南:预设色、反色与自定义色值的实现原理

ant-design-blazor 多彩标签(Colorful Tag)完全指南:预设色、反色与自定义色值的实现原理

前端UI组件设计系统 【免费下载链接】ant-design-blazor 基于 Ant Design 与 Blazor 的前端组件库。让开发者解放生产力,实现更大价值。 项目地址: https://gitcode.com/ant-design-blazor/ant-design-blazor 点击查看 免费下载 本文围绕 ant-design-bl…

2026/10/12 2:11:12 阅读更多 →
重庆地区地表太阳辐射数据分析与预测大数据项目大数据学习路线

重庆地区地表太阳辐射数据分析与预测大数据项目大数据学习路线

随着全球经济的快速发展和人口的不断增长,天气变化对人类社会的影响越来越大。极端天气事件,如洪水、干旱、飓风和暴雨等,不仅对人类的生命安全造成威胁,还给农业、能源、交通和生态环境等领域带来了巨大的经济损失。因此&#xf…

2026/10/12 2:11:12 阅读更多 →
VibeVibe 基础篇第 5 章验收指南:数字分身从“会回话“走到“更稳地代表你“

VibeVibe 基础篇第 5 章验收指南:数字分身从“会回话“走到“更稳地代表你“

文档教程Vibe Coding示例工程 【免费下载链接】vibe-vibe The First Systematic Vibe Coding Open-Source Tutorial | From Zero to Full-Stack, Empowering Everyone to Build Products with AI | Live at: www.vibevibe.cn ;首个系统化 Vibe Coding 开源教程 | 零…

2026/10/12 2:11:12 阅读更多 →

最新新闻

MySQL 64学时教学大纲拆解:从E-R图到PetStore建库全链路

MySQL 64学时教学大纲拆解:从E-R图到PetStore建库全链路

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

2026/10/12 2:55:40 阅读更多 →
MySQL与PostgreSQL整数类型选型:从INT到BIGINT的避坑指南

MySQL与PostgreSQL整数类型选型:从INT到BIGINT的避坑指南

搞数据库的人,十有八九都遇到过这种场景:建表时图省事顺手写了个INT,看着挺正常,结果业务量上来之后,主键突然撞到天花板,或者磁盘空间莫名暴涨。选整数类型这件事,在 MySQL 和 PostgreSQL 里看…

2026/10/12 2:55:40 阅读更多 →
互联网医院+居家养老:医养协同闭环如何落地

互联网医院+居家养老:医养协同闭环如何落地

晚上九点多,同事给我打电话,说她父亲在老家测出血压180/110,人有点晕。她自己在外地出差,隔着几百公里,语音那头全是慌张。这种场景,做互联网医院和居家养老医养结合项目之前,基本只能干着急&am…

2026/10/12 2:55:40 阅读更多 →
消息队列如何保证数据不丢失?生产、存储、消费三端全解析

消息队列如何保证数据不丢失?生产、存储、消费三端全解析

面试题这东西,十有八九是套路,但“消息队列如何保证数据不丢失”是我见过最容易“背了配置但答不出本质”的一道。很多人上来就背:Kafka 开 acksall、副本设 3、消费者别自动提交,听起来很全,但面试官只要换个问法——…

2026/10/12 2:55:40 阅读更多 →
Git常用命令实战:从核心设计逻辑到高频操作指南

Git常用命令实战:从核心设计逻辑到高频操作指南

每个写代码的人,早晚都要面对版本管理这件事。刚开始我不太在意,直到有一次熬夜改了三天代码,因为一次误操作把整个项目覆盖,才真正体会到版本管理的分量。后来把Git当成每日必用工具,才发现真正高频的“常用命令”就二…

2026/10/12 2:55:40 阅读更多 →
PLC程序质量四层评估模型:从能运行到可维护可演进

PLC程序质量四层评估模型:从能运行到可维护可演进

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

2026/10/12 2:54:40 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天,画面可以做到绝对的锐利、平滑与无瑕。然而,当一张秋日手账插画或拍立得照片过于“平整无瑕”时,往往会散发出一种冰冷生硬的“数码塑料感(Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中,横排(Horizontal Layout)早已经成为了绝对的主流。然而,当我们翻开泛黄的线装古籍、宋版木刻诗集,或是欣赏一张茶道雅集的手写便签时,那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点,很多人心里都会悄悄亮起一盏警示灯。 在心理学上,这种现象有一个专门的称谓——“周日夜晚焦虑症(Sunday Scaries)”。明天又是周一,闹钟又要重新在七点响彻卧房;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/11 14:36:54 阅读更多 →