.NET 高性能缓冲队列实现 BufferQueue
前言
BufferQueue 是一个用 .NET 编写的高性能的缓冲队列实现,支持多线程并发操作。
项目是从 mocha 项目中独立出来的一个组件,经过修改以提供更通用的缓冲队列功能。
目前支持的缓冲区类型为内存缓冲区,后续会考虑支持更多类型的缓冲区。
适用场景
生产者和消费者之间的速度不一致,需要并发批量处理数据的场景。
功能说明
- 支持创建多个 Topic,每个 Topic 可以有多种数据类型。每一对 Topic 和数据类型对应一个独立的缓冲区。
支持创建多个 Consumer Group,每个 Consumer Group 的消费进度都是独立的。支持多个 Consumer Group 并发消费同一个 Topic。
支持同一个 Consumer Group 创建多个 Consumer,以负载均衡的方式消费数据。
支持数据的批量消费,可以一次性获取多条数据。
支持 pull 模式和 push 模式两种消费模式。
pull 模式下和 push 模式下都支持 auto commit 和 manual commit 两种提交方式。auto commit 模式下,消费者在收到数据后自动提交消费进度,如果消费失败不会重试。manual commit 模式下,消费者需要手动提交消费进度,如果消费失败只要不提交进度就可以重试。
需要注意的是,当前版本出于简化实现的考虑,暂不支持消费者的动态扩容和缩容,需要在创建消费者时指定消费者数量。
使用示例
安装 Nuget 包:
dotnet add package BufferQueue
项目基于 Microsoft.Extensions.DependencyInjection,使用时需要先注册服务。
BufferQueue 支持两种消费模式:pull 模式和 push 模式。
builder.Services.AddBufferQueue(options =>
{
options.UseMemory(bufferOptions =>
{
// 每一对 Topic 和数据类型对应一个独立的缓冲区,可以设置 partitionNumber
bufferOptions.AddTopic<Foo>("topic-foo1", partitionNumber: 6);
bufferOptions.AddTopic<Foo>("topic-foo2", partitionNumber: 4);
bufferOptions.AddTopic<Bar>("topic-bar", partitionNumber: 8);
})
// 添加 push 模式的消费者
// 扫描指定程序集中的标记了 BufferPushCustomerAttribute 的类,
// 注册为 push 模式的消费者
.AddPushCustomers(typeof(Program).Assembly);
});
// 在 HostedService 中使用 pull模式 消费数据
builder.Services.AddHostedService<Foo1PullConsumerHostService>();
pull 模式的消费者示例:
public class Foo1PullConsumerHostService(
IBufferQueue bufferQueue,
ILogger<Foo1PullConsumerHostService> logger) : IHostedService
{
private readonly CancellationTokenSource _cancellationTokenSource = new();
public Task StartAsync(CancellationToken cancellationToken)
{
var token = CancellationTokenSource
.CreateLinkedTokenSource(cancellationToken, _cancellationTokenSource.Token)
.Token;
var consumers = bufferQueue.CreatePullConsumers<Foo>(
new BufferPullConsumerOptions
{
TopicName = "topic-foo1", GroupName = "group-foo1", AutoCommit = true, BatchSize = 100,
}, consumerNumber: 4);
foreach (var consumer in consumers)
{
_ = ConsumeAsync(consumer, token);
}
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_cancellationTokenSource.Cancel();
return Task.CompletedTask;
}
private async Task ConsumeAsync(IBufferPullConsumer<Foo> consumer, CancellationToken cancellationToken)
{
await foreach (var buffer in consumer.ConsumeAsync(cancellationToken))
{
foreach (var foo in buffer)
{
// Process the foo
logger.LogInformation("Foo1PullConsumerHostService.ConsumeAsync: {Foo}", foo);
}
}
}
}
push 模式的消费者示例:
通过 BufferPushCustomer 特性注册 push 模式的消费者。
push consumer 会被注册到 DI 容器中,可以通过构造函数注入其他服务,可以通过设置 ServiceLifetime 来控制 consumer 的生命周期。
BufferPushCustomerAttribute 中的 concurrency 参数用于设置 push consumer 的消费并发数,对应 pull consumer 的 consumerNumber。
[BufferPushCustomer(
topicName: "topic-foo2",
groupName: "group-foo2",
batchSize: 100,
serviceLifetime: ServiceLifetime.Singleton,
concurrency: 2)]
public class Foo2PushConsumer(ILogger<Foo2PushConsumer> logger) : IBufferAutoCommitPushConsumer<Foo>
{
public Task ConsumeAsync(IEnumerable<Foo> buffer, CancellationToken cancellationToken)
{
foreach (var foo in buffer)
{
logger.LogInformation("Foo2PushConsumer.ConsumeAsync: {Foo}", foo);
}
return Task.CompletedTask;
}
}
[BufferPushCustomer(
"topic-bar",
"group-bar",
100,
ServiceLifetime.Scoped,
2)]
public class BarPushConsumer(ILogger<BarPushConsumer> logger) : IBufferManualCommitPushConsumer<Bar>
{
public async Task ConsumeAsync(IEnumerable<Bar> buffer, IBufferConsumerCommitter committer,
CancellationToken cancellationToken)
{
foreach (var bar in buffer)
{
logger.LogInformation("BarPushConsumer.ConsumeAsync: {Bar}", bar);
}
var commitTask = committer.CommitAsync();
if (!commitTask.IsCompletedSuccessfully)
{
await commitTask.AsTask();
}
}
}
BufferQueue 内部设计概述
Topic 的隔离
BufferQueue 有以下的特性:
同一个数据类型 下的 不同 Topic 的 BufferQueue 互不干扰。
同一个 Topic 下的 不同数据类型 的 BufferQueue 互不干扰。
这个特性是通过以下两层接口设计实现的:
IBufferQueue:根据 TopicName 和 类型参数 T 将请求转发给具体的 IBufferQueue<T> 实现(借助 KeyedService 实现),其中参数 T 代表 Buffer 所承载的数据实体的类型。
IBufferQueue<T>:具体的 BufferQueue 实现,负责管理 Topic 下的数据。属于 Buffer 模块的内部实现,不对外暴露。
Partition 的设计
为了保证消费速度,BufferQueue 将数据划分为多个 Partition,每个 Partition 都是一个独立的队列,每个 Partition 都有一个对应的消费者线程。
Producer 以轮询的方式往每个 Partition 中写入数据。
Consumer 最多不允许超过 Partition 的数量,Partition 按平均分配到组内每个 Customer 上。
当一个 Consumer 被分配了多个 Partition 时,以轮训的方式进行消费。
每个 Partition 上会记录不同消费组的消费进度,不同组之间的消费进度互不干扰。
对并发的支持
Producer 支持并发写入。
Consumer 消费时是绑定 Partition 的,为保证能正确管理 Partition 的消费进度,Consumer 不支持并发消费。
如果要增加消费速度,需创建多个 Consumer。
Partition 的动态扩容
Partition 的基本组成单元是 Segment,Segment 代表保存数据的数组,多个 Segment 通过链表的形式组合成一个 Partition。
当一个 Segment 写满后,通过在其后面追加一个 Segment 实现扩容。
Segment 中用于保存数据的数组的每一个元素称为 Slot,每个 Slot 都有一个Partition 内唯一的自增 Offset。
Segment 的回收机制
每次在 Partition 中新增 Segment 时,会从头判断此前的 Segment 是否已经被所有消费组消费完,回收最后一个消费完的 Segment 作为新的 Segment 追加到 Partition 末尾使用。
Benchmark
测试环境:Apple M2 Max 64GB
写入性能测试
与 BlockingCollection 对比并发,并发线程数为 CPU 逻辑核心数 12, partitionNumber 为 1 和 12。
测试结果
在并发写入时,BufferQueue 的写入性能明显优于 BlockingCollection。
消费性能测试
pull 模式 consumer 与 BlockingCollection 对比并发读取性能,并发线程数为 CPU 逻辑核心数 12,partitionNumber 为 12。
测试结果
在批量消费时,随着批量大小的增加,BufferQueue 的消费性能优势更加明显。
.NET 高性能缓冲队列实现 BufferQueue的更多相关文章
- 构建高性能服务(三)Java高性能缓冲设计 vs Disruptor vs LinkedBlockingQueue--转载
原文地址:http://maoyidao.iteye.com/blog/1663193 一个仅仅部署在4台服务器上的服务,每秒向Database写入数据超过100万行数据,每分钟产生超过1G的数据.而 ...
- 构建高性能服务 Java高性能缓冲设计 vs Disruptor vs LinkedBlockingQueue
一个仅仅部署在4台服务器上的服务,每秒向Database写入数据超过100万行数据,每分钟产生超过1G的数据.而每台服务器(8核12G)上CPU占用不到100%,load不超过5.这是怎么做到呢?下面 ...
- 高性能环形队列框架 Disruptor 核心概念
高性能环形队列框架 Disruptor Disruptor 是英国外汇交易公司LMAX开发的一款高吞吐低延迟内存队列框架,其充分考虑了底层CPU等运行模式来进行数据结构设计 (mechanical s ...
- JUC并发编程与高性能内存队列disruptor实战-下
并发理论 JMM 概述 Java Memory Model缩写为JMM,直译为Java内存模型,定义了一套在多线程读写共享数据时(成员变量.数组)时,对数据的可见性.有序性和原子性的规则和保障:JMM ...
- dsp下基于双循环缓冲队列的视频采集和显示记录
对最近在设计的视频采集和显示缓冲机制做一个记录,以便以后使用. 视频采集和显示缓冲机制,其实是参考了Linux下v4L2的驱动机制,其采用输入多缓冲frame,输出多缓冲的切换机制.简单的就是ping ...
- 高性能消息队列 CKafka 核心原理介绍(上)
欢迎大家前往腾讯云技术社区,获取更多腾讯海量技术实践干货哦~ 作者:闫燕飞 1.背景 Ckafka是基础架构部开发的高性能.高可用消息中间件,其主要用于消息传输.网站活动追踪.运营监控.日志聚合.流式 ...
- Java并发编程:4种线程池和缓冲队列BlockingQueue
一. 线程池简介 1. 线程池的概念: 线程池就是首先创建一些线程,它们的集合称为线程池.使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动 ...
- 自定义ThreadPoolExecutor带Queue缓冲队列的线程池 + JMeter模拟并发下单请求
.原文:https://blog.csdn.net/u011677147/article/details/80271174 拓展: https://github.com/jwpttcg66/GameT ...
- 多线程操作C++ STL vector出现概率coredump问题及尽量避免锁的双缓冲队列
多线程操作全局变量,必须考虑同步问题,否则可能出现数据不一致, 甚至触发coredump. 前段时间, 遇到一个多线程操作了全局的vector的问题, 程序崩了.场景是这样的:某全局配置参数保存在一 ...
- 固定尺寸内存块的缓冲队列类及C++实现源代码
-------------------------------------------------------------------------------- 标题: 固定尺寸内存块的缓冲队列类及实 ...
随机推荐
- zabbix笔记_006 zabbix web监控
web监控 web监控是对http网站服务进行监控,模拟用户访问网站,对特定的结果进行告警,通知管理员网站状态. web监控是运维必备知识点,通过实验能够熟悉配置和了解zabbix是如何监控web站点 ...
- 《最新出炉》系列入门篇-Python+Playwright自动化测试-50-滚动条操作
1.简介 有些页面的内容不是打开页面时直接加载的,需要我们滚动页面,直到页面的位置显示在屏幕上时,才会去请求服务器,加载相关的内容,这就是我们常说的懒加载.还有就是在日常工作和学习中,经常会遇到我们的 ...
- dhcp报错
报错详情 查看dhcpd.service状态 使用命令检查配置文件报错 dhcpd -t -cf /etc/dhcp/dhcpd.conf 修改配置文件 重启dhcpd服务 [root@servera ...
- 瑞数456vmp逆向分析
声明 本文章中所有内容仅供学习交流,抓包内容.敏感网址.数据接口均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关,若有侵权,请联系我立即删除! 目标网站 aHR0cHM6 ...
- C#.NET 微信上传电子小票
HttpWebRequest 时,不认图片的Content-Type.Content-Type 实际是有传的. 报错内容:{"code":"PARAM_ERROR&quo ...
- 编程语言界的丐帮 C#.NET 国密数字信封 民生银行
民生银行的库DLL只有C版本和JAVA版本.按着JAVA版本做的C# 实现. 重点内容. 1.数字信封就是 CmsEnvelopedData Der编码后转BASE64 2.重点类:ContentIn ...
- vitepress使用createContentLoader时遇到["vitepress" resolved to an ESM file. ESM file cannot be loaded by `require`.]报错
在使用vitepress构建一个所有博客的概览页的时候,使用到了createContentLoader这个api,但是在index.data.ts中定义并导出后,在md文件中调用遇到了下面这个问题: ...
- 字节面试:MySQL自增ID用完会怎样?
在一些中小型项目开发中,我们通常会使用自增 ID 来作为主键的生成策略,但随着时间的推移,数据库的信息也会越来越多,尤其是使用自增 ID 作为日志表的主键生成策略时,可能很快就会遇到 ID 被用完的情 ...
- 使用Microsoft.SemanticKernel基于本地运行的Ollama大语言模型实现Agent调用函数
大语言模型的发展日新月异,记得在去年这个时候,函数调用还是gpt-4的专属.到今年本地运行的大模型无论是推理能力还是文本的输出质量都已经非常接近gpt-4了.而在去年gpt-4尚未发布函数调用时,智能 ...
- ClickHouse介绍(一)初次使用
ClickHouse使用 ClickHouse是一个面向列存储的OLAP分析数据库,以其强大的分析速度而闻名.有关ClickHouse的介绍可以参考其官网说明[1].本文主要介绍它的基本使用. 1. ...