资讯动态

Pulsar Lookup机制源码拆解与Producer创建卡顿排查

发布时间:2026/10/26 2:49:18 来源:尧图企业网站定制
生产环境里 Producer 创建偶尔要卡上两三秒日志翻来覆去就是LookupException、Broker is not available这几行可telnet过去 broker 端口明明是通的——这类问题排查到最后十有八九会落到 Apache Pulsar 的 Lookup 机制上。Pulsar 的客户端在真正建立 Producer 或 Consumer 之前必须先问清楚一件事这个 Topic 此刻归哪个 Broker 管。这个一问一答的过程就是 Lookup它看起来只是两条命令来回背后却串着 Topic、NamespaceBundle、Broker 三层映射串着元数据存储、所有权抢占、负载均衡、自动创建、分区元数据、跨集群复制一大串分支。我前后花了不少时间把BinaryProtoLookupService、NamespaceService、OwnershipCache这几块源码串起来读才把 Lookup 这条链路搞明白。这篇就把这条链路从头到尾拆一遍顺便说说我在实际排障中验证过的几个判断依据适合已经能跑起 Pulsar、想搞清楚内部行为的后端和中间件方向的同学参考。1. 断点跟一次 createProducerAsyncLookup 究竟在问什么1.1 ProducerImpl.start 之后的第一条网络命令很多人以为client.newProducer().topic(...).create()一返回成功连接的就已经是真正存放消息的 Broker 了。实际并不是。在PulsarClientImpl#createProducerAsync里客户端做的事情分两步先构造一个ProducerImpl对象再把它start()起来。而start()内部会走到grabCnx()这个方法里第一件正经事就是向 Lookup 服务要地址大致长这样简化后的结构private void grabCnx() { if (client.getLookup() null) { ... } client.getLookup().getBroker(topic) .thenAccept(pair - { InetSocketAddress brokerAddress pair.getLeft(); // broker 地址 InetSocketAddress proxyAddress pair.getRight(); // proxy 地址可能为 null ... client.getConnection(brokerAddress) .thenAccept(cnx - connectionOpened(cnx)); }) .exceptionally(this::handleCreateConnectionException); }也就是说客户端此刻手上只有一个逻辑地址persistent://tenant/ns/topic它并不知道该连哪台机器。getBroker()这一层调用就是 Lookup 的入口。它在二进制协议上对应的命令是CommandLookupTopicBroker 回复CommandLookupTopicResponse。理解这一点很关键Lookup 不是在问“这个 Topic 是否存在”也不是在问“这个 Topic 有多少分区”它问的是“这个 Topic 当前的所有者是谁请给我它的服务地址以及 TLS 地址、Proxy 地址”。后面所有关于消息收发的连接都是拿到这个地址之后才建立的。所以如果你在生产环境看到 Producer 卡顿而 Broker 的 CPU、磁盘、网络都正常那问题大概率不在消息链路而在 Lookup 这一问一答上。还有一点容易忽略Lookup 用的连接和消息用的连接是同一个ClientCnx连接池里的。客户端在 Lookup 阶段连接的那台 Broker未必就是 Topic 的 Owner。这种“先连 A 问路被指向 B”的情况非常普遍尤其是新集群或者刚做过 Bundle 重新分配的时候。1.2 Topic 到 Bundle 到 Broker三级映射不是一个接口查完的从Topic到最终地址中间其实隔着一层NamespaceBundle这是 Pulsar 设计上很有意思的一点。完整链路是这样的层级实体由谁决定存储位置第一层Topic 归属哪个 Bundle对 Topic 全名做哈希落到某个区间计算得出不落盘第二层Bundle 归属哪个 Broker负载均衡器选举 Broker 抢占/namespace/tenant/ns/bundle第三层Broker 对外服务地址Broker 启动时注册/brokers/cluster/broker-id第一层不需要查任何东西。NamespaceBundleFactory#getBundles在对 Topic 做Murmur3_32Hash之后按范围落到某个 Bundle 上。默认情况下一个 Namespace 切 4 个 Bundle范围分别是0x00000000_0x3fffffff、0x40000000_0x7fffffff、0x80000000_0xbfffffff、0xc0000000_0xffffffff。所以只要你拿到 Topic 字符串本地算一下就知道了它落在哪个 Bundle——这一步是纯计算没有网络开销。真正需要查的是第二层和第三层。Broker 收到 Lookup 请求后用 Bundle 名去OwnershipCache里找 Owner命中就直接返回没命中就去元数据存储里读/namespace/...节点还没有就说明这个 Bundle 暂时没主需要触发一次“选主 抢占”的流程。三级映射串起来看你就能理解为什么一次改动 Namespace 的 Bundle 数量会影响到所有 Topic 的 Lookup 结果——Bundle 边界变了Topic 落到的 Bundle 就变了Owner 自然也换了。2. 四种 LookupType 不是历史包袱是四种不同粒度的问答2.1 Topic 与 PartitionedTopic父 Topic 的地址并不等于分区地址Pulsar 客户端在发起 Lookup 时可以在CommandLookupTopic里带一个requestType字段。协议层面目前主要用到两种值Topic和PartitionedTopic。区别看着很小实际影响很大。Topic类型的 Lookup问的是“这个名字对应的逻辑 Topic 归谁”。如果是非分区 Topic返回的地址就是这条 Topic 真正所在 Broker 的地址客户端连上去就能直接收发消息。但如果是分区 Topic情况就不一样了父 Topic 本身并不存放任何消息它只是一个逻辑容器用来聚合 N 个子分区。这时候Topic类型 Lookup 返回的地址只是“能处理这个父 Topic 名字的 Broker”客户端拿到之后还得再做事。PartitionedTopic类型则更明确地表态“我要的是分区信息”。不过在较新的 Pulsar 版本里分区元数据其实已经走了一条独立的命令CommandPartitionedTopicMetadata不再复用 Lookup。这个变化带来的实际影响是你在抓包时看到 Lookup 返回了一个地址并不意味着客户端就知道这个 Topic 有几个分区。分区数是另一条命令给出的答案。这两件事分开看排障时才不会被日志误导。2.2 Bundle 与 BundleRange给内部组件用的粗粒度查询LookupType的枚举在源码里其实有四个值Topic、PartitionedTopic、Bundle、BundleRange。后两个在 bin 协议里用得不多更多是内部组件和管理接口在使唤。它们的存在说明一个设计意图Lookup 这套查询能力本身是分层的最细可以到 Topic最粗可以到 Bundle Range。为什么需要粗粒度因为有些组件关心的是“某个范围内的一批 Topic 归谁”如果逐个 Topic 去查开销会非常大。比如 Broker 内部做所有权迁移、Admin 接口做 Bundle 级别的统计、以及跨集群场景下做拓扑检查用 Bundle 粒度更合适。理解这一点对排查问题有帮助如果你在日志里看到 Bundle 级别的 Lookup那说明触发方不是客户端而是 Broker 自身或管理面。另外值得注意的是LookupType和前面说的requestType不完全是一回事前者是 Java 枚举后者是协议字段。老版本里两者可以一一映射新版本里则出现了收敛。写代码做二次开发时建议直接看当前使用版本的CommandLookupTopic定义不要凭记忆写。2.3 LookupOptions 里那几个布尔值改变了整条链路Broker 侧处理 Lookup 的核心入口是NamespaceService#getBrokerServiceUrlAsync(TopicName, LookupOptions)其中LookupOptions携带了几个关键开关它们在源码里以布尔值的形式出现但每一个都能把链路引向完全不同的分支authoritative告诉 Broker“你给出的答案必须是权威的不能是猜测”。这个标志直接决定了 Broker 会不会强制走一次元数据查证也决定了客户端愿不愿意缓存这次结果。readOnly在早期版本里用于只读语义避免 Lookup 过程触发 Topic 创建或所有权抢占。现在主要用于某些内部查询场景。requestMetadata只想要元数据不要服务地址。分区元数据请求走这条路径时不涉及 Owner 判断。advertisedListenerName指定监听器名称让 Broker 返回对应监听器地址而不是默认地址。多网卡、内外网分离的集群里这个字段配错会直接导致客户端连不上。我踩过一次比较隐蔽的坑某集群内外网分离Broker 注册了internal和external两个监听器客户端连接时指定的监听器名称和 Broker 配置里的不一致Lookup 本身成功了返回的地址却是内网 IP。排查时看到的是连接超时而不是 Lookup 失败很容易把方向带偏到网络策略上。后来在ClientCnx的 debug 日志里看到实际连接的目标地址才定位到是监听器名称配置的问题。3. 客户端视角BinaryProtoLookupService 如何合并请求、判超时、处理重定向3.1 lookupRequests 里做的是请求合并不是结果缓存BinaryProtoLookupService类里有一个看起来很不起眼但很关键的字段protected final ConcurrentHashMapString, CompletableFuturePairInetSocketAddress, InetSocketAddress lookupRequests new ConcurrentHashMap();很多人第一眼会以为这是“Lookup 结果缓存”其实不是。它是一个在途请求表。当多个 Producer 或 Consumer 同时对同一个 Topic 发起 Lookup 时第一个请求会真正发出去后续请求会挂在这个CompletableFuture上等待同一个结果。请求完成后whenComplete里会把这个 key 移除表里不会留下任何东西。这个设计解决的是经典的“惊群”问题。想象一个服务启动时同时创建 50 个 Producer 指向同一个 Topic如果没有请求合并就是 50 次 Lookup 打到 BrokerBroker 那边要做 50 次 Bundle 查找某些情况下还要触发 50 次元数据读。合并之后只发一次其余 49 个共用结果。实测在高并发启动场景下合并带来的收益非常明显。但这也意味着一个副作用Lookup 结果本身在客户端是没有长期缓存的。每次新建 Producer/Consumer 都会重新问一次。真正的“地址缓存”发生在更上层——PulsarClientImpl维护着 producer 和 consumer 的实例表同一个 Topic 复用已有的 Producer 实例而ConnectionPool按地址缓存ClientCnx连接避免重复建连。这两层缓存才是性能的关键lookupRequests只负责削峰。3.2 Redirect、Connect、Failed三种响应背后的分支逻辑Broker 回复的CommandLookupTopicResponse里有一个response字段取值有三种Redirect、Connect、Failed。客户端在ClientCnx#handleLookup里根据不同的值走不同分支响应类型含义客户端动作Connect目标 Broker 就是 Owner直接建立到该地址的连接开始创建 ProducerRedirect目标 Broker 不是 Owner但知道谁是断开当前连接或复用连接池向新地址重新发起 LookupFailed无法给出答案抛出LookupException交给上层重试策略Redirect分支是问题高发区因为它可能连续触发。客户端拿到新地址之后会重新走一遍lookupTopic如果新地址指向的 Broker 又说“我不是 Owner”就继续跳。协议里用originalRequestId来关联整条重定向链路方便日志追踪。Pulsar 客户端没有硬编码一个很小的重定向上限主要靠operationTimeoutMs默认 30 秒兜底。所以你在日志里看到的不是“重定向次数超限”而往往是“Lookup 超时”本质是重定向在超时窗口里没收敛。还有一个细节值得说Redirect响应里可能同时带brokerServiceUrl和proxyThroughServiceUrl两个字段。后者为 true 时客户端会先连 Proxy再由 Proxy 转发给 Broker。这套机制在 Proxy 部署模式下很关键配置错了会出现“客户端直连了本该经 Proxy 的地址”的情况表现为连接成功但请求被拒。3.3 authoritative 标志决定了客户端敢不敢记住这个地址authoritative这个字段我第一次看源码时完全没在意后来才意识到它是 Lookup 里最有设计感的部分之一。Broker 返回authoritativetrue等于在说“我给的这个地址是权威的你记下来短期内不会变”。客户端收到这个标志才敢把地址写进更持久的缓存里下次同一 Topic 建 Producer 时可以直接用。反过来如果 Broker 返回authoritativefalse说明它自己也没把握——可能是刚做完 Bundle 拆分、可能是元数据还没同步、可能是它只是个转发的中间人。这种情况下客户端不应该长期记住这个地址否则自己的缓存会和新状态脱节。这个逻辑在 Bundle 被拆分时尤其重要。拆分发生后原来的 Bundle 一分为二部分 Topic 的 Owner 变了。如果客户端死抱着旧地址不放发过去的请求会被新 Owner 拒绝然后被迫重新 Lookup。所以你会看到authoritative的取值和 Bundle 的生命周期紧密绑定。实际排障时我常用一个判断如果日志里同一个 Topic 短时间反复出现 Lookup而且每次都成功那问题很可能是客户端缓存的地址一直不是权威地址authoritative一直是 false导致它每次都重新问。这种情况下要去查 Broker 侧的元数据同步和 Bundle 状态而不是在客户端找原因。4. Broker 视角NamespaceService 凭什么说“这个 Topic 归我”4.1 getBrokerServiceUrlAsync 的分支顺序与短路条件Broker 收到 Lookup 命令后ServerCnx#handleLookup会把请求转交给NamespaceService#getBrokerServiceUrlAsync。这个方法的分支顺序很关键因为顺序本身决定了行为的优先级权限校验先行。如果客户端没有对该 Namespace 或 Topic 的生产/消费权限直接返回Failed。这一步在认证授权开启的集群里是必经之路也是很多“客户端报 Lookup 失败但地址明明正确”的根因。Topic 存在性判断。如果 Topic 不存在进入自动创建判断分支如果存在继续往下走。计算 Bundle。用 Topic 全名做哈希定位到 Bundle 区间。查 Owner。先查OwnershipCache本地内存未命中查元数据存储。选主与抢占。如果 Bundle 确实没有 Owner触发候选 Broker 选举和所有权抢占。返回地址。把 Owner 的LookupData含各类 URL填进响应。理解了顺序你就能解释一个常见困惑为什么有时候 Topic 明明存在Lookup 却报 not found因为存在性判断发生在 Owner 判断之前而且判断所依据的数据源不一定和pulsar-admin topics list用的数据源完全一致。这个后面第 6 章还会展开。短路条件也值得留意。Broker 在拿到 Owner 之后不会再去验证一次 Topic 是否真的存在因为这两步用的是不同的元数据来源。如果二者不一致就会出现“Lookup 成功但建 Producer 失败”的情况错误信息通常是 Topic 相关的而不是 Lookup 相关的排查时容易走岔路。4.2 OwnershipCache 抢锁与元数据节点写入的时序OwnershipCache是整个所有权机制的核心它的实现非常精妙。类里维护着一个ConcurrentHashMapNamespaceBundle, CompletableFutureOptionalSemaphore的ownedBundlesCache每个 Bundle 对应一个信号量用来做本地并发控制。当一个 Broker 决定抢占某个 Bundle 时流程大致是// 简化后的关键步骤 private CompletableFutureOptionalSemaphore tryAcquiringOwnership(NamespaceBundle bundle) { return ownedBundlesCache.computeIfAbsent(bundle, b - { return getBundleOwnerAsync(b) .thenCompose(owner - { if (owner.isPresent()) { return CompletableFuture.completedFuture(Optional.empty()); } // 发起元数据写入CAS 抢占 return createOwnershipNode(bundle); }); }); }注意这里有一个很容易被忽略的点抢占是通过元数据存储的 CAS 操作完成的不是靠谁先连上谁赢。写入的节点内容是LookupData包含brokerUrl、brokerUrlTls、nativeUrl、httpUrl等字段。节点写入成功才算抢到这个 Bundle。这里还有一个架构层面的区分很多人分不清Pulsar 的元数据分本地存储和全局存储两套。本地存储管/managed-ledgers、/namespace这类和具体集群强相关的数据全局存储管/brokers、/admin、/clusters这类跨集群共享的数据。Broker 自身的服务地址注册在全局存储里Bundle 归属注册在本地存储里。跨集群复制的 Lookup 出问题十有八九是这两套存储里某一套的视图不一致。时序上另一个坑是所有权节点的写入是异步的而本地ownedBundlesCache的更新也是异步的。如果在这两个异步操作之间的窗口里来了一个新的 Lookup 请求可能读到“元数据说这个 Bundle 有 Owner但本地缓存里没有”的中间状态。Pulsar 通过getOwnerAsync里的回填逻辑处理了大部分情况但在高并发和元数据存储响应慢的场景下短暂的不一致仍然可能发生表现为同一个 Topic 的 Lookup 结果在两个 Broker 之间来回跳。4.3 负载均衡器选出候选 Broker 时看的到底是什么Bundle 没有 Owner 时需要选一个候选 Broker 来接管。这个决策交给负载均衡器。Pulsar 里有两套实现早期的SimpleLoadManagerImpl和后期的ModularLoadManagerImpl默认是后者。无论哪套最终都会走LoadManager#findBrokerForAssignment这一层抽象。ModularLoadManagerImpl的决策依据主要有三块各 Broker 的负载报告。每个 Broker 定期上报自己的资源使用情况包括 CPU、内存、网络、消息吞吐量等汇总到全局元数据里。资源分配策略。SimpleResourceAllocationPolicies允许按 Namespace 做过滤比如某些 Namespace 只能落到特定 Broker 组上。这就是“隔离部署”的实现基础。Bundle 历史分布。为了避免一个 Broker 突然吃进太多 Bundle策略上会做一定的均衡。这里有个很有趣的设计候选 Broker 选出来之后它自己未必愿意接管。如果它认为自己负载过高或者此时不适合接管比如正在关闭会拒绝这次所有权。这就形成了一次“选举——拒绝——再选举”的循环而这个循环的每一次都在 Lookup 的调用栈里完成。所以如果你看到 Lookup 的 P99 延迟偏高但 P50 正常很可能就是撞上了这种循环。还有一个我实测过的细节负载均衡器在选 Broker 时读到的数据可能有一定滞后因为负载报告是周期性上报的。这意味着集群刚扩完容时新 Broker 可能短时间内接不到任何 Bundle因为其他 Broker 上还存着“旧”的负载视图。这时候手动pulsar-admin namespaces unload一下对应 Namespace反而比等它自动均衡更快因为强制卸载会让 Bundle 重新进入待分配状态。5. 自动创建、分区与跨集群三条把 Lookup 变复杂的岔路5.1 autoTopicCreation 打开时Topic not found 反而变多了默认情况下Pulsar 是允许自动创建 Topic 的但这个“允许”有多个层级每个层级都能覆盖上一层层级配置项作用范围全局allowAutoTopicCreation整个 Broker命名空间autoTopicCreationOverride单个 Namespace客户端ProducerBuilder#autoUpdatePartitions等单次创建请求autoTopicCreationOverride可以显式设成true、false或者不设置跟随全局。这套覆盖机制本意是给不同团队不同粒度的控制权实际使用中很容易出现层次混乱某个 Namespace 显式关掉了自动创建但代码是从别的 Namespace 抄过来的没改客户端参数结果就是 Lookup 时报 Topic not found而开发同学觉得“明明配置里是开启的”。更隐蔽的一种情况是自动创建确实被触发但创建过程失败了。失败原因可能是 Topic 名字不合法比如包含了不允许的字符、长度超限、可能是 Namespace 不存在、可能是被 Topic 级别的策略拦下来了。这些失败在 Lookup 的响应里都表现为Failed错误信息模糊。我在排查这类问题时一个有效的习惯是先把 Broker 日志里对应时间点的 WARN 级别以上日志全部抓出来往往能直接看到Failed to create topic这类记录比盯着客户端错误码猜要快得多。还有一点必须提醒自动创建是一个“副作用”它会让 Lookup 从纯读操作变成可能写元数据的操作。在高并发场景下大量客户端同时对一个不存在的 Topic 发起 Lookup会同时触发自动创建形成竞争。Pulsar 内部做了去重但如果你看到某个时刻 Broker 的元数据写入量突然飙升同时有一批 Lookup 变慢那基本就是这个原因。5.2 分区 Topic 的 N 次 Lookup 与一次 PartitionedMetadata 是两码事分区 Topic 是 Lookup 链路里最容易让人误解的地方。假设客户端要创建一个指向persistent://tenant/ns/orders的 Producer而这个 Topic 有 6 个分区实际发生的事是客户端先发一次CommandPartitionedTopicMetadata拿到分区数6。客户端把逻辑 Topic 名字改写成orders-partition-0到orders-partition-5。对每个分区名各发一次 Lookup。拿到 6 个地址可能落在不同 Broker 上分别建立连接。也就是说一个 6 分区的 Topic在创建 Producer 时会触发 6 次独立的 Lookup而不是一次。这解释了为什么分区数多的 Topic 在客户端启动阶段更慢。这也解释了为什么有些集群配置了很高的分区数却在客户端侧频繁出现连接超时——连接池和 Lookup 吞吐都成了瓶颈。排障时有个实用命令可以确认分区状态# 确认分区数与各分区的具体归属 pulsar-admin topics partitioned-stats persistent://tenant/ns/orders # 单独查某个分区的 Lookup 结果 pulsar-admin topics lookup persistent://tenant/ns/orders-partition-0lookup子命令背后走的就是本文讨论的这套机制它返回的地址和客户端拿到的地址是一致的。当你怀疑客户端拿到错误地址时用这条命令对照一下是最快的验证手段。另一个容易踩的坑是分区扩容。给已有 Topic 增加分区数之后新分区的名字是新的客户端需要重新做一次PartitionedMetadata才知道分区变多了。如果客户端没有开启autoUpdatePartitions它会一直按旧的分区数工作新分区上的消息就不会被消费到。这个现象表现出来是“消费延迟莫名其妙”而不是报错很难第一时间联想到 Lookup。5.3 跨集群复制场景下Lookup 先落本地还是先落远端开启跨集群复制后同一个逻辑 Topic 在多个集群里各有一份数据每个集群各有自己的 Owner。这时候 Lookup 的行为需要区分调用方是谁。如果是客户端连在集群 A那 Lookup 只会在集群 A 内部解析返回集群 A 里的 Owner 地址。这在逻辑上是自洽的客户端只和本地集群交互。但如果是集群 A 上的复制组件要把消息同步到集群 B它会在集群 B 上建一个 Producer这时候触发的是集群 B 的 Lookup。集群 B 的 Broker 列表来自全局元数据存储所以即使这两个集群之间的本地元数据互不相通复制链路依然能找到目标地址。这里的关键是跨集群场景下Lookup 用的是哪套元数据存储决定了它能看见哪些 Broker。如果全局存储的/clusters下缺少目标集群的信息或者目标集群的 Broker 没有正确注册到全局存储里Lookup 就会直接失败。这类失败的典型表现是Failed to look up topic加上Cluster not found之类的信息。还有一个实践中的细节跨集群复制是异步的延迟链路很长。我在实际排查中发现复制链路的 Lookup 失败往往不会立即暴露而是表现为“某个跨集群同步任务卡住不动”。这时候要查的是复制组件所在 Broker 的日志而不是源集群的客户端日志因为发起 Lookup 的是复制组件而不是用户客户端。6. 四类 Lookup 异常的排查链路以及我验证过的判断依据6.1 反复 re-lookup先看 Bundle 有没有被拆分或卸载现象很典型客户端日志里同一个 Topic 的 Lookup 请求每隔几秒就出现一次每次都能成功但过一会儿又来一轮。这种“反复 re-lookup”的根因绝大多数时候在 Bundle 的生命周期上。判断路径是这样的先确认这段时间有没有人执行过pulsar-admin namespaces split-bundle或者pulsar-admin namespaces unload。这两个操作都会导致 Bundle 的 Owner 变化客户端的缓存地址失效。如果没人手动操作查 Broker 的负载均衡日志。自动拆分splitBundleIfNeeded在 Bundle 负载过高时会自动触发触发时同样会导致地址变化。这类日志通常带有Splitting bundle或Unloading bundle字样。再看 Broker 有没有发生重启、扩容或缩容。Bundle 在 Broker 下线时会被重新分配这个过程的波峰很容易和客户端重连的波峰撞在一起。排查这类问题时我建议用pulsar-admin namespaces bundles tenant/namespace看一下当前的 Bundle 列表和归属。如果发现某个 Namespace 的 Bundle 数比预期多很多比如默认 4 个变成了 30 多个说明历史上发生过多次自动拆分客户端频繁 re-lookup 就有了合理的解释。长期方案是调整defaultNumberOfNamespaceBundles或者关掉自动拆分改成按需手动拆。6.2 管理端能列出 Topic客户端却报 not found这个场景非常经典也很容易把人带偏。pulsar-admin topics list能列出 Topic说明元数据里有这条 Topic 的记录客户端 Lookup 报 not found说明存在性判断没通过。两者用的是不同的数据视图所以完全可能矛盾。我的排查顺序是第一步用pulsar-admin topics lookup在管理端做一次 Lookup看看是不是也失败。如果管理端失败而 list 成功那就基本锁定是数据源不一致而不是客户端问题。第二步确认自动创建的状态。有些集群为了安全会关掉自动创建这时候如果 Topic 是“半创建”状态——元数据节点写了但managed-ledgers里的实际记录没写完整——就会出现 list 能看到但 lookup 失败的情况。第三步检查 Namespace 是否处于unloading状态。Namespace 卸载过程中Topic 的所有权会被暂时回收窗口期内 Lookup 会返回 not found 或者重定向到错误地址。这个窗口通常只有几百毫秒但在批量卸载场景下能持续好几分钟。第四步如果以上都正常就去看 Broker 的BrokerService#getTopic相关日志。Topic 的加载是有成本的尤其是 Ledger 数量多的 Topic首次加载可能耗时较长。如果 Lookup 的超时时间设得比较短而 Topic 加载又超过了这个时间就会出现“存在但查不到”的假象。6.3 Lookup 超时但端口连通把元数据存储会话和线程池排进去端口通、Lookup 超时这个组合最容易让人误判。TCP 层通只说明网络可达不代表 Lookup 链路能走完。我遇到过几次这个组合根因分别落在三个不同的地方元数据存储会话异常。Broker 的所有权判断强依赖元数据存储。如果 Broker 和存储之间的会话出问题读操作会挂住直到超时。表现出来就是 Lookup 一直不返回客户端 30 秒后抛超时。这种问题的特征是“同一时刻所有 Broker 的 Lookup 都慢”而不是某个 Topic 慢。Broker 的请求处理线程池打满。Lookup 是由 Broker 的ServerCnx所在线程池处理的。如果这个池子被慢请求占满新的 Lookup 请求会排队。判断方法很简单看 Broker 的线程池队列长度和拒绝数如果拒绝数在涨那就是它。这种情况下的典型特征是延迟分布很不均匀P99

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑