SpringCloud
概念
-
集群
多个相同的服务器完成相同的任务;提高吞吐量,减少服务器压力
-
分布式 、微服务
功能模块拆分成为单独的项目,独立运行; 利于解耦,减少单个服务器压力,提高吞吐量
-
SpringCloud
基于SpringBoot,约定大于配置,开箱即用; 方便构建分布式系统; 的一个工具集;
组件:Eureka & Ribbon & Feign &Zuul
-
单体项目性能差
-
——功能拆分为独立项目(分布式)
——每个功能多个服务器共同运作(集群)不同项目的服务需要互相调用协作(分布式) 运作相同功能的服务器被调用的频率需要控制以达到性能最优(集群)
-
——需要管理服务列表 :名称、地址、可用性,以便各个服务之间方便地互相调用
——需要配置服务的访问策略,以及服务宕机时的重试机制/服务熔断 -
——Eureka通过心跳机制、服务拉取和自我保护机制管理服务清单及其可用性
——Ribbon通过多种策略维护访问的频率,通过retry重试机制在轮询到的服务宕机时及时转向请求下一个
——Zuul通过服务降级、服务熔断,避免 对单个不可用服务的多个并发请求 导致 线程阻塞,线程资源消耗完毕,引发服务雪崩。服务宕机 Eureka会剔除失效服务,但需要一定时间,在此时间内若轮询到了失效服务,会出现错误页面,用户体验较差,故还是需要ribbon进行重试 zuul和ribbon重试机制的区别 重试机制适用于单体服务器本身的问题,可以通过重试(换一个服务器)解决; 若是业务逻辑本身的问题,只能通过Zuul对资源的处理(服务熔断等)避免产生更大雪崩
-
——feign 简化不同服务之间的调用过程
Eureka-服务治理
-
引入依赖(POM.xml)
服务注册中心引入
spring-cloud-starter-netflix-eureka-server
服务提供者和消费者引入spring-cloud-starter-netflix-eureka-client
(eureka替换为consol也可,以下全部内容无需改动)
-
配置Eureka注册中心集群(application.yml)
- 维护自己的端口号
server:port: 8081
- 向集群其他注册中心注册为client
eureka:client:service-url:defaultZone: http://localhost:8082/eureka
- 管理向自己注册的服务
server: eviction-interval-timer-in-ms: 3000 server: enable-self-preservation: false
- 维护自己的端口号
-
配置服务提供者(application.yml)
1. 维护自己的端口号server:port: 8089
2. 向集群全部注册中心注册为clienteureka:client:service-url:defaultZone: http://localhost:8082/eureka,http://localhost:8082/eureka
3. 向注册中心定期发送心跳,定义多久无心跳即为失效eureka:instance:lease-expiration-duration-in-seconds: 9 eureka:instance:lease-renewal-interval-in-seconds: 3
-
配置服务消费者(application.yml)
1. 维护自己的端口号server:port: 8088
2. 向集群其他注册中心注册为clienteureka:client:service-url:defaultZone: http://localhost:8082/eureka
3. 向注册中心定期拉取服务清单的频率:eureka:client:registry-fetch-interval-seconds: 3
所有eureka实例(注册中心、服务端、消费端)均可配置实例名
instance:
instance-id: ${spring.application.name}:${server.port}
prefer-ip-address: true
- 消费端向服务端通信
0. 定义OpenService(即消费端的Controller,对外开放请求)
1. DiscoveryClient实例.getInstances(“服务名”).get(index) 获取ServiceInstance对象
2. ServiceInstance对象.getHost() ServiceInstance对象.getProt() 拼接请求地址
3. RestTemplate对象.getForObject(url,返回类型) 向服务端请求并获取返回结果
复杂类型的通信
-
返回结果为List:使用数组接收(见UserController02 中第1个方法)
-
自定义的返回结果类中包含泛型:将泛型声明在类上即可(见UserController02 中第2个方法)
@Data
//自定义返回结果类,包含泛型
//注意,泛型若声明在参数中,则消费端无法获取泛型类型导致无法取到值
public class Result<T> {
Integer code;
List<T> list;
String status;
public Result(){}
public Result(String status, Integer code, List<T> list){
this.code=code;
this.list=list;
this.status=status;
}
}
@RestController
public class UserController02 {
@Resource
private RestTemplate template;
@Resource
private DiscoveryClient client;
@RequestMapping("/u")
public User[] getUsers(){
ServiceInstance serviceInstance = client.getInstances("userServer01").get(0);
String url = "http://" + serviceInstance.getHost()+":"+serviceInstance.getPort()+"/";
User[] users = template.getForObject(url, User[].class);
return users;
}
@RequestMapping("/r")
public Result getRes(){
ServiceInstance serviceInstance = client.getInstances("userServer01").get(0);
String url = "http://" + serviceInstance.getHost()+":"+serviceInstance.getPort()+"/res";
Result<User> result = template.postForObject(url,null, Result.class);
return result;
}
}
Ribbon-客户端负载均衡
在eureka的基础上:
- 引入
spring-cloud-starter-netflix-ribbon
依赖
在父POM或服务提供者和消费者POM都引入) - 配置服务提供者集群(如下)
拥有相同的服务功能(controller),相同的服务名称(application:name),不同的端口号(port)
spring:
application:
name: provider
server:
port: 8083
eureka:
client:
service-url:
defaultZone: http://localhost:8081/eureka, http://localhost:8082/eureka
instance:
lease-renewal-interval-in-seconds: 3
lease-expiration-duration-in-seconds: 9
instance-id: ${spring.application.name}:${server.port}
prefer-ip-address: true
spring:
application:
name: provider
server:
port: 8083
eureka:
client:
service-url:
defaultZone: http://localhost:8081/eureka, http://localhost:8082/eureka
instance:
lease-renewal-interval-in-seconds: 3
lease-expiration-duration-in-seconds: 9
instance-id: ${spring.application.name}:${server.port}
prefer-ip-address: true
-
服务消费者启用负载均衡(如下)
在创建RestTemplate类的方法上增加@LoadBalanced
注解告知Spring,启用了负载均衡,故后续调用getForObject方法时会对URL按负载均衡的形式进行解析
@SpringBootApplication
@EnableDiscoveryClient
public class ConsumerApp {
@LoadBalanced
@Bean
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerApp.class, args);
}
}
-
服务消费者获取具体服务实例(如下)
服务消费者web层装配一个LoadBalancerClient
类实例,用于调用choose方法,传入serviceId,获取一个服务实例ServiceInstance原理:拦截器拦截你的请求后: 1. 根据IloadBalancer策略类,默认使用 实现类ZoneAwareLoadBalancer 策略(同一个区域内负载较少的),从中选取一个Eureka中心实例 2. 根据实例名,得到字节码对象,从而获取服务实例对象列表 3. BaseLoadBalancer根据默认策略(RoundRibbonRuleBalancer轮询) 选择一个服务实例 4. 调用LoadBalanceClient的实现类RibbonLoadBalancerClient的execute方法执行请求
修改负载均衡策略(可选)
provider(你的serviceID): ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule(随机)
自定义负载均衡策略(可选)
a. 定义一个类,继承AbstractLoadBalancerRule
,重写choose
方法
b. 在yaml中配置NFLoadBalancerRuleClassName
-
调用服务,获取结果(如下)
使用RestTemplate实例调用getForObject方法,传入url和返回对象class此时URL是http://serviceId/+参数 ,不再是 ServiceInstance.getHost()+ServiceInstance.getPort()。因为spring会解析
@RestController
public class ConsumerController {
@Resource
private RestTemplate template;
@Resource
LoadBalancerClient client;
@RequestMapping(value = "/",method = RequestMethod.GET)
public User test(){
ServiceInstance instance = client.choose("provider");
String url ="http://provider/";
User u = template.getForObject(url, User.class);
System.out.println(u);
return u;
}
}
重试机制
-
添加依赖
spring-retry
-
配置重试参数
provider:
ribbon:
# 200ms连接不上就重试
ConnectTimeout: 200
# 100ms读不到/读不完就重试
ReadTimeout: 100
# 对所有操作重试
OkToRetryOnAllOperations: true
# 切换的间隔
MaxAutoRetriesNextServer: 1
# 重试次数
MaxAutoRetries: 1
feign——简化调用
简单使用
-
引入依赖
spring-cloud-starter-openfeign
必须是openfeign而不是feign!否则报错!
-
消费端仿造接口
定义一个interface,使用@FeignClient(value="你的serviceID")
注解
接口中定义一个 抽象方法 ,添加请求映射@requestMapping(value="需要调用的对方接口名")
-
消费端开启Feign
启动类使用@EnableFeignClients
注解
调用仿造接口的抽象方法,实现对其他服务的间接调用*无需再拼接URL、创建RestTemplate实例、DiscoveryClient实例
-
默认集成了Ribbon客户端负载均衡
默认轮询,无需单独引入组件或配置(也可在yaml中照常配置)
聚合项目
背景
原来即便使用了Feign,仍旧需要服务消费者(FeignConsumer)手动编写接口类和抽象方法,使其与服务提供方(Provider)的请求接口一致。但这在实际跨公司跨服务的项目中可行性较低(对方请求接口的变更,调用者无法得到通知)。
使用
-
由Provider直接对外提供接口
-
将项目变成接口子项目ProviderInterface和实现子项目ProviderImpl的聚合项目(父项目Provider打包成pom)
-
ProviderInterface中定义要提供的服务接口,并封装实体类,以便完整对外提供
@Data @NoArgsConstructor @AllArgsConstructor public class User { String username; Integer age; } public interface Pro1Api { @RequestMapping(value = "/",method = RequestMethod.GET) User pro1(); }
-
ProviderImpl依赖ProviderInterface并实现具体接口方法
<dependencies> <dependency> <groupId>com.zoya.springcloud</groupId> <artifactId>proAPI1-Interface</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies>
@RestController public class Pro1Controller implements Pro1Api { @RequestMapping(value = "/",method = RequestMethod.GET) public User pro1() { return new User("pro1Controller",1); } }
-
ProviderImpl向Eureka注册为服务提供者
(ProviderImpl的application.yaml中正常配置即可) -
启动ProviderImpl(启动实现子项目作为服务,而非父项目或接口子项目)
@SpringBootApplication @EnableDiscoveryClient public class ImplApp { public static void main(String[] args) { SpringApplication.run(ImplApp.class, args); } }
-
-
FeignConsumer依赖ProviderInterface并继承接口即可
-
引入ProviderInterface依赖
<dependencies> <dependency> <groupId>com.zoya.springcloud</groupId> <artifactId>proAPI1-Interface</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies>
-
编写接口类继承ProviderInterface提供的接口,@FeignClient(value=“Serviceid”)
通过ServiceID得到服务列表; 通过继承的Ribbon负载均衡后得到具体服务实例; 通过继承得到的方法中的注解映射到具体的对方服务; ——消费者端即获得了与对方服务接口对应的众多接口方法
@FeignClient(value = "provider-api") public interface myFeign extends Pro1Api { }
-
调用Feign接口的抽象方法,对外提供服务消费
@RestController public class FeignController { @Autowired private myFeign feign; @RequestMapping(value = "/get",method = RequestMethod.GET) public User getUser(){ return feign.pro1(); } }
-
请求过程分析
- 从浏览器请求consumer对外暴露的接口http://localhost:XXXX/get:
- 进入消费端,映射到 消费端web层 的 FeignController 的 getUser() 方法
- FeignController的getUser()调用了MyFeign接口的getUser()方法
- MyFeign接口通过注解声明自己是一个FeignClient
- 根据MyFeign声明的serviceid,获取一个服务实例providerXX
- 根据MyFeign继承来的方法及其@requestMapping注解,获取具体请求URL
- 调用到服务实例的某个请求方法
Hystrix
- 服务降级
接口/资源 不可用时,不进行阻塞,直接FallBack返回一个固定结果 - 服务熔断
- 降级的一种,从整个服务全局角度考虑资源
- 请求失败已达到阈值频次时,不再尝试请求,直接FallBack返回一个固定结果),定时再尝试恢复
- 使用 状态机 模型,判断 一定时间内错误请求的比例 ,达到阈值则状态机 打开 ,一定时间后改为 半开 (放过一些请求),被放过的请求没问题则状态机 关闭 ,请求正常通过。
- 资源隔离/限流(一般不通过状态机)
单独为某个接口限制访问线程数/每秒访问次数
通过缓存计数,每个IP对资源的访问频率,如果达到阈值则禁止访问
Zuul
过滤:
某些服务不经过网关(如文件上下传,会占用大量资源,不应经过网关)
前缀:
修饰作用,可以用于标识服务走了网关(如 /route 前缀)
Nginx
Nginx和Tomcat的异同
服务器 | 服务器类型 | 功能 |
---|---|---|
Nginx apahce | WEB服务器 | 接受请求,做出响应 |
apahce Tomcat WebLogic iis | web应用服务器 | 能充当web服务器,也能充当应用的运行环境,来运行程序 |
背景:高并发问题
通常对一个页面的加载需要向服务器发出几十至上千个请求,在高并发场景下并发请求数量巨大,而Tomcat可以负荷的并发请求理论值为200-500,完全无法应对。
故在请求到达Tomcat之前,提前对静态请求拦截,并直接返回静态资源;只放过需要请求后台数据的给Tomcat,能够有效利用服务器资源。(即反向代理)
反向代理:为服务器提供代理web请求
正向代理:为客户端提供代理
作用
- 反向代理,提高服务器资源利用率
- 集成了负载均衡策略,可以更高效的进行Tomcat集群
安装使用及配置
-
nginx使用C语言编写,故更适合运行于linux系统下
-
windows和linux系统下都有/etc/hosts.conf文件,可以配置ip地址对应的server名称(域名);
对外暴露域名而不暴露IP,提高安全性; 用户使用域名访问,浏览器会优先查询用户本地的host文件,从本地匹配IP,解析实际的访问IP对象;本地没有则到域名解析器去查找。
-
Tomcat集群+负载均衡
nginx可以指定tomcat_server_pool(Tomcat集群),将一个域名映射到一个服务器集群,更大的提高Tomcat对并发的支持量;对集群中具体访问哪个服务器的分配(负载均衡策略)使用轮询策略
1.需要强制刷新 2.可以使用权重策略,如下代码中,5次访问aaa.test2.com中,1次由8220处理,1次由8230处理,3次由8240处理(具体权重值由运维人员根据服务器性能并调试得出) 3.以上策略,可能出现同一个用户多次访问,会轮询到不同服务器处理,其session信息无法同步的问题。故可以使用 ip_hash; 告知nginx针对同一个ip,访问相同的服务器。 此时即便强制刷新也会一直由服务器处理。
#nginx.conf文件中配置多个虚拟主机(server)
upstream tomcat_server1 {
server zoyalinux:8210;
}
upstream tomcat_server_pool{
ip_hash;
server zoyalinux:8220 weight=1;
server zoyalinux:8230 weight=1;
server zoyalinux:8240 weight=3;
}
server {
listen 80;
server_name zoyalinux;
location / {
proxy_pass html;
index index.html;
}
}
server {
listen 90;
server_name zoyalinux;
location / {
proxy_pass http://tomcat_server1;
index index2.html;
}
}
server {
listen 80;
server_name aaa.test1.com;
location / {
proxy_pass http://tomcat_server1;
index index1.html;
}
}
server {
listen 80;
server_name aaa.test2.com;
location / {
proxy_pass http://tomcat_server_pool;
index index2.html;
}
}