微服务
什么是微服务
背景和SOA
单体构架
- 例如商场系统,在这个工程中创建不同的Service实现商城系统中不同的业务场景,如账户、商品、库存等
- 一个 jar包 或者 war包 里面包含一个应用的所有功能,称这种架构为 单体架构
- 这种架构足够简单,能够快速开发和上线,对于项目初期用户量不大的情况,这样的架构足以支撑业务的正常运行
集群和垂直化
背景:
- 用户量越来越大,网站的访问量不断增大,导致后端服务器的负载越来越高
- 当服务器的负载越来越高的时候,如果不进行任何处理,用户在网站上操作的响应会越来越慢,甚至出现无法访问的情况
- 用户量大了,产品需要满足不同用户的需求,使得业务场景越来越多并且越来越复杂
- 业务场景越多越复杂,意味着jar包中的代码量会持续上升,并且各个业务代码之间的耦合度也会越来越高,后期的代码维护和版本发布的测试和上线也会很困难
优化思路:
- 横向增加服务器,把单台机器编程多台机器的集群
- 按照业务的垂直领域进行拆分,减少业务的耦合度,以及降低单个 jar包带来的伸缩性问题
SOA
背景:
-
用户执行下单操作,业务逻辑会先检查商品的库存,库存足够的情况下才会提交订单,那么检查库存的逻辑是放在订单子系统还是库存子系统呢?在整个系统中,一定会存在非常多类似的共享业务的场景,这些业务场景的逻辑肯定会被重复的创建,从而产生非常多的冗余的业务代码
- 将这些共享业务逻辑抽离出来形成可重用的服务
-
在一个集团公司下有很多子公司,每个子公司都有自己的业务模式和信息沉淀,各个子公司之间不进行交互和共享,彼此之间形成了信息孤岛,价值无法最大化
-
SOA是面向服务的架构,核心是把一些 通用的、会被多个上层服务调用的共享业务提取成独立的基础服务,被提取出来的共享服务相对来说比较独立,并且可以重用
-
在SOA中,服务是最核心的抽象手段,业务被划分为一些 粗粒度的业务服务和业务流程
-
采用 ESB(企业服务总线) 来作为系统和服务之间的通信桥梁,ESB本身还提供服务地址的管理、不同系统之间的协议转化和数据格式转化等。调用端不需要关心目标服务的位置,从而使得服务之间的交互是动态的
- SOA主要解决的问题是:信息孤岛、共享业务的重用
SOA和微服务架构的不用
- SOA关注的是 服务的重用性 及解决 信息孤岛问题
- 微服务关注的是解耦,解耦和可重用性本质上是有区别的
- 解耦是降低业务之间的耦合度,可重用性关注的是服务的复用
- 微服务会更多地关注在 DevOps的持续交付上,因为服务粒度细化之后使得开发运维变得更加重要,因此微服务与容器化技术的结合更加紧密
微服务架构
- 随着业务的发展,用户量和业务复杂度逐渐增加,系统为了支撑更大的流量需要做很多优化
- 在硬件方面,升级服务器配置提升性能
- 在软件方面,会采用微服务架构、对业务服务进行微服务化拆分、水平扩容等来提升系统性能,以及解决业务的复杂性问题
- 微服务将 每个具体的业务服务构成可独立运行的微服务,每个微服务只关注某个特定的功能,服务之间采用轻量级通信机制
REST API
进行通信 - 微服务拆分到多大的粒度没有统一的标准,微服务的粒度越小,服务独立性带来的好处就越多,但是管理大量的微服务也会越复杂
微服务架构的优缺点
优点
- 复杂度可控: 通过对共享业务服务更细粒度的拆分,一个服务只需要关注一个特定的业务领域,并通过良好的接口清晰表达服务边界。由于体积小、复杂度低,开发、维护会更加简单
- **容错性:**在微服务架构中,如果某一个服务发生故障,可以使故障隔离在单个服务中,其他服
务可以通过重试、降级等机制来实现应用层面的容错 - **独立部署:**当某个微服务发生变更时不需要重新编译部署整个应用,并且单个微服务的代码量比较小使得发布更加高效
- **可扩展性更强:**可以根据每个微服务的性能要求和业务特点来对服务进行灵活扩展,比如通过增加
单个服务的集群规模,提升部署了该服务的节点的硬件配置 - **技术选型更灵活:**每个微服务都由不同的团队来维护,所以可以结合业务特性自由选择技术栈
缺点
-
**分布式架构的复杂性:**微服务本身构建的是一个分布式系统,分布式系统涉及服务之间的远程通信,而网络通信中网络的延迟和网络故障是无法避免的,从而增加了应用程序的复杂度
-
服务监控: 在一个单体架构中很容易实现服务的监控,因为所有的功能都在一个服务中。在微服务架构中,服务监控开销会非常大,不仅要对整个链路进行监控,还需要对每一个微服务都实现一套类似单体架构的监控
-
**故障排查:**一次请求可能会经历多个不同的微服务的多次交互,交互的链路可能会比较长,每个微服务会产生自己的日志,在这种情况下如果出现一个故障,开发人员定位问题的根源会比较困难
-
服务依赖:微服务数量增加之后,各个服务之间会存在更多的依赖关系,使得系统整体更为复杂
- 假设需要修改服务A、B、C ,而A依赖B , B依赖C。在单体式应用中,只需要改变相关模块,整合变化,再部署就好了。对比之下,微服务架构模式就需要考虑相关改变对不同服务的影响。比如,需要更新服务C ,然后是B,最后才是A,许多改变一般只影响一个服务,需要协调多服务的改变很少
-
**运维成本:**在微服务中,需要保证几百个微服务的正常运行,对于运维的挑战是巨大的
- 比如单个服务流量激增时如何快速扩容、服务拆分之后导致故障点增多如何处理、如何快速部署和统一管理众多的服务等
微服务架构的技术挑战
两大特性:高可用性、高可扩展性
服务间通信
- 服务治理、服务间调用、负载均衡
服务容错、异常排查
- 流量整形、降级熔断、服务链追踪
分布式能力建设
- 微服务网关、分布式事物、消息驱动、配置管理
服务注册与发现
为什么需要服务注册?
在微服务架构下,一个业务服务会被拆分成多个微服务,各个服务之间相互通信完成整体的功能
另外,为了避免单点故障,微服务都会采取集群方式的高可用部署,集群规模越大,性能也会越高
需要服务注册的原因
服务消费者要去调用多个服务提供者组成的集群
-
首先,服务消费者需要在本地配置文件中维护服务提供者集群的每个节点的请求地址
-
其次,服务提供者集群中如果某个节点下线或者宕机,服务消费者的本地配置中需要同步删除这个节点的请求地址,防止请求发送到已宕机的节点上造成请求失败
服务注册中心的功能
- 服务地址的管理
- 服务注册
- 服务动态通知
Nacos的特性
Nacos致力于解决微服务中的 服务注册与发现、统一配置等问题。它提供了一组简单易用的特性集,帮助开发者快速实现动态服务发现、服务配置、服务元数据及流量管理
服务发现和服务健康监测
- Nacos支持基于DNS和RPC的服务发现。服务提供者使用原生SDK、OpenAPI或一个独立的AgentTODO注册Service后,服务消费者可以使用DNS或HTTP&API查找和发现服务
- Nacos提供对服务的实时的健康检查,阻止向不健康的主机或服务实例发送请求。Nacos支持传输层 ( PING或TCP )和应用层(如HTTP、MySQL、用户自定义)的健康检查。对于复杂的云环境和网络拓扑环境中(如VPC、边缘网络等) 服务的健康检查, Nacos提供了agent上报和服务端主动检测两种健康检查模式。Nacos还提供了统一的健康检查仪表盘,帮助用户根据健康状态管理服务的可用性及流量
动态配置服务
- 业务服务一般都会维护一个本地配置文件,然后把一些常量配置到这个文件中
存在的问题
- 配置需要变更时要 重新部署应用
动态配置服务的优点
- 动态配置服务可以以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置,可以使配置管理变得更加高效和敏捷
- 配置中心化管理让实现无状态服务变得更简单,让服务按需弹性扩展变得更容易
其他优点
- Nacos提供了一个简洁易用的UI帮助用户管理所有服务和应用的配置,还提供了包括配置版本跟踪、金丝雀发布、一键回滚配置及客户端配置更新状态跟踪在内的一系列开箱即用的配置管理特性,帮助用户更安全地在生产环境中管理配置变更,降低配置变更带来的风险
动态DNS服务
- 动态DNS服务支持权重路由,让开发者更容易地实现中间层负载均衡、更灵活的路由策略、流量控制,以及数据中心内网的简单DNS解析服务
服务及其元数据管理
- Nacos可以使开发者从微服务平台建设的视角管理数据中心的所有服务及元数据,包括管理服务的描述、生命周期、服务的静态依赖分析、服务的健康状态、服务的流量管理、路由及安全策略、服务的SLA及最重要的metrics统计数据
这种方式在某些场景中会存在问题,比如配置需要变更时要重新部署应用。而动态配置服务可以以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置,可以使配置管理变得更加高效和敏捷。配置中心化管理让实现无状态服务变得更简单,让服务按需弹性扩展变得更容易。
SpringCloudGateWay
API网关的作用
API网关的作用
-
在客户端与服务端之间增加了一个API网关,所有的外部请求都会先经过网关这一层
-
网关层可以把后端的多个服务进行整合,然后提供唯一的业务接口 ,客户端只需要调用这个接口即可完成数据的获取及展示
- 服务的鉴权会分布在每个微服务中处理,客户端对于每个服务的调用都需要重复鉴权
- 在后端的微服务架构中,可能不同的服务采用的协议不同,比如有HTTP、RPC等。 客户端如果需要调用多个服务,需要对不同协议进行适配
网关还提供以下功能
- 针对所有请求进行统一鉴权、限流、熔断、日志
- 协议转化。针对后端多种不同的协议,在网关层统一处理后以HTTP对外提供服务 (用过Dubbo框架的读者应该知道,针对Dubbo服务还需要提供一个Web应用来进行协议转化)
- 统一错误码处理
- 请求转发,并且可以基于网关实现内、外网隔离
统一认证授权
统一认证授权包括两部分
- 客户端身份认证:主要用于判断当前用户是否为合法用户,一般的做法是使用账号和密码进行验证。当然,对于一些复杂的认证场景会采用加密算法来实现,比如公、私钥
- 访问权限控制:身份认证和访问权限一般是相互联系的,当身份认证通过后,就需要判断该用户是否有权限访问该资源,或者该用户的访问权限是否被限制了
在单体应用中,客户端身份认证及访问权限的控制比较简单,只需要在服务端通过session保存该用户信息即可
在微服务架构下,单体应用被拆分成多个微服务,鉴权的过程就会变得很复杂
- 首先要解决的问题是,原来单体应用中的session方式已经无法用于微服务场景
- 其次就是如何实现对每个微服务进行鉴权
解决方案
- 第一个问题,目前已经有非常多的成熟解决方案了 比如AccessToken、 Oauth (开放API)
- 第二个问题,可以把鉴权的功能抽离出一个统一认证服务,所有的微服务在被访问之前,先访问该认证服务进行鉴权。这种解决方案看似合理,但是在实际应用中,一个业务场景中可能会调用多个微服务,就会造成一次请求需要进行多次鉴权操作,增加了网络通信开销
增加API网关之后,在网关层进行请求拦截,获取请求中附带的用户身份信息,调用统一认证中心对请求进行身份认证,在确认了身份之后再检查是否有资源的访问权限
原理分析
spring:
cloud:
gateway:
routes:
- id: auth
uri: http://localhost:8080/say #访问地址
predicates:
- Path=/gateway/** #路径匹配
filters:
- StripPrefix=1 #跳过前缀
- id:自定义路由ID,保持唯一
- uri:目标服务地址,支持普通URI及 lb://应用注册服务名称。后者表示从注册中心获取集群服务地址
- predicates:路由条件,根据匹配的结果决定是否执行该请求路由
- filters:过滤规则,包含pre(前置)和post(后置)过滤
- 其中StripPrefix=1,表示Gateway根据该配置的值去掉URL路径中的部分前缀 (这里去掉一个前缀,即在转发的目标URI中去掉gateway)
工程流程
- Spring Cloud Gateway启动时基于Netty Server 监听一个指定的端口 (该端口可以通过server port属性自定义)
- 当客户端发送一个请求到网关时,网关会根据一系列Predicate的匹配结果来决定访问哪个Route路由,然后根据过滤器链进行请求的处理
- 过滤器链可以在请求发送到 服务器之前 和 之后执行,也就是首先执行Pre过滤器链,然后将请求转发到服务器,最后执行Post过滤器链
Predicate
Predicate
- ZonedDateTime
- BeforeRoutePredicateFactory
- AfterRoutePredicateFactory
- BetweenRoutePredicateFactory
- Cookie
- CookieRouterPredicateFactory
- Header
- HeaderRouterPredicateFactory
- CloudFoundryRouteServiceRoutePredicateFactory
- Host
- HostRouterPredicateFactory
- Method
- MethodRouterPredicateFactory
- Path
- PathRouterPredicateFactory
- Query
- QueryPathRouterPredicateFactory
- RemoteAddr
- RemoteRouterPredicateFactory
- WeightRouterPredicateFactory
- ReadBodyPathRouterPredicateFactory
时间规则
- 请求在指定日期之前,对应 BeforeRoutePredicateFactory
- 请求在指定日期之后,对应 AfterRoutePredicateFactory
- 请求在指定的两个日期之间,对应BetweenRoutePredicateFactory
// 希望在2021年9月22号之后发生的请求都路由到www.baidu.com
spring:
cloud:
gateway:
routes:
- id: after_route
uri: http://www.baidu.com
predicates:
- After=2021-09-22T24:00:00.000+08:00[Asia/Shanghai]
-
配置的日期时间必须满足 ZonedDateTime 的格式
-
年月日与时分秒用 T 分隔,+08:00是和UTC相差的时间,最后的[Asia/Shanghai ]是所在的时间地区 2021-09-22T24:00:00.000+08:00[Asia/Shanghai]
Cookie匹配路由
判断请求中携带的Cookie是否匹配配置的规则
spring:
cloud:
gateway:
routes:
- id: cookie_route
uri: http://xx.com
predicates:
- Cookie=chocolate, mic
当前请求需要携带一个name=chocolate , 并且value需要通过正则表达式匹配mic,才能路由到 http://xx.com
Header匹配路由
判断请求中Header头消息对应的name和value与Predicate配置的值是否匹配,value也是正则匹配形式
spring:
cloud:
gateway:
routes:
- id: header_route
uri: http://example.com
predicates:
- Header=X-Request-Id, \d+
该配置中会匹配请求中Header头中的name=X-Request-Id ,并且value会根据正则表达式匹配 \d+ ,也就是
匹配1个以上的数字
Host匹配路由
HTTP 请求会携带一个 Host 字段 ,这个字段表示请求的服务器网址
匹配请求中的Host字段进行路由
spring:
cloud:
gateway:
routes:
- id: host_route
uri: http://xx.com
predicates:
- Host=**.somehost.com,**.anotherhost.com
-
Host可以配置一个列表 ,列表中的每个元素通过,分隔
-
在上述配置中,当前请求中Host的值符合
**.somehost.com**
,**anotherhost.com**
时,才会将请求路由到http://xx.com -
路径命名及匹配规则支持Ant Path ,比如www.somehost.com、test.somehost.com、www. anotherhost.com都符合该规则
请求方法匹配路由
根据HTTP请求的Method属性来匹配以实现路由
spring:
cloud:
gateway:
routes:
- id: method_route
uri: http://example.com
predicates:
- Method=GET,POST
该配置表示,如果HTTP请求的方法是GET或POST,都会路由到https://example.com
请求路径匹配路由
请求路径匹配路由是比较常见的路由匹配规则
spring:
cloud:
gateway:
routes:
- id: path_route
uri: http://xx.com
predicates:
- Path=/red/{segment},/blue/{segment}
-
${segment}是一种比较特殊的占位符,
/*
表示单层路径匹配,/**
表示多层路径匹配 -
上述配置规则中,匹配请求的URI为
/red/*
、/blue/*
时,才会转发到 http://xx.com
Filter
Filter 分为 Pre类型过滤器 和 Post类型过滤器
- Pre类型的过滤器在请求转发到后端微服务之前执行,在Pre类型过滤器链中可以做鉴权、限流等操
作 - Post类型的过滤器在请求执行完之后、将结果返回给客户端之前执行
GatewayFilter
GatewayFilter只会应用到单个路由或者一个分组的路由上
AddRequestParameter GatewayFilter Factory
- 对所有匹配的请求添加一个查询参数
- 这段配置中,会对所有请求增加foo=bar这个参数
spring:
cloud:
gateway:
routes:
- id: add_request_paramater_route
uri: http://xx.com
filters:
- AddRequestParamter=foo, bar
AddResponseHeader GatewayFilter Factory
- 对所有匹配的请求,在返回结果给客户端之前,在Header中添加相应的数据
- 这段配置中会在Response中添加Header头,key=X- Response- Foo,Value= Bar
spring:
cloud:
gateway:
routes:
- id: add_response_header_route
uri: http://xx.com
filters:
- AddResponseHeader=X-Response-Foo, Bar
RequestRateLimiter GatewayFilter Factory
- 该过滤器会对访问到当前网关的所有请求执行限流过滤,如果被限流,默认情况下会响应HTTP 429-Too
Many Requests - RequestRateLimiterGatewayilterFactory默认提供了RedisRateLimiter的限流实现,它采用令牌桶算法
来实现限流功能
spring:
cloud:
gateway:
routes:
- id: requestratelimiter_route
uri: http://xx.com
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
redis-rate-limiter过滤器有两个配置属性
- replenishRate:令牌桶中令牌的填充速度,代表允许每秒执行的请求数
- burstCapacity:令牌桶最多能够容纳的令牌数,表示每秒用户最大能够执行的请求数量
实现限制同一个IP的请求频次
//添加Jar包依赖, Redis的限流器基于Stripe实现
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
// 创建一个KeyResolver的实现类
@Service
public class IpAddressKeyResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
}
-
KeyResolver接口主要用于设置限流请求的key,实现该接口来指定需要对当前请求中的哪些因素进行流量控制。在上述代码中设置了HostAddress,表示根据请求IP来限流
-
KeyResolver的默认实现是PrincipalNameKeyResolver,它会从ServerWebExchange检索Principal
并调用Principal.getName -
默认情况下,如果KeyResolver没有获取到key,请求将被拒绝。可以通过下面这两个属性来调整
spring.cloud.gateway.filter.request-rate-limiter.denyEmptyKey,是否允许空的key
spring.cloud.gateway.filter.request-rate-limiter.emptyKeyStatus,当deny-empty-key=true时返回的HttpStatus,默认为FORBIDDEN (403 , “Forbidden”)
spring: cloud: gateway: routes: - id: define_filter uri: http://localhost:8080/say predicates: - Path=/gateway/** filters: # - name: GpDefine # args: # name: Gp_Mic - name: RequestRateLimiter args: denyEmptyKey: true emptyKeyStatus: SERVICE_UNAVAILABLE keyResolver: '#{@ipAddressKeyResolver}' redis-rate-limiter: replenishRate: 1 burstCapacity: 2 - StripPrefix=1 redis: host: 192.168.198.128 port: 6379 password: 123456
在上述配置中, keyResolver采用的是SpEL表达式按照名称来引用Bean,#{@ipAddressKeyResolver}表示引用name=ipAddressKeyResolver的Bean
- 通过测试工具访问网关即可看到限流的效果,默认响应HTTP ERROR 429
- Redis中也会生成相应的key
Spring Cloud Gateway目前默认只实现了基于Redis的Ratelimiter限流方式,如果使用其他方式实现限流,它也提供了扩展功能,实现方式类似于keyResolver
- 创建自定义限流器,实现AbstractRateLimiter接口
- 指定自定义限流器, rateLimiter : # {@defineRateLimiter}
Retry GatewayFilter Factory
请求重试过滤器,当后端服务不可用时,网关会根据配置参数来发起重试请求
spring: cloud: gateway: routes: - id: retry_route uri: http://www.example.com predicates: - Path=/example/** filters: - name: Retry args: retries: 3 status: 503 - StripPrefix=1
-
retries : 请求重试次数,默认值是3
-
status : HTTP请求返回的状态码,针对指定状态码进行重试。(例如:当服务端返回的状态码是503时,才会发起重试,此处可以配置多个状态码)
-
methods : 指定HTTP请求中哪些方法类型需要进行重试,默认值是GET
-
series : 配置错误码段,表示符合某段状态码才发起重试,默认值是SERVER_ ERROR(5),表示5xx段的状态码都会发起重试。如果series配置了错误码段**,**但是status没有配置,则仍然会匹配series进行重试
GlobalFilter
GlobalFilter和GatewayFilter的作用是相同的,只是GlobalFilter针对所有的路由配置生效
Spring Cloud Gateway内置的全局过滤器也有很多,比如:
- GatewayMetricsFilter:提供监控指标
- LoadBalancerClientFilter:整合Ribbon针对下游服务实现负载均衡
- ForwardRoutingFilter:用于本地forward,请求不转发到下游服务器
- NettyRoutingFilter:使用Netty的HttpClient转发HTTP、HTTPS请求
- 全局过滤链的执行顺序是当Gateway接收到请求时,Filtering Web Handler处理器会将所有的GlobalFilter实例及所有路由上所配置的GatewayFilter实例添加到一条过滤器链中
- 该过滤器链里的所有过滤器都会按照**@Order注解所指定的数字大小进行排序**
LoadBalancerClientFilter
用于实现请求负载均衡的全局过滤器
spring:
cloud:
gateway:
routes:
- id: loadbalance_route
uri: lb://xx
predicates:
- Path=/service/**
如果URI配置的是 lb://xx,那么这个过滤器会识别到lb://,并且使用Spring CloudLoadBalancerClient将example_ service名称解析成实际访问的主机和端口地址
GatewayMetrics Filter
网关指标过滤器,这个过滤器会添加 name=gateway.requests的timer metric,其中包含以下数据
- routeId :路由ID
- routeUri : API网关将路由到的URI
- outcome : 返回的状态码段,值的枚举类定义在HttpStatus.Series中
- status : 返回给客户端的HTTP Status
- httpStatusCode : 返回给客户端的HttpStatusCode,如200
- httpMethod : 请求所使用的HTTP方法
这些指标通过 http://ip:port/actuator/metrics/gateway.requests 获得,前提是需要添加Spring BootActuator依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
management:
endpoint:
gateway:
enabled: true
endpoints:
web:
exposure:
include: "*"
自定义过滤器
自定义GatewayFilter
//案例1
/**
* <h1>HTTP 请求头部携带 Token 验证过滤器</h1>
* */
public class HeaderTokenGatewayFilter implements GatewayFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 从 HTTP Header 中寻找 key 为 token, value 为 imooc 的键值对
String name = exchange.getRequest().getHeaders().getFirst("token");
//若匹配则放行
if ("imooc".equals(name)) {
return chain.filter(exchange);
}
// 标记此次请求没有权限, 并结束这次请求
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
@Override
public int getOrder() {
return HIGHEST_PRECEDENCE + 2;
}
}
@Component
public class HeaderTokenGatewayFilterFactory
extends AbstractGatewayFilterFactory<Object> {
@Override
public GatewayFilter apply(Object config) {
return new HeaderTokenGatewayFilter();
}
}
//案例2
@Service
@Slf4j
public class GpDefineGatewayFilterFactory extends AbstractGatewayFilterFactory<GpDefineGatewayFilterFactory.Gpconfig> {
public GpDefineGatewayFilterFactory(){
super(Gpconfig.class);
}
@Override
public GatewayFilter apply(Gpconfig config) {
return ((exchange, chain) -> {
log.info("[Pre] Filter Request,name:" + config.getName());
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
log.info("[post] Response Filter");
}));
});
}
@Data
public static class Gpconfig{
private String name;
}
}
-
类名必须要统一以GatewayFilterFactory结尾,因为默认情况下过滤器的name会采用该自定义类的前缀,这里的name=GpDefine
-
在apply方法中,同时包含Pre和Post过滤,在then方法中是请求执行结束之后的后置处理
-
GpConfig是一个配置类 ,该类中只有一个属性name
- 这个属性可以在yml文件中使用
spring:
cloud:
gateway:
routes:
- id: define_filter
uri: http://localhost:8080/say
predicates:
- Path=/gateway/**
filters:
- name: GpDefine
args:
name: Gp_Mic
- StripPrefix=1
- name属性 就是GpDefineGatewayFilterFactory的前缀
- args中的name属性是GpConfig配置类中声明的属性,这个属性配置好之后,可以在代码中获得这个name对应的值Gp_Mic
自定义GlobalFilter
只需要实现GlobalFilter接口,自动会过滤所有的Route
@Service
@Slf4j
public class GpDefineFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("[pre]-Enter GpDefineFilter");
return chain.filter(exchange).then(Mono.fromRunnable(() ->{
log.info("[post]-Return Result");
}));
}
// getOrder表示该过滤器的执行顺序,值越小,执行优先级越高
@Override
public int getOrder() {
return 0;
}
}
- 通过AbstractGatewayFilterFactory实现的局部过滤器没有指定order ,它的默认值是0
- 如果想要设置多个过滤器的执行顺序,可以重写getOrder方法
OpenFeign
HTTP调用(超时、重试、并发)
与执行本地方法不同,进行HTTP调用 本质上是通过 HTTP协议 进行一次网络请求,网络请求是不可靠的,必然有超时的可能性
HTTP调用需要考虑到的问题
- 框架设置的 默认超时 是否合理
- 考虑到网络的不稳定,如果超时后请求 重试 的话,但需要考虑服务端 接口的幂等性 是否支持重试
- 还需要考虑框架是否会像浏览器那样 限制并发连接数,以免在服务并发很大的情况下,HTTP调用的并发数限制成为瓶颈
配置连接超时和读取超时参数
-
HTTP协议 底层是网络层的 TCP/IP 协议,它是面向连接的协议,在传输数据之前需要建立连接
-
连接超时参数
ConnectTimeout
,用户建立连接阶段的最长等待时间 -
读取超时参数
ReadTimeout
,用来控制从 Socket 上读取数据的最长等待时间
-
连接超时
- 连接超时配置不易配置过长, 一般来说,TCP三次握手建立连接需要的时间非常短,通常在毫秒级最多到秒级,不可能需要十几秒甚至几十秒。如果很久都无法建立连接,很有可能是网络或防火墙配置的问题,在这种情况下,如果几秒连接不上,那么大概率永远也连接不上
- 即设置特别长的连接超时意义不大,将其配置得短一些(比如 1~5秒)即可,如果纯内网调用,这个参数可以设置得更短,在下游服务离线无法连接的时候,可以快速失败
- 排查连接超时原因,服务有多个节点,如果别的客户端通过客户端负载均衡连接服务端,那么客户端和服务端会直接建立连接,此时出现连接超时大概率是服务端的问题
- 如果服务端通过类似 Nginx的反向代理来负载均衡,客户端连接的是Nginx,而不是服务端,应该排查Nginx
读取超时
- 读取超时,服务端的执行不会中断,类似 Tomcat 的 Web服务器 都是把服务端请求提交到线程池处理的,只要服务端收到了请求,网络层面的超时和断开不会影响服务端的执行
- 出现读取超时不能随意假设服务端的处理情况,需要根据业务状态考虑如何后续处理
- 读取超时不仅仅是 Socket 网络层面的概念,数据传输的最长耗时, 发生了读取超时,网络层无法区分是服务端没有把数据返回给客户端,还是数据在网络上耗时较久或丢包
- TCP是先建立连接后传输数据,对于网络情况不是特别糟糕的服务调用,通常可以认为出现连接超时是网络问题或服务不在线
- 出现读取超时是服务处理超时,读取超时指的是,向Socket写入数据后,等到Socket返回数据的超时时间,其中包含绝大部分的时间是服务端处理业务逻辑的时间
- 超时时间越长任务接口成功率不一定越高,不宜将读取超时参数配置太长,进行 HTTP请求一般是需要获得结果的,属于 同步调用。如果超时时间很长,在等待服务端返回数据的同时,客户端线程(通常是Tomcat线程池)也在等待,当下游服务出现大量超时的时候,程序可能也会受到拖累创建大量线程,最终崩溃
- 对定时任务或异步任务来说,读取超时配置得长些问题不大。面向用户响应的请求或是微服务短平快的同步接口调用,并发量一般较大,应该设置一个较短的读取超时时间, 以防止被下游服务拖慢,通常不会设置超过30秒的读取超时
- 如果把读取超时设置为2秒,服务端接口需要3秒,不是永远都拿不到执行结果了?
- 设置读取超时一定要根据实际情况,过长可能会让下游抖动影响到自己,过短又可能影响成功率。有些时候需要根据下游服务的SLA,为不同的服务端接口设置不同的客户端读取超时
Feign和Ribbon配置超时
-
默认情况下,
Feign
的读取超时是 1秒 -
如果要配置
Feign
的读取超时,就必须同时配置连接超时,才能生效-
FeignClientFactoryBean
只有同时设置ConnectTimeout
和ReadTimeout
,Request.Options
才会被覆盖-
feign.client.config.default.readTimeout=3000 feign.client.config.default.connectTimeout=3000
-
-
如果希望针对单独的
Feign Client
设置超时时间,可以把 default 替换为 Client 的 name-
feign.client.config.default.readTimeout=3000 feign.client.config.default.connectTimeout=3000 feign.client.config.clientsdk.readTimeout=3000 feign.client.config.clientsdk.connectTimeout=3000
-
-
-
单独的超时可以覆盖全局超时
-
除了可以配置
Feign
,也可以配置Ribbon
组件的参数来修改两个超时时间 (参数首字母要大写)-
ribbon.ReadTimeout=4000 ribbon.ConnectTimeout=4000
-
-
同时配置
Feign
和Ribbon
的参数,生效的是 Feign -
//这样配置最终生效的还是 Ribbon的超时(4秒),单独配置 Feign的读取超时并不能生效 clientsdk.ribbon.listOfServers=localhost:45678 feign.client.config.default.readTimeout=3000 feign.client.config.clientsdk.readTimeout=2000 ribbon.ReadTimeout=4000
Ribbon会自动重试请求
短信重复发送案例
-
把发短信接口从Get 改为 Post,有状态的 API接口 不应该定义为Get,选择Get还是Post,应该是 API 的行为,不是参数大小
- 根据HTTP协议的规范,Get请求用于数据查询,Post用于把数据提交到服务端用于修改或新增
- Get 请求的参数包含在Url QueryString中,会受浏览器长度限制,所以会会选择使用JSON以Post提交大参数,使用Get提交惨数。
-
Ribbon
的源码,MaxAutoRetriesNextServer
参数默认为1,Get 请求在某个服务端节点出现问题(比如读取超时)时,Ribbon会自动重试一次-
//禁用服务调用失败后在下一个服务端节点的自动重试 ribbon.MaxAutoRetriesNextServer=0
-
并发限制了爬虫能力
案例背景
并发数的限制 导致程序的处理能力上不去
-
爬虫项目,整体爬取数据的效率很低,增加线程池数量也无用,只能堆更多的机器做分布式的爬虫
-
爬虫需要多次调用一个接口进行数据抓取,为了确保线程池不是并发的瓶颈,使用一个没有线程上限的
newCachedThreadPool
作为爬取任务的线程池(一般不要使用没有线程数量上限的线程池), 然后使用HttpClient
实现HTTP请求,把请求任务循环提交到线程池处理,最后等待所有任务执行完成后输出执行耗时-
使用默认的
PoolingHttpClientConnectionManager
构造的CloseableHttpClient
,测试一下爬取10次的耗时 -
虽然一个请求需要1秒执行完成,但是线程池是可以扩张使用任意数量线程的。10个请求并发处理的时间基本相当于1个请求的处理时间,也就是1秒,但日志中显示实际耗时5秒
-
PoolingHttpClientConnectionManager的源码
defaultMaxPerRoute = 2
,也就是同一个主机 / 域名的最大并发请求数为2。爬虫需要10个并发,显然是默认值太小限制了爬虫的效率maxTotal = 20
,所有主机整体最大并发为20,这也是HttpClient
整体的并发度。目前,请求数是10,最大并发是10,20不会成为瓶颈- 例如,使用同一个
HttpClient
访问10个域名,defaultMaxPerRoute
设置为10,为确保每一个域名都能达到10并发,需要把maxTotal设置为100
- 例如,使用同一个