写点什么

释放.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:412915

评论

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

将新增和编辑的数据同步更新到列表

岛上码农

flutter ios开发 安卓开发 4月月更 跨平台开发

Java 结合实例学会使用 静态代理、JDK动态代理、CGLIB动态代理

爱好编程进阶

Java 面试 后端开发

采用百度飞桨EasyDL完成指定目标识别

DS小龙哥

4月月更

2021年秋招,薪资排行NO

爱好编程进阶

Java 面试 后端开发

LabVIEW实现应用程序停止或退出

不脱发的程序猿

LabVIEW

“亿”点点技术情怀

不脱发的程序猿

程序员 程序人生 技术情怀

读《Software Engineering at Google》(14)

术子米德

架构师成长笔记

k8s client-go源码分析 informer源码分析(1)-概要分析

良凯尔

Kubernetes 容器 云原生 Client-go

Choreographer全解析

爱好编程进阶

Java 面试 后端开发

Java 线程池原理分析

爱好编程进阶

Java 面试 后端开发

Java泛型机制详解;这些你都知道吗?

爱好编程进阶

Java 面试 后端开发

一个快速追踪密切接触者的开源脚本方案

冯骐

Python 数据分析 流调 密接 新冠疫情

我是如何用 Amazon Serverless 创建一个门铃的

亚马逊云科技 (Amazon Web Services)

Serverless Lambda AWS showdev

顶级元宇宙游戏Plato Farm,近期动作不断利好频频

小哈区块

AtomicIntegerArray源码分析与感悟

爱好编程进阶

Java 面试 后端开发

Java单例模式实现,一次性学完整,面试加分项

爱好编程进阶

Java 面试 后端开发

LabVIEW控制电脑关机、休眠、注销和重启

不脱发的程序猿

LabVIEW LabVIEW控制电脑

解决方案架构师的小锦囊 - 架构图的 5 种类型

亚马逊云科技 (Amazon Web Services)

技术 职业 亚马逊云科技

世界读书日:我想推荐这几本书

宇宙之一粟

书籍推荐 书单 4月月更

自动化的艺术

俞凡

架构 大厂实践 PayPal

[Day22]-[链表]相交链表

方勇(gopher)

链表 LeetCode 算法和数据结构

Java语言特点

爱好编程进阶

Java 面试 后端开发

Plato Farm-以柏拉图为目标的农场元宇宙游戏

西柚子

redis优化系列(二)Redis主从原理、主从常用配置

乌龟哥哥

4月月更

在docker上编译openjdk8

程序员欣宸

Java JVM 4月月更

[Day23]-[数据结构]手写LRU

方勇(gopher)

LeetCode LRU 数据结构算法

解锁OpenHarmony技术日!年度盛会,即将揭幕!

OpenHarmony

大会 OpenHarmony

22道Java Spring Boot高频面试题

爱好编程进阶

Java 面试 后端开发

Java岗大厂面试百日冲刺 - 日积月累,每日三题【Day39

爱好编程进阶

Java 面试 后端开发

初探 Lambda Powertools TypeScript

亚马逊云科技 (Amazon Web Services)

typescript Serverless Lambda AWS

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