
引言:
目录:
1.Spring Cloud Gateway 简介
2.网关数据管理
3.实现细节
客户端会多次请求不同的微服务,增加了客户端的复杂性。
存在跨域请求,在一定场景下处理相对复杂。
认证复杂,每个服务都需要独立认证。
难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将会很难实施。
某些微服务可能使用了防火墙 / 浏览器不友好的协议,直接访问会有一定的困难。

使用 API 网关后的优点如下:
易于监控。可以在网关收集监控数据并将其推送到外部系统进行分析。
易于认证。可以在网关上进行认证,然后再将请求转发到后端的微服务,而无须在每个微服务中进行认证。
减少了客户端与各个微服务之间的交互次数。

SCG架构

网关对外提供治理数据管理接口, 微服务治理平台可通过这些接口, 将治理配置推送到网关
网关通过治理数据统一存储接口, 将治理配置数据保持至治理数据持久存储(这里我们默认为Redis)
Redis通过发布订阅能力, 将数据的变更通知到各网关实例
各网关实例收到通知后, 将数据从持久存储同步至内部高速缓存
内部缓存在网关启动时, 会自动从持久存储加载对应配置进入缓存. 同时它也支持清空, 以及按需加载
外部业务请求经过网关时, 对数据执行鉴权,处理转换, 以及灰度策略时,所需要治理配置,都从内部缓存中获取, 以提升性能
方案中, 外部持久存储(默认用的Redis, 可以换成Mysql, 文件, Appolo等), 以及数据变更通知(默认使用的是Redis的发布订阅, 可以换成Appolo通知, 消息队列, 定时扫描等), 都是可以扩展的
@Bean
@ConditionalOnMissingBean(RouteDefinitionRepository.class)
public InMemoryRouteDefinitionRepository inMemoryRouteDefinitionRepository() {
return new InMemoryRouteDefinitionRepository();
}可以看出, 网关中如果没有RouteDefinitionRepository的Bean, 就会采用InMemoryRouteDefinitionRepository做为实现。这个 InMemoryRouteDefinitionRepository有一个问题, 就是数据没有持久化, 网关重启之后,原来通过接口设置的路由就会丢失了。
除此以外, 每当路由更改之后, 还需要通知网关刷新路由。这需要发送 RefreshRoutesEvent 来通知网关。如下列示例:
@Component
public class RouteDynamicService implements ApplicationEventPublisherAware {
private ApplicationEventPublisher publisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
/**
* 刷新路由表
*/
public void refreshRoutes() {
publisher.publishEvent(new RefreshRoutesEvent(this));
}
}
为对象生成key时, 建议为key添加一个命名空间(就是加一段有意义的前缀)
在redis中进行模糊搜索时, 提供给Redis的pattern, 不能是一个正则的通配, 它支持三种通配 *(多个), ?(单个)
如果数据量比较大, 不建议使用keys进行模糊查询, 应该使用scan方式
实现了 InitializingBean 以实现在网关启动时, 自动加载数据
内部使用了ConcurrentHashMap, 保证写时的线程同步, 又保证了get时的高效(get整个过程不需要加锁)
从缓存中取数据时, 如果需要懒加载, 当从持久存储中加载不到数据时, 建议使用空数据, 或空集合占位, 避免每次都去持久存储中查询
/**
* 根据 appCode 获取流量策略
*
* @param appCode
* @return
*/
public Set<ApplicationTrafficPolicy> getAppTrafficPolicies(String appCode) {
// 从缓存加载
Map<String, ApplicationTrafficPolicy> map = policyMap.get(appCode);
// 缓存中没有
if (map == null) {
// 尝试从持久存储中加载所有此网关的流量策略
Set<ApplicationTrafficPolicy> policies = trafficPolicyRepository.fuzzyQuery();
// 持久存储中没有任何流量策略,占个位置,防止缓存重复去加载
if (policies == null || policies.size() == 0) {
map = new ConcurrentHashMap<>();
policyMap.put(appCode, map);
} else {
// 持久存储中有流量策略,放入缓存
for (ApplicationTrafficPolicy policy : policies) {
setTrafficPolicy(policy);
}
// 重新从缓存中加载一次
map = policyMap.get(appCode);
// 如果还是没有,使用空 map 占位子
if (map == null) {
map = new ConcurrentHashMap<>();
policyMap.put(appCode, map);
}
}
}
return map.values().stream().collect(Collectors.toSet());
}notify-keyspace-events "K$g"
@Bean
RedisMessageListenerContainer container(MessageListenerAdapter listenerAdapter) {
String gtwReidsPattern = "__keyspace@*__:" + GTW + keyGenerator.getGatewayCode() + "]*";
String cofRedisPattern = "__keyspace@*__:" + COF + cacheKey.getKeyNameSpace() + USER_NAME + "*";
log.info("Add gateway redis message listener, patternTopic is {}", gtwReidsPattern);
log.info("Add coframe redis message listener, patternTopic is {}", cofRedisPattern);
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(redisTemplate.getConnectionFactory());
// PatternTopic 参考:http://redisdoc.com/topic/notification.html
container.addMessageListener(listenerAdapter, Arrays.asList(new PatternTopic(PatternUtil.fmt(gtwReidsPattern)), new PatternTopic(PatternUtil.fmt(cofRedisPattern))));
return container;
}
当redis事件订阅好了之后, 每次其中我们关心的数据有变更, 都会发送set或del事件.
我们需要定义一个 MessageListener, 来接收事件:
@Service(value = RedisMessageListener.REDIS_LISTENER_NAME)
public class RedisMessageListener implements MessageListener {
@Override
public void onMessage(Message message, byte[] pattern) {
String ops = new String(message.getBody());
String channel = new String(message.getChannel());
String key = channel.split(":")[1];
if ("set".equals(ops)) {
String value = redisTemplate.opsForValue().get(key);
handleSet(key, value);
} else if ("del".equals(ops)) {
handleDel(key);
}
}
...
}接收到事件后,会调用相应的内部缓存,更新内部缓存中的数据,以实现治理数据变更的及时生效。
精选提问:
问1:当前网关实例因为网络的原因,如果没有订阅到消息,消息会重发吗?
答:不会。但内存缓存会定期清理,以解决这种数据不同步的问题。也可以主动清理。
问2:网关使用了zuul了吗?还是自己实现的网关?
答:网关于Spring Cloud Gateway开发,他就是一个类似于zuul的API网关。
问3:netttyserver是干嘛的?
答:那是Spring Cloud Gateway本身使用的组件, 用来接收与处理请求的。
问4:文件上传的接口也通过网关吗?
答:这个要看具体需求。也可以走网关, 但会对性能有一定影响。不走网关, 就得在应用那一层来控制权限。网关控制权限, 只是相当于把权限校验前移与统一化了。
问5:在微服务化之后,网关路由到服务,调用会有超时的情况怎么处理?有些接口是必须要这么长时间,例如批量操作 。只能通过加大超时时间吗?
答:这个一个考虑适当增大超时时间, 另一个, 你可以考虑采用异步模式, 比如用任务来处理。
问6:我想提问下,目前gateway我看实现是基于netty实现的http协议的,通过相关的mapping处理断言然后处理过滤器。那有基于netty的tcp协议的实现方案吗?基于tcp怎么整合断言和过滤器呢?
答:TCP的我们也在考虑, 有这方面的需求. 但是直接基于TCP实现断言与过滤, 工作量估计会比较大. 现在倾向的方案是在网关前做一层TCP的协议转换, 将TCP将成 http 再发往网关. 这样可以直接利用网关现有能力。

关于作者:将晓渔,现任普元云计算架构师。曾在PDM,云计算,数据备份,移动互联相关领域公司工作,十年以上IT工作经验。曾为科企桌面虚拟化产品的核心工程师,爱数容灾备份云柜系统设计师,万达信息的食安管理与追溯平台开发经理。国内IAAS云计算的早期实践者,容器技术专家。
转载本文需注明出处:微信公众号EAWorld,违者必究。
关于EAWorld:微服务,DevOps,数据治理,移动架构原创技术分享。长按二维码关注!
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/31562043/viewspace-2654632/ ,如需转载,请注明出处,否则将追究法律责任。