前言
写这篇博客的主要目的是:结合原本参与的两个系列的产品,来梳理、归纳、总结分布式的演变,以及相关中间件的运用,给自己有一个宏观层面上的理解与概念。可能是一些概念性的东西,但是当真的回过头来看时,真的是有很多感触。
传统垂直式架构
笔者接触的都是电商类型的网站,这边以电商为例。在传统的架构中,我们的整个架构图大致是这样的。
简单的来说,服务在一起开发,打个war包上去,丢到服务器里去,就可以访问了。我们不关心你的实际业务用是什么开发,不管是SSM还是SSH,这些不关键。
垂直扩容
随着客户的发展,使用的用户越来越多,历史数据越来越庞大。然后客户发现系统运行越来越卡了,查询越来越慢了。然后他们查了一下,内存小了,磁盘不够了,于是该加内存的加内存,该加磁盘的加磁盘(实际我遇到的过程中,客户一听说要加磁盘就开始扯皮,有好几次都是删日志归档文件来减少磁盘空间的),该加带宽的加带宽。
这种的以升级硬件的方式的抗并发和访问量的行为,称之为垂直扩容。
垂直扩容的好处就是简单,不会对后续的程序产生任何影响。实施部署起来的成本比较低。
但是一台机器始终是存在性能瓶颈的,当你的访问量,并发量达到一个极其恐怖的程度的时候,你再去增加内存,磁盘,网络IO 你会发现他越来越贵(好比4G内存条和8G内存的差额 小于64G内存与32G的差额)。这也是阿里去IOE的一个原因。
水平扩容
由于单台服务器的本身限制问题,以及升级越来越贵的问题,之后发展成为增加一台服务器,来去扛访问量。而这样一种再水平方向上增加服务器的方式,称之为水平扩容。概念图如下:
这里产生了第一个问题,我现在有两台服务器,我应该把这个服务分发到哪台服务器上。这里面就涉及到路由与负载均衡的问题。
常见的负载均衡算法有以下几种:
轮询:
将请求按顺序轮流分配到后台服务器上,均衡的对待每一台服务器,而不关心服务器实际的连接数和当前的系统负载
缺点:当集群中服务器硬件配置不同、性能差别大时,无法区别对待
权重轮询:
不同的后台服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不一样。跟配置高、负载低的机器分配更高的权重,使其能处理更多的请求,而配置低、负载高的机器,则给其分配较低的权重,降低其系统负载,加权轮询很好的处理了这一问题,并将请求按照顺序且根据权重分配给后端
随机法:
通过系统随机函数,根据后台服务器列表的大小值来随机选取其中一台进行访问。随着调用量的增大,其实际效果越来越接近于平均分配流量到后台的每一台服务器,也就是轮询法的效果
优点:简单使用,不需要额外的配置和算法。
缺点:随机数的特点是在数据量大到一定量时才能保证均衡,所以如果请求量有限的话,可能会达不到均衡负载的要求。
源地址哈希法
根据服务消费者请求客户端的 IP 地址,通过哈希函数计算得到一个哈希值,将这个哈希值和服务器列表的大小进行取模运算,得到的结果便是要访问的服务器地址的序号。采用源地址哈希法进行负载均衡,相同的 IP 客户端,如果服务器列表不变,将映射到同一个后台服务器进行访问。
最小连接数
前面几种方式都是通过对请求次数的合理分配最大可能提高服务器的利用率,但是实际上,请求次数的均衡并不能代表负载的均衡。所以,引入了最小连接数法。它正是根据后端服务器当前的连接情况,动态的选取其中当前积压连接数最少的一台服务器来处理当前请求,尽可能的提高后台服务器利用率,将负载合理的分流到每一台服务器。
在解决完路由问题后,我们发现了第二个问题,Session问题。
我们打开一个网页,基本上需要浏览器和 web 服务器进行多次交互,我们都知道 Http 协议本身是无状态的,这也是 http 协议设计的初衷,客户端只需要简单的向服务器请求下载某些文件,无论是客户端还是服务器都没必要记录彼此过去的行为,每一次请求之间是独立的,好比一个顾客和一个自动售货机之间的关系一样
而实际上,我们很多的场景都需要带有状态的特性,因此我们引入了 session+cookie机制来记住每次请求的会话。在会话开始时,给当前会话分配一个唯一的会话标识(sessionid),然后通过 cookie 把这个标识告诉浏览器,以后在每次请求的时候,浏览器都会带上这个会话标识来告诉 web 服务器请求属于哪个会话。在 web 服务器上,各个会话有独立的存储,保存不同会话的信息。如果遇到禁用 cookie 的情况,一般的做法就是把这个会话标识放到 URL 的参数中。
Session共享问题
当我们横向扩展之后,session本身是存在两个不同的服务器上,这个时候产生的问题就会这样:用户登录,进入了A服务器,用户登录成功。用户下单,下单的时候校验用户是否登录,这个时候,对于B服务器来说,他没有记录到用户是否登录,于是他又弹出信息,说需要用户登录。这无疑是十分不合理的。
对于Session问题,一般有三种处理方式(事实上,我们公司只用了Redis记录缓存共享问题)
1.缓存拷贝
我们在 Web 服务器之间增加了会话数据的同步,通过同步就保证了不同 Web 服务器之间Session 数据的一致。一般应用容器都支持 Session Replication 方式
存在问题:
1. 同步 Session 数据造成了网络带宽的开销。只要 Session 数据有变化,就需要将数据同步到所有其他机器上,机器越多,同步带来的网络带宽开销就越大。
2. 每台 Web 服务器都要保存所有 Session 数据,如果整个集群的 Session 数据很多(很多人同时访问网站)的话,每台机器用于保存 Session 数据的内容占用会很严重。这个方案是靠应用容器来完成 Session 的复制从而解决 Session 的问题的,应用本身并不关心这个事情。这个方案不适合集群机器数多的场景。
2.redis缓存共享
Session 数据不保存到本机而且存放到一个集中存储的地方,修改 Session 也是发生在集中存储的地方。Web 服务器使用 Session 从集中存储的地方读取。这样保证了不同 Web 服务器读取到的 Session 数据都是一样的。存储 Session 的具体方式可以是数据库
存在问题:
1. 读写 Session 数据引入了网络操作,这相对于本机的数据读取来说,问题就在于存在时延和不稳定性,不过我们的通讯基本都是发生在内网,问题不大。
2. 如果集中存储 Session 的机器或者集群有问题,就会影响到我们的应用。
相对于 Session Replication,当 Web 服务器数量比较大、Session 数比较多的时候,这个集中存储方案的优势是非常明显的。
3.客户端缓存
很容易想到就是利用 cookie,但是客户端存在风险,数据不安全,而且可以存放的数据量比较小,所以将 session 维护在客户端还要对 session 中的信息加密。
我们的 Session 数据放到 Cookie 中,然后在 Web 服务器上从 Cookie 中生成对应的 Session数据。这就好比我们每次都把自己的碗筷带在身上,这样去那家饭店就可以随意选择了。
相对前面的集中存储方案,不会依赖外部的存储系统,也就不存在从外部系统获取、写入 Session数据的网络时延、不稳定性了。
存在问题:安全性。
Session 数据本来都是服务端数据,而这个方案是让这些服务端数据到了外部网络及客户端,因此存在安全性上的问题。我们可以对写入的 Cookie 的 Session 数据做加密,不过对于安全来说,物理上不能接触才是安全的。
数据库读写分离
在解决完以上问题之后,我们发现实际效果并不好,虽然增加的应用服务,有效的进行了负载均衡。但是,到了数据库级别的时候,我们发现很慢。就好像漏斗一样,上端很大,下端口子很小,所以,你出来的数据很少。
针对于大型的互联网来说,读写分离是有必要的。因为实际上,我接触到的客户一到月底月初就疯狂查数据,对账。平时只是做一下新增业务。
读写分离后的图,如下:
我们会遇到两个问题:
1.数据如何同步,当然这里必须强调。如果说写完之后,我们要立刻,实时的把数据读出来,那读写分离没有任何意义。
只有业务能够允许有一定时间的滞留与同步,这样才能使用
数据库同步我们可以用数据库级别的同步:Mysql支持Master-slave主从结构进行数据同步。Oracle的主从同步,无外乎Logical Standby、Stream以及11g的Physical Standby(Active Data Guard)
2.这里的路由怎么做,我怎么去区分然后选择不同的数据库,笔者能够想到的第一反应,就是靠程序做隔离,做一个Mybatis的拦截器,去拦截完之后,判断是读还是写,然后读取配置文件进行分发SQL。
在网上看了一些资料之后,有提到了一系列的新的知识点,这些我不太清楚,这边先把链接放上去,下次在鼓捣鼓捣。
https://www.jianshu.com/p/eba38b1ff43c?utm_source=coffeephp.com
搜索引擎
搜索引擎,我第一次接触的时候,是在看数据库优化性能时,由对field like '%key%' 这种的性能优化。在实际客户使用的场景中,如果商品的数据量达到百万基本,你要根据一个关键字的名称,来搜索商品,这个key可能存在的位置不定,这个时候依旧会在读的时候,存在性能瓶颈。这边就引入了搜索引擎的概念。
主流的搜索引擎分为Lucene ElasticSearch Solandra。
搜索引擎,需要你自己构建索引。索引的构建方式分为两种,一种是全量,一种是增量。全量可以在期初导入数据时,用定时任务之类的,增量可以在商品实时新增的过程中。同的增量的构建可以分为实时或者异步。
然后概念图就变成了这样:
从本质上来看 他就是一个读库。
缓存与分布式文件图片存储
分布式文件系统
对一些图片、大文本,使用数据库就不合适了,所以我们会采用分布式文件系统来实现文件存储,分布式文件系统有很多产品、比如淘宝的 TFS、google 的 GFS。还有开源的 HDFS
NOSQL
NOSQL 理解为NOT ONLY SQL。主流的noSql 分为 redis ,HBase, MongoDb。
在大型互联网搭建过程中,NoSQL更多的是充当一个缓存的角色。程序会优先往缓存中抓数据,只有当缓存中的数据不存在时,才会往数据库抓数据。这个时候的概念图,就是这样的
必须要申明的是,缓存不是仅仅只是指上面的那些,缓存有很多,最早开始的CPU的高速缓存,浏览器缓存(这也是我们有时候要经常强制刷新页面的原因,不然他不会重新加载),数据缓存(同样的一个复杂查询,在参数不变的情况下,第二次会明显比第一次快)。缓存用到了很多地方。
数据库分库分表
在实际的过程中,我们发现部分表的他们的数据量特别巨大,诸如商品表,日志表。这个时候,假设读写分离,缓存都已经撑不住了,我们就需要分库分表。分库分表能解决这个查询缓慢的问题,但是他也会带来一系列衍生的问题。先看架构图。
单个数据库的表少了,单个表的数据量少了,查询起来就快了。
那么数据库水平拆分之后遇到的问题:
水平拆分带来的影响
1. sql 路由问题,需要根据一个条件来决定当前请求发到那个数据库中 哪个表中
2. 主键的处理,不能采用自增 id,需要全局 id
3. 由于同一个业务的数据被拆分到不同的数据库,因此涉及到一些查询需要跨两个数据库获取,如果数据量太大并且需要分页,就比较难处理了
笔者没有很深究的研究这些,我当时鼓捣了一个MyCat的中间件,实际物理数据库层进行了分库分表,插入时,根据rule.xml
中定义的切片规则来。然后查询的时候,由于mycat把所有分开的数据库整合成一个大的数据库,所以没有遇到太多问题。
服务水平拆分
说完了数据库的水平拆分之后,我们的服务也需要进行模块化的水平拆分。
这样做会有以下好处:
1.不同模块之间进行业务解耦
2.如果其中一个模块的服务器挂了,不会影响其他服务器
看一下架构图:
这里增加了一层web层,其实我刚刚很犹豫这个到底叫controller层是不是更好一些。不过这个不影响。
我们的产品的结构失衡前后端分离的,以用户点击支付为列,他将去检查登录用户,是谁买的,检查商品信息,买了什么东西。对于暴露给前端的接口来说,我只要这么一个支付的入口就可以了,但是对于后端逻辑来说,我需要实现跨服务的访问。
这里面会涉及到很多问题:
1.分布式模块化交互
首先,必须要明确的是,服务之间的相互相互通讯。绝对不能在A服务中,写一大堆B服务的逻辑。服务之间的通讯,应该以接口暴露的方式,利用远程RPC的调用来实现。
当然了除了RPC的方式实现服务之间的通讯,我们也可以通过消息队列来实现异步消息传输。消息队列的作用不仅仅只是模块通讯,还可以异步处理,流量削峰等
2.分布式事务问题
分布式事务问题,也是分布式数据一致性问题,这是一个相对来说比较尴尬和头疼的问题
在Spring cloud alibaba中,存在fescar(seata)组件,可以利用注解的方式来实现分布式事务。(我没用过,我也不知道)
在我的认知里面,分布式事务,我可以用消息队列(rocket mq),相当于打标记位的形式,一旦出了问题了,进行事务回滚或者实施事务补偿 来实现一致性问题。
服务集群部署
对于一些大型的互联网来说,是对服务可用性,有要求的。一年中要求,99.9%可用,99.99可用。那在这钟要求下,我们就不能单独只部署一台服务器,我们需要部署集群。同样的,部署集群可以实现热部署(客户在用,你可以一个个替换class,不影响客户使用)
然后他的架构图,如下:
集群之间的路由 可以使用Nginx 或者F5硬件负载均衡。我记得zookeeper有一个leader选举机制的,不知道能不能通过这种方式,在一个服务器挂了之后,选举另外一个服务器。这边我们之前的公司用的是Nginx负载均衡
至此,整个我所认知的分布式架构就结束了。我想在总结归纳了这个整体的一个架构图之后,对于后续学习一些比较细节化的东西会有好处。