赞
踩
单体构架
集群和垂直化
背景:
优化思路:
SOA
背景:
用户执行下单操作,业务逻辑会先检查商品的库存,库存足够的情况下才会提交订单,那么检查库存的逻辑是放在订单子系统还是库存子系统呢?在整个系统中,一定会存在非常多类似的共享业务的场景,这些业务场景的逻辑肯定会被重复的创建,从而产生非常多的冗余的业务代码
在一个集团公司下有很多子公司,每个子公司都有自己的业务模式和信息沉淀,各个子公司之间不进行交互和共享,彼此之间形成了信息孤岛,价值无法最大化
SOA是面向服务的架构,核心是把一些 通用的、会被多个上层服务调用的共享业务提取成独立的基础服务,被提取出来的共享服务相对来说比较独立,并且可以重用
在SOA中,服务是最核心的抽象手段,业务被划分为一些 粗粒度的业务服务和业务流程
采用 ESB(企业服务总线) 来作为系统和服务之间的通信桥梁,ESB本身还提供服务地址的管理、不同系统之间的协议转化和数据格式转化等。调用端不需要关心目标服务的位置,从而使得服务之间的交互是动态的
SOA和微服务架构的不用
REST API
进行通信优点
缺点
**分布式架构的复杂性:**微服务本身构建的是一个分布式系统,分布式系统涉及服务之间的远程通信,而网络通信中网络的延迟和网络故障是无法避免的,从而增加了应用程序的复杂度
服务监控: 在一个单体架构中很容易实现服务的监控,因为所有的功能都在一个服务中。在微服务架构中,服务监控开销会非常大,不仅要对整个链路进行监控,还需要对每一个微服务都实现一套类似单体架构的监控
**故障排查:**一次请求可能会经历多个不同的微服务的多次交互,交互的链路可能会比较长,每个微服务会产生自己的日志,在这种情况下如果出现一个故障,开发人员定位问题的根源会比较困难
服务依赖:微服务数量增加之后,各个服务之间会存在更多的依赖关系,使得系统整体更为复杂
**运维成本:**在微服务中,需要保证几百个微服务的正常运行,对于运维的挑战是巨大的
两大特性:高可用性、高可扩展性
服务间通信
服务容错、异常排查
分布式能力建设
在微服务架构下,一个业务服务会被拆分成多个微服务,各个服务之间相互通信完成整体的功能
另外,为了避免单点故障,微服务都会采取集群方式的高可用部署,集群规模越大,性能也会越高
需要服务注册的原因
服务消费者要去调用多个服务提供者组成的集群
首先,服务消费者需要在本地配置文件中维护服务提供者集群的每个节点的请求地址
其次,服务提供者集群中如果某个节点下线或者宕机,服务消费者的本地配置中需要同步删除这个节点的请求地址,防止请求发送到已宕机的节点上造成请求失败
服务注册中心的功能
Nacos致力于解决微服务中的 服务注册与发现、统一配置等问题。它提供了一组简单易用的特性集,帮助开发者快速实现动态服务发现、服务配置、服务元数据及流量管理
服务发现和服务健康监测
动态配置服务
存在的问题
动态配置服务的优点
其他优点
动态DNS服务
服务及其元数据管理
这种方式在某些场景中会存在问题,比如配置需要变更时要重新部署应用。而动态配置服务可以以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置,可以使配置管理变得更加高效和敏捷。配置中心化管理让实现无状态服务变得更简单,让服务按需弹性扩展变得更容易。
API网关的作用
在客户端与服务端之间增加了一个API网关,所有的外部请求都会先经过网关这一层
网关层可以把后端的多个服务进行整合,然后提供唯一的业务接口 ,客户端只需要调用这个接口即可完成数据的获取及展示
网关还提供以下功能
统一认证授权包括两部分
在单体应用中,客户端身份认证及访问权限的控制比较简单,只需要在服务端通过session保存该用户信息即可
在微服务架构下,单体应用被拆分成多个微服务,鉴权的过程就会变得很复杂
解决方案
增加API网关之后,在网关层进行请求拦截,获取请求中附带的用户身份信息,调用统一认证中心对请求进行身份认证,在确认了身份之后再检查是否有资源的访问权限
spring:
cloud:
gateway:
routes:
- id: auth
uri: http://localhost:8080/say #访问地址
predicates:
- Path=/gateway/** #路径匹配
filters:
- StripPrefix=1 #跳过前缀
工程流程
Predicate
时间规则
// 希望在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 分为 Pre类型过滤器 和 Post类型过滤器
GatewayFilter只会应用到单个路由或者一个分组的路由上
AddRequestParameter GatewayFilter Factory
spring:
cloud:
gateway:
routes:
- id: add_request_paramater_route
uri: http://xx.com
filters:
- AddRequestParamter=foo, bar
AddResponseHeader GatewayFilter Factory
spring:
cloud:
gateway:
routes:
- id: add_response_header_route
uri: http://xx.com
filters:
- AddResponseHeader=X-Response-Foo, Bar
RequestRateLimiter GatewayFilter Factory
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过滤器有两个配置属性
实现限制同一个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和GatewayFilter的作用是相同的,只是GlobalFilter针对所有的路由配置生效
Spring Cloud Gateway内置的全局过滤器也有很多,比如:
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,其中包含以下数据
这些指标通过 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: "*"
//案例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
spring:
cloud:
gateway:
routes:
- id: define_filter
uri: http://localhost:8080/say
predicates:
- Path=/gateway/**
filters:
- name: GpDefine
args:
name: Gp_Mic
- StripPrefix=1
只需要实现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; } }
与执行本地方法不同,进行HTTP调用 本质上是通过 HTTP协议 进行一次网络请求,网络请求是不可靠的,必然有超时的可能性
HTTP调用需要考虑到的问题
配置连接超时和读取超时参数
HTTP协议 底层是网络层的 TCP/IP 协议,它是面向连接的协议,在传输数据之前需要建立连接
连接超时参数 ConnectTimeout
,用户建立连接阶段的最长等待时间
读取超时参数 ReadTimeout
,用来控制从 Socket 上读取数据的最长等待时间
连接超时
读取超时
默认情况下,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
短信重复发送案例
把发短信接口从Get 改为 Post,有状态的 API接口 不应该定义为Get,选择Get还是Post,应该是 API 的行为,不是参数大小
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设置为100Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。