分布式架构及Dubbo

发布时间 2023-05-22 01:07:00作者: JaxYoun

Dubbo的前世今生

一、分布式系统的架构演进过程

Dubbo框架的出现是分布式系统演进的结果,我们先来回顾一下分布式系统的演进过程

1 单应用架构

在这里插入图片描述

2 应用服务器和数据库服务器分离

单机负载越来越来,所以要将应用服务器和数据库服务器分离 在这里插入图片描述

3 应用服务器做集群

每个系统的处理能力是有限的,为了提高并发访问量,需要对应用服务器做集群

分布式和集群的概念经常被搞混,现在一句话让你明白两者的区别。

分布式:一个业务拆分成多个子业务,部署在不同的服务器上 集群:同一个业务,部署在多个服务器上

例如:电商系统可以拆分成商品,订单,用户等子系统。这就是分布式,而为了应对并发,同时部署好几个用户系统,这就是集群

在这里插入图片描述

这时会涉及到两个问题:

  1. 负载均衡
  2. session共享

负载均衡就是将请求均衡地分配到多个系统上,常见的技术有如下几种

DNS

DNS是最简单也是最常见的负载均衡方式,一般用来实现地理级别的均衡。例如,北方的用于访问北京的机房,南方的用户访问广州的机房。一般不会使用DNS来做机器级别的负载均衡,因为太耗费IP资源了。例如,百度搜索可能要10000台以上的机器,不可能将这么多机器全部配置公网IP,然后用DNS来做负载均衡。

Nginx、LVS、F5

DNS是用于实现地理级别的负载均衡,而Nginx、LVS、F5用于同一地点机器级别的负载均衡。其中Nginx是软件的7层负载均衡,LVS是内核的4层负载均衡,F5是硬件做4层负载均衡,性能从低到高位Nginx<LVS<F5

在这里插入图片描述

下图形象的展示了一个实际请求过程中,地理级别的负载均衡和机器级别的负载均衡是如何分工和结合的,其中粗线是地理几倍的负载均衡,细线是机器级别的负载均衡,实线代表最终的路由路径

在这里插入图片描述 session共享就是用户在A服务器登录,结果查看购物车时,请求发送到了B服务器,因此用户的session存在A服务器上,所以当请求发送到B服务器上时,会认为用户没有登录

目前解决session跨域共享问题有如下几种方式

  1. session sticky 将请求都落到同一个服务器上,如Nginx的url hash
  2. session replication session复制,每台服务器都保存一份相同的session
  3. session 集中存储 存储在db、 存储在缓存服务器 (redis)
  4. cookie (主流) 将信息存在加密后的cookie中

4 数据库高性能操作

搭建数据库主从集群,实现数据库读写分离,改善数据库负载压力 在这里插入图片描述

数据库读写分离的基本实现如下:

  1. 数据库服务器搭建主从集群,一主已从,一主多从都可以
  2. 数据库主机负责读写操作,从机只负责读操作
  3. 数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据
  4. 业务服务器将写操作分给数据库主机,将读操作分给数据库从机

实现方式

读写分离需要将读/写操作区分开来,然后访问不同的数据库服务器;分库分表需要根据不同的数据访问不同的数据库服务器,两者本质上都是一种分配机制,即将不同的SQL语句发送到不同的数据库服务器。

读写分离,包括后面要提到的分库分表的实现方式有两种:

  1. 程序代码封装
  2. 中间件封装

程序代码封装指在代码中抽象一个数据访问层来实现读写分离,分库分表

在这里插入图片描述

中间件封装指的是独立一套系统出来,实现读写分离和分库分表操作,如我们熟悉的MySQL Router和Mycat等

在这里插入图片描述

5 引入搜索引擎来查询

传统的关系型数据库通过索引来达到快速查询的目的,但是在全文搜索的业务场景下,索引也无能为力,主要体现在如下几点:

  1. 全文搜索的条件可以随意排列组合,如果通过索引来满足,则索引的数量会非常多
  2. 全文搜索的模糊匹配方式,索引无法满足,只能用like查询,而like查询是整表扫描,效率非常低

在这里插入图片描述

6 增加缓存

为了应对流量持续增加,必须增加缓存 在这里插入图片描述

常见的方式有如下几种:

Redis与Memcached

以我们常见的Mybatis为例,很容易和Redis与Memcached整合起来,缓存已经查询过的SQL,因为Mybatis知道自己不擅长缓存,所以提供了接口让这些缓存工具进行整合

CDN

CDN是为了解决用户网络访问时的“最后一公里”效应,本质上是一种“以空间换空间”的加速策略,即建内容缓存在离用户最近的地方,用户访问的是缓存的内容,而不是站点实时的内容。

7 分库分表

读写分离分散了数据库读写操作的压力,但没有分散存储压力,当数据量达到千万甚至上亿条的时候,单台服务器的存储能力会成为系统的瓶颈。常见的分散存储的方法有分库和分表两大类 在这里插入图片描述

业务分库

业务分库指的是按照业务模块将数据分散到不同的数据库服务器。例如,一个简单的电商网站,包括商品,订单,用户三个业务模块,我们可以将商品数据,订单数据,用户数据,分开放到3台不同的数据库服务器上,而不是将所有数据都放在一台数据库服务器上

当然业务分库也会带来新的问题:

  1. join操作问题:业务分库后,原本在同一个数据库中的表分散到不同数据库中,导致无法使用SQL的join查询
  2. 事务问题:原本在同一个数据库中不同的表可以在同一个事务中修改,业务分库后,表分散到不同数据库中,无法通过事务统一修改
  3. 成本问题:业务分库同时也带来了成本的代价,本来1台服务器搞定的事情,现在要3台,如果考虑备份,那就是2台变成6台

分表

表单数据拆分有两种方式,垂直分表和水平分表 在这里插入图片描述

垂直分表:垂直分表适合将表中某些不常用且占了大量空间的列拆分出去。如上图的nickname和description字段不常用,就可以将这个字段独立到另外一张表中,这样在查询name时,就能带来一定的性能提升

水平分表:水平分表适合表行数特别大的表,如果单表行数超过5000万就必须进行分表,这个数字可以作为参考,但并不是绝对标准,关键还是要看表的访问性能

水平分表后,某条数据具体属于哪个切分后的子表,需要增加路由算法进行计算,常见的路由算法有

范围路由:选取有序的数据列(例如,整型,时间戳等)作为路由条件,不同分段分散到不同的数据库表中。以最常见的用户ID为例,路由算法可以按照1000000的范围大小进行分段,1-999999放到数据库1的表中,1000000-1999999放到数据库2的表中,以此类推

Hash路由:选取某个列(或者某几个列组合也可以)的值进行Hash运算,然后根据Hash结果分散到不同的数据库表中。同样以用户Id为例,假如我们一开始就规划了10个数据库表,路由算法可以简单地用user_id%10的值来表示数据所属的数据库表编号,ID为985的用户放到编号为5的子表中,ID为10086的用户放到编号为6的子表中。

配置路由:配置路由就是路由表,用一张独立的表来记录路由信息,同样以用户ID为例,我们新增一张user_router表,这个表包含user_id和table_id两列,根据user_id就可以查询对应的table_id

8 应用拆分/微服务

随着业务的发展,业务越来越多,应用的压力越来越大。工程规模也越来越庞大。这个时候就可以考虑将应用拆分,按照领域模型将我们的商品,订单,用户分拆成子系统。 在这里插入图片描述

这样拆分以后,可能会有一些相同的代码,比如订单模块有对用户数据的查询,用户模块中肯定也有对用户数据的查询。这些相同的代码和模块一定要抽象出来。这样有利于维护和管理。这时可以将模块变为一个个服务,模块之间互相调用来获取数据,系统就变成一个微服务了。

在这里插入图片描述

服务拆分以后,服务之间的通信可以通过RPC技术,比较典型的有:Webservice、Hessian、HTTP、RMI等。

当前的Dubbo和Spring Cloud都是目前比较流行的微服务框架,这些微服务框架使得远程调和本地调用服务一样方便

两者的区别如下

区别 Dubbo Spring Cloud
注册中心 Zookeeper等 Eureka等
支持的协议 Dubbo,Http,Hessian等 Http
断路器 Spring Cloud Netflix Hystrix
服务网关 Spring Cloud Netflix Zuul
分布式配置 Spring Cloud Config
服务跟踪 Spring Cloud Sleuth

可以看到Dubbo因为出现的时间较早,偏向RPC和服务治理,而Spring Cloud则是一个完整的微服务生态,提供了一站式的解决方案。

另外Dubbo一般通过自定义的协议来进行调用,比使用Http方式的Spring Cloud性能高。但是自定义协议使得跨语言进行服务调用比较麻烦

下面我们就正式开始学习Dubbo了。

二、Dubbo工作流程

Dubbo架构的流程图如下,有5个比较重要的角色 在这里插入图片描述

  1. Provider:暴露服务的服务提供方
  2. Consumer:调用远程服务消费方
  3. Registry:服务注册与发现注册中心
  4. Monitor:监控中心和访问调用统计
  5. Container:服务运行容器

Dubbo服务注册和发现的流程如下

  1. 服务容器Container负责启动,加载,运行服务提供者。
  2. 服务提供者Provider在启动时,向注册中心注册自己提供的服务。
  3. 服务消费者Consumer在启动时,向注册中心订阅自己所需的服务。
  4. 注册中心Registry返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
  5. 服务消费者Consumer,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
  6. 服务消费者Consumer和提供者Provider,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心Monitor。

一般RPC的框架整体架构如下所示 在这里插入图片描述

  1. Server将服务信息注册到Registry,Client从Registry拉取Server的信息。
  2. Client通过代理对象(Client Stub)发送发送网络请求,Server通过代理对象(Server Stub)执行本地方法
  3. 网络传输过程中有编解码和序列化的过程

可以看到客户端发送网络请求和服务端收到网络请求执行本地方法都是通过代理对象来实现的。正是由于这2个代理对象的存在才使得我们调用远程方法和本地方法一样方便。这个代理对象在RPC框架中有一个专属名词Stub(桩)

至于更细节的问题,我们在《如何手写一个RPC框架?》仔细分析一波

架构分层

接着我们来分析一下Dubbo架构的分层。在日常开发中我们只会用到Service层和Config层,这是用户API层,其他的层都是基Dubbo SPI(后面的文章会聊这个Dubbo SPI的作用)来实现的,可以替换现成的组件,可扩展性强

在这里插入图片描述

每个分层的作用如下,了解了每个层的作用,对于我们后面分析源码有很大的用处

层次名 作用
Service 业务层,业务代码的接口与实现
Config 配置层,主要的配置类为ServiceConfig(服务导出配置)和ReferenceConfig (服务引入配置)
Proxy 代理层,即生成Client Stub和Server Stub的过程
Registry 注册中心层,通过注册中心动态的维护服务提供者列表,自动感知服务的上下线,而不用人工维护
Cluster 集群容错层,通过什么方式来调用服务提供者,如失败重试,快速失败等
Monitor 监控层,统计服务的调用信息
Protocol 协议层,dubbo中的协议主要分为两种,一种是服务注册用哪种注册中心的协议,例如zookeeper,redis。一种是服务导出的网络的协议,如dubbo,hessian,http协议
Exchange 信息交换层 ,建立Request-Reponse模型,封装请求和响应
Transport 网络传输层,发送请求和返回响应用哪种通信框架,如netty或者mina
Serialize 序列化层,对数据进行网络传输都涉及到序列化和反序列化的过程

好了,下节我们就用Dubbo实现了一个小功能!