原文: http://mp.weixin.qq.com/s?__biz=MzAwMDU1MTE1OQ==&mid=208931479&idx=1&sn=1dc6ea4fa28a3fb527a6204a9a5c23b1&key=c76941211a49ab5849fe180925fd9816350457f931e54a80feca07c081bffea5828ae0bbb2b1f7be41501db7dea48977&ascene=0&uin=Mjk1ODMyNTYyMg%3D%3D&devicetype=iMac+MacBookPro9%2C2+OSX+OSX+10.10.3+build(14D136)&version=11020012&pass_ticket=XBh3IrGvu4uh9iuU9u54bHyVlV1hde36lZbrQOc%2F4ICtKWbJji6PTss8d%2FMelkMW
此文根据【QCON高可用架构群】分享内容,由群内【编辑组】志愿整理,转发请注明来自“高可用架构(ArchNotes)”微信公众号。
陈宗志:奇虎360基础架构组 高级存储研发工程师,目前负责360分布式存储系统Bada的设计和实现,同时负责360虚拟化相关技术的研究。
主要向大家介绍一下360自主研发的分布式存储系统Nosql-Bada,作为设计者我一直觉得设计过程就是在做一些折衷,所以大部分的内容是我们开发实现Bada过程中的一些经验和坑, 也有很多的权衡, 希望和大家一起分享, 有不对的地方欢迎指出。
虽然项目目前还未开源, 但是我们的一些组件, 用于异步同步数据的Mario库等, 均已经开源,后续Bada也会开源。这是360官方的Github账号https://github.com/Qihoo360
我们的定位是海量数据的持久化存储, 为线上的热门应用服务。不过我们目前没有接入跟钱相关的业务, 因为我们的系统毕竟是最终一致性的系统。
我们倾向使用Bada的用户数据value的大小在10k以内, 那么我们的延迟能够做到1ms左右。我们为了读取性能有一定的优势, 一般要求机器都挂载SSD盘。如果用于存储冷数据, 我们会建议用户存数据到公司的其他存储产品, 比如hbase,cassandra等等。
目前公司内部云盘, 移动搜索, LBS, Onebox, 导航影视, 白名单等多个业务均在使用。
云盘的场景是:所有上传, 下载文件的时候通过Bada查询文件所在的集群。这个业务数据量大概600亿, 其中某一个机房的访问量15亿。
LBS这个业务是将所有的POI的信息存储在Bada中, 业务需要在5个机房进行数据同步。每天的请求量大概7亿。
Bada SDK 是我们提供给用户SDK, 360 QConf 配置管理服务 大家之前也了解过, 我们是QConf的重度用户。用户通过SDK从QConf中获得存活的Bada节点, 然后进行访问。
Data Server是我们的服务节点,其设计是学习自Amazon Dynamo(不过好像Dynamo 本身也被很多人喷), 每一个节点都是对等结构, 每一个节点存储了所有的元信息。为什么这么做?
目前主流的设计一般是两种:
BigTable 为代表的, 有MetaServer, DataServer的设计, MetaServer存储元数据信息, DataServer存储实际的数据。包括 BigTable, HBase, 百度的Mola等等。
Dynamo 为代表的, 对等结构设计. 每一个节点都是一样的结构, 每一个节点都保存了数据的元信息以及数据. 包括 Cassandra, Riak 等等。
其实我觉得两个结构都是合适的。为了部署, 扩展等方便,我们不希望部署的时候需要分开部署Meta节点, Data节点。计算机行业, 加一层可以解决大部分问题, 因此我们觉得对等网络的设计更有挑战性。个人观点, 在数据量更大的情况下, Meta 节点极有可能成为瓶颈。当然Dynamo的结构肯定也有自身的缺点, 比如如何保证元数据的一致性等问题。
Network Proxy: 用于接收客户端的请求, 我们的协议是定制的protobuf 协议, Network Proxy模块负责解析协议, 然后请求转发到对应的节点
Meta Info: 用于存储公共的元信息, 元信息包括每一个分片存储在哪个节点
DB Engine: 我们底下的引擎是基于LevelDB的定制化开发, 包括支持cas, 过期时间, 多数据结构等等
可以看到我们目前使用的是有主从的副本策略, 图中的Primary 是主节点, Secondary 是从节点。为什么这么做?
首先为什么不使用ec编码(erasure code 纠删码), 因为ec编码主要用于保存偏冷数据, ec编码遇到的问题是如果某一个副本挂掉以后, 想要恢复副本的过程必须与其他多个节点进行通信来恢复数据, 会照成大量的网络开销. 因此这里3副本更合适。
常见的分布式系统的多副本策略主要分成两类:
以Cassandra, Dynamo 为主的, 没有主从结构的设计, 读写的时候满足quorum W + R > N, 因此写入的时候写入2个副本成功才能返回。读的时候需要读副本然后返回最新的。这里的最新可以是时间戳或者逻辑时间。
以MongoDB, Bada为主的, 有主从结构的设计, 那么读写的时候, 客户端访问的都是主副本, 通过binlog/oplog 来将数据同步给从副本。
两种设计都只能满足最终一致性。那么我们再从CAP理论上看, 那么都是在哪些维度做了权衡?
从性能上来看,有主从的设计很明显性能会由于无主从的, 因为有主从的设计只需要访问一个副本就可以返回, 而无主从的至少两个副本返回才可以。
从一致性来看,有主从的设计如果挂掉一个节点, 如果这个节点是主, 那么就会造成由于数据同步的不及时, 这段时间写入的数据丢。如果挂掉的是从节点, 那么则对数据没有任何的影响。只要这个节点在接下来的时间内能够起来即可。无主从的设计如果挂掉一个节点, 理论上对结果是无影响的, 因为返回的时候会比较最新的结果。有主从的结构由于写入都在一个节点, 因此不存在冲突。而无主从的结构由于写入的是任意的两个副本, 会存在对同一个key的修改在不同的副本, 导致客户端读取的时候是两个不一致的版本, 这个时候就需要去解决冲突, 常见的方案就涉及到vector clock, 时间戳等等。不过, 总体来看无主从的设计一致性应该优于有主从的设计。
从分区容错来看, 两边都必须有一半以上的节点存活才能够对外提供服务, 因为有主从的设计中必须获得超过一半节点的投票才能成为主节点。而无主从的结构, 常见在W = 2, R = 2的情况下, 必须2个副本以上才能对外提供服务。
从可靠性来看,有主从的设计因为只访问一个副本, 性能优于无主从的设计。而且无主从的设计中, 因为对单条数据必须有两次读取, 因此对系统的访问压力也会比无主从的来的多。当然有主从的设计容易造成主落在同一个机器上, 造成负载不均的情况, 但是这里只要将主平均到所有的机器, 就可以解决这个问题。但是有主从的设计在切换主从的时候, 必然有一段时间无法对外提供服务, 而无主从的设计则不存在这样的问题。总体来说, 笔者认为从可靠性的角度来说, 有主从的设计应该比无主从来的可靠。
我们使用的是有主从结构的设计, 原因:
Bada主要的应用场景对性能的要求比较高, 大部分的请求需要在1ms左右的时间返回, 因此有主从的设计, 性能更满足需求
线上服务的可靠性是我们另外一个考虑的因素
具体的分析过程可以看 http://baotiao.github.io/2015/03/Bada-design-replicaset/
数据分片策略,我们叫两次映射.
key -> PartitionId(hash)
PartitionId -> Node(MetaData)
比如上面这张图中我们可以看出, 我们将所有数据分成10个Partition, 然后每一个机器存有主节点和从节点. 我们会尽可能的保证每一个机器上面的主节点是一样多的, 这样能够做到每一个节点的负载都是均衡的。
当请求的数据Primary正好是当前这个节点
当请求的数据Primary 不是当前节点
360的机房是比较多的, 而且某些机房之间的网络较差。业务部署一个服务的时候, 后端的DB也需要部署在多个机房上, 因此这个常常是业务的痛点。因此我们设计之初就考虑多机房的架构。
我们的多机房架构能保证
用户不用管理多个机房, 任意一个机房数据写入, 其他机房能够读取
在机房存在问题的时候, 我们可以立刻切换机房的流量
提供每一个机房之间数据的统计和Check
这个是目前LBS业务的场景
可以看出我们这里有一个专门的队列用于同步机房之间的数据。这个QBus 是我们团队内部基于kafka开发的消息队列服务。
目前主流的机房同步方法也是两种:
节点负责机房数据的同步, 比如Cassandra, CouchBase, Riak
由外部的队列来同步机房之间的数据, 比如 Yahoo pnuts
在写入的时候, 每一个机房的协调者。比如这个图里面10这个节点。会把写入发送给其它机房的某一个节点, 这个时候Client这边收到的只是根据配置的一致性级别就可以返回, 比如这里配置的只要1个返回即可, 那么Client写入成功10这个节点以后,即可返回。至于与其他机房同步是10这个节点的事情, 这样子客户端的写入就可以在本地写入, 不用管多机房的latency。
这里我们可以看到是Eventual Consistency. 那么Cassandra是如何做到冲突修复的呢. 这里Cassandra 读的时候有一个Read Repair 机制, 就是读取的时候读取本地多个副本. 如果副本不一致, 那么就选时间戳最新的重新写入. 让数据重新同步, 这里Cassandra只是说修复本地多副本数据不一致的方法, 同样的方法我们也可以用在多个IDC里面, 可以同时跑多个任务check不同机房的数据, 然后修复他们。
Continuous Replication提供配置的不同Server之间同步的Stream的个数,也就是不同的机房之间连接的数目是可配置的。解决冲突办法.CouchBase提供的是最终一致性的方法,不同的版本之间首先根据修改的次数, 然后是修改时间等信息。
我们最后考虑的是使用团队内部的QBus作为我们通信的队列, 主要考虑
省去了自己实现队列的麻烦
稳定运行于线上, 有专门的同事维护. 减少的很多问题
单机房写入, 任意机房读取
跨机房写入, 任意机房读取
任意机房写入, 任意机房读取
我们的实现方案也是通过QConf来实现。客户端访问的时候, 从QConf中读取目前需要访问的机房, 默认是访问本机房, 如果需要跨机房访问, 将QConf中的配置制定成需要访问的机房就可以了。
时间戳最新
任意机房写入数据, 根据时间戳来进行冲突解决。
Yahoo Pnuts Primary Key
这里我们对每一个Key 有一个Primary IDC, 也就是这个Key的修改删除等操作都只会在当前这个IDC完成, 然后读取可以有多个IDC去读取. 那么因为对于同一个Key的修改, 我们都在同一个IDC上. 我们通过给每一个Key加上一个Version信息, 类似Memcached的cas操作, 那么我们就可以保证做到支持单条数据的事务。如果这条数据的Primary IDC是在本机房, 那么插入操作很快。
如果这条数据的Primary IDC不是本机房, 那么就有一个Cross IDC的修改操作, 延迟将会比较高。不过我们考虑一下我们大部分的应用场景,比如微博, 90%的数据的修改应该会在同一个机房。比如一个用户有一个profile信息, 那么和修改这个信息的基本都是这个用户本人, 90%的情况下应该就是在同一个地点改, 当然写入也会在同一个机房. 所以大部分的修改应该是同一个机房的修改。但是访问可能来自各个地方,当然为了做优化, 有些数据可能在一个地方修改过了以后, 多次在其他地方修改, 那么我们就可以修改这个Key的Primary IDC到另外这个机房。
Vector Lock
Vector Lock的核心思想就是Client对这个数据的了解是远远超过服务端的, 因为对于服务端而言, 这个Key 对应的Value 对于Server 端只是一个字符串。而Client端能够具体了解这个Value所代表的含义, 对这个Value进行解析。那么对于这个例子,当这两个不一样的Value写入到两个副本中的时候, Client进行一次读取操作读取了多个副本。
Client发现读到的两个副本的结果是有冲突的, 这里我们假设原始的Key的Vector Lock信息是[X:1], 那么第一次修改就是[X:1,Y:1], 另一个客户端是基于[X:1]的Vector Lock修改的, 所以它的Vector Lock信息就应该是[X:1,Z:1]。这个时候我们只要检查这个Vector Lock信息就可以可以发现他们冲突, 这个就是就交给客户端去处理这个冲突.并把结果重新Update即可。
我们线上目前支持的是时间戳最新, 以及Primary Key的方案. 大部分使用的是时间戳最新来进行冲突解决。
我们开发了一套基于leveldb的多数据结构的引擎。目前支持 Hash, List, Set, Zset等结构。
主要是由于用户习惯了Redis提供的多数据结构, 能够满足用于快速开发业务的过程, 因此我们也提供了多数据结构的支持。
ZooKeeper 和 mnesia对比, ZooKeeper 是一个服务, 而 mnesia是一个库, 因此如果使用ZooKeeper的话, 我们需要额外的维护一套服务。而 mnesia可以直接集成在代码里面,使用更方便。
mnesia和 Erlang 集成的更好,mnesia本身就是用Erlang 来开发。
360的MongoDB 之前也是我们团队在维护, 在使用MongoDB的过程中, 我们也遇到一些问题, 比如MongoDB 的扩容非常不方便, 扩容需要很长的时间, 因为MongoDB 扩容的过程是将一条一条的数据写入的. 我们开发的时候考虑到这些问题, 因此Bada 使用的是leveldb, 当需要扩容的时候, 只要将某一个分片下面的数据文件拷贝过去即可. 前提是初始化的时候分片设置的足够大, 我们现实默认的分片是1024
MongoDB 的数据膨胀度比较大, 因为MongoDB 毕竟是文档型数据库, 肯定会保持一些冗余信息. 我们底下使用leveldb, leveldb 本身的压缩功能基于snappy 压缩. 还是做的比较好. 线上实际的磁盘空间大小相对于MongoDB 4:1
Cassandra的定位和Bada是不一样的, 我们面向的是线上频繁访问的热数据, 因此我们偏向于存储小value数据, 热数据, 对latency 的要求会苛刻。
比如在云盘的场景, 我们存储的就是文件的索引信息, 而Cassandra存储的是具体的Cassandra的数据, 也因此我们线上部署Bada的机器是挂载SSD盘的。
Bada 的性能比Redis 低, 但是目前redis cluster 还没发展完善. 我们公司的DBA也在跟进Redis cluster之中. 所以当数据量比较大的时候, Redis可能就不适用于这么大量的数据存储。
Bada 的多数据结构支持不如Redis来得完善. 因此我们也在逐步的支持Bada的多数据结构。
Redis 毕竟是内存型的服务. 因此假如用户是偏向于存储持久化数据, 可能Redis不太合适。
技术是为业务服务, 包括我们Bada在公司内部推广的过程中也发现, 我们很多业务很头疼的问题在于360的机房较多, 每一个小业务都需要维护在多个机房, 因此为了降低用户的开发试错成本, 我们将能标准化的事情都做了。包括我们组的定位也是专注底层技术, 加速产品团队开发效率, 尽可能降低业务对服务端集群架构的关注。
是的。从QConf 中获得是随机的一个节点的ip,所以对每一个节点的访问基本的均衡的。服务端这边, 因为我们是有主从结构的。但是我们的主从是分片级别的主从,这点和redis cluster 不一样。比如 Redis cluster 有Master 节点, slave节点,一般情况slave 节点不接受任何的线上访问,但是从下面的图中可以看到 Bada 每一个节点都有主, 从分片。 因为每一个节点的访问基本是均衡的。
Q2:我有一个问题,对于kv存储,选择leveldb的动机是什么?其他leveldb分支是否考虑过?
对于存储的考虑, 我们之前对 Rocksdb 和 leveldb 做过对比.在数据量小的情况下, leveldb 的性能和 Rocksdb 性能差不多. 数据量大的时候 Rocksdb 会有性能优势. 因为我们之前对leveldb 做了修改. 所以后续我们会迁移过去。 这里我们的读写都走的是 Master 节点. 只有当主节点挂掉以后, 才会访问从节点。
这个截图是之前对 leveldb 和 rocksdb 在数据量比较小的情况下的对比
Q3:能否说一下扩容,新增节点,以及摘除失效节点的处理?
从上面两张图中可以看出, 我们会将新增的节点中, 均衡的将新的主节点迁移的新节点上。目前扩容的过程是这样 我们先把当前这个节点加入到集群。然后通过 rebalance 来进行平衡。我们一般预先分配1024 个分配。这个应该也是业内场景的做法, 之前对腾讯的CKV 也是这么做,Riak 也是这么做。
Q4:迁移是直接对leveldb复制,延时会有多少,在迁移过程中的访问如何处理呢?
迁移是直接对 leveldb 的文件进行复制, 这个时候性能是取决于网络的开销。这也是我们比mongo扩容快的地方, mongo 在扩容的时候需要将数据一条一条写。迁移之前, 我们会将当前这个节点进行切主操作, 就是将所有的主切走。那么这个时候是不会影响线上访问,带来的最多的影响就是这个节点的网络有额外的开销,但是这个节点不是面向用户的请求的,所以影响不 大。
Q5 :主切走也需要有一个时间吧?这个时间段内,如果要访问原来主上的数据,怎么处理?
这里是这样的一个过程, 迁移的时候比如A 节点。 那么A节点上有主分片, 那么在迁移之前,我们会先将A节点上的主让给其他节点。这里就涉及到追Binlog 的问题,如果这个时候用户有大量的数据写入, 会导致Binlog 一直追不齐。确实会导致无法迁移。
Q6:关于leveldb的迁移,能否详细介绍一下?
leveldb 的迁移很简单,就是直接通过scp 就可以了。这个是leveldb 本身的功能,就是通过scp leveldb 对应的数据文件就可以。其实我们在binlog 这块也做了挺多的事情, 不过太细了有机会下次讲。使用binlog 来同步的副本策略之中, 常见的问题比如,分布式系统中由于主从切换导致的数据丢失,然后我们也开发了binlog merge 来减少这种问题带来的影响。
Q7:leveldb的部分数据在内存中,这个迁移的时候怎么解决的?
这个没有影响。因为leveldb 的memtable 的数据在磁盘上有对应的.log 文件。leveldb 启动的时候会默认读取.log文件, 将里面的内容加载到内存中。
Q8 : 我还是没太明白,扩容的时候,A节点切到其他节点,是把A的meta信息做切换,然后再复制数据,最后再映射meta?
扩容的时候是这样一个过程。先将新增的节点加入到现有的集群,不过这个节点不负责任何的分片, 因此没有任何数据在这个节点上;然后我们迁移的过程是节点上的一个个的分片进行迁移。比如A 这个节点有 10~20 这几个分片, 并且这个时候 10~20 这个分片是主, 那么依次我们先将A这个节点的10~20变成从, 这个时候需要修改meta信息。然后接下来是复制对应的数据文件到新节点, 复制结束以后, 修改10~20 这几个分片到新的主上.最后修改meta 信息 ,和大部分系统比最大的不同在于 Bada 的主从是分片级别的主从, 不是节点级别的主从.这样任何操作造成的影响都是非常小. 并且可以做到每个节点的负载尽可能的均衡。
Q9:mnesia用来存储meta信息吗?
mnesia 对于我们的定位就类似于ZooKeeper。有两个用途, 一个是选主的过程提供一个全局的锁, 一个是保存元信息。
为什么不使用ZooKeeper
ZooKeeper 和 mnesia 对比, ZooKeeper 是一个服务, 而mnesia是一个库, 因此如果使用ZooKeeper的话, 我们需要额外的维护一套服务. 而mnesia可以直接集成在代码里面. 使用更方便
mnesia 和 erlang 集成的更好. mnesia本身就是用Erlang 来开发
Q10:meta信息是存储在单独的机器上,而不是分布在存储节点上吗?
不是, 存储在每一个节点上. 每一个节点都部有mnesia
Q11:既然用mnesia,那你前端机器连在一个集群?规模多大?
前端是按照业务划分的,最大的有36个节点.
感谢王杰的记录与整理,国忠和四正的校对,其他多位编辑组志愿者对本文亦有贡献。更多关于架构方面的内容,读者可以通过搜索“ArchNotes”或长按下面图片,关注“高可用架构”公众号,查看更多架构方面内容,获取通往架构师之路的宝贵经验。转载请注明来自“高可用架构(ArchNotes)”微信公众号。