前言
如果你从事后台开发,你或许会接触到分布式系统的相关概念,但可能你对分布式系统还没有一个整体的认知。本文从一个简单的单体应用入手,帮你建立起对分布式系统的整体概念,希望能给你一些学习分布式系统的“方向感”。如果对你有帮助的话,希望给我点个赞哦~
以下是本文的大纲。
文章目录
分布式系统由来
分布式系统是相较于单体应用系统而言产生的一种新的方案和设计理论。传统的单体应用有以下几个特点:
-
代码/功能耦合
-
在同个网络分区对于同一功能服务,部署单一进程实例
例如一些比较小型的运营较久的分区分服模型的mmorpg端游,最直接的例子是在不同服务器的玩家之间数据不互通。
-
实际承载在线用户数和请求量不大
在功能稳定,用户基数和用户群体稳定,系统甚至会控制单服注册人数的前提下,我认为这种模式能稳定运营十几年,也确实有它的合理之处。
毕竟现实中确实有一些游戏,靠着一些特定模式的游戏运营策略,靠着那部分稳定的不会轻易流失的用户,也能给公司带来虽然有一定上限但稳定的收入。
但假设一个系统想要增加系统容量,提高系统的可用性和稳定性,就需要构建分布式系统,毕竟一个单一的关键进程挂了,服务就不可用了对于关键业务而言也是致命的事情。
分布式系统用最简单的话来描述就是把一个进程变成多个进程,一个节点变成多个节点,这样当某个节点挂了其他的节点也可以照常对外提供服务。
“软件开发就是一个按下葫芦浮起瓢的过程”,这是一个从业多年的技术大牛说的话,我觉得还蛮形象的。分布式系统也是如此,解决了单点故障的问题,分布式系统又引入了其他新的问题,如何解决这些问题,就是整个分布式系统的各种理论的由来。
那么分布式系统会面临哪些问题?又应该怎么解决,接下来我将从一个简单的例子入手,从单体应用演化成分布式系统,来大致的讲解这个过程。
如何设计一个最简单的图片托管系统
现在我们想象一下,假设我们设计一个这样的应用系统:
- 支持图片上传
- 支持图片查看
那么一开始的系统架构图可能是这样的:
这是最典型的单体应用,具有以下特征:
- 一个服务器实例处理上传的请求和查看的请求
- 用一个DB实例存储所有图片
- 当客户端请求时,服务器直接读写DB,并返回结果给客户端
请求拆分
当开始有大量的请求时,这种模型就很容易迎来瓶颈。
由于上传图片即写数据需要更多的计算和磁盘操作,即这个过程涉及检查磁盘空间是否足够、寻找一块可用的磁盘块、将内存数据刷新到磁盘等操作,而查看图片只需要查询数据然后返回给客户端;另外上传通常需要和客户端保持一个长时间的连接用以传输数据和写数据,因此上传图片往往比查看图片慢的多。
假设将读写数据的请求都放在一个进程里面,线程之间就需要相互竞争资源,例如当服务器处理上传请求的耗时操作时,其他用户查看图片的请求就容易被阻塞,而事实上这两个请求类型并无直接关联。
因此我们将系统做拆分,拆分为上传服务和查看服务,部署在不同的服务实例中。这样做的好处是服务之间不会相互影响,即当上传图片的服务出现问题时,用户也可以正常地使用查看图片的服务。
服务拆分是分布式系统的基础思想,在这个例子中我们可以看到服务拆分的好处是非常明显的,系统耦合度降低了,各个功能的依赖性降低,也更容易做进一步的系统扩展。
冗余:多个服务节点 多个数据副本
如何做进一步的系统扩展?
- 多个服务节点
在上面的架构中,上传图片服务和查看图片服务做了拆分,但对应功能的服务实例只有一个,当对应功能的服务实例发生故障时相应服务在下一次拉起前就不可用了。面对单点故障,解决办法是部署多个相同的服务节点,这样当其中一个节点挂了的时候,系统也可以正常对外提供对应服务。
- 多个数据副本
处理请求的服务节点可能出现单点故障,自然存储数据的DB也可能因为故障而发生数据丢失,即导致用户的图片丢失。防止数据丢失的手段是备份多份数据,即我们需要多个数据副本做冗余,这些副本可以位于不同的服务器,这样当一个服务器的数据丢失时,就可以启用别的数据副本。
于是架构进一步演变:
冗余是分布式系统实现高可用的重要手段之一。
分区
设想一下,随着越来越多的用户使用我们的系统,上传越来越多的图片,那么DB就会有越来越多的数据,这意味着我们需要扩展DB存储服务器。另一方面,按照现有的设计,所有用户的所有图片都存储在同个DB中,随着数据的增长查询指定数据的速率会越来越慢。
对此,我们有两种扩展方式:垂直扩展和水平扩展。
垂直扩展
垂直扩展是指对单个服务器添加更多的资源,例如,更大的磁盘容量以便容纳更大的数据集,更大的内存、更快的CPU从而提高服务器的计算能力比如提高查找数据的速率,即最终目的是为了让单个服务器能够处理更多的事务。
水平扩展
水平扩展是指扩展更多的节点,我们可以对数据做拆分,即进行数据分区或者分片,让不同的节点存储拆分过的数据集。
在我们的例子中,我们可以将存储图片的数据库或者文件系统拆分成多个库即多个图像集,每个图片拥有自己的唯一ID,这个ID与库建立联系即图片ID与库建立索引关系,这样当我们需要查找图片时,根据图片ID和索引,就可以到对于库的节点上去查找对应图片。
水平扩展的好处是我们可以根据实际情况灵活扩展或者缩减节点,例如当一个存储节点的磁盘满了时,我们可以随时新增一个新的节点来扩展存储服务。相较于垂直扩展,水平扩展更加灵活,机器资源的可控性更强。
当然水平扩展也引入了更多的问题,比如:
-
增加了系统的复杂性
如果拆分的规则不合适可能造成分配不均,需要根据业务做实际调整;
假设需要缩减节点,如何分配缩减节点上已有的数据或流量也是个问题; -
增加管理的难度,管理多个节点增加了运维成本,例如实现自动化扩缩容需要硬件和软件的支持。
垂直扩展相较于水平扩展显得更加简单易用,也不需要处理管理多个节点会面临的问题,但垂直扩展的单点故障以及如果需要提高系统性能只能通过提高单个服务器的处理能力的方式无疑也限制了系统的扩展性和可用性。因此,虽然水平扩展需要面临一些随之带来的挑战,但为了提高系统的扩展性和可用性,水平扩展无疑是更好的选择。当然,我们也需要提高系统的容错能力,处理负载均衡,数据不一致等问题。
于是架构又演变成了这样
如何进一步维护一个高可用的分布式系统
通过这个例子,你或许可以管中窥豹,对分布式系统有一个整体的认知。
在这个例子中,我们将上传图片的功能和查看图片的功能拆分成互不关联的服务,当系统的上传服务或者查看服务需要扩展时,我们可以通过增加对应服务的节点来扩展,数据也会备份多份或者分区。
随着系统扩展,数据库中的图片数量不断增加。即使进行了分区,当请求量急剧增加时,对同一分区数据库的访问可能导致频繁的磁盘I/O操作,从而成为系统瓶颈。
也就是我们会面临的挑战为:
- 对应用服务器的请求量急剧增加
- 对数据库的访问急剧增加
接下来我们就继续通过图片托管系统来讨论分布式系统对于以上的挑战有什么解决方案和策略。
假设用户有TB级别的图片数据存储在我们的应用中,系统允许用户随机访问任意一部分图片。在这种情况下,如何快速响应用户的请求就成为一个挑战。
将TB级别的数据加载到内存中的成本非常高。即使不加载到内存中,访问数据库也需要访问硬盘数据。磁盘的I/O操作通常是数据库操作中最耗时的操作。从磁盘中读取数据比从内存中读取数据慢得多,一般来说,从磁盘读取数据的时间可能需要几毫秒到数秒不等,而在内存中读取数据的时间通常只需要几微秒。数据集越大,这种时间差距会越明显。
在分布式系统中,有一些通用的概念和方案可用于解决此类问题。
- 缓存
- 负载均衡
- 消息队列
缓存
缓存是一种基于内存的技术,即将获取到的数据放到内存或者基于内存的中间件例如Redis中,它具有时效性和有限的空间量。
我们可以把缓存放在请求层节点上,比如放在查询服务的节点上。
缓存是如何提高系统的响应速度的呢?当用户需要第一次查询图片A时,由于缓存中没有对应数据,所以请求层节点需要先发送查询请求给数据层节点,数据层节点再去磁盘中查询对应数据,然后返回给请求层节点,此时请求层节点将查询结果放到本地缓存中,然后返回数据给用户。这里有两个开销,一个是请求层节点远程调用的网络开销,一个是数据层节点查询数据访问磁盘的开销。
根据局部性原理,最近访问的数据很可能会被再次访问。下一次当用户再次查询图片A时,请求层节点就不需要通过远程调用查询数据库,可以直接从缓存中找到数据返回给用户。这样一方面减少了服务的远程调用次数,可以减少网络开销,另一方面通过减少对数据库的访问进而也减少了对磁盘的访问次数,从而提升系统响应时间,提高系统的性能。
使用缓存的问题
-
缓存不一致
缓存实际上是数据在内存中的临时备份,因此当多个用户操作一份数据时就可能出现数据的不一致问题。例如A用户查看图片,此时图片缓存在节点1上,B用户去修改了同张图片的名称,A用户在节点1缓存未失效前去查看这张图片,就可能看到的还是之前的图片名称。 -
缓存击穿/缓存穿透/缓存雪崩
由于缓存具有时效性,另外在未触发从数据库中拉取数据的操作前,缓存中也没有对应数据,因此当出现同时大量的缓存数据失效时,请求依然会直接到达数据层访问DB。热点数据通常在短时间内会有大量的请求访问,大量的瞬时请求在数据库能力有限的前提下可能会导致数据库服务不可用进而导致服务出现故障,因此如何应对这种问题也需要提前做好预案。
负载均衡
在我们的例子中,服务存在多个节点,即请求可以到达任意一个节点,可能存在有些节点的请求分配不均的情况。另外,假设有些节点所在的机器性能较好,有些节点所在机器性能较差,我们也会希望机器性能较好的节点能够承担更多的流量。即我们需要一种负载均衡机制,能够根据实际情况分配流量,以便我们的系统各个节点可以达到比较理想的负载情况。
负载均衡系统是为了处理大量并发的连接,并根据实际情况将连接或者请求路由到一个最恰当的节点上面,从而保证系统可以通过添加节点以服务更多的请求。另外,负载均衡由于需要根据节点的实际负载做流量控制,因此负载均衡系统通常也可以承担部分服务的健康检查的功能,当某个节点不可用时,可以把节点从处理请求的节点池中删除。
消息队列
单个服务节点在单位时间内处理请求的数量是有限的,当请求的数量超过服务器的处理能力时,就会存在一部分客户端需要被迫等待其他客户端的请求完成才能生成响应。这种情况会严重降低客户端和服务器的性能,一方面客户端需要等待服务器响应才能继续往下执行别的操作,在用户看来就是系统有卡顿;另一方面服务器也可能因此发生故障。
所以我们需要一种机制提高服务器对请求的可控性,我们可以在请求层和实际提供服务的应用层之间插入消息队列。
当一个请求到来时,它会被先添加到消息队列中,当服务器有能力处理请求时,就从消息队列中取出一个请求进行处理。这样可以实现异步通信,一方面客户端不需要等待当前请求的响应再去处理别的操作,客户端可以先发送请求,然后定时检查服务器是否响应了任务;另一方面服务器也可以根据自身的处理能力,优雅地从队列中取出消息进行处理,甚至可以根据实际情况丢弃部分超过处理上限的请求,即提高了服务器对请求的可控性。
另外,当服务发生短暂的不可用然后恢复时,服务器还可以选择对消息队列中的请求进行主动的重试操作,这样对于用户而言甚至感知不到服务的短暂的故障,即提高的系统的可用性和健壮性,进而也提高了用户体验。
总结
以上通过一个简单的图片托管服务的例子,大致讲解了从单体应用到分布式系统的演变过程,涉及到了分布式系统的一些基本概念和策略。实际上根据具体业务,分布式系统要复杂的多,也并不是每一个分布式系统都会用到分布式系统所有的理论和策略。对于一些已有架构的系统而言,分布式的演变也是一步一步慢慢进行的。如果你对分布式系统还不太了解,希望这篇文章能够帮你建立起对分布式系统的一个大概的认知。
参考资料
这篇文章是我阅读这篇英文资料再融合自己的理解写的,如有错漏,欢迎批评指正~
https://aosabook.org/en/v2/distsys.html
如果这篇文章对你有所帮助的话,帮忙点个赞吧~
更多内容欢迎关注公众号:pipi的奇思妙想