1.概念
1.1 XID <-> UID
所有实体会被分配一个唯一的64位整型id,即UID。如果某个尸体有一个外部id(external id),即XID,DGraph会取出XID的指纹,并保存对应的UID。如果某个实体没有XID,DGraph只会添加一个新的UID,并标明这个UID已用。所有的posting list都通过UID引用实体。由于UID是8字节的整数,这种数据表示非常高效。在数据导入的时候,我们需要每个唯一的实体有一个唯一的UID。
1.2 Edges
典型的数据格式是RDF NQuad:
- 主语(Subject), 谓语(Predicate), 宾语(Object), 标签(Label), aka
- 实体(Entity), 属性(Attribute), 另一个实体或值(Other Entity / Value),标签(Label)
两种专门用语在Dgraph的代码里可以交替使用。Dgraph的edge是有向的,例如Subject -> Object
。这也是查询执行的方向
可以自动生成一个反向的边。如果用户想按相反的方向执行查询,需要将reverse edge定义为schema的一部分
在Dgraph内部,RDF NQuad倍解析为下面的格式:
type DirectedEdge struct {
Entity uint64
Attr string
Value []byte
ValueType uint32
ValueId uint64
Label string
Lang string
Op DirectedEdge_Op // Set or Delete
Facets []*facetsp.Facet
}
不考虑输入的话,Entity以及 Object/ValueId都通过上面的XID <-> UID被转化成UID格式。
1.3 Posting List
概念上,posting list包含与一个Attribute关联的所有有向边,通过如下格式:
Attribute: Entity -> sorted list of ValueId
//Everything in uint64 representation.
例如,如果我们存储一个朋友的列表,例如:
Entity | Attribute | ValueId |
---|---|---|
Me | friend | person0 |
Me | friend | person1 |
Me | friend | person2 |
Me | friend | person3 |
那么,将生成一个friend
的posting list。在这个PL中查找Me会生成一个friend的列表,即[person0, person1, person2, person3]
拥有这个结构的一个很大优势是可以把需要做一个join的所有数据保存在一个PL中。这意味着,一个到持有PL的服务器RPC调用的结果是一个连接,而不需要更多的网络调用,减少查询需要的连接
顾名思义,PL是Posting的列表,用Protocol Buffers表示的话,它是这个样子:
message Posting {
fixed64 uid = 1;
bytes value = 2;
enum ValType {
DEFAULT = 0;
BINARY = 1;
INT = 2; // We treat it as int64.
FLOAT = 3;
BOOL = 4;
DATE = 5;
DATETIME = 6;
GEO = 7;
UID = 8;
PASSWORD = 9;
STRING = 10;
}
ValType val_type = 3;
enum PostingType {
REF=0; // UID
VALUE=1; // simple, plain value
VALUE_LANG=2; // value with specified language
// VALUE_TIMESERIES=3; // value from timeseries, with specified timestamp
}
PostingType posting_type = 4;
bytes metadata = 5; // for VALUE_LANG: Language, for VALUE_TIMESERIES: timestamp, etc..
string label = 6;
uint64 commit = 7; // More inclination towards smaller values.
repeated facetsp.Facet facets = 8;
// TODO: op is only used temporarily. See if we can remove it from here.
uint32 op = 12;
}
message PostingList {
repeated Posting postings = 1;
bytes checksum = 2;
uint64 commit = 3; // More inclination towards smaller values.
}
Dgraph中的所有数据都会首先使用Protocol Buffers序列化为字节数组后被存储或传输。当结果被返回给用户的时候,protocol buffer对象会被转换为JSON对象
在一个PL中通常有超过一个的Posting
RDF标签在每个posting中用label
表示
现在Dgraph还不能通过查询获得label,但是将来会可以的
1.4 Badger
PostingList是通过Badger提供服务的,BadgerDB是一个嵌入式的、持久化的、简单的快速键值数据库,它使用纯Go语言开发.BadgerDB决定应该从内存、SSD或磁盘上提供多少数据。此外,它还支持在key上使用布隆过滤器,使随机查询的效率更高
为了让Badger对内存有完全的访问权限,以优化cache,每台服务器上都有一个Badger。每个实例包含这台服务器上的所有posting list
Posting list在Badger上以键值对的格式存储,例如:
(Predicate, Subject) --> PostingList
1.5 Group
包含同一个谓语(Predicate)的那些Posting list组成了一个group。每台服务器可以存储多个不同的group。
Group的配置文件用于确定每台服务器应该保存哪些group。在将来的版本中,在线的Dgraph服务器将可以使用探试法(heuristics)移动标签(tablets)
如果某个group变的太大,它可以被分割。在这种情况下,一个谓语实际上被分到两个group上,像下面这样:
Original Group:
(Predicate, Sa..z)
After split:
Group 1: (Predicate, Sa..i)
Group 2: (Predicate, Sj..z)
注意,这些key是被存储在RocksDB里的,因此group的split将会保持排序。例如,按字段顺序排序,在主语(subject)之前的被分到一个分组里,在主语之后的分到另一个分组里
1.6 复制与服务器故障
如果可能的话,每个group至少需要被三个服务器保存。当某台服务器故障时,其他保存同一个group数据的服务器可以继续提供服务
1.7 新服务器及发现
Dgraph集群可以自动检测加入到集群中的新服务器,建立连接,并将新服务器应该保存的group数据发过去
1.8 预写日志
对数据库的写入不会立马通过RocksDB写到磁盘上,而是先写到磁盘上的预写日志(Write ahead logs)里,这样可以避免posting list的经常重建
1.9 修改
除了被写入预写日志之外,对数据库的修改还会被存储在内存中,作为不可变的Posting list之上的一个可变层。它允许用户遍历Postings,仿佛它们是被排过序的,而无需重建posting list
当一个posting list在内存中有被修改后,它被视为dirty
的。Dgraph会周期性地重新生成不可变的版本,并将改变写入RocksDB。注意,写入RocksDB的过程是异步的,这意味着它们并不会立刻被刷写到磁盘,但在服务器宕机的时候,这可能会导致数据丢失。但是不用担心,在Posting list被初始化的时候,会读取预写日志,这时丢失的数据会被补上
每当重新生成posting list的时候,Dgraph还会写一个包含最后一次提交日志的时间戳,用来决定在初始化posting list的时候从预写日志回溯多久的数据。(Every time we regenerate a posting list, we also write the max commit log timestamp that was included – this helps us figure out how long back to seek in write-ahead logs when initializing the posting list, the first time it’s brought back into memory.)
1.10 事务
Dgraph现在还不支持事务。
0.9版本已经支持事务
With the availability of transactions, a user can query for data, and then write their mods back atomically. Thus, upsert and mutation variables can be done via client-side logic, instead of baking this limiting functionality in the server.
一个修改可以由多条边组成,每条边可能属于不同的Posting list。Dgraph需要RWMutex
来提供posting list上的锁。而在多个posting list上不存在锁
这意味着,一些边会比其他边先写入,而这时候读的话,只能读到部分被提交的数据。但是,还是有一致性保证的,当一个mutation成功的时候,任何成功的读操作都可以读到所有被更新的数据
如果一个mutation失败了,由用户决定是擦出部分写入的数据,还是用正确的逻辑重写。Dgraph的回复会说清楚哪些边没有被写入成功,用户可以设置正确的边来重新写入
局限性:
- 你应该考虑好是否你的读操作需要原子性的事务。除非你需要处理财务数据,这不是Dgraph的适用场景
- 为了保证原子性,每个mutation只能有一条边(RDF NQuads) 。这意味需要在clent及server之间反复地执行多次网络调用,这将影响你的写入吞吐量,但是会使错误处理的逻辑变得更简单
1.11版本
大体上来说,Dgraph存储有两种类型的数据,一种是关系(relationship)数据,一种是值
Me friend person0 [Relation]
Me name "Константи́н" [Value]
DGraph保存Posting list的方式,
网友评论