知识卡片

单点写+读验证:让无锁并发读写在"读多写少"场景下既快又对

普通读书笔记卡

内容

QConf客户端在操作共享内存时是多进程并行读取的典型场景,读操作远多于写操作,为了尽可能提高读效率,团队选择了无锁操作共享内存这条路,但无锁化天然要面对两个正确性风险:多个写入者并发写可能互相破坏数据、读取者可能读到一份还没完全写完的中间态数据。QConf用两个配套措施分别应对这两个风险。第一个措施是单点写:所有需要写共享内存的场景(用户进程通过消息队列请求新Key、ZooKeeper的Watcher通知触发更新、定时重新注册Watcher导致的更新、Agent重启或网络异常后重新拉取数据),全部被收敛到唯一一个线程(Main线程)来执行,其他任何需要触发写操作的地方都只能通过中间数据结构(WaitingWriting队列)和这个唯一写线程通信、排队等待被处理——这样一来,”多个写入者并发写破坏数据”这个风险从设计上被直接消除了,代价是牺牲了一部分写入的并发效率,但因为整体场景本身读多写少,这个代价是可以接受的。第二个措施是读验证:由于写操作本身没有加锁保护读者,无锁的读写方式理论上确实存在读到未完全写入数据的风险,QConf团队的判断是——在绝对读多写少的环境下,这种”读到脏数据”的情况发生概率本身就很低,与其为了消灭这个低概率场景而引入昂贵的锁机制拖慢所有读操作,不如坦然接受它可能发生,转而在读取时做验证:写入时给数据带上一个MD5校验值,读取时用这个预存的MD5值来验证这次读到的数据是否完整正确,一旦验证失败就知道这次读到了不完整的数据,可以重试。这套组合拳给出了一条并发正确性设计的通用思路:面对”高并发读、低频写”的场景,与其无差别地用锁保护所有操作、牺牲读性能去换取绝对的写安全,不如把”写”这一侧的复杂度用单点化收敛掉、把”读”这一侧可能出现的低概率异常用轻量级的事后验证去兜底,这样读操作的性能几乎不受影响,同时整体正确性依然有保障。

参考来源

- 位置:《高可用架构(第1卷)》第5章《运维保障》"5.1 360如何用QConf搞定两万台以上服务器的配置管理"节,"5.1.5 QConf客户端"(源文件:_epub-src/OEBPS/Text/Chapter5_1_6.xhtml) - 结论依据:原文说明"整个QConf客户端在操作共享内存时采用的是无锁的操作,同时为了保证数据的正确性,采取了如下两个措施:单点写……将写操作集中到单一线程……读验证……我们允许其发生,并通过读操作时的验证来发现",直接支撑本卡片结论。 - 原始内容:整个QConf客户端在操作共享内存时采用的是无锁的操作,同时为了保证数据的正确性,采取了如下两个措施……单点写:将写操作集中到单一线程,其他线程通过中间数据结构与之通信,写操作排队……读验证……所以我们允许其发生,并通过读操作时的验证来发现。