netty 時(shí)間輪介紹

背景

最近有接觸netty相關(guān)內(nèi)容,組內(nèi)有做關(guān)于netty時(shí)間輪的分享招狸,正好總結(jié)這篇文章,做個(gè)了解和記錄。時(shí)間輪在超時(shí)控制进鸠,異常處理司恳,鎖控制等方面都有非常多的應(yīng)用猎塞。本期要說(shuō)的netty時(shí)間輪是實(shí)現(xiàn)相對(duì)簡(jiǎn)單的一種,比較復(fù)雜的如kafka是多級(jí)的時(shí)間輪實(shí)現(xiàn)杠纵,我們暫不做介紹荠耽。

原理

首先我們理解什么是時(shí)間輪,其實(shí)可以形象的看做是一個(gè)手表比藻,按照固定的時(shí)間铝量,即tick,每次擺動(dòng)對(duì)應(yīng)一個(gè)階段的時(shí)間银亲,然后對(duì)應(yīng)時(shí)間刻度上綁定待觸發(fā)任務(wù)慢叨。當(dāng)時(shí)鐘指針到達(dá)時(shí),對(duì)應(yīng)的任務(wù)就可以執(zhí)行了群凶。比如一種場(chǎng)景插爹,每一個(gè)刻度代表一秒,某個(gè)超時(shí)判斷為10s,則這個(gè)超時(shí)就在10s的刻度上(假定時(shí)鐘一個(gè)周期大于10s赠尾,即格子數(shù)大于10個(gè))力穗。當(dāng)指針從0到達(dá)10s的刻度,就是該超時(shí)判斷執(zhí)行的時(shí)候气嫁。

時(shí)間輪

以上是個(gè)簡(jiǎn)單時(shí)間輪的原理当窗,實(shí)用的時(shí)間輪比之要復(fù)雜。

思路參考

關(guān)于時(shí)間輪的實(shí)現(xiàn)寸宵,基本都基于George VargheseTony Lauck的論文崖面,后面有鏈接。netty的時(shí)間輪處理除了上面的原理梯影,還在每個(gè)調(diào)度任務(wù)中記錄輪次巫员,在輪次減少到0后,再判斷當(dāng)前輪上的剩余格數(shù)甲棍,進(jìn)行任務(wù)執(zhí)行简识。

注意使用的netty版本是4.1.51.Final

netty實(shí)現(xiàn)代碼

  1. 時(shí)間輪構(gòu)造

    public HashedWheelTimer(
        ThreadFactory threadFactory,
        long tickDuration, TimeUnit unit, int ticksPerWheel, boolean leakDetection) {
        this(threadFactory, tickDuration, unit, ticksPerWheel, leakDetection, -1);
    }
    
        public HashedWheelTimer(
                ThreadFactory threadFactory,
                long tickDuration, TimeUnit unit, int ticksPerWheel, boolean leakDetection,
                long maxPendingTimeouts) {
    
            if (threadFactory == null) {
                throw new NullPointerException("threadFactory");
            }
            if (unit == 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);
            }
    
            // Normalize ticksPerWheel to power of two and initialize the wheel.
            wheel = createWheel(ticksPerWheel);
            mask = wheel.length - 1;
    
            // Convert tickDuration to nanos.
            long duration = unit.toNanos(tickDuration);
    
            // Prevent overflow.
            if (duration >= Long.MAX_VALUE / wheel.length) {
                throw new IllegalArgumentException(String.format(
                        "tickDuration: %d (expected: 0 < tickDuration in nanos < %d",
                        tickDuration, Long.MAX_VALUE / wheel.length));
            }
    
            if (duration < MILLISECOND_NANOS) {
                if (logger.isWarnEnabled()) {
                    logger.warn("Configured tickDuration %d smaller then %d, using 1ms.",
                                tickDuration, MILLISECOND_NANOS);
                }
                this.tickDuration = MILLISECOND_NANOS;
            } else {
                this.tickDuration = duration;
            }
    
            //創(chuàng)建調(diào)度線程
            workerThread = threadFactory.newThread(worker);
    
            leak = leakDetection || !workerThread.isDaemon() ? leakDetector.track(this) : null;
    
            this.maxPendingTimeouts = maxPendingTimeouts;
    
            if (INSTANCE_COUNTER.incrementAndGet() > INSTANCE_COUNT_LIMIT &&
                WARNED_TOO_MANY_INSTANCES.compareAndSet(false, true)) {
                reportTooManyInstances();
            }
        }

除了一些參數(shù)校驗(yàn),我們按照順序感猛,先看主要的方法createWheel七扰,按照時(shí)間輪每圈的格子數(shù),先做個(gè)轉(zhuǎn)換normalizeTicksPerWheel()陪白,找到最近的一個(gè)二進(jìn)制數(shù)值颈走,如7取8,13取16。這么做主要是通過(guò)利用&運(yùn)算可以快速取余咱士,用到了后面的定義的mask= wheel.length-1立由。這種方式在HashMap以及一些中間件中經(jīng)常使用。隨后司致,根據(jù)新的每圈格子數(shù)拆吆,創(chuàng)建對(duì)應(yīng)的拉鏈數(shù)組。繼續(xù)看構(gòu)造函數(shù)脂矫,會(huì)創(chuàng)建一個(gè)worker線程,用來(lái)遍歷時(shí)間輪霉晕,執(zhí)行調(diào)度庭再。


    private static HashedWheelBucket[] createWheel(int ticksPerWheel) {
        if (ticksPerWheel <= 0) {
            throw new IllegalArgumentException(
                    "ticksPerWheel must be greater than 0: " + ticksPerWheel);
        }
        if (ticksPerWheel > 1073741824) {
            throw new IllegalArgumentException(
                    "ticksPerWheel may not be greater than 2^30: " + ticksPerWheel);
        }

        ticksPerWheel = normalizeTicksPerWheel(ticksPerWheel);
        HashedWheelBucket[] wheel = new HashedWheelBucket[ticksPerWheel];
        for (int i = 0; i < wheel.length; i ++) {
            wheel[i] = new HashedWheelBucket();
        }
        return wheel;
    }

    private static int normalizeTicksPerWheel(int ticksPerWheel) {
        int normalizedTicksPerWheel = 1;
        while (normalizedTicksPerWheel < ticksPerWheel) {
            normalizedTicksPerWheel <<= 1;
        }
        return normalizedTicksPerWheel;
    }

  1. worker線程
    worker線程我們主要看下run方法。獲得當(dāng)前tick的一個(gè)過(guò)期時(shí)間牺堰,然后取余得到當(dāng)前tick的分桶拄轻,然后,指定的bucket再判斷當(dāng)前tick的截止時(shí)間伟葫,判斷輪次是否截止+是否取消恨搓,是否得處于過(guò)期timeout.deadline <= deadline。具體可以看io.netty.util.HashedWheelTimer.HashedWheelBucket.expireTimeouts

    private final class Worker implements Runnable {
        private final Set<Timeout> unprocessedTimeouts = new HashSet<Timeout>();

        private long tick;

        @Override
        public void run() {
            // Initialize the startTime.
            startTime = System.nanoTime();
            if (startTime == 0) {
                // We use 0 as an indicator for the uninitialized value here, so make sure it's not 0 when initialized.
                startTime = 1;
            }

            // Notify the other threads waiting for the initialization at start().
            startTimeInitialized.countDown();

            do {
                final long deadline = waitForNextTick();
                if (deadline > 0) {
                    int idx = (int) (tick & mask);
                    processCancelledTasks();
                    HashedWheelBucket bucket =
                            wheel[idx];
                    transferTimeoutsToBuckets();
                    bucket.expireTimeouts(deadline);
                    tick++;
                }
            } while (WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == WORKER_STATE_STARTED);

            // Fill the unprocessedTimeouts so we can return them from stop() method.
            for (HashedWheelBucket bucket: wheel) {
                bucket.clearTimeouts(unprocessedTimeouts);
            }
            for (;;) {
                HashedWheelTimeout timeout = timeouts.poll();
                if (timeout == null) {
                    break;
                }
                if (!timeout.isCancelled()) {
                    unprocessedTimeouts.add(timeout);
                }
            }
            processCancelledTasks();
        }
        
  1. 時(shí)間輪每個(gè)格子對(duì)應(yīng)的分桶
    如下,對(duì)應(yīng)到時(shí)間輪每個(gè)格子斧抱,其中的元素是實(shí)際調(diào)度任務(wù)常拓,構(gòu)造了一個(gè)雙向鏈表的結(jié)構(gòu)。其中expireTimeouts辉浦,即上面介紹的當(dāng)指定的tick到來(lái)時(shí)弄抬,判斷并執(zhí)行分桶上的調(diào)度任務(wù)。
    private static final class HashedWheelBucket {
        // Used for the linked-list datastructure
        private HashedWheelTimeout head;
        private HashedWheelTimeout tail;

        /**
         * Add {@link HashedWheelTimeout} to this bucket.
         */
        public void addTimeout(HashedWheelTimeout timeout) {
            assert timeout.bucket == null;
            timeout.bucket = this;
            if (head == null) {
                head = tail = timeout;
            } else {
                tail.next = timeout;
                timeout.prev = tail;
                tail = timeout;
            }
        }

        /**
         * Expire all {@link HashedWheelTimeout}s for the given {@code deadline}.
         */
        public void expireTimeouts(long deadline) {
            HashedWheelTimeout timeout = head;

            // process all timeouts
            while (timeout != null) {
                HashedWheelTimeout next = timeout.next;
                if (timeout.remainingRounds <= 0) {
                    next = remove(timeout);
                    if (timeout.deadline <= deadline) {
                        timeout.expire();
                    } else {
                        // The timeout was placed into a wrong slot. This should never happen.
                        throw new IllegalStateException(String.format(
                                "timeout.deadline (%d) > deadline (%d)", timeout.deadline, deadline));
                    }
                } else if (timeout.isCancelled()) {
                    next = remove(timeout);
                } else {
                    timeout.remainingRounds --;
                }
                timeout = next;
            }
        }

  1. 實(shí)際調(diào)度任務(wù)元素
    下面是實(shí)際執(zhí)行的調(diào)度任務(wù)元素宪郊,它實(shí)現(xiàn)了javaTimeout接口掂恕,元素包括對(duì)時(shí)間輪的索引,調(diào)度任務(wù)弛槐,本身的截止時(shí)間懊亡,以及狀態(tài),輪次乎串,分桶和前后鏈表指針等店枣。除了cancel,remove等方法,核心的是expire()用來(lái)真正實(shí)現(xiàn)任務(wù)的調(diào)度灌闺。每個(gè)有自己對(duì)應(yīng)的deadline艰争,即預(yù)計(jì)調(diào)度的時(shí)間點(diǎn)。

    private static final class HashedWheelTimeout implements Timeout {

        private static final int ST_INIT = 0;
        private static final int ST_CANCELLED = 1;
        private static final int ST_EXPIRED = 2;
        private static final AtomicIntegerFieldUpdater<HashedWheelTimeout> STATE_UPDATER =
                AtomicIntegerFieldUpdater.newUpdater(HashedWheelTimeout.class, "state");

        private final HashedWheelTimer timer;
        private final TimerTask task;
        private final long deadline;

        @SuppressWarnings({"unused", "FieldMayBeFinal", "RedundantFieldInitialization" })
        private volatile int state = ST_INIT;

        // remainingRounds will be calculated and set by Worker.transferTimeoutsToBuckets() before the
        // HashedWheelTimeout will be added to the correct HashedWheelBucket.
        long remainingRounds;

        // This will be used to chain timeouts in HashedWheelTimerBucket via a double-linked-list.
        // As only the workerThread will act on it there is no need for synchronization / volatile.
        HashedWheelTimeout next;
        HashedWheelTimeout prev;

        // The bucket to which the timeout was added
        HashedWheelBucket bucket;

        HashedWheelTimeout(HashedWheelTimer timer, TimerTask task, long deadline) {
            this.timer = timer;
            this.task = task;
            this.deadline = deadline;
        }

        @Override
        public Timer timer() {
            return timer;
        }

        @Override
        public TimerTask task() {
            return task;
        }

        @Override
        public boolean cancel() {
            // only update the state it will be removed from HashedWheelBucket on next tick.
            if (!compareAndSetState(ST_INIT, ST_CANCELLED)) {
                return false;
            }
            // If a task should be canceled we put this to another queue which will be processed on each tick.
            // So this means that we will have a GC latency of max. 1 tick duration which is good enough. This way
            // we can make again use of our MpscLinkedQueue and so minimize the locking / overhead as much as possible.
            timer.cancelledTimeouts.add(this);
            return true;
        }

        void remove() {
            HashedWheelBucket bucket = this.bucket;
            if (bucket != null) {
                bucket.remove(this);
            } else {
                timer.pendingTimeouts.decrementAndGet();
            }
        }

        public void expire() {
            if (!compareAndSetState(ST_INIT, ST_EXPIRED)) {
                return;
            }

            try {
                task.run(this);
            } catch (Throwable t) {
                if (logger.isWarnEnabled()) {
                    logger.warn("An exception was thrown by " + TimerTask.class.getSimpleName() + '.', t);
                }
            }
        }

  1. 添加調(diào)度任務(wù)
    這里添加任務(wù)就是創(chuàng)建實(shí)際調(diào)度的對(duì)象桂对,時(shí)間輪的啟動(dòng)也在這里start()甩卓,最后創(chuàng)建調(diào)度元素后,添加到一個(gè)隊(duì)列里蕉斜,然后在調(diào)度的時(shí)候逾柿,再?gòu)年?duì)列轉(zhuǎn)換到buckets中。這個(gè)隊(duì)列是使用timeouts = PlatformDependent.newMpscQueue();jclTools創(chuàng)建的宅此。是一個(gè)高性能的并發(fā)Queue包机错。
    @Override
    public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {
        if (task == null) {
            throw new NullPointerException("task");
        }
        if (unit == null) {
            throw new NullPointerException("unit");
        }

        long pendingTimeoutsCount = pendingTimeouts.incrementAndGet();

        if (maxPendingTimeouts > 0 && pendingTimeoutsCount > maxPendingTimeouts) {
            pendingTimeouts.decrementAndGet();
            throw new RejectedExecutionException("Number of pending timeouts ("
                + pendingTimeoutsCount + ") is greater than or equal to maximum allowed pending "
                + "timeouts (" + maxPendingTimeouts + ")");
        }

        start();

        // Add the timeout to the timeout queue which will be processed on the next tick.
        // During processing all the queued HashedWheelTimeouts will be added to the correct HashedWheelBucket.
        long deadline = System.nanoTime() + unit.toNanos(delay) - startTime;

        // Guard against overflow.
        if (delay > 0 && deadline < 0) {
            deadline = Long.MAX_VALUE;
        }
        HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline);
        timeouts.add(timeout);
        return timeout;
    }

  1. 啟動(dòng)
    時(shí)間輪對(duì)象本身的start()方法是public的,但是在添加調(diào)度任務(wù)的時(shí)候啟動(dòng)即可父腕,不用顯式地調(diào)用弱匪,畢竟如果時(shí)間輪沒(méi)有任務(wù),也沒(méi)有啟動(dòng)的必要璧亮。注意這里判斷狀態(tài)都是原子類萧诫,其中還主要用到了AtomicIntegerFieldUpdater,是jdk基于反射實(shí)現(xiàn)的原子狀態(tài)管理類枝嘶。
   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;
            case WORKER_STATE_STARTED:
                break;
            case WORKER_STATE_SHUTDOWN:
                throw new IllegalStateException("cannot be started once stopped");
            default:
                throw new Error("Invalid WorkerState");
        }

        // Wait until the startTime is initialized by the worker.
        while (startTime == 0) {
            try {
                startTimeInitialized.await();
            } catch (InterruptedException ignore) {
                // Ignore - it will be ready very soon.
            }
        }
    }

執(zhí)行示例

import io.netty.util.HashedWheelTimer;
import org.junit.Test;

import java.time.LocalDateTime;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

 @Test
    public void testTimeWheel() throws InterruptedException {
        HashedWheelTimer wheelTimer = new HashedWheelTimer(Executors.defaultThreadFactory(), 1, TimeUnit.SECONDS, 64, false);
        System.out.println("time wheel start@" + LocalDateTime.now());
        wheelTimer.newTimeout(timeout -> System.out.println("timeout 10s --> " + LocalDateTime.now()), 10, TimeUnit.SECONDS);
        wheelTimer.newTimeout(timeout -> System.out.println("timeout 20s --> " + LocalDateTime.now()), 20, TimeUnit.SECONDS);
        Thread.currentThread().join();
    }

運(yùn)行結(jié)果:

time wheel start@2020-11-13T18:10:03.232
timeout 10s --> 2020-11-13T18:10:14.234
timeout 20s --> 2020-11-13T18:10:24.233

總結(jié)

以上就是本期的全部?jī)?nèi)容帘饶,對(duì)netty時(shí)間輪算是有了個(gè)初步的了解。對(duì)于其中一些更加深入的細(xì)節(jié)群扶,還需要再努力研究下及刻。感謝閱讀镀裤。以下參考資料感興趣的讀者可以多做深入。

參考資料

  1. Hashed and Hierarchical timing wheels : Data structures for the efficient implementation of timer facility
  2. George Varghese and Tony Lauck's slide
  3. 時(shí)間輪詳解
  4. Netty學(xué)習(xí)
  5. netty-hashedwheeltimer
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末缴饭,一起剝皮案震驚了整個(gè)濱河市暑劝,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌茴扁,老刑警劉巖铃岔,帶你破解...
    沈念sama閱讀 218,204評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異峭火,居然都是意外死亡毁习,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)卖丸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)纺且,“玉大人,你說(shuō)我怎么就攤上這事稍浆≡芈担” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,548評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵衅枫,是天一觀的道長(zhǎng)嫁艇。 經(jīng)常有香客問(wèn)我,道長(zhǎng)弦撩,這世上最難降的妖魔是什么步咪? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,657評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮益楼,結(jié)果婚禮上猾漫,老公的妹妹穿的比我還像新娘。我一直安慰自己感凤,他們只是感情好悯周,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,689評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著陪竿,像睡著了一般禽翼。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上族跛,一...
    開(kāi)封第一講書(shū)人閱讀 51,554評(píng)論 1 305
  • 那天捐康,我揣著相機(jī)與錄音,去河邊找鬼庸蔼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛贮匕,可吹牛的內(nèi)容都是我干的姐仅。 我是一名探鬼主播,決...
    沈念sama閱讀 40,302評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼掏膏!你這毒婦竟也來(lái)了劳翰?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,216評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤馒疹,失蹤者是張志新(化名)和其女友劉穎佳簸,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體颖变,經(jīng)...
    沈念sama閱讀 45,661評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡生均,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,851評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了腥刹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片马胧。...
    茶點(diǎn)故事閱讀 39,977評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖衔峰,靈堂內(nèi)的尸體忽然破棺而出佩脊,到底是詐尸還是另有隱情,我是刑警寧澤垫卤,帶...
    沈念sama閱讀 35,697評(píng)論 5 347
  • 正文 年R本政府宣布威彰,位于F島的核電站,受9級(jí)特大地震影響穴肘,放射性物質(zhì)發(fā)生泄漏歇盼。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,306評(píng)論 3 330
  • 文/蒙蒙 一梢褐、第九天 我趴在偏房一處隱蔽的房頂上張望旺遮。 院中可真熱鬧,春花似錦盈咳、人聲如沸耿眉。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,898評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)鸣剪。三九已至,卻和暖如春丈积,著一層夾襖步出監(jiān)牢的瞬間筐骇,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,019評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工江滨, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留铛纬,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,138評(píng)論 3 370
  • 正文 我出身青樓唬滑,卻偏偏與公主長(zhǎng)得像告唆,于是被迫代替她去往敵國(guó)和親棺弊。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,927評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容