写点什么

苏宁 11.11:如何基于异步化打造会员任务平台?

基于异步化的性能优化实践

  • 2018-11-02
  • 本文字数:3973 字

    阅读完需:约 13 分钟

苏宁11.11:如何基于异步化打造会员任务平台?

本文为『InfoQ x 苏宁 2018 双十一』技术特别策划系列文章之一。

背景

苏宁会员任务平台是覆盖聚合电商、体育、金融、PPTV、直播、红孩子等各个业态,平台会实时获取用户的画像信息来计算用户在客群中的分布及画像属性,从而实时判断用户是否满足相关场景下任务,若满足相关场景以后可以领取任务下所有奖项;任务类型包含了订单红包、母婴、Super 会员、直播、双签、金融升级存等等。在大促特别是双十一期间,任务中心产品对于各个业态的引流,会员的留存及转化来说是一个重要的工具。

问题

因任务平台业务逻辑复杂、实时性要求高,涉及多个外围系统服务及数据调用;一期系统上线后部分功能遇到性能问题,例如聚合页打开时间过长,首先聚合页上要展示用户能看到的任务列表,以及当前用户是否达到领取条件,其次每个任务需要展示的状态依赖于后台多种信息的聚合,包括不在有效时间范围内、当前时段库存、可供领取的总库存、领取频次等。复杂逻辑和实时要求导致 TPS 在上线压测的时候没有能够达到一个理想预期效果。

即将到来的”双十一”流量高峰, 可以预见会使得超过现有的任务系统的 TPS 的峰值, 从而导致任务系统在”双十一”的场景下很容易触碰到性能瓶颈,影响用户体验;因此需要对苏宁任务平台的核心功能做性能优化, 提升实时性复杂业务逻辑场景下的性能, 以便于应对任务平台的流量暴涨以及双十一流量高峰。

定位

现有的每个任务可能依赖于多个异构系统的服务或者数据,例如直播任务及订单任务来自于不同的系统的服务,并且有些场景是基于外围系统的数据进行逻辑计算,有些则是通过服务接口调用的方式。

代码示例:

复制代码
public ResultDTO checkAndGetInfo() {
A a = getA();
B b = getB();
C c = getC();
......
ResultDTO result = computeResult(a, b, c ...);
return resultDTO;
}

由于页面实时性要求高,逻辑复杂,对于某个任务是否展示需要调用多个外围接口,响应时间不可控,理论上根据任务的复杂性可能涉及多个客群,调用次数及响应时间不可控。性能主要在响应时间不可控。

某个任务状态要调用多个本地接口或者外围接口。

主要思路:异步,缓存,线程池

针对以上定位到位问题,考虑到实时调用外围接口的方案会导致响应时间不可控,采用 NIO 的思想,对整个调用链进行梳理,尽量异步化调用,同时增加适当过期时间的缓存,达到性能优化的目的。

在一期设计的时候已经从业务逻辑的角度做了拆分,将不同生命周期的逻辑异步化处理,例如奖励是通过 kafka 推送到奖励资源系统异步发放的。

上述从业务生命周期角度分析,通过切分业务流程,达到优化的方式已经不能满足系统性能需求,需要从技术上考虑更细粒度的异步化处理方式。

优化方案的选择及演进

Kilim

Kilim 是一个 java 的协程框架,利用字节码技术编织技术将普通代码转化为支持协程的代码,当时是基于同步的思路下,想利用协程优化同步并发处理的能力。经过调研业界实践应用相对较少,因此考虑到项目开发周期等因素,没有采用 Kilim 方案。

Guava Listenable Future:

JDK 5 引入了 Future 模式。 Future 接口是 Java 多线程 Future 模式的实现,在 java.util.concurrent 包中,可以来进行异步计算。

Future 模式是多线程设计常用的一种设计模式。Future 模式可以理解成:有一个任务,提交给了 Future,Future 替我完成这个任务。期间我自己可以去做任何想做的事情。一段时间之后,我就便可以从 Future 那儿取出结果。

复制代码
ExecutorService executor = ...;
Future f = executor.submit(...);
f.get();

Future 接口可以构建异步应用,但依然有其局限性。它很难直接表述多个 Future 结果之间的依赖性。实际开发中,我们经常需要达成以下目的:

  1. 将多个异步计算的结果合并成一个
  2. 等待 Future 集合中的所有任务都完成
  3. Future 完成事件(即,任务完成以后触发执行动作)

Future 虽然可以实现获取异步执行结果的需求,但是它没有提供通知的机制,我们无法得知 Future 什么时候完成。

要么使用阻塞,在 future.get() 的地方等待 future 返回的结果,这时又变成同步操作。要么使用 isDone() 轮询地判断 Future 是否完成,这样会耗费 CPU 的资源。

Guava 的 Listenable Future 对其做了改进,支持注册一个任务执行结束后回调函数。

复制代码
ListenableFuture<String> listenableFuture =
listeningExecutor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return "";
}
});
Futures.addCallback(ListenableFuture<V>,FutureCallback<V>, Executor)

其中 FutureCallback 是一个包含 onSuccess(V),onFailure(Throwable) 的接口:

复制代码
Futures.addCallback(ListenableFuture, new FutureCallback<Object>() {
public void onSuccess(Object result) {
// do something on success
}
public void onFailure(Throwable thrown) {
// do something on failure
}
});

这也是一开始试验的方案,确定好了异步化的思路,自然联想到了增强版的 Listenable Future,虽然在任务完成时可以回调函数通知,但是仍然是阻塞的,主线程仍然要等待异步线程完成任务通知。

Completable Future

Java8 的 CompletableFuture 参考了 Guava 的 ListenableFuture 的思路,CompletableFuture 能够将回调放到与任务不同的线程中执行,也能将回调作为继续执行的同步函数,在与任务相同的线程中执行。它避免了传统回调最大的问题,那就是能够将控制流分离到不同的事件处理器中。

CompletableFuture 弥补了 Future 模式的缺点。在异步的任务完成后,需要用其结果继续操作时,无需等待。可以直接通过 thenAccept、thenApply、thenCompose 等方式将前面异步处理的结果交给另外一个异步事件处理线程来处理。

复制代码
CompletableFuture completableFuture = new CompletableFuture();
completableFuture.whenComplete(new BiConsumer() {
@Override
public void accept(Object o, Object o2) {
//handle complete
}
}); // complete the task
completableFuture.complete(new Object());//api method
completableFuture.thenApply(Function f); //api method
completableFuture.thenAccept(Consumer c); //api method

CompletableFuture 提出了 CompletionStage 的概念,代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另外一个阶段。

一个阶段的计算执行可以是一个 Function,Consumer 或者 Runnable。比如:

复制代码
stage.thenApply(x -> square(x)).thenAccept(x -> System.out.print(x)).thenRun(() -> System.out.println());

一个阶段的执行可能是被单个阶段的完成触发,也可能是由多个阶段一起触发。

与 Guava ListenableFuture 相比,CompletableFuture 不仅可以在任务完成时注册回调通知,而且可以指定任意线程,实现了真正的异步非阻塞。

Servlet 3.0

image

传统 Servlet 2.x web 容器处理 http 请求时是为每一个请求分配一个线程,处理完请求再释放线程,如果请求处理的比较慢或者请求过多,就可能达到线程池达到上限,这时候后续的用户请求就会处于等待状态或者超时,这里用户请求和处理请求是一个线程,Servlet 3.0 开始提供了 AsyncContext 用来支持异步处理请求,主要是把请求线程和工作线程分开,将耗时的业务处理工作交给另外一个线程来完成。

复制代码
@WebServlet(urlPatterns = "/servlet3",asyncSupported = true)
public class Servlet3 extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 在子线程中执行业务调用,并由其负责输出响应,主线程退出
AsyncContext ctx = request.startAsync();
new Thread(new Executor(ctx)).start();
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
class Executor implements Runnable {
private AsyncContext ctx = null;
public Executor(AsyncContext ctx){
this.ctx = ctx;
}
public void run(){
try {
Thread.sleep(3000);
ServletRequest request = ctx.getRequest();
ctx.dispatch("/index.jsp");
ctx.complete();
} catch (Exception e) {
e.printStackTrace();
}
}
}

最终方案

最终选定 Completable Future + Servlet 3.0 的方案,前台 web 接口层采用 Serlvet 3.0,后台服务层采用 Completable Future。

验证

优化前压测数据:

图 1:在访问聚合页 100 并发情况下的数据,TPS 值 3235

【图 1】

image
image

图 2:在访问聚合页 200 并发情况下的数据,TPS 值 3322,在用户并发量增加的时候,因依赖外部接口服务和原有的系统设计接口调用方法导致 TPS 基本不会随并发量的增加而提高。

【图 2】

image
image

优化后压测数据

在访问聚合页 100 并发情况下的数据,TPS 值 5869,相对于优化之前的 TPS 有明显的提升。

【图 3】

image
image

在访问聚合页 150 并发情况下的数据 TPS 值 8581,在提高并发量的时 TPS 有显著的提高,说明优化后的效果很明显,也证实了优化方案是可行的。

【图 4】

image

总结

利用异步化来提升系统性能是一个整体、全链路的工作,仅仅依靠业务上的异步化,或者服务层的异步化远远不够,随着不同技术方案的选择及演进,对异步非阻塞模型有了更深入的了解之后,从前台用户请求到后端服务层处理,根据一整条链路的上每一层场景的不同,需要选取不同的异步化技术方案,才能达到系统整体性能提升的目的。

作者

葛苏杰, 现担任苏宁易购 IT 总部技术经理职位, 从事多年的电商系统 2C 业务开发, 对于高可用、高并发的分布式系统的 JVM 性能调优、SQL 优化、Cache、NIO、NGINX 等相关技术有丰富的经验。

2018-11-02 10:378875

评论 2 条评论

发布
用户头像
之前的优化思路,一个是业务层面的调整,业务细化,控制流程,减少业务计算时间;另外一个是数据库层面的优化,缓存或sql优化;配置层面的tomcat调优,jvm调优等。 代码层面的优化有考虑过ForkJoin,Future。
2018-11-16 15:38
回复
没有更多了
发现更多内容

还愁追不到女神吗?一键生成舔狗日记,一秒速成舔狗之王

不脱发的程序猿

程序人生 28天写作 二月春节不断更 舔狗文化

话题讨论 | 现实中程序员是怎样飞快敲代码的?

xcbeyond

程序人生 话题讨论

话题讨论 | 程序员是做前端开发好,还是后端开发好呢?

xcbeyond

程序人生 话题讨论

为图片添加Emoji,微信这隐藏功能让你不花冤枉钱

彭宏豪95

微信 效率 效率工具 emoji

算力蜂系统开发|算力蜂软件APP开发

系统开发

浅谈nodejs进程和线程

梁龙先森

大前端 nodejs 2月春节不断更

用Stylish精简极客时间专栏页面

Tao

CSS

架构师不至于“架构”-《架构师应该知道的37件事》阅读笔记

Harris

读书笔记 架构 架构师

架构设计篇之微服务实战笔记(三)

小诚信驿站

架构师 刘晓成 小诚信驿站 28天写作 架构师成长笔记

话题讨论 | 你在互联网大厂是个啥级别?

架构精进之路

话题讨论 28天写作 话题王者

JVM又曾放过谁,垃圾终将被回收!

Simon郎

Java 大数据 架构 后端 JVM

2021阿里总监最新手码BAT等大厂面经!GitHub已标星86.2K

比伯

Java 编程 架构 面试 程序人生

话题讨论 | 比特币攻击重现江湖,你准备好了吗?

程序员架构进阶

话题讨论 28天写作 2月春节不断更 话题王者 勒索攻击

窝家恶补三月,字节跳动三面,终于喜提offer!分享面试感受

Java架构之路

Java 程序员 架构 面试 编程语言

话题讨论 | mongodb拥有十大核心优势,为何国内知名度不是很高?

杨亚洲(专注MongoDB及高性能中间件)

MySQL 数据库 mongodb 话题讨论 分布式数据库mongodb

Dapr 知多少 | 分布式应用运行时

圣杰

架构 云原生 k8s dapr

微信十年,弹指一挥间

彭宏豪95

微信 产品 互联网 写作

Laravel来信|Event

LeastCoding

laravel Event 观察者模式

泰山版震撼来袭!阿里巴巴2021年Java程序员面试指导小册已开源

Java架构追梦

Java 架构 面试 金三银四 跳槽

Java岗四面字节跳动成功之前,我都刷了那些面试题以及做了那些准备!

Java架构之路

Java 程序员 架构 面试 编程语言

2021最新百度/平安/蚂蚁金服/腾讯/拼多多面经总结(附答案解析)

比伯

Java 编程 架构 面试 计算机

什么!?金三银四,2021年阿里最新面试题惨遭泄露?

Java架构之路

Java 程序员 架构 面试 编程语言

颠覆技术-智能合约的说明文

CECBC

区块链

为什么不推荐使用汉字作为密码?

不脱发的程序猿

程序人生 密码学 28天写作 二月春节不断更

人人矿场APP开发|人人矿场系统软件开发

系统开发

有赞 Flink 实时任务资源优化探索与实践

Apache Flink

flink

GitHub上已获赞百万!阿里架构师10年磨一剑打造的Java面试小抄(2021版)开源分享

Java架构师迁哥

阿里巴巴云原生应用安全防护实践与 OpenKruise 的新领域

阿里巴巴云原生

容器 运维 云原生 k8s 调度

电影台词反向搜索视频片段,这个工具也太好用了吧|33 台词

彭宏豪95

效率 效率工具 电影

流媒体传输协议之 RTMP

阿里云视频云

TCP 音视频 RTMP 传输协议 流媒体;

使用doom-emacs三个月后, 春节期间从零配置一份自己的emacs(附详细文档)

lmymirror

苏宁11.11:如何基于异步化打造会员任务平台?_架构_葛苏杰_InfoQ精选文章