最近自己在写一个网络服务程序时需要管理大量客户端连接的,其中每个客户端连接都需要管理它的 timeout 时间。
通常连接的超时管理一般设置为30~60秒不等,并不需要太精确的时间控制。
另外由于服务端管理着多达数万到数十万不等的连接数,因此我们没法为每个连接使用一个Timer,那样太消耗资源不现实。
最早面临类似问题的应该是在操作系统和网络协议栈的实现中,以TCP协议为例:
其可靠传输依赖超时重传机制,因此每个通过TCP传输的 packet 都需要一个 timer 来调度 timeout 事件。
根据George Varghese 和 Tony Lauck 1996 年的论文<Hashed and Hierarchical Timing Wheels: data structures to efficiently implement a timer facility>(http://cseweb.ucsd.edu/users/varghese/PAPERS/twheel.ps.Z)
提出了一种定时轮的方式来管理和维护大量的 timer 调度,本文主要根据该论文讨论下实现一种定时轮的要点。
定时轮是一种数据结构,其主体是一个循环列表(circular buffer),每个列表中包含一个称之为槽(slot)的结构(附图)。
至于 slot 的具体结构依赖具体应用场景。
以本文开头所述的管理大量连接 timeout 的场景为例,描述一下 timing wheel的具体实现细节。
定时轮的工作原理可以类比于始终,如上图箭头(指针)按某一个方向按固定频率轮动,每一次跳动称为一个 tick。
这样可以看出定时轮由个3个重要的属性参数,ticksPerWheel(一轮的tick数),tickDuration(一个tick的持续时间)
以及 timeUnit(时间单位),例如 当ticksPerWheel=60,tickDuration=1,timeUnit=秒,这就和现实中的始终的秒针走动完全类似了。
这里给出一种简单的实现方式,指针按 tickDuration 的设置进行固定频率的转动,其中的必要约定如下:
在 Timing Wheel 模型中包含4种操作:
Client invoke:
1. START_TIMER(Interval, Request_ID, Expiry_Action)
2. STOP_TIMER(Request_ID)
Timer tick invoke:
3. PER_TICK_BOOKKEEPING
4. EXPIRY_PROCESSING
Timing Wheel 实现中主要考察的是前3种操作的时间和空间复杂度,而第4种属于超时处理通常实现为回调方法,由调用方的实现决定其效率,下面看一个用 java 实现的 Timing Wheel 的具体例子:
TimingWheel.java
/** * A timing-wheel optimized for approximated I/O timeout scheduling.<br> * {@link TimingWheel} creates a new thread whenever it is instantiated and started, so don't create many instances. * <p> * <b>The classic usage as follows:</b><br> * <li>using timing-wheel manage any object timeout</li> * <pre> * // Create a timing-wheel with 60 ticks, and every tick is 1 second. * private static final TimingWheel<CometChannel> TIMING_WHEEL = new TimingWheel<CometChannel>(1, 60, TimeUnit.SECONDS); * * // Add expiration listener and start the timing-wheel. * static { * TIMING_WHEEL.addExpirationListener(new YourExpirationListener()); * TIMING_WHEEL.start(); * } * * // Add one element to be timeout approximated after 60 seconds * TIMING_WHEEL.add(e); * * // Anytime you can cancel count down timer for element e like this * TIMING_WHEEL.remove(e); * </pre> * * After expiration occurs, the {@link ExpirationListener} interface will be invoked and the expired object will be * the argument for callback method {@link ExpirationListener#expired(Object)} * <p> * {@link TimingWheel} is based on <a href="http://cseweb.ucsd.edu/users/varghese/">George Varghese</a> and Tony Lauck's paper, * <a href="http://cseweb.ucsd.edu/users/varghese/PAPERS/twheel.ps.Z">'Hashed and Hierarchical Timing Wheels: data structures * to efficiently implement a timer facility'</a>. More comprehensive slides are located <a href="http://www.cse.wustl.edu/~cdgill/courses/cs6874/TimingWheels.ppt">here</a>. * * @author mindwind * @version 1.0, Sep 20, 2012 */ public class TimingWheel<E> { private final long tickDuration; private final int ticksPerWheel; private volatile int currentTickIndex = 0; private final CopyOnWriteArrayList<ExpirationListener<E>> expirationListeners = new CopyOnWriteArrayList<ExpirationListener<E>>(); private final ArrayList<Slot<E>> wheel; private final Map<E, Slot<E>> indicator = new ConcurrentHashMap<E, Slot<E>>(); private final AtomicBoolean shutdown = new AtomicBoolean(false); private final ReadWriteLock lock = new ReentrantReadWriteLock(); private Thread workerThread; // ~ ------------------------------------------------------------------------------------------------------------- /** * Construct a timing wheel. * * @param tickDuration * tick duration with specified time unit. * @param ticksPerWheel * @param timeUnit */ public TimingWheel(int tickDuration, int ticksPerWheel, TimeUnit timeUnit) { if (timeUnit == null) { throw new NullPointerException("unit"); } if (tickDuration <= 0) { throw new IllegalArgumentException("tickDuration must be greater than 0: " + tickDuration); } if (ticksPerWheel <= 0) { throw new IllegalArgumentException("ticksPerWheel must be greater than 0: " + ticksPerWheel); } this.wheel = new ArrayList<Slot<E>>(); this.tickDuration = TimeUnit.MILLISECONDS.convert(tickDuration, timeUnit); this.ticksPerWheel = ticksPerWheel + 1; for (int i = 0; i < this.ticksPerWheel; i++) { wheel.add(new Slot<E>(i)); } wheel.trimToSize(); workerThread = new Thread(new TickWorker(), "Timing-Wheel"); } // ~ ------------------------------------------------------------------------------------------------------------- public void start() { if (shutdown.get()) { throw new IllegalStateException("Cannot be started once stopped"); } if (!workerThread.isAlive()) { workerThread.start(); } } public boolean stop() { if (!shutdown.compareAndSet(false, true)) { return false; } boolean interrupted = false; while (workerThread.isAlive()) { workerThread.interrupt(); try { workerThread.join(100); } catch (InterruptedException e) { interrupted = true; } } if (interrupted) { Thread.currentThread().interrupt(); } return true; } public void addExpirationListener(ExpirationListener<E> listener) { expirationListeners.add(listener); } public void removeExpirationListener(ExpirationListener<E> listener) { expirationListeners.remove(listener); } /** * Add a element to {@link TimingWheel} and start to count down its life-time. * * @param e * @return remain time to be expired in millisecond. */ public long add(E e) { synchronized(e) { checkAdd(e); int previousTickIndex = getPreviousTickIndex(); Slot<E> slot = wheel.get(previousTickIndex); slot.add(e); indicator.put(e, slot); return (ticksPerWheel - 1) * tickDuration; } } private void checkAdd(E e) { Slot<E> slot = indicator.get(e); if (slot != null) { slot.remove(e); } } private int getPreviousTickIndex() { lock.readLock().lock(); try { int cti = currentTickIndex; if (cti == 0) { return ticksPerWheel - 1; } return cti - 1; } finally { lock.readLock().unlock(); } } /** * Removes the specified element from timing wheel. * * @param e * @return <tt>true</tt> if this timing wheel contained the specified * element */ public boolean remove(E e) { synchronized (e) { Slot<E> slot = indicator.get(e); if (slot == null) { return false; } indicator.remove(e); return slot.remove(e) != null; } } private void notifyExpired(int idx) { Slot<E> slot = wheel.get(idx); Set<E> elements = slot.elements(); for (E e : elements) { slot.remove(e); synchronized (e) { Slot<E> latestSlot = indicator.get(e); if (latestSlot.equals(slot)) { indicator.remove(e); } } for (ExpirationListener<E> listener : expirationListeners) { listener.expired(e); } } } // ~ ------------------------------------------------------------------------------------------------------------- private class TickWorker implements Runnable { private long startTime; private long tick; @Override public void run() { startTime = System.currentTimeMillis(); tick = 1; for (int i = 0; !shutdown.get(); i++) { if (i == wheel.size()) { i = 0; } lock.writeLock().lock(); try { currentTickIndex = i; } finally { lock.writeLock().unlock(); } notifyExpired(currentTickIndex); waitForNextTick(); } } private void waitForNextTick() { for (;;) { long currentTime = System.currentTimeMillis(); long sleepTime = tickDuration * tick - (currentTime - startTime); if (sleepTime <= 0) { break; } try { Thread.sleep(sleepTime); } catch (InterruptedException e) { return; } } tick++; } } private static class Slot<E> { private int id; private Map<E, E> elements = new ConcurrentHashMap<E, E>(); public Slot(int id) { this.id = id; } public void add(E e) { elements.put(e, e); } public E remove(E e) { return elements.remove(e); } public Set<E> elements() { return elements.keySet(); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + id; return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; @SuppressWarnings("rawtypes") Slot other = (Slot) obj; if (id != other.id) return false; return true; } @Override public String toString() { return "Slot [id=" + id + ", elements=" + elements + "]"; } } }
ExpirationListener.java
/** * A listener for expired object events. * * @author mindwind * @version 1.0, Sep 20, 2012 * @see TimingWheel */ public interface ExpirationListener<E> { /** * Invoking when a expired event occurs. * * @param expiredObject */ void expired(E expiredObject); }
我们分析一下这个简化版本 TimingWheel 实现中的 4 个主要操作的实现:
START_TIMER(Interval, Request_ID, Expiry_Action) ,这段伪代码的实现对应于TimingWheel的 add(E e) 方法。
STOP_TIMER(Request_ID),这段伪代码的实现对应于TimingWheel的 remove(E e) 方法。
PER_TICK_BOOKKEEPING,伪代码对应于 TimingWheel 中 TickerWorker 中的 run() 方法。
EXPIRY_PROCESSING,伪代码对应于TimingWheel 中的 notifyExpired() 方法
在维护大量连接的例子中:
这个简化版的 TimingWheel 实现一个实例只能支持一个固定的 timeout 时长调度,不能支持对于每个元素特定的 timeout 时长。
一种改进的做法是设计一个函数,计算每个元素特定的deadline,并根据deadline计算放置在wheel中的特定位置,这个以后再完善。