消费客户端的SDK(上)
从实现来看,消费相关功能包括消费模型、分区消费模式、消费分组(订阅)、消费确认、消费失败处理五个部分。
这里只涉及到前两个部分。
消费模型的选择为了满足不同场景的业务需求,从实现机制上来看,主流消息队列一般支持 Pull、Push、Pop 三种消费模型。
Pull 模型Pull(拉)模型是指客户端通过不断轮询的方式向服务端拉取数据。它是消息队列中使用最广泛和最基本的模型,主流的消息队列都支持这个模型。
它的好处是客户端根据自身的处理速度去拉取数据,不会对客户端和服务端造成额外的风险和负载压力。缺点是可能会出现大量无效返回的 Pull 调用(服务端没有数据可以拉去时),另外消费及时性不够。
为了提高性能,Pull是可以指定一次拉去多少条数据,然后传递给服务端,即批量拉取。
如上图,如果Topic1的数据已经被消费完,但是客户端还是不断的发请求拉数据,那么就会导致资源的浪费。
为了解决这个问题,一般服务端会协助处理,有如下两个思路:
1. 服务端 hold 住请求当客户端根据策略拉取数据时,如果没有足够的数据,就先在服务端等一段时间,等有数据后一起返回给客户端。
好处是可以尽可能提高 ...
生产者的SDK需要哪些设计
生产模块包含客户端基础功能和生产相关功能两部分,如下图:
基础功能是蓝色部分,生产功能是黄色部分。
客户端基础功能连接管理客户端和服务端之间基本都是创建 TCP 长连接进行通信的,为了避免连接数膨胀,每个客户端实例和每台 Broker 只会维护一条 TCP 连接。
分为两种:
初始化创建连接,指在实例初始化时就创建到各个 Broker 的 TCP 连接,等待数据发送。这种可能会导致连接空跑,会消耗一定的资源。
使用时创建链接,指在实例初始化时不建立连接,当需要发送数据时再建立。可能出现连接冷启动,会增加一点本次请求的耗时。
心跳检测消息队列一般都是基于 TCP 协议通信的,所以客户端和服务端之间的心跳检测机制的实现,一般有基于 TCP 的 KeepAlive 保活机制和应用层主动探测两种形式。
基于 TCP 的 KeepAlive 保活机制是 TCP/IP 协议层内置的功能,需要手动打开,优点是简单,缺点是需要Server主动发出检测包,当客户端出现故障而Server没有发包时,可能会出现不可用的TCP连接占用服务器资源。
应用层主动探测一般是 Client 向 Ser ...
消息队列如何保证存储的高性能
存储模块的优化主要是基于以下四点:
内存读写的效率高于硬盘读写
批量读写的效率高于单条读写
顺序读写的效率高于随机读写
数据复制次数越多,效率越低
提升写入操作的性能数据需要先写入内存,然后才会落盘,所以写入操作的性能优化就要从内存和磁盘入手。写入性能的提高主要有缓存写、批量写、顺序写三个思路。
1. 缓存写和批量写物理硬件的写入速度如下图:
所以,写入优化的主要思路之一是:将数据写入到速度更快的内存中,等积攒了一批数据,再批量刷到硬盘中。
平时可以看到的一种说法,数据先写入 PageCache,再批量刷到硬盘,就是这种思路。PageCache 指操作系统的页缓存,简单理解就是内存,通过缓存读写数据可以避免直接对硬盘进行操作,从而提高性能。
把缓存数据刷回到硬盘,一般有“按照空间占用比例”、“时间周期扫描”和“手动强制刷新”三种策略。操作系统内核提供了前两种处理策略,不需要应用程序感知。
按空间占用比例刷新是指当系统内存中的“脏”数据大于某个阈值时会将数据刷新到硬盘。操作系统提供了两个配置项:
“脏”数据在内存中的占比(dirty_background_ratio)
“脏”数 ...
消息队列存储之功能实现
存储模块的主流程是数据的写入、存储、读取、过期,因为消息队列本质是做一个缓冲,它的持久化在一定时间或者数据被消费后需要删除。
消息队列中的数据一般分为元数据和消息数据。元数据是指 Topic、Group、User、ACL、Config 等集群维度的资源数据信息,消息数据指客户端写入的用户的业务数据。
元数据信息的存储元数据信息的特点是数据量比较小,不会经常读写,但是需要保证数据的强一致和高可靠,不允许出现数据的丢失。同时,元数据信息一般需要通知到所有的 Broker 节点,Broker 会根据元数据信息执行具体的逻辑。比如创建 Topic 并生成元数据后,就需要通知对应的 Broker 执行创建分区、创建目录等操作。
所以元数据信息的存储,一般有两个思路:
基于第三方组件来实现元数据的存储。
在集群内部实现元数据的存储。
基于第三方组件来实现元数据的存储是目前业界的主流选择。比如 Kakfa 和 Pulsar 的元数据存储在 ZooKeeper 中,RocketMQ 存储在 NameServer 中。
优点是集成方便,而且第三方软件已经保证了一致性,高性能等需求,可以降低开发 ...
消息队列中如何设计高性能的网络模块
消息队列是需要满足高吞吐、高可靠、低延时,并支持多语言访问的基础软件,网络模块最需要解决的是性能、稳定性、开发成本三个问题。
网络模块的性能瓶颈分析消息队列的访问链路图如下:
对于单个请求来说,请求流程是:客户端构建请求,发送给服务端,服务端收到后交由业务线程处理,业务线程处理完后返回给客户端。
该流程性能消耗有三个点:
编解码的速度。即序列化与反序列化的速度。
网络延迟。这点几乎无法优化,与网络传输有关。
服务端 / 客户端网络模块的处理速度。发送 / 接收请求包后,包是否能及时被处理。
对于并发请求来说,在单个请求维度的问题的基础上,还需要处理高并发、高 QPS、高流量等场景带来的性能问题。主要包含三个方面。
高效的连接管理:当客户端和服务端之间的 TCP 连接数很多,如何高效处理、管理连接。
快速处理高并发请求:当客户端和服务端之间的 QPS 很高,如何快速处理(接收、返回)请求。
大流量场景:当客户端和服务端之间的流量很高,如何快速吞吐(读、写)数据。
大流量场景分为两类,单个请求包大,但是并发小,单个请求包小,但是并发大。
第一种的瓶颈主要在于 ...
主流消息队列的网络模型
Kafka 网络模型Kafka 的网络层没有用 Netty 作为底层的通信库,而是直接采用 Java NIO 实现网络通信。在网络模型中,也是参照 Reactor 多线程模型,采用多线程、多 Selector 的设计。
Processor 线程和 Handler 线程之间通过 RequestChannel 传递数据,RequestChannel 中包含一个 RequestQueue 队列和多个 ResponseQueues 队列。每个 Processor 线程对应一个 ResponseQueue。
具体流程上:
一个 Acceptor 接收客户端建立连接的请求,创建 Socket 连接并分配给 Processor 处理。
Processor 线程把读取到的请求存入 RequestQueue 中,Handler 线程从 RequestQueue 队列中取出请求进行处理。
Handler 线程处理请求产生的响应,会存放到 Processor 对应的 ResponseQueue 中,Processor 线程从其对应的 ResponseQueue 中取出响应信息,并返回给客户端。
...
并发场景下的幂等问题--分布式锁详解
业务流程:
1)用户选择实人认证后会在服务端初始化一条记录;2)用户在钉钉移动端按照指示完成人脸比对;3)比对完成后访问服务端修改数据库状态。
问题现象:数据库一个人有两条认证记录。
原因:并发导致了不幂等。
如果依赖的组件天然幂等,比如说数据库唯一键的约束,那么不需要做太多的处理,否则,可以采用以下方法来保证幂等。
分布式锁如何实现一个分布式锁?
方案一分布式系统中常见有两个问题:
1)单点故障问题,即当持有锁的应用发生单点故障时,锁将被长期无效占有;
2)网络超时问题,即当客户端发生网络超时但实际上锁成功时,我们无法再次正确的获取锁。
要解决问题1,一个简单的方案是引入过期时间(lease time),对锁的持有将是有时效的,当应用发生单点故障时,被其持有的锁可以自动释放。
要解决问题2,一个简单的方案是支持可重入,我们为每个获取锁的客户端都配置一个不会重复的身份标识(通常是UUID),上锁成功后锁将带有该客户端的身份标识。当实际上锁成功而客户端超时重试时,我们可以判断锁已被该客户端持有而返回成功。具体代码如下:
123456789101112131415161718192021 ...
并发编程实践之公平有界阻塞队列实现(下)
4、状态追踪解除竞争此处可以通过状态追踪,解除读与读之间和写与写之间的竞争问题。
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293public interface Queue { boolean offer(Object obj) throws InterruptedException; Object poll() throws InterruptedException;}class FairnessBoundedBlockingQueue implements Queue { // 容量 protected final int capacity; // 头指针,empty: head.next == tail == null ...
并发编程实践之公平有界阻塞队列实现(上)
1、基础版本123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960interface Queue { boolean offer(Object obj); Object poll(); }class FairnessBoundedBlockingQueue implements Queue { // 当前大小 protected int size; // 容量 protected final int capacity; // 头指针,empty: head.next == tail == null protected Node head; // 尾指针 protected Node tail; public FairnessBoundedBlockingQueue(int capacity) { ...
消息中间件单例服务优化
几乎所有串行化理论真正解决的问题只有一个:性能。 所以,在性能允许的前提下,对于消费者角色,建议采用单实例部署。通过单实例部署,有序性、串行化、完整性和一致性问题自动获得了解决。另外,单实例部署的消费者拥有全部所需信息,它可以在频次控制上采取很多优化策略。
单实例部署并非没有代价,它意味着系统可用性的降低,解决可用性问题的最直接的思路就是冗余(Redundancy)。最常用的冗余方案是Master-slave架构,不过大部分的Master-slave架构都是Active/active模式,即主从服务器都提供服务。
大部分基于负载均衡设计的Master-slave集群中,主服务器和从服务器同时提供相同的服务。这显然不满足单例服务优化需求。有序性和串行化需要Active/passive架构,即在某一时刻只有主实例提供服务,其他的从服务等待主实例失效。这是典型的领导人选举架构,即只有获得领导权的实例才能充当实际消费者,其他实例都在等待下一次选举。采用领导人选举的Active/passive架构可以大大缓解纯粹的单实例部署所带来的可用性问题。
参考《美团博客》