架构的演进及解决方案:
根据上一篇文章我们主要了解了 架构的演进 以及对应的 解决方案与技术:
本篇我们对这些技术进行主要介绍
介绍前先来看一下我们架构演进过程中的需求:
- 服务器方面:
用户增多,请求增多 --> 加服务器 --> 分配请求 --> 负载均衡/反向代理/路由(轮询,随机)
请求派发后的 session、cookie、JSESSIONID 问题 --> 固定来访者到同一台服务器(负载均衡的 Hash 算法)
被固定派发到的服务器宕机 --> 共享 session --> session共享不可行,使用 redis 存储 session
优化:负载均衡用 LVS(主从备份)做,再添加监听服务器(同一个IP) 确保服务器集群稳定
- 代码方面:
修改 BUG,添加功能部署重启 --> 前后端分离 + 功能模块拆分 + RPC(Controller随意调Service)
RPC --> 混乱无序,缺少治理 --> redis 存储调度记录 --> 宕机难以解决 --> 注册中心(动态感知)
微服务:微服务仍然是分布式的范畴,只是将上面的服务拆的更小了
- 数据库方面:
采用集群模式设计,摒弃外键,使用冗余数据减少操作关联,实现空间换时间,高并发情况采用 redis 集群过滤 SQL 操作
- Redis 缓存问题:
1、缓存穿透
查询无效数据 --> 缓存穿透 --> 主键自增:比对区间;主键非自增:所有数据主键的比对
2、缓存穿刺(击穿)
高并发访问高热度数据 且 缓存中没有,高并发请求都去数据库 --> 缓存穿刺(击穿)--> 分布式锁
3、缓存雪崩
大量缓存数据 在 极短时间内 到期 --> 缓存雪崩 --> 缓存有效期设置为区间
4、缓存倾斜
高并发访问高热度数据 且 缓存中存在 且 高热度数据只被读出到一台redis中 --> 这台 redis 崩溃 --> 缓存倾斜 -->
<1>、热点数据所在的服务器一定是主从,做读写分离,从机均分查询请求
<2>、将 SQL操作 分配到不同的服务器,通过改写 key 的方式,让不同的机器放相同的数据:zly --> zly:1;zly:2;zly:3......(zly:n :redis n),同时服务器也打上标记(1~n),分别请求相对应的 redis
微服务:
有人倾向于在系统设计与开发中采⽤ 微服务⽅式 实现软件系统的松耦合、跨部⻔开发;同时,反对之声也很强烈,持反对观点的⼈表示微服务增加了系统维护、 部署的难度,导致⼀些功能模块或代码⽆法复⽤,同时微服务允许使⽤不同的语⾔和框架来开发各个 系统模块,这⼜会增加系统集成与测试的难度,⽽且随着系统规模的⽇渐增⻓,微服务在⼀定程度上也会导致系统变得越来越复杂。http://microservices.io
- 什么是微服务架构?
简单说就是将⼀个完整的应⽤(单体应⽤)按照⼀定的拆分规则拆分成多个不同的服务,每个服务都能独⽴地进⾏开发、部署、扩展。服务于服务之间通过注入 RESTful api
- 优点:
1、易于开发和维护,只需要关注每个服务的单独业务即可,不需要关⼼其他
2.、启动较快 3、局部修改容易部署 4、技术栈不受限 5、按需伸缩 6、DevOps
- 缺点:
1、运维成本较⾼ 2、分布式复杂 3、接⼝调整成本⾼
4、重复劳动,因为⾮集中项⽬,当使⽤不同技术栈的时候,⼀些功能可能需要多次开发
SpringCloud:
Spring Cloud是在Spring Boot的基础上构建的,⽤于简化分布式系统构建的⼯具集,为开发⼈员提供 快速建⽴分布式系统中的⼀些常⻅的模式。
例如:配置管理(configuration management),服务发现(service discovery),断路器 (circuit breakers),智能路由( intelligent routing),微代理(micro-proxy),控制总线 (control bus),⼀次性令牌( one-time tokens),全局锁(global locks),领导选举 (leadership election),分布式会话(distributed sessions),集群状态(cluster state)。
- Spring Cloud 包含了多个子项目:
例如:Spring Cloud Config、Spring Cloud Netflix等
- Spring Cloud 特点:
1、约定优于配置 2、开箱即⽤、快速启动 3、适⽤于各种环境 4、轻量级的组件,⽐如 服务发现组件 Eureka
5、组件的⽀持很丰富,功能很⻬全 包含:配置中⼼,注册中⼼,智能路由
6、选型中⽴,比如服务发现组件,不限制必须使⽤某⼀种,例如 Eureka,Zookeeper,Consul
服务器提供者与消费者
服务提供者:服务的被调用方,即:为其他服务提供服务的服务
服务消费者:服务的调用方,即:依赖其他服务的服务
eureka + ribbon :
使用 Eureka 后我们可以通过服务器找到我们注册的客户端,但是如果我们的微服务有好几个,如何实现负载均衡,而且配置文件中还是有⼀些硬编码的存在,如何解决,我们就需要用到 ribbon。
Feign 中自带 ribbon,所以不需要导⼊依赖。Ribbon 是 Netflix 发布的云中间层服务开源项⽬,其主要功能是提供客户端侧负载均衡算法。 Ribbon 客户端组件提供⼀系列完善的配置项如连接超时,重试等。
简单的说,Ribbon是⼀个客户端负载均衡器,我们可以在配置文件中列出 Load Balancer 后⾯所有的机器,Ribbon会⾃动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器,我们也很容易使⽤ Ribbon实现⾃定义的负载均衡算法。
Ribbon⼯作时分为两步:第⼀步先选择 Eureka Server,它优先选择在同⼀个 Zone 且负载较少的 Server;第⼆步再根据⽤户指定的策略,再从 Server 取到的服务注册列表中选择⼀个地址。其中Ribbon提供了多种策略,例如轮询round robin、随机Random、根据响应时间加权等。
Erueka 采用的是客户端发现模式,Ribbon 实现的是客户端侧负载均衡。
<!--负载均衡依赖 客户端发现模式;服务端发现模式
客户端侧负载均衡;服务端侧负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
<!--eureka加密码的依赖包,设置密码后每次访问都需要输入用户名和密码
用户名:默认为user
密码:每次启动时默认随机生成密码-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
启动类:
@SpringBootApplication
@EnableEurekaClient
//开启负载均衡,参数为要给哪个服务开启,被开启的服务在访问的时候会有负载均衡
@RibbonClient(value = "04USERPROVIDER",configuration = RibbonConfig.class)
@RibbonClients({@RibbonClient("04USERPROVIDER"),@RibbonClient("04USERPROVIDER")})
public class OrderConsumerApp {
public static void main(String[] args){
SpringApplication.run(OrderConsumerApp.class,args);
}
/**
* 创建rest访问对象
* @return
*/
@Bean
@LoadBalanced//给rest模板开启负载均衡,注意一定要加这个注解,不然的话会提示找不到主机
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
负载均衡策略配置:
@Configuration
/*
The CustomConfiguration class must be a @Configuration class,
but take care that it is not in a @ComponentScan for the main application context.
(@ComponentScan在SpringBootApplication中包含,所以需要考虑启动类和配置类的层级关系)
Otherwise, it is shared by all the @RibbonClients. If you use @ComponentScan (or @SpringBootApplication)
*/
public class RibbonConfig {
@Bean
public IRule iRule(){
return new RandomRule();//服务选择策略:随机;默认的机制是轮询
}
}
负载均衡策略配置类 与 启动类 相对位置问题 解决方案:
/*
1、注解方式:
为ComponentScan配置一个过滤器,类型为@ComponentScan.Filter,过滤包含注解Abc.class的类
凡是类上边有Abc.class注解的类都不会被扫描
*/
//带有特定标记的类不会被扫描
@ComponentScan(excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Abc.class)})
#2、properties方式:给指定的服务设置负载均衡的方式
04USERPROVIDER1:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
feign:
Feign 是⼀个声明式的 Web 服务客户端,要想使用 Fegin 只需要定义⼀个接口并且添加响应的注解即可,支持包括 Feign注解和 JAX-RS 注解。Feign 还支持可插拔编码器和解码器。
Spring Cloud 增加了对 Spring MVC 注解的⽀持,并使⽤ Spring Web 中默认使用的 HttpMessageConverters 。Spring Cloud 集成 Ribbon 和 Eureka 以在使用 Feign 时提供负载均衡的 http 客户端。
只有在 DEBUG 级别下,Feign 才会打印⽇志,打印日志的设置方式:在 yml 中添加 logging: level:(要打印⽇志的 Feign 的接口的全限定名称): DEBUG
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
@FeignClient("04USERPROVIDER")//配置当前接口为feign的客户端,内部访问04PUSERPROVIDER这个服务
//Spring Cloud integrates Ribbon and Eureka to provide a load balanced http client when using Feign.
public interface UserFeign {
@RequestMapping("/users/{id}")
Users getUserInfo(@PathVariable("id") int id);
@GetMapping("/save")
//如果请求的参数是简单数据,会直接发送出去,
//如果是个复杂对象,不管上面是@GetMapping还是@PostMapping,都会以POST模式发出去
String save(Users users);
}
服务降级
- 服务雪崩:
A --> B --> C --> D D崩溃后 --> C抛出异常 --> B抛出异常 --> A抛出异常
在微服务架构中通常会有多个服务层调⽤,⼤量的微服务通过网络进行通信,从而支撑起整个系统。 各个微服务之间也难免存在⼤量的依赖关系。然而任何服务都不是100%可用的,网络往往也是脆弱的,所以难免有些请求会失败。基础服务的故障导致级联故障,进而造成了整个系统的不可用,这种现象被称为服务雪崩效应。
服务雪崩效应描述的是⼀种因服务提供者的不可⽤导致服务消费者的不可用,并将不可用逐渐放大的过程。
- 超时机制:
通过⽹络请求其他服务时,都必须设置超时。正常情况下,⼀个远程调⽤⼀般在几十毫秒内就返回了。当依赖的服务不可用,或者因为网络问题,响应时间将会变得很⻓(几十秒)。而通常情况下,⼀次远程调用对应了⼀个线程/进程,如果响应太慢,那这个线程/进程就会得不到释放。而线程/进程都对应了系统资源,如果⼤量的线程/进程得不到释放,并且越积越多,服务资源就会被耗尽,从而导致资深服务不可用。所以必须为每个请求设置超时。
- 断路器模式:
当依赖的服务有大量超时时,再让新的请求去访问已经没有太⼤意义,只会无谓的消耗 现有资源。断路器可以实现快速失败,如果它在⼀段时间内侦测到许多类似的错误(譬如超时),就会强迫其以后的多个调⽤快速失败,不再请求所依赖的服务,从而防止应用程序不断地尝试执行可能会失败的操作,这样应用程序可以继续执行而不用等待修正错误,或者浪费CPU时间去等待长时间的超时。断路器也可以使应用程序能够诊断错误是否已经修正,如果已经修正,应用程序会再次尝试调用操作。 断路器模式就像是那些容易导致错误的操作的⼀种代理。这种代理能够记录最近调用发⽣错误的次数,然后决定使用允许操作继续,或者立即返回错误。
“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝), 向调用方返回一个符合预期的、可处理的备选响应(FallBack) , 而不是长时间的等待或者抛出调用方无法处理的异常 , 这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
- 服务熔断:
熔断模式:这种模式主要是参考电路熔断,如果某个目标服务调用慢,有大量超时或服务挂掉了,此时,熔断该服务的调用,对于后续调用请求,不再继续调用目标服务,直接返回,快速释放资源。
如果目标服务情况好转则恢复调用(即隔一段时间会回头看一次)。
- 服务限流:
限流模式:提前对各个类型的请求设置最高的QPS(并发量 / 平均响应时间)阈值,若高于设置的阈值则对该请求直接返回,不再调用后续资源。
这种模式只能解决系统整体资源分配问题,即限流并不解决雪崩效应。
- Hystrix:
是一个用于处理分布式系统的 延迟 和 容错 的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下, 不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
隔离方式 | 是否支持超时 | 是否支持熔断 | 隔离原理 | 是否是异步调用 | 资源消耗 |
线程池隔离 | 支持,可直接返回 | 支持,当线程池到达maxSize后,再请求会触发fallback接口进行熔断 | 每个服务单独用线程池 | 可以是异步,也可以是同步,看调用的方法 | 大,大量线程的上下文切换,容易造成机器高负载 |
信号量隔离 | 不支持,如果阻塞,只能通过调用协议(如:socket超时才能返回) | 支持,当信号量达到maxConcurrentRequests后,再请求会触发fallback | 通过信号量的计数器 | 同步调用,不支持异步 | 小,只是个计数器 |
线程池:请求线程去线程池拿一个线程:主动,设置超时时间,超时切断
信号量:令牌筒,被动,被动切断
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
@RestController
public class OrderController {
@Autowired
private RestTemplate restTemplate;//一个专门请求rest风格的url对象
@Autowired
private LoadBalancerClient loadBalancerClient;//负载均衡客户端
@RequestMapping("/orders/{id}")
//创建一个类似于CommandHelloWorld的类,继承HystrixCommand,调用run方法,run会调用此处指向的方法
@HystrixCommand(fallbackMethod = "abc", commandProperties = {@HystrixProperty(name = "execution.isolation.strategy", value = "THREAD"), @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1")})//模拟非常短的超时时间,可以看到每次请求都凉凉了
// @HystrixCommand(commandProperties = {@HystrixProperty(name = "execution.isolation.strategy", value = "SEMAPHORE")})
//设置信号量的降级策略
//线程池 和 信号量 的隔离策略共存时,走信号量策略
public Users getInfo(@PathVariable int id){
System.out.println("getInfo-------->"+Thread.currentThread().getName());
Users user = restTemplate.getForObject("http://04USERPROVIDER/users/"+id, Users.class);
return user;
}
@RequestMapping("/test")
public String choose(){
int port = loadBalancerClient.choose("04USERPROVIDER").getPort();
System.err.println("端口==========>"+port);
return port+"";
}
/*
线程池 和 信号量 策略选择问题:
自定义超时时间必须用线程池
一般会设置超时时间,维护用户体验,信号量这种被动等待的方式在服务的链式调用发生“服务雪崩”时,请求多的情况下等待时间太久
*/
public Users abc(int id){
System.out.println("abc========>"+Thread.currentThread().getName());
Users users = new Users();
users.setId(-1);
users.setUserName("laowang");
users.setEmail("xxx@163.com");
return users;
}
}
- hystrix-dashboard:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
@SpringBootApplication
@EnableHystrixDashboard//此处监控15userconsumer-eureka-feign-hystrix-fallbackfactory
public class DashBoardStart {
public static void main(String[] args){
SpringApplication.run(DashBoardStart.class,args);
}
}
eureka + feign(ribbon) + hystrix:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
</dependencies>
feign: #Feign will wrap all methods with a circuit breaker.
hystrix:
enabled: true #必须为true,否则断路器无效
- 单个Fallback:
@Component//服务降级可以写到Controller里,但是用feign更好,因为是feign做请求,此处为feign默认实现类
public class UserFeignFallback implements UserFeign {
@Override
public Users getUserInfo(int id) {
Users users = new Users();
users.setId(-1000);
users.setUserName("mogui");
users.setEmail("nozuonodie@163.com");
return users;
}
@Override
public String save(Users users) {
return "chenggong";
}
}
- Fallback工厂:
@Component //泛型:当前的工厂是给哪个feign client用的,就写哪个
public class UserFeignFallbackFactory implements FallbackFactory<UserFeign> {
@Override
public UserFeign create(Throwable throwable) {
return new UserFeignFallback();//或者重写方法写新实现类
}
}
网关 Zuul:
Zuul’s rule engine lets rules and filters be written in essentially any JVM language.
解决问题:
1、api接口复杂混乱问题(https://www.jd.com页面的每个模块都是一个功能接口,一个功能挂了只会影响一部分页面)
2、跨域问题
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
management:
endpoints:
web:
exposure:
include: '*' #允许访问所有的管理地址(04xxx --> 04xxx/**)
zuul: #http://desktop-6ffb88r:15000/actuator/routes
ignored-services: 04userprovider
routes:
05userconsumer: /abc/** #自定义访问路径:/abc/**的请求会被转发到05userconsumer
zuul:
routes:
abcccc: #随便写,但是不允许重复
path: /bcd/**
serviceId: 05userconsumer
prefix: /xxx #给所有的地址添加前缀
@SpringBootApplication
@EnableZuulProxy
@EnableEurekaClient//开启服务注册
public class ZuulApplication {
public static void main(String[] args){
SpringApplication.run(ZuulApplication.class, args);
}
}
Zuul + hystrix:
/**
* 解决的是:请求下一个服务的时候,下一个服务出问题了
* Prudence 2019/7/3 21:28
*/
@Component
public class MyFallBackProvider implements FallbackProvider {
/**
* 设置当前是给哪个服务开启fallback,返回值就是服务的名字
* @return
*/
@Override
public String getRoute() {
return "*";//给所有的服务返回
}
/**
* 当出现问题的时候返回给调用者的具体内容
* @param route
* @param cause
* @return
*/
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
return new ClientHttpResponse() {
//此处正常情况应该根据服务和异常来分别返回不同的内容
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.BAD_REQUEST;
}
@Override
public int getRawStatusCode() throws IOException {
return HttpStatus.BAD_GATEWAY.value();
}
@Override
public String getStatusText() throws IOException {
return HttpStatus.BAD_GATEWAY.toString();
}
@Override
public void close() {
}
/**
* 响应正文
* @return
* @throws IOException
*/
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("恭喜你,电脑太垃圾了,换电脑吧".getBytes());
}
/**
* 响应头
* @return
*/
@Override
public HttpHeaders getHeaders() {
HttpHeaders httpHeaders = new HttpHeaders();
/*
add 和 set 有什么区别:
set覆盖,add追加
*/
httpHeaders.set("sfdfsfdsfdsfdsfds", "sfdsdfsfisdfs");
httpHeaders.set(HttpHeaders.CONTENT_TYPE, "text/html;charset=utf-8");
return httpHeaders;
}
};
}
}