WiredTiger作为MongoDB的默认存储引擎,以B+树的方式来组织数据,其中BTree页内支持3种格式的数据,分别是RowStore,Fixed-Length ColumnStore,以及Variable-Length ColumnStore。这篇文章介绍一下WiredTiger的ColumnStore。主要关注下WiredTiger ColumnStore对外提供的接口,Btree内部数据的组织方式,以及数据的读写方式。
MongoDB的使用方式,用来存储时序数据。https://www.mongodb.com/docs/manual/core/timeseries-collections/
接口
WiredTiger在创建表的时候,可以指定表的Schema,包括声明每一列的类型,名称,以及创建二级索引等。和列存相关的则是Column Group。
这里以ex_col_store.c
中的代码为例:
session->create(session, "weather",
"key_format=r,value_format=HHHHBBBB5S5S,columns=(id,hour,pressure,loc_lat,"
"loc_long,temp,humidity,"
"wind,feels_like_temp,day,country),colgroups=(day_time,temperature,"
"humidity_pressure,"
"wind,feels_like_temp,location)")
上面这个创建table的含义是,创建一个名字为weather
的表,其中key的类型为r
,代表record id,value的类型为HHHHBBBB5S5S
,分别是后面这些column的具体类型。这里H
是uint16_t
,B
是uint8_t
,S
是Null结尾的string。
并且以ColumnGroup的方式来组织这些列,这里有6个Column Group
然后声明这些column group所包含的列
session->create(session, "colgroup:weather:day_time", "columns=(hour,day)")
这里表示的就是,在weather这个表中,day_time
这个column group包含的列为hour和day
除此之外,我们还可以创建二级索引,比如:
session->create(session, "index:weather:hour", "columns=(hour)")
这里就是创建了排序键为hour的二级索引,在搜索的时候,可以打开对应索引的cursor上来读取索引,从而加速某类查询。
在写入的时候,对于列存来说,不需要指定key,只指定value即可,value就是schema中定义的所有列,按照定义的顺序排布即可。
session->open_cursor(session, TABLE_NAME, NULL, "append", &cursor)
cursor->set_value(cursor, weather_data[i].hour, weather_data[i].pressure,
weather_data[i].loc_lat, weather_data[i].loc_long, weather_data[i].temp,
weather_data[i].humidity, weather_data[i].wind, weather_data[i].feels_like_temp,
weather_data[i].day, weather_data[i].country);
cursor->insert(cursor)
在读取的时候,我们在open cursor的时候默认读出来的数据是表中所有的列。
session->open_cursor(session, TABLE_NAME, NULL, NULL, &cursor)
cursor->get_key(cursor, &recno)
cursor->get_value(cursor, &hour, &pressure, &loc_lat, &loc_long, &temp,
&humidity, &wind, &feels_like_temp, &day, &country)
这时候没有利用列存的优势来做skip io,为了能够利用column group的优势,WiredTiger的cursor提供了Projection的能力
session->open_cursor(session, "table:weather(hour, day)", NULL, NULL, &cursor);
cursor->get_value(cursor, &hour, &day)
这样在读取的时候,WiredTiger就会选择上面创建的day_time
这个column group,读取的时候也就不需要读其他的列了。
上面这些就是WiredTiger中和Schema相关的概念,即Column
,Column Group
,Index
以及Projection
WiredTiger对于变长的ColumnStore,还支持通过RLE进行压缩,来减少空间占用。
数据组织
WiredTiger的每一个ColumnGroup都对应了一个文件,也就是每一个ColumnGroup都对应一个Btree,其Eviction/Reconcile的逻辑和之前讲的RowStore Btree没有区别。
同一个ColumnGroup的列会被编码成一个string,以key为record id,value为string的形式存储到Btree中
WiredTiger的定长的ColumnStore最多只支持8bit的value,存储方式是把所有数据都存储到bitmap中,比较特化,这篇文章就不细关注他了。
对于变长的ColumnStore来说,有一点比较特殊的是,存储的每一行数据的record id都是连续递增的。比如有一个record id为10的数据,还有一个record id为100的数据,那么11到99也一定会被存下来,不过如果这些数据是空的话,会被RLE压缩掉。
之所以这样存,是因为这样可以减少Btree Key的存储,只存Value。在ColumnStore的Page中,每个Page都只会存这个page所对应的start record id,即所有包含的record id中最小的那个,剩下的数据的record id则通过offset来计算得到。
Page内的组织结构如图所示:
- 每一个不同的Value都会作为一个Slot出现
-
对于重复多次的Value,会有一个RLE Entry来维护他的数量
-
通过RLE Entry中的信息,以及Slot的offset,可以计算出某一个Slot所对应的是那些record id的value。
-
在更新的时候,通过上面的方法计算出当前需要更新的record id属于那个Slot。每一个Slot都对应一个Skiplist来存储更新,找到skiplist中的版本链,并追加新版本。
-
Page上有个特殊的append Skiplist用来存储越界的更新。比如上述例子中,Btree page负责的范围是10到正无穷,但是因为目前最大的value的record id是27,所以对于大于27的元素的更新,会保存到最右边的skiplist中。
-
部分情况下用户不指定record id,是WiredTiger负责分配,并将新的写入追加到Btree中。此时就会在最右边的skiplist中上锁分配record id,并插入数据。
-
读取的时候会遍历每个Slot,先看对应的skiplist中是否有版本链,如果有则读版本链,没有则读slot上的value,也就是磁盘上的值。直到最后遍历完append skiplist,就会切换到下一个Page。
-
对于RLE来说会有一些buffer的逻辑,来保证RLE的优化不会因为代码链路的开销而被削弱。这里WiredTiger做的就是如果发现当前Slot为RLE Cell,并且满足一定条件后(得保证RLE都是globally visible的,否则随便复用可能导致破坏快照),会保存这个value,如果下次定位到还是这个Cell,就会直接复用上一次的value,避免重新读盘。
- 在Reconcile的时候,会根据可见性判断规则把需要保存到磁盘上的版本拿出来,然后做RLE编码,得到新的磁盘上的数据,然后再基于此来重建内存中的Page。
文章评论