kong网关从入门到精通_分布式框架之可扩展:API网关

00f5ed5edc8b5b092d872de551feed2b.png

本文首发于Ressmix个人站点:https://www.tpvlog.com

本章,我将介绍微服务架构中的另一个非常重要的组件——API网关。网关在分布式系统中实在太重要了,可以提供动态路由、灰度发布、授权认证、性能监控、限流熔断等功能:fde22f5eaddec2ab2cadda79453f3adc.png

目前开源界有很多可供选择的API网关,云原生软件基金会(CNCF)的全景图就是一个很好的参考:

981f37b2e7ed17bea5ac91a4be18f874.png

本章,我会先讲解一个API网关应当具备哪些核心组件,并分析如果我们要自己实现这样一个API网关,系统设计和架构的思路应当是怎样的,然后对几种常见的开源API网关框架进行介绍。最后,我会以Spring Cloud Zuul为例,介绍Zuul的基本使用。

一、核心组件

一个API网关,一般至少需要具备如下核心组件:

  • 路由:定义一些规则来匹配客户端的请求,根据匹配的结果加载、执行相应的插件,并把请求转发到指定的上游。路由匹配规则可以由 host、uri、请求头等组成,比如Nginx 中的 location,就是路由的一种实现;

  • 插件:提供身份认证、限流限速、IP 黑白名单、Prometheus、Zipkin 等功能,插件之间不能互相影响,应该支持插件的热加载;

  • schema:对 API 的报文格式做校验,比如数据类型、字段内容、可空等,由schema来做统一、独立的定义和检查;

  • 存储:存放用户的各种配置,并在有变更时负责推送到所有的API网关节点。

在这些核心组件之上,我们还需要抽象出几个 API 网关的常用概念,它们在不同的 API 网关之间都是通用的。

1.1 Route

路由(Route)一般会包含三部分内容:即匹配的条件、绑定的插件和上游,如下图:

13347a55503a694be6a4b78ca32cf45f.png

在 API 和上游很多的情况下,会有很多重复的配置。这时候,我们一般需要 Service 和 Upstream 这两个概念来做一层抽象。

1.2 Service

Service是某类 API 的抽象,也可以理解为一组 Route 的抽象,它通常与上游服务是一一对应的,而 Route 与 Service 之间通常是 N:1 的关系:

fc7f77ad534a9f4610ed0c30ae94d687.png

通过 Service 的这层抽象,我们就可以把重复的插件和上游剥离出来。这样,在插件和上游发生变更的时候,我们只需要修改 Service 就可以了,而不用去修改多个 Route 上绑定的数据。

1.3 Upstream

如果两个 Route 中的上游是一样的,但是绑定的插件各自不同,那么我们就可以把上游单独抽象出来,如下图所示:

8b0f2d12bc7f7f2dfce4dfcaf4be385c.png

这样,在上游节点发生变更时,Route 是完全无感知的,它们都在 Upstream 内部进行了处理。其实,从这三个主要概念的衍生过程中,我们也可以看到,这几个抽象都基于用户的实际场景,而不是生造出来的。自然,它们适用于所有的 API 网关,和具体的技术方案无关。


当微服务 API 网关的这些关键组件都确定了之后,用户请求的处理流程,也就随之尘埃落定了。下面这张图可以表示这整个流程:

c0d76e40290c6af2d4f3b0004d8fd618.png

从这个图中我们可以看出:

  1. 当一个用户请求进入 API 网关时,首先,会根据请求的方法、uri、host、请求头等条件,去路由规则中进行匹配,如果命中了某条路由规则,就会从 etcd 中获取对应的插件列表;

  2. 然后,和本地开启的插件列表进行交集,得到最终可以运行的插件列表;

  3. 再接着,根据插件的优先级,逐个运行插件;

  4. 最后,根据上游的健康检查和负载均衡算法,把这个请求发送给上游。

当架构设计完成后,我们就可以去编写具体的代码了。

上图其实是基于OpenResty实现一个API网关的基本思路,关于源码实现,读者可以参考温铭的《OpenResty从入门到实战》(https://time.geekbang.org/column/intro/186)中的内容,也可以直接从GitHub寻找基于APISIX框架的网关源码进行阅读并扩展其功能。

二、技术选型

介绍完了一个API网关应该具备的基本功能及核心组件,我们就来看看目前开源界有哪些产品可供使用。

2.1 OpenResty

OpenResty 是一个兼具开发效率和性能的服务端开发平台,虽然它基于 NGINX 实现,但其适用范围,早已远远超出反向代理和负载均衡。它的核心是基于 NGINX 的一个 C 模块(lua-nginx-module),该模块将 LuaJIT 嵌入到 NGINX 服务器中,并对外提供一套完整的 Lua API,透明地支持非阻塞 I/O,提供了轻量级线程、定时器等高级抽象。

我们可以基于OpenResty开发自己的API网关,优点是抗并发的能力很强,少数几台机器部署一下,就可以抗很高的并发。缺点是需要精通Lua,而且OpenResty的很多API使用上都有性能上的坑,各类 lua-resty-*包也都零零散散,缺乏统一的规范和整合,所以要想使用好门槛是比较高的,否则生产上很容易引发性能问题和莫名其妙的异常。

2.2 Kong

Kong 是由 Mashape 开发的并于2015年开源的一款API 网关,它是基于OpenResty(Nginx + Lua模块)和 Apache Cassandra/PostgreSQL 构建的,能提供易于使用的RESTful API来操作和配置API管理系统。Kong 可以水平扩展多个 Kong Server,通过前置的负载均衡配置把请求均匀地分发到各个Server,来应对大批量的网络请求。

一句话,Kong可以看作是基于OpenResty 的一个开源API网关框架。

2.3 APISIX

与Kong类似,也是是基于OpenResty 的一个开源API网关框架,但是它是基于etcd 来 做配置管理,相对于Kong 来说,更加轻量级。

2.4 Envoy

Envoy 最初是在Lyft上构建的一种高性能C ++分布式代理服务,可以作为大型微服务“Service Mesh”架构的通信总线。

Envoy 本身可以做反向代理和负载均衡,也就是说它可以完全替换掉NGINX。Envoy具备一个API网关的所有功能,目前Envoy的社区活跃度也非常高,我们可以基于Envoy来构建自己的API网关。

缺点是Envoy的中文文档目前还很少。

2.5 Zuul

Netflix开源的API网关,基于Java开发,功能比较简单,灰度发布、限流、动态路由之类的功能基本都需要自己做二次开发。另外,Zuul的并发能力不强,还要基于Tomcat来部署,好处是基于Java语言开发,可以直接把控源码,方便做二次开发。

2.6 Spring Cloud Gateway

Spring Cloud的一个全新的API网关项目,目的是为了替换掉Zuul。优点是Java源码,背靠Spring全家桶,缺点和Zuul差不多,抗不了超高并发,需要自己做定制。

2.7 自研网关

目前很多大公司基本都是自研API网关,有的直接OpenResty,有的基于Netty+Servlet来实现网关的核心功能。

三、Zuul网关:动态路由

从本章节开始,我将以Zuul为例,介绍API网关的部分核心功能,感兴趣的读者也可以参考Zuul官方文档(https://github.com/Netflix/zuul/wiki)作更多了解。

在分布式框架之可扩展:Spring Cloud一章中,我通过一个简单的电商示例介绍过Zuul网关的基本使用,当时是直接在yml中写死了路由配置:

server:
port: 9000
spring:
application:
name: zuul-gateway
eureka:
instance:
hostname: localhost
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
# 所有访问/order/**这个URI的请求,全部转发给order-service服务处理
zuul:
retryable: true
routes:
order-service:
path: /order/**

如果我们要新增服务提供方,难道每次都要重新修改网关配置,然后重启网关?显然不现实,所以一般都要针对网关做动态路由

最常见的实现动态路由的方式是基于数据库(也可用Apollo配置中心、Redis、ZooKeeper等保存路由配置信息),本节我以Zuul为示例介绍API网关的动态路由功能。

3.1 路由表

首先创建一张路由配置表 gateway_api_route

CREATE TABLE `gateway_api_route` (`id` varchar(50) NOT NULL COMMMENT '自增主键',`path` varchar(255) NOT NULL COMMMENT '请求URI',`service_id` varchar(50) DEFAULT NULL COMMMENT '服务ID,唯一',`url` varchar(255) DEFAULT NULL COMMMENT '',`retryable` tinyint(1) DEFAULT NULL COMMMENT '是否允许重试:0-允许,1-不允许',`enabled` tinyint(1) NOT NULL COMMMENT '是否开启路由:0-关闭,1-开启',`strip_prefix` int(11) DEFAULT NULL COMMMENT '是否去除前缀路径:0-否,1-是',`api_name` varchar(255) DEFAULT NULL COMMMENT '',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

数据示例 INSERT INTO gateway_api_route(id,path,service_id,retryable,enabled,strip_prefix)VALUES('1','/order/**','order-service',0,1,NULL);

对于路由表的数据可以进行缓存,然后前端提供页面进行增删改查,以及缓存手动失效处理。

3.2 动态路由实现

首先,我们需要对Zuul网关应用的 application.yml配置进行改写,去掉写死的路由配置:

server:
port: 9000
spring:
application:
name: zuul-gateway
datasource:
url: jdbc:mysql://localhost:3306/test?autoReconnect=true&useUnicode=true&characterEncoding=utf-8
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
eureka:
instance:
hostname: localhost
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
registryFetchIntervalSeconds: 3
leaseRenewalIntervalInSeconds: 3
zuul:
retryable: true

然后,创建一个动态路由配置类:

@Configurationpublic class DynamicRouteConfiguration {
@Autowiredprivate ZuulProperties zuulProperties;
@Autowiredprivate ServerProperties server;
@Autowiredprivate JdbcTemplate jdbcTemplate;
@Beanpublic DynamicRouteLocator routeLocator() {
DynamicRouteLocator routeLocator = new DynamicRouteLocator(this.server.getServletPrefix(), this.zuulProperties);
routeLocator.setJdbcTemplate(jdbcTemplate);return routeLocator;
}
}

DynamicRouteLocator是我们自己实现的动态路由逻辑:

/**
* 动态路由实现类
*/public class DynamicRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {private JdbcTemplate jdbcTemplate;private ZuulProperties properties;public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {this.jdbcTemplate = jdbcTemplate;
}public DynamicRouteLocator(String servletPath, ZuulProperties properties) {super(servletPath, properties);this.properties = properties;
}
/**
* Spring容器接收到RoutesRefreshedEvent事件后触发
*/
@Overridepublic void refresh() {// 内部会调用locateRoutes()方法
doRefresh();
}
/**
* Zuul网关启动后会调用该方法
*/
@Overrideprotected Map locateRoutes() {
LinkedHashMap routesMap = new LinkedHashMap<>();// 加载application.yml中的路由表
routesMap.putAll(super.locateRoutes());// 加载db中的路由表
routesMap.putAll(locateRoutesFromDB());// 统一处理路由path的格式
LinkedHashMap values = new LinkedHashMap<>();for (Map.Entry entry : routesMap.entrySet()) {
String path = entry.getKey();if (!path.startsWith("/")) {
path = "/" + path;
}if (StringUtils.hasText(this.properties.getPrefix())) {
path = this.properties.getPrefix() + path;if (!path.startsWith("/")) {
path = "/" + path;
}
}
values.put(path, entry.getValue());
}
System.out.println("路由表:" + values); return values;
}
/**
* 从数据库查询路由信息,并转换成 =
*/private Map locateRoutesFromDB() {
Map routes = new LinkedHashMap<>();// 仅做示例,生产用Mybatis
List results = jdbcTemplate.query("select * from gateway_api_route where enabled = true ", new BeanPropertyRowMapper<>(GatewayApiRoute.class));for (GatewayApiRoute result : results) {if (StringUtils.isEmpty(result.getPath()) ) {continue;
}if (StringUtils.isEmpty(result.getServiceId()) && StringUtils.isEmpty(result.getUrl())) {continue;
}
ZuulProperties.ZuulRoute zuulRoute = new ZuulProperties.ZuulRoute();try {
BeanUtils.copyProperties(result, zuulRoute);
} catch (Exception e) {
e.printStackTrace();
}
routes.put(zuulRoute.getPath(), zuulRoute);
}return routes;
}
}
/**
* 数据库Bean实体类,对应表gateway_api_route
*/public class GatewayApiRoute {private String id;private String path;private String serviceId;private String url;private boolean stripPrefix = true;private Boolean retryable;private Boolean enabled;public String getId() {return id;
}public void setId(String id) {this.id = id;
}public String getPath() {return path;
}public void setPath(String path) {this.path = path;
}public String getServiceId() {return serviceId;
}public void setServiceId(String serviceId) {this.serviceId = serviceId;
}public String getUrl() {return url;
}public void setUrl(String url) {this.url = url;
}public boolean isStripPrefix() {return stripPrefix;
}public void setStripPrefix(boolean stripPrefix) {this.stripPrefix = stripPrefix;
}public Boolean getRetryable() {return retryable;
}public void setRetryable(Boolean retryable) {this.retryable = retryable;
}public Boolean getEnabled() {return enabled;
}public void setEnabled(Boolean enabled) {this.enabled = enabled;
}
}

最后,我们需要一个定时任务,每隔5分钟发布一个更新事件,让Zuul从数据库路由表加载路由信息到缓存中。

/**
* 定时任务,每隔5s触发一次RoutesRefreshedEvent事件
*/
@Component
@Configuration
@EnableScheduling public class RefreshRouteTask {
@Autowiredprivate ApplicationEventPublisher publisher;
@Autowiredprivate RouteLocator routeLocator;
@Scheduled(fixedRate = 5000) private void refreshRoute() {
System.out.println("定时刷新路由表");
RoutesRefreshedEvent routesRefreshedEvent = new RoutesRefreshedEvent(routeLocator);
publisher.publishEvent(routesRefreshedEvent);
}
}

四、Zuul网关:灰度发布

灰度发布(又名金丝雀发布),是API网关的一项重要功能,主要是按照一定策略选取部分用户,让他们先行体验新版本的应用,通过收集这部分用户对新版本应用的反馈(如:微博、微信公众号留言或者产品数据指标统计、用户行为的数据埋点),以及对新版本功能、性能、稳定性等指标进行评论,进而决定继续放大新版本投放范围直至全量升级或回滚至老版本。

本节我继续以Zuul为例,介绍一种实现灰度发布的方式。

4.1 灰度配置表

首先,我们创建一张灰度配置表 gray_release_config

CREATE TABLE `gray_release_config` (`id` int(11) NOT NULL AUTO_INCREMENT,`service_id` varchar(255) DEFAULT NULL COMMMENT '服务ID,唯一',`path` varchar(255) DEFAULT NULL COMMMENT '服务URI',`enable_gray_release` int(11) DEFAULT NULL COMMMENT '是否启用:0-否,1-是',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

数据示例 INSERT INTO gray_release_config(service_id,path,enable_gray_release)VALUES('order-service','/order/**',1)

4.2 灰度发布实现

首先,创建一个类,继承ZuulFilter,然后在shouldFilter方法中自定义路由规则:

@Configurationpublic class GrayReleaseFilter extends ZuulFilter {
@Autowiredprivate GrayReleaseConfigManager grayReleaseConfigManager;
@Overridepublic int filterOrder() {return PRE_DECORATION_FILTER_ORDER - 1;
}
@Overridepublic String filterType() {return PRE_TYPE;
}
/**
* 所有web请求都会被Zuul拦截,并经过该方法
*/
@Overridepublic boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();// requestURI类似http://localhost:9000/order/order?xxxx
String requestURI = request.getRequestURI();// 获取灰度配置
Map grayReleaseConfigs =
grayReleaseConfigManager.getGrayReleaseConfigs();for(String path : grayReleaseConfigs.keySet()) {// 如果请求路径命中if(requestURI.contains(path)) {
GrayReleaseConfig grayReleaseConfig = grayReleaseConfigs.get(path);// 开启了灰度if(grayReleaseConfig.getEnableGrayRelease() == 1) {
System.out.println("启用灰度发布功能"); // 返回true表示将执行该Filter的run()方法return true;
}
}
}
System.out.println("不启用灰度发布功能"); return false;
}
/**
* 这里实现灰度发布的逻辑
*/
@Overridepublic Object run() {// 生成一个0-99的随机数
Random random = new Random();int seed = random.nextInt() * 100;if (seed == 50) {// 1%的流量转发给version==new的后台服务
RibbonFilterContextHolder.getCurrentContext().add("version", "new");
} else {// 转发给version==current的后台服务
RibbonFilterContextHolder.getCurrentContext().add("version", "current");
}return null;
}
}
/**
* 数据库Bean实体类,对应表gray_release_config
*/public class GrayReleaseConfig {private int id;private String serviceId;private String path;private int enableGrayRelease;public int getId() {return id;
}public void setId(int id) {this.id = id;
}public String getServiceId() {return serviceId;
}public void setServiceId(String serviceId) {this.serviceId = serviceId;
}public String getPath() {return path;
}public void setPath(String path) {this.path = path;
}public int getEnableGrayRelease() {return enableGrayRelease;
}public void setEnableGrayRelease(int enableGrayRelease) {this.enableGrayRelease = enableGrayRelease;
}
}

每个服务的 application.yml配置中有一个 eureka.instance.metadata-map参数,该参数的值是一个Map。上述示例中key为version,我们以此来区分新老服务。此外,上述run()逻辑将1%流量转发给新服务处理,我们也可以根据请求中特定参数、源IP等信息进行路由处理。

最后,我们需要一个定时任务,每隔1s从数据库查询灰度发布配置信息,并更新到内部的HashMap中:

/**
* 定时任务,每隔1s从数据库查询灰度发布配置信息,并更新到内部的HashMap中
*/
@Component
@Configuration
@EnableScheduling public class GrayReleaseConfigManager {private Map grayReleaseConfigs = new ConcurrentHashMap();
@Autowiredprivate JdbcTemplate jdbcTemplate;
@Scheduled(fixedRate = 1000) private void refreshRoute() {
List results = jdbcTemplate.query("select * from gray_release_config", new BeanPropertyRowMapper<>(GrayReleaseConfig.class));for(GrayReleaseConfig grayReleaseConfig : results) {
grayReleaseConfigs.put(grayReleaseConfig.getPath(), grayReleaseConfig);
}
}public Map getGrayReleaseConfigs() {return grayReleaseConfigs;
}
}

五、总结

本章,我介绍了API网关的作用和动态路由、灰度发布的基本原理。API网关一般是整个分布式系统的门户,所有流量首先需要经过网关处理,所以API网关的性能优化是至关重要的。

以笔者曾经做过的一个银行快捷支付系统为例,其架构简化后,主要分为网关模块、联机模块、批量模块,所有支付请求首先要经过F5、Nginx/LVS进行负载均衡,然后到达API网关,由API网关进行服务路由、鉴权、日志记录等处理。所以,生产环境一般都需要根据预判的业务量,提前对整个系统进行压测。

一般来说,8核16G的机器,部署Zuul作为网关的话,单台可以抗1500QPS。基本上10~20台机器就可以支撑2W左右的总QPS。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值