QCon 演讲火热征集中,快来分享技术实践与洞见! 了解详情
写点什么

苏宁 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:379124

评论 2 条评论

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

不会 Lua?Python 助你快速上手 Apache APISIX 插件开发

API7.ai 技术团队

Python Apache 开源 API网关 APISIX

区块链“牵手”金融 面临诸多挑战

CECBC

面试官问的那些Java原理你都懂吗,Java面试手写代码题目

Java 程序员 后端

学生管理系统整理架构设计

小智

架构训练营

【Vuex 源码学习】第四篇 - Vuex 中 Getters 的实现

Brave

源码 vuex 9月日更

Vite + Vue3 + OpenLayers 手动控制缩放级别

德育处主任

大前端 地图 vite openlayers Vue 3

面试官都被搞懵了,阿里P7亲自讲解

Java 程序员 后端

webrtc simulcast 开启

webrtc developer

webrtc、 simulcast,

面试讲不清MySQL索引底层,Java面试

Java 程序员 后端

面试竟然被这31道Java基础题难倒了,被阿里面试官征服了

Java 程序员 后端

乘风破浪携手共赢——博睿数据深圳渠道大会圆满落幕

博睿数据

JavaScript 进阶(二)下

Augus

JavaScript 9月日更

面试官手里那些秀你一脸的求质数大法,疯狂复习半个月

Java 程序员 后端

南京主题展2021国际大数据产业展会/论坛会

南京专业智博会

大数据 智博会 南京智博会

(AL智博会)2021南京国际人工智能主题展

南京专业智博会

人工智能展览会 人工智能论坛会 人工智能智博会

雪花算法,什么情况下发生 ID 冲突?

Java 架构 分布式 算法

直播预告丨走进云溪数据库之高可用方案

云计算

联邦学习框架浅析

趣链科技

恒源云(GpuShare)_GPU租用保姆级教程,助力深度学习训练!

恒源云

模块三作业:学生管理系统架构设计文档

apple

面试被问Tomcat整体架构设计,深入浅出Java开发

Java 程序员 后端

研发工具链介绍

百度开发者中心

学习 最佳实践 方法论 研发工具

《中国梦》打造数亿中产阶级!疯了,疯了,全涨疯了!!

CECBC

Apache APISIX 为 KubeSphere 提供更好用的网关及 K8S Ingress Controller

API7.ai 技术团队

Apache 开源 API网关 APISIX KubeSphere

物联世界2021南京物联网展览会

南京专业智博会

物联网展览会 物联网论坛会 物联网智博会

2021南京国际智慧工地装备展览会

南京专业智博会

智博会 智慧工地展览会 智慧工地论坛会

2021年第十四届南京智慧城市展览会

南京专业智博会

智博会 智慧城市展览会 智慧城市论坛会

交Y所K线机器人系统开发功能介绍(源码搭建)

量化系统19942438797

机器人 k线

Serverless 工程实践 | Serverless 应用开发观念的转变

阿里巴巴云原生

Serverless Serverless架构

Alibaba船新制作“Java架构核心宝典”,全是流行技术,限时开放

Java 程序员 架构 面试 计算机

阿里大佬怒写“Java初学者宝典”,让你就业没压力

Java 阿里巴巴 程序员 面试 计算机

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