3

RocksDB RateLimiter解析与实践

 2 years ago
source link: https://zhuanlan.zhihu.com/p/398977228
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

RocksDB RateLimiter解析与实践

Pegasus的底层采用基于LSMTree的RockDB做存储引擎,当满足一定条件后,将会自动触发Compaction操作。然而Compaction往往具有较大的磁盘读写开销,尤其当业务执行了大批量的数据灌入操作之后,Compaction的磁盘负载甚至会达到100%,这极大的影响了在线业务的请求耗时。本文首先分析了Compaction的实现原理,随后给出在Pegasus上工程实践和带来的收益,希望对读者有所帮助。

RocksDB的速率限制器的有五个的参数:

  • rate_bytes_per_sec:数据写入的总速率,它包括compaction和flush的速率
  • refill_period_us:token被更新的周期,默认为100ms;
  • fairness:低优先级请求(compaction)相较高优先级请求(flush)获取token的概率。默认为10%;
  • mode:限速的类型,读、写、读写,默认是对写请求进行限速;
  • auto_tuned:是否开启auto_tune,默认为不开启

我们从rate_bytes_per_sec的定义可以知道:RateLimiter的限速其实并不仅仅针对compaction速率,还包括数据的flush操作。那么RateLimiter是怎么决定限制那种数据的写入呢?这就是参数fairness的作用:控制流量的分配优先级。其默认值10指代的意思是,低优先级请求(compaction)获取token的概率为1/10,也就是RateLimiter优先使得flush操作获取相应的Token,换句话说也就是:RateLimiter默认优先限制compaction的速率,而这正是我们所需要的(事实上,官方也建议直接使用默认值即可),接下来我们从三个方面来看一下RateLimiter是如何实现的。相关的源码可以参考RocksDB#RateLimiter

基本限速原理

RateLimiter的限速原理本质上也是令牌桶的实现,每个周期可以获得的token流量大小计算方式为:

token = rate_bytes_per_sec * (refill_period_us / 1000) ms / 1000ms 

当请求到来时,会首先比较当前请求的流量大小和此时已经允许分配的流量大小available_bytes_:

  • 若小于available_bytes_,则直接“放行”,同时:
 available_bytes_  = available_bytes_ - request_bytes 
  • 若大于available_bytes_,则继续等待下一个token更新周期。

流量分配选主

在等待的过程中,RateLimiter会分别把compaction请求放入低优先级队列(queue_[Env::IO_LOW]),把flush请求放入高优先级队列(queue_[Env::IO_HIGH])。两个队列中的请求会通过竞争获取唯一的分配流量的权利,即选主(leader_),leader_的选取结果可能有以下两种情况:

  • 若当前leader_为空:高优先级队列不为空,则直接选取高优先级队列的头部请求为leader_,否则选取低优先队列的头部请求为leader_
  • 若当前leader_不为空:上一个leader_由于没被分配足够的流量而留在队列中,则选取的结果就是上一个leader_,该次请求就直接等待这个leader_分配流量。

选出来的leader_会根据当前等待的周期数量更新num_drains_,待该周期等待结束,开始进行refill操作。refill操作首先会更新当前的available_bytes_,随后按照设置的fireness,概率性地把流量分配给某个队列,如果该leader_存在被选中的队列中,则分配流量后并不直接返回,而是继续分配流量给队列中的其他请求——即唤醒该请求使之成功执行并返回。每次分配流量都会更新当前可允许通过流量:

available_bytes_ = available_bytes_- request_bytes 

同时把该请求从队列中移除。如果available_bytes_ 被耗尽或者队列的请求被分配完,则该次refill流程结束,由于被选中成为leader_请求也会成功执行并返回,此时队列中不再有leader_,需要按照上述选取规则重新选取新的leader。

速率动态调整

除了有fairness控制流量分配的优先级,还有auto_tuned控制速率的动态调整。默认情况下,该值为false,则rate_bytes_per_sec就是RateLimiter的可通行速率,开启该功能后(auto_tuned=true)则rate_bytes_per_sec就变成了RateLimiter可通行的最大速率:

max_ rate_bytes_per_sec_ = rate_bytes_per_sec_ 
rate_bytes_per_sec_ = [rate_bytes_per_sec_/20, rate_bytes_per_sec_] 

同时初始可通行速率:

rate_bytes_per_sec_ = rate_bytes_per_sec_ / 2 

速率每100个refill周期调整一次(若refill周期为100ms,即每10s调整一次),调整的规则是根据这100个refill周期中因rate_bytes_per_sec_耗尽而等待的次数(即上文中的num_drains_)的比率

drained_pct = δnum_drains_ / 100 

来增减可通行速率:

  • 如果该值小于kLowWatermarkPct=50,则速率降低到1.05倍:
 int64_t sanitized_prev_bytes_per_sec = 
        std::min(prev_bytes_per_sec, port::kMaxInt64 / 100); 
    new_bytes_per_sec = 
        std::max(max_bytes_per_sec_ / 20, 
                 sanitized_prev_bytes_per_sec/1.05) 
  • 如果该值大于kHighWatermarkPct=90,则速率升高到1.05倍:
int64_t sanitized_prev_bytes_per_sec = 
        std::min(prev_bytes_per_sec, port::kMaxInt64 / 100); 
    new_bytes_per_sec = 
        std::max(max_bytes_per_sec_ / 20, 
                 sanitized_prev_bytes_per_sec * 1.05); 
  • 如果在50~90之间则速率保持不变。

限速支持

在Pegasus中添加Rocksdb的限速支持非常简单,只需要在Pegasus初始化的时候为rocksdb的options分配相应的RateLimiter实例:

static std::once_flag flag; 
std::call_once(flag, [&]() { 
     _s_rate_limiter = std::shared_ptr<rocksdb::RateLimiter>(rocksdb::NewGenericRateLimiter( 
      FLAGS_rocksdb_limiter_max_write_megabytes_per_sec << 20, 
      100 * 1000, // refill_period_us 
      10,         // fairness 
      rocksdb::RateLimiter::Mode::kWritesOnly, 
      FLAGS_rocksdb_limiter_enable_auto_tune)); 
        }); 
      _db_opts.rate_limiter = _s_rate_limiter; 

需要注意的是,Pegasus的每个replica_server拥有多个rocksdb实例(即Pegasus中的分片),由于我们是控制单节点的compaction速率,所以每个rocksdb实例的options共用一个RateLimiter实例即可。除此之外,Pegasus还添加了RateLimiter的速率监控,以方便查看当前的速率变化

参数调优

由于大流量的compaction往往发生在持续的数据写入(灌输据)过程,所以我们模拟了灌输据的场景来观察速率限制器对写请求的影响,测试环境为5台replica_server,3台YCSB客户端,单条value大小为1KB。

  • 无限速:3client×20线程,QPS=43097

我们观察到,在无限速的场景下,磁盘IO在某些时间段的使用率较高甚至达到100%,直接影响到了写延迟的尖峰现象。

  • 500MB限速,未开启auto-tune:3client×20线程,QPS=45121

开启限速后,我们观察到,由于磁盘IO毛刺现象的减少,写延迟也比较稳定,最终QPS也提升了5%左右

  • 500MB限速,开启auto-tune:3client×20,QPS= 44059

开启500MB限速同时开启auto-tune后,我们发现磁盘IO的毛刺现象更少了,与之对应的写延迟的尖峰现象也被大大减少。但是需要注意的是,由于开启auto-tune之后,每次的调整后的可通行速率如果过小,而数据写入吞吐过高,可能会产生write stall的现象,这个在我们单条value=10KB,客户端为3client×10线程的测试中也得到了验证,所以需要评估当前集群是否合适开启auto-tune,同时建议对开启auto-tune的集群设置更大的限速阈值,以避免出现write stall现象。

compaction速率过高导致的延迟抖动现象可以通过RateLimiter得到相应的优化,我们近期已经进一步测试出compaction对写的影响更多的集中在sharedLog的日志写入过程,我们后续会针对compaction对日志写的影响展开调研,以进一步优化compaction对线上业务的影响。

参考文档:

https://github.com/facebook/rocksdb/wiki/Rate-Limiter

https://blog.51cto.com/u_14621185/245543


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK