1. 分布式概述
回顾我们以前的项目,从大一到大三,架构慢慢的演变,从一开始堆在一起(乱成一团),到分成多个包,三层架构(水平拆分),到前后端分离成两个项目,后面后台部分分成多个模块小组开发(垂直拆分)等等
传统架构–>水平拆分–>垂直拆分(最早的分布式)–>soa(dubbo)–>微服务(springCloud)
接下来我们要接触到一些分布式,解决一些高并发情况
早期简单的分布式,就是启动多个相同的后台模块(比如启动多个用户管理模块,多个赛事管理模块)作为“服务”,然后前端发送请求时不是直接到服务,而是到一个分发中心模块,这个分发中心就平均分配请求到相同服务(这就是负载均衡),从而减轻其中一个服务的并发压力。
随着服务越来越多,目前主流的两个分布式服务架构:以dubbo为代表的采用RPC协议方式的SOA架构和以springcloud为代表的采用HTTP协议方式的微服务架构
RPC:Remote Produce Call远程过程调用,类似的还有RMI。自定义数据格式,基于原生TCP通信,速度快,效率高。早期的webservice,现在热门的dubbo,都是RPC的典型代表
Http:http其实是一种网络传输协议,基于TCP,规定了数据传输的格式。现在客户端浏览器与服务端通信基本都是采用Http协议,也可以用来进行远程服务调用。缺点是消息封装臃肿,优势是对服务的提供和调用方没有任何技术限定,自由灵活,更符合微服务理念。
现在热门的Rest风格,就可以通过http协议来实现。
1.1 流动计算架构(SOA)
SOA :面向服务的架构
代表就是dubbo(微服务和soa都是解决大量的服务的问题,界限其实并没有那么明显,soa也通常被视为微服务的一种方式,所以也有人说dubbo是微服务框架。另外由于采用了rpc协议,也有人说dubbo是rpc框架,我认为只是角度不同,都可以)
当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键
以前出现了什么问题?
- 服务越来越多,需要管理每个服务的地址
- 调用关系错综复杂,难以理清依赖关系
- 服务过多,服务状态难以管理,无法根据服务情况动态管理
服务治理要做什么?
- 服务注册中心,实现服务自动注册和发现,无需人为记录服务地址
- 服务自动订阅,服务列表自动推送,服务调用透明化,无需关心依赖关系
- 动态监控服务状态监控报告,人为控制服务状态
缺点:
- 服务间会有依赖关系,一旦某个环节出错会影响较大
- 服务关系复杂,运维、测试部署困难,不符合DevOps思想
1.2 微服务
前面说的SOA,英文翻译过来是面向服务。微服务,似乎也是服务,都是对系统进行拆分。因此两者非常容易混淆,但其实却有一些差别:
微服务的特点:
- 单一职责:微服务中每一个服务都对应唯一的业务能力,做到单一职责
- 微:微服务的服务拆分粒度很小,例如一个用户管理就可以作为一个服务。每个服务虽小,但“五脏俱全”。
- 面向服务:面向服务是说每个服务都要对外暴露Rest风格服务接口API。并不关心服务的技术实现,做到与平台和语言无关,也不限定用什么技术实现,只要提供Rest的接口即可。
- 自治:自治是说服务间互相独立,互不干扰
- 团队独立:每个服务都是一个独立的开发团队,人数不能过多。
- 技术独立:因为是面向服务,提供Rest接口,使用什么技术没有别人干涉
- 前后端分离:采用前后端分离开发,提供统一Rest接口,后端不用再为PC、移动段开发不同接口
- 数据库分离:每个服务都使用自己的数据源
- 部署独立,服务间虽然有调用,但要做到服务重启不影响其它服务。有利于持续集成和持续交付。每个服务都是独立的组件,可复用,可替换,降低耦合,易维护
微服务结构图:
代表是springcloud,springcloud不是一个东西,是集成,把世界上最好的框架拿过来,多个组件集成到自己的项目中(官网的说法叫做分布式解决方案,多好听),有下面这几个组件,集成后实现了:配置管理,服务发现,智能路由,负载均衡,熔断器,控制总线,集群状态等等功能。
- Eureka:服务治理组件,包含服务注册中心,服务注册与发现机制的实现。(服务治理,服务注册/发现)
- Zuul:网关组件,提供智能路由,访问过滤功能
- Ribbon:客户端负载均衡的服务调用组件(客户端负载)
- Feign:服务调用,给予Ribbon和Hystrix的声明式服务调用组件 (声明式服务调用)
- Hystrix:容错管理组件,实现断路器模式,帮助服务依赖中出现的延迟和为故障提供强大的容错能力。(熔断、断路器,容错)
1.3 选择
如果你们公司全部采用Java技术栈,那么使用Dubbo作为微服务架构是一个不错的选择。
相反,如果公司的技术栈多样化,而且你更青睐Spring家族,那么SpringCloud搭建微服务是不二之选。这里介绍SpringCloud套件,因此我们会使用Http方式来实现服务间调用。
2. 准备工作
用一个小例子来说明,咱们先做点准备工作
- 先用spring initializr创建一个springboot工程,作为服务提供者,也就是接触分布式之前的所谓“后台”,就不多做说明了。
- 再创建一个模块作为服务消费者(也用spring initializr,但是只需要web,也就是mvc部分,不需要mysql,jdbc,mybatis那些),也就是上面说的分发中心,前端发送的http请求先到这个分发中心,分发中心经过负载均衡(后面会讲到负载均衡)再主动发送http请求给各个服务提供者。
这个我也不多说了,直接建立就好
消费者的引导类注入RestTemplate
1 |
|
那么这个消费者的controller很简单,只需要主动发送http请求到提供者(也就是以前的controller)的服务接口就行
启动,访问的时候就是访问消费者的接口
好,那么准备工作就做好了,其实到这一步,我们的工作和以前并无二异,只是
以前:浏览器请求–>后台
现在:浏览器请求–>后台1–>后台2
我们起这么个名字,方便讲解:浏览器请求–>服务消费者–>服务提供者
我们说过了,模块化会有很多个服务提供者(这个我们之前小组开发做过了,模块化开发比如用户模块和赛事模块对吧),分布式呢,还会出现很多个相同的提供者,比如多个用户模块(比如多台服务器,就可以减轻压力),这个也简单,只要改个端口号然后在idea的配置中复制一份多次启动就可以了
这就是分布式的前提。
3. Eureka
3.1 eureka概述
Eureka就是服务注册中心(可以是一个集群),对外暴露自己的地址,有三个角色
服务注册中心:Eureka的服务端应用,提供服务注册和发现功能
服务提供者:提供服务的应用,可以是SpringBoot应用,也可以是其它任意技术实现,只要对外提供的是Rest风格服务即可。启动后向Eureka注册自己信息(地址,提供什么服务)
服务消费者:向Eureka订阅服务,Eureka会将对应服务的所有提供者地址列表发送给消费者,消费者从而得知每个服务方的信息,知道去哪里调用服务方。并且定期更新
还有个概念:心跳(续约):提供者定期通过http方式向Eureka刷新自己的状态
对于上面的工程,思考一个问题,我们服务提供者越来越多(相同的或者不同的),你有没有办法去管理???没有吧,eureka就是解决这个问题,不管是消费者还是提供者,统统都给我注册eureka,这样我就可以统一管理了
3.2 eureka使用步骤
- 为eureka搭建一个模块(服务注册中心),仍然用spring提供的初始化工具,不过这次勾选rureka Server
结构如下
- 确认依赖,一般都是没问题的
1 | <properties> |
- yml
1 | server: |
- 引导类
1 |
|
- 给之前的服务提供者和消费者注册eureka客户端
要修改的地方如下:
- 提供者和消费者都要增加依赖
1 | <!-- Eureka客户端 --> |
1 | <!-- SpringCloud的依赖 --> |
- 提供者增加后的yml
1 | server: |
- 消费者增加后的yml
1 | server: |
- 提供者和消费者引导类都要加上注解
- 这样就配置好啦,依次启动eureka服务端和服务提供者消费者,然后可以访问http://localhost:10086看eureka的管理台了
这样就可以统一用eureka管理已注册的服务了
3.3 其他
- 之前消费者的controller是直接发动到提供者1的接口,但是我们都知道,后期要分发到多个提供者(负载均衡),这要有个前提:java端可以读取一些eureka的情况,理所当然!比如下面这段
1 |
|
这里可以看到 我们动态获取了当前注册的所有提供者,并手动分发到instance.get(0)也就是第一台服务器,这时候有人就有想法了!!咱可以做负载均衡了,将多个请求分发到不同的提供者!!是的,别急,这些不用自己写的,ribbon帮我们写好了,学起来。
- 这里还有一个要了解的就是概述中说的心跳(续约),了解一下即可
在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉EurekaServer:“我还活着”。这个我们称为服务的续约(renew);
有两个重要参数可以修改服务续约的行为:
1 | eureka: |
- lease-renewal-interval-in-seconds:服务续约(renew)的间隔,默认为30秒
- lease-expiration-duration-in-seconds:服务失效时间,默认值90秒
也就是说,默认情况下每个30秒服务会向注册中心发送一次心跳,证明自己还活着。如果超过90秒没有发送心跳,EurekaServer就会认为该服务宕机,会从服务列表中移除,这两个值在生产环境不要修改,默认即可。
但是在开发时,这个值有点太长了,经常我们关掉一个服务,会发现Eureka依然认为服务在活着。所以我们在开发阶段可以适当调小。
1 | eureka: |
4. Ribbon
4.1 ribbon概述
前面说了ribbon可以帮助我们自动完成消息平均分发,是的就这么简单。要看复杂的概念也有,如下
4.2 ribbon使用步骤
ribbon是在消费者中使用
- 不需要引入依赖,因为ribbon通常不会单独使用,所以在引入eureka的时候自动就引入了ribbon
- 前面说了我们可以对同一个服务提供者修改一下端口号然后再次启动,这样就有了多个提供者,且服务名一样,只是端口号不一样,可能说的不够细,咱们再细一点,配图
- 服务消费者的引导类中,对restTemplate方法使用@LoadBalanced注解开启负载均衡
4.消费者的restTemplate.getForObject方法里面通过 服务名 调用提供者的接口,因为此时有多个服务名相同的服务,所以就可以自动负载均衡啦!!!!
- 测试
可以复制这段测试用例
1 |
|
RibbonLoadBalanceClient有一个choose方法,可以获取负载均衡得到的实例
可以看到ribbon默认使用了轮询的方式
4.3 源码跟踪
为什么我们只输入了service名称就可以访问了呢?之前还要获取ip和端口。
显然有人帮我们根据service名称,获取到了服务实例的ip和端口。它就是LoadBalancerInterceptor
在如下代码打断点:
一路源码跟踪:RestTemplate.getForObject –> RestTemplate.execute –> RestTemplate.doExecute:
点击进入AbstractClientHttpRequest.execute –> AbstractBufferingClientHttpRequest.executeInternal –> InterceptingClientHttpRequest.executeInternal –> InterceptingClientHttpRequest.execute:
继续跟入:LoadBalancerInterceptor.intercept方法
继续跟入execute方法:发现获取了8082端口的服务
再跟下一次,发现获取的是8081:
这就是负载均衡读取到不同端口的大致过程
上面提到了负载均衡器和负载均衡算法,也就是默认的是轮询方式。如果我们想自定义负载均衡策略,可以继续深入可以图中负载均衡算法那个类ILoadBalancer,可以看到是一个接口,继承了BaseLoadBalancer
我们看看这个rule是谁:
这里的rule默认值是一个RoundRobinRule
,看类的介绍:
这不就是轮询的意思嘛。
我们注意到,这个类其实是实现了接口IRule的,查看一下:
定义负载均衡的规则接口。
它有以下实现:
通过上面源码的分析,我们也可以试图猜测一下默认轮询的工作方式,记录一个count,消费方每次count++,然后%n个提供方,为几就分配到第几个提供方。当然源码内部非常复杂且实现时要考虑很多因素没有这么简单,不过错了也没关系,咱们就看过的这几段大胆的猜测就行。
4.4 修改负载均衡策略
上面那张图已经看到了内置的负载均衡策略,使用时可以替换默认的,只需更改yml文件就可以
1 | service-provider: # 服务提供方的服务名 |
service-provider顶格写,这里不会有提示,甚至提示没有这项配置,没关系,大胆写,这时候再跑测试用例,可以看到是随机的了
ribbon做了处理,无论是哪种方式,分配给各个提供方的数量大体上是一样的
5. Hystrix
5.1 hystrix概述
熔断,是一种保护机制,Hystix是Netflix开源的一个延迟和容错库,用于隔离访问远程服务、第三方库,防止出现级联失败。
微服务中,服务间调用关系错综复杂,一个请求,可能需要调用多个微服务接口才能实现,会形成非常复杂的调用链路,如果其中一个环节出现错误,请求一直阻塞,会导致服务器资源耗尽,从而导致所有其它服务都不可用,形成雪崩效应。
Hystix解决雪崩问题的手段有两个:
- 线程隔离
- 服务熔断
5.1.1 线程隔离,服务降级
Hystrix为每个依赖服务调用分配一个小的线程池,如果线程池已满调用将被立即拒绝,默认不采用排队.加速失败判定时间。用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,或者请求超时,则会进行降级处理(优先保证核心服务,而非核心服务不可用或弱可用)
故障时,不会被阻塞,更不会无休止的等待或者看到系统崩溃,最多会影响这个依赖服务对应的线程池中的资源,对其它服务没有响应。至少可以看到一个执行结果(例如返回友好的提示信息)
5.1.2 服务熔断
熔断就是大量请求都超时情况下(比如某个模块出问题了),主动熔断,防止整个系统直接裂开。Hystrix的熔断可以自动恢复,但并不是马上恢复,而是用小批量的请求测试服务器是否恢复。
熔断状态机3个状态:
- Closed:关闭状态,所有请求都正常访问。
- Open:打开状态,所有请求都会被降级。Hystix会对请求情况计数,当一定时间内失败请求百分比达到阈值,则触发熔断,断路器会完全打开。默认失败比例的阈值是50%,请求次数最少不低于20次。
- Half Open:半开状态,open状态不是永久的,打开后会进入休眠时间(默认是5S)。随后断路器会自动进入半开状态。此时会释放部分请求通过,若这些请求都是健康的,则会完全关闭断路器,否则继续保持打开,再次进行休眠计时
要注意不要搞混了,Closed才是可用,Open说明出问题了,这里理解成电路就行,闭合(Closed),断开(Open)
5.2 hystrix使用步骤
hystrix用在消费者而不是提供者,因为消费者是调用方
- 添加依赖
1 | <dependency> |
- yml
这个呢,由于生产环境中访问远程服务器要慢一点的,默认1s就熔断未免太过苛刻,开发环境下访问本地服务器倒是无所谓,要记得设置啊
1 | hystrix: |
- 引导类加注解@EnableCircuitBreaker
- controller方法上方添加注解,自定义熔断方法,修改了返回值为String方便测试
- 测试,我们可以把提供者关掉来模拟服务器出现问题
访问,我们可以看到错误提示
这就是我们有时浏览网页的报错提示(不是那种返回500时指定的,这种通常是并发量巨大,比如广东海洋大学的抢课系统!!那种)
- 也可以在类上方添加全局熔断方法@DefaultProperties(defaultFallback = “全局熔断方法名”)
这个不多说了,看看就懂了,要注意的是方法上还是要加@HystrixCommand注解的,只是不指定的话默认使用全局的熔断函数,而且全局熔断方法的参数要为空,返回值要和被熔断方法一致(不是String的话自己写序列化就行,一般都是json字符串的,问题不大)
5.3熔断机制测试
咱们主动抛出异常
在服务中心,提供者和消费者都正常的情况下,我们快速的访问http://localhost/consumer/user?id=1至少20次,触发了熔断机制,于是访问id=2时也会提示服务器正忙
5s后,再次访问id=2,恢复正常
6. Feign
6.1 feign概述
先看我们原来的http请求,他完成了消费者到提供者的消息传递
return restTemplate.getForObject("http://service-provider/user/" + id, String.class);
你看原来这段字符串拼接,是不是很low!!??
Feign可以把Rest的请求进行隐藏,伪装成类似SpringMVC的Controller一样。你不用再自己拼接url,拼接参数等等操作,一切都交给Feign去做。
6.2 feign使用步骤
feign是在引导类中使用
- 依赖
1 | <dependency> |
- 引导类注解@EnableFeignClients,且不再需要RestTemplate发送http请求
- 定义一个接口,接口上方的@FeignClient(“service-provider”)是服务提供者的服务名,方法对应提供者的controller接口方法(复制过来去掉方法体)
要注意提供者类上方还有一个@RequestMapping(“user”),这里不要也写成全局,而是在每一个方法的路径中定义
- controller改动也是比较大的,首先就是Feign继承了Ribbon和Hystrix,所以我们不需要按照以前的方式写callback函数了,可以看到跟以前的controller差不多了(@Controller+String类型也是可以的,返回值多加了个.toString()而已),比较优雅
1 |
|
6.3 添加Hystrix支持
原来的hystrix去掉了,Feign中集成了hystrix,但默认是关闭的,需要在yml中开启
- yml
1 | feign: |
- 新建一个类,实现UserClient接口,并加入spring容器
1 |
|
- 接口加上fallback类
要注意由于不是只有一个参数了,所以前面的value=要加上了
- 关掉提供者后测试结果如下
可以看到实现了熔断机制
6.4 添加ribbon支持
是的,集成后默认开启,无需配置!
7. Zuul
7.1 zuul概述
不管是来自于客户端(PC或移动端)的请求,还是服务内部调用。一切对服务的请求都会经过Zuul这个网关,然后再由网关来实现 鉴权、动态路由等等操作。Zuul就是我们服务的统一入口。
7.2 zuul使用步骤
- 使用初始化工具创建模块,zuul组件在spring cloud routing下
目录和前面的eureka那些同级就行 没有特殊要求
- yml
1 | server: |
- 引导类上方加上@EnableZuulProxy注解
也就是说localhost:10010/service-consumer会重定向到localhost:80
7.3 搭配eureka食用
yml中写死路径就很low,我们从eureka动态的获取服务列表
为zuul模块添加eureka还是那三大步骤
- 为zuul模块添加eureka依赖(别弄错了呀,要是这个starter的)
1 | <dependency> |
- zuul模块的yml增加
1 | eureka: |
- 引导类
- yml改成服务名
启动
简洁写法
当我们想让服务名和路径不一致时可以用上面的写法,也可以像下面这样简写,那么路径名默认就是服务名通常用这种
1 | zuul: |
最简洁写法
不写!
是的,依然可以通过服务名访问,并且所有服务都默认可以通过zuul端口+服务名访问
7.4 官方推荐api前缀
官方推荐加上前缀api,看看我们最终的yml
1 | zuul: |
这样浏览器访问时需要在端口后加上api,以后有api的就说明是通过zuul的了
因为配了映射名,我们也可以通过映射名访问
可以看到有两次user,第一次是yml中的映射,第二次才是provider中的controller类上方的@RequestMapping(“user”)全局路径,这也就是为什么很多人controller不带全局路径的原因,他们更习惯用zuul来完成每个模块的全局路径
7.5 zuul过滤器
和传统过滤器差不多,继承ZuulFilter然后复写run方法处理过滤操作
1 |
|
比如我们这里run方法是判断请求中是否带有参数token
正常流程:
- 请求到达首先会经过pre类型过滤器,而后到达route类型,进行路由,请求就到达真正的服务提供者,执行请求,返回结果后,会到达post过滤器。而后返回响应。
异常流程:
- 整个过程中,pre或者route过滤器出现异常,都会直接进入error过滤器,在error处理完毕后,会将请求交给POST过滤器,最后返回给用户。
- 如果是error过滤器自己出现异常,最终也会进入POST过滤器,将最终结果返回给请求客户端。
- 如果是POST过滤器出现异常,会跳转到error过滤器,但是与pre和route不同的是,请求不会再到达POST过滤器了。
7.6 添加Hystrix和ribbon支持
默认是支持的,修改一下超时时间就好了
1 | hystrix: |
附录
1. @SpringCloudApplication注解
引导类上的注解越来越多
在微服务中,经常会引入上面的三个注解,于是Spring就提供了一个组合注解@SpringCloudApplication
所以在引导类上写这个注解就可以了!