ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Cassandra 并发与状态审查清单深度解析:从 TOCTOU 到引用计数的并发缺陷排查实战指南

Cassandra 并发与状态审查清单深度解析:从 TOCTOU 到引用计数的并发缺陷排查实战指南 Cassandra 并发与状态审查清单深度解析从 TOCTOU 到引用计数的并发缺陷排查实战指南【免费下载链接】cassandraOpen source transactional distributed database. Linear scalability and proven fault-tolerance on commodity hardware or cloud infrastructure without compromising performance.项目地址: https://gitcode.com/GitHub_Trending/cassa/cassandra本文基于 Apache Cassandra 仓库中deep-concurrency.md这一份扩展版并发与状态审查清单系统梳理分布式数据库并发代码审查中最高频的缺陷模式TOCTOU、原子性、可见性、生命周期、死锁、线程池饥饿、引用计数等并结合 Cassandra 源码Ref引用计数、SEPExecutor共享执行器池、Stage调度阶段、SSTableReader自引用管理等给出可验证的实现级佐证帮助读者在审查 Cassandra 这类高并发系统代码时快速定位并发隐患并理解其底层运行原理。1. 这份清单解决什么问题deep-concurrency.md是一份面向代码审查deep review场景的结构化缺陷排查清单它不讨论并发是什么而是逐条列出在并发系统中具体会写错什么——从跨线程共享状态的读写到生命周期管理、计数器记账、状态机分支、死锁与线程池饥饿再到特定的可重入与异步边界危害。对 Cassandra 这类每个请求都要经过多线程调度、每个 SSTable 都要被引用计数管理、每个 Stage 都有固定线程约束的系统而言这份清单的价值在于它把并发正确性拆解成可逐一对照检查的条目每条都附带权重high / medium / low让审查者知道哪些模式一旦出现几乎必然是 bug哪些只是需要谨慎评估的风险点。清单的检查对象是目标文件TARGET FILES使用方式也明确审查前必须读取目标文件全文而非只看 diff先完成五类上下文收集再逐节过清单。2. 审查前置Context Gathering上下文收集清单在进入任何具体检查项之前强制要求先回答五个问题线程模型哪些线程会访问这个对象它是否被限定在单一线程内锁清单哪些锁保护这些状态锁的获取顺序是什么生命周期谁创建、启动、停止、销毁这个对象共享状态哪些字段被多个线程读写状态机这个对象可能处于哪些状态哪些状态迁移是合法的这五问对应到 Cassandra 源码中就是许多并发基础设施类的设计前提。以执行器为例src/java/org/apache/cassandra/concurrent/Stage.java中的Stage枚举明确区分了两类线程模型单线程阶段GOSSIP、ANTI_ENTROPY、MIGRATION、MISC、FETCH_METADATA等() - 1线程多线程阶段READ、MUTATION、COUNTER_MUTATION、REQUEST_RESPONSE等线程数由DatabaseDescriptor::getConcurrentReaders等配置决定。这个 executor 是单线程还是多线程决定了它的线程约束契约单线程阶段天然免于同阶段内数据竞争但一旦有人把状态变更任务错误地调度到其它阶段见后文Wrong executor stage就破坏了该契约。类似地Ref引用计数的注释src/java/org/apache/cassandra/utils/concurrent/Ref.java也以生命周期为第一原则被计数对象必须定义一个Tidy清理器且该清理器不得持有被跟踪对象的引用只能持有其资源和清理方式否则会造成清理器与对象互相引用、永远无法回收。3. TOCTOU 与原子性深度权重highTOCTOUTime-of-check to time-of-use是共享可变状态上最常见的并发错误先检查、后使用但检查与使用之间没有统一的原子边界。清单列出了四类典型形态字段在一步中读取、在另一步中无同步地依据该值行动布尔标志检查后使用但没有持锁覆盖两个操作状态机检查与状态迁移分离存在性检查与创建/插入操作分离volatile字段或并发集合被读两次而没有本地捕获导致 check 与 use 之间被并发置空。清单特别点名了几个高危的非原子复合操作ConcurrentHashMap上无外部锁的 get-then-put、check-then-act跨多个并发 map 的无共享锁操作putIfAbsent的返回值被忽略调用方仍使用自己构造的参数对象这个问题在清单末尾的Specific Concurrency Patterns中再次出现权重为 low 但语义相同putIfAbsent不检查返回值后续代码操作的是新创建对象而不是竞态胜出者。Cassandra 源码中对检查与使用必须原子的正面示范是SEPExecutor的许可permit管理。在 src/java/org/apache/cassandra/concurrent/SEPExecutor.java 中SEPExecutor用一个AtomicLong permits打包存储两类许可低 32 位排队任务数task permits范围[0..maxTasksQueued]高 32 位可用工作许可work permits范围[-resizeDelta..maximumPoolSize]。takeTaskPermit采用标准的 CAS 自旋循环读当前值 → 计算新值 →compareAndSet失败则重读重算将检查许可是否可用与扣减许可合并为一个原子操作takeWorkPermit同理同时扣减工作许可与任务许可任一不足即返回false。这正是清单所强调的check 与 act 必须处于同一原子操作的实现模板。清单中还有两类值得注意的次高频模式生产者-消费者别名权重 medium生产者把对象放入队列queue.put(obj)后仍保留引用并继续修改它原子交换后旧实例仍有并发写者权重 lowAtomicReference.getAndSet(newCollector)之后如果在新引用上排空旧收集器而旧引用上的写者尚未停止会造成旧实例上的写入被丢弃或重复锁下快照被下游无同步重读破坏权重 low持锁把共享字段捕获到局部变量后临界区内后续调用却重新读取原始引用写this.field.foo()而非local.foo()或传给辅助函数的局部变量在函数内部又间接解引用宿主对象的字段。4. 集合并发深度4.1 ConcurrentModificationException权重high清单列出三种必然触发ConcurrentModificationException的写法for-each 迭代共享可变集合同时另一线程修改它在 for-each 同一集合的过程中调用collection.remove()循环体内部的方法修改正在被迭代的集合。4.2 共享集合的无同步读权重high共享HashMap/ArrayList在同步块内写、同步块外读.keySet()、.entrySet()、.values()作为**活视图live view**返回而未拷贝在保护集合的锁之外迭代集合。清单进一步把活视图迭代列为独立模式迭代map.entrySet()或list.subList()之前不拷贝getter 直接返回内部 Map/Set/List 字段导致外部在锁外迭代。4.3 共享 ByteBuffer 的 position 副作用权重high这是一个非常 Cassandra 风格的检查项——相对读写relative get/put会推进position而position是共享可变状态共享/池化ByteBuffer未先duplicate()或slice()就直接相对get()反序列化辅助方法返回静态/单例缓冲区的引用而非新拷贝调用方会永久耗尽它比较器或序列化器推进了调用方传入缓冲区的 position 后再 rewind/flip让并发观察者暴露在中间状态堆缓冲区 slice 后索引底层数组时遗漏arrayOffset()。对照清单的告诫正确的做法是需要保留原缓冲区 position 时先duplicate()共享内容但独立 position/limit需要绝对寻址时用slice()并正确计算arrayOffset()。5. 可见性与内存模型深度5.1 缺失 volatile权重medium三类典型场景非 final、非 volatile 的共享字段在一线程写、另一线程读跨线程共享的懒初始化缓存字段没有 volatile可变单例引用没有可见性保证。清单的言下之意是即使没有同时写的数据竞争缺少 volatile或其它 happens-before 边也会导致读线程永远看不到写线程的最新值。5.2 信号先于发布Signal before publish权重medium在等待者将要读取的数据结构完全更新之前就latch.countDown()或完成 future数据必须以释放栅栏volatile 写、synchronized 退出发布。这是先发布信号、后发布数据的时序反转属于典型的初始化/唤醒竞态。5.3 比较器读取活状态权重mediumComparator在排序过程中读取 volatile、共享或持续更新的字段并发写入可能在排序中途改变值违反比较器的传递性transitivity导致排序结果不可预期甚至抛异常。6. 生命周期与初始化深度6.1 构造器发布 this权重medium构造函数启动捕获this的线程构造函数以this注册监听器、管理回调或观察者构造函数启动引用this的执行器。这是溢出构造leakingthis问题对象尚未完全构造完成其它线程就已经能看到并操作它。6.2 双重检查锁定DCL权重medium离开 synchronized 块后重新读取字段空检查和取值是两次独立的字段访问其它线程可能观察到中间状态。经典 DCL 需要volatile字段才能正确发布清单要求审查者确认字段是否在同步块外被重读以及是否可能观察到中间状态。6.3 监听器注册时机权重medium事件监听器在初始状态快照之后才注册监听器在异步操作发起之后才注册存在快照与注册之间的事件丢失窗口。6.4 关闭顺序权重high关闭是否与启动严格镜像逆序标志是否在受保护操作完成之前被设置静态初始化器是否过早触发离线工具是否假设集群在线。Cassandra 的Stage枚举本身就携带了关闭顺序信息Stage的构造参数shutdownBeforeCommitlog标记了该 executor 应在优雅关闭提交日志分配器之前先被关闭因为发出变更mutation任务的 executor 可能无限期阻塞等待新的提交日志段不先排空它们就无法干净地关闭提交日志——这正是关闭顺序与启动顺序镜像在真实系统中的落地。6.5 非阻塞停止信号后接破坏性状态变更权重medium关闭流程给后台任务发停止标志/信号后不 join、不等待、不确认任务已退出就直接 clear / truncate / delete 该任务正在读写的数据或功能禁用路径在周期性任务仍处于迭代中途时反注册/置空协作者。此类发信号即拆房的模式会在后台任务与清理动作之间制造竞态。6.6 引用计数未在并发读取前获取权重high读取者访问共享的引用计数资源前未先 acquire 引用并发的驱逐者/删除者/压缩器可能在空值/存在性检查与读取之间释放资源造成 use-after-free引用计数增量与 CAS 结合时竞态失败方可能让计数膨胀。这一条在 Cassandra 中对应着完整的引用计数体系值得展开RefCounted接口src/java/org/apache/cassandra/utils/concurrent/RefCounted.java定义了两个获取方法tryRef()尝试获取新引用并递增计数已释放时返回 null这是先获取引用再安全读取的入口ref()获取新引用已释放时抛IllegalStateException。Ref实现src/java/org/apache/cassandra/utils/concurrent/Ref.java用PhantomReference 全局状态实现最后一个引用被释放时执行清理Target -- selfRef -- [Ref.State] -- Ref.GlobalState -- Tidy ^ | Ref ---------------------- | Global --------------------每个Ref对应一个State继承PhantomReferenceRef通过AtomicIntegerFieldUpdaterState上的released字段保证每个引用恰好释放一次引用被 GC 回收而从未显式release()时State.release(true)会记录LEAK DETECTED错误日志并可通过OnLeak回调上报对已释放引用再次release()会记录BAD RELEASE并抛IllegalStateException提供enableTestTracing()/disableTestTracing()用于测试中追踪分配/释放线程与栈轨迹。Refs批量管理src/java/org/apache/cassandra/utils/concurrent/Refs.java是一次性持有多个Ref的集合release()释放全部引用并清空内部映射releaseIfHolds(T)则仅在确实持有时释放。SSTableReader是引用计数 并发读取的典型使用者src/java/org/apache/cassandra/io/sstable/format/SSTableReader.java它实现SelfRefCountedSSTableReader持有private final RefSSTableReader selfRef构造时selfRef new Ref(this, tidy)查询路径通过tryRef()先获取引用再读取避免读操作与压缩器/驱逐者删除文件之间的 use-after-free——这正对应清单 6.6 的三条检查项。7. 计数器与记账深度7.1 增量无匹配减量权重medium计数器在可能失败的操作之前递增每条失败分支是否都有回滚路径计数器在关联清理完成之前就递减。7.2 指标放在错误的点权重high指标在被测操作之前更新早退路径会虚增指标在另一个操作之后更新测错了对象指标在控制被测操作的守卫之外指标位于与真实选择循环不一致的估算循环中。7.3 大小/计数累加器缺口模式size 累加器只在部分写路径更新而非全部墓碑tombstones、元数据、索引块是否都计入累加器。Cassandra 的SEPExecutor提供了一个指标与操作对齐的正面例子completedTasks计数只在onCompletion()中incrementAndGet()而onCompletion()是任务真正完成时被调用的钩子保证指标与已完成任务这一语义严格同步。8. 状态机深度8.1 分支中缺失副作用权重high列出状态机处理器的所有分支检查每个分支是否执行了应有的副作用重点检查gossip 同步、指标更新、状态传播、传输停止。8.2 状态清理权重highreset / clear / truncate 是否更新了每一个伴随结构懒初始化if (x null) x init()是否把尚未初始化与已关闭混为一谈成功路径是否缺失错误路径才有的清理持久状态是否在操作开始时而非提交时被写入周期性任务是否在!enabled时短路返回却没有撤回已发布的状态。9. 死锁深度9.1 处理器中的阻塞 get权重lowRPC verb 处理器内future.get()是否饿死处理器线程池被等待 future 的完成是否依赖同一个池——若是则形成自我死锁。9.2 锁顺序具体模式持锁代码段委托给一个会独立再获取同一把锁的公共 APIsynchronized方法调用会重入同一把锁的 Netty/IO 回调static synchronized调用会加载另一个带自身静态初始化的类静态初始化死锁。9.3 线程池饥饿具体模式SynchronousQueueCallerRunsPolicy是否会阻塞提交者Netty 流水线中的有界阻塞队列是否会阻塞排空它所需的 I/O 线程。9.4 共享元数据变更缺少锁权重low元数据变更路径是否跳过了并发后台任务持有的 flush / compaction 锁后台任务是否在元数据对象已失效后仍写文件、索引项或偏移量。10. 作用域与守卫不匹配深度10.1 作用域不匹配权重highteardown 步骤位于其所对应操作的if块之外通知无条件执行而它本应位于条件内部不变式守卫位于可选分支内而非外围作用域校验逻辑只存在于一条路径如本地应用 local-apply另一条路径远程广播 remote-announce没有。10.2 条件递减链模式跨多个 if/else 分支的条件递减链是否数错dirty 标志是在写循环内部设置还是在循环结束后才设置。11. 线程池与调度深度11.1 错误的执行器阶段权重medium消息处理器、任务或状态变更被调度到错误的执行器阶段脱离了其线程约束契约。Cassandra 的Stage枚举正是用来防止这类错误的每个阶段有固定线程模型调度方必须按阶段语义提交任务。11.2 计数器作用域权重medium计数器递增位于条件大括号之外无条件触发本意只统计匹配项实际每个迭代都计数。11.3 在受限/单线程执行器上自我调度权重medium已在单线程执行器或线程约束 actor上运行的代码又向同一执行器提交新工作并阻塞等待结果协调者与副本处理器运行在同一执行器上时为本地副本又调度回同一执行器。11.4 Cassandra 的 SEPExecutor无锁许可调度的实现参照SEPExecutorsrc/java/org/apache/cassandra/concurrent/SEPExecutor.java是 Cassandra 自研的共享执行器池 自旋工作线程调度器其核心设计正是对清单多个条目的正面示范无锁入队与唤醒addTask先把任务加入ConcurrentLinkedQueue再用permits.getAndAdd递增任务许可若此前任务许可为 0则调用pool.maybeStartSpinningWorker()src/java/org/apache/cassandra/concurrent/SharedExecutorPool.java 中通过spinningCount.compareAndSet(0, 1)保证只有一个worker 进入自旋状态——任务先入队再发信号正是先发布数据、后发布信号的正确顺序CAS 保证扣许可必有活干takeTaskPermit的 CAS 循环保证一旦扣减成功随后的tasks.poll()必有任务可取立即执行优化maybeExecuteImmediately在持有工作许可时直接在当前线程执行任务通过ImmediateTaskHolder记录嵌套立即任务否则回退到入队路径并在 finally 中归还工作许可、重新maybeSchedule()维持调度不变量。SharedExecutorPool.spinningCount的 CAS 用法也值得注意它用compareAndSet(0, 1)把是否有 worker 正在自旋等待任务这个布尔状态做成无锁原子更新避免了多线程同时唤醒多个空转 worker 的浪费——对应清单中CAS 选举单一写者的正面形态。12. 可重入与异步危害12.1 可重入权重low持有锁的方法回调了会修改同一共享状态的代码这在取消与驱逐路径中尤其常见。12.2 ThreadLocal 跨异步边界权重low线程本地状态tracing、请求上下文在提交者线程设置、在异步任务中读取提交者可能在任务读取前就清除 ThreadLocal。Cassandra 的ExecutorLocalssrc/java/org/apache/cassandra/concurrent/ExecutorLocals.java与WithResources机制正是为跨线程传递请求上下文而设计任务包装器负责把提交者的局部状态绑定到任务执行线程这正是对ThreadLocal 跨异步边界问题的系统性解决。12.3 CAS 重试安全权重low无界 CAS 重试循环用在竞争不短暂brief的场景布尔 CAS 选举了单一写者而其它参与者继续使用陈旧状态。12.4 中断破坏共享 I/O 通道权重low任务在持有或使用共享网络通道时被中断后续对该共享通道的阻塞调用抛出ClosedByInterruptException把中断传播到被中断任务范围之外应评估通道应每任务专用或中断应在本地吸收。13. 可变性与不可变性违规清单在此部分只列了一个明确模式返回不可变集合但调用方期望可变权重low。方法返回Arrays.asList()、Collections.singletonList()或不可修改列表调用方却对其调用add()/remove()抛出UnsupportedOperationException。这类问题常见于内部 API 演进后调用方契约未同步更新的场景。14. 特定并发模式Specific Concurrency Patterns清单最后专门汇总了一批具体的、可点名的并发反模式按权重排序权重模式要点high版本门控的条件字段只在一个分支读写序列化/反序列化中受消息变体/类型/状态门控的字段只出现在一个分支serializedSize早退绕过共享尾字段协议版本未贯穿到集合元素解码器标志在条件早退前被消费版本门控字段被冗余写入但没有匹配的反序列化逻辑high配置选项被静默丢弃配置旋钮被解析但未传入 buildercipher suites 传 null、流式加密设置被丢、最大帧大小未设置mediumThrowable.getMessage()/getCause()/ 空栈迹未判空无参构造器的getMessage()返回 null无链式原因时getCause()为 nullwritableStackTracefalse时栈迹数组为空medium复制粘贴错误复制的代码块两次引用同一变量/列名/字面量而非各自应有的不同值medium原子计数器与集合失同步存活/完成检查用原子计数器而实际数据在另一集合计数器因 CPU 重排超前推进medium注册与资源就绪之间的竞态资源跟踪集合的注册或可用性标记在资源真正就绪之前/之后设置medium异常处理器捕获类型不匹配绕过清理try 块分配了池化资源catch 只处理一种异常类型其它类型的异常完全逃逸 catch清理被跳过lowgetter 修改状态名为getCompletedTasks()的方法调用incrementAndGet()而非get()lowThreadLocal 用于实例级状态ThreadLocal 缓存的值逻辑上属于某个对象实例多实例共享线程时缓存值互相污染low实例级共享游标/遍历状态类把可变遍历游标作为实例字段并发调用方互相破坏对方状态low清理完成前递减资源准入计数器在关联清理socket 关闭、线程退出完成前递减lowputIfAbsent返回值被忽略后续代码操作新创建对象而非竞态胜出者low重试/重入路径未处理幂等性可重试操作未区分哪里需要幂等与重复意味着 buglow测试中全局/静态标志未在 finally 复位断言前切换的全局标志在 try 体末尾复位断言失败后遗留脏状态low单例响应对象跨并发请求复用携带每请求可变字段的协议消息类被暴露为静态单例low并发目录创建竞态mkdirs()的结果驱动错误路径未考虑并发创建者可能竞争low锁已获取但关键工作在 lock() 与 try 之间变更放在lock.lock()与配对try之间实际在临界区外执行lowfinally 抛异常抑制后续资源清理finally 内可抛异常的调用抑制了原始异常并阻止后续清理low竞态失败方未释放资源对象在 putIfAbsent 前已构造并持有资源失败方未 disposelow独立更新计数器数组的缓存行竞争相邻原子计数器互相伪共享false sharing需检查填充或分段striped设计low发后不理的异步操作无在途去重提交到执行器的异步抓取未被跟踪第二个调用方触发并行操作low广播失效引发惊群重算缓存失效广播导致所有持有者同时竞争重新填充15. 状态清理——并发域模式State Cleanup15.1 耦合数据结构仅部分复位/清理权重highreset、clear、truncate 触碰到多个耦合结构之一时遗漏了其它结构——派生计数器、伴随 map、版本跟踪器或缓存联合cached unions。15.2 合并或库升级破坏接口方法签名权重high合并后一个或多个实现类保留旧签名没有Override时不匹配是静默的方法变成重载而非重写调用点仍走旧签名。15.3 成功路径缺失错误路径才有的清理权重medium清理逻辑存在于错误处理器但正常完成路径缺失。15.4 用陈旧快照做删除/清理权重medium通过快照 diff 计算什么变了的代码在快照被跳过时会漏掉变更。15.5 其余低权重模式基类构造器调用虚方法返回陈旧默认值可覆写方法在基类构造器中调用时子类尚未完成初始化返回默认值段间清除 open 标记/游标迭代器状态机在读取当前 open 状态时将其重置为副作用丢失跨越迭代边界的信息包装类 enable/disable 生命周期未传播到内部委托状态变更只影响包装类自身的标志不影响委托对象滚动升级中不安全地移除守卫权重 medium守卫被移除但滚动升级中的旧节点仍依赖被守卫的行为共享集合的 clear-and-refill 竞态窗口权重 low共享集合先清空再回填clear 与 refill 之间的并发读者看到空状态构造器参数被接受但从未存储或使用权重 low构造函数接受参数但从未赋值给字段。16. 如何把这套清单落到 Cassandra 代码审查中结合以上分析与仓库源码建议按如下顺序使用这份清单先做 Context Gathering对目标类明确线程模型对照Stage的单/多线程约束、锁清单、生命周期对照Stage.shutdownBeforeCommitlog等关闭顺序信息、共享字段、状态机优先检查 high 权重条目TOCTOU 与原子性、共享集合无同步读、ByteBuffer position 副作用、关闭顺序、引用计数先获取再读取对照Ref.tryRef()/RefCounted/SSTableReader.selfRef、状态机分支缺失副作用、耦合结构部分清理对照仓库基础设施验证写法凡是涉及线程池调度的对照SEPExecutor的 permit CAS 与SharedExecutorPool.spinningCount的原子唤醒凡是涉及引用释放的对照Ref的双重释放检测BAD RELEASE与泄漏检测LEAK DETECTED为测试代码单独过一遍清单Ref提供enableTestTracing()/disableTestTracing()供测试追踪分配/释放轨迹清单中全局/静态标志未在 finally 复位计数器与集合失同步等条目在测试代码中同样高频。17. 总结deep-concurrency.md提供了一份按权重排序、可直接对照执行的并发缺陷排查清单从上下文收集的五个前置问题到 TOCTOU/原子性、集合并发、可见性、生命周期、计数器、状态机、死锁、作用域、线程池、可重入、可变性再到二十余条具名并发反模式与状态清理模式。在 Cassandra 仓库中这份清单的每一条几乎都能找到对应的真实实现或真实风险Ref/RefCounted/Refs用 PhantomReference 原子字段更新器实现了恰好释放一次、泄漏可检测、双重释放即报错的引用计数SEPExecutor与SharedExecutorPool用打包 AtomicLong 许可与 CAS 自旋实现了检查与扣减原子、信号晚于数据发布的无锁调度Stage枚举把线程模型与关闭顺序固化成类型系统的一部分。理解这些实现再回头逐条核对清单就能把看起来正确的并发代码升级为可论证正确的并发代码。【免费下载链接】cassandraOpen source transactional distributed database. Linear scalability and proven fault-tolerance on commodity hardware or cloud infrastructure without compromising performance.项目地址: https://gitcode.com/GitHub_Trending/cassa/cassandra创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进