Vertx实战之如何追踪异步函数调用
Vertx实战之如何追踪异步函数调用
背景
日常开发中我们经常需要处理各种系统问题,而这些系统问题通常由一些非预期的因素引起(比如非预期的输入,内存不够,网络波动等)。此时就需要知道
- 本次系统问题影响了谁?
- 如果本次系统问题是因为非预期的输入而导致的,那么这个非预期的输入是什么?
上述两点在同步编程里可以通过全局try-catch来实现。但在异步编程里该怎么办呢?
思路
我的想法是绑定一个唯一id到一次完整的请求调用中(这个完整取决于我们想要监控的范围),无论程序执行到何处,我们总能拿到与请求一一对应的调用上下文
的id。
我将分两种场景进行讨论。一种是同步或基于jut下的线程池类进行异步调用的应用程序(如基于spring5之前的应用程序)。另外一种基于vertx(底层基于netty)的应用程序。
技术栈
vert.x 4
、netty 4
、alibaba TransmittableThreadLocal
、jboss javassist
、hotspot类加载&方法调用
1. 同步or基于jdk的异步
关于这种场景,有以下几种实现:
-
程序中的函数调用是完全同步的
则可通过
java.lang.ThreadLocal
绑定一个唯一id到当前线程。入口处生成,出口处销毁。
如果我们用的是logback,也可通过org.slf4j.MDC
来实现这个功能。其内部也是基于ThreadLocal。 -
程序中函数调用是异步的,异步通过新建
java.lang.Thread
的方式实现。这里可通过
java.lang.InheritableThreadLocal
绑定一个唯一id到当前线程。Thread
的构造函数里会自动把通过InheritableThreadLocal
绑定到当前线程的数据拷贝到这个正在创建的Thread
里。所以,只要这个Thread
是在我们需要监控的区域(当前线程通过InheritableThreadLocal
绑定了id后)创建的,就能实现这个唯一id的跨线程传递。 -
程序中函数调用是异步的,异步通过
java.util.concurrent
下的线程池类实现。由于线程池中的线程是可以复用的,所以,如果我们往线程池中丢任务时,有两种情况:
- 线程池创建新的线程来执行该任务(比如线程池中的线程数<coreSize)。这种情况下
InheritableThreadLocal
依然是有效的。 - 线程池把任务分配给了已有线程(比如线程池中的线程数≥coreSize并且待执行任务队列没有填满)。这种情况下
InheritableThreadLocal
是无法生效的(线程池中的存活线程会循环拉取等待任务队列中的task去执行,而这个过程是没有InheritableThreadLocal
拷贝的)。所以,这里可以用阿里的[TransmittableThreadLocal](https://github.com/alibaba/transmittable-thread-local)
组件来实现这个唯一id在线程池中的传递(其核心原理是在Runnable
中用一个变量来存储当前线程的ThreadLocal值,当线程执行此Runnable时再拿出来)。
- 线程池创建新的线程来执行该任务(比如线程池中的线程数<coreSize)。这种情况下
实现思路总结
异步环境下的关键在于如何跨线程传递ThreadLocal的值
2. Vertx 中的实现
vertx
是一个类似于spring
系列的用于构建分布式微服务的框架。基于vertx
构建的应用程序最大的特点是无阻塞&全异步。
vertx里的异步主要分两种。一种是在eventbus上传递消息的异步。另一种是基于netty的io操作异步
1. eventbus上异步传递消息
先来看一个简单的示例程序
static final String ADDRESS_1 = "address1";
static final String MESSAGE_1 = "message1";
static final String REPLY_MESSAGE_1 = "replyMessage1";
public static void sendMsgByEventbus() {
//初始化一个具有默认设置的vertx实例
Vertx vertx = Vertx.vertx();
//注册一个handler/consumer到eventbus
vertx.eventBus().consumer(ADDRESS_1, event -> {
log.info("receive msg:{}", event.body());
event.reply(REPLY_MESSAGE_1);
});
//通过eventbus发送消息给刚注册的handler
vertx.eventBus().request(ADDRESS_1, MESSAGE_1, reply -> {
log.info("receive replied msg:{}", reply.result().body());
});
}
下面是程序执行的结果
[vert.x-eventloop-thread-0] - receive msg:message1
[vert.x-eventloop-thread-0] - receive replied msg:replyMessage1
可以看到发起调用的是主线程,处理调用跟处理回调的是线程vert.x-eventloop-thread-0
。
那么,这个异步是如何实现的呢?显然,异步最明显的体现就是最后发送消息这里。所以我们就从Eventbus#request
逐步深入。
大概流程是这样的:
- 从
eventbus
中根据我们传入的address
拿到所有注册到这个地址上的handler。 - 将对应
handler
对传入消息的处理包装成一个runnable
丢进一个queue
eventLoop Thread
从这个queue
中抓取task
执行
关键代码片段如下:
//与本主题无关的只展示调用链路供大家参考
//Eventbus#request
// EventbusImpl#request
// EventbusImpl#sendOrPubInternal
// EventbusImpl#sendOrPubInternal
// EventbusImpl#OutboundDeliveryContext
// EventbusImpl#sendOrPub
// EventbusImpl#sendLocally
// EventbusImpl#deliverMessageLocally
protected ReplyException deliverMessageLocally(MessageImpl msg) {
//1.找出跟msg.address()的handlerHolder
ConcurrentCyclicSequence<HandlerHolder> handlers = handlerMap.get(msg.address());
if (handlers != null) {
for (HandlerHolder holder: handlers) {
//2.依次调用这些handler的receive方法
holder.handler.receive(msg.copyBeforeReceive());
}
return null;
}
}
void receive(MessageImpl msg) {
//3.匿名异步任务进队列
context.nettyEventLoop().execute(() -> {
doReceive(msg);
});
}
//SingleThreadEventExecutor#execute
private void execute(Runnable task, boolean immediate) {
//比较当前线程是否是eventLoop线程,是则返回true
//由于我们在main里调用的eventbus#request,所以这里是false
boolean inEventLoop = inEventLoop();
//4. 添加到queue中,后面执行任务的线程会调用该queue的poll方法
addTask(task);
if (!inEventLoop) {
//如果当前eventLoop对象的thread为空则创建一个Thread绑定到当前eventLoop
startThread();
}
}
//eventLoop中运行的thread通过这个方法从上面说的queue中拿task执行
//外层会循环执行该runAllTasks,直到Eventloop#shutdownGracefully被执行
//参考NioEventLoop#run
protected boolean runAllTasks(long timeoutNanos) {
Runnable task = pollTask();
for (;;) {
//5.执行task
safeExecute(task);
runTasks ++;
//如果队列中还有task,则继续执行
task = pollTask();
if (task == null) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
break;
}
}
return true;
}
protected static Runnable pollTaskFrom(Queue<Runnable> taskQueue) {
for (;;) {
Runnable task = taskQueue.poll();
if (task != WAKEUP_TASK) {
return task;
}
}
}
//safeExecute会去执行第3步中的匿名任务
protected boolean doReceive(Message<T> message) {
Handler<Message<T>> theHandler = handler;
deliver(theHandler, message);
return true;