API网关的定义
简单的来说:将所有的API调用接入API网关层,负责整个系统架构的输入输出,可以将其当作设计模式中的Facade模式,作为整个微服务的架构门面,所有外部客户端的请求都是由API网关负责调度。基本功能包含请求路由
、协议适配
、安全防护
、流量监控和容错
,此外还有负载均衡和认证等一系列高级功能。
为什么需要API网关?
要搞清楚这个疑问,让我们先回顾单体应用的时代,在业务发展初期,开发团队规模很小的时候,我们常常把所有的功能集中与一个应用中,这样做是因为开发、测试和运维方便。由于业务不断发展,更多的需求和功能,使得单体应用臃肿,往往我们每更新一个模块,就不得不将其他模块暂停,然后部署和构建我们的单体应用。以下是单体架构的示意图:
愈来愈多的扩展需求后,微服务就出现了。微服务是一种架构风格,将一个复杂的应用拆分成多个服务模块,每个模块单独专注对外提供服务,而且每个模块都有自己单独的运维和发布,这样修改一个模块进行发布并不影响其他功能模块的正常运行。这样以来就解决了单体应用的弊端。此时问题来了,ios端,android端和PC端需要调用服务端多个api,为了简化客户端和服务端的沟通方式,此时就引入了网关。如下图所示:
由上图可以看出网关发挥了了请求路由
、协议转换
的基本功能。此外一个网关除了以上功能,还应包括安全防护
、流量监控和容错
、负载均衡
、服务认证处理
等,这些构成了网关的核心能力。如下图所示:
网关的设计
- 泛化调用
一般在我们的项目中,A系统调用B系统的某个接口,通常我们是使用RPC方式调用:首先A系统依赖B系统,即在A系统的pom.xml文件中引入B系统的jar包,然后A在使用B系统中的jar包中的接口,对于一个网关来说,如果拆分的微服务模块很多,网关调用各个模块,都需要引入各个模块的jar包,这种就让网关依赖模块较多,且耦合性大。为了解决这个问题,我们可以使用泛化调用。泛化调用在接口的输入输出中所有的POJO用map集合表示,什么意思呢?以我们经常常见的发起支付请求参数为列:
LinkedHashMap xmlMap = new LinkedHashMap[String, Object]()
// cmd命令固定值
xmlMap.put("cmd", "cmd")
// 总公司商户编号
xmlMap.put("group_Id", "group_Id")
// 发起付款公司编号
xmlMap.put("mer_Id", "mer_Id")
// 打款批次号(这里截取订单号的后8位置)
xmlMap.put("batch_No", batchNo)
// 收款银行编号
xmlMap.put("bank_Code", bankCode)
// 订单号
xmlMap.put("order_Id", orderId)
// 打款金额
xmlMap.put("amount", amount)
// 收款账户的账户号(银行卡号)
xmlMap.put("account_Number", bankAccount)
// 账户名称
xmlMap.put("account_Name", new String(userName.getBytes("GBK"),"GBK"))
// 手续费收取方式,商户承担
xmlMap.put("fee_Type","SOURCE")
// 实时出款
xmlMap.put("urgency", "1")
以上将一个支付所需要的参数以一个Map进行封装,而不是一个POJO类。
- 发布API到网关
当我们新写完了一个API网关,如何发布到网关,对外提供服务呢?将现有的网关重新启动?这显然是不合理的,在一个大型的服务架构中,已有对外的访问如果中断,将会给客户端带来极差的体验。所以此时在不影响现有的API网关服务,可以动态获取api,此时我们可以使用DB存储我们的API服务,结合上面提到的泛化调用,网关系统只需知道新的API的类名和方法名,一般为了提高性能,都会使用Redis存储。如下图所示: - 链式调用
在设计模式中有一个叫做责任链模式,责任链模式是一种行为模式,在这个链条中每个对象都有机会去处理请求,这些对象被练成一条链路,请求会在这个链路去传递,直到有对象去处理这个请求。我们经常常见的servlet里面的filter,springmvc里面的Interceptor都是采用了此种设计模式。
为什么这么做呢?使用此种调用防止整个调用链如果有一模块块出现调用异常,可以快速移除此模块,而不影响整个调用链条,如下图所示: - 半异步化请求调用
为了提高系统的吞吐量,此时我们还需要将异步思维引入API网关中。提到同步和异步,从线程调用可以这么理解,一个请求到业务处理完成是由一个线程完成的即就是同步,有多个线程切换完成请求和业务的处理就可以认为是异步。
一般的异步化可以使用Tomcat+NIO+Servlet3将IO请求线程和业务模块分开进行处理,下面介绍以下Servlet3异步请求流程:
下面是代码演示:
- 接收请求类AsyncRunningServlet
/**
* servlet3异步使用继IO线程,创建业务Runnable,在IO线程传递AsyncContext对象,
* 使用线程池(自定义累继承ServletListener新建线程,设置request域对象中)执行这个业务线程
*
* @author codegeekgao
*/
@WebServlet(urlPatterns = "/AsyncRunningServlet", asyncSupported = true)
public class AsyncRunningServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
System.out.println("AsyncRunningServlet Start | Name=" + Thread.currentThread().getName() + "| ID=" + Thread.currentThread().getId());
String time = req.getParameter("time");
int processTime = Integer.valueOf(time);
if (processTime > 5000) {
processTime = 5000;
}
// 创建异步上下文对象AsyncContext
AsyncContext asyncContext = req.startAsync();
// 添加自定义的异步监听器
asyncContext.addListener(new MyAsyncListener());
// 设置4s超时,指定时间未处理完则报异常
asyncContext.setTimeout(4000L);
//开启线程执行异步
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) req.getServletContext().getAttribute("executor");
threadPoolExecutor.execute(new AsyncRequestProcessor(asyncContext, processTime));
long endTime = System.currentTimeMillis();
System.out.println("AsyncRunningServlet Start | Name=" + Thread.currentThread().getName() + "| ID=" + Thread.currentThread().getId() + "| Time=" + (endTime - startTime));
}
}
- 异步监听器MyAsyncListener
/**
* 异步监听器
*
* @author codegeekgao
*/
public class MyAsyncListener implements AsyncListener {
@Override
public void onComplete(AsyncEvent asyncEvent) throws IOException {
System.out.println("MyAsyncListener isComplete");
}
@Override
public void onTimeout(AsyncEvent asyncEvent) throws IOException {
System.out.println("MyAsyncListener timeOut");
ServletResponse response = asyncEvent.getAsyncContext().getResponse();
response.getWriter().write("timeOut");
}
@Override
public void onError(AsyncEvent asyncEvent) throws IOException {
System.out.println("MyAsyncListener error");
}
@Override
public void onStartAsync(AsyncEvent asyncEvent) throws IOException {
System.out.println("MyAsyncListener onStartAsync");
}
}
- 上下文监听器
/**
* 上下文监听器(监控整个Servlet)
*
* @author codegeekgao
*/
public class AppContextListener implements ServletContextListener {
/**
* Context初始化要做的事情
*/
@Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(100, 200, 5000L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100));
servletContextEvent.getServletContext().setAttribute("executor", threadPoolExecutor);
}
/**
* Context完成后调用的方法
* @param servletContextEvent
*/
@Override
public void contextDestroyed(ServletContextEvent servletContextEvent) {
ThreadPoolExecutor threadPoolExecutor= (ThreadPoolExecutor) servletContextEvent.getServletContext().getAttribute("executor");
threadPoolExecutor.shutdown();
}
}
- 具体的业务线程
/**
* 异步业务工作线程
* @author codegeekgao
*/
public class AsyncRequestProcessor implements Runnable {
private AsyncContext asyncContext;
private int time;
public AsyncRequestProcessor() {
}
public AsyncRequestProcessor(AsyncContext asyncContext, int time) {
this.asyncContext = asyncContext;
this.time = time;
}
@Override
public void run() {
System.out.println("是否异步:"+asyncContext.getRequest().isAsyncSupported());
longProcessing(time);
try {
// TODO 这里伪代码,具体的业务线程方法
asyncContext.getResponse().getWriter().write("处理时间:"+time);
} catch (IOException e) {
e.printStackTrace();
}
// 异步调用
asyncContext.complete();
}
private void longProcessing(int time) {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
以上虽然实现了io线程和业务线程隔离,但是请求还是同步阻塞的,所以在Servlet3.1以后增加了对Servlet IO异步处理。可以做以下优化:
5. 请求基类
/**
* @author codegeekgao
*/
@WebServlet(urlPatterns = "/NonBlockServlet", asyncSupported = true)
public class NonBlockServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;charset=UTF-8");
// 获取异步对象AsyncContext
AsyncContext asyncContext = req.startAsync();
// 获取非阻塞输入流
ServletInputStream inputStream = req.getInputStream();
// 异步读取io,也在MyReadListener异步处理业务
inputStream.setReadListener(new MyReadListener(asyncContext,inputStream));
//
resp.getWriter().write("异步处理中,可能业务没有处理完");
}
}
- 定义的readListener监听器
@NoArgsConstructor
@AllArgsConstructor
public class MyReadListener implements ReadListener {
private AsyncContext asyncContext;
private ServletInputStream servletInputStream;
/**
* 数据可用时,触发该方法
* @throws IOException
*/
@Override
public void onDataAvailable() throws IOException {
System.out.println("数据可以开始读啦!");
}
/**
* 数据读完后,触发该方法
* @throws IOException
*/
@Override
public void onAllDataRead() throws IOException {
try {
// 模拟业务使用1s,在这里也可以开启线程池进行业务处理
Thread.sleep(1000L);
asyncContext.getResponse().getWriter().write("业务处理完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 当出现异常后
* @param throwable
*/
@Override
public void onError(Throwable throwable) {
System.out.println("出错啦!");
}
}
- 全异步调用
上面介绍了servlet3的异步业务线程处理,也介绍了Servlet3.1后的非阻塞io请求的实现,这些调用终究还是会调用后台微服务RPC,如果我们也想把RPC请求也搞成异步的,怎么弄呢?
一般思路是:将接收到的请求放在队列中,然后用一个事件开启线程,不停的轮询队列事件,当有事件发生时,将触发触发回调函数来处理事件。
由于篇幅限制,今天先暂时写到这儿,后面继续。