当前位置: 首页 > 科技观察

分布式存储在哔哩哔哩

时间:2023-03-14 17:03:01 科技观察

业务的应用实践发展迅速。哔哩哔哩的存储系统将如何演进以支撑指数级增长的流量高峰?随着流量的进一步激增,如何设计稳定、可靠、易扩展的系统来满足未来进一步增长的业务需求?同时,面对更高的可用性需求,KV如何通过远程多活为应用提供更高的可用性保障?文末将介绍一些典型业务在KV存储上的应用实践。全文将重点关注以下4点:存储演进设计实现场景&问题总结思考01存储演进首先介绍哔哩哔哩早期的存储演进。针对不同的场景,早期的KV存储包括Redix/Memcache、Redis+MySQL、HBASE。但是随着B站数据量的快速增长,这种存储选型会面临一些问题:首先,MySQL是单机存储,部分场景数据量已经超过10T,无法存储在一台机器上。单机。当时我们也考虑过使用TiDB。TiDB是关系型数据库,不适合播放历史等没有强关系的数据。其次是RedisCluster的规模瓶颈,因为redis使用Gossip协议进行通信和传递信息。集群规模越大,节点之间的通信开销就越大,节点之间状态不一致的持续时间也会越长,就很难再横向扩展。此外,HBase存在长尾严重、缓存内存成本高等问题。基于这些问题,我们对KV存储提出了如下要求:易扩展:100x水平扩展;高性能:低延迟,高QPS;高可用:长尾稳定性,故障自愈;低成本:比较缓存;可靠性高:数据不丢失。02设计与实现下面介绍下我们是如何根据上述需求实现的。1.总体架构总体架构分为三个部分:Client、Node、Metaserver。Client是用户接入终端,决定了用户的接入方式,用户可以使用SDK接入。Metaserver主要存储表的元数据信息,表分为哪些分片,这些分片位于哪些节点上。用户在读写时只需要put和get方法,不需要关注分布式实现的技术细节。Node的核心点是Replica。每个表都会有多个分片,每个分片会有多个Replica副本。通过Raft实现副本之间的同步复制,保证高可用。2.集群拓扑Pool:资源池。根据不同的业务分工,分为线上资源池和线下资源池。Zone:可用区。主要用于故障隔离,保证每个分片的副本分布在不同的zone中。Node:存储节点,可以包含多个磁盘,存储Replicas。Shard:当一个表的数据量太大时,可以拆分成多个shard。拆分策略包括Range和Hash。3、Metaserver资源管理:主要记录集群的资源信息,包括有哪些资源池,可用区,有多少个节点。在创建表时,每个分片都会记录这样一个映射关系。元数据分布:记录分片位于哪个节点。健康检查:注册所有节点信息,检查当前节点是否正常,是否有磁盘损坏。基于这些信息,可以实现故障自愈。负载检测:记录磁盘使用率、CPU使用率、内存使用率。负载均衡:设置阈值,当达到阈值时,会重新分配数据。拆分管理:当数据量增加时,横向扩展。Raftmaster选举:当一个Metaserver挂掉时,可以进行自愈。RocksDB:元数据信息的持久化存储。4、Node作为存储模块,主要包括后台线程、RPC访问、抽象引擎层三部分。①后台线程Binlog管理。当用户执行写操作时,会记录一个binlog日志。当发生故障时,可以恢复数据。由于本地存储空间有限,Binlog管理会将一些冷数据存储在S3中,而将热门数据存储在本地。数据恢复功能主要用于防止数据被误删除。当用户执行删除操作时,实际上并没有删除数据。通常设置一个时间,比如一天,一天后数据会被回收。如果数据被误删除,可以使用数据恢复模块来恢复数据。健康检查检查节点的健康状态,如磁盘信息、内存等,并上报给Metaserver。Compaction模块主要用于数据恢复管理。存储引擎Rocksdb是用LSM实现的,其特点是采用appendonly的形式编写。RPC接入:当集群达到一定规模后,如果没有自动化运维,人工运维成本非常高。因此在RPC模块中加入了指标监控,包括QPS、吞吐量、延迟时间等,出现问题时,排查起来会非常方便。不同服务的吞吐量不同,如何实现多用户隔离?通过配额管理,业务接入时会申请一个配额。比如某表申请了10KQPS。当超过这个值时,用户的流量将被限制。不同的业务层级会进行不同的配额管理。②抽象引擎层主要是处理不同的业务场景。比如大值引擎,因为LSM存在写放大的问题,如果数据的值特别大,频繁写入会导致数据的有效写入很低。这些不同的引擎对上层是透明的,可以在运行时通过选择不同的参数来选择。5.Split-metadataupdate存储KV时,一开始会根据业务规模划分不同的shard。默认情况下,单个分片的大小为24G。随着业务数据量的增长,如果单个分片的数据放不下,就会进行数据拆分。有两种拆分方式,rang和hash。这里我们以hash为例介绍一下:假设一张表最初设计了3个shard,当数据4到达时,剩下的要根据hash保存在shard1中。随着数据的增长,如果3个shard放不下,就需要拆分,3个shard拆分成6个shard。此时访问数据4时,会根据Hash分配给shard4。如果shard4处于分裂状态,Metaserver会把访问重定向到原来的shard1,当分片完成,状态变为normal时,可以正常接收访问,用户并不知道这个过程。6.拆分——数据均衡回收首先需要对数据进行拆分,可以理解为做一个本地的checkpoint。Rocksdb的checkpoint相当于做了一个硬链接。通常,数据拆分可以在1ms内完成。拆分完成后,Metaserver会同步更新元数据信息,比如0-100的数据。split之后,fragment1中50-100的数据其实是不需要的,可以通过CompactionFilter回收数据。最后将拆分后的数据分配给不同的节点。因为整个过程是对一批数据进行操作,而不是像redis那样主从复制时一个一个复制,得益于这种实现,整个拆分过程是毫秒级别的。7.多活容灾上面提到的split和Metaserver保证高可用,对于某些场景还是不能满足需求。比如整个机房的集群挂了,业界多采用multi-active来解决。我们KV存储的多活也是基于Binlog实现的。比如在云立方的机房写了一条数据,会通过Binlog同步到嘉定的机房。如果嘉定机房存储部分宕机,代理模块会自动将流量切换到云立方机房进行读写操作。在最极端的情况下,如果整个机房都宕机了,所有的用户访问都会集中在一个机房来保证可用性。03Scenarios&Problems接下来介绍B站KV应用的典型场景和遇到的问题。最典型的场景就是用户画像,比如推荐,就是通过用户画像来完成的。其他的比如动态、追番、对象存储、弹幕等都是通过KV存储的。1.自定义优化基于抽象实现,可以方便的支持不同的业务场景,优化一些特定的业务场景。Bulkload全量导入的场景主要用于动态推荐和用户画像。用户画像主要是T+1数据。在没有使用Bulkload之前,主要是通过Hive一个一个的写。数据链很长。每天导入全量10亿条数据,大约需要6到7个小时。使用Bulkload后,只需要在hive离线平台将数据构建成一个rocksdb引擎,然后将数据上传到hive离线平台的对象存储即可。上传完成后通知KV拉取。拉取完成后,可以进行本地Bulkload,时间可以缩短到10分钟以内。另一种情况是固定长度的列表。你可能会发现你的播放历史只有3000条,你的动态也只有3000条。因为历史记录非常庞大,不可能无限存储。起初,使用脚本删除历史数据。为了解决这个问题,我们开发了一个自定义引擎来保存一个定长列表。用户只需要在其中写入。当长度超过固定长度时,引擎会自动删除。2.面临的问题——存储引擎前面提到的compaction在实际使用过程中遇到了一些问题,主要是存储引擎和raft。存储引擎主要是Rocksdb的问题。首先是数据淘汰。写入数据时,将通过不同的压缩将其向下推送。我们的播放历史会设置一个过期时间。过期时间后,假设数据当前位于L3层,当L3层未满时,不会触发compaction,数据不会被删除。为了解决这个问题,我们设置了一个regularcompaction。在压缩过程中,我们返回检查密钥是否已过期。如果过期,数据将被删除。还有一个问题是DEL导致SCAN查询慢。因为LSM在删除的时候需要一个一个扫描,所以key比较多。比如删除20到40之间的key,但是LSM在删除数据的时候,并不会真正进行物理删除,而是将其标记为delete。如果删除后做SCAN,会读到很多脏数据,需要过滤掉这些脏数据。当删除很多的时候,SCAN会很慢。为了解决这个问题,主要采用两种方案。首先是设置删除阈值。当超过阈值时,会强制触发Compaction,删除delete标记的数据。但是这样也会造成写放大的问题。比如删除L1层的数据,删除数据时会触发一次compaction,L1文件会带上一整层L2文件进行compaction,这样会带来非常大的写放大问题.为了解决写放大的问题,我们加入了延迟删除。SCAN时会统计一个指标,记录当前删除的数据占所有数据的比例,并根据这个反馈值触发compaction。三是大值写入放大的问题。目前业界的解决方案都是通过KV存储分离来实现的。我们也这样做了。3.面临的问题——RaftRaft层面有两个问题:第一,我们的Raft有三份。如果一个副本发生故障,另外两个副本可以提供服务。但在极端情况下,一半以上的副本挂掉了。虽然概率很低,但我们还是会做一些操作来缩短故障发生时的系统恢复时间。我们采用的方法是降级副本。比如三个副本中有两个挂掉了,后台的一个脚本会自动将集群降级为单副本模式,这样仍然可以正常提供服务。同时,后台会启动一个进程来恢复副本。恢复完成后会重置为多副本模式,大大缩短了故障的恢复时间。另一个是日志刷新的问题。比如在点赞和动态的场景下,这个值其实很小,但是吞吐量很高。这样的场景会造成严重的写放大问题。我们使用的是磁盘,默认是4k写入磁盘。如果每次取值都是几十个字节,会造成很大的磁盘浪费。基于这个问题,我们来做一次聚合刷盘。首先,我们将设置一个阈值。当写入了多少item,或者写入量超过了多少k,我们就会进行一次批量刷盘。这种batchflushing可以提高2~3倍的吞吐量。04结语1.在应用方面,我们会整合KV和缓存。因为业务开发对KV和缓存资源了解不多,所以不需要考虑集成后是用KV还是用缓存。另一个应用改进是支持哨兵模式,进一步降低复制成本。2、在运维方面,一个问题是慢节点的检测。我们可以检测故障节点,但是如何检测慢节点是目前业界的难题,也是我们未来努力的方向。另一个问题是自动磁盘平衡。磁盘出现故障后,目前的做法是第二天检查一些告警项,然后手动操作。我们希望做一个自动机制。3、系统层面是SPDK和DPDK的性能优化。通过这些优化,进一步提升了KV进程的吞吐量。