OpenStack 对接 Ceph 后端存储:安装测试报告验证项与故障注入实践
简介这份《OpenStack Ceph分布式存储安装测试报告》面向云计算运维工程师、存储架构学习者及OpenStack平台部署人员聚焦开源云平台与分布式存储的集成实践。报告从云计算基础概念切入系统梳理OpenStack组件构成及其与VMware的差异并深入讲解Ceph的架构原理涵盖OSD、MDS、RGW、MON等核心组件、CRUSH数据映射算法、RADOS强一致性与容错机制以及高性能、高可靠、高扩展等优势同时说明在OpenStack中选择Ceph作为后端存储的考量与测试主机规划。资源包内含1个docx文档约876KB结构完整、目录清晰便于按章节查阅。目前已有254人学习适合希望理解OpenStack与Ceph集成方案、掌握分布式存储选型与部署思路的读者参考。1. OpenStack 接 Ceph 做后端存储一份安装测试报告该验什么很多团队搭完 OpenStack 云平台虚拟机根盘还跑在本地 LVM 上一台计算节点挂了上面几十台虚机跟着陪葬。这时候就会想到 Ceph——把分布式存储接进 OpenStack让卷、镜像、快照都落在多副本池里。但真正动手时问题往往不在“装不装得上”而在“装完怎么证明它真的能用、扛得住、掉盘不炸”。这份安装测试报告要回答的就是 OpenStack 与 Ceph 对接后从集群健康、池策略、Cinder/Glance/Nova 三条链路到故障注入验证的完整闭环。适合正在做私有云存储选型、准备把 Ceph 接进现有 OpenStack 环境的一线运维和存储工程师也适合需要交付一份可复现测试记录的团队。2. 装之前先把 Ceph 集群的底子验清楚2.1 为什么先验 Ceph 再碰 OpenStack顺序搞反是血泪经验里最常见的一种。有人 OpenStack 环境已经跑着业务直接往上面叠 Ceph结果 MON 选举抖动、OSD 频繁 down最后分不清是网络问题还是 OpenStack 侧配置问题。正确做法是Ceph 集群独立验收通过后再让 OpenStack 接入。验收 Ceph 集群核心看三件事MON 仲裁是否稳定、OSD 是否全部 up/in、PG 是否 activeclean。这三项不达标后面所有对接都是空中楼阁。常见做法是用ceph -s和ceph osd tree做第一轮体检再用ceph health detail看有没有隐藏告警。# 查看集群整体状态重点看 health、mon 数量、osd up/in、pg 状态 ceph -s # 查看 OSD 分布确认每个 host 上的 OSD 都在 up 状态 ceph osd tree # 有告警时看明细定位到具体 pool 或 osd ceph health detailceph -s输出里health: HEALTH_OK是底线HEALTH_WARN要逐条消掉再往下走。mon: 3 daemons, quorum a,b,c说明仲裁正常少于 3 个 MON 在生产环境属于高风险。osd: 12 up, 12 in表示所有 OSD 在线且被集群接纳。PG 状态必须是activeclean出现undersized、degraded、peering都说明数据分布还没稳定。2.2 建池时把 pg_num 和副本数一次定对Ceph 池的pg_num和size是后面最难改的两个参数。pg_num改起来会触发数据重平衡生产环境动它等于做一次大规模迁移。所以安装阶段就要按预期容量算好。常见估算方式目标 PG 总数控制在每 OSD 100 到 200 个之间。假设 12 个 OSD目标每 OSD 128 个 PG总 PG 数约 1536三副本池的pg_num取接近 2 的幂次比如 512 或 1024。副本数size生产环境至少 3测试环境可以 2但绝不能 1。# 创建给 OpenStack 用的三副本池 ceph osd pool create volumes 512 512 ceph osd pool create images 128 128 ceph osd pool create vms 256 256 # 设置副本数为 3 ceph osd pool set volumes size 3 ceph osd pool set images size 3 ceph osd pool set vms size 3 # 开启应用模式Cinder/Glance 需要 ceph osd pool application enable volumes rbd ceph osd pool application enable images rbd ceph osd pool application enable vms rbdceph osd pool create的两个数字分别是pg_num和pgp_num新集群里通常设成一样。application enable这步容易被漏掉漏了之后ceph -s会报application not enabled告警虽然不影响基本读写但属于必须消掉的配置债。副本数用ceph osd pool get volumes size复查一遍确认生效。2.3 用 rbd 命令做一轮裸存储读写验证在接 OpenStack 之前先用rbd命令直接对池做读写排除 Ceph 自身问题。这一步能提前暴露网络延迟、OSD 性能瓶颈、认证配置错误。# 创建一块 1G 的测试卷 rbd create testpool/testvol --size 1024 # 映射到本地 rbd map testpool/testvol # 写入测试数据 dd if/dev/zero of/dev/rbd0 bs1M count512 oflagdirect # 查看卷信息 rbd info testpool/testvol # 清理 rbd unmap /dev/rbd0 rbd rm testpool/testvoloflagdirect绕过页缓存测的是真实落盘性能。如果这一步dd速度只有几 MB/s说明 OSD 所在磁盘或网络有问题先别急着接 OpenStack。rbd info里重点看size、features、orderfeatures里如果有layering说明支持克隆Cinder 的快照克隆功能依赖它。3. 把 Ceph 接进 OpenStack 的三条链路3.1 Glance 后端切到 Ceph镜像不再占本地盘Glance 默认用本地文件存镜像镜像一多控制节点磁盘就爆。切到 Ceph 后镜像直接存进images池多副本保护还能被 Cinder 克隆加速创建卷。配置在/etc/glance/glance-api.conf[DEFAULT] show_image_direct_url True [glance_store] stores rbd default_store rbd rbd_store_pool images rbd_store_user glance rbd_store_ceph_conf /etc/ceph/ceph.conf rbd_store_chunk_size 8show_image_direct_url True是给 Cinder 用的开启后 Cinder 能直接从镜像的 RBD 位置克隆卷省掉一次全量下载。rbd_store_chunk_size 8表示镜像按 8MB 分块大镜像并发上传时这个值影响吞吐常见范围 4 到 8。改完重启glance-api然后传一个镜像验证openstack image create --disk-format qcow2 --container-format bare \ --file cirros.qcow2 --public cirros-ceph # 确认镜像落在 rbd 池里 rbd ls imagesrbd ls images能看到以镜像 UUID 命名的块设备说明 Glance 已经写进 Ceph。如果openstack image list有记录但rbd ls为空多半是stores没配对或者 glance 用户没有池权限。3.2 Cinder 卷后端多后端配置与卷类型绑定Cinder 接 Ceph 是重头戏。生产环境通常配多个后端比如 SSD 池和 HDD 池分开通过卷类型让用户选择。/etc/cinder/cinder.conf关键配置[DEFAULT] enabled_backends ceph-ssd,ceph-hdd [ceph-ssd] volume_driver cinder.volume.drivers.rbd.RBDDriver rbd_pool volumes_ssd rbd_ceph_conf /etc/ceph/ceph.conf rbd_user cinder rbd_secret_uuid libvirt secret uuid rbd_flatten_volume_from_snapshot False volume_backend_name ceph-ssd [ceph-hdd] volume_driver cinder.volume.drivers.rbd.RBDDriver rbd_pool volumes_hdd rbd_ceph_conf /etc/ceph/ceph.conf rbd_user cinder rbd_secret_uuid libvirt secret uuid rbd_flatten_volume_from_snapshot False volume_backend_name ceph-hddrbd_secret_uuid必须和计算节点 libvirt 里配置的 secret 一致否则虚机挂载卷时会失败。rbd_flatten_volume_from_snapshot False保留克隆链创建快照卷更快但要注意克隆链过长会影响性能定期做 flatten 是常见运维动作。配完重启cinder-volume然后建卷类型并绑定后端openstack volume type create ceph-ssd openstack volume type set --property volume_backend_nameceph-ssd ceph-ssd openstack volume type create ceph-hdd openstack volume type set --property volume_backend_nameceph-hdd ceph-hdd建卷时指定类型验证落在对应池openstack volume create --size 10 --type ceph-ssd test-ssd-vol rbd ls volumes_ssd3.3 Nova 临时盘与虚机磁盘libvirt secret 不能漏Nova 本身不直接管 Ceph但虚机挂载 Cinder 卷时libvirt 需要通过 secret 访问 Ceph。每个计算节点都要配。# 生成 secret在控制节点执行一次记录 uuid ceph auth get-key client.cinder /tmp/cinder.key cat /tmp/secret.xml EOF secret ephemeralno privateno uuidxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/uuid usage typeceph nameclient.cinder secret/name /usage /secret EOF virsh secret-define --file /tmp/secret.xml virsh secret-set-value --secret xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \ --base64 $(cat /tmp/cinder.key)这个 UUID 就是cinder.conf里的rbd_secret_uuid。两边必须完全一致差一个字符虚机就起不来报Unable to access volume。配完在计算节点重启nova-compute然后从卷启动一台虚机验证。4. 安装测试报告里必须覆盖的验证项4.1 功能验证卷、快照、克隆、迁移一份能交付的测试报告功能验证至少覆盖这几项每项记录操作命令和结果验证项操作预期结果卷创建从镜像创建 10G 卷卷状态 availablerbd 池可见卷挂载挂到虚机并格式化虚机内可读写快照对卷打快照快照状态 available克隆从快照创建新卷新卷可独立挂载卷迁移同后端不同主机迁移迁移后数据完整镜像启动从 Ceph 镜像启动虚机虚机正常进入系统每项都要记录耗时。卷创建耗时能反映 Ceph 集群写入延迟克隆耗时能反映是否走了 COW 快速路径。如果克隆一个 10G 卷花了几分钟说明rbd_flatten_volume_from_snapshot配置或克隆链有问题。4.2 性能验证fio 测三副本真实 IOPS功能通了不代表性能能用。用 fio 在虚机内对 Ceph 卷做压测记录 4K 随机写、4K 随机读、1M 顺序写的 IOPS 和延迟。# 4K 随机写 fio --namerandwrite --ioenginelibaio --iodepth32 \ --rwrandwrite --bs4k --direct1 --size2G \ --numjobs4 --runtime60 --group_reporting # 4K 随机读 fio --namerandread --ioenginelibaio --iodepth32 \ --rwrandread --bs4k --direct1 --size2G \ --numjobs4 --runtime60 --group_reportingiodepth32模拟较高并发numjobs4模拟多线程。三副本池的 4K 随机写 IOPS 受网络往返和副本同步影响万兆网下单卷通常能到几千具体看 OSD 介质。如果 IOPS 低得离谱先查ceph osd perf看哪个 OSD 延迟高再查网络是否有丢包。4.3 故障注入拔盘、停 MON、断网这是测试报告里最有价值的部分也是最多人跳过的地方。至少做三项停掉一个 OSD 所在磁盘或systemctl stop ceph-osdN观察ceph -s是否变degraded虚机 IO 是否中断。预期是 IO 短暂抖动后恢复PG 变为activedegraded数据仍在。停掉一个 MON观察集群是否仍能读写。三 MON 集群停一个仲裁仍在业务无感。停两个集群只读或阻塞这是预期行为。断开一个计算节点网络观察该节点上的虚机卷是否可访问。这验证的是客户端侧多路径和重试机制。每项故障注入后记录恢复时间和数据一致性检查结果。恢复后ceph -s必须回到HEALTH_OKPG 回到activeclean。5. 避坑与排查那些让测试报告返工的问题5.1 虚机起不来报 Unable to access volume现象从 Ceph 卷启动虚机虚机卡在启动阶段nova-compute日志报Unable to access volume。原因计算节点 libvirt 的 secret UUID 与cinder.conf里rbd_secret_uuid不一致或者 secret 没设置值。解决在计算节点执行virsh secret-list确认 secret 存在virsh secret-get-value uuid确认有值。对比cinder.conf里的 UUID不一致就重新 define 并 set-value然后重启nova-compute和libvirtd。5.2 卷创建成功但虚机内看不到盘现象openstack volume create成功挂载命令也返回成功但虚机内lsblk看不到新盘。原因虚机需要重启或重新识别 SCSI 总线或者卷挂载到了错误的虚机。解决先openstack server volume list server确认卷挂到了目标虚机。然后在虚机内执行echo - - - /sys/class/scsi_host/host0/scan触发重新扫描。如果还不行检查虚机是否用了 virtio 驱动老内核可能需要重启。5.3 PG 长期 undersized副本数上不去现象ceph -s显示 PGactiveundersized明明设了 size3但实际只有 2 个副本。原因OSD 数量不足或 CRUSH 规则限制了副本分布。比如 3 个 host 各 1 个 OSDsize3 时 CRUSH 可能无法在 3 个 host 上各放一个副本。解决ceph osd tree确认 host 和 OSD 数量。如果 host 数少于副本数要么加 host要么调整 CRUSH 规则允许同 host 多 OSD。生产环境建议 host 数大于等于副本数。5.4 Glance 上传大镜像超时现象上传超过 10G 的镜像时openstack image create卡住或超时。原因glance-api的rbd_store_chunk_size太大或太小或者 Ceph 池 PG 数不足导致写入慢。解决把rbd_store_chunk_size调到 8 或 16重启glance-api。同时检查images池的 PG 数128 个 PG 对大量镜像偏少可以适当增加。上传时用--progress观察进度确认是卡住还是慢。5.5 删除卷后空间不释放现象openstack volume delete成功但ceph df显示池用量没降。原因卷有快照或克隆链RBD 镜像还有子节点引用不能直接删。解决rbd children pool/image查看是否有子镜像。先删子镜像或做 flatten再删父卷。Cinder 侧可以用openstack volume snapshot list确认没有残留快照。6. 让测试报告可复现脚本化验证与基线记录一份测试报告如果只有结论没有过程别人没法复现价值就折半。我一般会把验证步骤脚本化每次环境变更后重跑一遍把结果存档做基线对比。#!/bin/bash # ceph-openstack-check.sh # 用途一键采集 Ceph 与 OpenStack 对接后的关键状态 REPORT/var/log/ceph-openstack-$(date %Y%m%d-%H%M).log { echo Ceph 集群状态 ceph -s ceph health detail ceph osd tree ceph df detail echo 池状态 for pool in volumes images vms; do echo --- $pool --- ceph osd pool get $pool size ceph osd pool get $pool pg_num done echo OpenStack 服务状态 openstack volume service list openstack image list --long | head -20 openstack volume list --all-projects | head -20 echo RBD 卷列表 rbd ls volumes | head -20 rbd ls images | head -20 } $REPORT 21 echo 报告已生成$REPORT这个脚本的价值在于每次变更后跑一次把日志按时间戳存档。出问题时对比变更前后的差异能快速定位是哪一步引入的。比如某次扩容 OSD 后 PG 开始degraded对比前后ceph osd tree就能看出新 OSD 的 CRUSH 位置是否合理。基线记录要包含几个关键数字集群总容量、已用容量、平均 IOPS、平均延迟、PG 总数、每 OSD 的 PG 数。这些数字在后续扩容或调优时是参照系。没有基线调优就是盲调。最后说个习惯我做完任何存储对接都会在测试环境先跑一遍完整的故障注入把恢复时间记下来。生产环境真出故障时心里有底知道大概多久能恢复要不要切流量。这个习惯帮我避免过好几次手忙脚乱。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

git clone 指定路径实战指南:目录控制、参数用法与高频报错排查

git clone 指定路径实战指南:目录控制、参数用法与高频报错排查

简介:面向刚接触Git或常被克隆目录位置问题困扰的开发者,这份PDF教程系统讲解如何将git clone的代码保存到指定路径。内容覆盖基础clone命令的默认行为、目标目录参数写法,以及Sparse Checkout模式检出仓库子目录或多文件的完整步骤&#xff…

2026/10/5 7:04:29 阅读更多 →
组建小型以太网实验:从拓扑规划到ping通的全流程指南

组建小型以太网实验:从拓扑规划到ping通的全流程指南

简介:这份PDF面向计算机网络课程的初学者与实验指导教师,聚焦“组建小型以太网”这一基础实验,帮助读者在缺乏完整指导的情况下独立完成从规划拓扑到连通主机的全过程。资源包内含1个PDF文件,大小约241KB,内容以图文步…

2026/10/5 7:04:29 阅读更多 →
开端:初识计算机语言

开端:初识计算机语言

一、自我介绍本人是软件技术专业的大一新生,这是我的第一篇博客。此前,我对计算机和编程几乎没有任何接触。踏入大学,我的主要目标是从各方面提升自己——无论是学业事业,还是社交沟通与为人处世,我都希望全力以赴。学…

2026/10/5 7:03:29 阅读更多 →

最新新闻

双目相机选型与标定指南:从参数对比到工程避坑

双目相机选型与标定指南:从参数对比到工程避坑

/* 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 10:06:44 阅读更多 →
高通平台外部Codec调试实战:以ES7243E为例的完整流程

高通平台外部Codec调试实战:以ES7243E为例的完整流程

/* 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 10:06:44 阅读更多 →
卡尔曼滤波从原理到实践:五个公式、代码实现与调参经验全解析

卡尔曼滤波从原理到实践:五个公式、代码实现与调参经验全解析

/* 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 10:06:44 阅读更多 →
1019张车牌图训练YOLO:从数据体检到部署的完整指南

1019张车牌图训练YOLO:从数据体检到部署的完整指南

/* 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 10:06:44 阅读更多 →
STM32H743 FreeRTOS下SD卡读写避坑:MDMA与FATFS配置详解

STM32H743 FreeRTOS下SD卡读写避坑:MDMA与FATFS配置详解

/* 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 10:06:44 阅读更多 →
STM32CubeMX生成工程报错?三步清理法亲测有效

STM32CubeMX生成工程报错?三步清理法亲测有效

/* 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 10:05:44 阅读更多 →

日新闻

马斯克杀回智能体战场,Grok 4.5万亿参数撑腰,Cursor接手数字白领项目:用TaoToken统一Key跑通多模型Agent工作流

马斯克杀回智能体战场,Grok 4.5万亿参数撑腰,Cursor接手数字白领项目:用TaoToken统一Key跑通多模型Agent工作流

/* 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 0:00:22 阅读更多 →
AI编程工具插件机制详解:plugin.json配置与加载失败排查指南

AI编程工具插件机制详解:plugin.json配置与加载失败排查指南

1. 从“plugins”这个词说起:它到底在解决什么问题如果你最近在折腾 AI 编程工具,尤其是 Cursor、Codex CLI、Claude Code 这类带 CLI 的编辑器或命令行助手,那你大概率绕不开一个词——plugins。这个词本身不新鲜,从浏览器到 IDE…

2026/10/5 0:00:23 阅读更多 →
第26课:OpenClaw|日志审计与问题诊断:把日志链路改到 TaoToken 的排查清单

第26课:OpenClaw|日志审计与问题诊断:把日志链路改到 TaoToken 的排查清单

/* 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 0:00:23 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* 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 5:06:42 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* 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 1:10:22 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* 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 3:06:17 阅读更多 →

月新闻

我发现了一个新思路:用 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/4 11:40:45 阅读更多 →
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/4 9:43:54 阅读更多 →
黑夜航拍船只数据集训练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/4 20:14:29 阅读更多 →