微信海量数据查询如何从1000ms降到100ms?( 三 )


 
每个子查询都会先尝试获取缓存中的数据 , 此时有两种结果:
结果
解析
缓存未命中
如果子查询结果在缓存中不存在 , 即 cache miss 。只需要将调用 DruidBorker 获取数据 , 异步写入缓存中 , 同时该子查询缓存的修改的时间即可 。
缓存命中
 
在谈论命中之前 , 首先引入一个概念「阈值时间(threshold_time)」 。它表示缓存更新前的一段时间(一般为10min) 。我们默认缓存中的数据是不被信任的 , 因为可能因为数据积压等情况导致一部分数据延迟入库 。
 
如果子查询命中了缓存 , 则存在两种情况:「缓存部分命中」和「缓存完全命中」 。其中部分命中如下图所示 。
 

  • 缓存部分被命中:
end_time > cache_update_time - threshold_time:这种情况说明了「缓存部分被命中」 , 从 cache_update_time-thresold_time 到 end_time 这段时间都不可信 , 这段不可信的数据需要从 DruidBroker 中查询 , 并且在获取到数据后异步回写缓存 , 更新 update 时间 。
 
微信海量数据查询如何从1000ms降到100ms?

文章插图
 
 
  • 缓存完全命中:
  • 而缓存完全命中则是种理想形式:end_time > cache_update_time - threshold_time 。这种情况说明了缓存被完全命中 , 缓存中的数据都可以被相信 , 这种情况下直接拿出来就可以了 。
  •  

微信海量数据查询如何从1000ms降到100ms?

文章插图
 
 
经过上述分析不难看出:对于距离现在超过一天的查询 , 只需要查询一次 , 之后就无需访问 DruidBroker 了 , 可以直接从缓存中获取 。
 
而对于一些实时热数据 , 其实只是查询了
cache_update_time-threshold_time 到 end_time 这一小段的时间 。在实际应用里 , 这段查询时间的跨度基本上在 20min 内 , 而 15min 内的数据由 Druid 实时节点提供 。
 
3.2.2 维度组合子查询设计 
维度枚举查询和时间序列查询不一样的是:每一分钟 , 每个维度的量都不一样 。而维度枚举拿到的是各个维度组合在任意时间的总量 , 因此基于上述时间序列的缓存方法无法使用 。在这里 , 核心思路依然是打散查询和缓存 。对此 , 微信团队使用了如下方案:
 
缓存的设计采用了多级冗余模式 , 即每天的数据会根据不同时间粒度:天级、4小时级、1 小时级存多份 , 从而适应各种粒度的查询 , 也同时尽量减少和 Redis 的 IO 次数 。
 
每个查询都会被分解为 N 个子查询 , 跨度不同时间 , 这个过程的粗略示意图如下:
微信海量数据查询如何从1000ms降到100ms?

文章插图
 
举个例子:例如 04-15 13:23 ~ 04-17 08:20 的查询 , 会被分解为以下 10 个子查询:
04-15 13:23 ~ 04-15 14:00
04-15 14:00 ~ 04-15 15:00
04-15 15:00 ~ 04-15 16:00
04-15 16:00 ~ 04-15 20:00
04-15 20:00 ~ 04-16 00:00
04-16 00:00 ~ 04-17 00:00
04-17 00:00 ~ 04-17 04:00
04-17 00:00 ~ 04-17 04:00
04-17 04:00 ~ 04-17 08:00
04-17 08:00 ~ 04-17 08:20
 
这里可以发现 , 查询 1 和查询 10 , 绝对不可能出现在缓存中 。因此这两个查询一定会被转发到 Druid 去进行 。2~9 查询 , 则是先尝试访问缓存 。如果缓存中不存在 , 才会访问 DruidBroker , 在完成一次访问后将数据异步回写到 Redis 中 。
 
维度枚举查询和时间序列一样 , 同时也用了 update_time 作为数据可信度的保障 。因为最细粒度为小时 , 在理想状况下一个时间跨越很长的请求 , 实际上访问 Druid 的最多只有跨越 2h 内的两个首尾部查询而已 。
 
3.3 更进一步-子维度表 
通过子查询缓存方案 , 我们已经限制了 I/O 次数 , 并且保障 90% 的请求都来自于缓存 。但是维度组合复杂的协议 , 即 Segments 过大的协议 , 仍然会消耗大量时间用于检索数据 。


推荐阅读