腾讯时序数据库之准实时揭秘
一个理想的查询过程中,新增加或者修改的数据应该能立即被查询到。腾讯时序数据库CTSDB给人的第一印象好像就是如此工作的,而事实上并非如此。那它实际情况是怎么样的呢?
在进行说明之前先大概介绍一下CTSDB处理请求的流程。
在CTSDB和磁盘之间有一层FileSystem Cache的系统缓存,以使得能够更快地处理搜索请求。插入请求到来时document会先被放入到indexing buffer,然后被重写为一个segment直接写入到filesystem cache,这个操作是非常轻量级的,相对耗时较少,之后经过一定的间隔或外部触发后才会被flush到磁盘上,这个操作非常耗时。但只要sengment文件被写入cache后就可以被打开和查询,在短时间内就可以搜到,而不用执行一个flush也就是fsync操作。其请求处理流程如下图:
下面通过一个案例来验证进行观察分析。
新增加一条数据到新创建的索引中。
curl -u root:xxxxxx -XPOST 127.0.0.1:9200/test/test/1 -d '
{
"title":"test"
}'
为了进行验证,我们修改这条数据,并尝试立即查询它,连续执行下面两行命令。
curl -u root:xxxxxx -XPOST 127.0.0.1:9200/test/test/1 -d '
{
"title":"test2"
}'
curl -u root:xxxxxx -XGET 127.0.0.1:9200/test/test/_search -d '
{
"docvalue_fields":[
"title"
]
}'
前面两行命令返回结果如下:
{
"_index":"test@1555344000000_30",
"_type":"test",
"_id":"1",
"_version":2,
"result":"updated",
"_shards":{
"total":2,
"successful":2,
"failed":0
},
"created":false
}
{
"took":0,
"timed_out":false,
"_shards":{
"total":3,
"successful":3,
"skipped":0,
"failed":0
},
"hits":{
"total":1,
"max_score":1,
"hits":[
{
"_index":"test@1555344000000_30",
"_type":"test",
"_id":"1",
"_score":1,
"_source":{
"title":"test",
"timestamp":1556299203460
},
"fields":{
"title":[
"test"
]
}
}
]
}
}
我们可以看到第1条命令修改数据成功了,返回结果version字段为2,一切正常。但是第2条命令返回的title字段应该返回test2的,但是还是返回修改之前的数据test。
由于CTSDB底层是基于ElasticSearch的,而ElasticSearch的索引是基于Apache Lucene索引的,那我们先来看看Lucene的内部机制,Lucene是如何让新索引的文档在搜索时可用?
索引更新及更新提交
索引新文档时会被写入索引段。不时会有新增的索引段被添加到可被搜索的索引段集合中,Lucene通过创建后续的(基于索引只写一次的特性)segments_N文件来实现此功能。这个过程被称为提交,Lucene会以安全、原子的操作来提交保证数据一致性。
我们回来之前的案例中,尽管第1个命令修改了文档,但它还没有执行提交操作。然而执行了提交操作也不能保证能被搜索到,因为Lucene使用一个叫Searcher的抽象类来执行索引的读取。该类需要被刷新,如果索引更新提交了,但Searcher实例没有重新打开(刷新),那么Searcher察觉不到有新索引段的加入。对于Searcher的刷新间隔时间可以通过refresh_interval来进行设置。
同时在ElasticSearch中提供了refresh端点进行强制Searcher刷新。我们可以在上面两行命令中间加入强制刷新API再来验证。
curl -u root:xxxxxx -XPOST 127.0.0.1:9200/test/test/1 -d '
{
"title":"test3"
}'
curl -u root:xxxxxx -XGET 127.0.0.1:9200/test@1555344000000_30/_refresh
curl -u root:xxxxxx -XGET 127.0.0.1:9200/test/test/_search -d '
{
"docvalue_fields":[
"title"
]
}'
上面三行命令返回结果如下:
{
"_index":"test@1555344000000_30",
"_type":"test",
"_id":"1",
"_version":3,
"result":"updated",
"_shards":{
"total":2,
"successful":2,
"failed":0
},
"created":false
}
{
"_shards":{
"total":6,
"successful":6,
"failed":0
}
}
{
"took":0,
"timed_out":false,
"_shards":{
"total":3,
"successful":3,
"skipped":0,
"failed":0
},
"hits":{
"total":1,
"max_score":1,
"hits":[
{
"_index":"test@1555344000000_30",
"_type":"test",
"_id":"1",
"_score":1,
"_source":{
"title":"test3",
"timestamp":"1556301337152"
},
"fields":{
"title":[
"test3"
]
}
}
]
}
}
可以看到第3条命令返回的title字段为test3,返回了修改之后的数据test3。
事务日志
在ElasticSearch的索引实现中Apache Lucene能保证索引的一致性,但这不能保证当向索引中写数据失败时不损失数据(例如,磁盘空间不足,设备异常)。另外一个问题是频繁提交(触发一个索引段的创建操作,同时也可能触发索引段的合并)会导致性能问题。ElasticSearch使用事务日志来解决这些问题,事务日志保存所有未提交的事务。当有错误发生时,事务日志会被检查,必要时再次执行某些操作,以确保没有丢失任何更改。事务日志中的信息与存储介质之间的同步被称为事务日志刷新。同样事务日志的刷新间隔可以通过index.translog.flush相关参数进行配置,也可以通过_flush端点进行强制事务日志刷新。
事务日志刷新flush用于保证数据写入磁盘并清空事务日志。Searcher刷新refresh用于搜索到最新的文档。
准实时读取
事务日志给ElasticSearch带来了一个特性:实时读取。实时读取从索引中读取数据时,会先检查事物日志中是否有可用的新版本(未提交版本),如果有就会返回事务日志中的最新版本的文档。
为了演示实时读取,连续执行下面两条语句,第2条语句查询时指定索引文档id来查询,会从事务日志中读取最新的数据。
curl -u root:xxxxxx -XPOST 127.0.0.1:9200/test/test/1 -d '
{
"title":"test4"
}'
curl -u root:xxxxxx -XGET 127.0.0.1:9200/test@1555344000000_30/test/1
上面2行数据返回结果如下:
{
"_index":"test@1555344000000_30",
"_type":"test",
"_id":"1",
"_version":4,
"result":"updated",
"_shards":{
"total":2,
"successful":2,
"failed":0
},
"created":false
}
{
"_index":"test@1555344000000_30",
"_type":"test",
"_id":"1",
"_version":4,
"found":true,
"_source":{
"title":"test4",
"timestamp":"1556305107154"
}
}
可以看到第2条命令返回的title字段为test4,返回了修改之后的数据test4。
这时我们并没有使用refresh刷新技巧就查询到了最新的文档。
总结
修改数据、search数据:不一定会查询到最新的数据。
修改数据、refresh强制刷新、search数据:会查询到最新的数据。
修改数据、指定文档id查询数据:会查询到最新的数据。
| 本文作者:游成松,腾讯云数据库后台开发,负责腾讯云数据库CTSDB产品的设计、研发、运维等工作。曾负责腾讯云数据库SQLServer、PostgreSQL、TDSQL、Tbase、CynosDB产品的研发工作。
文章转自公众号:腾讯云数据库