漫谈分布式系统(7) -- 扩展性?切就完了

这是《漫谈分布式系统》系列的第 7 篇,预计会写 30 篇左右。每篇文末有为懒人准备的 TL;DR,还有给勤奋者的关联阅读。扫描文末二维码,关注公众号,听我娓娓道来。也欢迎转发朋友圈分享给更多人。

再谈分治

前面几篇文章,我反复提到一个词 -- 「分治」。

分治,分而治之,是为了解决数据太大存不下和数据太大算的慢的问题。这也是基础分布式系统要解决的核心问题。

或者,我们换到程序和服务的角度去理解,是要解决(横向)扩展性(Scalability)问题。

  • 以 HDFS 为代表的分布式存储框架,通过把数据切分成固定大小的 block,再搭配记录 block 位置的元数据,就解决了数据存储扩展性的问题。只要有足够多的硬盘,就能一直存下去。

  • 以 MapReduce 为代表的分布式计算框架,通过把计算逻辑切分成一个个 Mapper 和 Reducer,就解决了数据计算扩展性的问题。只要有足够的 CPU 和内存,就能一直算下去。

所以,分布式系统最基础的难题 -- 扩展性,就被 partitioning 这个通用的方法解决掉了。

不同的系统里,partition 可能会有其他名字,比如 block、region、bucket、shard、stage、task 等等。换汤不换药,讲的都是同一个故事。

而我们在系列第 5 篇提到过,计算逻辑的切分等价于数据的切分。所以,我们聚焦在数据的扩展性上,来了解下常见的 partitioning 方案。

典型的,我们可以把数据分为三种类型来研究:

  • 文件数据,文件系统上任意格式的文件,如 HDFS 上的文本文件。

  • key-value 数据,带主键的数据,如 MySQL 里的数据。

  • 文档数据,类似 json 格式的数据,和 key-value 的区别是没有业务意义上的主键,如 ElasticSearch 上的数据。

具体来说,对于扩展性,我们关注这么几个点:

  • parititioning 方式,即数据在逻辑上怎么切分

  • localization 方式,即数据在物理上怎么分布

  • rebalance 方式,即在节点数变化后,数据在物理上怎么重新分布

文件数据

Partitioning

文件数据,是最底层也是最灵活的数据。其他类型的数据,底层最终也一定是以文件的形式存在(不去纠结不落地的纯内存数据)。

也正是因为这个特点,文件数据的 partition 也只能做得更底层和应用无关。

所以,无论是 Linux 支持的各种文件系统,还是分布式领域的 HDFS,都采用了类似固定大小的 block 的做法来切分数据。

对分布式系统而言,还要再搭配一个元数据,记录文件和 block 的对应关系,以及 block 和机器的对应关系。

Localization & Rebalance

由于 block 是没有应用层含义的,所以文件数据的分布(Localization)接近随机,只是更多考虑从存储空间大小的角度去做

这样是很好理解的,好处有几个:

  • 需要保证足够的剩余空间,否则可能导致服务和机器故障。

  • 需要保证数据分布的均衡,因为计算跟着数据跑,数据分布的均衡,能更高效的利用计算资源和 IO 资源。

而增删节点之后的 Rebalance,实现起来也非常简单。

本质上,只需要复制数据到目标机器,然后修改元数据上的映射关系,就好了。

元数据的修改很轻量级。但数据的挪动会带来大量的 IO,所以可以考虑错开业务高峰时间,或者限制挪动速度来降低对业务程序的资源竞争。

像 HDFS 还提供了触发阈值的设置,以及自动 rebalance 的功能,就更大程度的方便了扩缩容。

Key-Value 数据

Partitioning

key-value 数据是我们经常处理的数据类型。和文件类型数据的最大区别是,key-value 的结构赋予了应用层的含义,使得我们可以摆脱底层的束缚,在应用层做很多事情。

具体到 partitioning,我们就不用在 block 级别去做了。key-value 以 key 为核心,所以我们以 key 为单位做 partitioning。

第一个很容易想到的办法,是按范围(key range)来切分。

比如一个以手机号为 key 的数据,就很容易这么来切分:

  • ........

  • 13100000000 - 13199999999

  • 13200000000 - 13299999999

  • 13300000000 - 13399999999

  • ........

HBase 就是采用的这种方法做 partitioning。

但是这样很容易导致数据分布不均衡。比如 135 这类号段会有非常多的数据,而 101 这类号段可能根本没有数据。

根本原因,还是 partitioning 的 key 带上了业务含义,而业务本身可能就是不均衡的。

好办,那就让 partiton key 变得业务无关。

有些场景下,通过一些简单的变换就能达到效果。

比如上面手机号的例子,把手机号翻转再作为 partitioning key 就能很好的解决数据倾斜(不均衡)了。

更普遍的场景下,想要打散数据,以下两个办法更通用:

  • 加随机数,这个效果肯定是最彻底的,但这样就不能精确定位 key 来访问数据了,只能范围扫描。

  • hash 处理,hash 算法的摘要提供了一定程度的打散,而输出的确定性又保证了事后访问能精确定位。

所以,一般选择对 key 做 hash 的方式,然后,仍旧可以继续按 range 来做切分。

广义来看,手机号翻转其实也可以看做一个 hash 函数,更加常用的标准 hash 算法有 md5、sha 等。

hash 确实能解决分布不均的问题,但也丢掉了 range 的一大好处:按范围查询。

做完 hash 之后,范围查询不得不扩散到多个 partition 上,查询性能会收到比较大的影响,并且丢掉了数据之间的顺序。

于是可以有一种折中的办法,所谓的组合主键(compound primary key)。类似 key1_key2,只有 key1 用来作为 hash partition,而 key2 就用来满足范围查询的需求。

比如考虑一个论坛的场景,要查询一个用户在某一天的所有发帖,就可以设计 (user_id, timestamp) 这样的主键,然后,一个 scan(user_id, start_timestamp, end_timestamp) 的范围查询就能很方便得到结果。

Cassandra 就是采用的这种方法做 partitioning。

Localization & Rebalance

由于 key-value 数据的 partitoin 带上了业务含义,不能再像文件数据那样,只考虑存储空间了。

当然,我们的 localization 策略,不能打破同样范围的数据在同一个 partiton 这个规则。

典型的,有几种选择。

第一种,hash mod N  来确定数据应该放到哪个分区,其中 N 为节点数。

这种方法的好处是没有元数据管理的成本,因为元数据从数据变成了计算逻辑。

缺点是非常不灵活,一旦节点数发生变化,rebalance 就可能要移动大量甚至所有数据。

根本原因,是 partition 带上了 N 这个变量,所以节点数一变,rebalance 就会影响 localization。

简单,那就不要带变量,于是有了第二种办法 -- 固定数量的 partition。无论节点数增减多少,partition 数量都不变。

这样一来,当有节点增减时,只要移动少量 partition 就够了。坏处自然是元数据管理带来的开销。

但元数据通常不大,所以像 ElasticSearch、Couchbase 等都采用了这种方案。

固定 partition 数量,还有另一个难题,需要合适,不然就得重新分布数据。但是,多少叫合适呢?

可能一开始设置了 100000,从第一天开始就被过大的元数据管理、同步成本拖累。

可能一开始设置了 100,很好。但是一年后数据量涨了 10 倍,就不够了。

所以没有标准答案,视业务场景而定,同样业务场景也可能要随时间而变。

既然找不到合适的值,怎么办?

不找!敌不动我不动,敌若动我也动。

于是有了第三种办法:动态数量的 partition

加节点了,就把旧的 partition split 掉,然后分一些给新节点。

减节点了,就把旧的 paritition merge 掉,然后挪到可用节点上。

数据挪动的范围,也只是受影响的部分 partition。

这样,就再也不怕 rebalance 了。

但也不是没问题。

  • split 和 merge 带来的运营成本和对性能的影响,是躲不过的。

  • split 和 merge 的触发条件是要仔细设计的,partition 的数据量、文件数、文件大小等,都可能需要考虑。

  • 虽然动态,但总得有个初始值,否则写热点之类的问题就足以让人头疼。把 pre-partition 这个功能开放给开发人员是个不错的选择。

HBase 就是采用的这种方法。

文档数据

Partitioning

文档(document)数据虽然没有业务意义上的主键,但通常会有一个唯一的内部 doc_id,正如我们在关系数据库里,经常会有一个自增 id 做主键。

这样文档数据就变成了 {doc_id: doc} 这样的 key-value 结构,自然就能用 key-value 数据里提到 partitioning 方法。

更常见的是采用 mod N 的方法,比如 ElasticSearch。

Localization & Rebalance

另一个和关系数据库相似的点是,文档数据通常也通过二级索引(Secondary Index)来查询,类似搜索。

这样一来,同样二次索引的数据就可能出现在不同的 partition 上,于是只能采用类似 map + reduce 的方法去查询数据

很显然对大量读的场景不友好,每个查询都要广播到所有 partition。

于是,有了把二级索引作为所谓 routing key 的方式,来读写数据。当然,这个对 localization 的优化,也势必影响到 partitioning。

这样,同意二级索引值的数据,就写到了固定的 partition,读放大的问题就解决了。

当然,多个二级索引的场景,routing key 不同,可能就不得不写多份数据了。

至于二次索引的存取,可以有所谓 document local index 和 term global index 两种实现,这里不再展开。

TL;DR

  • 分治是分布式系统的核心思想。

  • 分治的实现是 partition,解决的是分布式系统最基础的难题 -- 扩展性。

  • 扩展性要考虑三个重要问题:partioning、localization 和 rebalance。

  • 文件类数据,通常近似随机的做 partitioning,由元数据提供 localization,rebalance 影响范围也可控。

  • key-value 类数据,由于多了业务含义,通常从 key 的业务含义入手,不同方法优缺点都很明显。

  • 文档类的数据,由于 doc_id 的存在,可以借鉴 key-value 数据做 partitioning,但也要考虑二级索引带来的影响。


这篇文章,尝试总结了前面几篇文章,本质上都是在解决分布式系统的核心问题之一 -- 扩展性问题。解决的办法就是 partitioning。

下一篇,我们就一起看下分布式系统的另一个核心问题 -- 可用性。

前面一直在说分布式系统的好,现在,总算到了要直面分布式系统带来的诸多问题的时候了。

关联阅读

漫谈分布式系统(1) --  为什么要有分布式系统

漫谈分布式系统(3) -- 再也不怕存不下

漫谈分布式系统(5) -- 再也不怕算得慢

原创不易

关注/分享/赞赏

给我坚持的动力

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值