SpringCloud常见组件有哪些
注册中心组件:Eureka、Nacos
负载均衡组件:Ribbon
远程调用组件:OpenFeign
网关组件:Zuul、Gateway
服务保护组件:Hystrix、Sentinel
服务配置管理组件:SpringCloudConfig、Nacos
Nacos的服务注册表结构是怎样的?
Nacos采用了数据的分级存储模型,最外层是Namespace,用来隔离环境。然后是Group,用来对服务分组。接下来就是服务(Service)了,一个服务包含多个实例,但是可能处于不同机房,因此Service下有多个集群(Cluster),Cluster下是不同的实例(Instance)。
namespace命名空间
服务、分组
集群、实例
Nacos如何支撑阿里内部数十万服务注册压力?
Nacos内部接收到注册的请求时,不会立即写数据,而是将服务注册的任务放入一个阻塞队列就立即响应给客户端。然后利用线程池读取阻塞队列中的任务,异步来完成实例更新,从而提高并发写能力。
Nacos如何避免并发读写冲突问题?
Nacos在更新实例列表时,会采用CopyOnWrite技术,首先将旧的实例列表拷贝一份,然后更新拷贝的实例列表,再用更新后的拷贝的实例列表来覆盖旧的实例列表。
这样在更新的过程中,就不会对读实例列表的请求产生影响,也不会出现脏读问题了(读旧的实例列表)。
以服务service为锁对象,同一个服务service中多个instance实例,只能串行来完成注册。多个服务service之间互不影响。
Nacos与Eureka的区别有哪些?
Nacos | Eureka | |
---|---|---|
接口方式 | Nacos对外暴露了Rest风格的API接口,用来实现服务注册、发现等功能 | Eureka都对外暴露了Rest风格的API接口,用来实现服务注册、发现等功能 |
实例类型 | Nacos的实例有永久和临时实例之分 | Eureka只支持临时实例 |
健康检测 | Nacos对临时实例采用心跳模式检测,对永久实例采用主动请求来检测 | Eureka只支持心跳模式 |
服务发现 | Nacos支持定时拉取和订阅推送(当服务发生变更时,通过订阅推送主动通知各个实例)两种模式 | Eureka只支持定时拉取模式(30s) |
eureka服务注册、服务发现
eureka-server服务项目
1,创建eureka-server服务项目
2,启动类加@EnableEurekaServer
3,application.yml中将eurekaserver注册到eureka服务中
user-service项目
1,user-service pom文件中引入eureka-client
2,application.yml中将userservice服务注册到eureka服务中
3,RestTemplate远程调用,比如orderservice去调用userservice
nacos服务注册、服务发现
1,user-service pom文件中引入alibaba-nacos-discovery
2,application.yml中将userservice服务注册到nacos中
3,RestTemplate远程调用,比如orderservice去调用userservice
nacos配置集群
nocos注册中心,相应服务下创建集群,集群下创建对应实例,部分实例根据机器性能配置权重
server:
port: 8080
spring:
application:
name: orderservice # order服务名称
cloud:
nacos:
server-addr: localhost:8848
# 1,给order-service配置集群信息
discovery:
cluster-name: xa # 集群名称,自定义XA表示西安集群 BJ代表北京集群
userservice: # 配置负载均衡规则
ribbon:
# 2,修改负载均衡规则
NFLoadBalancerRuleClassName:
com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则
ribbon:
eager-load:
enabled: true # 开启饥饿加载
clients: # 指定饥饿加载的服务名称,这里是个集合,可以指定多个
- userservice
Nacos配置管理
bootstrap.yaml文件,会在application.yml之前被读取。经常修改、热更新的配置放到nacos中管理。基本不会变更的一些配置保存在微服务本地
userservice.yaml不包含环境,可以被多个环境dev、prod等共享。
1,nacos配置列表中创建比如userservice-dev.yaml
pattern:
dateformat: yyyy-MM-dd HH:mm:ss
2,引入nacos-config依赖
3,添加bootstrap.yaml,设置服务名userservice、开发环境dev、文件后缀名yaml
4,nacos配置热更新
方式1:@Value注入的变量,所在类上添加注解@RefreshScope
@RestController
@RequestMapping("/user")
public class UserController {
@Value("${pattern.dateformat}")
private String dateformat;
@GetMapping("now")
public String now(){
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}
}
方式2:使用@ConfigurationProperties
@Data
@Component
//@ConfigurationPropertie将application.yml配置的值注入到bean上,必须将对象注入IOC容器中才有配置绑定的功能
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
private String dateformat;
}
Nacos配置共享的优先级
nacos配置 > 本地配置
nacos各环境配置 > nacos不包含环境配置
userservice-dev.yaml > userservice.yaml > 本地application.yaml
微服务雪崩解决方式
流量控制是对服务的保护,避免因瞬间高并发流量而导致服务故障,进而避免雪崩。是一种预防措施。
超时处理、舱壁模式、熔断降级是在部分服务故障时,将故障控制在一定范围,避免雪崩。是一种补救措施。
1,超时处理:设定超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止等待
2,舱壁模式:舱壁模式来源于船舱的设计,船舱都会被隔板分离为多个独立空间,当船体破损时,只会导致部分空间进入,将故障控制在一定范围内,避免整个船体都被淹没。
于此类似,我们可以限定每个业务能使用的线程数,避免耗尽整个tomcat的资源,因此也叫线程隔离。
3,熔断降级:由断路器统计业务执行的异常比例,如果超出阈值则会熔断该业务,拦截访问该业务的一切请求。断路器会统计访问某个服务的请求数量,异常比例
当发现访问服务D的请求异常比例过高时,认为服务D有导致雪崩的风险,会拦截访问服务D的一切请求,形成熔断
4,流量控制:限制业务访问的QPS(每秒处理的请求数量),避免服务因流量的突增而故障。
Sentinel的线程隔离与Hystix的线程隔离有什么差别?
Hystix | Sentinel |
---|---|
Hystix默认是基于线程池实现的线程隔离 | Sentinel是基于信号量(计数器)实现的线程隔离 |
每一个被隔离的业务都要创建一个独立的线程池 | 不用创建线程池 |
支持主动超时,支持异步调用 | 不支持主动超时,不支持异步调用 |
线程的额外开销比较大,性能一般,但是隔离性更强 | 轻量级,无额外开销,性能较好,但是隔离性一般 |
Sentinel的限流与Gateway的限流有什么差别?
限流算法常见的有三种实现:滑动时间窗口、令牌桶算法、漏桶算法。
Gateway则采用了基于Redis实现的令牌桶算法。
Sentinel限流算法
对比项 | 滑动时间窗口 | 令牌桶 | 漏桶 |
---|---|---|---|
能否保证流量曲线平滑 | 不能,但窗口内区间越小,流量控制越平滑 | 基本能,在请求量持续高于令牌生成速度时,流量平滑。在请求量在令牌生成速率上下波动时,无法保证曲线平滑 | 能,所有请求进入桶内,以恒定速率放行,绝对平滑 |
能否应对突增流量 | 不能,突增流量,只要高出限流阈值都会被拒绝 | 能,桶内积累的令牌可以应对突增流量 | 能,请求可以增存在桶内 |
流量控制精准度 | 低,窗口区间越小,精度越高 | 高 | 高 |
默认限流模式是基于滑动时间窗口算法
滑动时间窗限流算法解决了固定时间窗限流算法的问题。其没有划分固定的时间窗起点与终点,而是将每一次请求的到来时间点作为统计时间窗的终点,起点则是终点向前推时间窗长度的时间点。这种时间窗称为“滑动时间窗”
比如1分钟阈值是100个,将1分钟分为4格,第一格假如10个请求,第二、三、四格15个请求,那么第二分钟的前15秒,就可以最多处理40个请求。
排队等待的限流模式则基于漏桶算法
将每个请求视作"水滴"放入"漏桶"进行存储;
"漏桶"以固定速率向外"漏"出请求来执行,如果"漏桶"空了则停止"漏水”;
如果"漏桶"满了则多余的"水滴"会被直接丢弃。
可以理解成请求在桶内排队等待,可以处理突发请求,请求处理曲线平滑
比如单机阈值=10,也就是100ms放行一个请求
热点参数限流则是基于令牌桶算法
以固定的速率生成令牌,存入令牌桶中,如果令牌桶满了以后,多余令牌丢弃
请求进入后,必须先尝试从桶中获取令牌,获取到令牌后才可以被处理
如果令牌桶中没有令牌,则请求等待或丢弃
Ribbon负载均衡注入RestTemplate
@LoadBalanced //加入负载均衡,order去调取user或者user2
@Bean //创建RestTemplate对象并注入spring容器
public RestTemplate restTemplate(){return new RestTemplate();}
修改Ribbon负载均衡策略
方式1:
代码方式(作用于order-service中调用别的任何微服务)
@Bean
public IRule randomRule(){return new RandomRule();}
方式2:
配置文件方式(只作用于userservice)
server:
port: 8080
spring:
application:
name: orderservice # order服务名称
eureka:
client:
service-url: # eureka的地址信息 eureka会将自己也注册到eureka服务中
defaultZone: http://127.0.0.1:10086/eureka
userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则
Ribbon负载均衡基本流程
拦截我们的RestTemplate请求http://userservice/user/1
- RibbonLoadBalancerClient会从请求url中获取服务名称,也就是user-service
- DynamicServerListLoadBalancer根据user-service到eureka拉取服务列表
- eureka返回列表,localhost:8081、localhost:8082
- IRule利用内置负载均衡规则,从列表中选择一个,例如localhost:8081
- RibbonLoadBalancerClient修改请求地址,用localhost:8081替代userservice,得到http://localhost:8081/user/1,发起真实请求
Ribbon负载均衡,首次请求慢
Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。
而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:
server:
port: 8080
spring:
application:
name: orderservice # order服务名称
eureka:
client:
service-url: # eureka的地址信息,eureka会将自己也注册到eureka服务中
defaultZone: http://127.0.0.1:10086/eureka
userservice: # 给某个微服务配置负载均衡规则,orderservice会远程调用userservice服务
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则
ribbon:
eager-load:
enabled: true # 开启饥饿加载
clients: # 指定饥饿加载的服务名称,这里是个集合,可以指定多个
- userservice
Ribbon的属性配置和类配置优先级
SpringCloud Alibaba—Ribbon的属性配置和类配置优先级
远程调用
RestTemplate远程调用
1,创建RestTemplate对象并注入spring容器
//创建RestTemplate对象并注入spring容器
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
2,Service中注入RestTemplate,比如get方法,getForObject()去远程调用http请求
实现了跨服务的远程调用,其实就是发送了一次http请求
@Service
public class OrderService {
@Autowired
private RestTemplate restTemplate;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
//2利用RestTemplate发起http请求,查询用户
String url = "http://localhost:8081/user/"+order.getUserId();
//get请求,设置返回值json封装成User对象
User user = restTemplate.getForObject(url, User.class);
//3封装user到order
order.setUser(user);
// 4.返回
return order;
}
}
Feign远程调用
1,引入openfeign依赖
2,启动类添加@EnableFeignClients注解
3,编写Feign的客户端
//在order-service中新建一个接口
@FeignClient("userservice")
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
4,Service中使用FeignClient中定义的方法远程调用
//实现了跨服务的远程调用,其实就是发送了一次http请求
@Service
public class OrderService {
@Autowired
private UserClient userClient;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
//2使用feign远程调用
User user = userClient.findById(order.getUserId());
//3封装user到order
order.setUser(user);
// 4.返回
return order;
}
}
Feign远程调用,修改自定义配置
Feign可以支持很多的自定义配置,feign.Logger.Level修改日志级别、feign.codec.Decoder响应结果的解析器(例如解析json字符串为java对象)、feign.codec.Encoder请求参数编码、feign.Contract支持的注解格式、feign.Retryer失败重试机制
方式1:配置文件方式
feign:
client:
config:
userservice: # 针对某个微服务的配置
loggerLevel: FULL # 日志级别
feign:
client:
config:
default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
loggerLevel: FULL # 日志级别
方式2:Java代码方式
1,FeignConfiguration类设置自定义配置
//设置Feign自定义配置
public class DefaultFeignConfiguration {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.BASIC; // 日志级别为BASIC
}
}
2,如果要全局生效,将其放到启动类的@EnableFeignClients这个注解中
@SpringBootApplication
//自动装配开关,全局有效
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
如果是局部生效,则把它放到对应的@FeignClient这个注解中:
//局部有效
@FeignClient(value = "userservice",configuration = DefaultFeignConfiguration.class)
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
Feign远程调用,日志级别
NONE:不记录任何日志信息,这是默认值。
BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
Feign远程调用,性能优化
Feign底层发起http请求,依赖于其它的框架。其底层客户端实现包括:
底层客户端 | 连接池 | 优化手段 |
---|---|---|
URLConnection | 默认实现,不支持连接池 | |
Apache HttpClient | 支持连接池 | 提高Feign的性能主要手段就是使用连接池代替默认的URLConnection |
OKHttp | 支持连接池 | 提高Feign的性能主要手段就是使用连接池代替默认的URLConnection |
1,pom文件引入Apache HttpClient依赖
2,application.yml中配置连接池
feign:
httpclient:
enabled: true # 开启feign对HttpClient的支持
max-connections: 200 # 最大的连接数
max-connections-per-route: 50 # 单个路径的最大连接数
Feign远程调用,最佳实践
1,创建module,feign-api
1.1,抽取 首先创建一个module,命名为feign-api
1.2,pom文件中引入openfeign依赖
1.3,UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中
@FeignClient(value = "userservice")
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
public class DefaultFeignConfiguration {
@Bean
public Logger.Level logLevel(){
return Logger.Level.BASIC;
}
}
@Data
public class User {
private Long id;
private String username;
private String address;
}
2,在order-service中使用feign-api
2.1,在order-service的pom文件中引入feign-api的依赖
2.2,解决扫描包问题
因为@SpringBootApplication只会扫描到本项目下的包,com.api.clients.UserClient是引入的第三方jar,需要再指定扫描
方式1:指定Feign应该扫描的包:
@SpringBootApplication
//自动装配开关
//basePackages指定feign client所在包
@EnableFeignClients(basePackages = "com.api.clients",defaultConfiguration = DefaultFeignConfiguration.class)
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
方式2:指定需要加载的Client接口:推荐方式二
@SpringBootApplication
//自动装配开关
//clients = UserClient.class指定加载哪个客户端
@EnableFeignClients(clients = UserClient.class,defaultConfiguration = DefaultFeignConfiguration.class)
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
FeignClient整合Sentinel
1,OrderService的application.yml文件,开启Feign的Sentinel功能
feign:
httpclient:
enabled: true # 支持HttpClient的开关
max-connections: 200 # 最大连接数
max-connections-per-route: 50 # 单个路径的最大连接数
sentinel:
enabled: true # 开启feign对sentinel的支持
2,编写失败降级逻辑
@Slf4j
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
@Override
public UserClient create(Throwable throwable) {
return new UserClient() {
@Override
public User findById(Long id) {
//失败降级逻辑
log.error("查询用户异常", throwable);
return new User();
}
};
}
}
3,在feing-api项目中的DefaultFeignConfiguration类中将UserClientFallbackFactory注册为一个Bean
public class DefaultFeignConfiguration {
@Bean
public UserClientFallbackFactory userClientFallbackFactory(){
return new UserClientFallbackFactory();
}
}
4,在feing-api项目中的UserClient接口中使用UserClientFallbackFactory
@FeignClient(value = "userservice", fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
sentinel服务熔断过程
熔断状态 | |
---|---|
closed关闭状态 | 断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态 |
open打开状态 | 服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态5秒后会进入half-open状态 |
half-open半开状态 | 放行一次请求,根据执行结果来判断接下来的操作。请求成功:则切换到closed状态;请求失败:则切换到open状态 |
gateway网关
1,创建module,pom文件中引入gateway依赖
2,创建application.yml,配置相关路由规则
server:
port: 10010
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
gateway:
routes:
- id: user-service # 路由标示,必须唯一
uri: lb://userservice # 路由的目标地址
predicates: # 路由断言,判断请求是否符合规则
- Path=/user/** # 路径断言,判断路径是否是以/user开头,如果是则符合
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
3,重启网关,访问http://localhost:10010/user/1时,符合/user/**规则,请求转发到http://userservice/user/1