响应式异步非阻塞编程在服务端的应用

开发
对于服务端的开发者而言,我们总有一个共同的目标,那就是如何用更少的资源获得足够的性能来支持我们的服务!,我们不是在性能优化中,就是在性能优化的路上。

作者 | 搜狐视频 赵文浩

1、前言

对于服务端的开发者而言,我们总有一个共同的目标,那就是如何用更少的资源获得足够的性能来支持我们的服务!,我们不是在性能优化中,就是在性能优化的路上。作为Javaer我们,服务性能优化的武器库中,异步和并发是永远不会过时的两个。

然而理想很美好,现实很骨感:

  • 异步编程的思维方式同大脑的命令式思维是背道而驰的。在Java的世界中,直到目前Jdk17,也没有async/await来帮我们解决回调地狱的问题,强行大量异步,将深陷回调地狱而不能解脱...
  • 并发调用方面,大量编排异步线程任务,不仅会造成资源的快速消耗,也会导致业务流程的实现难以理解,正所谓:写这段代码时能理解它的只有我和God,一个月后能理解它的就只有God了...。

在服务端引入响应式编程,是解决如上问题的一个好的思路。

下面,我以搜狐视频服务端PUGC团队在PUGC视频对象创建接口的重构工作的实践为背景,介绍响应式(基于RxJava)异步非阻塞编程在服务端的应用在服务端的应用。

2、问题概述

PUGC视频对象创建接口,从业务角度看:用于为用户上传视频数据前在服务端为其分配一个视频对象记录,该视频对象记录被用于描述当前视频的完整生命周期,从技术角度看:它是一个聚合接口,通过组合多个上游接口数据,主业务过程涉及:帐号、内容审核、视频对象存储、转码、CDN调度等,实现业务过程。

该接口代码年代久远,从提交记录中可查到的最早的历史在2013年,随着业务的变化,开发人员的变更,代码中充斥着各种各样的味道。不论是从业务角度、性能角度亦或是日常维护角度看,都难以满足需要。

  • 业务流处理过程逻辑冗长,逻辑复杂,可读性差,难于维护。
  • 并发任务较多,但缺少合理的编排措施,是否需要异步或并发控制完全随意。
  • 几乎没有正确的异常处理。
  • 充满Ctrl+C/V的味道
  • ......

为了解决以上的诸多问题,我们开始了重构(重写)之路。

3、读懂业务流

重构的原则是保证接口实现的业务规则的一致性,通过仔细研读代码,整理出接口中实现的诸多特性和业务规则。

视频对象创建主流程如下图所示:

图片

 注:时序图中描述的是主流程中的关键点,因篇幅所限并未列出每个调用的具体细节

从用户请求到达服务端开始划分业务执行阶段:

  1. 数据校验阶段:帐号状态/入参合规/内容重复性
  2. 对象创建阶段: 视频对象存储
  3. 关联数据对象创建阶段:
  • 3.1 必要内容:自媒体业务/视频对象扩展
  • 3.2 可选内容:视频元数据/转码/其它
  • 4 上传准备:从CDN调度视频内容存储结点
  • 5 结束

这其中每个阶段的内容都需要通过若干个上游接口调用协作来完成,因篇幅所限,并未完全描述每个阶段的具体实现细节。

4、选择合适的架构

通过分析业务流的特点:

  • 业务流程链路较长
  • 需要协作的上游接口较多
  • IO操作为主

结合接口重构核心目标:

  • 代码应该准确描述业务流
  • 合理的并发任务编排
  • 准确异常处理
  • 降低资源占用,提升接口性能

我们决定基于响应式异步非阻塞架构对接口进行重构:

  • 响应式编程是面向数据流的,业务流的特点可知,它有明确的阶段性,数据在每个阶段变换和流动
  • 整个过程涉及较多的接口调用和异步任务编排,基于异步非阻塞可显著减少异步线程的依赖
  • 响应式编程中提供了完善的异常处理机制,特别对异步环境下的异常处理非常友好

涉及的基础组件

  • 核心响应式编程框架: ReactiveX/RxJava
  • Http客户端 Vertx WebClient
  • CacheCloud响应式客户端 sohutv-basic / cachecloud-client
  • Dubbo响应式客户端sohutv-basic / dubbo-reactive-consumer

同步阻塞模式适配响应式异步非阻塞

因后续章节代码示例中主要以dubbo服务接口调用为主,所以我以视频服务端团队基于dubbo-2.6.5版本实现的dubbo响应式客户端dubbo-reactive-consumer为例,介绍将传统的同步阻塞模式适配响应式异步非阻塞的思路。

Dubbo是目前视频服务端用使用的较多的RPC框架,目前最新的版本是Dubbo3.x(注:由于历史原因,目前团队使用的版本还停留在2.6.5版本)。在Spring环境中,比较简便的注入dubbo服务接口代理对象(简称接口对象)的方式是通过@com.alibaba.dubbo.config.annotation.Reference注解自动注入,示例如下:

@Component
public class DubboSyncExample {

@Reference
private VideoInfoService videoInfoService;

@Nullable
public VideoInfo getVideoInfo(long vid) throws Exception {
return videoInfoService.getVideoInfo(vid);
}
}

默认情况下,这种方式注入的是基于同步阻塞模式接口对象,由于dubbo客户端基于Netty实现,所以它天生是是异步的。一般情况下,如果希望通过异步方式使用dubbo客户端可以按如下方式操作:

@Component
public class DubboAsyncExample {

/**
* 标记为异步调用
*/
@Reference(async = true)
private VideoInfoService videoInfoService;

@NotNull
public Future<VideoInfo> getVideoInfo(long vid) throws Exception {
// 提交异步请求
videoInfoService.getVideoInfo(vid);
// 通过ThreadLocal保存了对当前请求上下文的引用RpcContext
RpcContext rpcContext = RpcContext.getContext();
// 从RpcContext中获取当前请求的Future<T>
Future<VideoInfo> future = rpcContext.getFuture();
return future;
}
}

为支持响应式框架,同时保持通过@com.alibaba.dubbo.config.annotation.Reference注解自动注入接口对象的方式,我们的实现过程如下:

将接口对象包装成响应式引用类型:ReactiveReference<Service>

import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.core.Single;

import java.util.Optional;

public interface ReactiveReference<Service> {

@NotNull
<T> Maybe<T> maybe(@NotNull Function<Service, T> f);

@NotNull
<T> Single<Optional<T>> single(@NotNull Function<Service, T> f);
}

接口中提供了两个方法用到了整个单值类型的响应式流:

  • Maybe<T>: 当向下游Observer发射的对象为null时,会结束整个流的生命周期
  • Single<T>: 在整个流的生命周期中不允许发射null对象

实现响应式引用实现ReactiveReferenceImpl<Service>

import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.core.Single;
import org.jetbrains.annotations.NotNull;

import java.util.Optional;

final class RxJavaReactiveReferenceImpl<Service> implements ReactiveReference<Service> {

@NotNull
final Service service;

RxJavaReactiveReferenceImpl(@NotNull Service service) {
this.service = service;
}

/**
* maybe
*/
@Override
@NotNull
public <T> Maybe<T> maybe(@NotNull Function<Service, T> f) {
// 将Single<Optional<T>>解包成Maybe<T>
return this.single(f).mapOptional($ -> $);
}

/**
* single
* @param actual 实际的调用
*/
@Override
@NotNull
public <T> Single<Optional<T>> single(@NotNull Function<Service, T> actual) {
// 延迟创建一个Single流
return Single.defer(() ->
Single.create(
emitter -> {
DubboSubscription<T> subscription =
new DubboSubscription<>(
() -> actual.apply(service), // 将实际调用包装成 java.util.concurrent.Callable<T>
emitter::onSuccess, // 执行成功向下游发射结果
emitter::onError // 执行异常,向下游发射异常信息
);
emitter.setCancellable(subscription::cancel); // 支持流取消时的回调
subscription.request(1L);
}
)
);
}
}

DubboSubscription<T>用于同Dubbo框架桥接,实现如下:

import com.alibaba.dubbo.remoting.exchange.ResponseCallback;
import com.alibaba.dubbo.rpc.Result;
import com.alibaba.dubbo.rpc.RpcContext;
import com.alibaba.dubbo.rpc.protocol.dubbo.FutureAdapter;
import org.jetbrains.annotations.NotNull;
import org.reactivestreams.Subscription;

import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;

public class DubboSubscription<T> implements Subscription {

/**
* dubbo rpc调用的结果Future引用,用于维护生命周期
* 当响应式流被取消时需要取消这个Future
*/
@NotNull
private final AtomicReference<Future<T>> futureRef = new AtomicReference<>();

/**
* 实际的接口调用
*/
@NotNull
private final Callable<T> actual;
/**
* 当rpc调用成功时执行
*/
@NotNull
private final Consumer<Optional<T>> onNext;
/**
* 当rpc调用异常时执行
*/
@NotNull
private final Consumer<Throwable> onError;

public DubboSubscription(@NotNull Callable<T> actual, @NotNull Consumer<Optional<T>> onNext, @NotNull Consumer<Throwable> onError) {
this.actual = actual;
this.onNext = onNext;
this.onError = onError;
}

@Override
public void request(long n) {
try {
RpcContext rpcContext = RpcContext.getContext();
// 异步执行dubbo rpc请求
this.actual.call();
// 取到当前请求的Future<T>
Future<T> future = rpcContext.getFuture();
if (!(future instanceof FutureAdapter)) {
// Future<T>类型验证,要求是 com.alibaba.dubbo.rpc.protocol.dubbo.FutureAdapter,
// 因为FutureAdapter包装了 com.alibaba.dubbo.remoting.exchange.ResponseFuture对象,它支持设置结果回调
throw new UnsupportedOperationException(String.format("The future is not a instance of [%s]", FutureAdapter.class.getName()));
}
futureRef.set(future); // 设置Future引用
FutureAdapter<T> futureAdapter = (FutureAdapter<T>) future;
futureAdapter
.getFuture()
.setCallback(
new ResponseCallback() {
@SuppressWarnings("unchecked")
@Override
public void done(Object response) {
try {
T t = (T) ((Result) response).recreate(); // 结果对象反序列化
DubboSubscription.this.onNext.accept(Optional.ofNullable(t)); // dubbo rpc调用成功
} catch (Throwable e) {
DubboSubscription.this.onError(e); // 一般为反序列化操作引起的异常
}
}

@Override
public void caught(Throwable e) {
// // dubbo rpc调用异常
DubboSubscription.this.onError(e);
}
}
);
} catch (Throwable e) {
// 其它异常
onError(e);
}
}

private void onError(Throwable e) {
// 执行onError回调
DubboSubscription.this.onError.accept(e);
}

/**
* 响应式流取消时回调
*/
@Override
public void cancel() {
Future<T> future = futureRef.get();
if (future != null && !future.isDone()) {
future.cancel(true);
}
}
}

于是基于我们的响应式dubbo客户端,服务接口的调用方式为:

@Component
public class DubboReactiveExample {

@Reference
private ReactiveReference<VideoInfoService> videoInfoService;

/**
* 响应式请求
*/
@NotNull
public Maybe<VideoInfo> maybeVideoInfo(long vid) {
// 提交响应式请求
return videoInfoService.maybe(service -> service.getVideoInfo(vid));
}

/**
* 响应式请求
*/
@NotNull
public Single<Optional<VideoInfo>> singleVideoInfo(long vid) {
// 提交响应式请求
return videoInfoService.single(service -> service.getVideoInfo(vid));
}
}

注:此处的实现为常规的Spring生命周期处理,与本文无关,具体实现细节不再赘述

扩展com.alibaba.dubbo.config.spring.beans.factory.annotation.ReferenceAnnotationBeanPostProcessor使dubbo自动注入框架支持ReactiveReference<Service>类型,自动设置为异步模式,且向目标Component中注入的实际类型为ReactiveReferenceImpl<Service>

5、实践过程

我们在读懂业务流一节中对视频对象创建主流程做了说明,然后将主流程分成了5个阶段,现在我们使用RxJava代码来描述这5个阶段(注:后续代码仅用于演示主流程的实现过程,并不是完整的实现过程)。

输入参数Args

  • 为保证每个流处理结点的无状态性,Args被设计为不可变
/**
* 入参包装
* 为保证每个流处理结点的无状态性,Args被设计为不可变
*/
@Builder
@Getter
public class Args {
@NotNull
private final long accountId;

private final String title;
private final String coverImg;
private final String other;

/**
* 视频内容校验值
*/
private final String signature;
... // other fields
}

过程结果PassObj

  • 为保证每个流处理结点的无状态性,PassObj被设计为不可变
/**
* 响应式流处理过程对象包装
* 为保证每个流处理结点的无状态性,PassObj被设计为不可变
*/
@Builder(toBuilder = true)
@Getter
public class PassObj {
private final Args args;
private final Account account;
private final boolean argsAccepted;
private final VideoInfo videoInfo;
private final VideoInfoExtend videoInfoExtend;
private final PugcInfo pugcInfo;
private final CDNNodeInfo cdnNodeInfo;
... // other fields
}

响应式流主干

  • 清晰地表达了业务流的执行过程
@NotNull
public Single<PassObj> create(@NotNull Args inputArgs) {
return Single.fromCallable(() -> PassObj.builder().args(inputArgs).build())
.flatMap(this::checkStage) // 1 数据校验阶段:帐号状态/入参合规/内容重复性
.flatMap(this::createVideoObjectStage)// 2 对象创建阶段: 视频对象存储
.flatMap(this::createCorrelateObjectStage)// 3 关联数据对象创建阶段
.flatMap(this::cdnDispatchStage)// 4 上传准备:从CDN调度视频内容存储结点\
.doOnSuccess(passObj -> { // 5 结束
// 视频对象创建成功
})
.doOnError(e -> log.error(e.getMessage(), e))
;
}

每个阶段的详细实现:

数据校验阶段:帐号状态/入参合规/内容重复性

/**
* 1 数据校验阶段:帐号状态/入参合规/内容重复性
*/
@NotNull
public Single<PassObj> checkStage(@NotNull PassObj passObj) {
return this.checkAndGetAccount(passObj) // 验证并获取account对象
.map(account -> passObj.toBuilder().account(account).build())// 帐号状态校验通过, 重建passObj,并装配account
.flatMap(_passObj ->
this.checkInputArgs(_passObj) // 入参合规性校验
.map(argsAccepted -> _passObj.toBuilder().argsAccepted(argsAccepted).build())// 入参合规性校验通过, 重建passObj,并装配校验结果
)
.flatMap(_passObj -> // 视频元数据信息校验
this.videoMetaService.single(s -> s.getVideoMetaBySignature(passObj.getArgs().getSignature()))
.map($videoMeta -> {
if ($videoMeta.isPresent()) {
// 视频元数据信息存在,说明视频上传是重复的,执行相关的异常处理
throw new VideoDuplicatedException();
}
// 视频没有重复,可以创建,向后传递passObj
return _passObj;
})
)
;
}
  • 验证并获取Account对象
/**
* 验证并获取Account对象
*/
@NotNull
private Single<Account> checkAndGetAccount(@NotNull PassObj passObj) {
return Maybe.fromCallable(() -> passObj.getArgs().getAccountId())// 取accountId
.filter(accountId -> accountId > 0L) // 取基本有效的值
.flatMap(accountId -> this.accountService.maybe(s -> s.checkAccount(accountId)))// 验证帐号状态
.defaultIfEmpty(false)
.doOnSuccess(accountAccepted -> {
if (!accountAccepted) {
// 帐号状态校验失败
// 通过明确的异常来处理失败过程
throw new AccountRejectedException();
}
})
.flatMapMaybe(_accountAccepted -> this.accountService.maybe(s -> s.getAccount(passObj.getArgs().getAccountId())))
.switchIfEmpty(Single.error(AccountObjectNullPointerException::new))
;
}
  • 入参合规性校验
/**
* 入参合规性校验
*/
private Single<Boolean> checkInputArgs(@NotNull PassObj passObj) { // 入参合规性校验可并发执行
return Single.zip(
this.contentAuditService.maybe(s -> s.titleCheck(passObj.getArgs().getTitle())).switchIfEmpty(Single.error(TitleRejectedException::new)), // title
this.contentAuditService.maybe(s -> s.coverImgCheck(passObj.getArgs().getTitle())).switchIfEmpty(Single.error(CoverImgRejectedException::new)), // coverImage
this.contentAuditService.maybe(s -> s.otherCheck(passObj.getArgs().getTitle())).switchIfEmpty(Single.error(OtherRejectedException::new)), // others
(titleAccepted, coverImgAccepted, otherAccepted) -> titleAccepted && coverImgAccepted && otherAccepted
);
}

对象创建阶段: 视频对象存储

/**
* 2 对象创建阶段: 视频对象存储
*/
@NotNull
public Single<PassObj> createVideoObjectStage(@NotNull PassObj passObj) {
return Single.fromCallable(() ->
// 包装VideoInfo对象
VideoInfo.builder()
.userId(passObj.getAccount().getId())
.title(passObj.getArgs().getTitle())
.coverImg(passObj.getArgs().getCoverImg())
.other(passObj.getArgs().getOther())
.build()
)
.flatMapMaybe(videoInfo ->
this.videoInfoService.maybe(s -> s.createVideoInfo(videoInfo)) // 保存视频对象
.filter(createdVideoInfo -> createdVideoInfo.getId() > 0L) // 验证保存结果
)
.onErrorResumeNext(e -> Maybe.error(() -> new VideoInfoCreateException(e))) // 异常处理
.switchIfEmpty(Single.error(VideoInfoCreateException::new)) // 异常处理
.map(createdVideoInfo -> passObj.toBuilder().videoInfo(createdVideoInfo).build()) // 将保存结果装配到passObj中
;
}

关联数据对象创建阶段

/**
* 3 关联数据对象创建阶段
*/
@NotNull
public Single<PassObj> createCorrelateObjectStage(@NotNull PassObj passObj) {
return Single.zip( // 必要内容:自媒体业务/视频对象扩展 可并发执行
Single.fromCallable(() -> // 视频对象扩展
VideoInfoExtend.builder()
.userId(passObj.getAccount().getId())
.videoInfoId(passObj.getVideoInfo().getId())
.build()
)
.flatMap(videoInfoExtend ->
this.videoInfoService.maybe(s -> s.createVideoInfoExtend(videoInfoExtend))
.onErrorResumeNext(e -> Maybe.error(() -> new VideoInfoExtendCreateException(e))) // 异常处理
.switchIfEmpty(Single.error(VideoInfoExtendCreateException::new)) // 异常处理
),
Single.fromCallable(() -> // 自媒体业务
PugcInfo.builder()
.userId(passObj.getAccount().getId())
.videoInfoId(passObj.getVideoInfo().getId())
.build()
)
.flatMap(pugcInfo ->
this.pugcService.maybe(s -> s.createPugcInfo(pugcInfo))
.onErrorResumeNext(e -> Maybe.error(() -> new PugcInfoCreateException(e))) // 异常处理
.switchIfEmpty(Single.error(PugcInfoCreateException::new)) // 异常处理
),
(createdVideoInfoExtend, createdPugcInfo) ->
passObj.toBuilder()
.videoInfoExtend(createdVideoInfoExtend)
.pugcInfo(createdPugcInfo)
.build()
)
.doOnSuccess(updatedPassObj -> { // 可选内容:视频元数据/转码/其它 不关注结果,异步执行
Maybe.fromCallable(() ->
VideoMeta.builder()
.signature(updatedPassObj.getArgs().getSignature())
.build()
)
.flatMap(videoMeta -> this.videoMetaService.maybe(s -> s.createVideoMeta(videoMeta)))
.doOnError(e -> log.error(e.getMessage(), e))
.subscribe();
this.videoTranscodingService.maybe(s -> s.createVideoKeyFrame(updatedPassObj.getVideoInfo())).subscribe();
this.videoTranscodingService.maybe(s -> s.createVideoResolution(updatedPassObj.getVideoInfo())).subscribe();
this.otherOptionalService.maybe(s -> s.doSomething(updatedPassObj.getVideoInfo())).subscribe();
})
;
}

上传准备:从CDN调度视频内容存储结点

/**
* 上传准备:从CDN调度视频内容存储结点
*/
@NotNull
public Single<PassObj> cdnDispatchStage(@NotNull PassObj passObj) {
return cdnDispatchService.maybe(CDNDispatchService::dispatch) // 调度一个CDN上传节点
.onErrorResumeNext(e -> Maybe.error(() -> new CDNDispatchException(e))) // 调用异常
.switchIfEmpty(Single.error(CDNDispatchException::new)) // 没有取到节点, 异常处理
.map(cdnNodeInfo ->
passObj.toBuilder()
.cdnNodeInfo(cdnNodeInfo)
.build()
)
;
}

6、总结

以上内容是搜狐视频服务端PUGC团队,首次在核心业务接口中应用响应式异步非阻塞架构的思考和实施过程,文中主要阐述了两个内容:

  • Dubbo响应式客户端的实现过程
  • 视频对象创建接口的业务分析和实现过程

通过本次重构,我们获得了如下收益:

  • 重新梳理业务流,将业务流同响应式流整合,基于响应式编程规范业务流的实现过程
  • 视频对象创建接口平均响应时间从256ms降低到146ms,性能提供显著

结束,感谢阅读!

责任编辑:未丽燕 来源: 搜狐技术产品
相关推荐

2021-02-27 16:08:17

Java异步非阻塞

2015-07-03 10:12:04

编程同步非阻塞

2024-09-05 09:41:57

2012-05-29 10:44:17

WebApp

2012-02-22 21:15:41

unixIO阻塞

2022-06-22 08:16:29

异步非阻塞框架

2019-07-23 11:01:57

Python同步异步

2021-05-25 08:20:37

编程技能开发

2012-10-10 10:00:27

同步异步开发Java

2021-03-04 08:34:55

同步阻塞非阻塞

2018-03-28 08:52:53

阻塞非阻塞I

2023-07-12 08:16:54

JVM工具包Vert.x

2009-08-21 14:25:23

C#异步传输字符串

2022-09-01 08:00:00

响应式编程集成

2024-09-23 17:15:28

Python并发并行

2023-12-06 07:28:47

阻塞IO异步IO

2016-11-28 09:08:43

java系统异步非阻塞

2021-06-04 18:14:15

阻塞非阻塞tcp

2021-01-12 10:00:34

流式输出数据

2021-01-10 11:21:33

JavaScript语言开发
点赞
收藏

51CTO技术栈公众号