想象你是一家大型快递公司的仓库主管。仓库里每天蜂拥而至成千上万个包裹,有的今天就要派送,有的要放一周,有的可能要存放三个月。如果把所有包裹都堆在收银台旁的“快速取货区”,仓库很快就会爆满,而且找包裹会越来越慢。聪明的做法是分层:快速取货区放今天要送的,普通货架区放近期可能要调的,地下冷库放很久没人碰的。这样仓库既能高效运转,又不用无限扩建。 Apache Pulsar 的消息存储正是这个思路。它把“处理消息的大脑”和“存放消息的仓库”彻底分开,用 BookKeeper 这套“账本系统”来管理海量消息。消息像包裹一样先放在最方便的位置,随着时间推移自动沉降到更便宜的存储层——这就像给消息建了一座“会自己整理货架”的仓库,不用人工搬来搬去。读完这篇文章,你会理解这座“智慧仓库”的每一块砖是怎么砌的。

岗位关联

理解 Pulsar 分层存储与 BookKeeper 原理,能帮你在海量消息场景下设计高可靠、低成本的消息架构,也是面试消息队列深水区时证明“读过源码”的关键谈资。

目录
  1. 可视化演示
  2. 分层存储架构
  3. 账本核心模型
  4. 读写一致性
  5. 存储模型对比
  6. 保留与容灾
  7. 选型与迁移
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

演示Pulsar计算存储分离架构、BookKeeper的Ledger与Entry复制模型,以及热冷分层存储的卸载与成本优化,可切换三种模式观察。

本节配有交互动效(进入视口后自动加载,约 3KB)。

存储分离:计算与存储解耦 Broker负责消费服务,消息封装为Ledger Entry BookKeeper独立持久化,计算与存储可分别扩展 写可用性 = 1 - (d/N)^R 写入需R个副本确认,d个Bookie故障仍可写。

分层存储架构

想象那座智慧仓库。仓库主管最头疼的是“好地方有限”:收银台旁的快速取货区最贵但也最好用,地下冷库便宜却要坐电梯。聪明的仓库不会把三个月前的包裹堆在取货区,而是按“多久会被取走”分区域摆放。Pulsar 的分层存储,就是给消息仓库设计了同样的分区规矩。

在 Pulsar 中,处理消息的 Broker 只负责“接待和分拣”(计算),真正存放消息的是一套独立存储集群,用 BookKeeper 来管理,这就是“计算与存储分离”:收银员和仓库不再挤在同一间屋子里。需要更强的收发能力就多加 Broker,需要更大空间就多部署 Bookie 存储节点,两边各自扩容、互不干扰。

消息写入后按“年龄”自动分层:最新的一批留在最贵最快的热层,通常由 BookKeeper 的磁盘缓存承接;再过几天没被消费,就留在 BookKeeper 的常规存储(温层);一旦账本关闭且超过配置的卸载阈值,后台的卸载任务会把这段数据搬去 S3 等对象存储,这就是冷层。越冷的数据,每 GB 单价越便宜。

这套自动分拣就像仓库里的自动叉车,不需要主管操心,时间一到旧包裹自己就搬去冷库了。于是无论消息保留多久,热区永远清爽,账单永远可控。

Ssave=Vcold×(Pwarm−Pcold)\displaystyle S_{\text{save}} = V_{\text{cold}} \times (P_{\text{warm}} - P_{\text{cold}})
这个公式算的是“分层到底省了多少钱”:把一定体积的数据从温层搬到冷层,省下的成本等于数据量乘以两层的单价差。
  1. S\_save(节省的成本)分层存储相比全部放在温层所节约的总存储开销,单位通常是元或美元/月。
    SsaveS_{\text{save}}
  2. V\_cold(冷数据量)被卸载到冷层的数据体积,单位是 GB。只有已经“变老”的数据才会参与卸载。
    VcoldV_{\text{cold}}
  3. P\_warm 减去 P\_cold(层间单价差)温层每 GB 的存储单价减去冷层每 GB 单价。对象存储之所以便宜,正是这个差值成为分层的经济动力。
    Pwarm−PcoldP_{\text{warm}} - P_{\text{cold}}
  4. 乘法(总节省)把“省下多少空间”和“每单位省多少钱”相乘,就是总收益。冷数据越多、温差越大,收益越明显。
    Vcold×(Pwarm−Pcold)V_{\text{cold}} \times (P_{\text{warm}} - P_{\text{cold}})

账本核心模型

仓库里最怕什么?丢件。为了不丢件,每一件包裹的出库入库都要记在账本上。BookKeeper 这个名字本身就是“记账人”,它的核心模型就是“账本加账页”。

BookKeeper 把消息组织成 Ledger(账本分册)和 Entry(账页条目)。一个 Ledger 是一段连续追加的日志,有唯一的编号;写入方把消息按顺序追加成 Entry,每条 Entry 有一个单调递增的编号。一个 topic 的存储会被切分成多个 Ledger,每个 Ledger 通常对应几十到几百 MB 的数据段——就像仓库把流水账按月份装订成册,而不是一本记到天荒地老。

每个 Ledger 的 Entry 不是只存在一个地方。BookKeeper 会把同一份数据分散存到多个存储节点(Bookie)上。为了容错,通常会配置:总副本数(ensemble size)3 份、写入时需要至少 write quorum 个节点参与、确认时至少 ack quorum 个节点返回成功(一般 2)。这就是“多人各抄一份账本,关键条目要至少两人确认”。

有了账本分册,阅读方可以自由地按 Ledger 顺序回放,也可以只翻某一册。账本一旦写满并关闭就不再改动,天然支持只读归档——这正是“装订好的账本不能撕页重写”的道理。

ack≥Qw⇒confirmed\displaystyle \text{ack} \ge Q_w \Rightarrow \text{confirmed}
这是 BookKeeper 写入成功的“盖章规则”:只有当返回确认的副本数 ack 达到写仲裁数 Q_w 时,这条 Entry 才算真正写入成功。
  1. ack(确认副本数)一条 Entry 写入后,有多少个 Bookie 返回了“我已存好”的应答,相当于签收单上盖了几个章。
    ack\text{ack}
  2. Q\_w(写仲裁数)ack quorum 的缩写,表示最少需要几个副本确认才向客户端报告成功,生产环境典型值是 2。
    QwQ_w
  3. 比较与判定当确认数达到门槛,整个写入才成立;否则客户端收到失败并可重试。就像签收单必须盖满两个章才算生效。
    ack≥Qw⇒confirmed\text{ack} \ge Q_w \Rightarrow \text{confirmed}

读写一致性

几个仓库管理员同时记账,最容易出乱子:新来的管理员还没抄完,就有人拿着他的账本去发货。消息系统也一样,读者如果读到“尚未被足够多副本确认”的数据,在节点故障时就会出现读到一半又没了的情况。

BookKeeper 用 LAC(Last Add Confirmed,最后确认水位)来解决这个问题。写入方每次追加 Entry,要等足够多的副本回复“我记下了”,才会更新 LAC。读取方只承诺读到 LAC 为止的数据,LAC 之后就属于“尚未确认区”,读者要么等,要么被告知读不到。这就像仓库发货前要核对盖章数,没盖满章的记录绝不会被取出仓库。

写入流程是纯追加的:先选一个 Ledger 的尾部,把 Entry 发给多个 Bookie,Bookie 落盘后再应答。acks 满足 ack quorum 后,才回给生产者“成功”。读取时按 EntryId 顺序读取,并且借助 LAC 知道该读到哪,不会读到“空中楼阁”。

靠着这条“确认水位”,BookKeeper 不让读者看到空中楼阁。即使某台 Bookie 突然断电,只要确认数达标,账本记录依然安全——别的分店还有同样的盖章记录。

LAC=max⁡{n∣∀i≤n:ack(ei)≥Qw}\displaystyle LAC = \max \left\{ n \mid \forall i \le n: \text{ack}(e_i) \ge Q_w \right\}
LAC 的定义:它是“从第 0 条开始,连续地、每一条都拿到足够确认”的那段前缀里,最大的序号。读者只能信任这个序号之前的数据。
  1. e\_i(第 i 条 entry)Ledger 中按序号排列的记录,i 从 0 开始递增,相当于账本的第几行。
    eie_i
  2. ack(e\_i)(单条确认数)第 i 条 entry 在多个 Bookie 上被确认的次数。
    ack(ei)\text{ack}(e_i)
  3. 条件式(前缀全部确认)要求从第 0 条到第 n 条,每一条都满足确认数 ≥ Q_w,不允许中间断档。
    ∀i≤n:ack(ei)≥Qw\forall i \le n: \text{ack}(e_i) \ge Q_w
  4. 取最大值(水位位置)在所有满足条件的 n 中挑最大的那个,就是“最后一条被大家公认安全”的序号。
    max⁡{n∣… }\max \left\{ n \mid \dots \right\}

存储模型对比

如果消息仓库也有“同行”,Kafka 就像一家用“一条超长传送带”管理的仓库。每个分区是一条连续队列,放进传送带的包裹依次经过所有工位,谁要取货就在传送带上捞。传送带天然顺序好、性能好,但毛病是:传送带和地盘绑死了。

在 Kafka 中,一个 Partition 是一个物理日志,固定由某个 Broker 节点的磁盘承载。分区数越多、磁盘 IO 越吃紧,Broker 就像传送带机房堆满了货物,加机器还得先把旧货搬过去(数据重平衡)。Pulsar 则不同:Topic 只是逻辑名字,真正存储的单元是很多个 Ledger,散落在整个 BookKeeper 集群里。存储节点不够?直接加 Bookie,新账本自动往新节点上放,几乎不需要搬旧数据。

两者的一致性模型也不同。Kafka 用 ISR + 高水位(HW)表示“哪些消息可以被消费”,HW 是 ISR 中所有副本日志末尾的最小值减一;Pulsar 用 LAC 表示每个 Ledger 的确认水位。一个以分区为单位做整体水位管理,一个以账本为粒度逐条确认。

这份差异决定了性格:Kafka 适合轻量、极致的吞吐和单分区有序;Pulsar 适合高并发多租户、存储弹性伸缩和长保留。就像传送带仓库适合高产出的单一流水线,而账本仓库适合五花八门、存放期长的综合物流。

HW=min⁡r∈ISR(LEOr)−1\displaystyle HW = \min_{r \in \text{ISR}} (\text{LEO}_r) - 1
这是 Kafka 高水位的计算公式:取所有同步副本里“日志末尾偏移”最小的那个,再减一,才是消费者能看到的最高位置。它比 Pulsar 的 LAC 更“一刀切”。
  1. LEO\_r(副本日志末尾)副本 r 当前写到的最后偏移,可以理解为传送带上已经放到的位置。
    LEOr\text{LEO}_r
  2. ISR(同步副本集合)与 leader 保持同步的副本列表,追不上的副本会被暂时踢出去。
    r∈ISRr \in \text{ISR}
  3. 取最小(短板决定)所有同步副本中落得最慢的那个,决定了“数据整体安全”的下限。
    min⁡r∈ISR(LEOr)\min_{r \in \text{ISR}} (\text{LEO}_r)
  4. 减一(保守水位)高水位是“所有副本都拥有的最后一条”,所以要比最小 LEO 再减 1,确保没有副本缺数据。
    min⁡r∈ISR(LEOr)−1\min_{r \in \text{ISR}} (\text{LEO}_r) - 1

保留与容灾

仓库不能无限堆货,过期的包裹要么销毁,要么归档,还要防着货架倒塌。这一节看 Pulsar 如何做“定期盘点与灾备”。

Pulsar 的保留策略有两种:按时间(如保留 7 天)或按大小(如最多 10GB),超出的历史消息会被自动删除或卸载到冷层。消息积压时不用怕——因为存储和计算分离,消费者离线多久,消息都安安静静躺在 BookKeeper 里,积压只增加存储成本,不拖垮 Broker 内存。

容灾方面,BookKeeper 有自动修复(AutoRecovery):当某个 Bookie 节点挂掉,审计线程发现某些 Ledger 的副本数低于期望值,就会自动把缺失的副本复制到其他健康节点,就像仓库货架倒了,马上从别处分店抄一份补回来。此外,Ledger 写入达到上限会滚动(roll)到新 Ledger,旧的关闭后变成只读,方便备份和卸载。

因此 Pulsar 用户最常见的体验是:不管消费者宕多久,重新上线后消息原封不动等着它;存储空间不足时,加节点即可,系统自动把存量账本副本重新铺开。

Sbacklog=r×Δt\displaystyle S_{\text{backlog}} = r \times \Delta t
积压占用的存储空间,等于生产速率乘以积压持续时长。理解这个公式,就理解为什么“存储分离”能扛住消费者长时间离线。
  1. r(写入速率)生产者每秒写入的平均字节数,单位如 MB/s。
    rr
  2. Δt(积压时长)消费者离线或消费跟不上的持续时长,单位秒。
    Δt\Delta t
  3. 乘积(积压空间)积压消息占用的存储空间等于速率乘以时长。空间够大,就不怕慢慢追。
    r×Δtr \times \Delta t

选型与迁移

换仓库管理系统是件大事。知道自己要什么,才能决定用传送带还是账本仓库。Pulsar 的优势场景很清晰:多租户共享集群、消息保留超过几天甚至几个月、需要存储与计算独立扩容、跨地域复制。

如果团队已在使用 Kafka,迁移要讲究平滑。常见路径是“双写 + 回放”:让生产者在过渡期同时写 Kafka 和 Pulsar,消费者逐步切到 Pulsar,确认稳定后再停掉 Kafka 写入;也可以借助 Pulsar 的 KoP(Kafka-on-Pulsar)协议适配层,让现有 Kafka 客户端几乎零改动连上 Pulsar。

配置实践上,关键参数包括副本三件套(managedLedgerDefaultEnsembleSize / WriteQuorum / AckQuorum)、保留策略(retentionTimeInMinutes、retentionSizeInMB)以及卸载阈值(managedLedgerOffloadThreshold)等。运维重点则是紧盯 Bookie 磁盘使用率、LAC 是否健康、副本数是否达标,以及卸载任务是否按时执行。

选型没有银弹,但给仓库装上“自动分拣 + 灵活账本”后,长期运营和弹性扩容会从容很多。如果团队正为“消息保留 30 天就爆磁盘”发愁,Pulsar 的分层存储很可能是你那把自动叉车。

Drequired=S×E\displaystyle D_{\text{required}} = S \times E
规划存储容量时,不能只按原始数据量算:副本数 E 会把磁盘需求放大。这条公式是“建仓库前先算地基”的提醒。
  1. S(原始数据量)所有消息的实际体积,单副本口径。
    SS
  2. E(副本数)ensemble size,即一条消息会被存几份。Pulsar 默认 3,意味着同样的数据占 3 倍磁盘。
    EE
  3. 乘积(总磁盘需求)规划存储时要按原始数据乘以副本数提前留足空间,否则扩容还没到位,磁盘就先爆了。
    S×ES \times E

AI 应用场景

金融交易流水的低成本长期归档

阶段:数据存储与生命周期管理

解决的问题:金融风控和反欺诈模型需要保留数年交易流水做回溯训练,传统 Kafka 保留 30 天已是极限,超过就删,导致历史样本缺失。Pulsar 分层存储把热数据留在 BookKeeper,把超过 90 天的账本自动卸载到对象存储,模型训练时按需回放冷数据,存储成本下降约 80%。

具体案例:某券商实时交易系统每天产生约 2TB 流水。接入 Pulsar 后,热层只保留 3 天,温层保留 30 天,更早的数据进入 S3。AI 风控团队回溯 2023 年某次异常波动时,从冷层按 Ledger 精确拉取当日数据,无需恢复整个集群。

车联网百万级车辆遥测数据接入

阶段:数据采集与预处理

解决的问题:百万辆车每秒上报 GPS、电池状态等遥测数据,写入峰值极高,且不同车型的数据消费延迟差异大。若用 Kafka,Broker 磁盘很快成为瓶颈。Pulsar 的计算存储分离让 Broker 只做转发,BookKeeper 集群按需扩容,积压数据不占 Broker 内存。

具体案例:某新能源车企用 Pulsar 接入 120 万辆车遥测数据,Bookie 节点从 5 个扩到 12 个即可支撑 3 倍峰值流量;离线训练任务每天批量回放 24 小时数据,不影响在线查询。

Python 演示

python54 行
class TieredStorage:    """模拟 Pulsar 分层存储: 消息随年龄从热层自动沉降到温层和冷层"""     def __init__(self, hot_cost=1.0, warm_cost=0.2, cold_cost=0.03):        self.hot = []       # 每条消息为 [msg_id, age]        self.warm = []        self.cold = []        self.hot_cost = hot_cost        self.warm_cost = warm_cost        self.cold_cost = cold_cost        self.msg_id = 0     def write(self, data):        self.hot.append([self.msg_id, 0])   # 新消息写入热层        self.msg_id += 1     def tick(self):        # 所有消息年龄 +1        for bucket in (self.hot, self.warm):            for item in bucket:                item[1] += 1        # 热 -> 温: 年龄 >= 3        moved = [i for i in self.hot if i[1] >= 3]        self.warm.extend(moved)        self.hot = [i for i in self.hot if i[1] < 3]        # 温 -> 冷: 年龄 >= 10        moved = [i for i in self.warm if i[1] >= 10]        self.cold.extend(moved)        self.warm = [i for i in self.warm if i[1] < 10]     def monthly_cost(self):        return (len(self.hot) * self.hot_cost * 30 +                len(self.warm) * self.warm_cost * 30 +                len(self.cold) * self.cold_cost * 30)  def main():    ps = TieredStorage()    daily = 100    for day in range(30):        for j in range(daily):            ps.write("msg-%d-%d" % (day, j))        ps.tick()        if day in (6, 15, 29):            print("第 %2d 天: 热=%4d 温=%4d 冷=%4d 月成本约 %6.0f" %                  (day + 1, len(ps.hot), len(ps.warm), len(ps.cold), ps.monthly_cost()))    no_tier = 3000 * ps.hot_cost * 30   # 不分层成本    tiered = ps.monthly_cost()    print("不分层成本: %.0f, 分层后成本: %.0f, 节省: %.1f%%" %          (no_tier, tiered, (1 - tiered / no_tier) * 100))  if __name__ == "__main__":    main()
目的:模拟消息在 Pulsar 三大层级中的自动沉降过程,并量化分层存储带来的成本节省。
  1. self.hot.append([self.msg_id, 0])每条新消息先进入热层,年龄为 0,对应 Pulsar 消息先落在最快的存储。
  2. for bucket in (self.hot, self.warm):每 tick 一次,热层和温层的消息年龄都加 1,模拟时间流逝。
  3. moved = [i for i in self.hot if i[1] >= 3]找出年龄达到 3 的消息,准备搬到温层,对应 ledger 关闭后的卸载条件。
  4. self.hot = [i for i in self.hot if i[1] < 3]热层只保留年轻消息,实现“自动沉降”,旧数据不再占用昂贵空间。
  5. no_tier = 3000 * ps.hot_cost * 30不分层时,3000 条消息全部按最贵单价计费,作为成本对比基线。
  6. (1 - tiered / no_tier) * 100计算分层后的成本节省百分比,运行脚本会打印出 83% 左右的节省效果。

常见误区

误区我以为 Pulsar 的分层存储,就是把旧消息复制一份扔到 S3,当作冷备份;万一主存储坏了,再从 S3 把数据拉回来恢复。

正解分层存储不是“备份”,而是“主存储体系的延伸”。当 ledger 被卸载后,原始数据会从 Bookie 上删除,对象存储成为该数据的唯一长期居所;读取时按需从冷层加载回 Broker 供消费。它更像是把包裹从货架区正式移入地下冷库,而不是多抄了一份放在别处分店。

为何常见:因为对象存储常被用作备份介质,人们容易把“归档”和“备份”混为一谈,忽略了卸载会释放原存储空间这一关键点。

误区我看到 Pulsar 里也有“追加写日志”的感觉,就以为 Ledger 对应 Kafka 的 Partition,以为它们是一样的分片概念。

正解两者层级不同。Kafka 的 Partition 是“物理日志 + 存储分配单位”,固定由某个 Broker 的本地磁盘承载;Pulsar 的 Topic 才是逻辑概念,而 Ledger 是 BookKeeper 的存储单元,一个 Ledger 的 Entry 会分散在多台 Bookie 上。可以说,Topic 由一堆 Ledger 拼成,而每个 Ledger 又是一个“分布式的小日志”。

为何常见:因为两者都表现为“顺序追加的日志”,表面结构相似,容易忽略底层分布式的本质差异。

误区我以为只要 write quorum=3、ack quorum=2,数据就有三个副本,怎么都能保证不丢,于是不再关心磁盘和节点状态。

正解ack quorum 只是写入确认门槛,真正落地还要看 Bookie 是否 fsync 到磁盘,以及故障隔离策略。如果两个副本恰好同机架断电,或者落盘后才坏盘,仍可能丢数据。可靠性的完整拼图是:足够副本数 + 落盘确认 + 机架感知 + 自动修复。

为何常见:因为初学者喜欢把“副本数”等同于“可靠性保证”,忽略了分布式系统里故障总是成群结队地来。

误区我看了些基准测试,发现 Pulsar 吞吐很高,就觉得它全面碾压 Kafka,任何场景换 Pulsar 都更优。

正解Pulsar 的强项在架构弹性,而非绝对速度。小集群、单分区极致延迟、已有 Kafka 生态沉淀的场景,Kafka 依然更轻量。Pulsar 的优势体现在多租户、长保留、独立扩容、跨地域复制这些“架构级”需求上。选型要看团队维护成本和业务增长预期。

为何常见:因为技术选型容易陷入“谁跑分高选谁”的单一维度,忽略了运维复杂度、客户端生态和迁移成本。

误区我设置了 managedLedgerOffloadThreshold=1GB,以为数据一超过 1GB,立刻就能看到 S3 里有新文件,Bookie 磁盘立刻释放。

正解卸载是异步批处理任务。Broker 的后台线程会定期扫描(默认约 10 分钟一轮),只对“已关闭”的 ledger 执行卸载;正在写入的 ledger 必须滚动关闭后才可能被卸载。所以从触发条件到实际完成,通常有数分钟乃至更长的窗口期。

为何常见:因为控制台配置像是“开关”,让人以为它立即生效,而忽略了后台调度周期和 ledger 生命周期约束。

面试题

Q1. Pulsar 中负责实际存储消息数据的组件是?

单选题 · easy

  • A.消息代理 Broker
  • B.BookKeeper 集群← 正确答案
  • C.Zookeeper 系统
  • D.Kafka Connect

答案:B。BookKeeper 是 Pulsar 的存储层,好比仓库里的货架和账本,消息真正住在里面。Broker 只负责收发计算(前台收银),Zookeeper 管元数据,Kafka Connect 是 Kafka 生态的组件,与 Pulsar 存储无关。

Q2. BookKeeper 中“账本分册”对应的核心概念是?

单选题 · easy

  • A.Entry(数据条目)
  • B.Ledger(账本分册)← 正确答案
  • C.Topic(逻辑主题)
  • D.Partition(分区单元)

答案:B。Ledger 就是“装订成册的账本”,是一组连续追加的 Entry 的集合。Entry 是账本中的一行;Topic 是面向用户的消息主题;Partition 是 Kafka 的分区概念。题干问“账本分册”,只有 Ledger 与之一一对应。

Q3. 客户端写入一条 Entry 到 Ledger,至少需要多少个副本确认才算成功?

单选题 · mid

  • A.总副本数(ensemble size)
  • B.写入副本数(write quorum)
  • C.确认副本数(ack quorum)← 正确答案
  • D.最小副本数(min quorum)

答案:C。写入成功的“盖章线”是 ack quorum,即最少需要多少个副本返回“已存好”。总副本数决定冗余上限,write quorum 决定哪些副本参与写入,min quorum 不是标准配置。就像签收单要盖满 ack quorum 个章才生效。

Q4. Kafka 判断一个副本是否还在 ISR 同步副本集合中的核心依据是?

单选题 · mid

  • A.落后时间超过阈值← 正确答案
  • B.副本磁盘空间不足
  • C.所在机器 CPU 繁忙
  • D.网络延迟降为零

答案:A。Kafka 的 ISR 判据是 follower 与 leader 的落后时间,超过 replica.lag.time.max.ms 就会被踢出。磁盘、CPU、网络会影响复制速度,但算法本身只看“是否在阈值时间内跟上”,就像仓库考核管理员只看是否按时完成交接,不看他的自行车好坏。

Q5. Pulsar 自动将冷数据卸载到对象存储的前提条件是?

单选题 · mid

  • A.Ledger 仍在打开写状态
  • B.Ledger 已关闭且超过阈值← 正确答案
  • C.Topic 有活跃消费者
  • D.Bookie 磁盘使用率低于一半

答案:B。卸载只针对已关闭(不再写入)的 ledger,并且要超过按时间或大小配置的阈值。正在写入的账本相当于“还在记账的活页本”,不能搬进档案室;是否有消费者订阅与卸载无关,磁盘使用率低也不是卸载条件。

Q6. 下列关于 Pulsar LAC(Last Add Confirmed)的描述正确的是?

单选题 · extreme

  • A.它是账本中最后写入的序号
  • B.它是获得足够确认的最大序号← 正确答案
  • C.它是消费者最后消费的位置
  • D.它是最近落盘的那条序号

答案:B。LAC 是“从第 0 条起连续全部确认”的那段前缀里的最大序号,是读者安全读水位。选项 A 是 LEO(日志末尾);C 是消费者位点;D 是单机落盘位置,落盘不等于被足够多副本确认。三者都与 LAC 的核心语义不同。

Q7. 多个客户端并发写同一个 Ledger 时,Entry 顺序如何保证一致?

单选题 · hard

  • A.通过分布式锁串行化所有写
  • B.每个 Entry 写入时先读后写
  • C.客户端分配递增 EntryId← 正确答案
  • D.Zookeeper 统一分配序号

答案:C。BookKeeper 规定同一 Ledger 的并发写方必须先通过 fencing 机制取得写入权,拿到写入权的客户端在本地单调递增分配 EntryId,保证顺序唯一。分布式锁太重,先读后写违反追加模型,ZK 分配会成为性能瓶颈。好比只有一支笔能写账本,执笔者自己给行号。

Q8. Pulsar 中下列哪些属于与副本相关的配置项?

多选题 · easy

  • A.总副本数配置← 正确答案
  • B.写入副本数配置← 正确答案
  • C.确认副本数配置← 正确答案
  • D.保留大小配置

答案:ABC。Pulsar 用 ensembleSize(总副本数)、writeQuorum(写入副本数)、ackQuorum(确认副本数)三件套共同决定数据冗余策略,对应存几份、写几份、确认几份。retentionSize 属于消息保留策略,不参与副本仲裁。

Q9. 以下哪些场景更适合选择 Pulsar?

多选题 · mid

  • A.多租户隔离共享集群← 正确答案
  • B.消息需保留数周重放← 正确答案
  • C.单机部署追求极简运维
  • D.需要跨地域数据复制← 正确答案

答案:ABD。Pulsar 的多租户架构、分层存储长保留和跨地域复制是三大招牌能力。单机极简部署恰恰是它的短板,Kafka 单机更轻量。选型时要把“架构弹性需求”和“运维轻量需求”分开掂量。

Q10. 关于 BookKeeper 的写入路径,下列说法正确的是?

多选题 · hard

  • A.客户端将 Entry 追加到 Ledger 尾部← 正确答案
  • B.写入需满足 ack quorum 才算成功← 正确答案
  • C.所有副本必须全部落盘才算成功
  • D.写入失败的 Entry 可以随意覆盖

答案:AB。追加写和法定确认是 BookKeeper 的两大基石。C 过于严格,只要达到 ack quorum 即可向客户端报成功,不必等全部落盘;D 完全错误,Entry 一旦写入即不可变、不可覆盖,失败的写入只能重新追加新条目。这就像账本写错只能划线补记,不能撕页。