惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

美团技术团队
W
WeLiveSecurity
Stack Overflow Blog
Stack Overflow Blog
L
LangChain Blog
S
SegmentFault 最新的问题
Apple Machine Learning Research
Apple Machine Learning Research
Google DeepMind News
Google DeepMind News
F
Full Disclosure
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
The Register - Security
The Register - Security
G
Google Developers Blog
C
Check Point Blog
GbyAI
GbyAI
A
About on SuperTechFans
V
Vulnerabilities – Threatpost
T
The Blog of Author Tim Ferriss
T
Tor Project blog
AWS News Blog
AWS News Blog
Cyberwarzone
Cyberwarzone
C
CERT Recently Published Vulnerability Notes
MongoDB | Blog
MongoDB | Blog
Latest news
Latest news
aimingoo的专栏
aimingoo的专栏
U
Unit 42
Y
Y Combinator Blog
P
Privacy International News Feed
Cisco Talos Blog
Cisco Talos Blog
S
Securelist
S
Schneier on Security
雷峰网
雷峰网
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Attack and Defense Labs
Attack and Defense Labs
P
Proofpoint News Feed
C
Cisco Blogs
Webroot Blog
Webroot Blog
T
Troy Hunt's Blog
Google Online Security Blog
Google Online Security Blog
月光博客
月光博客
P
Privacy & Cybersecurity Law Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
罗磊的独立博客
Cloudbric
Cloudbric
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Hacker News: Ask HN
Hacker News: Ask HN
H
Hackread – Cybersecurity News, Data Breaches, AI and More
博客园 - 司徒正美
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Microsoft Security Blog
Microsoft Security Blog

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角 .NET Core 原生 DI 扩展之基于名称的注入实现 - 元视角
.NET Core 中对象池(Object Pool)的使用 - 元视角
飞鸿踏雪 · 2020-08-16 · via 元视角

在此前的博客中,博主参考 eShopOnContainers 实现了一个基于 RabbitMQ 的事件总线(EventBus)。在这个项目中,它提供了一个持久化连接的类DefaultRabbitMQPersistentConnection,主要解决了 RabbitMQ 在连接断开后自动重连的问题,可实际上我们都知道,RabbitMQ 提供的连接数是有一个上限的,如果频繁地使用短连接的方式,即通过ConnectionFactoryCreateConnection()方法来创建一个连接,从本质上讲,一个Connection对象就是一个 TCP 连接,而Channel则是每个Connection对象下有限的虚拟连接,注意“有限”这个限定词,这意味着ChannelConnection一样,都不能毫无节制的创建下去。此时,官方推荐的做法有两种:(1):一个Connection对应多个Channel同时保证每个Channel线程独占;(2):创建一个Connection池同时定期清除无效连接。这里的第二种做法,显然就是我们今天要说的对象池(Object Pool)啦,我们将从这里拉开这篇博客的帷幕。

什么是对象池

首先,我们来回答第一个问题,什么是对象池?简单来说,它就是一种为对象提供可复用性能力的软件设计思路。俗话说**“有借有还,再借不难”**,而对象池就是通过“借”和“还”这样两个动作来保证对象可以被重复使用,进而节省频繁创建对象的性能开销。对象池在游戏设计中使用的更普遍一点,因为游戏中大量存在着像子弹、怪物等等这类可复用的对象,你在玩第一人称射击游戏(FPS)时,总是有源源不断的子弹或者丧尸出现,可事实上这不过是数字世界的循环再生,因为玩家的电脑内存始终都有一个上限。而在数据库的世界里,则存在着一个被称为“连接池”的东西,每当出现数据库无法连接的情况时,经验丰富的开发人员往往会先检查“连接池”是否满了,这其实就是对象池模式在特定领域的具体实现啦,所以,对象池本质上就是负责一组对象创建和销毁的容器,下面是一个基本的对象池示意图:

对象池示意图 对象池示意图

可以注意到, 对象池最大的优势就是可以自主地管理“池子”内的每个对象,决定它们是需要被回收还是可以重复使用。我们都知道,创建一个新的对象,需要消耗一定的系统资源,而一旦这些对象可以重复地使用,就能有效地节省系统资源的开销,这对于我们提高系统性能会非常有帮助。也许,现在计算机的硬件水平越来越好,可我们还是要重新拾起这个领域的基础知识,即数据结构、算法、数学和英语。如果你完全理解了对象池模式,你应该可以非常轻松地给出你的实现:

public class ObjectPool<T> : IObjectPool<T>
{
  private Func<T> _instanceFactory;
  private ConcurrentBag<T> _instanceItems;

  public ObjectPool(Func<T> instanceFactory)
  {
    _instanceFactory = instanceFactory ?? 
      throw new ArgumentNullException(nameof(instanceFactory));
    _instanceItems = new ConcurrentBag<T>();
  }

  public T Get()
  {
    T item;
    if (_instanceItems.TryTake(out item)) return item;
    return _instanceFactory();
  }

  public void Return(T item)
  {
    _instanceItems.Add(item);
  }
}

注:以上代码片段来自微软的一篇文档:How to: Create an Object Pool by Using a ConcurrentBag。实际上,除了ConcurrentBag<T>,我们可以选择的数据结构还可以是Stack<T>Queue<T>以及BlockingCollection<T>,此中差别,大家可以自己去体会。

.NET Core 中的对象池

在.NET Core 中,微软已经为我们提供了对象池的实现,即Microsoft.Extensions.ObjectPool。它主要提供了三个核心的组件,分别是ObjectPoolObjectPoolProviderIPooledObjectPolicy,关于这三者间的关系,我绘制了下面的 UML 图来作为说明:

ObjectPool核心组件及其关系 ObjectPool核心组件及其关系

可以注意到,ObjectPool<T>是一个抽象类,它对外提供了 Get()和 Return()两个方法,所谓的“有借有还”,这一点没什么可说的。接下来,ObjectPoolProvider同样是一个抽象类,它的职责就是创建ObjectPool<T>,所以,它提供了两个Create<T>()方法,两者的区别是,无参数版本本质上使用的是DefaultPooledObjectPolicy<T>。顾名思义,它同DefaultObjectPool<T>DefaultObjectPoolProvider一样,都是微软提供的默认实现,其中IPooledObjectPolicy<T>可以为不同的对象池定义不同的策略,来决定对象如何“借”、是否可以“还”。默认的对象池DefaultObjectPool<T>内部使用ObjectWrapper[]这个数组来管理对象,数组的大小等于 maximumRetained - 1,因为它单独指定了首项,默认情况下,这个 maximumRetained 等于Environment.ProcessorCount * 2,这里主要用到了Interlocked.CompareExchange()方法:

public override T Get()
{
  var item = _firstItem;
  if (item == null || Interlocked.CompareExchange(ref _firstItem, null, item) != item)
  {
    var items = _items;
    for (var i = 0; i < items.Length; i++)
    {
      item = items[i].Element;
      if (item != null && Interlocked.CompareExchange(ref items[i].Element, null, item) == item)
      {
        return item;
      }
    }

    item = Create();
  }

  return item;
}

// Non-inline to improve its code quality as uncommon path
[MethodImpl(MethodImplOptions.NoInlining)]
private T Create() => _fastPolicy?.Create() ?? _policy.Create();

public override void Return(T obj)
{
  if (_isDefaultPolicy || (_fastPolicy?.Return(obj) ?? _policy.Return(obj)))
  {
    if (_firstItem != null || Interlocked.CompareExchange(ref _firstItem, obj, null) != null)
    {
      var items = _items;
      for (var i = 0; i < items.Length && Interlocked.CompareExchange(ref items[i].Element, obj, null) != null; ++i)
      {

      }
    }
  }
}

这里主要用到Interlocked.CompareExchange()这个方法,对于Get()方法而言,它将items[i].Elementnull进行交换,相当于将指定元素设为 null 并返回原始值;而对于Return()方法而言,如果将items[i].Elementobj交换后的值不为 null,则表示指定元素已经“归还”,因为这个方法只有在第一个参数和第三个参数相等时才会发生交换。好了,了解了.NET Core 中对象池的实现以后,我们来一起看看具体的使用:

var service = new ServiceCollection();

//使用DefaultObjectPoolProvider
service.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
//使用默认策略
service.AddSingleton<ObjectPool<Foo>>(serviceProvider =>
{
  var objectPoolProvider = serviceProvider.GetRequiredService<ObjectPoolProvider>();
  return objectPoolProvider.Create<Foo>();
});
//使用自定义策略
service.AddSingleton<ObjectPool<Foo>>(serviceProvider =>
{
  var objectPoolProvider = serviceProvider.GetRequiredService<ObjectPoolProvider>();
  return objectPoolProvider.Create(new FooObjectPoolPolicy());
});

var serviceProvider = _service.BuildServiceProvider();

var objectPool = _serviceProvider.GetService<ObjectPool<Foo>>();

//有借有还,两次是同一个对象
var item1 = objectPool.Get();
objectPool.Return(item1);
var item2 = objectPool.Get();
Assert.AreEqual(item1, item2);//true

//有借无还,两次是不同的对象
var item3 = objectPool.Get();
var item4 = objectPool.Get();
Assert.AreEqual(item3, item4);//false

其中,FooFooObjectPoolPolicy是两个非常典型的“工具类”,类似我们所说的“工具人”:

public class Foo
{
  public string Id { get; set; }
  public DateTime? CreatedAt { get; set; }
  public string CreatedBy { get; set; }
}

public class FooObjectPoolPolicy : IPooledObjectPolicy<Foo>
{
  public Foo Create()
  {
    return new Foo()
    {
      Id = Guid.NewGuid().ToString("N"),
      CreatedAt = DateTime.Now,
      CreatedBy = "Ezio"
    };
  }

  public bool Return(Foo obj)
  {
    return true;
  }
}

当你需要控制对象池内的对象如何被创建的时候,你可以考虑实现自定义的IPooledObjectPolicy<T>,否则,DefaultPooledObjectPolicy<T>这个默认实现完全可以满足你的使用,而这就是.NET Core 中对象池的所有用法,一个实现起来并不复杂但是在某些场景下非常有用的软件设计模式。

回到起点

好了,回到我们一开始的问题,即:如何解决 RabbitMQ 在多次重连后提示连接数不足的问题。由于 Channel 对象本质上是 Connection 对象上的 TCP 连接的软连接,所以,每当创建一个新的 Channel 的时候,实际上会独占一个 TCP 连接。考虑到在使用 RabbitMQ 的时候,发布消息/消费消息每次都是创建一个 Channel,在高并发场景下可能会导致 TCP 连接数被用完,进而出现无法连接或者响应过慢等一系列问题。既然 TCP 连接数是有限的,为什么不考虑复用这些 TCP 连接呢?从这个角度上来看,数据库连接池承担了相同的角色,增加连接数说到底是一种“治标不治本”的做法。在具体实现上,可以考虑 Connection“池”和 Channel“池”,我们我们像官方推荐的做法一样,一个 Connection 对应多个 Channel,实际上只需要实现 Channel“池”。除非在多个 Connection 对应多个 Channel 的情况下,我们需要考虑同时实现 Connection“池”和 Channel“池”。坦白说,我这里一直没能找到实现 Connection“池”的相关资料,高冷的 Catcher 大神只是让我去认真读官方文档,搞清楚 Connection 和 Channel 的关系。而这个 Channel“池”的实现,结合这篇博客里的内容,实现起来是非常简单的:

public class ChannelObjectPoolPolicy : IPooledObjectPolicy<IModel>
{
  private readonly IConnectionFactory _connectionFactory;
  public ChannelObjectPoolPolicy(IConnectionFactory connectionFactory)
  {
    _connectionFactory = connectionFactory;
  }

  public IModel Create()
  {
    var connection = _connectionFactory.CreateConnection();
    return connection.CreateModel();
  }

  public bool Return(IModel obj)
  {
    if (!obj.IsOpen)
    {
      obj?.Dispose();
      return false;
    }

    return true;
  }
}

第一步是实现IPooledObjectPolicy<IModel>,注意到,这里通过构造函数注入了ConnectionFactory,所以,除了常规的注入项以外,这里还需要注入ConnectionFactory:

services.AddSingleton<IConnectionFactory, ConnectionFactory>(sp => new ConnectionFactory() { 
    HostName = "localhost", 
    UserName = "guest", 
    Password = "guest" 
});
services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
services.AddSingleton<ObjectPool<IModel>>(serviceProvider =>
{
  var objectPoolProvider = serviceProvider.GetRequiredService<ObjectPoolProvider>();
  var connectionFactory = serviceProvider.GetRequiredService<ConnectionFactory>();
  return objectPoolProvider.Create(new ChannelObjectPoolPolicy(connectionFactory));
});

然后,我们只需要在 EventBus 里注入ObjectPool<IModel>即可,此时,我们调用 Channel 的画风是下面这样子的:

var channel = _channelPool.Get();
try {  //在这里做点什么吧  }
finally
{
  //好借好还,再借不难
  _channelPool.Return(channel);
}

关于 Connection“池”的实现,我认为我的想法还不太成熟,暂时列入未来的思考计划中,所以,这篇博客就先写到这里。

本文小结

对象池(ObjectPool)是一种通过复用对象来减少资源开销进而实现提高系统性能的软件设计模式,其核心是控制容器内对象的生命周期来规避系统的主动回收,从对象池中(ObjectPool)“借”出的对象必须要及时“归还”,否则会造成对象池(ObjectPool)中没有可用资源。实现对象池可以考虑ConcurrentBag<T>Stack<T>Queue<T>以及BlockingCollection<T>等多种数据结构,而微软在.NET Core 中已经为我们实现了一个简单的对象池,大多数情况下,我们只需要定义自己的IPooledObjectPolicy<T>去决定对象应该怎么样“借”、怎么样“还”。因为此前实现基于 RabbitMQ 的 EventBus 的时候,我们是每次创建一个 Channel,即官方所谓的“短连接”的方式,因为 Channel 本质上是 Connection 在 TCP 连接上的一个虚拟连接,所以,每次创建 Channel 都会占用一个 TCP 连接,当我们系统中的 TCP 连接被用完的时候,就会出现无法连接、连接过慢的问题,为了解决这个问题,我们最终引入了对象池,实际上这里是实现了一个 Channel“池”,关于是否应该实现 Connection“池”,这一点我还没有想好,总而言之,游戏世界里可以复用的 GameObject、各种数据库里的连接池,都是对象池模式在各自领域中的具体实现,这就是这篇博客的内容啦,欢迎大家在评论中留言,谢谢大家!