最近几周我们 Databend 项目 https://github.com/datafuselabs/databend 内部提出一个将来如何在云上给大家提供一个免费的试用环境。当第一次看到这个问题时,我在想如果有万级的用户申请试用,可能对我们运维的资源挑战也非常大,直到看到 CockroachDB Serverless cluster 架构说明,我也才恍然大悟,也让我看到了 k8s 还可以这么玩。
下面文章是我个人的一个翻译,很多地方是我看过后,按个人的理解写的,并不是一个完全准备的翻译。如果看着不爽,可以直接原文:https://www.cockroachlabs.com/blog/how-we-built-cockroachdb-serverless/
经历了 18 个月有挑战的工作后,我们运行第一个真正的可伸缩的 Serverless SQL Database。它现在可以使用了,而且免费。继续读下去,去了解 CockroachDB Serverless 由内到外的工作,以及我们为什么可以免费,而且不是在有限的时间内免费,而是永远免费。这里需要一些重要的和令人着急的工程才能实现。我想你会喜欢听听他的实现。
CockroachDB Serverless 是什么 ?
如果你之前创建过数据,你一定做过容量评估类的工作。如果你评估的资源太低,你的数据库可能在高负载的环境直接因负载过高而挂了。如果你评估的资源过多,你又面临资源的浪费,机器中大量的资源在闲置。这里有没有更好的方案呢?
Serverless 意味着你不用在关心服务器的数量。当然了还是有服务器会为你的程序努力工作,这个是我们底层支撑的工作,而不是你的。我们在背后做了很多工作如:资源分配,配置 和服务器相关的运维工作。这样可以让你不再和从以前一样为服务器付钱,变成了只为你程序请求次数和存储使用到的资源付费。
在 Serverless 下你只需要为你的实际使用到资源付费,你也不需要弄清楚前后都是用的什么。如果你的服务压力大了,或是需求的资源多了,Serverless 可以自动的分配更多的资源。如果你使用的少了, 你就可以付的少一点, 其它就没什么事情了。最后,你也不用担心账单超出预算,因为你可以设置消费的限制。如果费用接近限制时,我们会提醒你,甚至在超过限制后,我们还可以为你提供一个 free baseline (免费基准的) 性能服务能力。
这个让我想到了 CockRoachDB 最精彩的部分。CockroachDB Serverless 是“永远免费”的, 每个月都会有一定配额的请求次数和存储配额。你只需要用鼠标点几下或是基于 API 请求几次,你就可以秒级创建一个全功能的 CockroachDB 。而且这个数据库是“ Aways On” , 即使你的 IDC 挂了,或是遭到黑客攻击,你申请的 CockroachDB也会保存下来,而且是一个多副本的(数据是加密过的) 它可以在你的需求范围中自动的伸缩, 不管是大是小, 你的应用程序都不用变动。它也支持在线的表结构变更, Postgres 完全兼容, 最后也支持访问企业级的功能。
对了,所有的环境中可以用你喜欢的开发语言, SDK, 或是工具来使用 CockroachDB Serveless . 使用 CockroachDB Serverless 并不意味着你必须使用 AWS Lambda, Google Cloud Functions 这些计算的 Serverless 服务。
我们怎么负担的起”永远免费“这个方案呢?当然,我们也希望你们中有一些人可以开发出成功的应用,最后转化成付费用的用户。但除此之外,我们还创建了一种新的无服务器架构, 它允许我们在单个物理 CockroachDB database Cluster上提供上千个虚拟化的 CockroachDB数据库集群。这意味着我们只需要少量的服务器和少量的存储就可以运行起这个多租户模型,几乎没什么成本。因为每个租户只是运行在物理硬件上的一个非常小的部分。我将在下面更详细的解释这一切如何工作的, 但这里有一个图需要你先思考一下:
CockroachDB 单租户的架构
在以前, 一个物理的 CockroachDB 集群被一个用户或是一个组织独享。这被称为单租户模式。在过去的 CockroachDB release 中, 我们已经开始为它添加多租户的支持, 它将使用 CockroachDB 从单租户到共享的多租户模型转变。每一个租户从物理的 CockroachDB Cluster 中获取一个虚拟的 CockroachDB Cluster,他们之间相互安全,而且是资源隔离的。你可能熟悉虚拟机的工作原理,是吧?就象那样,但只适用于数据库的集群。
在我详细的解释多租户如何工作时,我需要带你们先回顾一下单租户的架构。首先, 一个单租户的 CockroachDB Cluster 可以由任意的节点组成。每个节点都可以用于计算和存储,并且通常驻留在自己的机器上。在单个节点中 CockroachDB 具有分层的架构:最高层是 SQL 层,它解析,优化和执行SQL语句。它将复杂的SQL巧妙的转化成简单的请求发送到下层的 KV 层。
在 KV 层维护着事务,分布,多副本存储。这个可能太难理解了,让我来解释一下。每个 key 都是一个唯一的字符串,对应着具体的 value , 就象字典一样。KV将这些键值对按顺序存储, 以便快速的查找。键值对也被分组到不同的区间 range , 每组区间存储的键值对是连续的,不重叠的,按键排序。为了高可用,每组键值对至少要三个副本。键值对可以在事务或是没事务的情况下添加,删除和更新。下面是一个简单位的示例,说明如何将高级的 SQL 语句转成简单的 KV GET 调用:
在单租户模型中 CockroackDB 的 SQL 层和 KV 层在同一个进程中,所以 SQL 层总是请求本地 KV层,但 KV 总会请求到其它节点上的 KV。这是因为 SQL 请求的数据所在的 Range 可能在其它节点上的 KV 中存储。
多租户架构
我们如何把单租户的CockroachDB 扩展成多租户的?让每个租户可以感觉他们拥有一个独立的 CockroachDB cluster, 并且在性能上和安全方面与其它租户隔离。但是, 我们试图通过共享 CockroachDB的 SQL 层,但这是很难实现的。一个租户的SQL查询很容易把一个SQL节点破坏掉,让同一个进程的其他租户的性能变的不可用。此外共享 SQL 层可能会引入许多跨租户的安全威胁,难以可靠的消灭掉这些威胁。
这些问题可能有效的解决方案是为每个租户提供一组独立的进程,这些进程同时运行 SQL 和 KV层。然而,这又来带来新的麻烦。我们不能在不同租户间共享存储。这就失去了共享多租户中一个主要优点:可以把一些较小的用户数据一起打包到一个共享存储中。
经过在这个问题上的思考,我们发现可以隔离一些组件,同时也可以共享一些组件。既然 SQL 层很难共享, 我们决定让每一个租户独享 SQL 层,以及 KV 层的事务和分布式处理。另一方面, KV 的多副本和存储在所有的租户间共享。通过这种分离的实现,我们可能获得了 "两个世界的最佳结果" — 每个用户的SQL进程安全性和隔离性及共享存储的效率。下面是更新后的架构图,显示了两个独立的每个租户 SQL 节点与共享存储层的交互:
存储节点不在提供租户的 SQL查询,但仍利用单租户的 CockroachDB 提供的强大的分布式存储能力。在不影响数据可用性的情况下检测和修复节点故障。为每个租户的范围区间提供读取和写入协调者可以根据负载进行移动。特别活跃的 Range 会自动的拆分;不怎么访问的 Range 会自动合并。Ranges的平衡是根据节点的负载情况决定。存储节点的热的range会在内存中,冷的数据会放到disk。三副本复制会放到不同的可用区里来保证你的数据安全和保障高可用。
在你看到这个架构中, 你可能对共享架构数据的安全怀疑。我们花了大量的时间来设计和实现强大的安全措施来保护租户数据。每个租户的数据是隔离的,每个租户的数据会存在独立 Key keyspace 中并使用租户的密钥加密。每个租户的SQL写入会生成一个类似: /// 的键 ,而不是生成一类似的 /// 的键。这意味着不同租户生成的键值对被隔离在它们自己的范围内。
除了安全之外,我们还关心确保跨租户的基本服务质量。当多租户同一时间访问同一个 KV 节点会发生什么?为了确保每个用户不会把一个存储节点独占存储节点上的的资源, 我们测量来自每个租户的读写请求的数量和大小,并在超过某个阈值时限制其活动。与 SQL 语句不同, KV调用相对简单的操作, 如键值对上的 GET, PUT 和 DELETE , 可以在共享的KV存储中有效地管理这些操作。
Serverless Architecture
等等, 是不是最后就是一个 serverless 架构就完事了?可以说是也不是。如前所述,我们做给内核做了一个重大升级去支持多租户。但那只是故事的一半。我们同样需要一个重大改进我们可以在 Serverless 中部署多租户 CockroachDB 集群。
我们使用云上提供的 K8S 服务来管理 Serverless cluster,包含:共享存储 和 每个租户的 SQL 节点。每个节点独立运行在一个 Pod 中, 他们只是一个带有虚拟网络,有限 CPU 和内存容量的 Docker 容器。深入研究,你会发现 cgroup 可以限制一个进程的 CPU 和内存资源。这也是使我们可以较容易的限制每个租户基础资源。这也可以让我们把每个租户的 pod 的资源进行限制,来保证每个租户运行较重的任务时尽量的少的相互影响。
这是一个架构图来描述一下大概的部署结构:
”Proxy Pod“ 在 K8S 集群中用来做什么?事实证明,它们非常用:
•这个允许很多租户用一个 IP 地址。当一个租户连接进来, 这个 Prxoy 会从接入进来的 Postgres 的协议中分析出来租户的 Id 以及 Pg 的连接相关的参数。通过解析到的 Id 和 相关的参数,Proxy Pod决定给路由到哪个 SQL 节点。
•在不同租户间平衡调度后面的 SQL 节点。新的连接会被路由到负载低的 SQL 节点。
• Proxy Pod 也可以用于发现滥用服务的现象,这个也是用于保护你的数据安全的一种方法。
• 它可以自动恢复租户不活跃关闭掉的集群。我们后面会在伸缩章节中更加详细的描述它。
在云上的负载均衡的路由器接收到新的连接会路由到 Proxy Pods, Proxy Pods 收接到连接后依赖 租户的 Id 转给 SQL Pod。每一个 SQL Pod 同一时间只能被一个租户拥有, 但一个租户可以有多个 SQL Pod。网络安全策略会禁止不同租户间的 SQL node 进行通信。最后 SQL 节点通过共享的 KV 层获取数据,共享的 KV 层是使用 AWS EBS 或是 GCP PD 这样的块存储构建。
使用 Serverless Cluster 的一个好处就是它创建非常快。在传统模式下创建一个独享集群,大概需要 20-30 分钟, 这个过程大概需要:申请一个新的 VM, 挂载存储, 分配 IP 和 DNS 相关的地址及其它的事情有。但在 Serverless Cluster 中这个往往只需要秒级就可以创建成功,在我们使用了 k8s 集群后只需要在 VM中创建一个 SQL pod 就可以了。
除了创建速度快之外, Serverless SQL Pod 还有一个更大的成本优势。这些 SQL Pod 可以一起部署在一个 VM 中,通过共享一个 OS 上的 CPU 和 内存。这个可以大大降低一些长尾小用户的成本, 因为他们只需要硬件中的一个很小的部分。相比于 VM 至少要预留 1 vCPU 和 1GB 的内存的成本低多了。
伸缩
数据伸缩这块和原来单租户的伸缩有点类似,随着租户的数据量和访问频度的增加,租户的数据会被拆分成更多的 KV Range 并散到多个 storage pod 上。这种拆分方式和原来的 CockroachDB 的方式一样。所以这里就不在详细说明了。
类似的, 随着租户的 SQL 查询和事务处理的数量增加,分配给该租户的计算资源也必须按比例增加。这里有的租户的工作需要十几个甚至上百个 vCPU 来执行, 而有的租户只需要 vCPU 的部分时间。事实上, 我们希望更多的租户都不需要 vCPU, 这是因为大部分开发者尝试使用 CockroachDB Serverless 只是一个 “kicking the tires” (随便的检查|体验一下) 。他们都创建了集群,也许会再运行几个查询,然后就放弃他了,可能还是永远的放弃它了。即使每个租户保持一小部分的 vCPU 为集群闲置,如果存在大量不活跃的租户,也是巨大的资源浪费。即使对于经常使用的集群租户, SQL流量负载也不是恒定的, 它可能每天,每小时,甚至每秒都有很大的波动。
CockroachDB Serverless 如何处理如此宽泛的机器资源需求呢?CockroachDB Serverless 依据租户的秒级级负载情况,动态的将正确数量的 SQL pod 分配给租户。在好的情况下,可以做到立即给租户分配资源,最坏的情况需要秒级才给租户分配资源。这使得即使在极端情况下,租户的流量峰值也可以处理,延迟也很低。类似地, 当流量下降时, 租户的SQL Pods 可以回收后分配给其它租户,这样就有了最小的未使用容量。如果流量降为零,这个租户的所有SQL节点都会被终止, 当新的流量进来了后几百毫秒内,可以从后面的 SQL pod (预热池)中恢复一个新的 SQL 节点进行处理。基于CockroachDB Serverless 集群这种调度方式,让Cockroach Labs 可以为大家提供一个生产级低延迟的实验环境, 而且对于租户来说根本没有成本。
只有在多租户的环境中把 SQL 层和 KV 存储层进行拆分后,才能实现这种响应式伸缩方式。因为 SQL pods 是无状态的,可以随意的创建和收缩, 也不会影响租户数据的一致性或持久性。SQL pods 之间也没有复杂的协调,也不需要仔细的调试和安全退出 SQL pod, 我们只需要保证有状态的 存储pods 数据保持一致性和可用就可以了。存储节点需要运行很长时间,但 SQL pods 可能是非常短暂的,运行几分钟可能就关闭了。
The Autoscaler (自动伸缩)
让我们更深入的讨论一下这个伸缩机制。在每个 Serverless 集群中,都会存在一个自动伸缩的控制组件,该组件控制给每个租户分配理想的 SQL Pods, 不论是一个,多个或是 0 个。自动伸缩控制组件监视着集群中每个 SQL pods 的 CPU 负载,并根据两个指示计算 SQL Pod 的数量:
•最近 5 分钟的 CPU 负载平均值
•最近 5 分钟的 CPU 使用的最高值
基于 CPU 使用率的平均值将分配给租户 “基线”数量的 SQL pod。基于这个基线也会少过度的供应一点 SQL pod, 以便每个租户的 SQL Pod 有备用的 CPU 可以处理立即爆发。但是, 如果近的峰值 CPU 使用率甚至超过更高的过度供应阈值, 那么自动供应比例就会通过调整 SQL pod 基线的数量来解决这个问题。该算法结合了 移动平均线的稳定性和瞬间最大值的响应性。这个自动伸缩控制组件避免了过于频繁的缩放,但仍然可以快速检测和响应负载的巨大峰值。
一个租户一旦获得了一个最理想的 SQL pod 数量, 自动伸缩控制组件就会触发一个 k8s 协调过程,添加或是删除 pod 以达到理想的数量。下图显示了可能的结果:
如图所示,我们维护了一个 “预热” 的 Pod 池, 这些 SQL Pod 随时可以使用被使用,它们只需要给加上 租户的 Id 和安全证书,这个需要几分之一秒的时间,而 k8s 创建一个新的 SQL pod 节点需要 20-30秒。相反,如果需要删除 Pod,则不会突然终止它们,因为这样会导致在该 Pod 上的连接突然终止。相反 SQL Pod 被标置为”驱逐“状态, 这使它他们提供了更好地放弃 SQL 连接,一旦所有的 SQL 连接消息,或者 10 分钟过去了, 以先到为准,”驱逐“状态的Pod将会被终止。
如果应用程序的负载降到零,那么自动伸缩控制品最终决定挂起该租户,这意味着该租户的 SQL pod 将被删,一旦用户不再拥有任何的 SQL Pod,它就不会消耗任何的 CPU , I/O 和带宽。唯一的成本就是存储数据,与其它资源相比,它相对便宜。这也是我们可以为大家提供免费的数据库集群的原因之一。
然而,这里还有一个问题需要解决。当一个租户没有 SQL Pod,但又有新的连接接入,如何处理呢?对于这个问题,记住我们还有一个 Proxy Pod 在这个 Serverless 集群中, 每个 SQL 从外面连接进来是先到 Proxy Pod上,然后转给到后面的 SQL Pod。在这个过程中,如果 Proxy pod 发现当前没有可用的 SQL Pod分配给租户( 有了,就直接从预热池子中分配),那么它就会触发自动伸缩控制器来调度 K8S 来扩容。从预热的 SQL pod 中分配是一个新的节点就是一个盖章的过程,非常快的可以用于新的连接。整个恢复过程只需要不到一秒钟的时间,我们正在努力进一步缩短这个时间。
Databend 是一款开源云原生数仓: https://github.com/datafuselabs/databend