每隔60秒一次的心跳检测或发送请求超时等待响应;即:等待xxx时间就执行yyy任务;可以开启定时任务timer,周期性检测是否要执行任务来处理此类需求
当存在有大量的心跳检测任务或超时控制任务, 如 每个超时任务都开启定时任务timer则会消耗大量资源; 只开启一个定时任务timer用来检测大量的任务则会遇到 遍历耗时长问题 (每次循环遍历所有任务检测时间)如:java.util.Timer
更好的解决思路,开启一个定时任务time,拆分大量的定时任务,每次只检测部分任务,没有被检测到的任务在理论上保证不需要执行;即可以节省资源,也很大程度缓解遍历耗时长的问题 如: HashedWheelTimer
HashedWheelTimer使用方式
// 第一步编写TimeTask任务 private class PrintTask implements TimerTask { @Override public void run(Timeout timeout) { final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); System.out.println("task :" + LocalDateTime.now().format(formatter)); } }// 第二步创建 HashedWheelTime Timer timer = new HashedWheelTimer();// 第三步 创建timeout (1s后执行PrintTask#run方法) timer.newTimeout(new PrintTask(), 1, TimeUnit.SECONDS);
Hash轮
HashedWheelTimer创建的时候创建worker线程(用于循环检测),走一圈有多少个bucket, 走一个bucket 隔需要多久时间 tickDuration
public HashedWheelTimer( ThreadFactory threadFactory, long tickDuration, TimeUnit unit, int ticksPerWheel, long maxPendingTimeouts) { // Normalize ticksPerWheel to power of two and initialize the wheel. // wheel 是刚好大于等于ticksPerWheel中最小的2的N次方 N是整数 wheel = createWheel(ticksPerWheel); // mask 用二进制表示 111111...; 用“与操作”计算wheel下标 mask = wheel.length - 1; // 走一个bucket 需要花费的纳秒时长 this.tickDuration = unit.toNanos(tickDuration); // 创建work线程 workerThread = threadFactory.newThread(worker); }
第一个timeout 创建时,启动worker线程,开始循环检测,并记录开始时间startTime;新创建timeout时不会计算加入到具体bucket中
public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) { // 调用start方法 start(); // deadline 计算方式中有减去worker线程启动的时间点startTime long deadline = System.nanoTime() + unit.toNanos(delay) - startTime; HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline); // 新创建的timeout添加到timeousts集合,此时并没有计算timeout在哪个bucket timeouts.add(timeout); return timeout; } public void start() { switch (WORKER_STATE_UPDATER.get(this)) { case WORKER_STATE_INIT: // 初始化状态只启动一次 if (WORKER_STATE_UPDATER.compareAndSet(this, WORKER_STATE_INIT, WORKER_STATE_STARTED)) workerThread.start(); } break; } // Wait until the startTime is initialized by the worker. while (startTime == 0) { try { // 调用newTimeout方法的线程等待 worker线程设置 startTime值 startTimeInitialized.await(); } catch (InterruptedException ignore) { // Ignore - it will be ready very soon. } } }
worker作为HashedWheelTimer循环检测调度器,会将新创建的timeout添加到对应的bucket,并周期性检测每个bucket取出到期(deadline)的timeout 执行对应的TimeTask
private final class Worker implements Runnable { public void run() { // worker线程初始化 startTime startTime = System.nanoTime(); // 通知第一次调用newTimeout方法的线程 startTimeInitialized.countDown(); // HashedWheelTimer 不停,循环不停 do { // 等待并返回走到下一个bucket时对应deadline final long deadline = waitForNextTick(); if (deadline > 0) { // &操作计算当前bucket[]下标 int idx = (int) (tick & mask); // 将已取消任务从bucket中删除 processCancelledTasks(); HashedWheelBucket bucket = wheel[idx]; // 将新创建的timeout 放到对应的bucket中 transferTimeoutsToBuckets(); // 将bucket中所有的timeout 判断超时处理 bucket.expireTimeouts(deadline); tick++; } } while (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_STARTED); // HashedWheelTimer停止后处理,未到期的timeout对象 处理逻辑 忽略。。。 } 。。。 }
waitForNextTick() 等待期间做什么呢?答案是sleep;那么在sleep期间如果有timeout到期了怎么办?答案是睡醒来在执行;那么tickDuration 就控制着时间的精准度,值越小精准度越高;如tickDuration=1秒,在1.1秒时添加了timeout,2秒时间点扫描发现没有超时,3s秒才扫描到timeout已超时;此时已经过了1.9秒;若tickDuration非常小worker线程则越繁忙
dubbo中发送请求超时检测time中tickDuration=30毫秒
private long waitForNextTick() { long deadline = tickDuration * (tick + 1); for (; ; ) { final long currentTime = System.nanoTime() - startTime; long sleepTimeMs = (deadline - currentTime + 999999) / 1000000; // deadline 已过则返回 if (sleepTimeMs <= 0) { if (currentTime == Long.MIN_VALUE) { return -Long.MAX_VALUE; } else { return currentTime; } } if (isWindows()) { sleepTimeMs = sleepTimeMs / 10 * 10; } try { Thread.sleep(sleepTimeMs); } catch (InterruptedException ignored) { 。。。 } } }
未到期但被取消的任务会放到 cancelledTimeouts集合中, worker线程周期性的执行do while方法会调用processCancelledTasks() 会从bucket中删除调对应的timeout;如请求1s超时检测任务,正常返回之后需要cancel调对应的timeout
worker线程执行 transferTimeoutsToBuckets() 将新创建的timeout根据deadline计算出remainingRounds几轮与idx 并加入到对应的bucket
private void transferTimeoutsToBuckets() { // 每次循环最多加10W个 for (int i = 0; i < 100000; i++) { HashedWheelTimeout timeout = timeouts.poll(); if (timeout == null) { break; } if (timeout.state() == HashedWheelTimeout.ST_CANCELLED) { continue; } long calculated = timeout.deadline / tickDuration; timeout.remainingRounds = (calculated - tick) / wheel.length; // tick 表示当前的的tick数, // 如果timeout创建较早,可能导致已过期还没加入到bucket,则此时 calculated < tick final long ticks = Math.max(calculated, tick); int stopIndex = (int) (ticks & mask); HashedWheelBucket bucket = wheel[stopIndex]; bucket.addTimeout(timeout); } }
bucket.expireTimeouts(deadline); 将到期的bucket中所有的timeout判断并进行超时处理,到期的timeout进行超时处理,即调用TimeTask的run方法;此处风险:TimeTask.run() 方法是阻塞同步执行的,如果某task执行时间过久则会阻塞Worker线程 进一步拖慢超时检测流程
void expireTimeouts(long deadline) { HashedWheelTimeout timeout = head; // 处理所有timeout while (timeout != null) { HashedWheelTimeout next = timeout.next; if (timeout.remainingRounds <= 0) { next = remove(timeout); if (timeout.deadline <= deadline) { // 执行TimeTask.run方法 timeout.expire(); } else { // error } } else if (timeout.isCancelled()) { // 取消则直接删除 next = remove(timeout); } else { // 经过一轮后 timeout.remainingRounds--; } timeout = next; } }
Dubbo中应用
请求超时检测,发送请求时创建 timeout
public class DefaultFuture extends CompletableFuture { // 创建HashedWheelTimer 对象 public static final Timer TIME_OUT_TIMER = new HashedWheelTimer( new NamedThreadFactory("dubbo-future-timeout", true), 30, // tickDuration=30毫秒 检测一次 TimeUnit.MILLISECONDS); private Timeout timeoutCheckTask; // 发送请求时会创建DefaultFuture,并创建timeout对象 public static DefaultFuture newFuture(Channel channel, Request request, int timeout, ExecutorService executor) { 。。。。 // 创建TimeTask TimeoutCheckTask task = new TimeoutCheckTask(future.getId()); // 创建timeout对象 future.timeoutCheckTask = TIME_OUT_TIMER.newTimeout(task, future.getTimeout(), TimeUnit.MILLISECONDS); return future; } private static class TimeoutCheckTask implements TimerTask { private final Long requestID; @Override public void run(Timeout timeout) { DefaultFuture future = DefaultFuture.getFuture(requestID); if (future.getExecutor() != null) { future.getExecutor().execute(() -> notifyTimeout(future)); } else { notifyTimeout(future); } } private void notifyTimeout(DefaultFuture future) { // create exception response. Response timeoutResponse = new Response(future.getId()); 。。。 // 超时则返回构造timeoutResponse 返回 DefaultFuture.received(future.getChannel(), timeoutResponse, true); } }
请求正常返回时cancel timeout
public static void received(Channel channel, Response response, boolean timeout) { try { DefaultFuture future = FUTURES.remove(response.getId()); if (future != null) { Timeout t = future.timeoutCheckTask; if (!timeout) { // cancel timeout t.cancel(); } future.doReceived(response); } else { // warn } } finally { CHANNELS.remove(response.getId()); } }
发送同步请求,将超时检测交给HashedWheelTimer中worker线程 ,那么发送请求的工作线程在做什么呢?答案是在阻塞等待response
public class AsyncToSyncInvoker implements Invoker { @Override public Result invoke(Invocation invocation) throws RpcException { Result asyncResult = invoker.invoke(invocation); try { // 同步阻塞等待 if (InvokeMode.SYNC == ((RpcInvocation) invocation).getInvokeMode()) { asyncResult.get(Integer.MAX_VALUE, TimeUnit.MILLISECONDS); } } catch (InterruptedException e) { // error } catch (Throwable e) { throw new RpcException(e.getMessage(), e); } return asyncResult; } 。。。}