Zookeeper FileTxnLog 事务日志持久化机制与调优实战
1. 从一次磁盘写满的线上告警说起Zookeeper 集群半夜告警磁盘使用率飙到 98%节点频繁进入只读模式。排查下来不是数据量暴涨而是事务日志目录里堆了几万个log.xxxxx文件单个文件不大但数量惊人。当时第一反应是日志没清理干净可翻配置发现autopurge是开着的。真正的问题出在对FileTxnLog这个类的理解上——它负责本地事务日志的持久化但它的文件滚动、预分配、刷盘策略和很多人想当然的写一条日志追加一行完全不是一回事。这篇就围绕FileTxnLog这个 Zookeeper 事务日志持久化的核心类把它的文件结构、写入流程、预分配机制、刷盘时机、滚动策略、以及和FileTxnSnapLog、SyncRequestProcessor的协作关系拆开讲。适合已经用过 Zookeeper、想深入源码层理解持久化机制的同学也适合正在做分布式一致性组件、需要参考 WALWrite-Ahead Log设计的开发者。读完你应该能回答几个问题为什么事务日志要预分配、txnlog文件里到底存了什么、forceSync什么时候触发、zxid是怎么编码进文件名的、以及为什么删日志不能简单rm。先把结论摆前面FileTxnLog的本质是一个带校验、带预分配、按 zxid 滚动、可批量刷盘的追加型日志文件管理器。它不负责日志内容怎么序列化那是SerializeUtils和TxnHeader的事也不负责什么时候该写那是SyncRequestProcessor的事它只专注一件事——把一条条已经序列化好的Txn记录安全、有序、高效地落到磁盘上并且保证重启后能按顺序读回来。2. FileTxnLog 在 Zookeeper 持久化链路里的位置2.1 一次写请求的完整落盘路径要理解FileTxnLog得先看它在整条链路里站在哪。客户端发起一次写请求比如create请求先到 LeaderLeader 把请求包装成Proposal通过 ZAB 协议广播给 Follower。在真正提交之前Leader 和 Follower 都会把这条事务写到本地事务日志——注意是先写日志再改内存这就是典型的 WAL 思想。具体到代码路径请求进入SyncRequestProcessor的队列这个 Processor 单独起一个线程从队列里批量取请求调用ZooKeeperServer.getTxnLogFactory().append()最终落到FileTxnLog.append()。写完一批之后调用commit()commit()里根据配置决定是否forceSync。只有日志落盘成功请求才会继续往FinalRequestProcessor走去修改DataTree内存结构。所以FileTxnLog是持久化的最后一道关口它写失败整个写请求就失败。这也是为什么它的异常处理特别保守——一旦append或commit抛异常Zookeeper 会直接把自己关掉System.exit因为此时内存状态和磁盘状态可能已经不一致继续跑下去会出大问题。2.2 它和 FileTxnSnapLog、Snapshot 的分工很多人会把事务日志和快照搞混。简单说事务日志TxnLog记录每一条写操作追加写文件小但数量多用于崩溃恢复时重放。快照Snapshot某一时刻全量内存数据的序列化文件大但数量少用于加速恢复。FileTxnSnapLog是个门面类同时持有TxnLog和SnapShot两个接口的实现对外提供save、restore、getLastLoggedZxid等方法。FileTxnLog是TxnLog接口的默认实现。恢复时先加载最新快照再用快照的 zxid 之后的事务日志做增量重放两者配合才能把状态恢复到最新。这里有个容易忽略的点事务日志和快照的目录是分开配置的dataLogDir和dataDir。生产环境强烈建议把dataLogDir放到独立磁盘因为事务日志是顺序追加写、对延迟极敏感和快照的随机读写混在一起会互相拖累。我见过不少集群图省事只配dataDir结果高峰期写延迟抖动明显。3. 事务日志文件的物理结构一个文件里到底装了什么3.1 文件命名与 zxid 的编码关系事务日志文件名格式是log.16进制zxid比如log.0000000001。这个 zxid 是该文件第一条事务记录的 zxid不是文件名随便起的。为什么要用 16 进制因为 zxid 是 64 位 long用 16 进制表示长度固定、排序方便FileTxnLog.getLogFiles()直接按文件名排序就能得到时间顺序不需要读文件内容。zxid 本身的结构也值得说高 32 位是 epochLeader 任期低 32 位是计数器。所以同一个 Leader 任期内zxid 单调递增换 Leader 后 epoch 变大zxid 整体跳变。这个设计保证了全局唯一且有序事务日志按 zxid 排序就等于按时间排序。3.2 文件头魔数与版本每个log.xxx文件开头有 16 字节的文件头结构是字段长度说明magic4 字节固定值ZKLG用于识别这是事务日志文件version4 字节版本号当前为 2dbid8 字节数据库 id通常为 0读日志时先校验 magic不匹配直接报错。这个设计是为了防止把快照文件或其他文件误当成事务日志读。version 字段则为将来格式升级留了口子——不同版本可以有不同的解析逻辑。3.3 单条事务记录的组成文件头之后就是一条条事务记录。每条记录的结构是[checksum: 8字节][len: 4字节][txn bytes: len字节]checksum对len txn bytes计算的校验和用的是 Adler32 算法。读的时候会重新计算比对防止磁盘位翻转导致读到脏数据。len后面 txn 字节数组的长度。txn bytes序列化后的事务内容包含TxnHeader记录 zxid、时间、类型等和Record具体操作数据。这里有个细节checksum 是在写入时算好一起写进去的不是单独一个文件。所以每条记录自带校验读取时可以逐条验证。如果某条记录校验失败说明文件损坏Zookeeper 会从这条记录开始截断——这也是为什么事务日志损坏通常只影响最后几条而不是整个文件报废。注意checksum 用的是 Adler32 而不是 CRC32Adler32 计算更快但碰撞概率略高。对于日志场景速度优先且配合 len 字段基本够用。4. 预分配机制为什么文件一创建就是 64MB4.1 预分配解决的是什么问题FileTxnLog创建新文件时不是从 0 字节开始慢慢写而是一次性把文件扩展到预分配大小默认 64MB由preAllocSize控制。这个操作在createTxnLog里通过RandomAccessFile.setLength()完成。为什么要这么做核心是减少文件系统元数据更新和磁盘碎片。如果文件从 0 开始追加每写一段就要更新文件大小、分配新块元数据操作频繁。预分配之后文件大小固定写入只是在已分配的块上覆盖元数据几乎不动。对于顺序追加写的 WAL 场景这个优化对吞吐提升很明显。另一个原因是避免写入过程中磁盘满。预分配时如果磁盘空间不够会立刻失败而不是写到一半才发现没空间。这种提前失败对分布式系统很重要能让节点尽早进入只读或下线而不是产生半截日志。4.2 预分配的边界与滚动触发预分配不是无脑扩到 64MB 就完事。FileTxnLog内部维护一个currentSize每次append后累加。当currentSize 本次写入长度 preAllocSize时触发滚动——关闭当前文件创建新文件新文件名用下一条事务的 zxid。这里有个容易踩的坑预分配大小和实际文件大小是两回事。ls -l看到文件是 64MB但实际有效数据可能只有几 KB。判断日志是否写满不能看文件大小要看currentSize这个逻辑值。我见过有人写监控脚本按文件大小告警结果全是误报。预分配大小也不是越大越好。设太大磁盘空间浪费严重尤其日志滚动频繁时设太小滚动太频繁每次滚动都要创建文件、写文件头、刷盘开销上升。默认 64MB 是个经验值如果单条事务很大比如大批量create可以适当调大如果事务都很小且写入频繁可以调小到 16MB 或 32MB。4.3 预分配与磁盘空间的真实关系假设preAllocSize64MB集群每秒产生 1000 条事务每条平均 200 字节那么每秒写入约 200KB写满一个文件需要约 320 秒。如果保留 3 天的日志大约需要3*24*3600/320 * 64MB ≈ 51GB。这个估算方法比拍脑袋靠谱。但要注意预分配的空间在文件删除前一直占着。所以清理策略必须跟上否则就是开头那个磁盘写满的场景。autopurge.purgeInterval和autopurge.snapRetainCount要配合设置前者是清理间隔小时后者是保留的文件数。生产环境建议purgeInterval1snapRetainCount3到5。5. append 与 commit写入流程的两次关键动作5.1 append 做了什么没做什么append(TxnHeader hdr, Record txn)的核心逻辑检查当前文件是否为空为空则先写文件头。用SerializeUtils.serialize把hdr和txn序列化成字节数组。计算 checksum组装成[checksum][len][data]格式。写入BufferedOutputStream。更新currentSize判断是否需要滚动。注意append 只是写进了缓冲区没有落盘。BufferedOutputStream默认 64KB 缓冲数据先到内存攒够或 flush 时才真正 write 到文件。所以 append 返回成功不代表数据安全这点非常关键。5.2 commit 与 forceSync 的刷盘策略commit()才是真正决定数据是否落盘的地方。它调用stream.flush()把缓冲区刷到文件然后根据forceSync参数决定是否调用fileChannel.force(false)。forceSync从哪来来自SyncRequestProcessor的构造参数最终由配置zookeeper.forceSync控制默认是yes。如果设为nocommit 只 flush 不 force数据到了操作系统页缓存就返回性能高但掉电会丢数据。这里要区分两个层次flush把 Java 缓冲区刷到 OS 页缓存。forcefsync把 OS 页缓存刷到磁盘物理介质。只有 force 之后掉电才不丢。forceSyncyes时每次 commit 都 fsync延迟高但安全forceSyncno时依赖 OS 定期回写吞吐高但有丢失窗口。生产环境除非能接受数据丢失否则别关。实操心得如果写入压力大且对延迟敏感可以考虑批量提交——SyncRequestProcessor本身就是批量取请求的一次 commit 覆盖多条 appendfsync 次数大幅减少。这也是为什么它要单独起线程攒批而不是来一条写一条。5.3 滚动时的收尾动作文件滚动时FileTxnLog会做几件事关闭当前BufferedOutputStream和FileChannel把当前文件加入txnLogStreams列表用于后续读取然后用新 zxid 创建下一个文件。滚动过程中如果抛异常会尝试关闭资源并抛出由上层决定是否终止进程。滚动还有个细节新文件的 zxid 用的是触发滚动的那条事务的 zxid而不是当前文件最后一条的 zxid1。这样保证新文件名精确对应它包含的第一条记录读取时不会错位。6. 读取与恢复getLogFiles 和 read 的配合6.1 如何找到需要重放的日志恢复时FileTxnLog.read(zxid)的逻辑是先getLogFiles()列出所有log.*文件并按文件名排序找到最后一个文件名 zxid 小于等于目标 zxid 的文件作为起点然后从这个文件开始逐条读跳过 zxid 小于目标值的记录返回第一条大于等于目标的记录。getLogFiles的实现有个优化它用FilenameFilter过滤出log.开头的文件解析出 zxid 存进TreeMap天然有序。如果目录里文件很多几万个这个操作会遍历整个目录有一定开销。所以日志清理要及时别让目录膨胀。6.2 读取时的校验与截断读每条记录时先读 8 字节 checksum再读 4 字节 len然后读 len 字节数据重新计算 checksum 比对。不匹配就抛IOException上层FileTxnSnapLog.restore会捕获并停止重放——从损坏点之后的数据全部丢弃。这个行为要理解清楚Zookeeper 认为损坏点之后的数据不可信宁可少恢复也不恢复错。所以如果事务日志损坏可能丢失最后若干条已提交的事务。这也是为什么生产环境要用 RAID 或副本盘降低单盘损坏概率。另外读到文件末尾时如果剩余字节不足以构成一条完整记录比如预分配的空间还没写满会正常结束不算损坏。判断依据是len读出来是否合理——如果 len 是负数或超大说明读到的是预分配的空白区停止即可。6.3 恢复顺序先快照后日志完整恢复流程是FileTxnSnapLog.restore先找最新快照加载得到快照的 zxid然后调用FileTxnLog.read(snapshotZxid1)开始重放日志逐条 apply 到DataTree直到日志读完。重放过程中会更新lastProcessedZxid最终得到集群的最新状态。这里有个性能点快照越新需要重放的日志越少恢复越快。所以snapCount配置默认 10 万决定多久做一次快照。设太小快照频繁影响性能设太大恢复慢。一般保持默认或根据写入量调整。7. 那些文档不会告诉你的实操坑7.1 删日志不能直接 rm有人图省事直接rm log.*这是大忌。原因FileTxnLog内部维护了文件句柄和currentSize状态直接删文件会导致句柄失效、状态错乱轻则写入异常重则数据不一致。正确做法是用autopurge自动清理或者停掉节点后用zkCleanup工具清理。清理时也要保留最近的日志否则重启无法恢复。7.2 preAllocSize 设太大导致磁盘告警前面提过预分配的空间立即占用。如果preAllocSize设成 256MB 而写入量很小一个文件可能几天才写满但 256MB 一直占着。多个这样的文件累积磁盘很快满。建议根据实际写入速率估算一般 64MB 够用写入量特别小的场景可以降到 16MB。7.3 forceSyncno 的隐性风险有些团队为了压测性能把forceSync关了测完忘了改回来上线后遇到机房断电丢了一批已确认的写请求。Zookeeper 的写请求是多数派确认后才返回客户端成功的但如果日志没 fsync掉电后这些已确认的请求可能丢失导致客户端看到的数据和实际不一致。这个坑很隐蔽因为平时不出问题一出就是数据事故。7.4 日志目录和快照目录混用的性能问题dataDir同时放快照和日志时快照的大文件写入会和日志的追加写抢 IO。快照是随机写大文件日志是顺序追加两者 IO 模式冲突。分开到不同磁盘后写延迟通常能降一个档次。如果只能用一块盘至少保证日志目录所在分区有足够空间和 IO 余量。7.5 文件句柄泄漏FileTxnLog在滚动时会关闭旧文件的流但如果异常路径没走到关闭逻辑可能泄漏句柄。长时间运行、频繁滚动的集群要监控进程的打开文件数。ulimit -n要设够否则会出现 Too many open files 导致写入失败。8. 从 FileTxnLog 看 WAL 设计的通用套路把FileTxnLog抽象出来看它其实是一套通用 WAL 设计的教科书案例几个关键决策值得借鉴第一追加写 预分配。顺序写是磁盘最擅长的模式预分配把元数据开销摊薄。任何需要高吞吐持久化的场景都可以参考。第二按逻辑 id 滚动文件。用 zxid 而不是时间戳命名保证排序即时间序且和业务逻辑对齐。滚动阈值用逻辑大小而非物理大小避免预分配干扰。第三每条记录自带校验。checksum 让损坏可检测、可截断而不是整个文件报废。这是容错的基本功。第四flush 和 fsync 分离。给上层选择权在性能和安全之间权衡。默认安全允许显式关闭。第五批量提交。单条 fsync 太贵攒批后一次 fsync 覆盖多条是吞吐和延迟的平衡点。如果你在自研分布式组件需要设计 WAL这五点基本可以直接抄。区别只在于序列化格式、滚动策略和清理策略的细节。9. 调优参数速查与验证方法最后给一份FileTxnLog相关的参数速查以及怎么验证调优效果参数默认值作用调优建议preAllocSize64MB日志文件预分配大小写入量大调大量小调小snapCount100000多少事务后做快照写入频繁可适当调大forceSyncyescommit 时是否 fsync除非能接受丢数据否则保持 yesautopurge.purgeInterval0关闭清理间隔小时生产设 1autopurge.snapRetainCount3保留文件数设 3 到 5dataLogDir同 dataDir日志目录独立磁盘验证方法用zkServer.sh起单机写一批数据观察dataLogDir下文件生成和滚动用jstack看SyncRequestProcessor线程状态用iostat看日志盘的写延迟和 IOPS。调forceSync前后对比写入吞吐能直观看到 fsync 的代价。我个人在实际操作中的体会是FileTxnLog这类组件的调优先搞清楚瓶颈在 fsync 还是磁盘带宽再动参数。盲目调preAllocSize或关forceSync往往解决不了根本问题反而引入新风险。先把监控做起来看清楚每次 commit 的耗时分布再决定往哪个方向优化比拍脑袋靠谱得多。

相关新闻

Nexus 3.15.2 Windows 部署实战:Maven 私服搭建与避坑指南

Nexus 3.15.2 Windows 部署实战:Maven 私服搭建与避坑指南

简介:本资源为 Nexus Repository Manager 3.15.2 Windows 64位官方安装包(RAR格式),专为Java开发团队及DevOps工程师设计,用于在局域网内快速搭建私有Maven仓库,解决多开发人员共享依赖、加速构建、离线缓存…

2026/10/15 14:11:12 阅读更多 →
用Python和SymPy工程化学习高等代数:从定义验证到Jordan标准形

用Python和SymPy工程化学习高等代数:从定义验证到Jordan标准形

简介:本资源是面向数学专业本科生及高等代数自学者的系统性学习笔记,深度对标丘维声教授北京大学《高等代数》课程(B站151讲视频),聚焦线性方程组求解、行列式理论与线性空间三大核心模块,有效解决概念抽象…

2026/10/15 14:11:12 阅读更多 →
Cursor + Figma MCP 实战教程:一键实现网页设计1:1像素级还原|TaoToken 统一 Key 接入

Cursor + Figma MCP 实战教程:一键实现网页设计1:1像素级还原|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/15 14:11:12 阅读更多 →

最新新闻

C++老鼠走迷宫:数据结构课程设计中的DFS与回溯算法解析

C++老鼠走迷宫:数据结构课程设计中的DFS与回溯算法解析

简介:一份基于C的数据结构课程设计项目,围绕“老鼠走迷宫”经典问题,展示二维数组、图结构、栈与递归的综合运用。项目通过随机迷宫生成与路径搜索,帮助读者理解深度优先搜索(DFS)原理及其在连通性、回溯场…

2026/10/15 14:54:37 阅读更多 →
AI算力报告实战指南:从增速预测到扩容落地的完整方法

AI算力报告实战指南:从增速预测到扩容落地的完整方法

简介:这份《2025年中国人工智能计算力发展评估报告》由浪潮信息与IDC联合推出,共计45页,面向AI基础设施决策者、算力产业研究人员及技术规划人员,可用于把握智能算力市场演进方向、辅助制定采购与建设策略。报告涵盖全球及中国AI发…

2026/10/15 14:54:37 阅读更多 →
AI安全:提示词注入攻击防御与实战方案

AI安全:提示词注入攻击防御与实战方案

1. 提示词注入攻击的本质与危害去年我在参与一个企业级AI系统安全审计时,发现了一个令人后怕的现象:攻击者仅通过精心构造的输入文本,就成功让系统输出了本应严格保密的企业财务数据。这种攻击手法正是典型的提示词注入(Prompt In…

2026/10/15 14:54:37 阅读更多 →
【Web 安全实战】文件上传漏洞花式绕过,90% 渗透新手都踩过这些坑

【Web 安全实战】文件上传漏洞花式绕过,90% 渗透新手都踩过这些坑

【Web 安全实战】文件上传漏洞花式绕过,90% 渗透新手都踩过这些坑 前言 文件上传漏洞是 Web 安全里最经典、出现频率最高的漏洞。很多新手以为上传防护只需要简单判断后缀名,写个黑名单就能搞定。但在真实渗透场景里,各种解析漏洞、后缀变形…

2026/10/15 14:54:37 阅读更多 →
MyBatis Generator配置文件全解析:从入门到排查实战

MyBatis Generator配置文件全解析:从入门到排查实战

搞Java后端的人,只要做过几个正经的数据库项目,基本都躲不开MyBatis Generator这个工具。它能把数据库表直接变成实体类、Mapper接口和XML映射文件,省掉一大坨纯体力劳动的CRUD代码。但很多人对它的印象就是"找个模板改改数据库连接就跑…

2026/10/15 14:54:37 阅读更多 →
C++国际化改造实战:字符串外部化、编码统一与乱码排查

C++国际化改造实战:字符串外部化、编码统一与乱码排查

说实话,第一次在国内项目里正儿八经做 C 代码国际化支持,我一开始是拒绝的。因为很多 C 项目跑起来没毛病,一提到多语言就原地爆炸:界面文案全是硬编码的字符串、日志文件默认按本地编码写入、测试环境的中文一切正常,…

2026/10/15 14:53:36 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 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/15 0:14:36 阅读更多 →
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/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练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/15 9:49:14 阅读更多 →