REST已然成为最流行的提供外界服务API的方式。同时,随着互联网和物联网的普及,如今的应用需要处理大量并发的请求。因此,开发高性能REST服务已经成为一个成功应用的必备条件。

我这里集中讨论Java和JVM相关技术。基于Java的REST应用比基于python和ruby的应用往往具备更好的性能。而另外一些新兴的语言如Go超出了讨论的范围。

标准

JAX-RS是Java世界定义REST API的通用标准。Jersey 是JAX-RS的官方实现,(其他实现如rest-easy)。这是一个简单的Jersey REST GET例子。

@Path("myresource")
public class MyResource {

    /**
     * Method handling HTTP GET requests. The returned object will be sent
     * to the client as "text/plain" media type.
     *
     * @return String that will be returned as a text/plain response.
     */
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String getIt() {
        try {
            Thread.sleep(100); // do some job
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "Got it!";
    }
}

标准Jersey及其底层servlet实现最大的问题是每处理一个讲求,就需要一个相应的Socket线程处理。在成千上万的并发请求下,系统性能将会明显下降。

异步

为了解决这个问题,JAX-RS提出了异步的解决方案。在这种模式下,请求线程和用户连接之间的联系被打破。I/O容器不再假设等待请求完成,在关闭连接。这是上面简单的Jersey REST GET例子的异步实现:

@Path("myresource")
public class MyResource {
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public void asyncGet(@Suspended final AsyncResponse asyncResponse) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                String result = veryExpensiveOperation();
                asyncResponse.resume(result);
            }

            private String veryExpensiveOperation() {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return "Got it!";
            }
        }).start();

    }
}

非阻塞

标准Jersey及其底层servlet实现最大的问题是每处理一个讲求,就需要一个相应的Socket线程处理。在成千上万的并发请求下,系统性能将会明显下降。

为了解决这个问题,JAX-RS提出了异步的解决方案。在这种模式下,请求线程和用户连接之间的联系被打破。I/O容器不再假设等待请求完成,在关闭连接。这是上面简单的Jersey REST GET例子的异步实现:

@Path("myresource")
public class MyResource {
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public void asyncGet(@Suspended final AsyncResponse asyncResponse) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                String result = veryExpensiveOperation();
                asyncResponse.resume(result);
            }

            private String veryExpensiveOperation() {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return "Got it!";
            }
        }).start();

    }

}


使用异步技术可以大幅提高吞吐量,但是后端的工作已然需要创建独立的线程完成。系统的负担已然很大。

另外一种完全不同的实现是把“非阻塞”应用到方方面面,不仅仅是HTTP请求处理。在这种模式下,系统仅仅需要少量线程来检测事件的发生。使用这种方法最为重要的是将任务分解为一个个小部分。每个部分的执行事件非常短。Vert.x就是这样一个非阻塞,事件驱动,跨语言的开发框架。Node.JS则是另外一个JavaScript领域很流行的非阻塞框架。

上面简单REST服务的Vert.x实现如下

public class RestServer extends Verticle {
    public void start() {

        RouteMatcher rm = new RouteMatcher();
        rm.get("/myapp/myresource", new Handler() {
            public void handle(final HttpServerRequest req) {
                // sleep 100 ms
                vertx.setTimer(100, new Handler() {
                    public void handle(Long timerID) {
                        req.response().end("Got it");
                    }
                });
            }
        });

        vertx.createHttpServer().requestHandler(rm).listen(8080);
    }
}

我这里比较了jersey同步,异步和vert.x的实现。这里的服务实现工作100毫秒,然后返回结果。我使用gatling模拟了1000个用户并发,每个用户重复请求100次。测试结果显示vertx具有最高的吞吐率(即每秒中完成请求次数)5K和最低的平均时延105ms。Jersey异步居中,达到1K的吞吐量和300ms的平均时延。Jersey同步表现最差,吞吐量只有70,平均时延高达12秒。完整的例子在我的github项目vertx-jersey-benchmark。当然,这不意味着Vertx是所有人的最佳选择。Vertx也有很多问题。例如,Vertx缺少JAX—RS那样方便易懂的annotation来描述服务资源路径,参数等等。有一个第三方模块vertx-jersey试图将vertx和jersey集成。但是目前它的性能和jersey同步的性能差不多。