Low cost timestamp acquisition

Posted by bryan52803 on Wed, 09 Feb 2022 17:50:21 +0100

This article has been included https://github.com/lkxiaolou/lkxiaolou Welcome, star.

preface

In the previous article Design and implementation of Cobar SQL audit A question about the performance of timestamp acquisition is raised in

Get the operating system time, and directly call system in Java currentTimeMillis(); It's OK, but in Cobar, if you get the time in this way, it will lead to serious performance loss (how to solve it? Go to Cobar's github warehouse to see the code).

Let's talk about this topic in detail. Our method of obtaining timestamp in Java is system Currenttimemillis() returns a millisecond timestamp. Check the source code and the comments are clear. Although this method returns a millisecond timestamp, the accuracy depends on the operating system. The accuracy returned by many operating systems is 10 milliseconds.

/**
     * Returns the current time in milliseconds.  Note that
     * while the unit of time of the return value is a millisecond,
     * the granularity of the value depends on the underlying
     * operating system and may be larger.  For example, many
     * operating systems measure time in units of tens of
     * milliseconds.
     *
     * <p> See the description of the class <code>Date</code> for
     * a discussion of slight discrepancies that may arise between
     * "computer time" and coordinated universal time (UTC).
     *
     * @return  the difference, measured in milliseconds, between
     *          the current time and midnight, January 1, 1970 UTC.
     * @see     java.util.Date
     */
    public static native long currentTimeMillis();

About why system Currenttimemillis() is slow. A big man wrote an article explaining the reasons in detail. It is recommended to read it carefully. It is very deep and detailed. The article address is

http://pzemtsov.github.io/2017/07/23/the-slow-currenttimemillis.html

To sum up, the reason is system Currenttimemillis called gettimeofday()

  • Calling gettimeofday() requires switching from user mode to kernel mode;
  • The performance of gettimeofday() is affected by the timer (clock source) of Linux system, especially under HPET timer;
  • The system has only one global clock source. High concurrency or frequent access will cause serious contention.

Let's test the system The performance of currenttimemillis() under different threads is tested by using JHM commonly used in middleware. The time required to obtain 10 million timestamps under threads 1 to 128 is tested. The test data on my computer is given here:

Benchmark                    Mode  Cnt  Score   Error  Units
TimeStampTest.test1Thread    avgt       0.271           s/op
TimeStampTest.test2Thread    avgt       0.272           s/op
TimeStampTest.test4Thread    avgt       0.278           s/op
TimeStampTest.test8Thread    avgt       0.375           s/op
TimeStampTest.test16Thread   avgt       0.737           s/op
TimeStampTest.test32Thread   avgt       1.474           s/op
TimeStampTest.test64Thread   avgt       2.907           s/op
TimeStampTest.test128Thread  avgt       5.732           s/op

It can be seen that it is relatively fast under 1-4 threads, and it increases linearly after 8 threads.
Test code reference:

@State(Scope.Benchmark)
public class TimeStampTest {

    private static final int MAX = 10000000;

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(TimeStampTest.class.getSimpleName())
                .forks(1)
                .warmupIterations(1)
                .measurementIterations(1)
                .warmupTime(TimeValue.seconds(5))
                .measurementTime(TimeValue.seconds(5))
                .mode(Mode.AverageTime)
                .syncIterations(false)
                .build();

        new Runner(opt).run();
    }

    @Benchmark
    @Threads(1)
    public void test1Thread() {
        for (int i = 0; i < MAX; i++) {
            currentTimeMillis();
        }
    }

    @Benchmark
    @Threads(2)
    public void test2Thread() {
        for (int i = 0; i < MAX; i++) {
            currentTimeMillis();
        }
    }

    @Benchmark
    @Threads(4)
    public void test4Thread() {
        for (int i = 0; i < MAX; i++) {
            currentTimeMillis();
        }
    }

    @Benchmark
    @Threads(8)
    public void test8Thread() {
        for (int i = 0; i < MAX; i++) {
            currentTimeMillis();
        }
    }

    @Benchmark
    @Threads(16)
    public void test16Thread() {
        for (int i = 0; i < MAX; i++) {
            currentTimeMillis();
        }
    }

    @Benchmark
    @Threads(32)
    public void test32Thread() {
        for (int i = 0; i < MAX; i++) {
            currentTimeMillis();
        }
    }

    @Benchmark
    @Threads(64)
    public void test64Thread() {
        for (int i = 0; i < MAX; i++) {
            currentTimeMillis();
        }
    }

    @Benchmark
    @Threads(128)
    public void test128Thread() {
        for (int i = 0; i < MAX; i++) {
            currentTimeMillis();
        }
    }

    private static long currentTimeMillis() {
        return System.currentTimeMillis();
    }
}

solution

The easiest way to think of is to cache the timestamp and update it with a separate thread. In this way, the acquisition is only taken from the memory. The overhead is very small, but the disadvantage is also obvious. The update frequency determines the accuracy of the timestamp.

Cobar

Cobar gets and updates timestamp related codes at

https://github.com/alibaba/cobar/blob/master/server/src/main/server/com/alibaba/cobar/util/TimeUtil.java

/**
 * For the timer with weak precision, considering the performance, the synchronization strategy is not used.
 * 
 * @author xianmao.hexm 2011-1-18 06:10:55 PM
 */
public class TimeUtil {
    private static long CURRENT_TIME = System.currentTimeMillis();

    public static final long currentTimeMillis() {
        return CURRENT_TIME;
    }

    public static final void update() {
        CURRENT_TIME = System.currentTimeMillis();
    }

}

The scheduled scheduling code is located in

https://github.com/alibaba/cobar/blob/master/server/src/main/server/com/alibaba/cobar/CobarServer.java

timer.schedule(updateTime(), 0L, TIME_UPDATE_PERIOD);
...
// System time regular update task
private TimerTask updateTime() {
    return new TimerTask() {
        @Override
        public void run() {
            TimeUtil.update();
        }
    };
}

The update interval time in Cobar_ UPDATE_ The period is 20 milliseconds

Sentinel

Sentinel also uses a cache timestamp, whose code is located in

https://github.com/alibaba/Sentinel/blob/master/sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/TimeUtil.java

public final class TimeUtil {

    private static volatile long currentTimeMillis;

    static {
        currentTimeMillis = System.currentTimeMillis();
        Thread daemon = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    currentTimeMillis = System.currentTimeMillis();
                    try {
                        TimeUnit.MILLISECONDS.sleep(1);
                    } catch (Throwable e) {

                    }
                }
            }
        });
        daemon.setDaemon(true);
        daemon.setName("sentinel-time-tick-thread");
        daemon.start();
    }

    public static long currentTimeMillis() {
        return currentTimeMillis;
    }
}

You can see that Sentinel implements caching every 1 millisecond.
Let's modify the test code to test the performance of Sentinel's implementation under 1-128 threads

Benchmark                    Mode  Cnt   Score   Error  Units
TimeStampTest.test1Thread    avgt       ≈ 10⁻⁴           s/op
TimeStampTest.test2Thread    avgt       ≈ 10⁻⁴           s/op
TimeStampTest.test4Thread    avgt       ≈ 10⁻⁴           s/op
TimeStampTest.test8Thread    avgt       ≈ 10⁻³           s/op
TimeStampTest.test16Thread   avgt        0.001           s/op
TimeStampTest.test32Thread   avgt        0.001           s/op
TimeStampTest.test64Thread   avgt        0.003           s/op
TimeStampTest.test128Thread  avgt        0.006           s/op

You can and directly use system Compared with currenttimemillis, the gap is very obvious.

last

Although the cache timestamp performance can be improved a lot, it is only limited to very high concurrency systems. It is generally applicable to high concurrency middleware. If the general system does this optimization, the effect is not obvious. Performance optimization still needs to grasp the main contradiction, solve the bottleneck, and avoid excessive optimization.

About the author: focus on the development of the middleware in the back end, the author of the official account of "bug catching master", pay attention to me, give you the most pure dry cargo technology.

Topics: Java Multithreading Middleware