REST已然成为最流行的提供外界服务API的方式。同时,随着互联网和物联网的普及,如今的应用需要处理大量并发的请求。因此,开发高性能REST服务已经成为一个成功应用的必备条件。
我这里集中讨论Java和JVM相关技术。基于Java的REST应用比基于python和ruby的应用往往具备更好的性能。而另外一些新兴的语言如Go超出了讨论的范围。
标准
JAX-RS是Java世界定义REST API的通用标准。Jersey 是JAX-RS的官方实现,(其他实现如rest-easy)。这是一个简单的Jersey REST GET例子。
class="java" name="code">@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同步的性能差不多。
?