赞
踩
微服务架构的提出者:马丁福勒
https://martinfowler.com/articles/microservices.html
// 微服务架构的提出者:马丁福勒(中午网)
http://blog.cuicc.com/blog/2015/07/22/microservices/
马丁.福勒对微服务大概的概述如下:
就目前而言,对于微服务业界并没有一个统一的、标准的定义 (While there is no precise definition of this architectural style ) 。
但通在其常而言,微服务架构是一种架构模式或者说是一种架构风格,它提倡将单一应用程序划分成一组小的服务,每个服务运行独立的自己的进程中,服务之间互相协调、互相配合,为用户提供最终价值。服务之间采用轻量级的通信机制互相沟通(通常是基于 HTTP 的 RESTful API ) 。每个服务都围绕着具体业务进行构建,并且能够被独立地部署到生产环境、类生产环境等。
另外,应尽量避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具对其进行构建,可以有一个非常轻量级的集中式管理来协调这些服务。可以使用不同的语言来编写服务,也可以使用不同的数据存储。
简而言之,微服务架构样式[1]是一种将单个应用程序开发为一组小服务的方法,每个小服务都在自己的进程中运行并与轻量级机制(通常是HTTP资源API)进行通信。这些服务围绕业务功能构建,并且可以由全自动部署机制独立部署。这些服务的集中管理几乎没有,它可以用不同的编程语言编写并使用不同的数据存储技术。
1、 微服务架构只是一个样式,一个风格。
2、 将一个完成的项目,拆分成多个模块去分别开发。
3、 每一个模块都是单独的运行在自己的容器中。
4、 每一个模块都是需要相互通讯的。 Http,RPC,MQ。
5、 每一个模块之间是没有依赖关系的,单独的部署。
6、 可以使用多种语言去开发不同的模块。
7、每个模块都是一个独立的进程
8、中心化的管理
9、 使用MySQL数据库,Redis,ES去存储数据,也可以使用多个MySQL数据库。
总结:将复杂臃肿的单体应用进行细粒度的划分,每个拆分出来的服务各自打包部署。
dubbo和springcloud都是解决微服务的调用
使用springcloud的时候要注意和springboot版本匹配,官方有推荐的版本
springcloud中五大核心组件
注册中心
服务调用
路由网关
服务的降级
配置文件中心
Eureka 是 Netflix 出品的用于实现服务注册和发现的工具。 Spring Cloud 集成了 Eureka,并提供了开箱即用的支持。其中, Eureka 又可细分为 Eureka Server 和 Eureka Client。
Eureka是SpringCloud中的注册中心,在Eureka管理了所有的微服务,服务的提供者和消费者都要在注册中心进行注册,当有消费者需要调用服务的时候,直接去注册中心进行拉取提供者的地址和端口号,以及服务名称等消息,然后消费者根据消息内容使用RestTemplate进行请求到对应的提供者,当下次在进项该调用时候,服务器会将上次拉取的消息缓存在服务器内,所以说,当第二次请求的时候,就算注册中心宕机也还是能正常访问的。
提供者:要访问的目标服务器,提供数据
消费者:请求访问的服务器,获取数据
1、Eureka服务正常启动,如果存在集群的话就要互相同步
2、Eureka客户端启动的时候,会根据配置的地址,将该服务注册到Eureka服务中,
3、Eureka客户端会每隔30s发送一个心跳给Eureka服务
4、Eureka服务在90s之内没有收到Eureka客户端的心跳,会认为客户端出现故障,然后从服务列表中移除,
5、在一段时间内,Eureka服务端统计到有大量的(85%)Eureka客户端没有发送心跳Eureka服务会认为此时,自己出现了网络故障,就会触发自我保护机制,不会再移除eureka客户端。当前不会把数据同步给其他的Eureka服务,但是对外还是提供服务的
6、如果网络恢复正常,自我保护机制关闭,接着将数据同步到其他的Eureka服务器
7、Eureka客户端要调用其他服务,需要先到Eureka服务器中拉取其他服务的信息,然后再缓存到本地,再根据客户端的负载均衡策略进行负载均衡
8、Eureka客户端会在一段时间内从Eureka服务端拉取最新的数据,更新本地的缓存数据。
9、Eureka客户端关闭后,Eureka就不会再发送心跳,Eureka服务就从自己的列表中移除
上图是基于集群配置的eureka;
- 处于不同节点的eureka通过Replicate进行数据同步
- Application Service为服务提供者
- Application Client为服务消费者
- Make Remote Call完成一次服务调用
1、创建一个springBoot工程,创建一个父工程,并且在父工程中指定SpringCloud的版本,并且将packaing修改为pom,选择EurekaServer的依赖,继承springboot的依赖,继承springcloud的依赖
<properties> <java.version>1.8</java.version> <spring-cloud.version>Hoxton.SR7</spring-cloud.version> </properties> <dependencies> <!-- eureka服务端的依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <dependencyManagement> <!--springBoot的核心依赖,也是继承,可插拔机制,只会加载配置的依赖--> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
2、application.yml文件的配置
server:
port: 7777 #表示该服务的端口
eureka:
client:
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka # 需要进行消息注册的请求地址
register-with-eureka: false #表示当前服务不会注册到服务中心
fetch-registry: false #表示当前服务不会拉取其他服务
instance:
hostname: localhost
3、启动类添加注解@EnableEurekaServer 表示该服务器为一个消息注册中心的服务端
@SpringBootApplication
@EnableEurekaServer//配置后代表是一个eureka的客户端
public class Day0101SpringcloudEurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(Day0101SpringcloudEurekaServerApplication.class, args);
}
}
所有相对于EurekaServer他们都是客户端,也就是说提供者和消费者都是注册中心的客户端
服务器的搭建即将一个服务注册到注册中心
1、创建一个springBoot项目,创建的时候勾选Eureka Discovery Client的依赖
<dependencies> <!--eureka客户端的依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <!--springCloud的核心依赖--> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
2、编写配置文件application.yml ,注意,注册的名称不要使用"_"
server:
port: 8081 # 该服务的端口号
eureka:
client:
service-url:
defaultZone: http://${eureka.instance.hostname}:7777/eureka #此服务注册的地址
instance:
hostname: localhost
spring:
application:
name: 2002-eureka-client-provider #注册到注册中心的名称 注:不要使用“_”
3、启动类添加注解@EnableEurekaClient 表示该服务器为一个消息注册中心的客户端
@SpringBootApplication
@EnableEurekaClient//表示该服务器是是一个Eureka的客户端
@ComponentScan("com.jn")
public class Day0102SpringcloudEurekaClientProviderApplication {
public static void main(String[] args) {
SpringApplication.run(Day0102SpringcloudEurekaClientProviderApplication.class, args);
}
}
//操作eurekClient的API,初始化容器后已经存在 @Autowired private EurekaClient eurekaClient; /** * 从eureka服务器中拉取指定名称提供者的信息 */ @Test void contextLoads() { //从eureka服务器中拉取提供者的信息 第二个参数false InstanceInfo info = eurekaClient.getNextServerFromEureka("2002-EUREKA-CLIENT-CONSUMER", false); //获取提供者的端口号 int port = info.getPort(); System.out.println("port:"+port);//port:8082 //获取IP地址 String hostName = info.getHostName(); System.out.println("hostName:"+hostName);//hostName:localhost //获取提供者的地址 String homePageUrl = info.getHomePageUrl(); System.out.println("homePageUrl:"+homePageUrl);//homePageUrl:http://localhost:8082/ }
实现Eureka认证
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
编写配置类,要让spring扫描到这个类
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 忽略掉/eureka/**
http.csrf().ignoringAntMatchers("/eureka/**");
super.configure(http);
}
}
编写主启动类,@EnableWebSecurity // 开启EnableWebSecurity组件
@SpringBootApplication
@EnableEurekaServer//配置后代表是一个eureka的服务端
@EnableWebSecurity // 开启EnableWebSecurity组件
@ComponentScan("com.jn")
public class Day0101SpringcloudEurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(Day0101SpringcloudEurekaServerApplication.class, args);
}
}
编写配置文件
# 指定用户名和密码
spring:
security:
user:
name: root
password: root
其他服务想注册到Eureka上需要添加用户名和密码进行认证
eureka:
client:
service-url:
defaultZone: http://用户名:密码@localhost:8761/eureka
如果程序的正在运行,突然Eureka宕机了。
如果调用方访问过一次被调用方了,Eureka的宕机不会影响到功能。
如果调用方没有访问过被调用方,Eureka的宕机就会造成当前功能不可用。
这里将eureka集群3台为例,将单机版的eureka拷贝3份后,配置分别如下
如果程序的正在运行,突然Eureka宕机了。
如果调用方访问过一次被调用方了,Eureka的宕机不会影响到功能。
如果调用方没有访问过被调用方,Eureka的宕机就会造成当前功能不可用。
这里将eureka集群3台为例,将单机版的eureka拷贝3份后,配置分别如下
eureka8001
server:
port: 8001
spring:
application:
name: eureka-server # 服务器域名
eureka:
client:
fetch-registry: false
register-with-eureka: false
service-url:
#集群的情况下,服务端之间要互相注册,指向对方,多个地址用逗号隔开
defaultZone: http://eureka8002.com:8002/eureka,http://eureka8003.com:8003/eureka
instance:
instance-id: eureka8001.com
eureka8002
server:
port: 8002
spring:
application:
name: eureka-server2
eureka:
client:
fetch-registry: false
register-with-eureka: false
service-url:
defaultZone: http://eureka8001.com:8001/eureka,http://eureka8003.com:8003/eureka
instance:
instance-id: eureka8002.com
eureka8003
server:
port: 8003
spring:
application:
name: eureka-server3
eureka:
client:
fetch-registry: false
register-with-eureka: false
service-url:
defaultZone: http://eureka8001.com:8001/eureka,http://eureka8002.com:8002/eureka
instance:
instance-id: eureka8003.com
域名映射
集群后三台Eureka服务的IP都不一样,所以为了方便在本地测试,可以在hosts文件中做域名映射,把三台服务的ip 都映射成127.0.0.1。hosts文件路劲如下:
C:\Windows\System32\drivers\etc
127.0.0.1 eureka8001.com
127.0.0.1 eureka8002.com
127.0.0.1 eureka8003.com
发布的每一个服务都应该注册到所有的Eureka中,所以每一个服务中都要写三个Eureka服务地址。
server:
port: 8082
spring:
application:
name: provider-server
eureka:
client:
service-url:
# 这里写三台Eureka地址
defaultZone: http://eureka8001.com:8001/eureka,http://eureka8002.com:8002/eureka,http://eureka8003.com:8003/eureka
EurekaClient启动是,将自己的信息注册到EurekaServer上,EurekaSever就会存储上EurekaClient的注册信息。
当EurekaClient调用服务时,本地没有注册信息的缓存时,去EurekaServer中去获取注册信息。
EurekaClient会通过心跳的方式去和EurekaServer进行连接。(默认30sEurekaClient会发送一次心跳请求,如果超过了90s还没有发送心跳信息的话,EurekaServer就认为你宕机了,将当前EurekaClient从注册表中移除)
eureka:
instance:
lease-renewal-interval-in-seconds: 30 #心跳的间隔
lease-expiration-duration-in-seconds: 90 # 多久没发送,就认为你宕机了
EurekaClient会每隔30s去EurekaServer中去更新本地的注册表
eureka:
client:
registry-fetch-interval-seconds: 30 # 每隔多久去更新一下本地的注册表缓存信息
Eureka的自我保护机制,统计15分钟内,如果一个服务的心跳发送比例低于85%,EurekaServer就会开启自我保护机制
eureka:
server:
enable-self-preservation: true # 开启自我保护机制
CAP定理,C - 一致性,A-可用性,P-分区容错性,这三个特性在分布式环境下,只能满足2个,而且分区容错性在分布式环境下,是必须要满足的,只能在AC之间进行权衡。
如果选择CP,保证了一致性,可能会造成你系统在一定时间内是不可用的,如果你同步数据的时间比较长,造成的损失大。
Eureka就是一个AP的效果,高可用的集群,Eureka集群是无中心,Eureka即便宕机几个也不会影响系统的使用,不需要重新的去推举一个master,也会导致一定时间内数据是不一致。
Eureka关闭自我保护机制,默认是开启的
eureka:
server:
enbale-self-preservation: false
根据注册到注册中心的服务名调用,底层还是基于RestTemplate实现
1、使用的时候需要引入依赖
<!--ribbon通讯的核心依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
2、启动类添加开始负载均衡注解
@SpringBootApplication
@EnableEurekaClient
@ComponentScan("com.jn")
public class Day0110SpringcloudRibbonConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(Day0110SpringcloudRibbonConsumerApplication.class, args);
}
@LoadBalanced//表示是负载均衡
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
请求方式
@RequestMapping("/test")
public String testRibbon(){
// ribbon调用的方式是根据服务名称调用 RIBBON-PROVIDER表示是发布者在注册中心的名称
return restTemplate.getForObject("http://RIBBON-PROVIDER/provider/hello?msg={0}",String.class,"ribbon");
}
负载均衡策略
Ribbon支持的负载均衡策略
RandomRule 随机策略 随机选择server
RoundRobinRule 轮询策略 按照顺序选择server(ribbon默认策略)
RetryRule 重试策略 在一个配置时间段内,当选择server不成功,则一直尝试选择一个可用的server
BestAvailableRule 最低并发策略 逐个考察server,如果server断路器打开,则忽略,再选择其中并发链接最低的server
AvailabilityFilteringRule 可用过滤策略 过滤掉一直失败并被标记为circuit tripped的server,过滤掉那些高并发链接的server(active connections超过配置的阈值)
ResponseTimeWeightedRule 响应时间加权重策略 根据server的响应时间分配权重,响应时间越长,权重越低,被选择到的概率也就越低。响应时间越短,权重越高,被选中的概率越高,这个策略很贴切,综合了各种因素,比如:网络,磁盘,io等,都直接影响响应时间
ZoneAvoidanceRule 区域权重策略 综合判断server所在区域的性能,和server的可用性,轮询选择server并且判断一个AWS Zone的运行性能是否可用,剔除不可用的Zone中的所有server
使用指定类型的负载均衡,将其注入容器内
@Bean
public IRule robbinRule(){
return new RandomRule();
}
1、自定义一个继承AbstractLoadBalancerRule的类,并且交给spring容器管理,内部编写负载均衡策略
import com.netflix.client.config.IClientConfig; import com.netflix.loadbalancer.AbstractLoadBalancerRule; import com.netflix.loadbalancer.ILoadBalancer; import com.netflix.loadbalancer.RoundRobinRule; import com.netflix.loadbalancer.Server; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; /** * 自定义负载均衡策略,每三次切换一次 */ public class RibbonMyRule extends AbstractLoadBalancerRule { private AtomicInteger nextServerCyclicCounter; private static final boolean AVAILABLE_ONLY_SERVERS = true; private static final boolean ALL_SERVERS = false; private int total =0; // 记录调用的次数 private int index = 0; // 记录调用哪个服务 private static Logger log = LoggerFactory.getLogger(RoundRobinRule.class); public RibbonMyRule() { nextServerCyclicCounter = new AtomicInteger(0); } public RibbonMyRule(ILoadBalancer lb) { this(); setLoadBalancer(lb); } public Server choose(ILoadBalancer lb, Object key) { if (lb == null) { log.warn("no load balancer"); return null; } Server server = null; int count = 0; while (server == null && count++ < 10) { List<Server> reachableServers = lb.getReachableServers(); List<Server> allServers = lb.getAllServers(); int upCount = reachableServers.size(); int serverCount = allServers.size(); if ((upCount == 0) || (serverCount == 0)) { log.warn("No up servers available from load balancer: " + lb); return null; } // int nextServerIndex = incrementAndGetModulo(serverCount); // server = allServers.get(nextServerIndex); if(total <=2){ server = allServers.get(index); // 0 0 0 1 1 total++; // 1 2 3 0 1 }else{ index++; total = 0; if(index >= serverCount){ // 索引是从0开始的,长度是2 index = 0; } } if (server == null) { /* Transient. */ Thread.yield(); continue; } if (server.isAlive() && (server.isReadyToServe())) { return (server); } // Next. server = null; } if (count >= 10) { log.warn("No available alive servers after 10 tries from load balancer: " + lb); } return server; } /** * Inspired by the implementation of {@link AtomicInteger#incrementAndGet()}. * * @param modulo The modulo to bound the value of the counter. * @return The next value. */ private int incrementAndGetModulo(int modulo) { for (;;) { int current = nextServerCyclicCounter.get(); int next = (current + 1) % modulo; if (nextServerCyclicCounter.compareAndSet(current, next)) return next; } } @Override public Server choose(Object key) { return choose(getLoadBalancer(), key); } @Override public void initWithNiwsConfig(IClientConfig clientConfig) { } }
在主启动类自定义负载均衡
/**
* 设置使用负载均衡的策略
* @return
*/
@Bean
public IRule rule(){
return new RibbonMyRule();//表示使用自定义的负载均衡策略
}
Feign 是一个声明web服务客户端,这便得编写web服务客户端更容易,使用Feign 创建一个接口并对它进行注解,它具有可插拔的注解支持包括Feign注解与JAX-RS注解,Feign还支持可插拔的编码器与解码器,Spring Cloud 增加了对 Spring MVC的注解,Spring Web 默认使用了HttpMessageConverters, Spring Cloud 集成 Ribbon 和 Eureka 提供的负载均衡的HTTP客户端 Feign.
提供者的对外暴露接口
package com.jn.controller; import com.jn.entity.User; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; /** * @param * @return */ @RestController @RequestMapping("/provider") public class HelloController { @Value("${server.port}") private Integer port; @RequestMapping("/hello") public String testHello(String msg){ System.out.println( "访问的端口号为+"+port+",传递的参数为"+msg); return "访问的端口号为+"+port+",传递的参数为"+msg; } @RequestMapping("/test1") public String test1(){ return "hello test1"+port; } //方法接收多个参数 @RequestMapping("/login") public String login(String username,String password){ return "login:username="+username+",password="+password; } //方法接收对象 @RequestMapping("/addUserFrom") public String addUserFrom(User user){ return "addUserFrom"+user.toString(); } //方法接收JSON数据 @RequestMapping("/addUserJson") public User addUserJson(@RequestBody User user){ return user; } //restful风格 @RequestMapping("/getUserById/{id}") public String getUserByID(@PathVariable Integer id){ return "getUserById:id="+id; } }
因为feign底层是使用了ribbon作为负载均衡的客户端,而ribbon的负载均衡也是依赖于eureka 获得各个服务的地址,所以要引入eureka-client
<!--eureka的核心依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--feign的核心依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
调用服务编写映射接口 并且添加@FeignClient注解,表示为映射接口,一级路径可以在类上使用@RequestMapping注解,也可以在方法直接写全路径
/** * 这个接口映射调用的模块 * 使用@FeignClient会自动去eureka拉取该服务的信息 */ @FeignClient("RIBBON-PROVIDER") @RequestMapping(value = "/provider") public interface IProviderClient { @RequestMapping("/test1") String test1(); // // //方法接收多个参数 @RequestMapping("/login") String login(@RequestParam("username") String username, @RequestParam("password") String password); // // //方法接收JSON数据 响应如果是json格式数据,会自动根据返回值进行封装,如果该方法返回值是String类型,将会返回json格式的数据 @RequestMapping("/addUserJson") User addUserJson(@RequestBody User user); // // //restful风格 @RequestMapping("/getUserById/{id}") String getUserByID(@PathVariable("id") Integer id); }
测试,直接调用接口即可
@SpringBootTest class Day0111SpringcloudFeignConsumerApplicationTests { @Autowired IProviderClient client; @Test//测试传递字符串格式 public void testLogin(){ String result = client.login("kobe", "123"); System.out.println("result:"+result); } @Test//测试传递json数据(常用) public void addUserJson(){ User user = new User(); user.setId(10); user.setUsername("admin"); user.setPassword("123"); User returnUser = client.addUserJson(user); System.out.println(returnUser); } @Test//测试restful请求方式 void testGetUserById() { String userById = client.getUserByID(100); System.out.println(userById); } }
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其他的微服务,
这就是所谓的"扇出”、如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”。
对于高流量的应用来说,单- -的后端依赖可能会导致所有服务器上的所有资源都在几秒中内饱和。比失败更糟的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障,这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。
我们需要:弃车保帅:
Hystrix是一个用于处理分布式系统的延迟和容错的开源库, 在分布式系统里,许多依赖不可避免的会调用 失败,比如超时,异常等,Hystrix能够保证在一 个依赖出问题的情况下, 不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
服务的调用者(消费者)会从Hystrix线程池中申请一个线程帮助自己访问服务,服务调用者的线程会阻塞住,等着线程池中的线程反馈结果,当服务器宕机,线程池的线程效应超时时,该线程池的线程就被全部被申请完,此时就拒绝访问该服务器
每个前线程去访问服务会有标记,这个服务每访问一次会加1,服务访问成功会减一,如果这个值超过10就不让访问这个服务,请求就被拦截下来。
当需要调用的微服务出现问题时,默认会去调用一个本地降级方法,降级方法会返回错误信息或者一个合理的默认值,从而继续后面的业务,线程不会阻塞在哪里。
在微服务的访问过程中,如果大量的请求访问超时或者失败,则熔断器就会自动打开,如果熔断器打开之后,后续所有访问这个微服务的请求,会立刻被通知失败。熔断器开启后,会每隔一段时间进入半开状态,半开状态下,会释放一个服务尝试请求资源,如果请求成功,则熔断器就会 关闭,反之又会回到半开的状态。
1、导入hystrix核心依赖,还要导入eureka、ribbon的依赖
<!--hystrix核心依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
2、编写本地降级方法,接口使用@HystrixComman并指定对应方法
@RestController public class HystrixTestController { @Autowired RestTemplate restTemplate; @HystrixCommand(fallbackMethod = "helloHystrix")//表示服务降级调用本地的helloHystrix方法 @RequestMapping("/hello") public String hello(){ String forObject = restTemplate.getForObject("http://RIBBON-PROVIDER/provider/hello?msg=123", String.class); return forObject; } //本地降级方法 public String helloHystrix(){ return "【hystrix】:调用的服务器异常......"; } }
3、主启动类添加@EnableHystrix开启服务降级注解
@SpringBootApplication
@ComponentScan("com.jn")
@EnableEurekaClient
@EnableHystrix//表示开启服务降级功能
public class Day0213SpringcloudHystrixRibbonApplication {
public static void main(String[] args) {
SpringApplication.run(Day0213SpringcloudHystrixRibbonApplication.class, args);
}
@Bean
@LoadBalanced//开启负载均衡
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
4、将访问资源服务器关闭后,访问的结果
1、在配置文件中开启hystrix
#开启hystrix
feign:
hystrix:
enabled: true
2、提供一个映射接口的实现类。并且交给spring容器管理
@Component public class ProviderClientImpl implements IProviderClient { @Override public String testHello(String msg) { return "服务器出现异常,msg="+msg; } @Override public String test1() { return "请求服务器出现异常"; } @Override public String login(String username, String password) { return null; } @Override public User addUserJson(User user) { return null; } @Override public String getUserByID(Integer id) { return null; } }
3、在映射接口IproviderClient的@FeignClient注解中指定降级调用方法,
细节:开启服务降级后,Controller上不能再使用@RequestMapping指定一级路径,而应该在方法上指定
/** * 这个接口映射调用的模块 * 使用@FeignClient会自动去eureka拉取该服务的信息 */ @FeignClient(value = "RIBBON-PROVIDER", fallback = ProviderClientImpl.class ) //@RequestMapping(value = "/provider")//开启了hystrix后这个路径要写到下面的方法中 public interface IProviderClient { @RequestMapping("/provider/hello") String testHello(String msg); @RequestMapping("/provider/test1") String test1(); // // //方法接收多个参数 @RequestMapping("/provider/login") String login(@RequestParam("username") String username, @RequestParam("password") String password); // // //方法接收JSON数据 响应如果是json格式数据,会自动根据返回值进行封装,如果该方法返回值是String类型,将会返回json格式的数据 @RequestMapping("/provider/addUserJson") User addUserJson(@RequestBody User user); // // //restful风格 @RequestMapping("/provider/getUserById/{id}") String getUserByID(@PathVariable("id") Integer id); }
调用方无法知道具体的错误信息是什么,我们可以使用FallBackFactory方式
1、通过FallBackFactory的方式去实现这个功能,FallBackFactory基于Fallback,创建一个POJO类,实现FallBackFactory,在服务请求超时时,会调用该类的creat方法,其中cause就是异常信息
细节:要交给spring容器管理,
返回值一定要是映射接口的实现类
/** * 服务降级调用降级方法前如果是服务器出现异常会调用该类的create方法,返回值为映射接口, * @param * @return */ @Component public class ProviderClientFactory implements FallbackFactory<IProviderClient> { @Autowired ProviderClientImpl client; //注:这里一定要指定的是映射接口实现类 @Override public IProviderClient create(Throwable cause) { System.out.println("调用的服务器出现异常......"); cause.printStackTrace(); return client; } }
2、修改Client接口中的属性,指定该POJO类
@FeignClient(value = "RIBBON-PROVIDER",
// fallback = ProviderClientImpl.class
fallbackFactory = ProviderClientFactory.class
)
在配置文件中添加配置即可
hystrix:
command:
default:
circuitBreaker:
enabled: true
requestVolumeThreshold: 2
sleepWindowInMilliseconds: 10000
导入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
在启动类中添加注解
@EnableHystrixDashboard
配置一个Servlet路径,指定上Hystrix的Servlet
@WebServlet("/hystrix.stream")
public class HystrixServlet extends HystrixMetricsStreamServlet {
}
//------------------------------------------------------------
// 在启动类上,添加扫描Servlet的注解
@ServletComponentScan("com.jn.servlet")
高版本的SpringCloud中需要添加
hystrix:
dashboard:
proxy-stream-allow-list: "*"
测试直接访问http://host:port/hystrix
在当前位置输入映射好的servlet路径
网关是系统的唯一对外的入口,介于客户端和服务器端之间的中间层,处理非业务功能 提供路由请求、鉴权、监控、缓存、限流等功能。它将"1对N"问题转换成了"1对1”问题。
通过服务路由的功能,可以在对外提供服务时,只暴露网关中配置的调用地址,而调用方就不需要了解后端具体的微服务主机。
路由网关优点
微服务网关介于服务端与客户端的中间层,所有外部服务请求都会先经过微服务网关,客户只能跟微服务网关进行交互,无需调用特定微服务接口,使得开发得到简化。
服务网关 = 路由转发 + 过滤器+负载均衡
(1)路由转发:接收一切外界请求,转发到后端的微服务上去。
(2)过滤器:在服务网关中可以完成一系列的横切功能,例如权限校验、限流以及监控等,这些都可以通过过滤器完成。
(3) 调用者只需要维护网关的地址,对外只暴露网关的地址,其他地址用服务名称映射
(4)可以支持负载均衡
(5)里面也是支持服务降级
1、导入依赖
<!--注册中心客户端依赖-->
<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-zuul</artifactId>
</dependency>
2、主启动类添加@EnableZuulProxy注解,开启路由网关
@SpringBootApplication
@EnableZuulProxy//表示开启路由网关
@EnableEurekaClient//表示是Eureka注册中心的客户端
public class Day0216SpringcloudZuulApplication {
public static void main(String[] args) {
SpringApplication.run(Day0216SpringcloudZuulApplication.class, args);
}
}
3、编写配置文件
server:
port: 80
eureka:
client:
service-url:
defaultZone: http://localhost:7777/eureka #服务注册地址
spring:
application:
name: zuul
4、测试
http://网关IP:网关端口/服务名称/服务地址
注:注册名一定要转为小写
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
编写配置文件
# 查看zuul的监控界面(开发时,配置为*,上线,不要配置)
management:
endpoints:
web:
exposure:
include: "*"
直接访问地址http://localhost/actuator/routes
# zuul的配置
zuul:
# 基于服务名忽略服务,无法查看 ,如果要忽略全部的服务 "*",默认配置的全部路径都会被忽略掉(自定义服务的配置,无法忽略的)
ignored-services: eureka
# 监控界面依然可以查看,在访问的时候,404
ignored-patterns: /**/search/**
配置文件,两种方式
# zuul的配置
zuul:
# 指定自定义服务(方式一 , key(服务名):value(路径))
# routes:
# feign-hystrix-dashboad: /dashboad/** # kye:服务名称,value:映射路径
# 指定自定义服务(方式二)
routes:
kehu: # 自定义名称
path: /ccc/** # 映射的路径
serviceId: customer # 服务名称
灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。
灰度发布可以根据规定服务名称,生成固定的服务映射地址,
如服务名称server-v1可以映射为v1/server
使用步骤
1、在zuul服务器中添加一个配置类
@Bean
public PatternServiceRouteMapper serviceRouteMapper() {
return new PatternServiceRouteMapper(
"(?<name>^.+)-(?<version>v.+$)",
"${version}/${name}");
}
2、准备一个服务,提供两个版本
#v1版本
version: v1
server:
port: 8088
spring:
application:
name: ribbon-provider-${version}
eureka:
client:
service-url:
defaultZone: http://localhost:7777/eureka
# v2版本
version: v2
server:
port: 8088
spring:
application:
name: ribbon-provider-${version}
eureka:
client:
service-url:
defaultZone: http://localhost:7777/eureka
3、测试
zuul的过滤器都做一些权限判断等操作
客户端请求发送到zuul网关服务器上,首先调用PreFileter过虑链中,如果正常放行,会把请求转发到RoutingFilter过虑链,再转发到指定的服务,指定服务返回一个结果后,会再次经过一个PostFilter过虑链,最终在将响应信息交给客户端,这其中,如果出现错误,会转发到Error过虑链中,然后在发送到PostFilter过虑链中
1、在zuul的基础上搭建环境
2、编写过滤器,继承ZuulFilter,并且交给spring容器管理
/** * 路由网关的过滤器 */ @Component public class TestPreFilter extends ZuulFilter { /** * 设置filter类型 * @return */ @Override public String filterType() { return FilterConstants.PRE_TYPE; } /** * filter的优先级,数值越小,优先级越高 * @return */ @Override public int filterOrder() { return FilterConstants.PRE_DECORATION_FILTER_ORDER -1; } @Override public boolean shouldFilter() { return true; // 使用这个filter } @Override public Object run() throws ZuulException { // 1校验 // 获取HttpServletRequest RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletRequest request = requestContext.getRequest(); String token = request.getParameter("token"); if(StringUtils.isEmpty(token) || !"admin".equals(token)){ System.out.println("校验失败"); requestContext.setSendZuulResponse(false); // 不在往下执行了 // 在这里做出响应,这里设置的是枚举,表示没有权限 requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value()); } return null; } }
执行的顺顺序:可以在过滤器的filterOrder( )方法中指定
测试:验证失败
测试验证通过
路由网关的服务降级指的是通过旅游网关进行其他服务的远程调用时,如果调用的服务响应超时时,将会调用本地的方法进行响应
使用步骤
1、创建一个POJO类,并且实现FallbackProvider接口,复写内部方法
/** * * 测试服务的降级 * @param * @return */ @Component public class ZuulFallbackTest implements FallbackProvider { //指定进行服务降级策略的服务名,*表示所有的服务 @Override public String getRoute() { return "*"; } //服务降级后调用的本地方法 @Override public ClientHttpResponse fallbackResponse(String route, Throwable cause) { return new ClientHttpResponse() { //表示返回的状态 @Override public HttpStatus getStatusCode() throws IOException { return HttpStatus.INTERNAL_SERVER_ERROR;//表示服务异常 } //返回的状态码 @Override public int getRawStatusCode() throws IOException { return HttpStatus.INTERNAL_SERVER_ERROR.value();//500 } //返回对应返回的文本信息 @Override public String getStatusText() throws IOException { return HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase();//返回的是状态码的文本信息 } //当服务异常时,如果有资源需要关闭,就可以在这个方法中进行关闭 @Override public void close() { } //对应响应的数据 @Override public InputStream getBody() throws IOException { String errorMsg = "服务异常......进行服务降级......"; return new ByteArrayInputStream(errorMsg.getBytes("utf-8")); } //返回数据的格式 如json,xml @Override public HttpHeaders getHeaders() { HttpHeaders httpHeaders = new HttpHeaders(); //指定返回数据格式为json格式 httpHeaders.setContentType(MediaType.APPLICATION_JSON); return httpHeaders; } }; } }
注:如果服务间的调用超过1s任然未响应,那么路由网关就会自动调用服务降级,可以在application.yml文件中进行配置
ribbon:
ReadTimeout: 6000 # 设置zuul超时时间
ConnectTimeout: 6000 # 设置连接的超时时间
在我们之前使用自定义的服务配置后,我们都需要重启路由网关从而重新加载配置文件,在使用动态路由后就不在需要了,而是动态进行服务配置,本质上是使用过滤器来实现的
配置动态路由,是在路由网关的基础上实现的
添加一个过滤器,在过滤器的中配置访问服务的名称和访问的url
/** * 测试动态路由 * @param * @return */ @Component public class DynamicRoutingTest extends ZuulFilter { @Override public String filterType() { return FilterConstants.PRE_TYPE; } @Override public int filterOrder() { return FilterConstants.PRE_DECORATION_FILTER_ORDER+1;// } @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { //1、获取request RequestContext currentContext = RequestContext.getCurrentContext(); HttpServletRequest request = currentContext.getRequest(); //2、获取参数 String routing = request.getParameter("routing"); System.out.println(routing+"______________"); //3、进行判断 if(routing!=null && routing.equals("login")){ //指定访问服务的名称 currentContext.put(FilterConstants.SERVICE_ID_KEY,"ribbon-provider-v2"); //指定跳转的路径 currentContext.put(FilterConstants.REQUEST_URI_KEY,"/provider/login"); } if(routing!=null && routing.equals("hello")){ //指定访问服务的名称 currentContext.put(FilterConstants.SERVICE_ID_KEY,"ribbon-provider-v2"); //指定跳转的路径 currentContext.put(FilterConstants.REQUEST_URI_KEY,"/provider/hello"); } return null; } }
在springcloud的项目中,我们需要允许使用不同语言去实现微服务,但是非java语言是无法介入eureka,hystrix,fegin,ribbon等相关组件。有一种思路就是启动一个代理的微服务,由该代理微服务同非jvm服务交流,并接入eureka等相关组件。Sidecar就是该思路的实现,总结的说就是作为一个代理的服务来间接性的让其他语言可以使用Eureka等相关组件。
1、导入依赖
<!--sidecar的核心依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-sidecar</artifactId>
</dependency>
<!--eureka客户端的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2、编写配置文件,使用sidecar指定第三方服务的地址和端口号
server:
port: 8080
eureka:
client:
service-url:
defaultZone: http://localhost:7777/eureka
spring:
application:
name: baidu
# 配置第三方的服务信息
sidecar:
hostname: 14.215.177.38
port: 80
3、启动类中添加@EnableSidecar,以及eureka客户端的依赖
@SpringBootApplication
@EnableSidecar//表示开启sidecar
@ComponentScan("com.jn")
@EnableEurekaClient
public class Day0322SpringcloudSidecarApplication {
public static void main(String[] args) {
SpringApplication.run(Day0322SpringcloudSidecarApplication.class, args);
}
}
4、测试,开启该服务器以及路由网关服务器和注册中心,直接输入我们配置文件指定的地址http://lcoalhost/baidu进行测试
Stream就是在消息队列的基础上,对其进行封装,让咱们更方便的去操作MQ消息队列。
启动RabbitMQ
消费者-导入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
消费者-配置文件
spring:
# 连接RabbitMQ
rabbitmq:
host: 192.168.199.109
port: 5672
username: test
password: test
virtual-host: /test
消费者-监听的队列
public interface StreamClient {
@Input("myMessage")
SubscribableChannel input();
}
//-------------------------------------------------
@Component
@EnableBinding(StreamClient.class)
public class StreamReceiver {
@StreamListener("myMessage")
public void msg(Object msg){
System.out.println("接收到消息: " + msg);
}
}
生产者-导入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
生产者-配置文件
spring:
# 连接RabbitMQ
rabbitmq:
host: 192.168.199.109
port: 5672
username: test
password: test
virtual-host: /test
生产者-发布消息
public interface StreamClient {
@Output("myMessage")
MessageChannel output();
}
//---------------------------------------------- 在启动类中添加注解 @EnableBinding(StreamClient.class)
@Autowired
private StreamClient streamClient;
@GetMapping("/send")
public String send(){
streamClient.output().send(MessageBuilder.withPayload("Hello Stream!!").build());
return "消息发送成功!!";
}
只需要添加一个配置,指定消费者组
spring:
cloud:
stream:
bindings:
myMessage: # 队列名称
group: customer # 消费者组
编写配置
spring:
cloud:
stream:
# 实现手动ACK
rabbit:
bindings:
myMessage:
consumer:
acknowledgeMode: MANUAL
修改消费端方法
@StreamListener("myMessage")
public void msg(Object msg,
@Header(name = AmqpHeaders.CHANNEL) Channel channel,
@Header(name = AmqpHeaders.DELIVERY_TAG) Long deliveryTag) throws IOException {
System.out.println("接收到消息: " + msg);
channel.basicAck(deliveryTag,false);
}
我们一直都是将配置文件配置在各个服务器中,当我需要修改时,不方便维护,比如修改eureka服务端的端口号,那么所有的eureka客户端的配置文件都需要发生改变,而且修改完后服务器还需要进行重启,使用这些步骤变得很繁琐
那么我们可以准备一台专门配置文件信息的服务器,这台服务器配置了所有的公共配置信息,当其他服务器需要启动时,就去配置文件服务器中去拉取,然后再启动
配置文件的服务器也可以配置到git,数据库,RabbitMQ,redis等持久化存储中,将来配置文件服务器只需要进行对文件配置数据进行推送和拉取即可。
所以,文件配置服务也分服务端和客户端,所有依赖文件配置服务拉取配置的服务都是文件配置服务的客户端
1、创建工程,导入依赖
<!--文件配置服务器 config-server的核心依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
2、在主启动类上添加@EnableConfigServer注解,表示该服务为一个配置文件服务器
/**
* 搭建文件配置服务器
*/
@EnableConfigServer//表示该服务为一个配置文件服务器
@SpringBootApplication
public class Day0220SpringcloudConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(Day0220SpringcloudConfigServerApplication.class, args);
}
}
3、在resources目录下编写其他服务器的配置文件
如:application-eureka-server.yml
server:
port: 8888
eureka:
client:
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka
register-with-eureka: false
fetch-registry: false
instance:
hostname: localhost
再来一个用来后面测试的配置文件application-test.yml
str1: 111
str2: 222
4、配置服务器的配置文件
spring:
cloud:
config:
server:
native:
search-locations: classpath:config # 指定其他服务器的配置文件目录
server:
port: 9999
1、创建工程,并且导入依赖
<!--配置文件客户端的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
<!--为了后期试验,这里添加了eureka的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<!--为了后期试验,这里添加了web的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2、编写配置文件bootstrap.yml配置文件使用bootstrap.yml配置文件比使用application.yml配置文件的优先级要高:
spring:
cloud:
config:
uri: http://localhost:9999 # 配置服务的地址
name: application # 指的是读取文件类型为application类型的配置文件
profile: eureka-server,test # 指的是读取eureka-serverh和test的application配置文件,即读取配置文件服务器的eureka-server-application.yml和application-test.yml配置文件
3、主启动类
@SpringBootApplication
@EnableEurekaServer//表示是eureka的客户端
@ComponentScan("com.jn")
public class Day0221SpringcloudConfigClientEurekaApplication {
public static void main(String[] args) {
SpringApplication.run(Day0221SpringcloudConfigClientEurekaApplication.class, args);
}
}
4、启动文件配置服务器和该服务,访问eureka服务器配置的路径http://localhost:8888/,发现文件配置客户端已经从文件配置服务器中拉取了对应的配置文件
5、继续编写一个controller进行测试
/** * 测试配置文件客户端读取配置文件服务器的配置文件 * @param * @return */ @RestController public class ConfigClientTestController { @Value("${server.port}") private int port; @Value("${str1}") private String str1; @RequestMapping("/test") public String test(){ return "port:"+port+",str1:"+str1; } }
6、重启服务器访问http://localhost:8888/test,说明可以同时从文件配置服务器中拉取多个配置文件
在整个微服务架构中,微服务很多,一个请求可能需要调用很多的服务,最终才能完成一个功能,如果说整个功能出现了问题,在那么多的服务中,如何去定位问题的所在点,出现问题的原因是什么,我们就可以使用Sleuth技术
1、导入sleuth插件的依赖
<!--引入sleuth插件的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
2、配置文件中设置日志级别
#编写sleuth的配置文件
logging:
level:
org.springframework.web.servlet.DispathcherServlet: debug
3、测试,当添加该插件的服务去远程调用其他服务时,会输出如下日志
SEARCH(字段1):服务名称
e9c(字段2):总链路id
f07(字段3):当前服务的链路id
false:不会将当前的日志信息,输出其他系统中
日志可读性不强,Zipkin是提供服务链路的可视化页面
zipkin的安装
虚拟机使用docker创建zipkin服务,为了提高效率,整合了RabbitMQ,采用异步的方式,同时为了保证数据的持久化,还整合了elasticSearc用于持久化
version: "3.1"
services:
zipkin:
image: daocloud.io/daocloud/zipkin:latest
restart: always
container_name: zipkin
ports:
- 9411:9411
environment:
- RABBIT_ADDRESSES=192.168.199.109:5672 #指定了rabbitMQ消息队列的地址
- RABBIT_USER=guest # rabbitMQ认证的用户名
- RABBIT_PASSWORD=guest # rabbitMQ认证的密码
- RABBIT_VIRTUAL_HOST=/ # rabbitMQ的内置虚拟机
- STORAGE_TYPE=elasticsearch # 持久化的类型
- ES_HOSTS=http://192.168.40.100:9200 #持久化的地址 使用es会自动为我们生成索引和数据到es服务中
2、导入依赖
<!--zipkin核心依赖,里面包含了sleuth组件的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
3、编写配置文件
#指定服务的名称
spring:
sleuth:
sampler:
probability: 1 # 百分之多少的sleuth信息需要输出到zipkin中
zipkin:
base-url: http://192.168.199.109:9411/ # 指定zipkin的地址
sender:
type: rabbit # 指定发送到rabbitMQ中
#编写sleuth的配置文件
logging:
level:
org.springframework.web.servlet.DispathcherServlet: debug
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。