一看到这个现象,先别急着怀疑面板坏了。KV用量才26%,磁盘空间明明充足,调度器却一条请求都放不进去,这种“容量充足、服务却拒绝”的故障形态,在分布式存储和负载调度链路上出现得太多了。我这些年排查过几次类似的线上问题,结论往往不在一处——有时候是调度器把节点摘了,有时候是KV自己悄悄进入了只读状态,有时候则是全局面板和单分片水位差了十万八千里。把这条链路彻底捋清楚,你才能在下一次故障时不在错误的方向上浪费时间。
这个场景适合所有维护自研KV、使用负载调度器做读写分发,以及正在做容量压测和链路治理的开发者。下面我把整个排查思路、统计口径、根因案例和预防手段完整展开。
1. 现场现象:容量充足却拒绝服务
先还原一下当时的故障画面。早上压测刚开始,业务方反馈新请求大量超时,调度器那边的日志里刷出一片“no available backend”或者“all upstream refused connection”之类的错误。打开KV监控面板,全局存储用量只有26%,集群节点全部显示在线,进程也活着,看起来一切正常。但你手动连上去执行一条简单的写操作,可能直接就返回失败或者卡到超时。
这时候最容易犯的错就是“围着26%这个数字找问题”。磁盘都没满,队列也没有堆积,面板上什么异常都看不到,于是怀疑监控数据不对、网络抖动、甚至觉得是调度器自身的bug。但实际排查下来,多数情况是:调度器在它自己的维度上已经判定“后端不可用”,而KV面板还没有在容量维度上出现异常。换句话说,面板展示的是存储占用率,调度器判断的是“能不能正常完成一次请求”,这两者本来就不是一回事。
理解这个区别很重要。调度器作为流量入口,它根本不关心你对端用了多少磁盘,它只关心你还能不能干活。只要后端的健康检查失败,或者最近一秒内的错误率超阈值,它就会主动把后端摘掉、把流量挡在外面。这时候你去看存储面板,当然看不出问题,因为容量高不高,压根不在调度器的判定逻辑里。
2. 面板上的26%,到底统计的是哪个“KV用量”
2.1 物理磁盘占用不等于可写容量
大多数KV面板上展示的“用量”,默认是后端存储文件的物理磁盘占用,比如数据文件、日志文件占了多少字节,和磁盘总容量做比值。这个数字看着低,不代表存储引擎真的还能写。存储引擎有自己的一套高水位保护机制,典型的就是磁盘水位配置。
我之前遇到过一个真实案例:RocksDB实例配置了keep_log_file_num、max_total_wal_size,数据量确实不大,但WAL文件预分配策略导致磁盘实际可用空间比预期少很多。更常见的是,文件系统层面就有预留,比如ext4默认会保留5%的块给root用户,普通应用进程写到95%就报“磁盘满”,但这个比例在面板上因为数值口径不同完全看不出来。
还有一类坑是compaction的临时空间需求。很多KV引擎在合并数据时需要额外一份临时空间,比如RocksDB在compaction过程中写临时sst文件。如果磁盘可用空间不足以完成一次大范围合并,引擎会认为“无法维持正常写入”,从而触发写入保护。这个状态下,容量面板可能依然停在26%、30%这种低水位,但你发写请求就是被拒。
2.2 全局聚合值掩盖了单分片的水位失衡
这才是这个故障里最容易让人误判的地方。KV集群通常会用一致性哈希、范围分区或者固定分片把数据打散到多个节点上。监控面板上的“全局用量26%”是把所有分片的存储使用量加总后算的。但数据的分布可能极不均匀,尤其是线上出现热点key的时候。
举个实际例子:某个商品详情页的KV使用场景里,一个爆款SKU的缓存键被高频访问和更新,所有写入都收敛到同一个分片或者同一个节点。这个分片的本地存储用量可能已经冲到90%甚至触发了磁盘保护阈值,而其他分片只有10%左右,加总之后全局依然显示26%。调度器看到的是“整体看起来健康”,但实际路由到热分片的请求已经在持续失败。
这种场景最迷惑的一点在于:KV节点进程本身是存活的,健康检查也都能通过,但具体分片已经进入了只读或拒绝写入状态。如果你只盯着全局面板排查,永远找不到入口。
2.3 配额使用率与真实容量的错位
还有一个维度是“配额”。多租户系统里,面板上的“KV用量”可能是某个namespace的配额使用率,这个数值和底层物理磁盘容量没有直接对应关系。假设租户配额是1TB,现在用了26%,面板显示26%。但底层数据节点的物理存储已经满了,或者节点上某个目录的inode用尽,KV引擎在写入时会因为“无法分配新文件”而拒绝请求。
另外,KV系统经常有多个索引结构同时占用空间,比如主索引、二级索引、倒排索引、内存缓存等。有些面板展示的是主数据存储用量,而没有算上索引构建的临时空间、内存中的memtable、block cache等资源。如果某类索引内存或者memtable数量打满,也会出现写入被限流、bulkload被拒绝。这些都属于面板指标覆盖不到的死角。
所以在看面板时,首先要确认你看到的“用量”是哪个采样对象:是全集群物理磁盘?是某个分片?是某个租户的配额?还是某个存储引擎目录的占用?不同口径下,同样的26%代表的意义完全相反。
3. 调度器“放不进去”的真实链路和卡点
3.1 调度器的准入控制到底在看什么
负载调度器(不管自研还是开源组件)在决定一条请求能不能往后端转发时,通常要过好几道关卡:后端健康状态、连接池可用性、当前并发数、排队超时、错误熔断、主动降级开关等等。
按经验,我把调度器的“放不进去”分成三类原因:
- 明确拒绝:调度器检查到后端池里所有节点的健康检查都失败了,或者有主动摘除标记,直接返回“无可用节点”。这种情况日志通常很明确。
- 隐性超时:后端其实是活的,但接受请求后迟迟不响应,调度器等不到完成信号,客户端侧表现为“请求放不进去”。这种情况下,调度器日志里不一定有明显错误。
- 熔断拦截:调度器根据滑动窗口统计错误率/延迟,一旦后端指标超过阈值,就快速失败,不再把请求打进后端。
其中第二种和第三种是最容易和“KV容量面板”的认知产生冲突的。你想啊,KV服务进程还活着,磁盘也没满,但它的内部可能已经在做长时间的compaction,或者某个请求拿到了全局锁,导致所有后续请求排队。调度器的健康检查探针如果只是探测端口或者进程存活,根本发现不了这种状态;但一旦真实业务请求进去后超时,调度器就开始熔断了。
3.2 一次请求从进门到落盘会经过哪些“关卡”
我画过一条链路,有助于大家理解调度器为什么说“放不进去”。请求先到调度器,调度器做路由,挑一个后端节点,建立连接或复用连接池,然后把请求发给KV节点。KV节点内部要过网络接收、反序列化、鉴权、线程池调度、定位分片、构建写入事务、写WAL、更新memtable、返回结果。
这里面任何一把锁卡住了,请求就可能被调度器判定为“失败”或“超时”。最典型的就是KV节点的线程池被打满。很多KV单实例能扛高并发,靠的是多线程模型,但当出现大量慢查询、一次大范围scan或者超大value的读写时,有限的线程会被长时间占用,新请求进入队列后排队超时。调度器这边的健康检查探针也排在线程池里,同样超时,于是调度器判定后端不健康,把所有流量都挡在门外。这个过程可能只需要几十秒,而存储面板的数字根本来不及变化。
3.3 存储引擎的“写保护”机制,面板往往看不见
存储引擎为了自保护,会设置很多“安全阀”。我梳理一个常见的触发条件对照表,方便排查时快速判断:
| 引擎状态 | 可能原因 | 面板表现 |
|---|---|---|
| 写入被拒,报磁盘空间不足 | 实际分区可用空间低于水位,或inode耗尽 | 磁盘占用率不高,但xx分区不可写 |
| 写入阻塞在memtable flush | memtable数量打满,刷新不及时 | 面板正常,后台线程池高延迟 |
| compaction积压过大 | 写入速度远大于合并速度 | 全局容量低,但待合并文件数量巨大 |
| 单分片写保护 | 热分片触发高水位停机 | 全局用量低,但单分片容量已高 |
| 请求大面积超时 | 线程池排队、锁竞争、慢查询拖累 | 一切容量指标正常,但延迟P99飙升 |
这些状态有不少是“容量面板”不提供的指标,你必须去KV引擎的metrics接口里专门看。比如RocksDB有rocksdb_memtable_flush_pending、rocksdb_compaction_pending、rocksdb_current_size_all_mem_tables这些指标,和磁盘占用率完全是两码事。也可以在队列积压的早期就发现异常。
4. 实操排查:从调度器反向追踪到KV根因
4.1 第一步:先把调度器拒绝请求的原因钉死
接到问题时,我的习惯是不要急着去查KV存储,而是先在调度器上找到“这一条请求到底是被什么逻辑拦住”的证据。不同的调度器暴露的观测方式不同,但通常有两个入口:一是调度器的访问日志,二是调度器的状态接口。
比如某个自研调度器会返回业务侧的拒绝码,你可以反问自己几个问题:日志里是因为“backend inactive”拒绝,还是因为“circuit open”拒绝?是连接超时还是读超时?是健康检查失败导致节点被摘除,还是熔断器统计的错误率达到阈值主动拦截?这里的区别决定了往下排查的方向。
如果日志显示“backend inactive”,下一步就是查这个后端节点什么时候被标记为inactive。健康检查连续失败几次就会被摘除?检查的探针是TCP connect、HTTP GET还是写一条探活key?不同探针能看到的问题层次完全不一样。
如果日志显示“circuit open”,那重点方向变成:熔断器统计的错误窗口是多长,错误率阈值是多少,触发熔断的请求具体返回了什么错误。把这些基础证据先收集好,再往下走。
4.2 第二步:确定KV面板和真实服务状态的偏差
拿到调度器侧的证据后,立刻去KV集群做一次“真实读写探测”。不要只看面板,直接手动执行一次写入,观察返回时间、返回码。同时看KV的管理接口或监控指标,确认几个关键项:
# 以某KV集群的管理端口为例,拉取核心指标 curl -s http://kv-node-01:9100/metrics | grep -E \ "rocksdb_(memtable_flush_pending|compaction_pending|current_size_all_mem_tables)" # 查看分片维度的容量分布 curl -s http://kv-node-01:9100/shard_status | jq '.shards[] | {id, used_bytes, is_writable}'这一步的核心在于:区分“整个集群写不了”还是“部分分片写不了”。如果是部分分片写不了,那调度器为什么把所有请求都拒了?因为调度器做负载均衡时可能按节点维度做熔断,或者热点分片恰好覆盖了所有写路径,失败率快速拉满,从而触发了全局熔断。这是最典型的“存储局部故障放大到全局不可用”的链路。
4.3 第三步:按链路节点逐一排查,用速查表定位
我整理了一个常用排查速查表,每个方向都值得在故障时快速过一遍:
| 顺序 | 检查对象 | 关键指标/命令 | 判定方法 |
|---|---|---|---|
| 1 | 调度器日志 | 拒绝码、backend状态变化历史 | 确认是哪类拒绝:inactive、timeout、circuit open |
| 2 | 健康检查探针 | 探针类型、超时阈值、连续失败次数 | 探针能否反映真实读写能力 |
| 3 | KV节点进程 | 进程存在性、端口连通性 | 确认不是崩溃或网络分区 |
| 4 | KV线程池 | 活跃线程数、排队任务数、拒绝任务数 | 线程池是否被打满 |
| 5 | 存储引擎内部 | memtable、WAL、compaction积压状态 | 是否触发写入保护机制 |
| 6 | 分片维度容量 | 每个分片/节点的实际用量、热点分布 | 是否出现单分片水位失衡 |
| 7 | 资源配额/预留 | 租户配额、文件系统预留、inode | 面板用量不等于实际可写剩余空间 |
大多数情况下,答案会在第4到第6项里浮出来。我把这步称为“反向追踪”:从调度器的拒绝行为出发,一路推导到KV内部真正的瓶颈点,不要停留在“容量面板是否正常”这个表面问题上。
4.4 实操心得:记录时间线是所有排查的前提
还有一条长期积累的教训:一旦发现这个类型的故障,要立刻记录“什么时间点、调度器开始拒绝、哪个后端节点第一次异常、业务报错量和拒绝数的曲线”。如果没有同步好的时间线,复盘的时候很容易出现“调度器说是存储先挂了,存储面板显示当时一切正常”这种无解的口水仗。
推荐的做法是:故障发生时先把监控系统的关键面板截图或者导出原始数据,哪怕当时看不出来什么,事后对比时间线,往往能在调度器的错误率曲线和KV的某个指标曲线上找到共振点。我经历的几次同类故障,最终定位都依赖这些细碎的时间戳。
5. 一个典型根因实例:热分片写满引发的全线熔断
讲一个我实际调试过的案例,结构和你遇到的这个问题基本一致。
业务侧用了自研KV存储,前置一个负载调度器做读写分发。某天大促压测一开始,大量写请求集中到一个特定的key前缀——这批key描述的是同一个热销商品的库存和价格信息。因为Key前缀集中,一致性哈希把它们都打到了一个分片上。
起初只是这个分片的CPU和磁盘写流量偏高,全局面板上完全看不出来。但这个分片的写入速度超过了内部compaction的消化速度,memtable开始积压,sst文件越堆越多。等到某次后台合并触发了临时空间水位保护,这个分片直接进入“只读”状态,所有路由到该分片的写请求开始返回失败。
这时候调度器怎么反应的呢?它的健康检查探针探测的是KV节点进程和端口,所以节点状态依然显示“存活”;但调度器还有一层熔断逻辑,它统计过去5秒内转发请求的错误率。短短几秒内,因为所有写请求都命中这个热分片,错误率迅速冲高到阈值以上,熔断器打开,调度器就把所有写流量都挡下了。于是业务侧的表现就是:面板上KV用量才26%,调度器却一条请求都放不进去。数据全堆在那一个分片里,其他分片都被热分片拖累了。
我把当时的处理动作拆成几步,供遇到类似场景时参考:
- 先解除表面熔断。把调度器的熔断阈值临时调高一点,或者手动把熔断器恢复到半开状态,让一部分探活请求能进去,确认KV节点是否可恢复写入。
- 定位到热分片后,在KV侧通过管理接口强制触发了该分片的compaction,优先清理堆积的sst文件,同时暂停了对该分片的写入流量。
- 对热key的写入路径做临时修改,在KV的路由层对key做哈希散列,把同一个商品key的写入分散到多个分片。这里注意,散列方案会影响读取路径,要同步修改路由配置。
- 等分片容量回落到安全水位、业务错误率归零后,再把调度器的熔断配置恢复原值,观察一段时间确认稳定性。
这个案例的价值在于:它同时踩中了全局面板误判、单分片容量失衡、调度器健康检查和熔断层级错位这几个典型坑。只要少看任何一个维度,排查方向就会跑偏到“为什么磁盘没满却写不了”的死胡同里。
6. 复盘与预防:把容量面板和调度准入对齐
6.1 监控维度必须补上“分片级容量”和“引擎内部状态”
经历过几次类似故障后,我的监控改造原则很明确:全局面板可以保留,但不能用它作为容量唯一的决策依据。必须增加分片维度的容量监控,每个分片单独设置容量预警和写保护告警。一旦任何分片的利用率超过70%或80%,就要提前介入,而不是等到全局平均数值异常再处理。
同时,把KV引擎内部的几个关键指标纳入监控:memtable当前总大小、flush排队数、compaction排队数、线程池活跃线程数、等待队列长度。这些指标才是真正反映“还能不能写”的指标。调度器的健康检查探针也要改,从“端口存活检查”升级为“真实读写探活”,比如用一条固定key做实时读写,一旦超过阈值就判定节点不健康。这个改进,能让调度器在KV“拒绝写入但进程存活”时立刻感知。
6.2 容量规划和数据分布要提前验证
预防这类故障,光有监控还不够,还得在容量规划阶段就把热点因素考虑进去。这里分享三个我常用的检查清单:
第一,压测前必须检查KV集群的key分布情况,尤其是写入请求的key前缀是否集中。可以用一致性哈希模拟工具跑一遍现有流量,看看每个分片理论上的负载是否均衡。如果写入集中在少数几个分片,要提前调整散列规则或者为对应分片增加副本。
第二,为KV磁盘预留安全缓冲区。建议容量水位控制在70%以内,给后台compaction、临时文件、日志预留足够的余量。不要等用到90%才告警,因为触发写保护的水位往往比面板看起来高得多。
第三,调度器的熔断阈值要和KV的真实容量能力匹配。一个需要高吞吐的KV后端,熔断阈值不能设得太低,否则一次慢查询或一次热点分片抖动就会熔断全部流量。试着把熔断窗口拉长、阈值抬高,给KV内部恢复时间。
6.3 故障演练和告警分级也要跟上
我自己的经验是,这类“容量充足但不可用”的故障,必须在演练中主动注入一次。比如人为把一个分片的写入速度打上去,模拟写保护触发,然后观察调度器会不会正常摘节点,监控能不能在早期发现问题,告警会不会准确分级。不要等到大促现场才第一次看到“面板显示用量正常但服务拒绝流量”的诡异现象。
告警分级方面,我建议分三级看:第一级是分片容量超过70%或写拒绝数开始上升,属于预警;第二级是单分片进入只写保护或线程池活跃线程持续打满,属于故障;第三级是调度器开始熔断、业务错误率飙升,属于重大事故。每一级要有对应的处理负责人和预案,这样再遇到类似问题,至少不会慌乱。
我个人的体会是:这种故障最考验的不是单点排查能力,而是对整个链路各层“健康定义”的统一认知。容量面板、健康探针、熔断窗口、引擎写保护,各自在不同维度上定义“能不能用”。你在做监控、设定告警、调调优参数的时候,要时刻提醒自己:这些指标反映的是同一条链路的不同切片,切片之间可能有滞后、有口径差异,甚至互相矛盾。把这一层想明白,下次再看“面板26%但一条请求都进不去”,你至少能少走一半弯路。