之前在想如何降低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因素也需要考虑内存因素。

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

CompletableFuturerunAsyncsupplyAsync方法有不带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#asyncawait语法糖就要甜的多。

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. re正则表达式的使用

    1.查找电话号码 #! coding=utf-8import re"""查找字符串中的文本"""txt="your number ...

  2. 计蒜客 2019 蓝桥杯省赛 B 组模拟赛(三)一笔画

    #include<iostream> #include<cstring> #include<cstdio> #include<algorithm> us ...

  3. java+phantomjs实现动态网页抓取

    1.下载地址:http://phantomjs.org/download.html 2.java代码 public void getHtml(String url) { HTML="&quo ...

  4. 第43章:MongoDB-集群--Sharding(分片)--多机的搭建

    ①环境准备 服务器规划 服务器[192.168.0.75] 服务器[192.168.0.84] 服务器[192.168.0.86] mongos mongos mongos config server ...

  5. STM32-跑马灯实验

    实验环境: STM32开发板 Keil uVision4 FlyMcu.exe 一. 建立文件夹 1.建立跟文件夹"BASE"(名称任意),再在其下建立四个子文件夹 2. 在&qu ...

  6. win7里linux虚拟机安装vmware tools(ubuntu12.04)

    安装Vmware Tools工具 1.安装linux虚拟机(略) 2.虚拟机去启动,选择虚拟机à设置,“硬件”中选择CD/DVD(IDE),右侧选择“使用ISO镜像文件(M)”  -- 文件选择vmw ...

  7. [转] C++中为什么要用指针,而不直接使用对象?

    原文点击这里 问题描述 我刚从 Java 转到使用 C++ 进行面向对象开发,我发现一个很让我非常困惑的问题:C++ 中经常出现使用对象指针,而不是直接使用对象本身的代码,比如下面这个例子: C++ ...

  8. 洛谷P1064--金明的预算方案(简单背包)

    https://www.luogu.org/problemnew/show/P1064 #include<iostream> #include<algorithm> #incl ...

  9. .Net Trace->Listeners->Remove

    今天在调试一个别人写的ASP.NET老程序,log文件怎么都写不了.web.config里的trace->listeners里有这么一行: <remove type="Syste ...

  10. 31.Stack

    在Java中Stack类表示后进先出(LIFO)的对象堆栈.栈是一种非常常见的数据结构,它采用典型的先进后出的操作方式完成的.每一个栈都包含一个栈顶,每次出栈是将栈顶的数据取出,如下: Stack通过 ...