你好,欢迎来到潮汕IT智库!
您的位置:首页 > IT资讯> 热点新闻 热点新闻
如何提高数据库性能的系统设计方案
2022-07-20 10:02:45 作者: (评论0条)

1658282023492.png

◆  简介

一个有趣的面试问题,我已经听到并问过很多次了。

"你将如何提高数据库的性能?"

这个问题可能有很多答案,因为我想深入了解每个答案,所以我将分别写三篇文章,每篇都针对某一类答案。

这个要更注重架构层面的变化,管理服务等。他们会更关注云计算架构师或对系统设计概念有良好了解的人。

第三组答案将更注重于数据库和操作系统的配置。

请记住,这是一个非常广泛的话题,这是我对如何回答这个问题的看法,我将提供进一步阅读的链接,并尽可能多地提供实际的例子。

◆  问题

问题是,"我的数据库越来越慢,你将如何提高数据库的性能?".在这篇文章中,我假设是一个SQL数据库,特别是Postgres,但这些解决方案是通用的,应该主要适用于任何其他数据库。

在你向下滚动之前,想一想你会怎么回答,如果你发现我的文章中没有包括这个问题,请在评论中告诉我。

◆  可能的答案

请记住,每个答案都是有取舍的。根据不同的情况和问题陈述,有些答案可能不相关。我将尝试解释每个答案的取舍。

◆  垂直扩展数据库

纵向扩展数据库意味着增加你的数据库实例的大小。这可能意味着增加内存、CPU、网络带宽、存储等。

很多人都会讨厌我说这是一个可行的解决方案,但我觉得在很多情况下,这是一个可行的,甚至是唯一的解决方案。但在我阐述我为什么这么想之前,请允许我再解释一下什么是垂直缩放,以及为什么它不总是被认为是一个好的解决方案。

1658282058171.png

垂直扩展只是意味着改进你的数据库服务器。这让你有更多的资源可以利用,并且是一个快速解决你所面临的与扩展有关的大多数问题的方法。然而,最大的缺点是它不具有可持续性,如果经常这样做,会增加技术债务。这意味着,如果你不断增加数据库资源,你可能很快就会达到无法提供更高容量数据库的地步。现在你需要考虑一个系统来提高你的性能,如果你到现在还在不断地增加你的系统资源,现在要迁移这样一个沉重的系统就比较困难了。

除此以外,当你拥有更多的资源时,成本往往会迅速上升。还有许多其他原因导致垂直扩展会产生问题,例如,在你的系统中产生一个单点故障,更难进行灾难恢复,难以进行修补/更新,等等。

有了所有这些缺点,你会认为垂直缩放绝不是正确的解决方案,但它也有一个巨大的优势,那就是时间。垂直扩展是非常快速和容易做到的。只要扔更多的钱,你就能得到更高的性能。

1658282098605.png

根据我的经验,当你必须快速修复一个问题时,在你和你的团队调试问题并找到一个成本更低、更可持续的解决方案时,支付几天的费用是可以的。重要的是要记住,作为工程师,我们希望创造完美的解决方案,但我们为之工作的客户对正常运行时间和成本等指标更感兴趣。

我想是Tim Peters在 "Python的本质 "中写了以下内容。

"特殊情况并不特殊,不足以打破规则。
虽然实用性胜过纯洁性。"

Special cases aren’t special enough to break the rules.
Although practicality beats purity

◆  代理层面的连接池

在上一篇文章中,我们讨论了连接池如何帮助你的应用程序运行更多的并发事务。

为了进一步讨论这个问题,让我们看看一个应用实例。一个简单的REST API连接到一个Postgres数据库。

1658282134137.png

由于一个交易可以在任何时候流经连接,连接池或在应用层面上维护大量的连接有助于向数据库发送大量的交易。

1658282167721.png

上图显示了我们现在可以发送八个事务,因为我们现在在应用程序和数据库之间有三个连接。我在之前的文章中更深入地讨论了这个问题.

然而,这又产生了另一个问题,你的数据库现在需要管理三个连接而不是一个。虽然这对你的应用程序是一个巨大的推动,但这给你的数据库增加了更多的工作。当你的应用程序被水平扩展时,例如在docker容器中容器化或作为Lambda函数运行时,这个问题就会恶化。

1658282210676.png

你可以想象,运行几十或几百个容器/无服务器函数,每个都有5-10个连接,会对数据库造成多大的负荷。

当运行默认的Postgres docker镜像时,我得到的变量max_connections的值是100。这是可配置的,但增加更多的连接需要更多的内存,所以你的数据库的连接数是有限制的。

所以,我们有一个问题。在应用端有大量的连接对应用来说是件好事,因为它现在可以并发发送更多的事务,但所有这些连接都会漏到数据库中,而数据库现在必须承担所有这些连接的成本。横向扩展数据库也不是一种选择,因为数据库很难横向扩展。

实际真正的问题是,数据库在做两件事。一件是存储、检索和插入数据的实际责任,另一件是存储大量的连接。

那么,解决方案是什么?在中间添加一个代理来处理连接!

1658282241577.png

代理可以作为你的数据库的一种漏斗。它可以承担起管理所有与应用服务器的数据库连接的重任,而只将其中的几个连接暴露给你的数据库。

代理还有很多其他的优势,比如灾难恢复和帮助故障转移,安全,使你的应用程序更健壮等等,但由于这篇文章是专门针对性能的,我就不多说其他的优势了。

当你有大量的数据库连接时,数据库代理可能是有用的,这通常会发生在你有大量的应用程序运行实例时。常见的例子是运行无服务器功能或运行大量的Docker容器。另外,即使你有较多的数据库连接,代理也只能在你没有大量的事务或连接大部分是空闲的情况下工作。代理只是把你正在执行的事务,合并到一个较小的连接池上,如果你想支持极高数量的并发事务,那么代理可能不是你正在寻找的解决方案。

数据库代理还为你的系统增加了另一个组件,从而增加了其复杂性和成本。有很多数据库代理可供选择,像RDS代理这样的管理服务在它所提供的丰富功能方面是惊人的,而且很容易设置,但比ProxySQL这样的开源代理的成本要高。

◆  使用消息队列的异步通信

当你按部就班地进行操作时,你是同步进行的,这意味着你首先执行步骤1,等待它完成,然后是步骤2,等待步骤2完成,然后是步骤3,以此类推。让我们举一个简单的例子,一个连接到数据库的REST API。API收到一个更新数据库中某些数据的POST请求,它在数据库中执行一个命令,等待数据库发送一个响应,然后向用户返回一个适当的响应。这是一个同步的流程。

1658282272399.png

同步流动

让我们将其与异步通信进行对比。在异步通信中,API将不会等待数据库的到来。它可以简单地返回给用户的响应,即它已经接受了请求,而数据库将在API已经对用户作出响应后作出响应。

1658282305417.png

异步流

你可能会想,当你还没有执行数据库查询的时候,你怎么会向用户返回一个响应。有一些用例是可以这样做的。有时在更新或插入数据时,你可以假设数据会被插入并更新用户,你已经得到了他/她的更新请求。

同步与异步的调用真的取决于你的使用情况。有时用户需要即时反馈,但偶尔你也可以等待几秒钟,甚至几分钟来执行更新。例如,当用户添加评论时,你会希望该评论是即时可见的,因为这被用作不同用户之间快速沟通的形式。然而,也许对于上调或下调,你可以不同步更新数据库,而是将上调/下调添加到一个队列中,以便以后处理。一个服务可以轮询队列中新的加注/减注请求,并可以相应地更新数据库。

队列可以使其更容易处理流量的峰值,因为它可以作为一个缓冲区来存储请求。

缺点是数据的一致性,这取决于你的实现。由于你的数据在队列中而不是在数据库中停留一小段时间,这意味着它对你的API(将查询你的数据库)来说基本上是不可见的,进而对你的用户也是不可见的。你的数据一致性可能只是几秒钟或更多,这取决于你的实现。

另一个有趣的反问是,你将使用哪种队列来存储这些请求,仅仅存储在内存中可能是一个答案,但这也有局限性(比如成本很高),并可能导致其他副作用,比如使你的服务器具有状态。像Redis这样的东西可以是一个很好的解决方案,因为它支持在内存中存储数据,也支持在磁盘中持久化。

这个解决方案的一个很好的用例是来自用户的行动,这些行动是即发即忘的(在这种情况下,用户根本不关心回应,例如,报告一个堆栈溢出问题,用户不期望立即得到回应)。

简而言之,如果你能容忍某种程度的数据不一致,而且你主要是为了处理不可预测的请求高峰,这是一个很好的答案。

让我们快速谈论一下实现,你通常有两种方法来实现这个,一种是添加另一个小的工作者服务,轮询队列并将数据推送到你的数据库。

1658282336467.png

或者你可以使用一个插件,如果你能找到一个,避免额外的服务。This是一个很好的起点。

◆  改变你的数据库

数据库通常是为特定的使用情况而建立的。当然,你可以尝试以一种它们并不打算被使用的方式来使用它们,它可能会起作用,但你可能会面临性能、数据完整性、一致性等问题。

例如,SQL数据库有很多很好的功能,比如对ACID的良好支持,创建关系以表格形式存储数据的能力,连接关系的能力,等等。然而,如果你的数据是非结构化的,将其存储在SQL数据库中并不容易(不过也不是不可能,有一些数据模型如 EAV这样的数据模型也存在于非结构化数据的模型中)。如果你的要求是存储非结构化数据,基于文档的数据库可能会更好地满足你的需求。

除了你想存储的数据外,还有其他因素需要考虑,例如,性能。某些数据库为特定的使用情况提供更好的性能。

例如,列式数据库将以面向列的方式存储数据,并且能够比SQL或文档数据库更快地执行列聚合查询。因此,如果你想获取所有行的列和/或对其执行聚合功能,像Cassandra或Redshift这样的东西会比Postgres或Mongo快很多。

除此之外,一些数据库将数据存储在内存中而不是磁盘中。从内存中检索数据比从磁盘中检索数据要快得多,所以这些数据库的数据检索速度明显要快。Redis就是一个很好的例子。不过它们也有缺点,比如不支持存储关系型数据,或者因为数据现在存储在内存中而不是磁盘中,所以存储数据的成本更高。我在以前的文章中写了很多关于Redis的内容,有很多实用的项目,所以请查看更多关于Redis的内容。here.

简而言之,数据库是为特定的使用情况而建立的,有些是为了解决特定的问题。尝试找到一个能很好地解决你的问题的数据库总是一个好主意。

这种解决方案的缺点是,你需要将你的数据从一个数据库迁移到另一个数据库,而数据迁移并不简单或直接。你还需要在应用层面上做重大改变,以使用新的数据库。这将需要开发和测试时间。

◆  添加一个辅助数据库

很多时候,一个数据库并不能满足你的所有要求。当你想使用多个数据库时,有几个很好的例子可以说明。

例如,也许你想存储具有ACID属性的关系型数据,但也想让更多的流行数据能够非常迅速地被使用。这方面的一个很好的例子可能是我们的Stackexchange数据,我一直在用它做例子。

简而言之,这就是我们数据的使用情况。

1658282364561.png

95%的请求是针对同样的前10%的帖子。事实上,50%以上的请求是针对前1%的帖子。

1658282392511.png

我在我的文章中更深入地谈到了这个问题。previous post如果你有兴趣的话。

简单的想法是,我们需要将流行的数据存储在一个缓存中,比如Redis,其余的数据存储在Postgres。我们不能把所有的数据都存储在Redis中,因为Redis的存储可能相当昂贵。

根据用户如何使用我们的服务,我们可以根据用户如何使用我们的服务来定义数据如何被发送到Redis和Postgres。例如,根据使用情况的统计,我们发现大多数帖子在一天内很受欢迎,然后就很少再被请求。我们可以有一个简单的架构,最初我们将帖子存储到Redis和Postgres,并每12小时运行一个cron工作,简单地将超过一天的帖子转移到Postgres。由于现在大多数的检索都发生在Redis上,我们的Postgres服务器也可以更容易地处理它所得到的请求。

这就是流程的模样。

1658282421662.png

这里的缺点是使你的系统更加复杂,一般来说成本也更高。除此之外,你还必须考虑如何处理每个数据库中的数据,如果用户更新了数据,需要在多个数据库中如何更新,如何快速运行你的cron或你想出的其他解决方案。

一般来说,这使你的系统变得复杂,并为更多的问题打开了空间,尽管如果使用得当,那么这可以成为修复性能甚至增加更好功能的一个伟大的解决方案。

添加读取副本

正如我们已经看到的,很多应用程序是重读的。这意味着我们在数据库上得到的大多数请求都是读请求,而不是写请求。这实际上是一件好事,因为扩展大多数数据库以处理更多的读取请求是比较容易的。

我们可以创建复制主数据库的读副本。这是一个独立的数据库,甚至可以在不同的服务器上运行。在不同实例上运行的多个数据库可以通过网络交换数据并进行通信。

架构成为

1658282448101.png

主数据库为写保留,读副本可以处理读。你甚至可以在同一个数据库中添加多个读副本,以服务于更多的数据库读。

那么,这如何提高性能呢?你的读取请求(占你流量的大部分)现在可以被分割成多个数据库,每个数据库都运行在不同的硬件上,有自己的CPU、内存和网络带宽。

你需要回答的一个基本问题是如何同步这些数据库。通常有两种选择,对每个请求进行同步更新,主数据库的每一次更新都会首先同步到读取的副本,然后将响应返回给用户。

1658282473916.png

或者做一个异步更新,更新只是写到你的主数据库,在某些时候,你把你的读副本数据库同步到你的主数据库。

1658282505944.png

你可能已经猜到了,这也造成了很大的弊端。例如,如果你异步同步你的读取副本,你的数据库可能不同步。然而,同步请求会使进行数据库更新的时间增加一倍,因为它们现在应该发生在两个数据库上。

如果数据的一致性不是特别重要,而且你的大部分流量都是重读的,那么添加异步更新的读副本可以是一个很好的工具。

◆  在回答问题前先反问

在回答这个问题之前,你一般应该问几个反面的问题,以帮助更好地理解这个问题。这些可以帮助你衡量系统中的瓶颈问题。整个系统可能相当复杂,可能有很多原因导致数据库开始表现不佳。为了更好地了解原因,并更好地理解数据库的要求,你可以向面试官提出一些问题,这些问题可以帮助你找出最佳解决方案。

由于这一部分需要对上面的答案有一定的了解,所以我在讨论了可能的答案后将其列入,但你在回答之前可能应该提出反问。

◆  是读取性能慢还是写入性能慢?

一个非常重要的因素可以推动你的决策,就是有关数据库的读写性能如何。有些解决方案可能会提高读取性能(如添加读取副本),有些可能会提高写入性能。

◆  了解用户如何使用你的服务

这对于做出所需的一致性、性能要求和可用性的决定至关重要。很多修复性能的方法可能会影响你的数据库的一致性。例如,增加一个队列并以异步方式而不是同步方式进行更新会影响你的数据库的一致性。

了解用户模式,用户何时使用你的服务也很重要。一个监测服务可能会被24小时持续使用,但一个电子商务网站可能会在一天中的特定时段看到非常高的峰值。

像在你的数据库前面使用队列,扩大你的缓存,增加更多的读取副本等解决方案可以很好地处理高峰期的流量,而缩小你的缓存,在用户不使用你的服务时删除读取副本以节省成本。

◆  用户数量

大量的消费者通常意味着大量的连接,这可能会严重影响数据库。如果达到其连接数的限制,数据库将拒绝为新的连接提供服务,这可能会影响数据库的性能。

如果你的数据库有很多消费者(例如,大量的无服务器功能或在ECS、EKS、GKE等容器编排服务上运行的容器),你可能需要采用一种解决方案来解决这个问题。

你有很多选择,添加队列、添加数据库代理、添加读复制、或分片数据库都是可能的解决方案。

◆  时间线?

推动我决策的一个关键因素是时间表。如果数据库表现不佳,用户感到沮丧并转而使用竞争对手的产品,那么就必须尽快找到一个解决方案,即使它带有技术债务或更高的成本。

这是一个很好的例子,说明垂直缩放可能比水平缩放更好。

◆  了解成本计算

重要的是要知道这些改进如何影响你公司的钱包。

例如,使用像RDS这样的管理服务,将减少开发人员的时间和减少错误,但也会更昂贵。

在可能的情况下,确保你的解决方案也是具有成本效益的,这一点很重要。最好是在你可以的时候考虑开源服务并托管它们,只有在你认为使用开源服务可能不可行或可能更复杂的时候才考虑托管服务。

◆  总结

我还想介绍几个要点,那就是分片和缓存,但是,这篇文章已经变得非常长了,所以不会在这篇文章中介绍。但是,如果你有兴趣,你可以对这些要点进行更多的研究,以更好地理解它们。

相关文章
红帽RHEL将成为微软官方WSL发行版...
请求都有并发数的限制...
Vue 3 编译器...
C++ 的两个派系之争...