我们现在所看到的大型网站或者架构,都是从小的网站和简单的架构一步步发展起来的,当然,也有一些是基于已有的分布式架构来构建的,也是看业务发展的情况而定。在架构的迭代演进的过程中,会遇到很多问题,就像升级打怪一样,等级越高,遇到的怪兽越强。
之前有个学员问了我,什么是架构。我是这么回答的。比如我们要建一栋房子,那建房子之前,一定要有一个建筑图纸,这个图纸描述了建筑的形状、内部结构、材料、设备等信息。工程实施的时候会基于这个图纸进行构建。软件架构也是如此,软件架构相当于软件系统的一个设计图纸,这个图纸上描述了各个组件之间的连接方式和详细的描述了组件之间的通讯机制。 而程序员在实施阶段,就是将这些抽象图纸细化为实际组件,比如具体的接口定义,类的定义等
那么我们接下来基于纯技术角度模拟一个简单的案例来看看架构迭代带来的问题和解决方案,通过这样一个迭代让大家更清晰的理解架构。整个过程,重点关注的是数据量和访问量的变化带来架构的变化。不具体关注业务功能
为了更好的理解,我们用电商网站来举例,作为一个交易类型的网站,一定会具备
用户(用户注册、用户管理)、商品(商品展示、商品管理)、交易(下单、支付)这些功能
假如我们只需要支持这几个基本功能,那么我们最开始的架构应该可能是这样的
这个地方要注意的是,各个功能模块之间是通过JVM内部的方法调用来进行交互的,而应用和数据库之间是通过JDBC进行访问。
随着网站的开放,访问量不断增大,那么这个时候服务器的负载势必会持续升高,必须要才需一些办法来应付。这里先不考虑更换机器和各种软件层面的优化,先从架构的结构上来做一些调整。我们可以把数据库与应用从一台机器分到两台机器
变化:
网站从一台变成了2台,这个变化对我们来说影响非常小。单机的情况下,我们应用采用JDBC的方式来和数据库进行连接,现在数据库与应用分开了,我们只需要在配置文件中把数据库的地址从本机改成数据库服务器的ip地址就行。对于开发、测试、部署都没有影响
调整以后我们能够缓解当前的系统压力,不过随着时间的退役,访问量继续增大的话,我们的系统还是需要做改造
**为什么这么分呢?**从计算机本身的角度来考虑的话,一个请求的访问到处理最终到返回,性能瓶颈只会是:CPU、文件IO、网络IO、内存、等因素。而一台计算机中这些纬度是有性能瓶颈的,如果某个资源消耗过多,通常会造成系统的响应速度较慢,所以增加一台机器,使得数据库的IO和CPU资源独占一台机器从而增加性能。
这个地方插入一点题外话,就是简单说一下各个资源的消耗原因。
**CPU/IO/**内存:
主要是上下文的切换,因为每个CPU核心在同一时刻只能执行一个线程,而CPU的调度有几种方式,比如抢占式和轮询等,以抢占式为例,每个线程会分配一定的执行时间,当达到执行时间、线程中有IO阻塞或者有高优先级的线程要执行时。CPU会切换执行其他线程。而在切换的过程中,需要存储当前线程的执行状态并恢复要执行的线程状态,这个过程就是上下文切换。比如IO、锁等待等场景下也会触发上下文切换,当上下文切换过多时会造成内核占用比较多的CPU。
文件IO,比如频繁的日志写入,磁盘本身的处理速度较慢、都会造成IO性能问题
网络IO,带宽不够
内存,包括内存溢出、内存泄漏、内存不足
实际上不管是应用层的调优也好,还是硬件的升级也好。其实无非就是这几个因素的调整。
假如说这个时候应用服务器的压力变大了,根据对应用的检测结果,可以针对性的对性能压力大的地方进行优化。我们这里考虑通过水平扩容来进行优化,把单机变为集群
应用服务器从一台变为两台,这两个应用服务器之间没有直接的交互,他们都依赖数据库对外提供服务,那么这个时候会抛出两个问题
对于这个问题,可以采用DNS解决,也可以通过负载均衡设备来解决
对于大型的分布式架构而言,我们一直在追求一种简单、优雅的方式来应对访问量和数据量的增长。而这种方式通常指的是不需要改动软件程序,仅仅通过硬件升级或者增加机器就可以解决。而这种就是分布式架构下的伸缩设计
伸缩分为垂直伸缩和水平伸缩两种
**垂直伸缩:**表示通过升级或者增加单台机器的硬件来支撑访问量以及数据量增长的方式,垂直伸缩的好处在于技术难度比较低,运营和改动成本也相对较低。但是缺点是机器性能是有瓶颈的,同时升级高性能的小型机或者大型机,成本是非常大的。这也是阿里去IOE的一个原因之一
**增加CPU核心数:**增加CPU后系统的服务能力能够得到大的增长,比如响应速度、同时可以处理的线程数。但是引入CPU后也会带来一些显著的问题
1.锁竞争加剧;多个线程同时运行访问某个共享数据,那么就涉及到锁竞争,锁竞争激烈时会导致很多线程都在等待锁,所以即时增加CPU也无法让线程得到更快的处理。当然这里是有调优手段的,可以通过调优手段来降低锁竞争*
2.支撑并发请求的线程数是固定的,那么即时增加CPU,系统的服务能力也不会得到提升*
3.对于单线程任务,多核心CPU是没有太大的作用的*
***增加内存:**增加内存可以直接提成系统的响应速度,当然,也有可能达不到效果,就是如果JVM堆内存是固定的。
**水平伸缩:**通过增加机器来支撑访问量及数据量增长的方式,成为水平伸缩,水平伸缩理论上来说没有瓶颈,但是缺点是技术要求比较高,同时给运维带来了更大的挑战
垂直伸缩和水平伸缩都有各自的有点,我们在实际使用过程中都会对两者做结合,一方面要考虑硬件升级的成本,一方面要考虑软件改造的成本。
服务路由,基于负载均衡设备来实现
引入负载均衡器以后,会带来session相关的问题
将请求按顺序轮流分配到后台服务器上,均衡的对待每一台服务器,而不关心服务器实际的连接数和当前的系统负载
缺点:当集群中服务器硬件配置不同、性能差别大时,无法区别对待
通过系统随机函数,根据后台服务器列表的大小值来随机选取其中一台进行访问。随着调用量的增大,其实际效果越来越接近于平均分配流量到后台的每一台服务器,也就是轮询法的效果
优点:简单使用,不需要额外的配置和算法。
缺点:随机数的特点是在数据量大到一定量时才能保证均衡,所以如果请求量有限的话,可能会达不到均衡负载的要求。
根据服务消费者请求客户端的IP地址,通过哈希函数计算得到一个哈希值,将这个哈希值和服务器列表的大小进行取模运算,得到的结果便是要访问的服务器地址的序号。采用源地址哈希法进行负载均衡,相同的IP客户端,如果服务器列表不变,将映射到同一个后台服务器进行访问。
不同的后台服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不一样。跟配置高、负载低的机器分配更高的权重,使其能处理更多的请求,而配置低、负载高的机器,则给其分配较低的权重,降低其系统负载,加权轮询很好的处理了这一问题,并将请求按照顺序且根据权重分配给后端
前面几种方式都是通过对请求次数的合理分配最大可能提高服务器的利用率,但是实际上,请求次数的均衡并不能代表负载的均衡。所以,引入了最小连接数法。它正是根据后端服务器当前的连接情况,动态的选取其中当前积压连接数最少的一台服务器来处理当前请求,尽可能的提高后台服务器利用率,将负载合理的分流到每一台服务器。
我们打开一个网页,基本上需要浏览器和web服务器进行多次交互,我们都知道Http协议本身是无状态的,这也是http协议设计的初衷,客户端只需要简单的向服务器请求下载某些文件,无论是客户端还是服务器都没必要记录彼此过去的行为,每一次请求之间是独立的,好比一个顾客和一个自动售货机之间的关系一样.
而实际上,我们很多的场景都需要带有状态的特性,因此聪明的我们引入了session+cookie机制来记住每次请求的会话。
在会话开始时,给当前会话分配一个唯一的会话标识(sessionid),然后通过cookie把这个标识告诉浏览器,以后在每次请求的时候,浏览器都会带上这个会话标识来告诉web服务器请求属于哪个会话。在web服务器上,各个会话有独立的存储,保存不同会话的信息。
如果遇到禁用cookie的情况,一般的做法就是把这个会话标识放到URL的参数中。
而我们应用服务器从一台变成两台后,就会遇到session问题
Session共享在当前这个互联网背景下,已经不是一个新鲜的话题了,而且如何解决session共享其实也有很多非常成熟的方案
服务器实现的session复制或session共享,这类型的共享session是和服务器紧密相关的
我们在Web服务器之间增加了会话数据的同步,通过同步就保证了不同Web服务器之间Session数据的一致。一般应用容器都支持Session Replication方式
存在问题:
同步Session数据造成了网络带宽的开销。只要Session数据有变化,就需要将数据同步到所有其他机器上,机器越多,同步带来的网络带宽开销就越大。
每台Web服务器都要保存所有Session数据,如果整个集群的Session数据很多(很多人同时访问网站)的话,每台机器用于保存Session数据的内容占用会很严重。
这个方案是靠应用容器来完成Session的复制从而解决Session的问题的,应用本身并不关心这个事情。这个方案不适合集群机器数多的场景。
利用成熟的技术做session复制,比如12306使用的gemfire,比如常见的内存数据库如Redis
Session数据不保存到本机而且存放到一个集中存储的地方,修改Session也是发生在集中存储的地方。Web服务器使用Session从集中存储的地方读取。这样保证了不同Web服务器读取到的Session数据都是一样的。存储Session的具体方式可以是数据库
存在问题:
读写Session数据引入了网络操作,这相对于本机的数据读取来说,问题就在于存在时延和不稳定性,不过我们的通讯基本都是发生在内网,问题不大。
如果集中存储Session的机器或者集群有问题,就会影响到我们的应用。
相对于Session Replication,当Web服务器数量比较大、Session数比较多的时候,这个集中存储方案的优势是非常明显的。
很容易想到就是利用cookie,但是客户端存在风险,数据不安全,而且可以存放的数据量比较小,所以将session维护在客户端还要对session中的信息加密。
我们的Session数据放到Cookie中,然后在Web服务器上从Cookie中生成对应的Session数据。这就好比我们每次都把自己的碗筷带在身上,这样去那家饭店就可以随意选择了。相对前面的集中存储方案,不会依赖外部的存储系统,也就不存在从外部系统获取、写入Session数据的网络时延、不稳定性了。
存在问题:
安全性。Session数据本来都是服务端数据,而这个方案是让这些服务端数据到了外部网络及客户端,因此存在安全性上的问题。我们可以对写入的Cookie的Session数据做加密,不过对于安全来说,物理上不能接触才是安全的。
随着业务的继续增长,数据量和访问量持续增加。对于大型网站来说,有不少业务是读多写少,这个情况也会直接反馈到数据库上。那么对于这种情况来说,我们可以考虑采用读写分离的方式来优化数据库的压力
这个结构的变化会带来两个问题
我们希望通过读库来分担主库上读的压力,那么首先需要解决的是怎么复制到读库的问题。数据库系统一般都提供了数据复制的功能,我们可以直接使用数据库系统自身的机制。不同的数据库系统有不同的支持,比如Mysql支持Master+slave的结构提供数据复制机制
对于应用来说,增加一个读库对结构变化产生了一定的影响,也就是我们的应用需要根据不同的情况来选择不同的数据库源
搜索引擎其实可以理解成一个读库,我们的商品存储在数据库中,而网站需要提供用户实时检索的功能,尤其是在商品搜索这块。对于这样的读请求,如果全部走读库,其实性能也会存在一个瓶颈。而使用搜索引擎,不仅仅能大大提高检索速度。还能减轻读数据库的压力
而搜索引擎最重要的工作,就是需要根据被搜索的数据来构建索引,而随着被搜索的数据的变化,索引也需要相应变化。
搜索集群的使用方式和读库的使用方式是一样的,只是构建索引的过程基本都是需要我们自己来实现。可以从两个纬度对搜索引擎构建索引的方式进行规划,一个是按照全量/增量划分。一种是按照实时/非实时划分。
全量方式用于第一次建立索引,可能是新建,也可能是重建。而增量的方式是在全量的基础上持续更新索引。
实时和非实时提现在索引更新的时间上,实时是最好的,非实时主要考虑到对数据源头的保护
总的来说,搜索引擎技术解决了站内搜索时某些场景下的读的问题,提供了更好的查询效率。
在大型网站中,基本上就是在解决存储和计算的问题,所以存储是一个很重要的支撑系统。网站建设初期我们都是从关系型数据库开始的,而且很多时候为了方便,我们会把一些业务逻辑放在数据库里面去做,比如触发器、存储过程。虽然在前期能够很方便的解决问题,但是在未来的发展过程中会带来很多的麻烦,比如数据量大了以后,要做分库分表操作等. 同时,业务发展到一定的体量以后,对存储的需求不能完全通过关系型数据库来满足
对一些图片、大文本,使用数据库就不合适了,所以我们会采用分布式文件系统来实现文件存储,分布式文件系统有很多产品、比如淘宝的TFS、google的GFS。还有开源的HDFS
NoSQL 我们可以理解成Not Only SQL、或者是No SQL。 两种意思都是为了表达在大型网站中,关系型数据库可以解决大部分问题,但是对于不同内容的特征、访问特征、事务特征等对存储的要求是不一样的。NoSQL是定位于是文件系统和SQL关系型数据库之间的范畴。
大型网站内部都会用到一些数据缓存,主要用于分担数据库的读的压力,缓存系统一般是用来保存和查询键值对的。应用系统中一般会把热点数据放入到缓存,而缓存的填充也应该是由应用系统完成。如果数据不存在,则从数据库独处数据后放入缓存。随着时间的推移,当缓存容量不够需要清除数据时,最近不被访问的数据就会被清理掉。还有一种方式就是在数据库的数据发生变化后,主动把数据放入到缓存系统中,这样的好处是数据变化时能够及时更新缓存的数据,不会造成读取失效
除了数据缓存外,我们还可以对页面做缓存,数据缓存可以加速应用在响应请求时的数据读取数度,但是最终展示给用户的还是页面,有些动态产生的页面或者访问量特别高的页面,我们会对页面或者内容做一些缓存。
我们应用最多的主要还是关系型数据库,但是在有些场景中,关系型数据库不是很合适。所以我们会引入分布式存储系统,比如redis、mongoDB、cassandra、HBase等。
根据不同的场景和数据结构类型,选择合适的分布式存储系统可以极大提高性能。分布式系统通过集群提供一个高容量、高并发访问、数据冗余融债的支持。
通过读写分离以及在某些场景用分布式存储系统替换关系型数据库的方式,能够降低主库的压力,解决数据存储方面的问题,不过随着业务的发展,我们的主库也会遇到瓶颈。推演到现在,我们的网站各个模块:交易、商品、用户数据都还是存储在一个数据库。尽管增加了缓存、读写分离的方式,但是数据库的压力仍然在持续增加,因此我们可以对数据垂直拆分和水平拆分来解决数据库压力问题
垂直拆分的意思是把数据库中不同的业务数据拆分到不同的数据库中,那么根据我们推演的例子,把用户、交易、商品的数据分开
不同业务的数据从原来的一个数据库拆分到了多个数据库中,那么就需要考虑到如何处理原来单机跨业务的事务
使用分布式事务解决
去掉事务或者不追求强事务的支持
对数据进行垂直拆分后,解决了把所有业务数据放在一个数据库中的压力问题,并且也可以根据不同业务的特点进行更多的优化
与垂直拆分对应的还有数据水平拆分,数据水平拆分就是把同一个表的数据拆分到两个数据库中,产生数据水平拆分的原因是某个业务的数据表的数据量或者更新量达到了单个数据库的瓶颈,这个时候就可以把表拆到两个或者多个数据库中。
数据水平拆分与读写分离的区别是,读写分离解决的是读压力大的问题,对于数据量大或者更新量大的情况并不起作用。
数据水平拆分与数据垂直拆分的区别是,垂直拆分是把不同的表拆分到不同的数据库,而水平拆分是把同一个表拆分到不同的数据库中。
我们可以进一步把用户表拆分到两个数据库中,它们拥有结构一模一样的用户表,而且每个库中的用户表都只涵盖了一部分的用户,两个数据库的用户和在一起就相当于没有拆分之前的用户表
水平拆分带来的影响
sql路由问题,需要根据一个条件来决定当前请求发到那个数据库中
主键的处理,不能采用自增id,需要全局id
由于同一个业务的数据被拆分到不同的数据库,因此涉及到一些查询需要跨两个数据库获取,如果数据量太大并且需要分页,就比较难处理了
前面讲的读写分离、分布式存储、数据垂直拆分和水平拆分都是解决数据方面的问题,接下来我们要看看应用方面的变化
随着业务的发展,应用的功能会越来越多,应用也会越来越大,我们需要思考如何不让应用持续变大,这就需要把应用拆开,从一个应用变为两个甚至是多个。
第一种方式
根据业务的特性把应用拆分,在我们的例子中,主要业务功能分三个部分、用户、商品、交易。我们可以把原来的一个应用拆成分别以交易和商品为主的两个应用,对于交易和商品都会有设计使用用户的地方,我们让这两个系统自己完成涉及用户的工作,而类似用户注册、登录等基础的用户工作,可以暂时交给两个系统之一来完成
我们还可以按照用户注册、用户登录、用户信息维护等再拆分,变成三个系统,不过这样拆分后在不同系统中会有一些相似的代码,比如用户相关的代码,如何能够保障这部分代码的一致以及如何对其他模块提供复用也是需要解决的问题。而且,这样拆分出来的新系统之间没有直接的相互调用
我们在来看一下服务化的做法,我们把应用分为三层,处于最上端的是web系统,用于完成不同的业务功能,处于中间的是一些服务中心,不同的服务中心提供不同的业务服务;处于最下层的则是业务的数据库
与之前相比有几个重要的变化,首先业务功能之间的访问不仅仅是单机内部的方法调用,还引入了远程的服务调用。其次,共享代码不再是散落在不同的应用中,这些实现被放在各个服务中心。最后,数据库的连接也发生了一些变化,我们把数据库的交互工作放到了服务中心,让前端的web应用更加注重与浏览器的交互工作,而不必过多关注业务逻辑的事情。链接数据库的任务交给响应的业务服务中心了,这样可以降低数据库的连接数。
而服务中心不仅把一些可以共用的代码集中管理,而且还使得这些代码变得更好维护。
服务化的方式会带来很多好处,首先,从结构上来看,系统架构更加清晰了,比原本的架构更加立体。从稳定性上来看,一些散落在多个应用系统中的代码变成了服务并且由专门的团队进行统一维护,一方面可以提高代码的质量,另一方面由于基础核心模块相对稳定,修改和发布的频次相对于业务系统来说会少很多,这也会提高整个架构的稳定性。最后,更加底层的资源由服务层统一管理,结构更加清晰,对于团队开发效率来说有比较大的提高
服务化的方式,对于研发也会有很大的影响,以前的开发模式是几个大团队负责几个大应用,随着服务化的落地,我们的应用数量会飞速增长,系统内部的依赖关系也会变的错综复杂,同时团队也进行了拆分,每个小团队专注于某个具体的服务或者应用上,迭代效率也会更高
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自
Mic带你学架构
!
如果本篇文章对您有帮助,还请帮忙点个关注和赞,您的坚持是我不断创作的动力。