设计原理:GE有一个分布式内存基础设施,成为内存云。内存云由一组内存主干组成。集群中的每台机器承载256个内存中继。我们将一台机器的本地内存空间划分为多个内存中继的原因有两方面:1)中继级别的并行性可以没有任何锁定开销的情况下实现;2)内存中继使用散列机制进行内存寻址。由于哈希冲突发生的概率较高,单个大哈希表的性能不够理想。
GE内存云提供键值访问接口,键是64位全局唯一标识符。只是任意长度的二进制数。由于内存云分布在多台机器上,我们无法使用其物理内存地址来处理键值对。为了定位给定键的值,我们首先确定存储键值对的机器,然后在该机器上的一个内存中定位键值对。
对于不同的GE应用程序,键-值对中的值组件具有不同的数据结构或数据模式。我们使用单元格来表示值组件。例如,让我们考虑具有以下内容的单元格:1)32位整数Id;2)64位整数的列表。换句话说,我们希望实现一个可变长度列表(带有Id)作为值组件。在c#中,这样的结构可以定义为:
struct CellA
{
int Id;
List<long> Links;
}
如何有效的存储结构化类型化数据并支持高效而优雅的操作是一个挑战。
我们可以直接使用大多数面向对象语言(如c++或c#)支持的对象来建模用户数据。这提供了一种方便直观的数据操作方式。我们可以通过对象的接口来操作对象,例如int Id = cell.Id 或者 cell.Links[0] = 1000001其中cell是类型的对象。这种方法虽然简单而优雅,但有明显的缺点。首先,在内存中保存对象的存储开销非常高。其次,语言运行时通常不是为处理大量对象而设计的。随着对象的增加,系统性能几句下降。第三,加载和存储数据需要大量的时间,因为序列化/反序列化对象非常耗时,尤其是数据量大。
我们可以将值组件视为二进制数并通过指针访问数据。将数据存储为二进制可以最小化内存开销。它还可以提高数据操作性能,因为数据操作不涉及数据反序列化。但是系统不知道数据的模式。在实际操作二进制数中的数据之前,我们需要知道确切的内存布局。话句话说,我们需要使用指针和地址偏移量来访问二进制数中的数据元素。这使得编程变得困难和容易出错。
按照上面描述的数据结构,我们需要知道字段Id存储在偏移量0处,连接的第一个元素位于二进制数中的偏移量8处。要获取字段Id的值,并设置Links[0]的值,他需要编写一下代码:
byte* ptr = GetBlobPtr(123); // Get the memory pointer
int Id = *(int*)ptr; // Get Id
ptr += sizeof(int);
int listLen = *(int*)ptr;
ptr+=sizeof(int);
*(long*)ptr = 1000001; // Set Links[0] to 1000001
注意,我们不能天真的将二进制数转换为使用c#等编程语言提供的内置关键字结构定义的结构。也就是说,下面显示的代码将会失败:
// C# code snippet
struct
{
int Id;
List<long> Links;
}
....
struct * cell_p = (struct*) GetBlobPtr();
int id = *cell_p.Id;
*cell_p.Links[0] = 100001;
这是因为上面示例中的链接等数据字段是引用数据字段。这样一个结构体的数据域病没有被平铺在内存中。我们不能使用结构化数据指针操作平面内存区域。
我们还可以将值组件视为二进制数,并通过高级语言声明和访问数据。在第三种方法中,我们通过声明行语言(如SQL)定义和操作数据。然而,通常情况下,声明性语言要么表达能力非常有限,要么没有有效的实现。但对GE来说,表达能力和效率极其重要。
GE将用户数据存储为二进制数而不是运行时对象,这样可以最小化存储开销。同时,GE使我们能够以面向对象的方式访问数据,就像我们在c#或java中所做的那样。例如,在GE中,我们可以执行以下操作,即使我们操作的数据是一个二进制数。
CellA cell = new CellA();
int Id = cell.Id;
cell.Links[0] = 1000001;
换句话说,我们仍然可以以一种优雅的、面向对象的方式操作二进制数。GE通过单元访问机制实现了这一点。具体来说,我们首先使用TSL脚本声明数据模式。GE编译脚本并为TSL脚本中定义的单元结构生成单元访问器。然后,我们可以通过单元访问器访问二进制数据,就好像数据是运行时c#对象一样,但实际上,是单元访问器将单元结构中声明的字段映射到正确的内存位置。所有数据访问操作将被正确映射到正确的内存位置,而不会产生任何内存复制开销。
让我们用一个例子来演示单元访问器是如何工作的。要使用单元访问器,我们必须首先指定其单元结构。这是使用TSL完成的。对于前面的例子,我们定义TSL中的数据结构如下:
cell struct CellA
{
int Id;
List<long> Links;
}
注意CellA不是c#中的struct定义,尽管它看起来很相似。这个代码片段将由TSL编译器编译成CellA_Accessor。编译器生成用于将CellA字段上的操作转换为底层二进制数上的内存操作的代码。编译后,我们可以使用面向对象的数据访问接口访问二进制中的数据,如下图所示。
除了直观的数据操作接口外,单元访问器还提供了线程安全的数据操作保证。GE被设计成在一个高度多线程的环境中运行,在这个环境中,大量的单元以非常复杂的模式相互作用。为了简化应用程序开发人员的工作,GE通过单元访问提供了线程安全的单元操作接口。
用法:
在使用访问器时,必须应用一些使用规则。
访问器无法缓存:
访问器工作起来就像一个数据指针,由于访问器指向的内存块可能会被其他访问器操作移动,因此无法缓存以供将来使用。
例如,如果我们有一个被定义为:
cell struct MyCell
{
List<string> list;
}
TrinityConfig.CurrentRunningMode = RunningMode.Embedded;
Global.LocalStorage.SaveMyCell(0, new List<string> { "aaa", "bbb", "ccc", "ddd"});
using (var cell = Global.LocalStorage.UseMyCell(0))
{
Console.WriteLine("Example of non-cached accessors:");
IEnumerable<StringAccessor> enumerable_accessor_collection = cell.list.Where(element => element.Length >= 3);
foreach(var accessor in enumerable_accessor_collection)
{
Console.WriteLine(accessor);
}
Console.WriteLine("Example of cached accessors:");
List<StringAccessor> cached_accessor_list = cell.list.Where(element => element.Length >= 3).ToList();
// Note the ToList() at the end
foreach (var accessor in cached_accessor_list)
{
Console.WriteLine(accessor);
}
}
上面显示的代码片段将输出:
Example of non-cached accessors:
aaa
bbb
ccc
ddd
Example of cached accessors:
ddd
ddd
ddd
ddd
对于非缓存访问器的示例,将在使用访问器时计算访问器的值。对于缓存访问器的示例,由cel.list返回的访问器列表。(=>元素。Length>=3).ToList()实际上只是指向同一个访问器的引用,这个访问器指向cell.list的最后一个元素。
单元格访问器和消息访问器在使用后必须进行处理:
单元格访问器是一次性对象。单元格访问器使用后必须进行处理。在c#中,一次性对象可以 通过使用构造来处理。
long cellId = 314;
using (var myAccessor = Global.LocalStorage.UseMyCell(cellId))
{
// Do something with myAccessor
}
单元格访问器或消息访问器必须正确处理。如果访问器在使用后没有被处理,就会发生非托管资源泄露。
TSL协议的请求/响应阅读器和写入器成为消息访问器。他们也是一次性物品。使用后必须妥善处理。
using (var request = new MyRequestWriter(...))
{
using (var response = Global.CloudStorage.MyProtocolToMyServer(0, request))
{
// Do something with the response
}
}
单元格访问器不能以嵌套方式使用
每个单元格访问器都有一个自旋锁。单元格访问器的嵌套使用可能导致死锁。下面代码片段中显示的单元格访问器使用不当。
using (var accessorA = Global.LocalStorage.UseMyCellA(cellIdA))
{
using(var accessorB = Global.LocalStorage.UseMyCellB(cellIdB))
{
// Nested cell accessors may cause deadlocks
}
}
单元格访问器不应嵌套在另一个单元格中。两个或多个嵌套单元格访问器可能导致死锁。
注意,单元格访问器和一个或多个请求/响应阅读器/写入器可以以嵌套的方式使用。