1、分布式链路追踪技术 Sleuth + Zipkin
1.1 分布式链路追踪技术适用场景(问题场景)
为了支撑日益增⻓的庞大业务量,我们会使用微服务架构设计我们的系统,使得 我们的系统不仅能够通过集群部署抵挡流量的冲击,又能根据业务进行灵活的扩展。那么,在微服务架构下,一次请求少则经过三四次服务调用完成,多则跨越几十 个甚至是上百个服务节点。那么问题接踵而来:
1)如何动态展示服务的调用链路?(比如A服务调用了哪些其他的服务—依赖 关系)
2)如何分析服务调用链路中的瓶颈节点并对其进行调优?(比如A—>B—>C,C 服务处理时间特别⻓)
3)如何快速进行服务链路的故障发现? 这就是分布式链路追踪技术存在的目的和意义
如果我们在一个请求的调用处理过程中,在各个链路节点都能够记录下日志,并最终将日志进行集中可视化展示,那么我们想监控调用链路中的一些指标就有希望了~~~比如,请求到达哪个服务实例?请求被处理的状态怎样?处理耗时怎 样?这些都能够分析出来了… 分布式环境下基于这种想法实现的监控技术就是就是分布式链路追踪(全链路追踪)。
市场上的分布式链路追踪方案:
- Spring Cloud Sleuth + Twitter Zipkin
- 阿里巴巴的“鹰眼”
- 大众点评的“CAT”
- 美团的“Mtrace”
- 京东的“Hydra”
- 新浪的“Watchman”
- 另外还有最近也被提到很多的Apache Skywalking。
1.2 分布式链路追踪技术核心思想
本质:记录日志,作为一个完整的技术,分布式链路追踪也有自己的理论和概念
微服务架构中,针对请求处理的调用链可以展现为一棵树
上图描述了一个常⻅的调用场景,一个请求通过网关服务路由到下游的微服务-1, 然后微服务-1调用微服务-2,拿到结果后再调用微服务-3,最后组合微服务-2和微服务-3的结果,通过网关返回给用户.
为了追踪整个调用链路,肯定需要记录日志,日志记录是基础,在此之上肯定有一些理论概念,当下主流的的分布式链路追踪技术/系统所基于的理念都来自于Google 的一篇论文《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》,这里面涉及到的核心理念是什么,我们来看下,还以前面的服务调用来说
上图标识一个请求链路,一条链路通过TraceId唯一标识,span标识发起的请求信 息,各span通过parrentId关联起来
Trace: 服务追踪的追踪单元是从客户发起请求(request)抵达被追踪系统的边界开始到被追踪系统向客户返回响应(response)为止的过程
Trace ID: 为了实现请求跟踪,当请求发送到分布式系统的入口端点时,只需要服 务跟踪框架为该请求创建一个唯一的跟踪标识Trace ID,同时在分布式系统内部流转 的时候,框架失踪保持该唯一标识,直到返回给请求方
一个Trace由一个或者多个Span组成,每一个Span都有一个SpanId,Span中会记录 TraceId,同时还有一个叫做ParentId,指向了另外一个Span的SpanId,表明父子关系,其实本质表达了依赖关系
Span ID: 为了统计各处理单元的时间延迟,当请求到达各个服务组件时,也是通过 一个唯一标识Span ID来标记它的开始,具体过程以及结束。对每一个Span来说, 它必须有开始和结束两个节点,通过记录开始Span和结束Span的时间戳,就能统计 出该Span的时间延迟,除了时间戳记录之外,它还可以包含一些其他元数据,比如 时间名称、请求信息等。
每一个Span都会有一个唯一跟踪标识Span ID,若干个有序的 span 就组成了一个 trace。
Span可以认为是一个日志数据结构,在一些特殊的时机点会记录了一些日志信息, 比如有时间戳、spanId、TraceId,parentIde等,Span中也抽象出了另外一个概 念,叫做事件,核心事件如下
- CS :client send/start 客户端/消费者发出一个请求,描述的是一个span开始
- SR: server received/start 服务端/生产者接收请求 SR-CS属于请求发送的网络延 迟
- SS: server send/finish 服务端/生产者发送应答 SS-SR属于服务端消耗时间
- CR:client received/finished 客户端/消费者接收应答 CR-SS表示回复需要的时 间(响应的网络延迟)
Spring Cloud Sleuth (追踪服务框架)可以追踪服务之间的调用,Sleuth可以记录 一个服务请求经过哪些服务、服务处理时⻓等,根据这些,我们能够理清各微服务 间的调用关系及进行问题追踪分析。
- 耗时分析:通过 Sleuth 了解采样请求的耗时,分析服务性能问题(哪些服务调 用比较耗时)
- 链路优化:发现频繁调用的服务,针对性优化等 Sleuth就是通过记录日志的方式来记录踪迹数据的
注意:我们往往把Spring Cloud Sleuth 和 Zipkin 一起使用,把Sleuth的数据信息发送给 Zipkin 进行聚合,利用Zipkin存储并展示数据。
1.3 Sleuth + Zipkin
- Sleuth 整合
pom.xml
<!--链路追踪-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
application.yaml
#分布式链路追踪
logging:
level:
org.springframework.web.servlet.DispatcherServlet: debug
org.springframework.cloud.sleuth: debug
请求到来时,我们在控制台可以观察到 Sleuth 输出的日志(全局 TraceId、SpanId 等)。
这样的日志首先不容易阅读观察,另外日志分散在各个微服务服务器上,接下来我 们使用zipkin统一聚合轨迹日志并进行存储展示。
2)Zipkin 结合
Zipkin 包括Zipkin Server和 Zipkin Client两部分,Zipkin Server是一个单独的服务,Zipkin Client就是具体的微服务
-
Zipkin Server 构建
pom.xml<!--zipkin-server的依赖坐标--> <dependency> <groupId>io.zipkin.java</groupId> <artifactId>zipkin-server</artifactId> <version>2.12.3</version> <exclusions> <!--排除掉log4j2的传递依赖,避免和springboot依赖的日志组件冲突--> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </exclusion> </exclusions> </dependency> <!--zipkin-server ui界面依赖坐标--> <dependency> <groupId>io.zipkin.java</groupId> <artifactId>zipkin-autoconfigure-ui</artifactId> <version>2.12.3</version> </dependency>
application.yml
server: port: 9411 management: metrics: web: server: auto-time-requests: false # 关闭自动检测请求
入口启动类 添加注解
@EnableZipkinServer // 开启Zipkin Server功能 -
Zipkin Client 构建(在具体微服务中修改)
pom.xml
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId> </dependency>
application.yml
spring: zipkin: base-url: http://127.0.0.1:9411 # zipkin server的请求地址 sender: # web客户端将踪迹日志数据通过网络请求的方式传送到服务端,另外还有配置 # kafka/rabbit 客户端将踪迹日志数据传递到mq进行中转 type: web sleuth: sampler: # 采样率 1 代表100%全部采集 ,默认0.1 代表10% 的请求踪迹数据会被采集 # 生产环境下,请求量非常大,没有必要所有请求的踪迹数据都采集分析,对于网络包括server端压力都是比较大的,可以配置采样率采集一定比例的请求的踪迹 数据进行分析即可 probability: 1
另外,对于log日志,依然保持开启debug状态
-
Zipkin server ⻚面方便我们查看服务调用依赖关系及一些性能指标和异常信息
-
追踪数据Zipkin持久化到mysql
mysql中创建名称为zipkin的数据库,并执行如下sql语句(官方提供)CREATE TABLE IF NOT EXISTS zipkin_spans ( `trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit', `trace_id` BIGINT NOT NULL, `id` BIGINT NOT NULL, `name` VARCHAR(255) NOT NULL, `remote_service_name` VARCHAR(255), `parent_id` BIGINT, `debug` BIT(1), `start_ts` BIGINT COMMENT 'Span.timestamp(): epoch micros used for endTs query and to implement TTL', `duration` BIGINT COMMENT 'Span.duration(): micros used for minDuration and maxDuration query', PRIMARY KEY (`trace_id_high`, `trace_id`, `id`) ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci; ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTracesByIds'; ALTER TABLE zipkin_spans ADD INDEX(`name`) COMMENT 'for getTraces and getSpanNames'; ALTER TABLE zipkin_spans ADD INDEX(`remote_service_name`) COMMENT 'for getTraces and getRemoteServiceNames'; ALTER TABLE zipkin_spans ADD INDEX(`start_ts`) COMMENT 'for getTraces ordering and range'; CREATE TABLE IF NOT EXISTS zipkin_annotations ( `trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit', `trace_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.trace_id', `span_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.id', `a_key` VARCHAR(255) NOT NULL COMMENT 'BinaryAnnotation.key or Annotation.value if type == -1', `a_value` BLOB COMMENT 'BinaryAnnotation.value(), which must be smaller than 64KB', `a_type` INT NOT NULL COMMENT 'BinaryAnnotation.type() or -1 if Annotation', `a_timestamp` BIGINT COMMENT 'Used to implement TTL; Annotation.timestamp or zipkin_spans.timestamp', `endpoint_ipv4` INT COMMENT 'Null when Binary/Annotation.endpoint is null', `endpoint_ipv6` BINARY(16) COMMENT 'Null when Binary/Annotation.endpoint is null, or no IPv6 address', `endpoint_port` SMALLINT COMMENT 'Null when Binary/Annotation.endpoint is null', `endpoint_service_name` VARCHAR(255) COMMENT 'Null when Binary/Annotation.endpoint is null' ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci; ALTER TABLE zipkin_annotations ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `span_id`, `a_key`, `a_timestamp`) COMMENT 'Ignore insert on duplicate'; ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`, `span_id`) COMMENT 'for joining with zipkin_spans'; ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTraces/ByIds'; ALTER TABLE zipkin_annotations ADD INDEX(`endpoint_service_name`) COMMENT 'for getTraces and getServiceNames'; ALTER TABLE zipkin_annotations ADD INDEX(`a_type`) COMMENT 'for getTraces and autocomplete values'; ALTER TABLE zipkin_annotations ADD INDEX(`a_key`) COMMENT 'for getTraces and autocomplete values'; ALTER TABLE zipkin_annotations ADD INDEX(`trace_id`, `span_id`, `a_key`) COMMENT 'for dependencies job'; CREATE TABLE IF NOT EXISTS zipkin_dependencies ( `day` DATE NOT NULL, `parent` VARCHAR(255) NOT NULL, `child` VARCHAR(255) NOT NULL, `call_count` BIGINT, `error_count` BIGINT, PRIMARY KEY (`day`, `parent`, `child`) ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
pom文件引入相关依赖
<dependency> <groupId>io.zipkin.java</groupId> <artifactId>zipkin-autoconfigure-storage-mysql</artifactId> <version>2.12.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> </dependency>
修改配置文件
spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/zipkin? useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true username: root password: 123456 druid: initialSize: 10 minIdle: 10 maxActive: 30 maxWait: 50000 # 指定zipkin持久化介质为mysql zipkin: storage: type: mysql
启动类中注入事务管理器
@Bean public PlatformTransactionManager txManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); }
2、Spring Cloud OAuth2
2.1 微服务架构下统一认证思路
- 基于Session的认证方式
在分布式的环境下,基于session的认证会出现一个问题,每个应用服务都需要 在session中存储用户身份信息,通过负载均衡将本地的请求分配到另一个应用 服务需要将session信息带过去,否则会重新认证。我们可以使用Session共享、 Session黏贴等方案。
Session方案也有缺点,比如基于cookie,移动端不能有效使用等 - 基于token的认证方式
基于token的认证方式,服务端不用存储认证数据,易维护扩展性强, 客户端可 以把token 存在任意地方,并且可以实现web和app统一认证机制。其缺点也很 明显,token由于自包含信息,因此一般数据量较大,而且每次请求 都需要传 递,因此比较占带宽。另外,token的签名验签操作也会给cpu带来额外的处理 负担。
- 资源所有者(Resource Owner):可以理解为用户自己
- 客户端(Client):我们想登陆的网站或应用,比如拉勾网
- 认证服务器(Authorization Server):可以理解为微信或者QQ
- 资源服务器(Resource Server):可以理解为微信或者QQ
第三方授权登录的场景:比如,我们经常登录一些网站或者应用的时候,可以选择 使用第三方授权登录的方式,比如:微信授权登录、QQ授权登录、微博授权登录 等,这是典型的 OAuth2 使用场景。
单点登录的场景:如果项目中有很多微服务或者公司内部有很多服务,可以专⻔做 一个认证中心(充当认证平台⻆色),所有的服务都要到这个认证中心做认证,只 做一次登录,就可以在多个授权范围内的服务中自由串行。
OAuth2的颁发Token授权方式
1)授权码(authorization-code)
2)密码式(password)提供用户名+密码换取token令牌
3)隐藏式(implicit)
4)客户端凭证(client credentials)
2.2 搭建认证服务器(Authorization Server)
负责颁发token
pom.xml
<!--导入spring cloud oauth2依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.11.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
认证服务器配置类
/**
* 当前类为Oauth2 server的配置类(需要继承特定的父类AuthorizationServerConfigurerAdapter)
*/
@Configuration
@EnableAuthorizationServer // 开启认证服务器功能
public class OauthServerConfiger extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
/**
* 认证服务器最终是以api接口的方式对外提供服务(校验合法性并生成令牌、校验令牌等)
* 那么,以api接口方式对外的话,就涉及到接口的访问权限,我们需要在这里进行必要的配置
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
super.configure(security);
// 相当于打开endpoints访问接口的开关,这样的话后期我们能够访问该接口
security
// 允许客户端表单认证
.allowFormAuthenticationForClients()
// 开启端口/oauth/token_key的访问权限(允许)
.tokenKeyAccess("permitAll()")
// 开启端口/oauth/check_token的访问权限(允许)
.checkTokenAccess("permitAll()");
}
/**
* 客户端详情配置,
* 比如client_id,secret
* 当前这个服务就如同QQ平台,拉勾网作为客户端需要qq平台进行登录授权认
证等,提前需要到QQ平台注册,QQ平台会给拉勾网
* 颁发client_id等必要参数,表明客户端是谁 * @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
super.configure(clients);
clients.inMemory()// 客户端信息存储在什么地方,可以在内存中,可以在数据库里
.withClient("client_lagou") //添加一个client配置,指定其client_id
.secret("abcxyz") // 指定客户端的密码/安全码
.resourceIds("autodeliver") // 指定客户端 所能访问资源id清单,此处的资源id是需要在具体的资源服务器上也配置一样
// 认证类型/令牌颁发模式,可以配置多个在这里,但是不一定 都用,具体使用哪种方式颁发token,需要客户端调用的时候传递参数指定
.authorizedGrantTypes("password","refresh_token")
// 客户端的权限范围,此处配置为all全部即可
.scopes("all");
}
/**
* 认证服务器是玩转token的,那么这里配置token令牌管理相关(token此时
就是一个字符串,当下的token需要在服务器端存储, * 那么存储在哪里呢?都是在这里配置)
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
super.configure(endpoints);
endpoints.tokenStore(tokenStore()) // 指定token的存储方法
.tokenServices(authorizationServerTokenServices()) // token服务的一个描述,可以认为是token生成细节的描述,比如有效时间多少等
.authenticationManager(authenticationManager) // 指定认证管理器,随后注入一个到当前类使用即可
.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.PO ST);
}
/*
* 该方法用于创建tokenStore对象(令牌存储对象) token以什么形式存储
*/
public TokenStore tokenStore(){
return new InMemoryTokenStore();
}
/**
* 该方法用户获取一个token服务对象(该对象描述了token有效期等信息)
*/
public AuthorizationServerTokenServices authorizationServerTokenServices() {
// 使用默认实现
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setSupportRefreshToken(true); // 是 否开启令牌刷新
defaultTokenServices.setTokenStore(tokenStore());
// 设置令牌有效时间(一般设置为2个小时) defaultTokenServices.setAccessTokenValiditySeconds(20);
// access_token就是我们请求资源需要携带的令牌 // 设置刷新令牌的有效时间
defaultTokenServices.setRefreshTokenValiditySeconds(259200); // 3天
return defaultTokenServices;
}
}
-
关于三个configure方法
configure(ClientDetailsServiceConfigurer clients)
用来配置客户端详情服务(ClientDetailsService),客户端详情信息在 这 里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存 储调取详情信息configure(AuthorizationServerEndpointsConfigurer endpoints)
用来配置令牌(token)的访问端点和令牌服务(token services)configure(AuthorizationServerSecurityConfigurer oauthServer)
用来配置令牌端点的安全约束. -
关于 TokenStore
InMemoryTokenStore
默认采用,它可以完美的工作在单服务器上(即访问并发量 压力不大 的情况下,并且它在失败的时候不会进行备份),大多数的项目都可以 使用这个版本的实现来进行 尝试,你可以在开发的时候使用它来进行 管理,因为不会被保存到磁盘中,所以更易于调试。JdbcTokenStore
这是一个基于JDBC的实现版本,令牌会被保存进关系型数据库。使用 这个版本的实现时, 你可以在不同的服务器之间共享令牌信息,使用 这个版本的时候请注意把"spring-jdbc"这个依赖加入到你的 classpath 当中。JwtTokenStore 这个版本的全称是 JSON Web Token(JWT),它可以 把令牌相关的数据进行编码(因此对于后端服务来说,它不需要进行存 储,这将是一个重大优势),缺点就是这个令牌占用的空间会比较大, 如果你加入了比较多用户凭证信息,JwtTokenStore 不会保存任何数据。
认证服务器安全配置类
/**
* 该配置类,主要处理用户名和密码的校验等事宜
*/
@Configuration
public class SecurityConfiger extends WebSecurityConfigurerAdapter {
/**
* 注册一个认证管理器对象到容器
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 密码编码对象(密码不进行加密处理)
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Autowired
private PasswordEncoder passwordEncoder;
/**
* 处理用户名和密码验证事宜
* 1)客户端传递username和password参数到认证服务器
* 2)一般来说,username和password会存储在数据库中的用户表中 * 3)根据用户表中数据,验证当前传递过来的用户信息的合法性
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 在这个方法中就可以去关联数据库了,当前我们先把用户信息配置在内存中
// 实例化一个用户对象(相当于数据表中的一条用户记录)
UserDetails user = new User("admin","123456",new ArrayList<>());
auth.inMemoryAuthentication().withUser(user).passwordEncoder(passwordEncoder);
}
}
endpoint: /oauth/token
获取token携带的参数
- client_id:客户端id
- client_secret:客户单密码
- grant_type:指定使用哪种颁发类型,
- password username:用户名
- password:密码
2) 校验token: http://localhost:9999/oauth/check_token?token=a9979518-838 c-49ff-b14a-ebdb7fde7d08
刷新token: http://localhost:9999/oauth/token?grant_type=refresh_token&c lient_id=client_lagou&client_secret=abcxyz&refresh_token=8b640340-30a3- 4307-93d4-ed60cc54fbc8
资源服务器(希望访问被认证的微服务)Resource Server配置
@Configuration
@EnableResourceServer // 开启资源服务器功能
@EnableWebSecurity // 开启web访问安全
public class ResourceServerConfiger extends ResourceServerConfigurerAdapter {
private String sign_key = "lagou123"; // jwt签名密钥
/**
* 该方法用于定义资源服务器向远程认证服务器发起请求,进行token校验等事宜
* @param resources
* @throws Exception
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
// 设置当前资源服务的资源id
resources.resourceId("autodeliver");
// 定义token服务对象(token校验就应该靠token服务对象)
RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
// 校验端点/接口设置
remoteTokenServices.setCheckTokenEndpointUrl("http://localh ost:9999/oauth/check_token");
// 携带客户端id和客户端安全码
remoteTokenServices.setClientId("client_lagou");
remoteTokenServices.setClientSecret("abcxyz");
// 别忘了这一步
resources.tokenServices(remoteTokenServices);
}
/**
* 场景:一个服务中可能有很多资源(API接口)
* 某一些API接口,需要先认证,才能访问
* 某一些API接口,压根就不需要认证,本来就是对外开放的接口
* 我们就需要对不同特点的接口区分对待(在当前configure方法中完成),设置是否需要经过认证
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http // 设置session的创建策略(根据需要创建即可)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.authorizeRequests()
.antMatchers("/autodeliver/**").authenticated() // autodeliver为前缀的请求需要认证
.antMatchers("/demo/**").authenticated() // demo为前缀的请求需要认证
.anyRequest().permitAll(); // 其他请求不认证
}
}
思考:当我们第一次登陆之后,认证服务器颁发token并将其存储在认证服 务器中,后期我们访问资源服务器时会携带token,资源服务器会请求认证 服务器验证token有效性,如果资源服务器有很多,那么认证服务器压力会 很大…
另外,资源服务器向认证服务器check_token,获取的也是用户信息 UserInfo,能否把用户信息存储到令牌中,让客户端一直持有这个令牌,令 牌的验证也在资源服务器进行,这样避免和认证服务器频繁的交互…
我们可以考虑使用 JWT 进行改造,使用JWT机制之后资源服务器不需要访问认证服务器…
2.3 JWT改造统一认证授权中心的令牌存储机制
HMACSHA256( base64UrlEncode(header) + “.” + base64UrlEncode(payload), secret)
认证服务器端JWT改造(改造主配置类)
/*
* 该方法用于创建tokenStore对象(令牌存储对象) token以什么形式存储
*/
public TokenStore tokenStore(){
//return new InMemoryTokenStore();
// 使用jwt令牌
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 返回jwt令牌转换器(帮助我们生成jwt令牌的)
* 在这里,我们可以把签名密钥传递进去给转换器对象 * @return
*/
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(sign_key); // 签名密钥
jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key)); // 验证时使用的密钥,和签名密钥保持一致
return jwtAccessTokenConverter;
}
不需要和远程认证服务器交互,添加本地tokenStore
@Configuration
@EnableResourceServer // 开启资源服务器功能
@EnableWebSecurity // 开启web访问安全
public class ResourceServerConfiger extends ResourceServerConfigurerAdapter {
private String sign_key = "lagou123"; // jwt签名密钥
/**
* 该方法用于定义资源服务器向远程认证服务器发起请求,进行token校验等事宜 * @param resources
* @throws Exception
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
// jwt令牌改造
resources.resourceId("autodeliver").tokenStore(tokenStore()).state less(true);// 无状态设置
}
@Override
public void configure(HttpSecurity http) throws Exception {
http // 设置session的创建策略(根据需要创建即可)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF _REQUIRED)
.and()
.authorizeRequests()
.antMatchers("/autodeliver/**").authenticated() //autodeliver为前缀的请求需要认证
.antMatchers("/demo/**").authenticated() // demo为前缀的请求需要认证
.anyRequest().permitAll(); // 其他请求不认证
}
/*
* 该方法用于创建tokenStore对象(令牌存储对象) token以什么形式存储
*/
public TokenStore tokenStore(){
// 使用jwt令牌
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 返回jwt令牌转换器(帮助我们生成jwt令牌的)
* 在这里,我们可以把签名密钥传递进去给转换器对象 * @return
*/
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(sign_key); // 签名密钥
jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key)); // 验证时使用的密钥,和签名密钥保持一致
return jwtAccessTokenConverter;
}
}
从数据库加载Oauth2客户端信息
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for oauth_client_details -- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(48) NOT NULL,
`resource_ids` varchar(256) DEFAULT NULL,
`client_secret` varchar(256) DEFAULT NULL,
`scope` varchar(256) DEFAULT NULL,
`authorized_grant_types` varchar(256) DEFAULT NULL,
`web_server_redirect_uri` varchar(256) DEFAULT NULL,
`authorities` varchar(256) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additional_information` varchar(4096) DEFAULT NULL,
`autoapprove` varchar(256) DEFAULT NULL,
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of oauth_client_details
-- ----------------------------
BEGIN;
INSERT INTO `oauth_client_details` VALUES ('client_lagou123', 'autodeliver,resume', 'abcxyz', 'all', 'password,refresh_token', NULL, NULL, 7200, 259200, NULL, NULL);
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
认证服务器主配置类改造
@Autowired
private DataSource dataSource;
/**
* 客户端详情配置,
* 比如client_id,secret
* 当前这个服务就如同QQ平台,拉勾网作为客户端需要qq平台进行登录授权认证
等,提前需要到QQ平台注册,QQ平台会给拉勾网
* 颁发client_id等必要参数,表明客户端是谁 * @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
super.configure(clients);
// 从内存中加载客户端详情改为从数据库中加载客户端详情
clients.withClientDetails(createJdbcClientDetailsService());
}
@Bean
public JdbcClientDetailsService createJdbcClientDetailsService() {
JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
return jdbcClientDetailsService;
}
import com.lagou.edu.dao.UsersRepository;
import com.lagou.edu.pojo.Users;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service; import java.util.ArrayList;
@Service
public class JdbcUserDetailsService implements UserDetailsService {
@Autowired
private UsersRepository usersRepository;
/**
* 根据username查询出该用户的所有信息,封装成UserDetails类型的对象返 回,至于密码,框架会自动匹配
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws
UsernameNotFoundException {
Users users = usersRepository.findByUsername(username);
return new User(users.getUsername(),users.getPassword(),new ArrayList<>());
}
}
// 使用自定义的用户详情服务对象
@Autowired
private JdbcUserDetailsService jdbcUserDetailsService;
/**
* 处理用户名和密码验证事宜
* 1)客户端传递username和password参数到认证服务器
* 2)一般来说,username和password会存储在数据库中的用户表中
* 3)根据用户表中数据,验证当前传递过来的用户信息的合法性
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 在这个方法中就可以去关联数据库了,当前我们先把用户信息配置在内存中
// 实例化一个用户对象(相当于数据表中的一条用户记录)
/*UserDetails user = new User("admin","123456",new ArrayList<>());
auth.inMemoryAuthentication() .withUser(user).passwordEncoder(passwordEncoder);*/
auth.userDetailsService(jdbcUserDetailsService).passwordEncoder(pas swordEncoder);
}
认证服务器生成JWT令牌时存入扩展信息(比如clientIp) 继承DefaultAccessTokenConverter类,重写convertAccessToken方法存入扩展信息
@Component
public class LagouAccessTokenConvertor extends DefaultAccessTokenConverter {
@Override
public Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
// 获取到request对象
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.getRequestAttributes())).getRequest();
// 获取客户端ip(注意:如果是经过代理之后到达当前服务的话,那么这 种方式获取的并不是真实的浏览器客户端ip)
String remoteAddr = request.getRemoteAddr();
Map<String, String> stringMap = (Map<String, String>) super.convertAccessToken(token, authentication);
stringMap.put("clientIp",remoteAddr);
return stringMap;
}
}
资源服务器取出 JWT 令牌扩展信息
资源服务器也需要自定义一个转换器类,继承DefaultAccessTokenConverter,重 写extractAuthentication提取方法,把载荷信息设置到认证对象的details属性中
@Component
public class LagouAccessTokenConvertor extends DefaultAccessTokenConverter {
@Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
OAuth2Authentication oAuth2Authentication = super.extractAuthentication(map);
oAuth2Authentication.setDetails(map); // 将map放入认证对象 中,认证对象在controller中可以拿到
return oAuth2Authentication;
}
}
业务类比如Controller类中,可以通过 SecurityContextHolder.getContext().getAuthentication()获取到认证对象,进一步 获取到扩展信息
3、SCA
SCA 是由一些阿里巴巴 的开源组件和云产品组成的,2018年,Spring Cloud Alibaba 正式入住了 Spring Cloud 官方孵化器。
Nacos(服务注册中心、配置中心)
Sentinel哨兵(服务的熔断、限流等)
Dubbo RPC/LB
Seata分布式事务解决方案
3.1 Nacos
Nacos (Dynamic Naming and Configuration Service)是阿里巴巴开源的一个针 对微服务架构中服务发现、配置管理和服务管理平台。
Nacos就是注册中心+配置中心的组合(Nacos=Eureka+Config+Bus)
- 服务发现与健康检查
- 动态配置管理
- 动态DNS服务
- 服务和元数据管理(管理平台的⻆度,nacos也有一个ui⻚面,可以看到注册的 服务及其实例信息(元数据信息)等),动态的服务权重调整、动态服务优雅下 线,都可以去做
官网: https://nacos.io 下载地址: https://github.com/alibaba/Nacos
执行命令启动 sh startup.sh -m standalone
访问nacos管理界面: http://127.0.0.1:8848/nacos/#/login (默认端口8848, 账号和密码 nacos/nacos)
保护阈值:可以设置为0-1之间的浮点数,它其实是一个比例值(当前服务健康实例 数/当前服务总实例数)
场景:
一般流程下,nacos是服务注册中心,服务消费者要从nacos获取某一个服务的可用 实例信息,对于服务实例有健康/不健康状态之分,nacos在返回给消费者实例信息 的时候,会返回健康实例。这个时候在一些高并发、大流量场景下会存在一定的问题
如果服务A有100个实例,98个实例都不健康了,只有2个实例是健康的,如果nacos 只返回这两个健康实例的信息的话,那么后续消费者的请求将全部被分配到这两个 实例,流量洪峰到来,2个健康的实例也扛不住了,整个服务A 就扛不住,上游的微 服务也会导致崩溃,,,产生雪崩效应。
保护阈值的意义在于: 当服务A健康实例数/总实例数 < 保护阈值 的时候,说明健康实例真的不多了,这个时候保护阈值会被触发(状态true)
nacos将会把该服务所有的实例信息(健康的+不健康的)全部提供给消费者,消费 者可能访问到不健康的实例,请求失败,但这样也比造成雪崩要好,牺牲了一些请 求,保证了整个系统的一个可用。
注意:阿里内部在使用nacos的时候,也经常调整这个保护阈值参数。
SCA依赖
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 引入nacos客户端依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
application.yml修改,添加nacos配置信息
spring:
application:
nacos:
discovery:
################ 配置nacos server地址
server-addr: 127.0.0.1:8848
Namespace命名空间、Group分组、集群这些都是为了进行归类管理,把服务和配置文件进行归类,归类之后就可以实现一定的效果,比如隔离 比如,对于服务来说,不同命名空间中的服务不能够互相访问调用
Namespace:命名空间,对不同的环境进行隔离,比如隔离开发环境、测试环境和 生产环境
Group:分组,将若干个服务或者若干个配置集归为一组,通常习惯一个系统归为 一个组
Service:某一个服务,比如简历微服务
DataId:配置集或者可以认为是一个配置文件
Namespace + Group + Service 如同 Maven 中的GAV坐标,GAV坐标是为了锁定 Jar,二这里是为了锁定服务
Namespace + Group + DataId 如同 Maven 中的GAV坐标,GAV坐标是为了锁定 Jar,二这里是为了锁定配置文件
Nacos 默认使用嵌入式数据库进行数据存储,它支持改为外部Mysql存储
- 新建数据库 nacos_config,数据库初始化脚本文件 ${nacoshome}/conf/nacos-mysql.sql
- 修改${nacoshome}/conf/application.properties,增加Mysql数据源配置
spring.datasource.platform=mysql
### Count of DB:
db.num=2
### Connect URL of DB:
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&au toReconnect=true
db.user=root
db.password=123456
db.url.1=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&au toReconnect=true
db.user=root
db.password=123456
nacos集群配置
之前:Spring Cloud Config + Bus 需要创建Config Server 配置中心—>从Github上去下载配置信息
有Nacos之后,分布式配置就简单很多 Github不需要了(配置信息直接配置在Nacos server中),Bus也不需要了(依然可以完成动态刷新)
在我们的微服务中开启 Nacos 配置管理
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
微服务中如何锁定 Nacos Server 中的配置文件(dataId)
通过 Namespace + Group + dataId 来锁定配置文件,Namespace不指定就默认public,Group不指定就默认 DEFAULT_GROUP
# ${prefix}-${spring.profile.active}.${file-extension}
# prefix 默认为 spring.application.name 的值,也可以通过配置项 spring.cloud.nacos.config.prefix 来配置。
# spring.profile.active 即为当前环境对应的 profile。 注意:当spring.profile.active 为空时,对应的连接符 - 也将不存在,dataId 的拼 接格式变成 ${prefix}.${file-extension}
# file-exetension 为配置内容的数据格式,可以通过配置项 spring.cloud.nacos.config.file-extension 来配置。目前只支持 properties 和 yaml 类型。
spring:
cloud:
nacos:
discovery:
# 集群中各节点信息都配置在这里(域名-VIP-绑定映射到各个实例的地址信息)
server-addr: 127.0.0.1:8848
config:
server-addr: 127.0.0.1:8848
namespace: f965f7e4-7294-40cf-825c-ef363c269d37
group: DEFAULT_GROUP
file-extension: yaml
通过 Spring Cloud 原生注解 @RefreshScope 实现配置自动更新
一个微服务希望从配置中心Nacos server中获取多个dataId的配置信息,可 以的,扩展多个dataId
# nacos配置 cloud:
spring:
nacos:
discovery:
# 集群中各节点信息都配置在这里(域名-VIP-绑定映射到各个实例的地址信息)
server-addr: 127.0.0.1:8848,127.0.0.1:8849,127.0.0.1:8850
# nacos config 配置
config:
server-addr: 127.0.0.1:8848,127.0.0.1:8849,127.0.0.1:8850
# 锁定server端的配置文件(读取它的配置项)
namespace: 07137f0a-bf66-424b-b910-20ece612395a # 命名空间id
group: DEFAULT_GROUP # 默认分组就是DEFAULT_GROUP,如果使用默认 分组可以不配置
file-extension: yaml #默认properties
# 根据规则拼接出来的dataId效果:lagou-service-resume.yaml
ext-config[0]:
data-id: abc.yaml
group: DEFAULT_GROUP
refresh: true #开启扩展dataId的动态刷新
ext-config[1]:
data-id: def.yaml
group: DEFAULT_GROUP
refresh: true #开启扩展dataId的动态刷新
优先级: 根据规则生成的dataId > 扩展的dataId(对于扩展的dataId,[n] n越大优 先级越高)
3.2 Sentinel
分布式系统的流量防卫兵
Sentinel是一个面向云原生微服务的流量控制、熔断降级组件。
下载地址:https://github.com/alibaba/Sentinel/releases 我们使用v1.7.1
启动:java -jar sentinel-dashboard-1.7.1.jar &
用户名/密码:sentinel/sentinel
<!--sentinel 核心环境 依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
sentinel:
transport:
dashboard: 127.0.0.1:8080 # sentinel dashboard/console 地址 的话,控制台定义的一些限流等规则才能发送传递过来,
port: 8719 # sentinel会在该端口启动http server,那么这样 #如果8719端口被占用,那么会依次+1
management:
endpoints:
web:
exposure:
include: "*" # 暴露健康接口的细节
endpoint:
health:
show-details: always
Sentinel 流量规则模块
系统并发能力有限,比如系统A的QPS支持1个,如果太多请求过来,那么A就应该进 行流量控制了,比如其他请求直接拒绝
资源名: 默认请求路径
针对来源: Sentinel可以针对调用者进行限流,填写微服务名称,默认default(不区分来源)
阈值类型/单机阈值 QPS:(每秒钟请求数量)当调用该资源的QPS达到阈值时进行限流
线程数: 当调用该资源的线程数达到阈值的时候进行限流(线程处理请求的时候, 如果说业务逻辑执行时间很⻓,流量洪峰来临时,会耗费很多线程资源,这些线程 资源会堆积,最终可能造成服务不可用,进一步上游服务不可用,最终可能服务雪崩)
是否集群: 是否集群限流
流控模式:
- 直接:资源调用达到限流条件时,直接限流
- 关联:关联的资源调用达到阈值时候限流自己
- 链路:只记录指定链路上的流量
流控效果: 快速失败:直接失败,抛出异常
Warm Up:根据冷加载因子(默认3)的值,从阈值/冷加载因子,经过预热时⻓, 才达到设置的QPS阈值
排队等待:匀速排队,让请求匀速通过,阈值类型必须设置为QPS,否则无效
流控模式之关联限流
关联的资源调用达到阈值时候限流自己,比如用户注册接口,需要调用身份证校验 接口(往往身份证校验接口),如果身份证校验接口请求达到阈值,使用关联,可 以对用户注册接口进行限流。
流控模式之链路限流
链路指的是请求链路(调用链), 链路模式下会控制该资源所在的调用链路入口的流量。需要在规则中配置入口资源,即该调用链路入口的上下文名称。
上图中来自入口 Entrance1 和 Entrance2 的请求都调用到了资源 NodeA , Sentinel 允许只根据某个调用入口的统计信息对资源限流。比如链路模式下设置入口资源为 Entrance1 来表示只有从入口 Entrance1 的调用才会记录到 NodeA 的限流统计当中,而不关心经 Entrance2 到来的调用。
流控效果之Warm up
当系统⻓期处于空闲的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮,比如电商网站的秒杀模块。
通过 Warm Up 模式(预热模式),让通过的流量缓慢增加,经过设置的预热时间以后,到达系统处理请求速率的设定值。
Warm Up 模式默认会从设置的QPS 阈值的 1/3 开始慢慢往上增加至 QPS 设置值。
流控效果之排队等待
排队等待模式下会严格控制请求通过的间隔时间,即请求会匀速通过,允许部分请 求排队等待,通常用于消息队列削峰填谷等场景。需设置具体的超时时间,当计算 的等待时间超过超时时间时请求就会被拒绝。
很多流量过来了,并不是直接拒绝请求,而是请求进行排队,一个一个匀速通过 (处理),请求能等就等着被处理,不能等(等待时间>超时时间)就会被拒绝
例如,QPS 配置为 5,则代表请求每 200 ms 才能通过一个,多出的请求将排队等 待通过。超时时间代表最大排队时间,超出最大排队时间的请求将会直接被拒绝。 排队等待模式下,QPS 设置值不要超过 1000(请求间隔 1 ms)。
Sentinel 降级规则模块
Sentinel 降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比 例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源 而导致级联错误。当资源被降级后,在接下来的降级时间窗口之内,对该资源的调 用都自动熔断.
Sentinel不会像Hystrix那样放过一个请求尝试自我修复,就是明明确确按照时间窗 口来,熔断触发后,时间窗口内拒绝请求,时间窗口后就恢复。
策略
-
RT(平均响应时间 )
当 1s 内持续进入 >=5 个请求,平均响应时间超过阈值(以 ms 为单位),那么 在接下的时间窗口(以 s 为单位)之内,对这个方法的调用都会自动地熔断(抛 出 DegradeException)。注意 Sentinel 默认统计的 RT 上限是 4900 ms,超出 此阈值的都会算作 4900 ms,若需要变更此上限可以通过启动配置项 - Dcsp.sentinel.statistic.max.rt=xxx 来配置。
-
异常比例
当资源的每秒请求量 >= 5,并且每秒异常总数占通过量的比值超过阈值之后, 资源进入降级状态,即在接下的时间窗口(以 s 为单位)之内,对这个方法的调 用都会自动地返回。异常比率的阈值范围是 [0.0, 1.0] ,代表 0% - 100%。
-
异常数
当资源近 1 分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口 是分钟级别的,若 timeWindow 小于 60s,则结束熔断状态后仍可能再进入熔 断状态。时间窗口 >= 60s
Sentinel 自定义兜底逻辑
@SentinelResource注解类似于Hystrix中的@HystrixCommand注解
@SentinelResource注解中有两个属性需要我们进行区分,
- blockHandler属性用来指定不满足Sentinel规则的降级兜底方法,
- fallback属性用于指定Java运行时异常兜 底方法
/**
* @SentinelResource
value:定义资源名
blockHandlerClass:指定Sentinel规则异常兜底逻辑所在class类
blockHandler:指定Sentinel规则异常兜底逻辑具体哪个方法
fallbackClass:指定Java运行时异常兜底逻辑所在class类
fallback:指定Java运行时异常兜底逻辑具体哪个方法
*/
@GetMapping("/checkState/{userId}")
@SentinelResource(value = "findResumeOpenState",
blockHandlerClass = SentinelFallbackClass.class,
blockHandler = "handleException",
fallback = "handleError",
fallbackClass = SentinelFallbackClass.class)
public Integer findResumeOpenState(@PathVariable Long userId) {
// 模拟降级:
/*try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
// 模拟降级:异常比例
int i = 1/0;
Integer defaultResumeState = resumeServiceFeignClient.findDefaultResumeState(userId);
return defaultResumeState;
}
自定义兜底逻辑类 注意:兜底类中的方法为static静态方法
public class SentinelHandlersClass {
// 整体要求和当时Hystrix一样,这里还需要在形参最后添加 BlockException参数,用于接收异常
// 注意:方法是静态的
public static Integer handleException(Long userId, BlockException blockException) {
return -100;
}
public static Integer handleError(Long userId) {
return -500;
}
}
目前,Sentinel Dashboard中添加的规则数据存储在内存,微服务停掉规则数据就消失,在生产环境下不合适。我们可以将Sentinel规则数据持久化到Nacos配置中心,让微服务从Nacos获取规则数据。
<!-- Sentinel支持采用 Nacos 作为规则配置数据源,引入该适配依赖 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
spring:
application:
name: lagou-service-autodeliver
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848,127.0.0.1:8849,127.0.0.1:8850
sentinel:
transport:
dashboard: 127.0.0.1:8080 # sentinel dashboard/console地址
post: 8719 # sentinel会在该端口启动http server,那么这样的话,控制台定义的一些限流等规则才能发送传递过来,
#如果8719端口被占用,那么会依次+1
# Sentinel Nacos数据源配置,Nacos中的规则会自动同步到sentinel控 制台的流控规则中
datasource:
# 此处的flow为自定义数据源名
flow: # 流控规则
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
data-id: ${spring.application.name}-flow-rules
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow # 类型来自RuleType类
degrade:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
data-id: ${spring.application.name}-degrade-rules
groupId: DEFAULT_GROUP
data-type: json
rule-type: degrade # 类型来自RuleType类
Nacos Server中添加对应规则配置集(public命名空间—>DEFAULT_GROUP中 添加)
流控规则配置集 lagou-service-autodeliver-flow-rules
[
{
"resource":"findResumeOpenState", # 资源名称
"limitApp":"default", # 来源应用
"grade":1, # 阈值类型 0 线程数 1 QPS
"count":1, # 单机阈值
"strategy":0, # 流控模式,0 直接 1 关联 2 链路
"controlBehavior":0, # 流控效果,0 快速失败 1 Warm Up 2 排队等待
"clusterMode":false # true/false 是否集群
}
]
降级规则配置集 lagou-service-autodeliver-degrade-rules
[
{
"resource":"findResumeOpenState", # 资源名称
"grade":2, # 降级策略 0 RT 1 异常比例 2 异常数
"count":1, # 阈值
"timeWindow":5 # 时间窗
}
]
3.3 Nacos + Sentinel + Dubbo 三剑合璧
需要删除工程中的热部署依赖
1) 服务提供者工程
pom文件添加spring cloud + dubbo整合的依赖,同时添加dubbo服务接口 工程依赖
<!--spring cloud alibaba dubbo 依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-dubbo</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-apache-dubbo-adapter</artifactId>
</dependency>
在service的实现类上添加 dubbo的@Service 注解
spring:
main:
# Spring Boot 2.1 需要设定
allow-bean-definition-overriding: true
dubbo:
scan:
# dubbo 服务扫描基准包
base-packages: com.lagou.edu.service.impl
protocol:
# dubbo 协议
name: dubbo
# dubbo 协议端口( -1 表示自增端口,从 20880 开始)
port: -1
registry:
# 挂载到 Spring Cloud 的注册中心
address: spring-cloud://localhost
2) 服务消费者工程
pom.xml添加内容和服务提供者一样
spring:
main:
allow-bean-definition-overriding: true
dubbo:
registry:
# 挂载到 Spring Cloud 注册中心
address: spring-cloud://localhost cloud:
# 订阅服务提供方的应用列表,订阅多个服务提供者使用 "," 连接
subscribed-services: lagou-service-resume