之前在想如何降低API的延迟,这些API里有几个比较耗时的操作且是串行执行,那通过异步执行的方式理论上可以降低运行的时间,如下图所示:

具体的实现比较简单,例如这样:

public class ParallelRetrievalExample {
final CacheRetriever cacheRetriever;
final DBRetriever dbRetriever;
ParallelRetrievalExample(CacheRetriever cacheRetriever,
DBRetriever dbRetriever) {
this.cacheRetriever = cacheRetriever;
this.dbRetriever = dbRetriever;
}
public Object retrieveCustomer(final long id) {
final CompletableFuture<Object> cacheFuture =
CompletableFuture.supplyAsync(() -> {
return cacheRetriever.getCustomer(id);
});
final CompletableFuture<Object> dbFuture =
CompletableFuture.supplyAsync(() -> {
return dbRetriever.getCustomer(id);
});
return CompletableFuture.anyOf(
cacheFuture, dbFuture);
}

用java8引入的CompletableFuture即可。

这里不再赘述。

主要讲一下这样实践遇到的坑和一些自己的理解。

性能测试

优化后的代码需要和未修改(基准)的版本做比较,要考虑在不同负载下的性能情况。

针对API的修改可以使用AB工具,比较方便,能通过设定不同的并发用户模拟不同的负载。

测试是必要的,很多直觉上会提高性能的点可能会在实际表现上收到资源的限制等原因无法提高甚至不如优化前的性能。

适合处理的任务 & 线程池的设定

我们要优化怎样的任务呢?

任务也就三大分类,计算密集,IO密集和混合,其中混合里面也可以通过细化变为前两类。

在一般的web开发中计算不太会成为瓶颈,主要是IO。

一些耗时的阻塞IO操作(数据库,RPC调用)往往是导致接口慢的原因,这里要优化的就是这类操作。

不过与其说是优化,更恰当的说法是让这些阻塞操作异步化,缩短整体的时间,这里也要注意这些任务所在的位置,如果在API的最后面的逻辑里那优化他们也没什么必要,或者在不影响业务逻辑的情况下可以把他们置前。

我们需要的怎样的线程池?

如上所说要优化的任务几乎都是阻塞IO,也就意味着这些任务占用CPU的时间很短,主要是处在waiting状态下,这种线程的增加最大的开销就是内存,对上下文切换影响较小。

其次,线程数必定要有限,java的线程过于重量,不考虑CPU因素也需要考虑内存因素。

最后还要考虑线程池耗尽的情况,最差的情况是回到没优化之前,也就是在调用者线程上执行。

CompletableFuture的runAsync和supplyAsync方法有不带Executor的版本,首先看一下默认的线程池是否合适。

private static final Executor asyncPool = useCommonPool ?
ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();

useCommonPool的判断是根据ForkJoinPool的并行度,可以简单地先认为多核下会返回true(也可以通过java.util.concurrent.ForkJoinPool.common.parallelism参数进行设定)。

而使用的commonPool()线程数量不是很多(默认和CPU核数相等),其次ForkJoinPool是设计用于短任务的运行,不适合做阻塞IO,我们要优化的主要慢操作几乎都是阻塞IO带来的。

接下去看需求比较接近的Executors.newFixedThreadPool,但通过实现不难发现他的队列是无界的,如果线程耗尽新的任务就会等待,也无法使用拒绝策略。

只有定制了,根据上面说到的需求,定制如下:

private static final ThreadPoolExecutor IO = new ThreadPoolExecutor(20, 20, 0, TimeUnit.MILLISECONDS,
new SynchronousQueue<Runnable>(),
new CallerRunsPolicy());

线程数量定长,数量的多少可以根据测试情况做下调整,使用SynchronousQueue不产生队列,拒绝策略使用在调用者线程上运行,满足了所需。

这个线程池专门为IO密集任务使用,不要让计算密集的代码使用。

在实践中遇到了使用这种方式结果测试时性能降低了5倍左右的情况,一看代码中除了从数据库获取数据还有几个for循环在做修改字段的工作,导致上下文切换带来了很大的开销。

思考

上述实现中,限制线程数量的原因是因为线程的开销(这里主要是在内存上)过大,这就意味着在这里使用了线程过重了,更好的实现应该使用类似绿色线程的技术,和系统线程进行1对多的映射。

此外这种场景下用事件驱动的方式可能会更好。

追究其核心原因还是java世界中同步阻塞操作还是占多数,而主要的优化手段底层还是使用了昂贵的线程,一些在其他语言/平台上很容易实现的扩展在java上就会遇到问题。

此外,异步没有得到语言上的支持,造成异步编程在java上比较麻烦和显式,这点C#的async和await语法糖就要甜的多。

java之后的发展还是任重而道远啊。

参考资料

apache ab

reactive design pattern 上述的图和ParallelRetrievalExample代码取自这里

多线程的代价及上下文切换

Java CompletableFuture 详解

并发之痛 Thread,Goroutine,Actor

使用异步任务降低API延迟_实践总结的更多相关文章

  1. java异步编程降低延迟

    目录 java异步编程降低延迟 一.ExecutorService和CompletionService 二.CompletableFuture(重要) 三.stream中的parallel(并行流) ...

  2. 异步化DAO的设计和实践

    目前,公司技术规划要求未来所有的服务要全面实现异步化接口,使得每个服务能达到1万/秒的单机性能.我们知道,在一个服务请求中,可能会调用其他服务,还会使用memcache.kv以及mysql等.目前,大 ...

  3. RESTful API 设计指南,RESTful API 设计最佳实践

    RESTful API 设计指南,RESTful API 设计最佳实践 网络应用程序,分为前端和后端两个部分.当前的发展趋势,就是前端设备层出不穷(手机.平板.桌面电脑.其他专用设备......). ...

  4. RESTful API 设计最佳实践

    背景 目前互联网上充斥着大量的关于RESTful API(为了方便,以后API和RESTful API 一个意思)如何设计的文章,然而却没有一个"万能"的设计标准:如何鉴权?API ...

  5. ****RESTful API 设计最佳实践(APP后端API设计参考典范)

    http://blog.jobbole.com/41233/ 背景 目前互联网上充斥着大量的关于RESTful API(为方便,下文中“RESTful API ”简写为“API”)如何设计的文章,然而 ...

  6. RESTful API 设计最佳实践(转)

    摘要:目前互联网上充斥着大量的关于RESTful API(为了方便,以后API和RESTful API 一个意思)如何设计的文章,然而却没有一个”万能“的设计标准:如何鉴权?API格式如何?你的API ...

  7. RESTful API 设计最佳实践(转)

    背景 目前互联网上充斥着大量的关于RESTful API(为方便,下文中“RESTful API ”简写为“API”)如何设计的文章,然而却没有一个”万能“的设计标准:如何鉴权?API 格式如何?你的 ...

  8. [转] 阿里研究员谷朴:API 设计最佳实践的思考

    API是软件系统的核心,而软件系统的复杂度Complexity是大规模软件系统能否成功最重要的因素.但复杂度Complexity并非某一个单独的问题能完全败坏的,而是在系统设计尤其是API设计层面很多 ...

  9. 关于RestFul API 介绍与实践

    之前演示的PPT,直接看图...     •参考链接: •RESTful API 设计最佳实践 •RESTful API 设计指南 •SOAPwebserivce和RESTfulwebservice对 ...

随机推荐

  1. 【Java】学习笔记(1)

    Java数据类型: 基本数据类型:(变量在栈中)数值型:byte(1个字节) short(2个字节) int(四个字节) long(8个字节) ,float(4字节) double(8字节) 字符型: ...

  2. Chapter5_初始化与清理_成员初始化

    在java中,成员初始化在使用之前应该都要保证已经完成初始化.对于在方法体中的局部变量,如果没有使用指定初始化的方法对成员变量进行初始化,编译器会提示一个错误.而对于类的数据成员,编译器会对这些成员赋 ...

  3. Jenkins构建maven项目跳过测试用例的命令

    在Jenkins构建项目的时候,有时候执行大量的单元测试用例需要浪费很多时间,又或者测试环境与其他dubbo,zookeeper服务器环境不通执行失败, 为了更快速的构建,可在build选项中使用如下 ...

  4. Vue history模式支持ie9

    vue 路由里面的history能让浏览器显示平常一样的链接,可以去掉#这种,但是在ie9下面会强制变成hash,因为history不支持ie9自动降级,可能就会影响美感,解决:可以在路由里面添加fa ...

  5. 1 - IO

    IO流第二天 今日内容 字节流 字符流 字节流 在前面的学习过程中,我们一直都是在操作文件或者文件夹,并没有给文件中写任何数据.现在我们就要开始给文件中写数据,或者读取文件中的数据 字节输出流Outp ...

  6. ABP学习之旅

    1.我使用ABP的启动模板(http://www.aspnetboilerplate.com/Templates)来创建一个Web应用程序. 2.加载项目解决方案 在abp根据模板创建解决方案后,编译 ...

  7. 偏差(Bias)和方差(Variance)——机器学习中的模型选择zz

    模型性能的度量 在监督学习中,已知样本 ,要求拟合出一个模型(函数),其预测值与样本实际值的误差最小. 考虑到样本数据其实是采样,并不是真实值本身,假设真实模型(函数)是,则采样值,其中代表噪音,其均 ...

  8. mybatisGenerator代码生成器

    使用mybatisGenerator可以生成实体类,Mapper接口以及对应xml文件.本文介绍如何使用. 可以直接从本人github下载,只需按照如下步骤即可: 1.导入项目至idea中,项目结构如 ...

  9. JAVA 8 主要新特性 ----------------(五)Lambda方法引用与构造器引用

    一.Lambda方法引用 当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法引用!(实现抽象方法的参数列表,必须与方法引用方法的参数列表保持一致!) 方法引用: 使用操作符 “::” 将 ...

  10. JavaScript-BOM与DOM

    BOM与DOM BOM: Browser Object Model(浏览器对象模型),即把 浏览器 当做一个对象来看待.BOM 除了可以访问文档中的组件之外,还可以访问 浏览器组件,比如页面中的 na ...