1. 前言
微服务架构做分布式系统开发时的四大问题:
1 客户端如何访问这么多的服务
- API、网关
2 服务与服务之间如何通信
同步通信:
- HTTP(Apache Http Client)
- RPC(Dubbo,只支持Java)
异步通信:
- 消息队列(kafka RabbitMQ RocketMQ)
3 这么多服务如何管理(管理实质是管理微服务的地址)
服务治理:服务注册与发现
- 基于客户端的服务注册与发现(Dubbo+Zookeeper)
- 基于服务端的服务注册与发现(Spring Cloud中的Eureka)
4 服务挂了怎么办
- 重试机制
- 服务熔断
- 服务降级
- 服务限流
微服务架构中就需要解决以上四个大问题,这些问题最大根源就是网络是不可靠的。
常用的有两套微服务解决方案:
- Spring Boot+Spring Cloud
基于HTTP
使用Eureka(Netflix旗下)完成服务注册与发现
组件多,功能完备 - Spring Boot+Dubbo+Zookeeper(Apache旗下)
基于RPC的通信框架
使用Dubbo+Zookeeper完成服务注册与发现
组件少,功能没有第一套方案完备
2. RPC
2.1 概念
RPC(Remote Procedure Call)是指远程过程调用,是一种进程间通信方式,是一种技术思想,而不是规范。
它允许程序调用另一个地址空间(通常是共享网络的另一台机器)的过程或函数,而不是程序员显示编码这个远程调用的细节。即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。
分布式服务框架Dubbo和微服务架构如SpringCloud中的各个服务之间的通信区别:Dubbo基于的RPC协议通信,而微服务Cloud中采用的是基于HTTP的RESTful API。
2.2 RPC基本原理
假设A服务器上的程序要调用B服务器上的程序,A、B服务器上执行的代码如下,A服务器调用B服务器上的hi()方法,hi()方法返回一个字符串给A服务器:
客户端程序A:
hello(){
String msg = B.hi(new User(“张三”));
System.out.println(msg);
}
服务器端程序B:
String hi(user){
return “你好:”+user.getName();
}
下图展示了RPC的基本原理。A服务器和B服务器分别使用了RPC框架,当它发现A服务器调用B服务器的接口时,便和B服务器建立起连接。并且客户端需要携带参数,便对参数进行序列化处理。
可以看出RPC的两个核心模块就是:通讯(通过网络通信传递数据)和序列化(对参数和结果进行序列化和反序列化)。
RPC框架有很多,如dubbo、gRPC、THrift、HSF(High Speed Service Framework)
下面再以一张分布式服务架构简图进一步了解RPC:
在分布式服务框架中,各个服务之间会(后台服务之间,前台和后台服务之间)相互调用接口,这些调用接口的管理交给RPC框架处理。
3. Zookeeper
Zookeeper是一个分布式协调服务。分布式协调服务主要用来解决分布式环境中的多个进程之间的同步控制,让它们有序的访问某种临界资源,防止造成“脏数据”的后果。
Dubbo官方推荐使用Zookeeper作为注册中心,注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小。
下面通过画图来帮助理解这个概念,多个订单服务部署在多个服务器上(多个进程),当有15个并发到达Nginx服务器时,Nginx转发到订单服务上,三个订单服务分别分到5个请求,去访问商品服务,商品只剩下5个(临界资源),如果三个订单服务没有组织管理地去访问商品服务,则可能会出现扣减的商品库存大于5,这就是造成了“脏数据”。
这种问题不会存在于单体应用中,因为单体应用一个JVM是一个进程,一个进程里有多个线程,单体应用下,多线程会导致临界资源访问问题,可以通过同步代码的方式保证资源的有序访问。
分布式协调服务Zookeeper就是为了解决这一问题。协调的本质为“分布式锁”,当第一个服务请求到临界资源的时候,就给临界资源上锁,其他服务就无法再请求得到临界资源。
3.1 Zookeeper的数据模型
Zookeeper的数据模型像树结构,也像文件系统目录。树由节点组成,Zookeeper的数据存储也同样是基于节点,这种节点称为Znode。
但是不同于树的节点,Zonde的引用方式是路径引用,类似于文件路径:/用户服务/用户登录
、/订单服务/处理订单
每个Znode节点存储的数据不能超过1MB
Zonde节点包含的元素:
- data:存储数据信息(比如服务ip、服务名称)
- ACL:记录Znode的访问权限,即哪些用户或哪些IP可以访问本节点
- child:记录子节点的地址
- stat:包含Zonde的各种元数据,比如事务ID、版本号、时间戳、大小等等
3.2 Zookeeper的事件通知
当Znode发生改变,也就是调用了create(创建节点)、delete(删除节点)、setData(设置节点数据)方法的时候,将会触发Znode上注册的对应事件,请求Watch的客户端会接收到异步通知。
具体交互过程如下:
- 客户端调用 getData 方法,watch 参数是 true。服务端接到请求,返回节点数据,并且在对应的哈希表里插入被 Watch 的 Znode 路径,以及 Watcher 列表。
- 当被 Watch 的 Znode 已删除,服务端会查找哈希表,找到该 Znode 对应的所有 Watcher,异步通知客户端,并且删除哈希表中对应的 Key-Value。
3.3 服务注册与发现
前言中提到常用的有两套微服务解决方案,解决了服务注册与发现,下面通过一张图来理解服务注册与发现的概念:
- 开启Zookeeper服务注册与发现中心
- 各个微服务向服务中心注册信息(ip、服务名称等)
- 用户请求
订单服务
,并设置watch为true,假设此时返回给服务注册与发现服务的微服务ip是192.168.0.1
- API Geteway(API网关)中由zkClient(Zookeeper客户端)维护了一个列表,列表中存储了
订单服务
服务的地址为192.168.0.1
,当用户下次再请求订单服务
时,就可以直接拿到这个ip - 如果
192.168.0.1
宕机了,Zookeeper的服务发现功能能够发送异步通知给API网关通知该192.168.0.1
服务已下线,然后zkClient从列表中删除这个服务ip - 当用户再次访问
订单服务
时,返回的服务ip就是192.168.0.4
或者192.168.0.5
事件通知机制就是帮助Zookeeper完成服务注册与发现
3.4 Zookeeper一致性问题
如果只部署一台Zookeeper服务中心(单机),那么Zookeeper服务中心挂掉之后,整个服务就无法使用了。这时候就可以部署多个服务中心(集群),微服务可以连接任意一个服务中心,那么新的问题又产生了,就是数据如何同步。
Zookeeper服务是一主多从结构,在更新数据时,首先更新到Zookeeper服务集群的主节点(Leader),再同步到从节点(Follower)。读取数据时,直接读取任意节点。【读取分离】。
当主节点崩溃时,会从从节点中重新选举新的主节点。
数据写入流程保证数据一致性:
- 客户端发出写入数据请求给任意Follower
- Follower把写入数据请求转发给Leader
- Leader采用二阶段提交方式,先发送Propose广播给Follower
- Follower接收到Propose消息,写入日志成功后,返回ACK消息给Leader
- Leader接到半数以上ACK消息,返回成功给客户端,并且广播Commit请求给Follower
3.5 Zookeeper的基本使用
Zookeeper是一个服务,需要被单独启动,有服务端和客户端,客户端可用命令行工具,Zookeeper的安装和基本使用见下面的Dubbo环境搭建
小节。
4. 分布式锁
为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度,而这个分布式协调技术的核心就是来实现这个分布式锁。
4.1 分布式锁应该具备的条件
- 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行
- 高可用的获取锁与释放锁
保证锁能正常获取和释放(比如获取到锁的程序在未释放锁之前就挂掉,导致锁不能正常释放,其他程序就一直无法获取锁) - 高性能的获取锁和释放锁
获取锁和释放锁的时间快 - 具备可重入特性
可由多个任务并发使用,不会造成数据错误 - 具备锁失效机制,防止死锁
- 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失效
4.2 现有分布式锁的实现
- Memcached
利用Memcache的add
命令,此命令是原子性操作,只有在key
不存在的情况下,才能add
成功 - Redis
利用Redis的setnx
命令,此命令是原子性操作,只有在key
不存在的情况下,才能set
成功 - Zookeeper
利用Zookeeper的顺序临时节点,来实现分布式锁和等待队列。Zookeeper的设计初衷就是为了实现分布式锁服务。 - Chubby
利用了Paxos一致性算法
4.3 分布式锁的三大问题及解决
1 非原子性操作
设置锁的键值对和键值对的过期时间必须是一个原子操作,利用redis的set(key, value, expire)
保证原子操作
2 误删锁
- 假设JVM1设置锁的过期时间为30s
- JVM1执行程序的时间超过了30s,这时JVM2便可获得锁
- JVM1执行完程序后去删除锁,这时候删除的实际上是JVM2的锁
解决:
删除锁之前先判断是不是自己线程设置的锁
3 锁过期时间小于程序执行时间
也就是说程序还未执行完成,锁就过期了。
解决:利用守护线程,当检测到程序持有的锁块过期了,但是程序还未执行完,则增加锁的过期时间
4.4 Zookeeper如何实现分布式锁
利用Zookeeper的顺序临时节点,来实现分布式锁和等待队列。Zookeeper 的数据存储结构就像一棵树,这棵树由节点组成,这种节点叫做 Znode。
Znode 分为四种类型:
- 持久节点:默认的节点类型。创建节点的客户端与 Zookeeper 断开连接后,该节点依旧存在。
- 持久顺序节点:顺序节点,就是在创建节点时,Zookeeper 根据创建的时间顺序给该节点名称进行编号。
- 临时节点:当创建节点的客户端与 Zookeeper 断开连接后,临时节点会被删除。
- 临时顺序节点::在创建节点时,Zookeeper 根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与 Zookeeper 断开连接后,临时节点会被删除。
Zookeeper应用了临时顺序节点实现分布式锁,步骤如下:
- 获取锁:在 Zookeeper 当中创建一个持久节点 ParentLock。当第一个客户端想要获得锁时,需要在 ParentLock 这个节点下面创建一个临时顺序节点 Lock1。之后,客户端遍历ParentLock 下的所有锁,判断自己的锁是不是最靠前的(锁编号最小),如果是,则获取锁成功,否则向它的前一个锁注册Watcher,用于监听前一个锁是否存在,直到前一个锁释放之后(由于是临时顺序节点,因此锁释放之后,锁就删除了),该监听该锁的客户端获取锁成功。
- 释放锁,释放锁分两种情况:任务完成,客户端显示释放;执行任务过程中,客户端崩溃。这两种情况都会导致相关联的锁删除。
4.5 Zookeeper和Redis分布式锁的比较
Zookeeper分布式锁的优点:
- 有封装好的框架,容易实现
- 有等待锁的队列,大大提升抢锁效率。因为Zookeeper中利用顺序锁,后一个锁的客户端监听前一个客户端的锁即可,监听的锁一旦释放,则该客户端即可获得锁。而Redis中没有等待锁的队列,假设此时创建了三个锁,其中一个客户端获取锁成功,则另外两个客户端要不断地去竞争锁,没有向Zookeeper一样的Watcher机制和队列机制。
Zookeeper的缺点:
- 添加和删除节点性能较低
Redis分布式锁的优点:
- Set和Del指令的性能较高
Redis分布式锁的缺点:
- 实现复杂,需要考虑超时、原子性、误删等情况
- 没有等待锁的队列,只能在客户端自旋来等锁(没有获取到锁的客户端都不停地去看有没有可用锁),效率低下
5. Dubbo
5.1 简介
Dubbo是一款高性能、轻量级的开源Java RPC分布式服务框架,特性如下【来自dubbo官网】:
- 面向接口代理的高性能RPC调用
即当A服务器调用B服务器中的方法时,A服务把B服务的方法接口拿过来,A服务只需要调用接口的方法即可,Dubbo会自动去B服务器上找到相应的方法并执行,为开发者屏蔽了远程调用的底层细节。 - 智能负载均衡
比如当“用户业务”服务器访问量很大,增加“用户业务”服务器后,Dubbo管理具体调用哪台服务器。 - 自动服务注册和发现
Dubbo支持多个服务注册中心(其中一个就是Zookeeper),可以立即在线/离线检测服务。 - 高扩展性
Dubbo的微内核和插件设计确保了它可以通过第三方实现轻松地跨核心功能(如协议、传输和序列化)进行扩展。 - 运行时流量路由
Dubbo可以在运行时进行配置,以便流量可以根据不同的规则进行路由,这使得它很容易支持如灰度发布、数据中心感知路由等功能。 - 可视化服务治理
Dubbo为服务治理和维护提供了丰富的工具,如查询服务元数据、运行状况和统计信息
简单来说,Dubbo就是一个远程服务调用的分布式框架,在分布式系统中,Dubbo提供了RPC远程服务调用方案,以及SOA服务治理方案。
5.2 Dubbo核心功能
- Remoting:远程通讯,提供对多种 NIO(Dubbo使用了著名的通信框架:Netty) 框架抽象封装,包括“同步转异步”和“请求-响应”模式的信息交换方式。
通讯模型BIO:同步并阻塞;NID:异步并阻塞;AIO:异步非阻塞 - Cluster:服务框架,提供基于接口方法的透明远程过程调用,包括多协议支持,以及软负载均衡,失败容错,地址路由,动态配置等集群支持。
- Registry:服务注册中心,服务自动发现: 基于注册中心目录服务,使服务消费方能动态的查找服务提供方,使地址透明,使服务提供方可以平滑增加或减少机器。
5.3 Dubbo组件角色
Dubbo的五个角色:
- Provider:暴露服务的服务提供方
- Consumer:调用远程服务的服务消费方
- Registry:服务注册与发现的注册中心(是一个软件系统,一般使用Zookeeper)
- Monitor:统计服务的调用次数和调用时间的监控中心
- Container:服务运行容器
上图中的调用关系说明:
- 服务容器Container:负责启动、加载、运行服务提供者
- 服务提供者Provider:在启动时,向注册中心注册自己提供的服务
- 服务消费者Consumer:在启动时,向注册中心订阅自己所需的服务
- 注册中心Registry:返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者
- 服务消费者Consumer:从提供者地址列表中,基于软负载均衡算法,选择一台服务提供者进行调用,如果调用失败,再选择另一台调用
- 服务消费者Consumer和提供者Provider:在内存中累计调用次数和调用时间,定时分钟发送一次统计数据到监控中心Monitor
总结图:
5.4 Dubbo环境搭建
【参考官方文档:http://dubbo.apache.org/zh-cn/docs/user/quick-start.html】
1 配置注册中心使用官方推荐的zookeeper
- 到zookeeper官网下载,我这里下载的版本是3.4.11
- zookeeper-3.4.11\conf下的zoo_sample.cfg文件复制一份改名为zoo.cfg
- 新建文件夹zookeeper-3.4.11\data
- 修改zoo.cfg配置项
dataDir=../data
- 进入zookeeper-3.4.11\bin目录,在Windows上的命令窗口中运行
zkServer.cmd
打开注册中心服务 - 运行
zkCli.cmd
进入zookeeper客户端管理终端
2 安装dubbo的管理控制台
dubbo的管理控制台提供了可视化web界面供我们管理各个服务
- 到GitHub上下载安装包(https://github.com/locationbai/incubator-dubbo-ops-master)
- 打开incubator-dubbo-ops-master\dubbo-admin\src\main\resources\application.properties文件,修改配置项
dubbo.registry.address=zookeeper://127.0.0.1:2181
- 保证maven环境搭配好
- 在incubator-dubbo-ops-master\dubbo-admin\下打开命令窗口,运行命令
mvn clean package
,意思是将dubbo-admin程序打成一个jar包(application.properties默认配置是jar包)
- 将incubator-dubbo-ops-master\dubbo-admin\target下的dubbo-admin-0.0.1-SNAPSHOT.jar文件包放到incubator-dubbo-ops-master的同级目录下
- 在命令终端中执行
java -jar dubbo-admin-0.0.1-SNAPSHOT.jar
,即以SpringBoot的方式启动dubbo-admin程序(保证在第一步zookeeper注册中心服务已开启)
- 浏览器方法
http://localhost:7001/
,用户名和密码都是root
3 配置控制中心
- 在
incubator-dubbo-ops-master\dubbo-monitor-simple
目录下打开控制台,执行命令mvn package
- 将dubbo-monitor-simple-2.0.0-assembly.tar.gz文件放到incubator-dubbo-ops-master的同级目录下并解压
- 打开dubbo-monitor-simple-2.0.0\conf\dubbo.properties做如下配置:
dubbo.container=log4j,spring,registry,jetty-monitor
dubbo.application.name=simple-monitor
dubbo.application.owner=dubbo
dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.protocol.port=7070
dubbo.jetty.port=8080
dubbo.jetty.directory=${user.home}/monitor
dubbo.charts.directory=${user.home}/monitor/charts
dubbo.statistics.directory=${user.home}/monitor/statistics
dubbo.log4j.file=logs/dubbo-monitor-simple.log
dubbo.log4j.level=WARN
- 双击
dubbo-monitor-simple-2.0.0\assembly.bin\start.bat
- 浏览器访问http://localhost:8080
5.5 在SSM中使用Dubbo
下面模拟一个用户-订单地址管理服务
- user-service-provider是生产者服务,提供查询用户订单的服务
- order-service-consumer消费者服务,调用生产者服务中的接口
- gmall-interface是将生产者服务和消费者服务中的接口提取出来,接口的具体实现在user-service-provider或order-service-consumer中
- provider.xml中配置dubbo服务中心的相关信息(服务中心的ip和端口,申明暴露的服务接口等)
- consumer.xml中配置dubbo消费者相关信息(申明需要调用的远程服务的接口等)
源码
链接:https://pan.baidu.com/s/1TRyjIVErV1GwTtYyza2fvQ
提取码:0lsd
5.6 在Spring Boot中使用Dubbo
hello-dubbo-service-user-api为服务暴露的接口
hello-dubbo-service-user-provider为服务提供者
hello-dubbo-service-user-consumer为服务消费者
使用:
- 启动dubbo
- 启动hello-dubbo-service-user-provider
- 启动hello-dubbo-service-user-consumer
- 浏览器访问:http://localhost:9000/hi
- 使用incubator-dubbo-ops管理控制台查看:
源码:
链接:https://pan.baidu.com/s/1ZIYVJKY-5PjGbH3ZwjF8zA
提取码:bafg
6 附录
参考:
https://www.funtl.com/zh/guide/Apache-Dubbo.html
http://dubbo.apache.org/en-us/