Kafka 的存储模型长期只有一层:所有消息落在 broker 本地磁盘,保留期一过就删。这在「留存 7 天」的时代足够,但当业务要求「保留 30 天甚至一年」「随时可回溯历史事件」时,本地盘就成了成本与容量的死结。KIP-405 引入的分层存储(Tiered Storage)把数据分成热层(本地盘)与冷层(对象存储),让 Kafka 第一次可以把历史数据放在比本地盘便宜一个数量级的介质上,同时保持对消费者透明的读取语义。
1. 为什么需要分层存储
1.1 本地盘容量与成本的矛盾
Kafka 的容量规划一直是个两难。按「峰值写入速率 × 保留时长」估算,一个日均写入 1 TB 的集群,保留 7 天就要 7 TB 有效容量;考虑副本因子 3,实际裸容量 21 TB。若业务把保留期改成 30 天,裸容量立刻涨到 90 TB——而这些数据里真正被读的比例,可能不到 1%。
问题在于本地盘无法「按需扩容」:Kafka 的分区与 broker 是绑定的,扩容意味着加盘、重平衡分区、迁移数据,每一步都有停机与运维成本。于是团队被迫在两种坏方案之间选:要么缩短保留期,牺牲回溯能力;要么买昂贵的高性能 SSD 去装那些几乎不会被读的历史数据。
# 一个典型的容量账(副本因子 3)
# 日写入 1 TB
# 保留 30 天
# 有效数据 30 TB
# 裸容量 90 TB(3 副本)
# 读比例 历史数据 < 1% 的请求会触达
# 结论 90 TB 本地 SSD 里,99% 的字节在「躺尸」
1.2 留存与成本的权衡
分层存储把「存多久」和「存哪里」解耦。热数据(最近写入、被消费延迟最低的那一段)留在本地盘,保证生产与消费的低延迟;冷数据(超出本地保留窗口的历史 segment)卸载到对象存储,用极低的单价长期保存。消费者读冷数据时,broker 从对象存储回源并缓存,读到的是同样的 offset、同样的消息,应用层几乎无感。
这把成本曲线从「线性随保留期增长」压成了「本地盘固定 + 对象存储按量」。本地盘容量只需覆盖热层窗口,不再需要为历史数据买单。
更准确地说,分层存储把「容量规划」从空间问题变成了时间问题:你不再需要为「存多少 GB」买盘,而只需为「最近多久的数据要留在本地」买盘。这个视角的转换,让容量规划从一次性的重决策变成了可随时调整的参数。
1.3 典型场景
- 事件溯源与审计:需要保留 90 天以上的原始事件,用于重放、对账、合规审计。
- CDC 与数仓同步:上游变更日志保留期长,下游消费进度可能滞后数天,需要回读历史。
- 跨集群重放:新集群或新消费方需要从历史某点开始消费,冷数据可被回源读取。
- 成本敏感的大流量集群:写入量大但历史读少,分层存储能显著降低单位 GB 成本。
若集群本身只需要保留几小时的数据、且读放大严重,分层存储带来的回源延迟可能得不偿失。它解决的是「容量与成本」问题,不是「延迟」问题——延迟反而是它引入的新代价。关于本地存储本身的组织方式,可以参考 https://plumephp.com/kafka-storage-log-compaction/。
还有一个容易被忽略的收益:分层存储让分区迁移变轻。分区重平衡时,若历史 segment 已在远端,新 broker 只需同步本地热层数据,迁移的数据量大幅下降,扩容与故障恢复都更快。这也是很多团队在容量尚未告急时提前上分层存储的理由——它顺带优化了集群的运维弹性。
2. KIP-405 架构与 Remote Log Manager
2.1 整体组件与角色
KIP-405 在 broker 内部新增了一组组件,负责把「写本地」扩展成「写本地 + 卸载远端」:
| 组件 | 职责 | 位置 |
|---|---|---|
| RemoteStorageManager(RSM) | 实际读写远端对象的插件接口 | broker 侧 |
| RemoteLogMetadataManager(RLMM) | 维护远端 segment 的元数据与状态 | broker 侧 |
| RemoteLogManager(RLM) | 编排卸载、清理、回源读取的调度器 | broker 侧 |
| RemoteLogSegmentMetadata | 描述一个已卸载 segment 的元数据记录 | 元数据载体 |
关键设计原则:所有远端交互都通过插件接口抽象。RSM 决定「怎么存」,RLMM 决定「元数据放哪」。Kafka 自身不绑定任何云厂商,生产上你需要提供一个 RSM 实现(如 Aiven tiered storage 插件),或自行对接 S3、GCS、Azure Blob。
RSM 的接口方法很精简,理解它们就理解了卸载的边界:
# RemoteStorageManager 的核心方法(简化签名)
# copyLogSegmentData(metadata, segmentData) 上传一个 segment 的全部文件
# fetchLogSegment(metadata, startPos, endPos) 按字节区间回源读取
# fetchIndex(metadata, type) 回源读取偏移索引或时间索引
# deleteLogSegmentData(metadata) 删除远端 segment
注意 fetchLogSegment 是按字节区间而非 offset 读取的:RLM 先取回索引定位到字节位置,再按区间拉取日志数据。这也解释了为什么索引文件必须与日志文件一起上传——少了索引,回源就无法高效定位。
2.2 RemoteLogManager 与 RLMM
RLM 是每个 broker 上的单例服务,它做三件事:
- 上传:当本地 segment 满足卸载条件,RLM 通过 RSM 把 segment 的日志文件与索引文件上传到远端。
- 删除:本地 segment 超过
local.retention.ms后,RLM 从本地删除(但远端元数据保留)。 - 回源:消费者请求的 offset 落在已卸载区间时,RLM 通过 RSM 从远端拉取并交给 fetch 路径。
RLMM 的默认实现是 TopicBasedRemoteLogMetadataManager,它把元数据写进一个内部主题 __remote_log_metadata。这个主题自身不能启用分层存储,且必须有足够的副本与分区,否则元数据写入失败会直接阻塞卸载。
# broker 侧启用分层存储的核心配置
remote.log.storage.system.enable=true
remote.log.storage.manager.class.name=io.aiven.kafka.tieredstorage.RemoteStorageManager
remote.log.metadata.manager.class.name=org.apache.kafka.server.log.remote.metadata.storage.TopicBasedRemoteLogMetadataManager
remote.log.metadata.manager.listener.name=PLAINTEXT
rlmm.config.remote.log.metadata.topic.num.partitions=50
rlmm.config.remote.log.metadata.topic.replication.factor=3
remote.log.manager.task.interval.ms=30000
remote.log.manager.thread.pool.size=10
2.3 消费者侧 fetch 路径
对消费者而言,分层存储是完全透明的。消费者照常发送 FetchRequest,broker 判断请求的 offset 落在本地还是远端:
- 本地命中:走原有零拷贝路径,从本地 segment 读取。
- 远端命中:RLM 触发一次远端读取任务,从对象存储拉取对应 segment,放入 broker 本地的一个临时缓存目录,再从缓存返回数据。
broker 返回给消费者的 FetchResponse 里,数据长度、offset、时间戳与本地读取完全一致。唯一的差别是延迟——第一次读冷数据要等回源,后续读同一 segment 则命中缓存。
# 冷读的完整路径
# 1) 消费者 FetchRequest(offset=1000),该 offset 已卸载
# 2) broker 查 RLMM:定位 offset 1000 属于哪个远端 segment
# 3) RLM 提交回源任务到线程池
# 4) RSM.fetchIndex 取回索引,定位 offset 1000 的字节位置
# 5) RSM.fetchLogSegment 按字节区间拉取日志数据
# 6) 数据写入本地回源缓存,再返回给消费者
# 7) 后续读同一 segment 直接命中缓存,不再回源
消费者无需任何配置改动,这也意味着:一个配置错误的分层存储集群,可能让消费者的延迟悄悄劣化而无人察觉。集群层面的可用性设计可参考 https://plumephp.com/kafka-cluster-ha/。
还有一点值得强调:回源发生在 broker 侧而非客户端侧。消费者的网络拓扑、凭证、访问权限都不需要触碰对象存储,这也是分层存储相比「客户端自行读 S3」方案的核心优势——安全边界与语义一致性都保留在 Kafka 内部。
3. 本地保留窗口与卸载条件
3.1 local.retention.ms 与 local.retention.bytes
分层存储下,topic 的保留策略被拆成两层:
local.retention.ms/local.retention.bytes:本地热层的保留窗口。超出后 segment 从本地删除。retention.ms/retention.bytes:远端冷层的保留窗口。超出后 segment 从对象存储删除。
两者的关系必须是「本地窗口 ≤ 远端窗口」,否则数据会在还没卸载前就被本地删除,造成数据丢失。
# topic 级配置:本地留 1 天,远端留 30 天
bin/kafka-configs.sh --bootstrap-server localhost:9092 \
--alter --entity-type topics --entity-name orders \
--add-config remote.storage.enable=true,local.retention.ms=86400000,local.retention.bytes=10737418240,retention.ms=2592000000
# 查看生效值
bin/kafka-configs.sh --bootstrap-server localhost:9092 \
--describe --entity-type topics --entity-name orders
local.retention.bytes 通常比 ms 更关键:它直接决定本地盘要预留多少容量。经验做法是让本地保留窗口覆盖「最大消费延迟 + 缓冲」,而不是拍一个天数。
| 配置项 | 作用层 | 语义 | 默认 |
|---|---|---|---|
| remote.storage.enable | topic | 是否启用分层存储 | false |
| local.retention.ms | topic | 本地热层保留时长 | 继承 retention.ms |
| local.retention.bytes | topic | 本地热层保留字节数 | 继承 retention.bytes |
| retention.ms | topic | 远端冷层保留时长 | 604800000 |
| retention.bytes | topic | 远端冷层保留字节数 | -1 |
当 local.retention.* 未显式设置时,它默认继承 retention.*,等于本地与远端窗口一致——此时分层存储只会把数据「搬运」而不会「释放」本地盘,起不到容量优化的作用。因此显式设置 local.retention.* 才是分层的真正开关。
3.2 retention.ms 与卸载的关系
一个常见的误解是「retention.ms 到了才卸载」。实际上卸载与远端保留是两个独立的时间判断:
- 卸载触发:segment 一旦「封口」(不再写入,即已滚动到下一个 segment)且超出
local.retention.ms,就进入待上传队列。注意——是先上传到远端、成功之后才从本地删除。 - 远端保留:
retention.ms到期后,RLM 才删除远端对象与元数据。
因此即使 local.retention.ms 很短,数据也不会丢:它只是从本地搬到了远端。真正的删除只由 retention.ms 决定。
3.3 copy 与 delete 的语义
RLM 的卸载是先复制、后删除(copy-then-delete)。若上传失败,本地 segment 不会被删——这保证了「宁可占本地盘,也不能丢数据」。失败会按 remote.log.manager.task.interval.ms 的节奏重试。
需要特别注意的是:卸载以 segment 为最小单位。如果某个 segment 长期不封口(例如分区写入极慢,segment.ms 设得很大),它永远不会被卸载,本地盘就会被这个「活跃但巨大」的 segment 撑住。生产上应配合 segment.ms 与 segment.bytes 控制 segment 大小,让滚动频率与卸载节奏匹配。
# 让 segment 更快封口,才能更快卸载
segment.ms=3600000
segment.bytes=1073741824
# 高吞吐 topic 可放大 segment.bytes 减少文件数
# 低频 topic 必须调小 segment.ms,否则永远不滚动
一个经验值是:segment.ms 应显著小于 local.retention.ms,否则在本地窗口内根本来不及滚动出可卸载的 segment,分层存储形同虚设。对于低流量分区尤其要注意,因为它们最容易长时间不封口。
4. 对象存储布局与元数据
4.1 remote log segment 的命名
KIP-405 只规定了 RSM 接口,不规定对象存储上的路径格式,具体布局由 RSM 实现决定。主流的参考实现大致遵循这样的命名:
# 远端 segment 的逻辑标识(由 RemoteLogSegmentMetadata 描述)
# 字段: topic 分区号 起始 offset 结束 offset 时间戳 唯一 uuid
# 示例: orders-0-00000000000000000100-00000000000000000499-1696000000000-3f2a...
因为 uuid 的存在,即使同一个 offset 区间被重复上传,也不会互相覆盖,避免了幂等性问题。
offset 用零填充到固定宽度,是为了让对象存储的字典序与 offset 的数值序一致——这样按前缀列举对象时,天然就是按时间与 offset 排好序的,便于排查与批量操作。
4.2 index 与 metadata 文件
一个 segment 在本地并不是单个文件,而是一组:.log 日志文件、.index 偏移索引、.timeindex 时间索引,可能还有事务索引与生产者快照索引。RSM 的 copyLogSegmentData 接口会把这组文件一次性上传,保证远端也有完整的索引,回源读取时才能按 offset 高效定位。
元数据(哪些 segment 已上传、状态如何、leader epoch 等)不放在对象存储,而由 RLMM 统一管理。默认实现写进 __remote_log_metadata 主题。这个主题的写入延迟与可用性直接决定卸载能否推进——它是分层存储的隐藏单点。
4.3 路径结构与 RLMM 插件
自定义 RSM 时,常见做法是按 cluster/topic/partition/segment 分层组织对象键,便于按前缀做生命周期策略与权限隔离:
# 以 S3 为例的路径组织(具体由 RSM 决定)
# s3://my-kafka-tier/prod-cluster/orders/0/00000000000000000100-.../
# ├── 00000000000000000100.log
# ├── 00000000000000000100.index
# └── 00000000000000000100.timeindex
RLMM 也可以替换。当默认的 topic-based 实现在大规模下成为瓶颈(元数据主题分区过多、跨集群访问受限),可以换成基于外部数据库或对象存储的实现,代价是引入新的依赖与一致性考量。选择 RSM 与 RLMM 时,务必确认其与你的 Kafka 版本兼容,并支持你所用的存储后端。
5. 读写路径与延迟
5.1 热读与冷读的分野
读写路径的分野完全由 offset 决定:
- 热读(本地命中):与未启用分层存储时性能一致,走页缓存与零拷贝,P99 通常在毫秒级。
- 冷读(远端命中):需要一次对象存储的 GET,延迟取决于对象大小与网络往返,首次读通常几十到几百毫秒。
关键区别在于:冷读的延迟是突发性的。正常消费跟得上写入时几乎不碰冷数据;一旦消费组滞后超过本地保留窗口,或新消费方从历史位点开始消费,冷读比例会突然升高,延迟随之抬升。
这也解释了为什么「消费组滞后」在分层存储集群里格外危险:滞后一旦超过本地窗口,消费就从热读滑向冷读,延迟抬升又让消费更慢,滞后进一步扩大,形成正反馈。本地窗口本质上就是这套系统抵御滞后雪崩的缓冲带。
5.2 预取与本地缓存
为降低冷读延迟,broker 侧有几层缓解机制:
- 回源缓存:RLM 把拉取的远端 segment 放入本地缓存目录,后续读同一 segment 直接命中,不重复回源。
- 预取:RLM 可在拉取时一次性取回相邻的多个 segment,减少往返次数。
- 消费端并行:消费者若同时拉取多个分区,冷读的延迟可被并行掩盖。
缓存是有限且会被淘汰的:如果消费模式是「随机跳读历史」而非「顺序回放」,缓存命中率会很低,回源次数上升,对象存储的请求费与流量费也会随之上涨。
一个实用的判断标准是「冷读是否顺序」。顺序回放(从某个历史 offset 一路追到当前)几乎每次回源都能命中相邻 segment,延迟平滑;随机跳读(按 key 查历史、按时间点抽查)则会频繁 miss,此时应优先考虑把数据落到可查询的存储(如数仓、对象存储上的 Iceberg 表)而非依赖 Kafka 回源。
5.3 fetch 延迟的影响因素
影响冷读延迟的主要因素:对象存储的首字节延迟、segment 大小(越大越慢)、回源并发度(remote.log.manager.thread.pool.size 决定同时能处理多少回源任务)、以及broker 到对象存储的网络带宽。
调优方向很直接:调大回源线程池、控制 segment 大小、把 broker 部署在离对象存储更近的区域。这些与整体 broker 的吞吐调优一脉相承,可参考 https://plumephp.com/kafka-performance-tuning/。
6. 成本与延迟权衡
6.1 存储成本对比
分层存储的核心收益是单位 GB 成本。以下为量级对比(具体价格随云厂商与区域波动):
| 介质 | 相对单位成本 | 访问延迟 | 适用数据 |
|---|---|---|---|
| 本地 NVMe SSD | 100 | 亚毫秒 | 热数据、实时消费 |
| 本地 HDD | 20 至 30 | 毫秒级 | 温数据、大容量本地盘 |
| 对象存储标准层 | 3 至 5 | 数十毫秒 | 冷数据、低频读取 |
| 对象存储低频层 | 1 至 2 | 数十至百毫秒 | 归档、极少读取 |
把 90% 的历史数据从本地 SSD 挪到对象存储低频层,存储成本往往能下降 60% 以上。但成本账不能只算存储单价——还要算回源流量费与请求费。
6.2 回源流量与请求费
对象存储的计费是双向的:存储费之外,读回(GET)请求与出流量都要付费。冷读频繁的场景下,回源流量费可能反超存储节省。
# 回源成本的粗略估算
# 单次回源流量 = segment 大小
# 回源次数 = 冷读请求数 / 缓存命中率
# 月回源流量 = 回源次数 × segment 大小
# 注意:跨区域回源还会叠加跨区流量单价
工程建议:让本地保留窗口足够覆盖正常消费延迟,把冷读压到极低频的「回放、审计、补数」场景。若某个消费组长期滞后、天天回源,说明本地窗口设置不当,应先调窗口而不是硬扛流量费。
对象存储的计费还有一个隐性项:请求次数。小 segment 意味着更多对象、更多 GET 请求;大 segment 则单次回源流量更大。需要在「对象数量」与「单次回源体量」之间找平衡,通常把 segment 控制在几百 MB 到 1 GB 是比较稳妥的区间。
6.3 实例选型与 SLA
分层存储改变了 broker 的选型逻辑:
- 本地盘不再需要「按总留存容量」配置,可按热层窗口 + 缓冲来配,容量压力骤减。
- CPU 与网络反而更吃紧:回源与缓存管理会消耗额外 CPU 与带宽。
- 若对冷读延迟有 SLA 要求(例如历史回放必须在秒级完成),需要评估对象存储的首字节延迟能否满足。
延迟与成本是一对无法两全的变量:本地窗口越长,延迟越稳、成本越高;窗口越短,成本越低、回源越频繁。分层的艺术就在这个旋钮上。
选型时可以先算三笔账:存储账(本地盘按热窗口配、远端按总量配,对比全本地方案省多少)、流量账(预估冷读频次与单次回源量,估算月出流量费)、延迟账(冷读路径的 P99 是否满足业务 SLA)。三笔账都算清,才能判断分层存储对当前业务是净收益还是新负担。通常写入大、读少、留存长的场景收益最明显,而高频随机读历史数据的场景则要谨慎。
7. 迁移、回退与常见坑
7.1 启用的前置条件
启用分层存储前,必须确认:
- broker 版本支持 KIP-405(Kafka 3.6+ 起为正式特性)。
- 已选定并部署 RSM 插件,且 broker 能访问对象存储(凭证、网络、权限)。
- RLMM 元数据主题(
__remote_log_metadata)已规划好分区数与副本数。 - 已评估对象存储的请求配额与限流策略。
同时要确认权限的最小集:RSM 需要对目标前缀的 PutObject、GetObject、DeleteObject、ListBucket 权限,缺任何一项都会让某类操作静默失败。建议先用一个测试 topic 走通「写入 → 卸载 → 回源读取 → 到期删除」的完整闭环,再上生产。
7.2 升级与启用顺序
正确顺序是先升级集群、再逐 topic 启用:
- 先把所有 broker 升级到支持分层存储的版本,此时功能是关闭的,无行为变化。
- 配置 RLM 与 RSM,重启 broker(可滚动)。
- 再对目标 topic 逐个
remote.storage.enable=true。启用后新滚动的 segment 才会被卸载,已有 segment 不会追溯上传。 - 观察一段时间,确认卸载、回源、延迟均正常,再扩大范围。
不建议「一次性全量开启」:分层存储会改变 broker 的磁盘与网络行为,逐 topic 灰度能让你在影响面可控的前提下验证 RSM 与 RLMM 是否真正可用。尤其要留意第一批被卸载的 segment 是否能在需要时被正确回源读取——这是最容易被忽略、也最致命的验证点。
# 逐 topic 启用并验证
bin/kafka-configs.sh --bootstrap-server localhost:9092 \
--alter --entity-type topics --entity-name orders \
--add-config remote.storage.enable=true
# 确认 topic 已开启分层存储(看 remote.storage.enable 是否为 true)
bin/kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic orders
7.3 关闭与回退风险
关闭(remote.storage.enable=false)不等于数据自动回到本地。已卸载到远端的 segment 不会自动拉回;如果本地窗口很短,关闭后可能出现「本地没有、远端有、但不再回源」的空洞。回退前必须确认本地仍保留全部所需数据,或先把远端数据迁移回本地。
另一个风险是 RSM 或 RLMM 插件的升级:插件版本与 broker 不兼容,会导致卸载静默失败。失败通常只体现在本地盘增长,不会报错,需要主动监控。
还有一类回退陷阱:远端数据被外部工具删除或生命周期策略清理。若对象存储上配置了「30 天自动过期」的生命周期规则,而 Kafka 侧 retention.ms 设的是 90 天,会出现「Kafka 以为数据还在、实际对象已没了」的错配。两端保留策略必须对齐,且远端删除应交给 Kafka 的 RLM,而不是对象存储的生命周期规则。
7.4 监控指标
分层存储的关键指标分布在 broker 与 partition 两级:
| 指标 | 含义 | 关注点 |
|---|---|---|
| RemoteCopyLagSegments | 待上传 segment 数 | 持续增长说明上传跟不上 |
| RemoteDeleteLagSegments | 待删除 segment 数 | 长期不降说明清理卡住 |
| RemoteLogSizeBytes | 远端数据总量 | 用于成本与容量核算 |
| RemoteLogReaderTaskQueueSize | 回源任务队列长度 | 队列堆积说明回源过载 |
| RemoteLogManagerTasksAvgIdlePercent | RLM 线程空闲率 | 接近 0 说明线程池太小 |
除上述指标外,还应监控本地磁盘使用率(判断卸载是否正常推进)与消费组滞后(判断冷读是否异常)。完整的监控体系搭建可参考 https://plumephp.com/kafka-monitoring-operations/。
# 通过 JMX 拉取分层存储指标(示例)
# 查看 RLM 线程空闲率与回源队列
# jmxterm / Prometheus jmx_exporter 均可采集
# 关键 MBean 前缀: kafka.log.remote
# 典型 MBean: kafka.log:type=Log,name=RemoteLogSizeBytes,topic=orders,partition=0
告警建议:RemoteCopyLagSegments 持续大于 0 超过 10 分钟即告警(上传滞后),RemoteLogReaderTaskQueueSize 持续增长即告警(回源过载)。这两个指标是最能提前暴露问题的信号。
7.5 常见错误与排查
- 元数据主题写入失败:
__remote_log_metadata副本不足或磁盘满,卸载全部卡住。优先检查该主题健康度。 - 本地盘不降反升:segment 不滚动(
segment.ms过大)导致永不封口,无法卸载。 - 冷读延迟飙升:回源线程池过小或缓存命中率低,调大
remote.log.manager.thread.pool.size。 - 凭证过期导致卸载静默失败:对象存储的临时凭证需有自动续期机制。
- local.retention 大于 retention:数据在本地未卸载就被删除,属于配置事故,必须加校验。
- 对已启用 topic 调小 retention.ms:可能导致远端数据被提前删除,回源时读不到历史,需谨慎操作。
排查时有一条通用思路:先看元数据主题,再看本地盘,最后看回源指标。元数据主题不健康则一切停滞;本地盘持续增长说明卸载没推进;回源指标异常则说明读路径有问题。按这个顺序能快速定位到是「写不进远端」「删不掉本地」还是「读不回来」这三类问题中的哪一类。
8. 总结
分层存储的本质是把 Kafka 的存储从「单一本地层」拆成「本地热层 + 对象存储冷层」,用 KIP-405 定义的 RSM、RLMM、RLM 三件套把冷热调度对消费者完全透明。工程落地的关键有四点:本地保留窗口要覆盖真实消费延迟,否则回源流量费会吃掉存储节省;卸载是 copy-then-delete,本地盘不会因上传失败而丢数据,但会因 segment 不滚动而膨胀;元数据主题是隐藏单点,它的健康度决定整个卸载链路能否推进;回退不等于数据回流,关闭前必须确认本地数据的完整性。把成本账算清、把回源延迟纳入 SLA 评估、把监控指标建全,分层存储才能从「省钱方案」变成「可长期依赖的架构能力」。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。