S43

NoSQL 与缓存

点击正文段落选定收藏或划线位置;未选定时使用屏幕内第一段。

课内目录

1 NoSQL 与缓存要回答什么

运维控制台要显示主机的采集配置:采样间隔、日志目录、允许上报的指标。采集进程启动时需要读取配置,运行中也要定期检查更新。配置可以保存在数据库里,但如果每个进程每隔 5 秒都发起查询,主机数量增加后,数据库连接数、网络往返和查询耗时会同时上升。把最近读过的配置保存在靠近进程的位置,能够减少重复读取;问题随之变成:配置更新后,旧值还能使用多久,缓存失效时谁承受请求,读失败时进程应该继续运行还是停止。

NoSQL 与缓存回答的不是同一个问题。NoSQL 是对一类数据存储的概括,常见接口包括按键取值、按文档字段查询、按行键与列族访问。缓存是一种数据职责:保存可以重新获取或重新计算的副本,用空间换取更少的访问成本。某个键值存储可以承担缓存,也可以承担持久数据存储;应用进程中的字典也可以成为缓存。判断方案时,应先问谁保存权威数据,再问产品提供什么接口,不能只看它是否把数据放在内存里。

本课使用配置读取作为主线。权威配置记为版本 42,数据库是能够确认该版本已提交的位置。缓存保存配置副本,采集进程保存最近成功读取的本地快照。正常情况下,读路径从近到远逐层寻找;发生故障时,系统可能继续使用旧快照,也可能拒绝执行危险操作。例如显示采样频率可以容忍短暂陈旧,撤销文件读取权限则不能仅靠等待缓存过期来生效。可接受的行为来自业务约束,速度指标无法替代它。

这里还要区分应用缓存与文件系统页缓存。应用缓存知道“主机配置”与“版本 42”,可以按业务键失效;页缓存服务于文件内容访问,不理解配置是否仍然有效。即使数据库文件已经在操作系统内存中,应用查询仍可能需要进程调度、网络传输、解析与权限检查。反过来,增加应用缓存也不会消除磁盘满、文件权限错误或网络连接失败。排查时应写清是哪层命中,避免把所有内存相关现象统称为缓存。

配置的权威来源与缓存职责
配置的权威来源与缓存职责

做方案前可以先写出 3 个问题:请求按什么键读取,返回值允许陈旧多久,缓存完全不可用时系统怎样工作。键决定能否直接定位数据,陈旧时间决定更新策略,失效行为决定是否需要本地快照、限流或拒绝服务。后面的存储选择、容量估算和故障实验,都围绕这些问题展开。

2 键值、文档与宽表的能力边界

键值模型把访问入口压缩成明确的键,例如 host-config:node-17。读取接口可以表达为 get(key),返回配置内容以及应用保存的版本。它适合已知主机编号后直接读取配置、保存短期计算结果等场景。边界是:如果临时要求找出“日志目录包含 /var/log 且采样间隔大于 10 秒”的全部主机,单纯的按键接口不能自动完成高效筛选。扫描全部键可能占用服务资源,给键名加前缀也不会自然产生任意字段查询能力。

文档模型把相关字段放进同一个结构,例如包含 host_id、revision、interval_seconds 和 log_paths 的对象。应用可以整体读取,也可能按字段查询或更新。能否高效查询取决于具体实现及索引配置,不能因为内容看起来像 JSON,就认为每个字段都适合搜索。新增字段也不等于取消数据约束:采集进程仍然需要检查间隔范围、路径类型和版本格式,否则错误配置会沿着缓存快速传播。

宽表模型适合围绕预先设计的行键组织大量记录。例如按主机及时间范围读取状态摘要,可以设计相应的行键与列组织方式。但“宽”不意味着任意统计都便宜。访问是否集中到少数键、是否需要读取很大的时间区间、是否能够按已知入口定位,都会影响实际成本。本课只需要掌握按访问路径设计数据的直觉,暂不展开内部存储布局和分布式分区实现。

模型本课中的自然入口设计时必须追问
键值按主机编号取配置是否需要按其他字段查找
文档读取或筛选主机配置对象哪些字段有索引,怎样校验结构
宽表按主机与时间范围取状态记录行键能否支持主要读取范围

这些模型与持久性、事务、复制并不是可以互相替代的标签。某种存储是否在返回成功前落盘,是否支持指定范围的原子更新,故障后是否可能丢失已确认的数据,都要查具体接口和配置。把缓存写入成功当作配置发布成功,就是混淆了数据职责与接口能力。发布接口应给出权威提交结果,缓存更新只是随后改善读取的动作。

选择时还要考虑日常操作能力。维护者需要知道如何备份与恢复、如何查看连接与延迟、如何限制大对象、如何处理磁盘容量。对于数量较少、更新不频繁的配置,现有关系数据库配合简单缓存可能已经足够。采用 NoSQL 应当对应明确的访问需求,而不是为了给架构图增加组件。模型越贴近查询入口,应用越容易解释每次读取到底付出了什么成本。

3 缓存穿透、击穿与一致性窗口

先看常见的旁路读取:应用查缓存;命中则返回;未命中则查询权威库;查询成功后把副本写入缓存,再返回结果。这里的“未命中”是正常状态,而超时、连接被拒绝、数据无法解析是故障。接口应将这些情况区分开,否则维护者会把网络故障当作正常缺失,不断增加回源流量。缓存本身增加了网络调用,命中率低且超时较长时,读路径甚至可能比直接查库更慢。

穿透指大量请求查询不存在的数据,缓存无法替权威库挡住这些读取。例如错误脚本不断请求不存在的主机编号,每次都得到未命中。可以先校验编号格式,对明确不存在的结果缓存短期否定记录,并限制异常来源的请求速率。但否定记录也会过时:刚创建的主机可能暂时仍被判断为不存在。必须为它设置较短有效期,或在创建流程中清理该记录;查询超时不能冒充“主机不存在”。

击穿强调某个热门键失效后,大量并发读取同时回源。假设许多采集进程共用站点配置,缓存条目到期时,原本分散的请求可能集中访问数据库。进程内可以把相同键的并发加载合并,让等待者共享加载结果;多个应用实例之间还需要考虑总回源量。加载失败必须唤醒等待者并释放状态,不能让某个键永久停在“正在加载”。本课关注减少重复回源的目的,不展开跨节点锁的实现。

缓存雪崩通常描述很多条目集中失效或缓存服务不可用引起的整体回源高峰。为有效期加入适度随机偏移,可以分散批量过期;为回源设置并发上限、超时与退避,可以限制故障扩散。随机偏移只能改变过期时间分布,不能修复缓存宕机。若数据库只能承受每秒 300 次额外读取,缓存故障时放行每秒 3000 次请求,最终可能连权威库也无法服务。

缓存命中、缺失与故障的读取分支
缓存命中、缺失与故障的读取分支

一致性窗口指权威数据更新后,读者仍可能看到旧副本的时间范围。TTL 为 60 秒,只说明条目在约定条件下到期,并不证明全体读者会在 60 秒内得到新值。进程本地缓存可能另有有效期,网络故障可能触发继续使用旧值,迟到的加载也可能重新填入旧内容。要声明窗口,就必须同时说明计时起点、各层失效行为、请求耗时上限和故障时的例外规则。

观察动画时,重点跟踪版本而不是只看命中颜色。同样是缓存命中,返回版本 41 与返回版本 42 的意义不同。命中率描述减少了多少远端读取,一致性描述返回内容是否符合承诺。它们需要分别记录,不能用较高命中率证明配置发布正确。

4 动手:画一条读路径上的缓存层次

先画出实际调用路径:采集进程的本地快照、共享缓存、配置数据库,以及连接这些组件的网络。每个箭头注明调用接口、超时和失败结果。例如本地读取没有远程往返,共享缓存调用有 30 毫秒超时预算,数据库读取有 200 毫秒预算。这些数值只是练习起点,正式环境应由测量确定。顺序调用的耗时会叠加,不能让每层都独占整个请求的时间预算。

在练习环境准备 1 个配置读取服务,约定 GET /config/node-17 返回配置与 revision,GET /metrics 返回命中、未命中、缓存错误和回源计数。以下 URL 是练习接口示例,需要用实际服务地址替换。先连续读取同一个键,记录首个请求与后续请求的版本、耗时和回源次数;再读取不存在的键,确认“未找到”与“后端不可用”有不同状态及日志。

curl --max-time 2 -sS -w '\nhttp=%{http_code} total=%{time_total}\n' \
  http://127.0.0.1:8080/config/node-17
curl --max-time 2 -sS http://127.0.0.1:8080/metrics
ss -tnp
ps -eo pid,comm,rss

curl 的总耗时包括客户端看到的完整请求过程,不能直接当作数据库查询耗时;应用日志需要分别记录缓存与回源耗时。ss -tnp 用来观察 TCP 连接与相关进程,显示进程信息可能需要相应权限。ps 中的 RSS 可以提供进程常驻内存线索,但不能精确等同于缓存对象占用,内存分配器与进程其他数据也会影响它。命令提供证据,解释仍要对应具体层次。

接着在隔离的练习环境中,通过服务提供的测试开关模拟共享缓存不可用,避免修改生产网络规则。再次读取已有配置,观察是否触发回源、是否使用旧快照、接口是否在预算内结束。随后模拟权威库不可用,确认服务按约定返回旧配置或明确错误。若使用旧值,响应或日志必须标记其版本与年龄,不能把降级返回包装成正常刷新成功。

从请求结果定位到进程与网络证据
从请求结果定位到进程与网络证据

把结果整理成路径记录:请求编号、主机键、命中层、配置版本、各阶段耗时、回源结果、是否降级。不要把完整配置或凭据写入日志,也不要把每个主机编号都放进监控指标标签造成过多时间序列。聚合指标用于发现异常,有限采样的日志用于解释单次请求,两者通过请求编号与时间关联。

最后检查磁盘上的本地快照。读取失败可能来自文件不存在、权限不足、内容损坏或磁盘写满后的不完整更新。程序应验证快照结构与版本,保存时考虑先写临时文件再替换,并正确处理失败结果;不能假定文件存在就能使用。具体持久化保证还取决于文件系统与写入流程,本课只要求把本地文件也当作可能失败的数据层。

5 淘汰策略与容量估算取舍

TTL 控制条目在时间维度上的有效性,淘汰控制空间不足时保留哪些条目。条目尚未到期,也可能因为容量限制被淘汰;条目已经到期,也不代表它占用的空间在那个瞬间全部释放。因此,“设置 10 分钟 TTL”不能回答需要多少内存,更不能保证某个键在 10 分钟内始终命中。容量设计需要同时估算活跃键、对象大小、元数据和额外运行空间。

假设有 100000 个活跃配置键,每份编码后的内容平均 2 KiB,仅内容约占 195 MiB。实际占用还包括键名、版本、过期信息、对象管理及分配开销。若练习中暂按内容的 1.5 倍估算,再预留约 30% 空间,预算约为 381 MiB。这个系数不是通用常数,应通过代表性数据和目标实现测量修正。连接缓冲、日志缓冲以及进程本身的内存也不能挤进同一份内容预算里。

LRU 倾向淘汰较久未访问的数据,适合近期访问具有重复性的负载;LFU 倾向保留访问频繁的数据,但需要考虑历史热度如何变化。不同实现可能采用近似策略,不能把名称当成严格行为保证。周期性全量巡检可能短时间触碰大量低频键,挤走平时热门的配置;刚启动的新站点则可能没有足够访问历史。选择策略要看工作负载,而不是只背诵定义。

还有一种容易遗漏的成本是失效后的重建。缓存 1 份需要复杂计算的汇总结果,和缓存 1 份按主键直接读取的配置,虽然字节数相同,回源代价却可能不同。容量紧张时只按对象大小淘汰,可能使最昂贵的读取反复重建。入门方案可以先限制单个对象大小、隔离不同用途的预算,并观察回源耗时分布,不急于设计复杂的自定义评分规则。

滑块展示的是给定假设下的估算。增加容量可能降低重复回源,但无法保证命中率按比例增加;延长 TTL 可能减少刷新次数,同时扩大正常情况下看到旧值的机会。若访问均匀且几乎不重复,再大的缓存也可能收益有限。应先用实际键访问分布验证假设,再讨论增加内存。交互中的数值用于比较取舍,不是可直接套用的容量承诺。

上线后至少观察内存占用、淘汰次数、命中率、回源速率与请求延迟。命中率应明确分母,例如命中次数除以命中与正常未命中次数之和,并另外报告缓存错误。缓存错误增多时,只看这个比率可能误以为系统仍然健康。分位延迟与峰值回源量也比单纯的平均值更能暴露集中失效问题。

6 常见坑:把 TTL 当事务提交

配置发布者将采样间隔从 30 秒改为 10 秒,数据库写入成功后返回版本 42。共享缓存里仍有版本 41,TTL 还剩 40 秒。此时发布已经在权威库提交,但读取端还可能看到旧值。若界面写着“全体主机立即生效”,系统行为就违背了承诺。TTL 是副本的时间规则,既不参与数据库提交,也不负责确认所有进程已经加载新版本。

反过来,先更新缓存再写数据库也不能让发布变得可靠。数据库写入失败时,部分读者可能已经得到并不存在于权威库中的配置。随后缓存过期,又会回到旧值,看起来像配置自动回滚。发布接口应区分权威提交成功、缓存刷新结果与客户端加载状态。需要确认某台采集进程已应用新配置时,应查看该进程报告的已加载版本,不能只查看缓存里的版本。

另一个陷阱是把过期时间当作准点执行器。设置某键 60 秒后过期,并不意味着第 60 秒会准时运行清理脚本、撤销权限或关闭网络连接。缓存过期通常影响后续读取行为;已经复制到进程内存的数据、已经打开的文件及已经建立的连接都有自己的生命周期。权限撤销应依赖明确的检查与执行机制,缓存只能在规则允许的范围内帮助减少读取。

时间定义也需要写清。固定 TTL 从写入缓存开始计时,滑动有效期可能在每次访问时延长。如果热点配置一直被访问,滑动规则可能让旧副本长期存在。用于控制陈旧程度时,应记录数据版本与生成时间,并明确最大可接受年龄,而不是仅记录“最后访问时间”。持续使用旧快照的降级策略还需要单独上限,避免正常 TTL 在故障时失去约束却没有人知道。

这些问题可以通过发布与读取日志关联发现:权威提交时间、提交版本、失效结果、读取版本以及进程确认时间。普通主机时钟可能存在偏差,跨进程日志不能只靠毫秒时间戳推断严格先后;版本号和请求关联信息更可靠。版本也必须由明确规则生成,不能随手用本机时间充当全局顺序。更复杂的跨节点顺序问题留到后续课程。

7 案例:缓存与库双写导致脏数据

某配置服务采用“先更新数据库,再删除缓存”。维护者认为删除成功后,下次读取自然会获得新值。实际出现了这样的并发:读请求 A 发现缓存未命中,开始读取版本 41;发布请求 B 随后把数据库更新为版本 42,并删除缓存;A 的旧查询结果稍后返回,又把版本 41 写入缓存。B 的数据库更新与缓存删除都成功了,用户仍然读到了旧配置。

这里的“脏数据”指缓存副本与已提交的权威版本不符,并不是在声称数据库发生了某种事务隔离级别的脏读。问题来自跨组件操作的交错:读取、提交、删除与回填不在同一个原子动作中。故障也不一定需要宕机才能触发,普通的网络延迟、进程暂停或慢查询就可能改变操作先后。仅用单线程顺序测试,很容易遗漏这类现象。

旧查询回填后的排查与修复分支
旧查询回填后的排查与修复分支

排查时先确定权威版本确实是 42,再查看返回旧值的来源。若共享缓存已是 42,而采集进程仍报告 41,应该继续检查本地快照与刷新错误;若共享缓存就是 41,则查找其写入者、写入时间和来源版本。只反复删除键可能暂时恢复,却会丢掉定位线索。应先保留必要的版本与请求日志,避免记录配置中的敏感内容。

修复有不同强度。若普通采样配置允许短暂陈旧,可以采用数据库提交后失效、较短 TTL、失效失败重试和旧版本年龄监控,并明确这只是有界陈旧的目标。其边界还依赖加载耗时、本地缓存及故障降级规则,不能只报出 TTL。延迟再删除一次可能降低某些竞争发生的机会,但延迟无法覆盖所有慢请求,因此不能把它描述成严格一致性证明。

若需要防止旧结果覆盖新结果,可以把版本带入缓存记录,并使用存储支持的原子条件更新。但仅仅“写入时比较当前缓存版本”还不够:键被删除后没有版本可比较,旧请求仍可能写入。方案必须另外保留可比较的版本状态,或采用经过分析的版本化访问协议,处理状态保留、过期及并发更新。入门阶段应先说明这一前提,不能用一行伪代码宣称竞争已经解决。

对不能容忍旧值的操作,可以在执行前向权威服务确认当前版本或权限,并接受其延迟和可用性成本。恢复缓存服务时也要控制预热与回源速率,避免所有进程立即刷新。修复验收应重放上述交错:暂停 A 的回填,让 B 完成提交与失效,再放行 A,观察最终读取是否满足所选承诺。验收的对象是可解释的行为,而不是某次测试恰好返回了新值。

8 把缓存职责写进数据访问方案

数据访问方案首先列出数据分类与权威来源。主机采样配置由配置数据库确认提交,缓存保存副本,本地快照用于有限时间的故障降级。权限撤销、配置展示和历史状态查询可以采用不同规则,不能因为它们都按主机编号读取就共用同一个陈旧上限。每类数据还应写明缺失、故障和无权限的返回语义,避免调用方把所有错误都转换成默认配置。

然后描述键与值的契约。键包含必要的命名空间及主机标识,值包含结构版本、配置版本和生成时间;租户或环境之间应有明确隔离。读取配置前仍需完成访问控制,不能因为缓存命中就绕过权限检查。对包含文件路径的数据,采集进程还应按自身权限与允许目录验证,防止错误配置诱导它读取不应访问的文件。缓存的快速传播不会降低输入校验的必要性。

更新协议应写到可以实现与复核的程度:什么结果算提交成功,提交后如何失效,失效失败由谁重试,重试期间读者可能看到什么,以及如何确认进程已加载目标版本。读取协议则注明每层超时、并发加载限制、最大对象大小和降级年龄上限。它们共同决定服务在网络中断、进程重启、缓存冷启动时的表现,不能仅画几个带箭头的方框。

运行部分应给出容量假设与证据入口。记录活跃键估算、大小分布、预留空间以及扩容触发条件;列出缓存错误、淘汰、回源限流和配置版本落后的监控。告警需要对应可执行动作,例如缓存连接失败时检查服务进程和 TCP 连接,版本落后时检查发布记录与加载日志。只有“命中率下降请关注”的告警,很难帮助值班人员判断是否正在影响配置正确性。

最后安排故障演练与回退。至少覆盖缓存不可用、权威库不可用、失效失败、旧查询迟到回填和本地快照损坏。回退到直接查库前,应确认权威库能承受预期流量,并配好限流;禁用缓存不是无成本开关。对于允许旧值的配置,回退也可能是继续使用经过验证的快照并明确标记降级。把条件写清,才能在故障时按证据行动。

9 从NoSQL 与缓存走向分布式系统基础(S44)

即使这里只讨论配置读取,也已经出现了多个独立进程、网络消息与数据副本。数据库提交成功,缓存失效可能失败;失效已经完成,旧查询还可能迟到;请求超时,服务端可能实际上已经执行成功。这些现象说明,跨进程协作不能依靠“代码按顺序写了几行”来推断全局结果。每个参与者只能直接确认自己观察到的状态。

下一课讨论分布式系统基础时,会继续处理消息延迟、部分失败、重复请求与副本之间的关系。本课先保留具体问题:超时后能否重试,重复操作会不会造成额外影响,读者要看到哪个版本,以及故障时优先保留哪些能力。不要急着用某个术语替代分析,应先把请求与状态变化画出来,再判断需要何种保证。

回顾当前读路径,可以把每次返回解释为“从哪层取得了哪个版本,在什么条件下允许返回”。这句话同时约束性能与正确性。只说缓存很快,无法说明它是否返回了可接受的数据;只说数据最终会更新,也无法说明采集进程在故障期间该怎样运行。能够将来源、版本、时间和失败结果连起来,才算为后续分布式问题准备好了观察方法。

放进方案里

为主机配置读取服务增加缓存时,先把数据库指定为权威来源,发布成功以数据库确认提交为准。共享缓存与进程快照保存副本,值中携带配置版本、结构版本与生成时间。普通采样参数允许短暂陈旧,权限撤销另设执行前确认路径;任何缓存命中都不能跳过调用者身份与访问权限检查。

读取顺序为本地快照、共享缓存、权威库,各层使用独立且可累计到请求总预算的超时。明确区分未找到、缓存连接失败和权威库失败。相同键的并发加载应合并,回源设置并发上限;允许使用旧快照时记录降级原因与数据年龄,超过约定上限便返回明确错误,禁止无限续用旧值。

更新采用权威提交后失效,并记录提交版本与失效结果。失效失败进入可观察的重试流程,不把 TTL 解释成提交确认。测试应覆盖旧查询在失效之后回填的交错;若承诺需要阻止旧值覆盖,必须说明版本比较状态如何保留,以及原子条件更新的实际接口。无法提供这种保证时,应如实声明陈旧窗口及故障例外。

容量按活跃键数量、对象大小分布、管理开销与运行余量估算,再用代表性数据校准。监控同时展示命中、正常缺失、缓存错误、淘汰、回源速率及加载版本落后情况。命中率只用于分析访问成本,版本证据用于判断更新是否生效,2 类证据共同支持值班判断。

上线前在隔离环境演练缓存断连、权威库中断、快照损坏与集中失效,确认接口在预算内结束。回退方案注明直接查库的限流条件、旧快照的最大使用年龄和恢复后的刷新速率。验收材料保留命令输出摘要、请求关联日志与版本变化,保证下位维护者能够据此复现和判断。

10 小结:下一步分布式系统基础(S44)

NoSQL 的模型选择围绕访问入口,缓存的职责围绕副本、成本与陈旧程度。TTL、淘汰和提交分别解决时间、空间与权威状态问题。面对旧值,先查来源和版本,再还原读取、更新、失效与回填的顺序。

继续学习 S44 时,带着这条配置读路径思考:请求超时后哪些结果仍未知,重复请求怎样处理,多个副本怎样表达各自知道的状态。这些问题会把单次缓存故障连接到分布式系统的基本约束。

同阶段相关课

AI 答疑

默认关闭。启用后,只发送本课标题、最近阅读的屏幕内正文摘录(最多 6000 字)与本次问题。

配置自己的服务