浅谈微服务之网关(一)

版权声明:本文为博主原创文章版权归作者和CSDN共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。 https://blog.csdn.net/javaee_gao/article/details/89673107

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异步请求流程:
    在这里插入图片描述
    下面是代码演示:
  1. 接收请求类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));
    }
}
  1. 异步监听器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");
    }
}
  1. 上下文监听器
/**
 * 上下文监听器(监控整个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();
    }
}
  1. 具体的业务线程
/**
 * 异步业务工作线程
 * @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("异步处理中,可能业务没有处理完");
    }

}
  1. 定义的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请求也搞成异步的,怎么弄呢?
    一般思路是:将接收到的请求放在队列中,然后用一个事件开启线程,不停的轮询队列事件,当有事件发生时,将触发触发回调函数来处理事件。

由于篇幅限制,今天先暂时写到这儿,后面继续。由于本人能力有限,如果有任何错误或者不当之处,可以留言给我,谢谢。

猜你喜欢

转载自blog.csdn.net/javaee_gao/article/details/89673107
今日推荐