Apache Pulsar 技术系列 - PulsarClient 实现解析

导语

Apache Pulsar 是一个多租户、高性能的服务间消息传输解决方案,支持多租户、低延时、读写分离、跨地域复制(GEO replication)、快速扩容、灵活容错等特性。同时为了达到高性能,低延时、高可用,Pulsar 在客户端也做了很多的优化,本文主要讲述 PulsarClient 基本原理和实现。

PulsarClient 简介

Pulsar 客户端 API 设计优雅简洁,使用 PulsarClient 作为客户端的总入口,方便用户记忆和构建出具体的客户端,例如:

  • Producer: 生产者用来发送消息到指定 Topic。

  • Consumer: 消费者通过订阅关联到指定 Topic 并接收消息。

  • Reader: 手动管理 Cursors 的消费者。(内部使用 Consumer 实现)。

PulsarClient 还统一管理客户端系统资源,为具体的客户端提供了部分通用化处理,包括连接管理、线程管理、内存管理等。接下来让我们了解一下 PulsarClient 是如何实现的。

PulsarClient 有哪些功能

作为客户端的统一入口,下面代码片段不难看出 PulsarClient 主要功能是构建、销毁 PulsarClient 实例,以及构建各种具体 Client 和事务实例。

public interface PulsarClient extends Closeable {
    ProducerBuilder<byte[]> newProducer();
    <T> ProducerBuilder<T> newProducer(Schema<T> schema);
    ConsumerBuilder<byte[]> newConsumer();
    <T> ConsumerBuilder<T> newConsumer(Schema<T> schema);
    ReaderBuilder<byte[]> newReader();
    <T> ReaderBuilder<T> newReader(Schema<T> schema);
    void updateServiceUrl(String serviceUrl) throws PulsarClientException;
    CompletableFuture<List<String>> getPartitionsForTopic(String topic);
    CompletableFuture<Void> closeAsync();
    void shutdown() throws PulsarClientException;
    boolean isClosed();
    TransactionBuilder newTransaction() throws PulsarClientException;
}

实现原理

初始化过程

PulsarClient 可以使用以下代码来实例化。

PulsarClient client = PulsarClient.builder().serviceUrl("pulsar://broker:6650").build();

PulsarClient 以及具体客户端都使用 Builder 模式构建,每种客户端都有对应的 ConfigurationData 来管理配置,PulsarClient 核心配置如下:

public class ClientConfigurationData implements Serializable, Cloneable {
    private String serviceUrl;
   // 用来在运行时外部改变url
    private transient ServiceUrlProvider serviceUrlProvider;
    private long operationTimeoutMs = 30000;
    private long statsIntervalSeconds = 60;
    private int numIoThreads = 1;
    private int numListenerThreads = 1;
    private int connectionsPerBroker = 1;
    private boolean useTcpNoDelay = true;
    private int concurrentLookupRequest = 5000;
    private int maxLookupRequest = 50000;
    private int maxLookupRedirects = 20;
    private int maxNumberOfRejectedRequestPerConnection = 50;
    private int keepAliveIntervalSeconds = 30;
    private int connectionTimeoutMs = 10000;
    private int requestTimeoutMs = 60000;
    private long initialBackoffIntervalNanos = TimeUnit.MILLISECONDS.toNanos(100);
    private long maxBackoffIntervalNanos = TimeUnit.SECONDS.toNanos(60);
    private boolean enableBusyWait = false;
    private String listenerName;
   // 全局内存限制(producer使用)
    private long memoryLimitBytes = 0;
    private String proxyServiceUrl;
    private ProxyProtocol proxyProtocol;
    long tickDuration = 1;
    // transaction
    private boolean enableTransaction = false;
}

PulsarClient 的初始化过程比较简单,逐个初始化内部模块,以下代码片段展示了 Client 内部主要的模块。

public class PulsarClientImpl implements PulsarClient {
    // 配置
    protected final ClientConfigurationData conf;
   // 本地元数据管理器,主要负责topic分区个数、topic对应的owner节点以及schema信息
    private LookupService lookup;
   // 共享连接池 双层map结构
    private final ConnectionPool cnxPool;
   // 时间轮
    private final Timer timer;
   // 执行外部逻辑线程组(主要消费使用)
    private final ExecutorProvider externalExecutorProvider;
   // 执行内部逻辑线程组(主要消费使用)
    private final ExecutorProvider internalExecutorService;
    private final AtomicReference<State> state = new AtomicReference<>();
   //producer集合
    private final Set<ProducerBase<?>> producers;
   //consumer集合
    private final Set<ConsumerBase<?>> consumers;
   //producer自增Id
    private final AtomicLong producerIdGenerator = new AtomicLong();
   //consumer自增Id
    private final AtomicLong consumerIdGenerator = new AtomicLong();
   // 请求自增Id
    private final AtomicLong requestIdGenerator = new AtomicLong();
   // netty 线程组
    protected final EventLoopGroup eventLoopGroup;
   // 生产本地buffer内存限制器
    private final MemoryLimitController memoryLimitController;
  ...
}

PulsarClient 初始化时主要创建了 Netty 客户端,连接池、时间轮等对象,只是准备好资源,并没有和服务端建立连接进行任何交互。只有在创建具体的客户端时,才会和服务端有交互。

Producer 创建

Pulsar 是以 Topic 粒度对外提供服务,多分区 Topic 等同于多个不同数字后缀的 Topic 集合。下文提到的 Topic-Partition 包含了单分区 Topic 和多分区 Topic 中的一个  Partition。Pulsar 客户端的实现 Topic-Partition 之间是相互独立的,SDK 内部会为每个 Topic-Partition 单独创建一个具体的客户端。我们在这里只介绍 Producer 的初始化流程(Consumer 类似)。

可以用以下代码构建 Producer。

Producer<byte[]> producer = client.newProducer().topic("my-topic").create();

当 My-topic 为 Non-partitioned Topic,会实例化一个 ProducerImpl 对象并返回,当 My-topic 分区数量大于0时,则会创建 PartitionedProducerImpl 对象。PartitionedProducerImpl 对象内包含了 List。可以理解为 PulsarClient 创建 Producer 时,最终会创建和分区数量一致的 ProducerImpl 对象,每个 ProducerImpl 都独立工作,互不影响(Consumer 类似)。

在创建 Producer 时客户端与服务端命令字交互如下:

  1. PulsarClient 通过用户指定的 ServiceUrl 挑选一个 url 来连接服务端,并做认证相关操作。

  2. 使用 LookupService 发送 PARTITIONED_METADATA 命令字查询给定 Topic 的分区数。

  3. 根据 Metadata 返回结果中的分区数循环创建 ProducerImpl 对象。

    3.1 ProducerImpl 对象初始化时会使用 LookupService 发送 LOOKUP 请求查询对应的分区的 Owner 节点 Lookup 过程可参考https://km.woa.com/articles/show/555638。

    3.2 根据 LOOKUP 响应连接到 Owner 节点,并发送 PRODUCER 请求向服务端创建 Producer。

    到这里 Producer 就已经创建完毕,可以正式使用来发送消息了。

ps: 如果创建好 Producer 后,分区数量有变化了,比如服务端扩容了,客户端可以感知到并增加 ProducerImpl 对象数量吗。答案是可以的,默认会定时1分钟发起一次检测,有分区变化会做相应处理。

连接管理

与大部分组件一样,客户端和服务端使用长连接通信。Pulsar 协议设计上不是传统的应答模式,可以同时支持多个客户端使用同一个连接并行发送接收请求(服务端会串行处理单个 Topic-partition 上的请求来保证消息顺序性)。

得益于连接共享,客户端消耗的连接数是很少的,PulsarClient 会为每台 Broker 创建一个连接池,默认连接数为1, 用户可以使用 ConnectionsPerBroker 配置来设置每台 Broker 最大连接数。ProducerImpl、ConsumerImpl 在初始化时,会随机从连接池中获取一个连接用来和服务端通信。

下图中 maxConnectionsPerHosts=2, 连接池中为每个 Broker 创建2个连接,6个客户端会在对应 T opic owner 节点里随机挑选一个连接绑定。

连接健康管理

Pulsar keepAlive 检测是双向的,连接创建成功后,客户端和服务端都会定时30s(KeepAliveIntervalSeconds 配置可修改)发送 Ping 请求到对端,接收到 Ping 请求后会回应 Pong 来标识存活。在以下几种情况下,客户端、服务端都会主动断开连接:

  • 超时时间内没有完成握手动作。

  • 发送 Ping 或者 Pong 命令时,Netty 回调发送失败。

  • 连接 isAutoRead 打开并且超时时间内没有收到任何请求(包含 Ping、Pong)。

连接断开后,会通知绑定在该连接上的所有客户端,这些客户端会重新从连接池中获取健康的连接。Pulsar 中空闲连接不会自动回收。

线程模型

PulsarClient 使用 Netty 作为网络通信框架, 是标准的 Netty 客户端。协议处理和事件驱动都是依托于 Netty。核心处理类直接继承于 Netty Handler。

所以线程模型也主要围绕于 Netty 的 EventLoopGroup。上文提到,客户端资源管理都收敛于 PulsarClient,也就是使用同一个 PulsarClient 创建出来的具体客户端都共享该 PulsarClient 中的线程等资源,比如使用 ClientA 对象分别创建一个或多个 Producer、Consuemer、Reader 客户端,这些客户端都共享 Client 中的线程资源。

PulsarClient 线程、线程组如下:

图中实线表示客户端会从线程池中挑选一个线程绑定运行。

  • Pulsar-client-io: io 线程( Netty 内部线程),负责网络连接和读写。NumIoThreads 参数配置,默认值为1。客户端不直接绑定 IO 线程,而是由其内部的连接来绑定 IO 线程,所以 IO 线程数配置最好小于或者等于总连接数,否则有些线程不会使用到。

  • Pulsar-client-internal: 主要用于 Consumer 内部处理,比如接收到消息后放置到接收队列等。也是通过 NumIoThreads 参数配置,默认值为1。

  • Pulsar-external-listener: 主要用于 Consumer 外部处理,比如用户消费逻辑回调。NumListenerThreads 参数配置,默认值为1。

  • Pulsar-timer: 时间轮内部线程,负责所有定时操作,比如连接重连,发送超时检测等。一个 PulsarClient 对应一个线程。

简单描述一下生产消费时线程是如何交互:

  • 生产: 用户线程创建消息并放置到本地缓存,IO 线程负责把消息发送到服务端。

  • 消费: IO 线程接收到服务端的消息推送,使用  Pulsar-client-internal 线程把消息放在本地缓存队列,然后使用 Pulsar-external-listener 线程执行用户消息处理逻辑。

总结和思考

本文介绍了 Pulsar 整体客户端架构,讲解了 PulsarClient、Producer 初始化过程以及客户端的连接管理和线程模型。并没有涉及到详细的生产消费过程。大家不难发现 Pulsar 客户端和其他组件客户端相比,较大的区别就是会给每个 Topic-partition 创建  Producer/consumer。如果客户端关联的 Topic-partition 数量很大,Producer/consumer 数量会急剧膨胀,从而导致客户端需要消耗更多的资源。也正是因为  Producer/consumer 数量可能较大,连接和线程等资源不可能做到独立,只能是 Producer/consumer 共享。而资源共享就不可避免出现客户端之间会相互影响,比如限流是控制在连接维度,但是由于连接是共享的,某些 Topic 的限流就会影响到该连接上的全部客户端。建议用户客户端关联的 Topic-partition 数量较大时,可以适当调大连接池和线程池大小来缓解影响,或者使用不同的 PulsarClient 来做客户端隔离。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/261510.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

STM32F4的DHT11初始化与实例分析

STM32—— DHT11 本文主要涉及STM32F4 的DHT11的使用以及相关时序的介绍&#xff0c;最后有工程下载地址。 文章目录 STM32—— DHT11一、 DHT11的介绍1.1 DHT11的经典电路 二、DHT11的通信2.1 DHT11的传输数据格式2.2 DHT11 通信分步解析 三、 DHT11 代码3.1 引脚图3.2 电路图…

Springboot数据加密篇

一、密码加密 1.1Hash算法(MD5/SHA-512等) 哈希算法&#xff0c;又称摘要算法&#xff08;Digest&#xff09;&#xff0c;是一种将任意长度的输入通过散列函数变换成固定长度的输出的单向密码体制。这种映射的规则就是哈希算法&#xff0c;而通过原始数据映射之后得到的二进制…

PMP项目管理 - 整合管理

系列文章目录 系统架构设计 PMP项目管理 - 质量管理 PMP项目管理 - 采购管理 PMP项目管理 - 资源管理 PMP项目管理 - 风险管理 PMP项目管理 - 沟通管理 现在的一切都是为将来的梦想编织翅膀&#xff0c;让梦想在现实中展翅高飞。 Now everything is for the future of dream…

Amazon Toolkit — CodeWhisperer 使用

tFragment--> 官网&#xff1a;https://aws.amazon.com/cn/codewhisperer/?trkcndc-detail 最近学习了亚马逊云科技的 代码工具&#xff0c;感慨颇多。下面是安装 和使用的分享。 CodeWhisperer&#xff0c;亚马逊推出的实时 AI 编程助手&#xff0c;是一项基于机器学习…

C语言struct,union内存对齐

测试环境&#xff1a; #include<stdio.h> int main(){//1字节对齐struct XXX{unsigned char ch;unsigned int in;unsigned short si;}__attribute__((packed));struct XXX xxx;printf("%zd\n",sizeof(xxx));//7#pragma pack(1)struct YYY{unsigned char ch;u…

文心一言 VS 讯飞星火 VS chatgpt (161)-- 算法导论13.1 1题

一、用go语言&#xff0c;按照图 13-1(a)的方式&#xff0c;画出在关键字集合(1&#xff0c;2&#xff0c;…&#xff0c;15)上高度为 3 的完全二叉搜索树。以三种不同方式向图中加入 NIL 叶结点并对各结点着色&#xff0c;使所得的红黑树的黑高分别为2、3和4。 文心一言&…

Eigen库之Quaterniond

Eigen::Quaterniond 是 Eigen C++ 库中用于表示四元数的类,四元数在计算机图形学、机器人学等领域中广泛用于表示旋转操作。 四元数是一种数学结构,通常用于表示和计算三维空间中的旋转。一个四元数由一个实部和三个虚部组成,可以写成如下形式: 主要性质和运算: 以下是如何…

R语言【cli】——cli_warn可以更便捷的在控制台输出警告信息

Package cli version 3.6.2 cli_warn(message, ..., .envir parent.frame()) 参数【message】&#xff1a;它是通过调用 cli_bullets() 进行格式化的。进一步地&#xff0c;还需要调用 inline-makeup&#xff08;内联标记&#xff09;。 参数【...】&#xff1a;传递给 rlan…

Lamda表达式和匿名内部类

文章目录 前言一、匿名内部类是什么&#xff1f;1.实现接口和抽象类(普通类也可以&#xff09;2.可实现接口或继承抽象类&#xff1a;3. 访问外部变量&#xff1a; 二、Lambda表达式1.语法三.区别&#xff1a; 总结 前言 匿名内部类和抽象类总是往&#xff0c;现在来写一篇文章…

如何用DevEcoStudio创建本地模拟器

第一步&#xff1a;打开devEcoStudio 工具 第二步&#xff1a;点击IDE 右上角previewer - Device Manager 第三步&#xff1a;选择Local Emulator 的tab &#xff0c;如果要创建的设备类型为Phone 选中即可 第四步&#xff1a;点击界面的右下角 New Emulator 第五步&#xff…

ansible的脚本---playbook剧本

ansible的脚本---playbook剧本 playbook组成部分 1、tasks任务&#xff1a;包含要在目标主机上执行的操作&#xff0c;使用模块定义这些操作&#xff0c;每个任务都是一个模块的调用 2、varlables变量&#xff1a;存储和传递数据&#xff0c;变量可以自定义&#xff0c;可以…

Android 大版本升级变更截图方法总结

Android 大版本升级变更截图方法总结 一、Android R (11) 平台二、Android S (12) 平台三、Android U (14) 平台 Android 原生的截屏功能是集成在 SystemUI 中&#xff0c;因此我们普通应用想要获取截图方法&#xff0c;就需要研读下 SystemUI 截屏部分的功能实现。 一、Androi…

Java--包,访问修饰符,多态数组,==和equals,hashcode,toString

包 同一个包里面不能有重复的类&#xff0c;不同的包可以有相同的类&#xff0c;包和包之间互不干涉。一个包下面有很多的类。 包的命名规则&#xff1a; 只能包含数字&#xff0c;字母&#xff0c;下划线&#xff0c;小圆点&#xff0c;但不能用数字开头&#xff0c;不能是关…

Databend 开源社区上榜 2023 年度 OSCHINA 优秀开源技术团队

2023 年 12 月 8 日&#xff0c;OSCHINA 对其平台上众多认证的官方技术团队和开源社区进行了全面评估&#xff0c;并颁发了“ 2023 年度优秀开源技术团队”奖项&#xff0c;以表彰各团队在推动中国开源生态系统发展方面所展现的创新能力和显著贡献。 在这一评选中&#xff0c;…

【开源软件】2022年最佳开源软件-排名第一:AlmaLinux

自我介绍 做一个简单介绍&#xff0c;酒架年近48 &#xff0c;有20多年IT工作经历&#xff0c;目前在一家500强做企业架构&#xff0e;因为工作需要&#xff0c;另外也因为兴趣涉猎比较广&#xff0c;为了自己学习建立了三个博客&#xff0c;分别是【全球IT瞭望】&#xff0c;【…

网络编程 DAY6 作业

1.使用数据库创建一个工人信息库&#xff0c;包含工号&#xff08;主键&#xff09;、姓名、年龄、薪资 2.添加三条工人信息&#xff08;可以完整信息&#xff0c;也可以非完整信息&#xff09; 3.修改某一个人的薪资&#xff08;确定的一个&#xff09; 4.展示出工资在10000到…

C++ Qt开发:StringListModel字符串列表映射组件

Qt 是一个跨平台C图形界面开发库&#xff0c;利用Qt可以快速开发跨平台窗体应用程序&#xff0c;在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置&#xff0c;实现图形化开发极大的方便了开发效率&#xff0c;本章将重点介绍QStringListModel字符串映射组件的常用方法及…

智慧工地一体化云平台APP源码:监管端、工地端、危大工程、智慧大屏、物联网、塔机、吊钩、升降机

智慧工地管理平台是依托物联网、互联网建立的大数据管理平台&#xff0c;是一种全新的管理模式&#xff0c;能够实现劳务管理、安全施工、绿色施工的智能化和互联网化。 智慧工地源码技术说明 1.微服务架构JavaSpring Cloud UniApp MySql 2.支持多端展示&#xff08;PC端、手…

istio 认证:对等身份认证+服务请求认证

istio 中有两种不同的身份认证方式&#xff1a; 基于 mTLS 的对等身份认证 PeerAuthentication基于 JWT&#xff08;JSON Web Token&#xff09;令牌的服务请求认证 RequestAuthentication 对等身份认证 PeerAuthentication 概念 提供服务到服务的认证服务网格的主要场景就…

循环神经网络中的梯度消失或梯度爆炸问题产生原因分析

循环神经网络中&#xff0c;通过时间反向传播&#xff08;backpropagation through time&#xff0c;BPTT&#xff09;实际上是循环神经网络中反向传播技术的一个特定应用。 &#xff08;1&#xff09;它要求我们将循环神经网络的计算图以此展开一个时间步&#xff0c;以获得模…