ClapDB

朋友,你真的知道如何利用“弹性”么?

Leo

K8S 的流行让大家认为可以放在 pod 里的程序就是弹性的。但是这只是前菜,要想做好一个真正的弹性应用,首先要重新理解什么是资源。

🔑 云资源不再是一维的,而是 单位价格✖️占用时间

云上资源不单卖,都是 cpu + memory + io 售卖的。按比例,实际上的价格锚点是 memory。可以理解为你买多少 memory,赠送多少 cpu 和 io(包括网络和本地存储)。

速度越快越便宜

这是云上最反直觉的一个事情。

在云时代之前,性能和成本是成比例的。同样一个计算任务要想执行得快,就需要更贵的计算资源。然而,在云上的计费规则是价格✖️占用时间。我们可以认为,解决同一个任务,理论上用更多的资源尽快解决问题最多用和之前同样的成本。考虑到整个任务中一般总有一些子任务在等待其他子任务完成,它等待的时间就是浪费。此外,还有一些临时存储是按时间计费的,比如 AWS ElastiCache。由此可见,申请更多的计算资源来更快地处理工作负载,反而可以带来更低的成本,这是云时代到来之前难以想象的可能性。这就是云的“弹性”带来的巨大变化。如何利用这个变化就成了我们 前文 里提到的云优先架构的关键。

面向弹性架构

顾名思义,整个架构首先要从如何更好地利用弹性去思考和设计。

弹性是过去面向服务架构盛行的时候从未考虑过的问题,原因也很简单,网络带宽太慢弹不起来。

面向弹性的架构的前提

  1. 问题可分解为较小计算难度的小问题,并且之间没有依赖关系(也包括对一个共享)。

  2. 能够让分解出来的工作尽量均匀。

以上的部分和传统的分布式计算并没有什么不同。

如何让弹性最大化才是关键。下面的讨论主要关于 ClapDB 的架构选择,对于其他业务的可借鉴性,请读者自己判断。

因为数据仓库业务的特殊性,对于任何 query,我们几乎都可以分为 dispatcher 和 worker 两种 workload。

Cloud function 的 active time 优化

账单是和 cloud function 的 active time 直接挂钩的,所以如何缩短同一个 function 的 active time 就是最直接的优化。

所以首先要让一个 function 的 active 时间最短化。这里就有很多事情要做了

active_time = coldstart + pulling + decompressing + processing + sending_result

cold start 优化

因为虚拟机的启动较慢,只能提前启动,所以这里只讨论 cloud function 的冷启动。

每个云的 cloud function 的实现都不是完全一样,cold start 都有自己的窍门,有的云因为这方面表现太差,最好的优化就是别用

  1. 在 AWS lambda 上,docker 的表现远远不如 zip 包,而 zip 包的大小也是关键,如何缩小 zip 包的大小也是一个很有意思的话题,我们做了很多尝试和努力,得到了不错的结果。

  2. 程序自己的冷启动速度也是非常关键的,你的程序起来后要先 load 一大堆资源,读一些有用没用的配置,这都是在花钱,要珍惜用户的钱,不要让用户为你懒得好好写代码而付费。

pulling 优化

📌 最关键的优化其实是不要 pull 没有用的数据,所以如何设计你的数据结构,只加载你需要的数据,是整个系统设计的关键。从这个角度来说,索引是优化的关键,避免加载无用的数据完全依赖于索引的艺术。

确保只加载必要的数据后,我们再进行下面的优化

lambda 的网络带宽并不慢,但是你首先要知道 lambda 的 bandwidth 是和 memory size 绑定的。不同的 region 情况还不太一样。也就是说不同 region 有不同的最佳实践的参数,这个就是作为开发者要去解决的问题。

利用一些工具可以发现类似下面的数据。

eu-north-1 lambda bandwidth

Memory (MB)Baseline (Gbps)Burst (Gbps)Burst Duration (seconds)
1280.5962.0430
2560.6142.6750
5120.6122.7420
10240.6092.7390
17920.6042.7400
20480.6072.7320
30080.5682.7490

如果是 AWS Fargate 你会看到下面的数据

eu-north-1

vCPUMemory (MB)Baseline (Gbps)Burst (Gbps)Burst Duration (seconds)
0.255120.0634.185410
0.2510240.1274.547570
0.2520480.2544.683920
0.5010240.1274.584540
0.5020480.2544.738820
0.5040960.5084.819900
1.0020480.2544.706920
1.0040960.5084.7821080
1.0081920.7449.720310
2.0040960.77511.802260
2.0081920.7449.720350
2.00163841.2419.729660
4.0081921.2419.828710
4.00163841.2419.827700
4.00307201.2419.747710

对于 lambda 你会发现无论是多少内存,带宽都一样,所以用低配 lambda 是带宽最优的。一个 lambda 600M/s, 如果你用 100 个低配 lambda 一起工作,你就等于享用 60Gbps 的带宽,如果是 1000 个 lambdas,就是 600Gbps。TB 级数据可以在 2-3s 以内加载完毕是大约的物理上限,

如果是 Fargate,高配置的实例的带宽也不会不断增加,用低配版本还是带宽最优的

但是如何在低配的 function 里运行你的程序就是一个要被解决的问题,毕竟这个时代,我们都习惯了动辄几十 G 内存,40core 的服务器。这其实关于类 NUMA 计算机上的 design pattern 的问题,我们在后面的文章会单独详细介绍。

Decompressing

在过去,为了降低网络带宽的压力,常常使用压缩比大的压缩算法。现代网络环境已经远超过去,压缩比不再是追求的重点。更快的解压速度意味着节省 active time。这里不讲太多细节,但仅仅使用 ZSTD 或者 SNAPPY 不是最优选择。在不同场景下选择最合适的算法,甚至不用任何压缩,才是优化的关键。而且在不同的 bandwidth/cpu 配比下,同一份数据可能适合使用不同的算法。在本地 NVME 上运行,可能与依赖网络的情况适合于不同的压缩策略。统一使用一种格式和算法是不够优秀的选择,例如有的算法在 arm 上表现不佳。即使是相同的数据格式,不同的数据分布也更适用于不同的压缩格式。这可以类比 Netflix 针对不同类型的影片采用不同的流媒体压缩策略。如何针对不同的数据采用不同的压缩策略,将在后面的章节中介绍。

Processing

处理数据和其他环节是衔接的,所以为了一味提高 processing 的性能。而牺牲其他环节是得不偿失的,大部分情况 processing 是改变不大的。这里如何高效的 processing,业界有很多实践,比如向量化已经是常规手段,并没有什么值得拿出来说的,现在还没有用上向量化手段的产品已经几乎绝迹。但是在 function 内存紧张的环境里如何避免 oom 似乎更重要一些。

Sending Result

worker 如何将 processing 的结果返回给 dispatcher 在过去就是跨机器的 rpc 调用而已。但是 aws lambda 一个 request 或者 response 有 6MB payload 限制,所以尽量不超过 6M 并且如果在超过 6M 的时候如何处理呢?尽量利用好 6M 的 payload 就是一个课题。

我们开源了一个可以完美替代 Protobuf 的项目,https://github.com/clapdb/FBE, 通过我们的 benchmark,FBE 是业界生成代码最优雅,decoding size 最小,decoding 速度最快的 serialization 实现,通过使用它,我们可以尽量减少 rpc 的 transfer 成本。

dispatcher

Dispatcher 是分配任务和汇总结果的 function,在某些时候它会驻留最久,流式处理数据是避免 dispatcher 内存爆炸的关键,但是对于数据仓库这样的场景,总有一个时刻数据量远远超过单个 function 的内存限制,直接利用云存储(包括各种存储)是系统的关键。

弹性就是一句话么?

如果说利用弹性就是一句话的事,也未免太小看这个问题了,能 all in serverless 的架构不可能就这么简单。

让我们看看这里面有哪些要注意的事情,希望大家更好的利用弹性,真正走入云的时代。

先看以下云的弹性的粒度。

  1. 云上的伸缩粒度并不是任意粒度的,并且锚点是内存。

  2. 云上资源都是按比例伸缩的,所以你不应该过度依赖其中某一种。

让资源的需求更平均

对于 dispatcher + worker 的经典模式来看,让 worker 的处理时间平均非常重要,如何给一个 worker 分配的资源适合它承担的任务就很关键。这样可以尽量让资源物尽其用,也不要浪费太多。用 index 尽量简化计算,让瓶颈变成更容易估计的数据大小相关对合理分配资源是更有利的。

不要想耗尽资源

最符合直觉的想法是要把资源用尽。但是事实上,ClapDB 是希望尽量快速处理完 workload,而资源是否耗尽并不关心。毕竟费用等于配置单价✖️ 耗时。

资源隔离的妙用

如果资源不隔离,其实弹性就没法用,因为你永远不知道你弹起来的workload 是否会影响正在进行的其他查询。

在我们的前文 云是一台新的计算机 里提到过,云更应该被看做一台拥有无限资源的电脑,并且资源隔离性远远超过传统计算机,所以和传统数据库在单机上螺蛳壳里做道场不同,不需要去挤占别的需求的资源,完全不需要考虑资源互斥的问题,反而可以尽其所能的通过灵活的分配资源来获取好处。

同一份数据创建多种 index

基于云的弹性和隔离性,数据仓库有了全新的玩法。在数据写入的时候,我们可以用更多的资源来产生多种索引为未来的可能性的读取做读优化,因为一份数据写入一次,读取无数次。读成本优化的价值远远胜于写成本优化。这里的思路和前几天被 OpenAI 收购的 Rockset 不谋而合。

我们甚至为同一份数据生成若干份索引,分别用来应对不同的查询或者子查询,甚至会为同一份数据产生多种数据压缩格式的索引进行遴选,留下最合适该数据分布的版本,放弃不好的。

ClapDB 的索引是在写入时生成的,而在读取时是享受写入时的投资收益。因为云的特点,我们可以同时产生多种不同类型的索引去应对可能的各种查询。好处是在查询的时候可以选择一种最佳索引,而不是一个中庸的对任何情况都不是最优解的索引选择。在单机时代,indexing 产生过多的 index 会因为产生过高的计算和 io 成本而影响写入性能,但是云的弹性和隔离性让这个策略变得可行。

软件是硬件的载体,发挥出硬件最大的特性才是软件超过前辈的关键。我们在整个项目周期里一直警惕传统的单机数据库的经验会给我们带来干扰和盲目,用第一性原理思考过去的经验是否还有效是我们在过去 2 年多的开发中不断重复的工作。

利用 spot instance reindexing

AWS Spot instance 这种云上市场经济的产物可以提供非常惊人的性价比,所以在价格便宜的时候,将所持有的数据重新 indexing 为下次读取优化就有一种闲时擦枪的效果,虽然我们不能把闲时的价格留到我们用的时候,但是可以把闲时的努力作用到忙时的效果也是云下没有的体验。(云下没有资源隔离,如果你做 reindexing 的时候突然来了请求,会因为资源不足无法响应)

将 cpu bound workload 和 io bound workload 拆开

数据库里大部分的简单查询都是 io bound 的应用为主,但是查询优化器的问题其实是纯计算密集的问题,如果和利用多核来穷举是关键,在传统单机上,要考虑不能用完所有的计算资源避免别的请求饿死,所以一般都强行牺牲了效果来简化探索。但是云的资源隔离性是可以完美解决此类问题的,ClapDB enterprise 的 optimizer 可以利用云的弹性和资源隔离对复杂的查询的优化问题进行更大规模的求解。

能异步不要同步

简单地理解同步都是要花 billing time 的,你自己考虑一下你是否愿意付网约车的等待费就明白了。

在云上,我们尽量让所有的算力都是被 event driven 的是最好的,ClapDB 就是通过云服务提供的 event driven 功能把若干的 lambda 串成一个完全的复杂系统的。

不要浪费资源

云上的资源不便宜,所以不应该因为不必要的原因被浪费掉。

以下的一些点都是区别于传统 SOA 架构的点,我们这里单独列出来:

  1. coldstart time,上面提过,这里是说,很多程序都是为了 service 使用准备的,大家完全不在乎启动的速度,里面太多的冗余动作和无意义的为了“优雅“的成本。比如有的服务启动会 scan 一下文件目录,或者用配置文件动态装载自己的字节码。

  2. memory,有 GC 的语言都应该重新思考为了不管理内存,多付50% 到 100% 的成本真的值得么?很多内存分配习惯对于这种低内存环境都可能是不适合的,过去很多实现都是大水漫灌的,比如 tcmalloc 对于内存就极为饥渴。

  3. cpu,总有一些传统观点把 cpu 的 utility 当做重要的资源利用率的指标,其实云上的 cpu是买 memory 送的,cpu 的 utility 完全没有可参照性,因为这完全取决于你的数据结构和架构设计,如果你的数据需要非常 heavy 的 decompress 和 decode,那么 cpu 肯定很辛苦,但是这完全取决于你的设计,把 cpu 用的更更干净不等于会得到更高的性价比。

灵活才能适配

其实在云上的资源配比并不是恒定的,在不同的资源配比的情况下,最合适的数据结构,压缩算法,数据分块策略,索引选择,查询计划其实都是不同的,没有灵活的架构,是无法适配多种不同计算资源的组合甚至是不同的云的。

而 workload 是分散到不同的 VM 上还是 lambda 上,也去取决于所在云所在 region 上的实际情况。

而同一个查询是 cpu bound 还是 io bound 还是 memory bound 其实完全取决于你用了什么索引或者列存储数据,你是希望在内存里快速解决问题还是用存储减少内存压力。所以根据资源申请偏好和当时的价格做出不同的选择是 ClapDB 里最难的一部分工作。我们的选择是避免 cpu bound 出现,后面的文章我们会专门讨论这个问题,敬请期待。

结论

弹性的本质是资源的动态申请,以及资源隔离的效果,基于这两点,ClapDB 可以快速获取最适合的资源,并且用完即弃,但是在实际开发细节中和基于 smp 架构的方法大相径庭,而是更像基于 NUMA 架构开发的设计范式,这是我们整个设计的核心要素,我们会在后面的文章详细介绍。

← Back to Blog