当前位置: 首页 > 科技观察

基于Redis的SpringCloudGateway动态管理

时间:2023-03-19 00:06:42 科技观察

介绍:SpringCloudGateway是一个应用非常广泛的API网关。其自身的能力还不能完全满足企业对网关的期望,人们希望它能够提供更多的业务管理能力。但是SpringCloudGateway不提供对数据的动态管理,甚至修改一条路由都需要重启。如何解决其不足,同时实现治理配置数据的高效动态管理?本文将带来我们的网关与Redis结合的实践。内容:1.SpringCloudGateway简介2.网关数据管理3.实现细节1.SpringCloudGateway简介APIGatewayAPIGateway的出现是因为微服务架构的出现。不同的微服务一般有不同的网络地址,一个外部客户端可能需要调用多个服务接口来完成一个业务需求。如果客户端直接和各个微服务通信,会存在以下问题:客户端会多次请求不同的微服务,增加了客户端的复杂性。存在跨域请求,在某些场景下处理相对复杂。认证比较复杂,每个服务都需要独立的认证。重构难度大,随着项目迭代,可能需要重新划分微服务。例如,可以将多个服务合并为一个或将一个服务拆分为多个。如果客户端直接与微服务通信,重构将难以实施。一些微服务可能使用防火墙/浏览器不友好的协议,使得直接访问变得困难。以上问题都可以借助API网关来解决。API网关是客户端和服务端之间的中间层,所有外部请求都会先经过API网关层。也就是说,在API的实现上有更多的考虑。使用API网关的优点如下:易于监控。监控数据可以在网关收集并推送到外部系统进行分析。易于验证。可以在将请求转发到后端微服务之前在网关上完成身份验证,而不是在每个微服务中进行身份验证。客户端和各个微服务之间的交互次数减少了。SpringCloudGatewaySpringCloudGateway是Spring基于Spring5.0、SpringBoot2.0、ProjectReactor等技术开发的网关。SpringCloudGateway旨在为微服务架构提供一种简单有效的统一API路由管理方式。作为SpringCloud生态中的网关,SpringCloudGateway旨在取代NetflixZUUL。它不仅提供了统一的路由方式,还提供了基于Filter链的网关的基本功能,如:安全、监控/埋点、限流等。如图所示,SCG的架构看起来很简单。首先,它包含一个高性能的NettyServer,用于接收各种网络请求。请求进来后,会根据配置的路由匹配处理请求。每个路由可以定义多个谓词进行路由匹配。SCG默认提供了10多个内置断言,可以根据请求的各个方面(请求头、路径、路径、时间、cookie、http方法等)进行路由匹配。如果这还不够,用户还可以自己扩展它。请求匹配到合适的路由后,会根据路由中配置的过滤器依次处理请求。Filter也基本可以对请求的所有属性进行处理,修改、增删请求头、修改请求数据、修改返回数据等等,几乎无所不能。当然,修改请求只是目的的一方面。鉴权、鉴权、登录等也可以在网关中完成。所有的过滤器组成一个处理链,直到所有的过滤器都被处理完,才会交给最后一个NettyClient,由它把处理完的请求发送给对应的微服务。在将请求发送到微服务之前,还可以定义其负载均衡策略(LoadBalancerRule)来确定将请求发送到微服务的哪个实例。Filter和LoadBalancerRule都支持自扩展。2.网关数据管理要实现一个合适的网关,数据管理应该考虑哪些方面?(1)首先,我们需要考虑我们需要管理哪些数据。SCG自身对数据管理的管理非常薄弱。它不提供数据持久化解决方案,它的所有数据都来自初始化,来自它的配置文件(application.yml)。虽然它也对外提供了一些管理接口(ActuatorAPI),但是能力还不够,这些修改都是暂时的。一旦网关停止,数据就会消失。这就需要我们用一个更完善的方案来管理网关的数据,不是让它只写在配置文件里,而是要支持持久化和动态变化。然后是每个微服务的治理数据。网关只用于路由转发,太浪费了。统一认证、统一认证、访问日志记录、应用访问统计、黑白名单过滤、API订阅管理、流量限制,甚至数据格式转换、网络协议转换等,都可以在网关完成。而所有这些能力都不需要数据的支持。因此,这些服务的治理配置也是网关需要管理的数据。(2)一旦有了数据,就要考虑如何保存。一旦网关重新启动,所有数据都将消失。(3)你又要考虑数据的读取了。网关的性能要求非常高。每次管理经过网关的数据,都需要读取配置信息。如果读取配置信息过于消耗资源,无疑对网关不利。因此,我们不得不考虑如何缓存数据以提高数据读取性能。(4)单个网关可以处理的请求量有上限。为了应对大流量,我们可能需要横向扩展网关。当多个网关实例并存时,如何保证网关的修改能够快速同步到各个网关实例?还必须考虑数据更改通知。(5)最多只能考虑方案的扩展。数据存储是否可以更改到其他地方,通知是否可以更改?经过这几个方面的考虑,我们网关的架构是这样的:如图所示,以上是我们网关的整体设计。方案设计要点如下:网关对外提供治理数据管理接口,微服务治理平台可以通过这些接口将治理配置推送给网关。对于Redis)Redis通过发布和订阅的能力通知每个网关实例数据的变化。每个网关实例收到通知后,将数据从持久化存储同步到内部缓存。配置进入缓存。同时还支持按需清算和加载。外部业务请求经过网关,进行数据鉴权、转换处理、灰度策略时,需要从内部缓存获取治理配置,提升性能解决方案中,外部持久化存储(默认为Redis,可替换为Mysql、files、Appolo等),数据变化通知(默认是Redis的发布订阅,可以替换为Appolo通知、消息队列、定时扫描等),可扩展3.实现细节动态路由管理SpringCloudGateway作为所有请求流量的入口。在实际生产环境中,为了保证高可靠和高可用,尽量避免重启,需要实现SpringCloudGateway动态路由配置。实现动态路由其实很简单,重点在RouteDefinitionRepository接口上。该接口继承自两个接口,其中RouteDefinitionLocator用于加载路由。它有很多实现类,PropertiesRouteDefinitionLocator用于从yml加载路由。另一个RouteDefinitionWriter用于添加和删除路由。通过查看springcloudgateway的源码,我们可以发现在org.springframework.cloud.gateway.config.GatewayAutoConfiguration中有这么一段:@Bean@ConditionalOnMissingBean(RouteDefinitionRepository.class)publicInMemoryRouteDefinitionRepositoryinMemoryRouteDefinitionRepository(){returnnewInMemoryRouteDefinitionRepository(){returnnewInMemoryRouteDefinition()Repository}Repository可以看到如果没有RouteDefinitionRepositoryBean,会使用InMemoryRouteDefinitionRepository作为实现。这个InMemoryRouteDefinitionRepository有个问题,就是数据没有持久化。网关重启后,原来通过该接口设置的路由将丢失。这当然是不能接受的,所以我们需要实现自己的RouteDefinitionRepository来提供路由配置信息。比如使用redis作为存储,实现路由存储。实现请参考文章:https://dwz.cn/tsHfKwMe另外,每当路由发生变化时,需要通知网关刷新路由。这需要发送一个RefreshRoutesEvent来通知网关。如下列表示例:@ComponentpublicclassRouteDynamicServiceimplementsApplicationEventPublisherAware{privateApplicationEventPublisherpublisher;@OverridepublicvoidsetApplicationEventPublisher(ApplicationEventPublisherpublisher){this.publisher=publisher;}/***刷新路由表*/publicvoidrefreshRoutes(){publisher.publishEvent(newRefreshRoutes)}可以路由由消息通知机制触发,当然也可以通过外部提供rest接口手动触发。数据存储如上图类图所示,IGovernDataRepository是治理数据的统一存储接口。RedisGovernDataRepository是其实现的抽象类。它需要依赖两个,一个是StringRedisTemplate,用来实现redis数据的存储。另一个是RedisKeyGenerator,用于为每个治理对象生成对应的key。RedisGovernDataRepository下面是各个治理数据存储的实现类。使用Redis作为持久化存储时,需要注意以下几点:为对象生成键时,建议为键添加命名空间(即添加有意义的前缀)。在redis中进行模糊搜索时,提供给Redis的pattern,不能是常规的通配符,支持三种通配符*(多个),?(单)如果数据量比较大,不建议使用key进行模糊查询,应该使用scan方式进行数据缓存我们提供了一个内部缓存,它位于消费者和持久化存储之间,缓存数据以提高性能。缓存的实现主要包括以下几点:实现了InitializingBean,在网关启动时自动加载数据。内部使用了ConcurrentHashMap,保证写入时线程同步,获取时高效(获取整个过程不需要加锁)从缓存中取数据时,如果需要懒加载,无法从持久化中加载数据时存储,建议使用空数据或空集合,避免每次都查询持久化存储。代码示例如下:/***根据appCode获取流量策略**@paramappCode*@return*/publicSetgetAppTrafficPolicies(StringappCode){//从缓存中加载Mapmap=policyMap.get(appCode);//缓存中没有if(map==null){//尝试从持久化存储中加载本网关的所有流量策略Setpolicies=trafficPolicyRepository.fuzzyQuery();//持久化存储中没有流量策略,占位防止缓存重复加载if(policies==null||policies.size()==0){map=newConcurrentHashMap<>();policyMap.put(appCode,map);}else{//持久化存储中有流量策略,放入缓存for(ApplicationTrafficPolicypolicy:policies){setTrafficPolicy(policy);}//从缓存重新加载一次map=policyMap.get(appCode);//如果还是没有,使用一个空的map占位符if(map==null){map=newConcurrentHashMap<>();policyMap.put(appCode,map);}}}returnmap.values().stream().collect(Collectors.toSet());}Eventnotification事件通知,这里我们使用的是redis的发布有了订阅能力,Redis默认不发送事件。要让它发布事件,需要先修改它的配置文件redis.conf,增加一个配置:notify-keyspace-events"K$g"上面的配置会让Redis在新增、修改或删除数据时,设置一个或发送del事件。然后,我们需要配置一个RedisMessageListenerContainer来订阅我们感兴趣的事件。@BeanRedisMessageListenerContainercontainer(MessageListenerAdapterlistenerAdapter){StringgtwReidsPattern="__keyspace@*__:"+GTW+keyGenerator.getGatewayCode()+"]*";StringcofRedisPattern="__keyspace@*__:"+COF+cacheKey.getKeyNameSpace()+USER_NAME+"*";log.info("Addgatewayredismessagelistener,patternTopicis{}",gtwReidsPattern);log.info("Addcoframeredismessagelistener,patternTopicis{}",cofRedisPattern);RedisMessageListenerContainercontainer=newRedisMessageListenerContainer();Factory.setConnectionFactory(redis)/PatternTopic参考:http://redisdoc.com/topic/notification.htmlcontainer.addMessageListener(listenerAdapter,Arrays.asList(newPatternTopic(PatternUtil.fmt(gtwReidsPattern)),newPatternTopic(PatternUtil.fmt(cofRedisPattern))));returncontainer;}当订阅redis事件时,每当我们关心的数据发生变化时,都会发送一个set或del事件。我们需要定义一个MessageListener来接收事件:@Service(value=RedisMessageListener.REDIS_LISTENER_NAME)publicclassRedisMessageListenerimplementsMessageListener{@OverridepublicvoidonMessage(Messagemessage,byte[]pattern){Stringops=newString(message.getBody());Stringchannel=newString(message.getChannel());Stringkey=channel.split(":")[1];if("set".equals(ops)){Stringvalue=redisTemplate.opsForValue().get(key);handleSet(key,value);}elseif("del".equals(ops)){handleDel(key);}}...}收到事件后,会调用相应的内部缓存,更新内部缓存中的数据,实现治理数据变化的及时效果。作者:蒋晓宇,现任朴元云计算架构师。曾就职于PDM、云计算、数据备份、移动互联网相关公司,拥有十余年IT工作经验。曾任科技企业桌面虚拟化产品核心工程师、爱数灾备云柜系统设计师、万达信息食品安全管理与追溯平台开发经理。国内IAAS云计算早期实践者,容器技术专家。