写点什么

释放.NET Big Memory 和内存映射文件的能量

  • 2017-08-08
  • 本文字数:4507 字

    阅读完需:约 15 分钟

要点:

  • 通常 Web 服务器具有的内存远远超过了.NET GC 在正常情况下可以有效处理的量。
  • 缓存服务器的性能优势通常会因增加了网络成本而下降。
  • 内存映射文件通常是在系统重新启动后填充缓存的最快方式。
  • 服务器端调优的目的是出站网络连接达到饱和的程度。 通过最小化 CPU、磁盘和内部网络的使用来达到网络连接饱和的程度。
  • 通过在内存中保存对象图,可以获得图形数据库的性能优势,而且不会提高复杂性。

延续关于.NET 平台( part1 part2 )的 Big Memory 主题,本文介绍了使用 Agincore’s Big Memory Pile 在受管 CLR 服务器环境中使用大型数据集的优势。

综述

现今 RAM 已经非常快速和实惠,但是生命周期较短。 每次重新启动进程,内存将被清除,一切都必须重新加载。为了解决这个问题,我们最近添加了内存映射文件来支持我们的解决方案,即 NFX 堆。 使用内存映射文件,可以在重新启动后从磁盘快速获取数据。

总的来说,Big Memory 方法对于开发人员和企业来说是有益的,因为它改变了.NET 平台上的高性能计算模式。 传统的 Big Memory 系统是以 C/C ++ 风格的语言构建的,主要处理字符串和字节数组。 但是当专注于底层数据结构时,就很难解决实际的业务问题了,所以我们要专注于 CLR 对象。 内存堆允许开发人员根据对象实例进行思考,并与具有属性 **、编码、**继承和其他 CLR 原生功能的数亿个实例配合工作

与语言无关的对象模型不同,就像一些软件供应商(如支持 Java 和.NET 的供应商)所提出的那些,它们引入额外的变换以及需要额外流量 / 上下文切换 / 序列化的所有进程外解决方案。相反,我们将讨论进程内的本地堆或类“堆”对象,或者是在托管代码中的大型字节数组中的对象。这些对象对于 GC 来说是不可见的。

用例

为什么有人会使用几十或几百 GB 的 RAM 呢?以下是 Big Memory Pile 技术的一些已验证过的用例。

首先是缓存。在电子商务后端,存储着数十万种用来显示为详细目录列表的产品。每种产品可能有数十种变体。当在单个屏幕上构建一个展示 30 多个产品的目录视图时,即使是单个用户使用渐进式加载滚动页面,也需要很快速地获取到这些对象。为什么不使用 Redis 或 Memcached?因为在同一个进程中做相同的事情,可以节省网络流量和序列化开销。将网络数据包转换成对象可能是非常昂贵的操作过程。如果要持有数十万种产品及其变体,您是否会使用字典 <id,Product>(或 IMemoryCache )?单单缓存数据便提供了足够的动机来使用 RAM,但其实还有更多的方面…

另一个缓存用例是 REST API 服务器,可以将约 5000 万个很少更改的 JSON 向量序列化为 UTF8 编码的字节数组。大约 1024 字节的数组可以直接发送到 Http 流中,使得网络瓶颈处在 80,000 req / sec 左右。

使用复杂对象图是 Pile 的另一个完美案例。在社交应用中,需要遍历 Twitter 上的对话线程。当追踪谁在社区媒体网站上什么时候说了什么时,在内存中持有数亿个小向量的价值是无法估量的。也可以选择使用 graph DB,而在这个案例中,在同一个进程中(它是由 Web MVC 应用程序托管的组件),正是使用了 graph DB。我们现在正在处理每秒 100K 以上的 REST API 调用,这受限于我们的网络连接数,并且我们将 CPU 使用率保持在较低的水平。

在这个和其他用例中,后台工作人员随着变化而异步地更新社交图表。在许多情况下,如前面提到的产品目录,它可被优先完成。但是你不应该使用只持有数据子集的普通缓存来完成更新。

工作原理

Big Memory Pile 通过在大型字符数组中使用 CLR 对象图的透明序列化来解决 GC 问题,有效地“隐藏”了 GC 可获取范围内的对象。然而并不是所有的对象类型都需要完全序列化, 字符串和字节数组对象会被写入堆中,因为缓冲区会绕过所有的序列化机制,在 6 核主机上每秒可完成超过 6 百万次 64 字的字符串插入。

这种方法的主要优点是其实用性。现实生活中的案例,在使用原生 CLR 对象模型时已显示出惊人的整体性能,因为不需要创建专用 DTO,这样可以节省开发时间,而且因为不需要创建中间过程所需的额外副本,运行速度也会 **** 更快

总的来说,Pile 将大部分I/O 绑定代码转换成CPU 限制代码。通常情况下,异步(具有 I/O 绑定)实现的典型案例是100%的同步线性代码,这种代码更加简单,并且在单个服务器上执行多个 100K 操作 / 秒时性能更好,因为 Task 和其他异步 / 等待的实现有隐藏开销(参见这里这里)。

Big Memory 映射文件

内存中的处理过程是快速和容易实现的,但是当进程重新启动时,数据集会丢失,这个数据集通常(几十到几百 GB)是很大的。从原始数据源拉出所有数据可能是非常耗时的,那是在重新启动后无法承受的时间开销。

为了解决这个问题,我们添加了内存映射文件(MMF)来支持使用标准的.NET 类: MemoryMappedFile MemoryMappedViewAccessor 。现在,我们使用 MemoryMappedViewAccessor实例和一些低级技巧来直接使用指针访问数据,而不是使用字节数组作为内存段的后备存储,所有这些操作都使用标准的C#来完成,不需要C ++ 参与,以保持一切简单,特别是构建链。

通过MemoryMappedViewAccessor( MMFMemory 类)写入内存直接修改 OS 层中的虚拟内存页面。如果操作系统不能将这些页面交换到磁盘中,便会尝试将这些页面放在物理 RAM 中。将 Pile 写入 MMF 的一个很好的功能是,进程在关闭后马上重启则不需要从磁盘重新读取所有内容。即使在进程终止之后,操作系统仍将映射到进程地址空间的页面保留。启动时, MMFPile 可以以比从磁盘重新读取更快的方式访问 RAM 中的页面。

请注意,由于在 MMFMemory 类中完成了非托管代码的上下文切换,MMFPile 的性能会比 DefaultPile 慢(基于字节数组)。

以下是一些测试结果:

基准测试插入 200,000,000 字符串 [32] 12 个线程:

(机器:Intel Core I7 3.2 Ghz,6 Core,Win 7 64bit,VS2017,.NET 4.5)

DefaultPile

24 秒 @ 8.3 百万次插入 / 秒 = 8.5 Gb 内存 ; 全 GC <8 ms

MMFPile

  • 41 秒 @ 4.9 百万次插入 / 秒 = 8.5 GB 内存 + 磁盘 ; 全 GC <10 ms
  • 在 Stop()上清除所有数据到磁盘:10 秒
  • 读取所有数据到 ram:48 秒 =〜177 mbyte / 秒

正如你所看到的,MMF 解决方案确实有额外的开销。由于非托管的 MMF 转换,吞吐量较低,并且一旦从磁盘安装 Pile,则耗时与使用磁盘数据预先分配给 RAM 的内存量成比例。然而,你不需要等待加载整个工作集,因为 MMFPile在 Pile.Start() 之后立即可用于写入和读取,数据的全部负载将需要时间开销,在上面的示例中 8.5 GB 数据集需要 48 秒的时间才能在中档 SSD 的 RAM 中预热。

基准测试插入 200,000,000 个 Person**** 对象(具有 7 个字段12 个线程:

DefaultPile

85 秒 @ 2.4 百万次插入 / 秒 = 14.5 Gb 内存 ; 全 GC <10ms

MMFPile

  • 101 秒 @ 1.9 百万次插入 / 秒 = 14.5 GB 内存 + 磁盘 ; 全 GC <10ms
  • 在 Stop() 上清除所有数据到磁盘:30 秒
  • 读取所有数据到 ram:50 秒 =〜290 万字节 / 秒

其他改进

从上一次在 InfoQ 上发表文章以来,我们对 NFX.Pile 做了很多改进:

原始分配器 / 分层设计

Pile 的实现可以更好地分层,从而可以将字符串和字节数组直接从 RAM 的较大的连续块中直接写入 / 读取。整个序列化机制完全绕过字节数组,从而可以使用 Pile 作为一个原始的字节数组分配器。

复制代码
var ptr = pile.Put(“abcdef”); // 这将绕过所有序列化程序
// 并使用 UTF8Encoding 代替
var original = pile.Get(ptr)as string;

性能提升

由于引入了试图避免多线程竞争的滑动窗口优化实现,段分配逻辑已经被修改,并且在多线程插入期间提高了 50%以上的性能。此外,在大多数情况下字符串和字节数组完全绕过串行器会提高速度近 5 百万次插入 / 秒(200%以上的改进)。

枚举

利用 IEnumerable 接口实现,可以获得整个堆的内容。 PileEntry 结构体如下:

复制代码
foreach(var entry in pile){
Console.WriteLine(“{0} points to
{1} bytes”.Args( entry.Pointer,
entry.Size));
var data = pile.Get(entry.Pointer);
}

缓存持久化

出于性能原因,缓存的默认模式是“推测的”。在这种模式下,即使存在足够的内存,散列码冲突也可能导致较低的优先级项从高速缓存中弹出。

缓存服务器可以以“持久”模式存储数据,工作方式更像普通的字典。因为持久化模式需要在 bucket 中进行重新排列,所以相比推测模式会慢 5-10%。对于大多数应用程序来说是难以察觉的。但是需要根据特定的情况进行测试从而确定最佳实现方式。

复制代码
// 为所有表指定 TableOptions,将表持久化
cache.DefaultTableOptions = new TableOptions(“*”)
{
CollisionMode = CollisionMode.Durable
};

地对象突变和预分配

可以在现有 PilePointer 地址改变对象。新的 API Put(PilePointer …)允许在现有位置放置不同的有效载荷。如果新的有效载荷不适合现有的块,那么 Pile 将创建一个内部链接到新位置(* nix 系统中的一个文件系统链接),有效地使原始指针指向新的位置。删除原始指针将删除链接及其所指向的内容。别名是完全透明的,并在读取时产生目标有效载荷。

还可以通过在调用 Put()时指定 preallocateBlockSize,为有效负载预分配更多的 RAM。

复制代码
// 堆中存储链表的实现
public class ListNode{
public PilePointer Previous;
public PilePointer Next;
public PilePointer Value;}
...
private IPile m_Pile;//big memory pile
private PilePointer m_First;//list head
private PilePointer m_Last;//list tail
...
// 将 Person 实例追加到堆中存储的人员链接列表
// 返回最后一个节点
public PilePointer Append(Person person){
var newLast = new ListNode{ Previous = m_Last,
Next = PilePointer.Invalid,
Value = m_Pile.Put(person)};
var existingLast = m_Pile.Get(m_Last);
existingLast.Next = node;
m_Pile.Put(m_Last,existingLast); // 在现有的 ptr m_Last 中进行编辑
m_Last = m_Pile.Put(newLast); // 向尾部添加新节点
return m_Last;
}

更多信息,请参阅我们的视频:.NET Big Memory Object Pile - 在 RAM 中使用数百万个对象

链接

关于作者

Dmitriy Khmaladze在美国的有超过 20 年的 IT 从业经验,主要工作在创业公司和财富 500 强客户。1998 年 Galaxy 在医疗行业主创了 SaaS。在语言与编译设计、分布式架构、系统编程和架构、C / C ++、.NET、Java、Android、IOS、Web 设计、HTML5、CSS、JavaScript、RDBMS 和 NoSQL / NewSQL 等领域有 15 年以上的研究。

查看英文原文: https://www.infoq.com/ articles/Big-Memory-Part-3


感谢冬雨对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2017-08-08 17:412325

评论

发布
暂无评论
发现更多内容

终于把性能测试这事儿讲清楚了

博睿数据

看完源码记不住?掌握这套方法,Alibaba不会少你一个工位

热爱java的分享家

Java 面试 程序人生 编程语言 经验分享

Spring Bean生命周期你除了会背八股文面试,真的会用了吗?

热爱java的分享家

Java spring 程序人生 编程语言 经验分享

分布式系统的架构演进过程(一)

卢卡多多

分布式, 11月日更

GitHub霸榜月余的24万字Java面试手册,竟是阿里机密

热爱java的分享家

Java 面试 程序人生 编程语言 经验分享

华为顶级安全专家分享出来这份‘典藏版’Linux开发实战笔记

热爱java的分享家

Java 架构 程序人生 编程语言 经验分享

如何设计一款跨平台低延迟的RTMP|RTSP直播播放器

音视频牛哥

WebRTC HLS RTMP RTSP

提升研发效能的低代码思路

赫杰辉

研发效能 低代码平台 x-series

Python代码阅读(第61篇):延迟调用函数

Felix

Python 编程 Code 阅读代码 Python初学者

Spark面试题大全

编程江湖

大数据 Spark面试题

通过 for 循环,比较 Python 与 Ruby 编程思想的差别

Python猫

Python

Java 项目中使用 Resilience4j 框架实现客户端 API 调用的限速/节流机制

码语者

Java 限速 节流 Resilience4j RateLimit

App 端自动化的最佳方案,完全解放双手!

星安果

Python 自动化 爬虫

海康摄像机RTSP地址格式(官方最新版)

音视频牛哥

WebRTC RTMP RTSP 播放器

Go WebSocket开发与测试实践【gorilla/websocket】

FunTester

Java websocket 接口测试 Go 语言 FunTester

构建多云战略的注意事项

浪潮云

云计算

如何运用Camtasia中的交互性?

淋雨

Camtasia

EMQ 出席 2021 ArchSummit,打造全连接时代的数据基础设施

EMQ映云科技

大数据 物联网 IoT 智能

钉钉小程序快照技术初窥

阿里巴巴终端技术

小程序 ios android 快照 体验优化

一次搞懂SpringBoot核心原理:自动配置、事件驱动、Condition

热爱java的分享家

Java 程序人生 Spring Boot 编程语言 经验分享

拒绝卡顿,揭秘盒马鲜生 APP Android 短视频秒播优化方案

阿里巴巴终端技术

android App 短视频 移动开发 体验优化

在 JavaScript 中如何检查对象为空

编程江湖

JavaScript 大前端

如何判断一个需求是真的用户需求,还是伪需求呢?

石云升

产品思维 需求分析 11月日更

快上车丨直播课“Hello ArkansasUI:初识Slider组件(eTS)”来啦

HarmonyOS开发者

HarmonyOS

线程如何实现保持同步

编程江湖

Java 多线程

Python Qt GUI设计:QComboBox下拉列表框类(基础篇—14)

不脱发的程序猿

Python qt PyQt GUI设计 QComboBox下拉列表框类

【AI最前线】精准优质-资讯|分享|热议第41期

百度大脑

人工智能

如何实现在线播放mpeg-ts文件?

Changing Lin

11月日更

远程工作的这几年

程序员铮铮

程序员 代码人生 远程工作

Spring中的Controller和Service是线程安全的吗?我有点懵

热爱java的分享家

Java spring 程序人生 编程语言 经验分享

贪心:柠檬水找零、跳跃游戏🍋

空城机

JavaScript 贪心算法 11月日更

释放.NET Big Memory和内存映射文件的能量_.NET_Dmitriy Khmaladze_InfoQ精选文章