简单来说,微服务架构风格是一种将一个单一应用程序开发为一组小型服务的方法,每个服务运行在自己的进程中,服务间通信采用轻量级通信机制(通常用HTTP资源API)。这些服务围绕业务能力构建并且可通过全自动部署机制独立部署。这些服务共用一个最小型的集中式的管理,服务可用不同的语言开发,使用不同的数据存储技术。
http://blog.cuicc.com/blog/2015/07/22/microservices/
简单的说,微服务是架构设计方式,分布式是系统部署方式,两者概念不同
分布式服务顾名思义服务是分散部署在不同的机器上的,一个服务可能负责几个功能,是一种面向SOA架构的,服务之间也是通过 RPC 来交互或者是 webservice 来交互的
逻辑架构设计完后就该做物理架构设计,系统应用部署在超过一台服务器或虚拟机上,且各分开部署的部分彼此通过各种通讯协议交互信息,就可算作分布式部署,生产环境下的微服务肯定是分布式部署的,分布式部署的应用不一定是微服务架构的,比如集群部署,它是把相同应用复制到不同服务器上,但是逻辑功能上还是单体应用
简单来说微服务就是很小的服务,小到一个服务只对应一个单一的功能,只做一件事
这个服务可以单独部署运行,服务之间可以通过 RPC 来相互交互,每个微服务都是由独立的小团队开发,测试,部署,上线,负责它的整个生命周期
在做架构设计的时候,先做逻辑架构,再做物理架构,当你拿到需求后,估算过最大用户量和并发量后,计算单个应用服务器能否满足需求
如果用户量只有几百人的小应用,单体应用就能搞定,即所有应用部署在一个应用服务器里
如果是很大用户量,且某些功能会被频繁访问,或者某些功能计算量很大,建议将应用拆解为多个子系统,各自负责各自功能,这就是微服务架构
微服务相比分布式服务来说,它的粒度更小,服务之间耦合度更低,由于每个微服务都由独立的小团队负责,因此它敏捷性更高,分布式服务最后都会向微服务架构演化,这是一种趋势, 不过服务微服务化后带来的挑战也是显而易见的,例如服务粒度小,数量大,后期运维将会很难
选型依据:
主流微服务框架:
微服务框架对比:
分布式微服务架构下的一站式解决方案,是各个微服务架构落地技术的集合体,俗称微服务全家桶
相关资源:
SpringBoot 和 SpringCloud 有啥关系?
版本说明
版本选择:
商品服务
查询商品列表
查询商品详情
订单服务
案例功能:前台调用订单服务,订单服务远程调用商品服务获取商品详情信息,基于该商品信息创建订单
为什么需要注册中心:
远程服务调用在没有注册中心前存在什么问题
微服务应用和机器越来越多,调用方需要知道接口的网络地址,如果靠配置文件的方式去控制网络地址,对于动态新增机器,url地址维护带来很大问题
注册中心提供服务注册与发现功能,对服务的url地址进行统一管理
常见的注册中心:
zookeeper、Eureka、consul、nacos、etcd
Eureka采用了CS的设计架构,Eureka Server作为服务注册功能的服务器,它是服务注册中心。而系统中的其他微服务,使用Eureka的客户端连接到 Eureka Server 并维持心跳连接。这样系统的维护人员就可以 通过Eureka Server来监控 系统中的各个微服务是否正常运行
在服务注册于发现中,有一个注册中心。当服务器启动的时候, 会把当前自己服务器的信息
比如:服务地址(IP:端口)等以别名方式注册到注册中心, 另一个(消费者|服务提供者),以该别名的方式去注册中心上获取到实际的服务通信地址(IP:端口),然后通过RPC框架进行远程方式(Ribbon+RestTemplate)调用接口实现功能
SpringCloud 将它集成在其子项目 spring-cloud-netflix 中,以实现 SpringCloud 的服务发现功能
Eureka服务端,负责服务发现与管理
Provider
启动的时候向注册中心上报自己的网络信息
Consumer
启动的时候向注册中心上报自己的网络信息,拉取provider的相关网络信息
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.1.5.RELEASEversion>
<relativePath/>
parent>
<properties>
<java.version>1.8java.version>
<spring-cloud.version>Greenwich.SR1spring-cloud.version>
properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.12version>
<scope>providedscope>
dependency>
dependencies>
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
dependency>
dependencies>
application.yml
server:
port: 8761
eureka:
instance:
hostname: localhost
client:
#是否将自己注册进去eureka,false为不注册,true注册
registerWithEureka: false
#是否从eureka抓取注册信息,单点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: false
serviceUrl:
#eureka注册中心地址
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
官方文档
https://cloud.spring.io/spring-cloud-netflix/single/spring-cloud-netflix.html#spring-cloud-eureka-server
启动类-EurekaMain8761
@SpringBootApplication
@EnableEurekaServer
public class EurekaMain8761 {
public static void main(String[] args) {
SpringApplication.run(EurekaMain8761.class, args);
}
}
运行启动类,打开浏览器输入
http://localhost:8761
pom.xml
暂时不用
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Product implements Serializable {
private Long id;//商品id
private String name;//商品名称
private BigDecimal price;//商品价格
private int stock;//商品库存
}
pom.xml
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>comgroupId>
<artifactId>cloud-product-apiartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
application.yml
server:
port: 8080
spring:
application:
name: product-server
eureka:
client:
#是否将自己注册进去eureka,false为不注册,true注册
registerWithEureka: true
#是否从eureka抓取注册信息,单点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
serviceUrl:
#eureka注册中心地址
defaultZone: http://localhost:8761/eureka/
官方文档
https://cloud.spring.io/spring-cloud-netflix/single/spring-cloud-netflix.html#netflix-eureka-client-starter
启动类
@SpringBootApplication
@EnableEurekaClient
public class ProductMain8080 {
public static void main(String[] args) {
SpringApplication.run(ProductMain8080.class,args);
}
}
//这里本应该使用mybatis接口,因为不想将项目弄麻烦,不整合mybatis类,使用class类模拟
@Repository
public class ProductMapper {
//模拟查询出来的数据
private static final Map<Long, Product> PRODUCT_MAP = new HashMap<>();
static {
PRODUCT_MAP.put(1L, new Product(1L, "小米10", new BigDecimal("1000"), 10000));
PRODUCT_MAP.put(2L, new Product(2L, "荣耀10", new BigDecimal("2000"), 10000));
PRODUCT_MAP.put(3L, new Product(3L, "iphone10", new BigDecimal("3000"), 10000));
PRODUCT_MAP.put(4L, new Product(4L, "vivo10", new BigDecimal("4000"), 10000));
}
//模拟数据库查询
public void save(Product product){
PRODUCT_MAP.put(PRODUCT_MAP.size()+0L, product);
}
public Product get(Long id){
return PRODUCT_MAP.get(id);
}
}
@Service
public class ProductServiceImpl implements IProductService {
@Autowired
private ProductMapper productMapper;
@Override
public void save(Product product) {
productMapper.save(product);
}
@Override
public Product get(Long id) {
return productMapper.get(id);
}
}
@RestController
@RequestMapping("products")
public class ProductController {
@Autowired
private IProductService productService;
@GetMapping("/get/{id}")
private Object get(@PathVariable Long id){
return productService.get(id);
}
}
先启动cloud-eureka-server8761
再启动cloud-provider-product8080
打开浏览器输入
http://localhost:8761
查看服务注册栏
Instances currently registered with Eureka
打开浏览器,查询id为1的商品信息
http://localhost:8080/products/get/1
Register(注册)
Renew(更新 / 续借)
Fetch Registry(抓取注册信息)
Cancel(取消)
概述:
保护模式主要用于一组客户端和 Eureka Server 之间存在网络分区场景下的保护,一旦进入保护模式EurekaServer 将会尝试保护其注册表中的信息,不再删除服务注册表的数据,也就是就算服务断开也不销毁任何服务
Eureka Server 在运行期间会去统计心跳失败比例在 15 分钟之内是否低于 85%,如果低于 85%,Eureka Server 会将这些实例保护起来,让这些实例不会过期,但是在保护期内如果服务刚好这个服务提供者非正常下线了,此时服务消费者就会拿到一个无效的服务实例,此时会调用失败,对于这个问题需要服务消费者端要有一些容错机制,如重试,断路器等。
我们在单机测试的时候很容易满足心跳失败比例在 15 分钟之内低于 85%,这个时候就会触发 Eureka 的保护机制,一旦开启了保护机制,则服务注册中心维护的服务实例就不是那么准确了,此时我们可以使用eureka.server.enable-self-preservation=false
来关闭保护机制,这样可以确保注册中心中不可用的实例被及时的剔除(不推荐)
server:
port: 8761
eureka:
instance:
hostname: localhost
client:
#不向注册中心注册自己
registerWithEureka: false
#自己是服务端,不需要检查服务
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
server:
#默认是true,现在关闭自我保护机制,保证不可用服务被删除
enable-self-preservation: false
默认是等90秒钟(30秒发一次,共三次),eureka服务才删除,如果不想等,可在客户端配置修改90s时间
eureka:
client:
#是否将自己注册进去eureka,false为不注册,true注册
registerWithEureka: true
#是否从eureka抓取注册信息,单点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
serviceUrl:
defaultZone: http://localhost:8761/eureka/
instance:
#eureka客户端向服务端发送心跳的时间间隔,单位为秒,默认是30
lease-renewal-interval-in-seconds: 1
#eureka服务端收到最后一次心跳等待的时间上限,单位为秒,默认是90,超时剔除
lease-expiration-duration-in-seconds: 2
domain
@Setter
@Getter
public class Order implements Serializable {
private String orderNo;
private Date createTime;
private String productName;
private BigDecimal productPrice;
private Long userId;
}
pom.xml
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>comgroupId>
<artifactId>cloud-product-apiartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
<dependency>
<groupId>comgroupId>
<artifactId>cloud-order-apiartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
application.yml
server:
port: 8090
spring:
application:
name: order-server
eureka:
client:
#是否将自己注册进去eureka,false为不注册,true注册
registerWithEureka: true
#是否从eureka抓取注册信息,单点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
serviceUrl:
defaultZone: http://localhost:8761/eureka/
instance:
#eureka客户端向服务端发送心跳的时间间隔,单位为秒,默认是30
lease-renewal-interval-in-seconds: 1
#eureka服务端收到最后一次心跳等待的时间上限,单位为秒,默认是90,超时剔除
lease-expiration-duration-in-seconds: 2
官方文档
https://cloud.spring.io/spring-cloud-netflix/single/spring-cloud-netflix.html#netflix-eureka-client-starter
启动类
@SpringBootApplication
@EnableEurekaClient
public class OrderMain8090 {
public static void main(String[] args) {
SpringApplication.run(OrderMain8090.class, args);
}
}
@Service
public class OrderServiceImpl implements IOrderService {
@Override
public Order save(Long userId, Long productId) {
Product product = null; //假装通过远程调用 product-server 获取
Order order = new Order();
order.setOrderNo(UUID.randomUUID().toString().replace("-",""));
order.setCreateTime(new Date());
order.setUserId(userId);
order.setProductName(product.getName());
order.setProductPrice(product.getPrice());
System.out.println("执行保存订单操作");
return order;
}
}
为了方便测试,使用 GET 请求方式,按理说要使用 POST 请求
@RestController
@RequestMapping("orders")
public class OrderController {
@Autowired
private IOrderService orderService;
@GetMapping("/save/{userId}/{productId}")
public Order save(@PathVariable Long userId,@PathVariable Long productId){
return orderService.save(userId,productId);
}
}
使用目前已学习的技术来实现远程调用
A项目:
cloud-provider-product8080
B项目:
cloud-consumer-order8090
AB 项目交互 ------> 之前发短信 ----> http协议 -----> RestTemplate
此处不会有 跨域 的错误,是因为 RestTemplate 类就相当于是 浏览器,可以访问任何端口服务
而出现跨域错误是,在项目中,使用 Ajax 的方式,url 地址是跨域请求
在 OrderMain8090 启动类中添加 RestTemplate 的 bean
@Bean
public RestTemplate template(){
return new RestTemplate();
}
使用 RestTemplate.getObject 获取远程接口的信息
@Service
public class OrderServiceImpl implements IOrderService {
/*
这种实现方式存在问题:写死 product-server 的ip端口,后续如果product-server做了集群
那么这种方式就无法使用集群中其他 product-server 服务,比如说端口8081的这个服务
此时解决方案:使用ribbon组件里面负载均衡(狭义上的理解多个服务平均分配请求)功能
*/
@Autowired
private RestTemplate template;
public static final String PRODUCT_URL = "http://localhost:8080";
@Override
public Order save(Long userId, Long productId) {
//真实远程获取
//http请求方式获取 product-server 服务里面的商品信息
Product product = template.getForObject(
PRODUCT_URL + "/products/get/" + productId, Product.class
);
Order order = new Order();
order.setOrderNo(UUID.randomUUID().toString().replace("-",""));
order.setCreateTime(new Date());
order.setUserId(userId);
order.setProductName(product.getName());
order.setProductPrice(product.getPrice());
System.out.println("执行保存订单操作");
return order;
}
}
测试
http://localhost:8090/orders/save/1/3
修改启动类 OrderMain8090
@SpringBootApplication
@EnableEurekaClient
public class OrderMain8090 {
@LoadBalanced //ribbon给restTemplate开启负载均衡访问操作
@Bean
public RestTemplate template(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(OrderMain8090.class, args);
}
}
修改订单服务 OrderServiceImpl
//因为要进行负载均衡调用,此时需要的是Eureka中 product-server 服务名
//因为Eureka中可以通过服务名获取到product-server所有服务集群的ip与端口
@Autowired
private RestTemplate template;
public static final String PRODUCT_URL = "http://PRODUCT-SERVER";
修改 ProductController
@RestController
@RequestMapping("products")
public class ProductController {
@Autowired
private IProductService productService;
@Value("${server.port}")
private String port;
@GetMapping("/get/{id}")
private Object get(@PathVariable Long id){
Product product = productService.get(id);
//防止端口重复拼接
Product pp = new Product();
BeanUtils.copyProperties(product,pp);
//想让order-server服务调用product接口时,知道调用哪一个服务(8080?8081)
pp.setName(product.getName() + "_" + port);
return pp;
}
}
测试
1:先启动ProductMain8080
2:修改application.yml里面端口8081
再启动ProductMain8080 , 模拟启动了2个Product-server服务
上面的负载均衡策略是 轮询选择 即两个服务之间循环调用
修改 cloud-consumer-order8090 配置
注意:服务的名称需要和代码中的服务名称一致,不然是修改不了负载均衡策略
server:
port: 8090
spring:
application:
name: order-server
eureka:
client:
#是否将自己注册进去eureka,false为不注册,true注册
registerWithEureka: true
#是否从eureka抓取注册信息,单点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
serviceUrl:
defaultZone: http://localhost:8761/eureka/
instance:
#eureka客户端向服务端发送心跳的时间间隔,单位为秒,默认是30
lease-renewal-interval-in-seconds: 1
#eureka服务端收到最后一次心跳等待的时间上限,单位为秒,默认是90,超时剔除
lease-expiration-duration-in-seconds: 2
PRODUCT-SERVER: #必须是provide的服务名
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule
其他策略
此次使用定义Feign接口,使用接口方式解决上述问题
https://cloud.spring.io/spring-cloud-static/Greenwich.SR1/single/spring-cloud.html#_spring_cloud_openfeign
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
dependencies>
/**
* @FeignClient:feign客户端,用来取代之前ribbon操作
* name表示调用服务名
*/
@FeignClient(name = "PRODUCT-SERVER")
public interface IProductFeignApi {
/**
* 访问的指定服务的具体接口
* 此处表示:访问product-server服务的 products/get/id 接口
*/
@GetMapping("/products/get/{id}")
Product get(@PathVariable Long id);
}
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class OrderMain8090 {
//注释掉ribbon调用方式
/*@LoadBalanced //ribbon给restTemplate开启负载均衡访问操作
@Bean
public RestTemplate template(){
return new RestTemplate();
}*/
public static void main(String[] args) {
SpringApplication.run(OrderMain8090.class, args);
}
}
@Service
public class OrderServiceImpl implements IOrderService {
//注释掉ribbon的调用方式
/*//因为要进行负载均衡调用,此时需要的是Eureka中 product-server 服务名
//因为Eureka中可以通过服务名获取到product-server所有服务集群的ip与端口
@Autowired
private RestTemplate template;
public static final String PRODUCT_URL = "http://PRODUCT-SERVER";*/
//改用feign接口方式
@Autowired
private IProductFeignApi productFeignApi;
@Override
public Order save(Long userId, Long productId) {
//http 请求方式获取 product-server 服务里面的商品信息
//Product product = template.getForObject(PRODUCT_URL + "/products/get/" + productId, Product.class); //真实远程获取
//Product product = null; //假装通过远程调用 product-server 获取
//feign 接口方式获取 product-server 服务里面的商品信息
Product product = productFeignApi.get(productId);
Order order = new Order();
order.setOrderNo(UUID.randomUUID().toString().replace("-",""));
order.setCreateTime(new Date());
order.setUserId(userId);
order.setProductName(product.getName());
order.setProductPrice(product.getPrice());
System.out.println("执行保存订单操作");
return order;
}
}
在 cloud-provider-product8080 建新类 ProductFeignClient
springcloud推崇的不适用controller进行对外提供接口服务,使用feign客户端方式
其实本质还是controller,换一种写法而已,注意要把之前的controller删除掉
@RestController
public class ProductFeignClient implements IProductFeignApi {
@Autowired
private IProductService productService;
@Value("${server.port}")
private String port;
@Override
public Product get(Long id) {
Product product = productService.get(id);
Product result = new Product();
BeanUtils.copyProperties(product,result);
result.setName(result.getName()+",data from "+port);
return result;
}
}
源码中默认options中配置的是6000毫秒,但是Feign默认加入了Hystrix,此时默认是1秒超时
我们可以通过修改配置,修改默认超时时间.
@Override
public Product get(Long id) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Product product = productService.get(id);
Product result = new Product();
BeanUtils.copyProperties(product,result);
result.setName(result.getName()+",data from "+port);
return result;
}
会报错,因为睡了 3 秒, java.net.SocketTimeoutException:Read timed out
# 设置超时时间
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
https://github.com/Netflix/ribbon/wiki/Getting-Started#the-properties-file-sample-clientproperties
默认重试 1 次,即会执行 2 次;默认超时时间 1 s
重试设置要谨慎配置:如果请求是添加操作,不能配置重试,除非方法中进行判断,以防出现重复结果
# Max number of retries on the same server (excluding the first try)
# Max number of next servers to retry (excluding the first server)
PRODUCT-SERVER:
ribbon:
#ConnectTimeout: 5000
#ReadTimeout: 5000
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
MaxAutoRetries: 0
MaxAutoRetriesNextServer: 1
抛弃非核心业务,尽量保障核心页面的正常运行 (例如:网络不好时视频清晰度越来越低)
服务器忙,请稍后再试, 不让客户端等待立刻返回一个友好的提示, fallback
引发服务降级:
1>程序运行异常
2>调用超时
3>服务熔断出发服务降级
4>线程池 / 信号量打满也会导致服务降级
类比保险丝超过最大功率后熔断一样, 服务器达到最大访问处理量之后,拒绝再接受服务,被拒绝的请求直接调用服务降级方法,返回友好提示
一般熔断操作过程:
正常服务访问 ----> 遭遇异常/超时等意外情况,服务降级 ---->
多次请求处理无果进而熔断 ----> 熔断时间到,尝试恢复调用链路
java 跟 javascript 关系【没多大关系】
服务雪崩
多个服务之间调用的时候,假设微服务A调用微服务B和微服务C, 微服务B和微服务C又调用了其他微服务,这就是所谓的 “扇出” , 如果扇出的链路上某个微服务的调用响应时间过长或者不可用, 对微服务A的调用就会占用越来越多的系统资源,进而引起系统的崩溃, 所谓的 “雪崩效应”
对于高流量的应用来说, 单一的后端依赖可能会导致所有服务器上的资源都在几秒钟内饱和,比失败更悲催的是, 这些应用程序还可能导致服务间的延迟增加, 备份队列,线程和其他系统资源紧张, 导致整个系统发生更多的 级联故障 , 这些都表示需要对故障和延迟进行隔离和管理, 以便单个依赖关系的失败, 不能取消整个应用程序或系统。
所以, 通常你发现一个模块下某个服务失败后, 这时候这个模块依赖接受流量, 然后这个有问题的模块还调用其他某款,这样就会发生级联故障, 或者叫雪崩
如果我们加入超时机制,例如 2s ,那么超过 2s 就会直接返回了,那么这样就在一定程度上可以抑制消费者资源耗尽的问题
通过线程池+队列的方式或者通过信号量的方式。比如商品评论比较慢,最大能同时处理10个线程,队列待处理5个,那么如果同时20个线程到达的话,其中就有5个线程被限流了,其中10个先被执行,另外5个在队列中
当依赖的服务有大量超时时,在让新的请求去访问根本没有意义,只会无畏的消耗现有资源,比如我们设置了超时时间为 1s ,如果短时间内有大量请求在 1s 内都得不到响应,就意味着这个服务出现了异常,此时就没有必要再让其他的请求去访问这个服务了,这个时候就应该使用熔断器避免资源浪费
有服务熔断,必然要有服务降级。
所谓降级,就是当某个服务熔断之后,服务将不再被调用,此时客户端可以自己准备一个本地的 fallback(回退)回调,返回一个缺省值
例如:(备用接口 / 缓存 / mock数据),这样做,虽然服务水平下降,但好歹可用,比直接挂掉要强,当然这也要看适合的业务场景
熔断机制
为应对雪崩效应的一种微服务链路保护机制,当扇出链路的某个微服务出错,不可用或者响应时间太长时,会进行服务降级,进而熔断节点微服务的调用,快速返回错误的响应信息
当检测到该节点微服务调用响应正常,恢复调用链路
在springcloud架构里, 熔断机制通过Hystrix实现, Hystrix会监控微服务调用状况,当失败的调用到一定阈值,缺省是5秒内20次调用失败,会启动熔断机制
hystrix对应的中文名字是“豪猪”
是一个用于处理分布式系统的延时和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时,异常等,Hystrix 能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,通过熔断隔离的方式,以提高分布式系统的弹性
https://github.com/Netflix/Hystrix
https://github.com/Netflix/Hystrix/wiki
在大中型分布式系统中,通常系统很多依赖(HTTP,hession,Netty,Dubbo等),在高并发访问下,这些依赖的稳定性与否对系统的影响非常大,但是依赖有很多不可控问题:如网络连接缓慢,资源繁忙,暂时不可用,服务脱机等
当依赖阻塞时,大多数服务器的线程池就出现阻塞(BLOCK),影响整个线上服务的稳定性,在复杂的分布式架构的应用程序有很多的依赖,都会不可避免地在某些时候失败。高并发的依赖失败时如果没有隔离措施,当前应用服务就有被拖垮的风险。
解决方案:对依赖做隔离
提供了熔断、隔离、Fallback、cache、监控等功能
断路器
本身是一种开关装置,当某个服务单元发生故障之后,通过断路器监控(类似熔断保险丝), 向调用方返回一个符合预期的,可处理的备选响应(FallBack),而不是长时间等待或者抛出调用方法无法处理异常,这样就保证服务调用方的线程不会被长时间、不必要的占用, 从而避免故障在分布式系统中蔓延,乃至雪崩
当断路器打开, 对主逻辑进行熔断后, hystrix 会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的成为主逻辑;
当休眠时间窗到期, 断路器将进入半开状态,释放一次请求到原来的主逻辑上;
如果此次请求正常返回,那么断路器将继续闭合, 主逻辑恢复;
如果这次请求依然有问题, 断路器继续进行打开状态, 休眠时间窗重新计时
大神论文:https://martinfowler.com/bliki/CircuitBreaker.html
只要有请求接收,都可以进行降级操作
https://cloud.spring.io/spring-cloud-static/Greenwich.SR1/single/spring-cloud.html#_circuit_breaker_hystrix_clients
@EnableCircuitBreaker
注解@HystrixCommand(fallbackMethod = "saveFail")
注解
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
@EnableCircuitBreaker //或者 @EnableHystrix
public class OrderMain8090 {
//......
}
@RestController
@RequestMapping("orders")
public class OrderController {
@Autowired
private IOrderService orderService;
//降级配置,一旦此方法异常/超时,就会执行fallbackMethod指定的降级方法
@HystrixCommand(fallbackMethod = "saveFallback")
@GetMapping("/save/{userId}/{productId}")
public Order save(@PathVariable Long userId,@PathVariable Long productId){
return orderService.save(userId,productId);
}
//降级方法
//注意:方法签名跟映射接口一样,仅仅是方法名不一样
public Order saveFallback(@PathVariable Long userId,@PathVariable Long productId){
System.out.println("走降级方法....");
return new Order() ;
}
}
修改 cloud-provider-product8080类 ProductFeignClient,模拟出现异常
多次请求观察打印信息
熔断后发短信或邮寄进行提示,提醒运维人员紧急系统维护
需求:异步短信提示频率20秒内
添加 redis 依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
配置文件
spring:
redis:
host: 127.0.0.1
修改 cloud-consumer-order8090 的接口 OrderController 中 saveFallback 降级方法
//降级方法
//注意:方法签名跟映射接口一样,仅仅是方法名不一样
public Order saveFallback(@PathVariable Long userId,@PathVariable Long productId){
System.out.println("走降级方法....");
//通知运维人员或者程序员赶紧过来修复
//发短信或发邮件
//redis限制频率
//启动一个线程,让线程执行发短信逻辑
new Thread(()->{
String redisKey = "order-save";
String value = stringRedisTemplate.opsForValue().get(redisKey);
if(StringUtils.isEmpty(value)){
System.out.println("order下订单服务失败,请查找原因.");
stringRedisTemplate.opsForValue().set(redisKey,"save-order-fail",20, TimeUnit.SECONDS);
}else{
System.out.println("已经发送过短信");
}
}).start();
return new Order();
}
因为 cloud-product-api 使用的是 Feign 的方式,在 cloud-product-api 里面如果出现异常/超时,也需要熔断,name就需要 Feign 集成 Hystrix
注意点:
1. 自定义类实现 IProductFeignApi 接口
2. 贴 @Component 交给spring容器管理
/**
* 实现 IProductFeignApi 接口目的是为了指定哪些方法需要进行降级处理
* 该类用于product-server服务对外提供接口进行降级保护
*/
@Component
public class ProductFeignHystrix implements IProductFeignApi {
//IProductFeignApi接口里面get方法的降级方法
@Override
public Product get(Long id) {
System.out.println("走降级方法了。。ProductFeignHystrix。。");
Product product = new Product();
product.setName("降级方法:默认对象");
return product;
}
}
/**
* @FeignClient:feign客户端,用来取代之前ribbon操作
* name表示调用服务名
* fallback表示降级方法所在类
*/
@FeignClient(name = "PRODUCT-SERVER", fallback = ProductFeignHystrix.class)
public interface IProductFeignApi {
/**
* 访问的指定服务的具体接口
* 此处表示:访问product-server服务的 products/get/id 接口
*/
@GetMapping("/products/get/{id}")
Product get(@PathVariable Long id);
}
默认是关闭的,需要手动开启一下
feign:
hystrix:
enabled: true
当 product-server 的 get 方法出现异常时,执行了降级方法
又因为此时 cloud-product-api 里面的降级方法返回了一个正常的 product 类对象,所以 consumer-server 里面没有异常,就不会执行此降级方法
现学组件有 ribbon feign hystrix 都有超时控制,该如何选择?
推荐方案:
1. feign 或者 hystrix > 正常调用业务耗时
2. hystrix > feign
因为 ribbon 和 feign 的超时设置会冲突
PRODUCT-SERVER: # ribbon
ribbon:
ConnectTimeout: 2000
ReadTimeout: 2000
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
MaxAutoRetries: 0
MaxAutoRetriesNextServer: 0
feign: # feign
hystrix:
enabled: true #Feign集成Hystrix
client:
config:
default:
connectTimeout: 4000
readTimeout: 4000
hystrix: # hystrix
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 5000
除了隔离依赖服务调用以为,Hystrix还提供了准实时的调用监控(Hystrix DashBoard) Hystrix会持续的几率所有通过Hystrix发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求,多少是成功的,多少是失败的等, Netflix通过Hystrix-metrics-event-stream项目实现对上面指标的监控。SpringCloud也提供了Hystrix Dashboard的整合,对监控内容转化成可视化界面
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboardartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
测试的时候,无法将 Dashboard 注入 Eureka 注册中心,会报错
registerWithEureka: false 但是不影响其他操作
server:
port: 8100
eureka:
instance:
hostname: localhost
client:
#是否将自己注册进去eureka,false为不注册,true注册
registerWithEureka: true
#是否从eureka抓取注册信息,单点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
#hystrix监控配置
management:
endpoints:
web:
exposure:
include: ["health","info","hystrix.stream"]
@SpringBootApplication
@EnableHystrixDashboard
public class DashBoard8100 {
//请求数据获取路径
@Bean
public ServletRegistrationBean servletRegistrationBean(){
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
public static void main(String[] args) {
SpringApplication.run(DashBoard8100.class, args);
}
}
运行启动类,打开浏览器输入 http://localhost:8100/hystrix
然后在页面中的地址栏输入框输入:需要监控服务的地址 http://localhost:启动端口/actuator/hystrix.stream
此次以cloud-consumer-order8090为例子
http://localhost:8090/actuator/hystrix.stream
Zuul 网关是系统的唯一对外的入口,介于客户端和服务器端之间的中间层,处理非业务功能 提供路由请求、鉴权、监控、缓存、限流等功能
Zuul包含了对请求的 路由 和 过滤 两个最主要的功能:
其中路由能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一的入口的基础,而过滤器功能则负责对请求的处理过程进行干预,是实现请求验证、服务聚合等功能的基础。Zuul和Eureka进行整合,将Zuul自身注册为Eureka服务治理下的应用,同时从Eureka中获取其他微服务的信息,也既以后的访问微服务都是通过Zuul跳转后获得
注意:Zuul服务最终还是会注册进Eureka
提供 = 代理 + 路由 + 过滤三大功能
zuul
Netflix开源的微服务网关,和Eureka,Ribbon,Hystrix等组件配合使用.
kong
由Mashape公司开源的,基于Nginx的API gateway
nginx+lua
是一个高性能的HTTP和反向代理服务器,lua是脚本语言,让Nginx执行Lua脚本,并且高并发、非阻塞的处理各种请求
GateWay
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-zuulartifactId>
dependency>
server:
port: 9000
spring:
application:
name: zuul-server
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
@SpringBootApplication
@EnableZuulProxy
public class ZuulMain9000 {
public static void main(String[] args) {
SpringApplication.run(ZuulMain9000.class, args);
}
}
http://localhost:8090/orders/save/1/1
http://localhost:9000/order-server/orders/save/1/1
可以自定义路由规则
可以通过网关实现网络隔离
因为前面要使用网关,就要在地址里面加入想要访问的 服务名称,较麻烦。自定义路由规则
server:
port: 9000
spring:
application:
name: zuul-server
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
zuul:
#忽略匹配,即: order-server这种格式请求路径忽略掉
# 访问 http://localhost:9000/order-server/orders/save/1/1 会报错404
ignoredPatterns: /*-server/**
routes:
#定制路由匹配规则
order-server-route:
#凡是请求路径中带有/order前缀的转发到order-server进行处理
#简单的理解:配置前:http://localhost:9000/order-server/orders/save/1/1
# 配置后:http://localhost:9000/order/orders/save/1/1
path: /order/**
serviceId: order-server
product-server-route:
path: /product/**
serviceId: product-server
默认情况,网关会把 “Cookie”、“Set-Cookie”、“Authorization” 这三个请求头过滤掉,下游的服务是获取不到这几个请求头的
如果不需要过滤这个请求头,可以修改过滤的集合的值
这个属性直接设置:sensitiveHeaders
server:
port: 9000
spring:
application:
name: zuul-server
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
zuul:
#配置为空,表示什么都不过滤
sensitiveHeaders:
#忽略匹配,既: order-server这种格式请求路径忽略掉
ignoredPatterns: /*-server/**
routes:
#定制路由匹配规则
order-server-route:
#凡是请求路径中带有/order前缀的转发到order-server进行处理
#简单的理解:配置前:http://localhost:9000/order-server/orders/get/1/1
# 配置后:http://localhost:9000/order/orders/get/1/1
path: /order/**
serviceId: order-server
product-server-route:
path: /product/**
serviceId: product-server
https://github.com/Netflix/zuul/wiki/How-it-Works
核心类:ZuulServlet 是一个servlet
Zuul 的多个功能 是在各种 filters 里面实现的
做登录判断,如果带了 token,表示登录了
没有token 表示没登录返回
在 cloud-gateway-zuul9000 项目里自定义类 AuthZuulFilter
注意,必须交给 Spring 管理
/**
* 鉴权过滤器
* 做登录判断,如果带了token,表示已登录,没有token表示没登录返回
*/
@Component
public class AuthZuulFilter extends ZuulFilter {
//过滤器类型:指定当前是前置过滤器
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
//过滤器序号(优先级):优先级越小,越先执行
@Override
public int filterOrder() {
return 1;
}
//是否执行鉴权过滤:true表示当前请求要执行鉴权操作,false表示当前请求不执行
// 指定穿过zuul网关的请求过滤规则,true表示当前请求满足拦截条件
@Override
public boolean shouldFilter() {
//做登录判断,如果带了token,表示已登录,没有token表示没登录返回
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
String token = request.getHeader("token");
return StringUtils.hasLength(token);
}
/**
* 鉴权操作逻辑
* 执行前提:shouldFilter()返回true时
* 表示实现过滤拦截逻辑
*/
@Override
public Object run() throws ZuulException {
RequestContext requestContext = RequestContext.getCurrentContext();
//有token,已登录的情况,需要鉴权
System.out.println("已登录,执行鉴权");
//不放行,不转发
requestContext.setSendZuulResponse(false);
//设置响应状态码401
requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
try {
//设置响应内容
requestContext.getResponse().getWriter().write("no unauthorized");
} catch (IOException e) {
e.printStackTrace();
}
//不携带数据
return null;
}
}
下面是存放在 cookie 的情况
@Component
public class AuthZuulFilter extends ZuulFilter {
//指定是前置过滤器
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
//定义过滤器优先级,越小越优先级越高
@Override
public int filterOrder() {
return 1;
}
//指定穿过zuul网关的请求过滤规则,true表示当前请求满足拦截条件
@Override
public boolean shouldFilter() {
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
if(request.getRequestURI().indexOf("/order/")>=0){
return true;
}
return false;
}
//当shouldFilter为true,执行该方法
//表示实现过滤拦截逻辑
@Override
public Object run() throws ZuulException {
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
String cookie = request.getHeader("Cookie");
if(StringUtils.isEmpty(cookie)){
cookie = request.getParameter("Cookie");
}
if(StringUtils.isEmpty(cookie)){
requestContext.setSendZuulResponse(false);
requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
try {
requestContext.getResponse().getWriter().write("no unauthorized");
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
在微服务框架中,一个由客户端发起的请求在后端系统中经过多个不同的服务节点调用,协同生产最后的请求结果,每一个前端请求会形成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引起整个请求最后的失败
那该如何解决呢?
sleuth 记录的是一次请求链路(请求经过哪些服务,哪些类)
一条链路有唯一标识(Trace ID), 每个经过一个链路(服务)使用Span来标识不同请求(记录请求相关信息), 各个span间使用 parent ID 关联
Sleuth是一个专门用于记录链路数据的开源组件
https://cloud.spring.io/spring-cloud-static/Greenwich.SR1/single/spring-cloud.html#sleuth-adding-project
springcloud 默认集成了 sleuth + zipkin, 导一个即可
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zipkinartifactId>
dependency>
贴上日志标签
@Slf4j //lombok.extern.slf4j.Slf4j
public class OrderController {
//降级配置,一旦此方法异常/超时,就会执行fallbackMethod指定的降级方法
@HystrixCommand(fallbackMethod = "saveFallback")
@GetMapping("/save/{userId}/{productId}")
public Order save(@PathVariable Long userId,@PathVariable Long productId){
log.info("OrderController..save..");
return orderService.save(userId,productId);
}
//......
}
@Slf4j
public class OrderServiceImpl implements IOrderService {
@Override
public Order save(Long userId, Long productId) {
log.info("OrderController..save..");
//......
}}
springcloud 默认集成了 sleuth + zipkin, 导一个即可
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zipkinartifactId>
dependency>
//贴上日志标签
@Slf4j
public class ProductController {
//记录调用日志
log.info("ProductController.get....");
//贴上日志标签
@Slf4j
public class ProductServiceImpl implements IProductService {
//记录调用日志
log.info("ProductController.get....");
日志格式:
[order-server,c323c72e7009c077,fba72d9c65745e60,false]
order-server
,spring.application.name的值c323c72e7009c077
,sleuth生成的Trace ID,用来标识请求链路,一条请求链路中包含一个Trace ID,多个Span IDfba72d9c65745e60
,spanID 基本的工作单元,获取元数据,如发送一个httpfalse
,是否要将该信息输出到 zipkin 服务中来收集和展示zipkin是Twitter基于google的分布式监控系统Dapper(论文)的开发源实现,zipkin用于跟踪分布式服务之间的应用数据链路,分析处理延时,帮助我们改进系统的性能和定位故障。
官网:https://zipkin.io/
相关阅读:https://www.zhihu.com/question/27994350
https://cloud.spring.io/spring-cloud-static/Greenwich.SR1/single/spring-cloud.html#_sleuth_with_zipkin_via_http
启动zipkin服务,我们直接使用的是jar的方式
需要在product-server和order-server中的配置文件中添加zipkin地址
spring:
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
probability: 1
zipkin 是一个java客户端工具,直接使用 java -jar 方式启动即可
spring:
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
#采用率,介于0到1之间, 1表示全部收集
probability: 1
spring:
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
#采用率,介于0到1之间, 1表示全部收集
probability: 1
访问 http://localhost:9411
统一管理配置,快速切换各个环境的配置
在微服务体系中,服务的数量以及配置信息的日益增多,比如各种服务器参数配置、各种数据库访问参数配置、各种环境下配置信息的不同、配置信息修改之后实时生效等等,传统的配置文件方式或者将配置信息存放于数据库中的方式已无法满足开发人员对配置管理的要求,如:
所以,一套集中式的,动态的配置管理设施是必不可少的
Springcloud 提供了 config 组件来解决这种问题
是什么
SpringCloud config 为微服务架构中的微服务提供 集中化的外部配置 支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置
怎么玩
SpringCloud config 分服务端和客户端两部分
就是先在 码云 上新建仓库并初始化好
这里的 git 服务器可以是:gitlab、github、码云等
https://cloud.spring.io/spring-cloud-static/Greenwich.SR1/single/spring-cloud.html#_spring_cloud_config_server
去码云中注册账号,创建自己的项目
pom.xml
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-config-serverartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
application.yml
server:
port: 7000
spring:
application:
name: config-server
cloud:
config:
server:
git:
#在gitee中新建的仓库路径,注意是地址栏上的
uri: https://gitee.com/xxx/cloud-config
#码云账号
username: [email protected]
#码云账号秘密
password: dafei666
label: master
eureka:
client:
#是否将自己注册进去eureka,false为不注册,true注册
registerWithEureka: true
#是否从eureka抓取注册信息,单点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
serviceUrl:
defaultZone: http://localhost:8761/eureka/
启动类-ConfigMain7000
@SpringBootApplication
@EnableConfigServer
public class ConfigMain7000 {
public static void main(String[] args) {
SpringApplication.run(ConfigMain7000.class, args);
}
}
在码云master分支上新建配置文件order-dev.yml
server:
port: 8888
在码云dafei分支上新建配置文件order-dev.yml
server:
port: 9999
启动服务-ConfigMain7000
分别访问地址
/{application}/{profile}/{label}
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml # 上面使用的是这种规则,建议使用这种
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties
application: 配置文件名,一般是服务名
profile: 环境 dev test prod等
label:git 分支
https://cloud.spring.io/spring-cloud-static/Greenwich.SR1/single/spring-cloud.html#_spring_cloud_config_client
修改pom.xml
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-config-clientartifactId>
dependency>
bootstrap.yml
spring:
application:
name: order-server
cloud:
config:
label: master #分支名称
name: order #配置文件名称
profile: dev #读取后缀名称:
uri: http://localhost:7000
#上述4个综合: http://localhost:7000/master/order-dev.yml
eureka:
client:
#是否将自己注册进去eureka,false为不注册,true注册
registerWithEureka: true
#是否从eureka抓取注册信息,单点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
serviceUrl:
defaultZone: http://localhost:8761/eureka/
boostrap 与 application 区别:
bootstrap.yml 没配置那些, order-dev.yml必须配置
一般来说, bootstrap.yml 配置那些不经常改动的, 码云 order-dev.yml 配置经常改动的
server:
port: 8090
spring:
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
#采用率,介于0到1之间, 1表示全部收集
probability: 1
PRODUCT-SERVER:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
MaxAutoRetries: 0
MaxAutoRetriesNextServer: 0
feign:
hystrix:
enabled: true
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
启动 eureka 服务器
启动 config 服务器
启动 order 服务器
观察 order 端口是不是8090(从码云上加载而来)