Skip to content

Commit 7995b1a

Browse files
jasonjoo2010taz
authored and
taz
committed
Improve performance of TimeUtil adaptively in different load conditions (alibaba#1746)
To achieve this by making TimeUtil have multiple running states: - IDLE: Direct invocation state for idle conditions. - RUNNING: Legacy mode for busy conditions. - REQUEST: Temporary state from IDLE to RUNNING. Through this design, TimeUtil won't cost much in idle systems anymore. Signed-off-by: Jason Joo <[email protected]>
1 parent f34cfa1 commit 7995b1a

File tree

2 files changed

+300
-16
lines changed

2 files changed

+300
-16
lines changed

sentinel-core/src/main/java/com/alibaba/csp/sentinel/util/TimeUtil.java

+189-16
Original file line numberDiff line numberDiff line change
@@ -15,38 +15,211 @@
1515
*/
1616
package com.alibaba.csp.sentinel.util;
1717

18+
import java.util.List;
1819
import java.util.concurrent.TimeUnit;
20+
import java.util.concurrent.atomic.LongAdder;
21+
22+
import com.alibaba.csp.sentinel.log.RecordLog;
23+
import com.alibaba.csp.sentinel.slots.statistic.base.LeapArray;
24+
import com.alibaba.csp.sentinel.slots.statistic.base.WindowWrap;
25+
import com.alibaba.csp.sentinel.util.function.Tuple2;
1926

2027
/**
21-
* Provides millisecond-level time of OS.
28+
* <p>Provides millisecond-level time of OS.</p>
29+
* <p>
30+
* Here we should see that not all the time TimeUtil should
31+
* keep looping 1_000 times every second (Actually about 800/s due to some losses).
32+
* <pre>
33+
* * In idle conditions it just acts as System.currentTimeMillis();
34+
* * In busy conditions (significantly more than 1_000/s) it keeps loop to reduce costs.
35+
* </pre>
36+
* For detail design and proposals please goto
37+
* <a href="https://github.com/alibaba/Sentinel/issues/1702#issuecomment-692151160">https://github.com/alibaba/Sentinel/issues/1702</a>
2238
*
2339
* @author qinan.qn
40+
* @author jason
2441
*/
25-
public final class TimeUtil {
42+
public final class TimeUtil implements Runnable {
43+
private static final long CHECK_INTERVAL = 3000;
44+
private static final long HITS_LOWER_BOUNDARY = 800;
45+
private static final long HITS_UPPER_BOUNDARY = 1200;
46+
47+
public static enum STATE {
48+
IDLE,
49+
PREPARE,
50+
RUNNING;
51+
}
52+
53+
private static class Statistic {
54+
private final LongAdder writes = new LongAdder();
55+
private final LongAdder reads = new LongAdder();
56+
57+
public LongAdder getWrites() {
58+
return writes;
59+
}
60+
61+
public LongAdder getReads() {
62+
return reads;
63+
}
64+
}
65+
66+
private static TimeUtil INSTANCE;
67+
68+
private volatile long currentTimeMillis;
69+
private volatile STATE state = STATE.IDLE;
70+
71+
private LeapArray<Statistic> statistics;
2672

27-
private static volatile long currentTimeMillis;
73+
/**
74+
* thread private variables
75+
*/
76+
private long lastCheck = 0;
2877

2978
static {
30-
currentTimeMillis = System.currentTimeMillis();
31-
Thread daemon = new Thread(new Runnable() {
79+
INSTANCE = new TimeUtil();
80+
}
81+
82+
public TimeUtil() {
83+
this.statistics = new LeapArray<TimeUtil.Statistic>(3, 3000) {
84+
3285
@Override
33-
public void run() {
34-
while (true) {
35-
currentTimeMillis = System.currentTimeMillis();
36-
try {
37-
TimeUnit.MILLISECONDS.sleep(1);
38-
} catch (Throwable e) {
39-
40-
}
41-
}
86+
public Statistic newEmptyBucket(long timeMillis) {
87+
return new Statistic();
4288
}
43-
});
89+
90+
@Override
91+
protected WindowWrap<Statistic> resetWindowTo(WindowWrap<Statistic> windowWrap, long startTime) {
92+
Statistic val = windowWrap.value();
93+
val.getReads().reset();
94+
val.getWrites().reset();
95+
windowWrap.resetTo(startTime);
96+
return windowWrap;
97+
}
98+
};
99+
this.currentTimeMillis = System.currentTimeMillis();
100+
this.lastCheck = this.currentTimeMillis;
101+
Thread daemon = new Thread(this);
44102
daemon.setDaemon(true);
45103
daemon.setName("sentinel-time-tick-thread");
46104
daemon.start();
47105
}
48106

107+
@Override
108+
public void run() {
109+
while (true) {
110+
// Mechanism optimized since 1.8.2
111+
this.check();
112+
if (this.state == STATE.RUNNING) {
113+
this.currentTimeMillis = System.currentTimeMillis();
114+
this.statistics.currentWindow(this.currentTimeMillis).value().getWrites().increment();
115+
try {
116+
TimeUnit.MILLISECONDS.sleep(1);
117+
} catch (Throwable e) {
118+
}
119+
continue;
120+
}
121+
if (this.state == STATE.IDLE) {
122+
try {
123+
TimeUnit.MILLISECONDS.sleep(300);
124+
} catch (Throwable e) {
125+
}
126+
continue;
127+
}
128+
if (this.state == STATE.PREPARE) {
129+
RecordLog.debug("TimeUtil switches to RUNNING");
130+
this.currentTimeMillis = System.currentTimeMillis();
131+
this.state = STATE.RUNNING;
132+
continue;
133+
}
134+
}
135+
}
136+
137+
/**
138+
* Current running state
139+
*
140+
* @return
141+
*/
142+
public STATE getState() {
143+
return state;
144+
}
145+
146+
/**
147+
* Current qps statistics (including reads and writes request)
148+
* excluding current working time window for accurate result.
149+
*
150+
* @param now
151+
* @return
152+
*/
153+
public Tuple2<Long, Long> currentQps(long now) {
154+
List<WindowWrap<Statistic>> list = this.statistics.listAll();
155+
long reads = 0;
156+
long writes = 0;
157+
int cnt = 0;
158+
for (WindowWrap<Statistic> windowWrap : list) {
159+
if (windowWrap.isTimeInWindow(now)) {
160+
continue;
161+
}
162+
cnt++;
163+
reads += windowWrap.value().getReads().longValue();
164+
writes += windowWrap.value().getWrites().longValue();
165+
}
166+
if (cnt < 1) {
167+
return new Tuple2<Long, Long>(0L, 0L);
168+
}
169+
return new Tuple2<Long, Long>(reads / cnt, writes / cnt);
170+
}
171+
172+
/**
173+
* Check and operate the state if necessary.
174+
* ATTENTION: It's called in daemon thread.
175+
*/
176+
private void check() {
177+
long now = currentTime(true);
178+
// every period
179+
if (now - this.lastCheck < CHECK_INTERVAL) {
180+
return;
181+
}
182+
this.lastCheck = now;
183+
Tuple2<Long, Long> qps = currentQps(now);
184+
if (this.state == STATE.IDLE && qps.r1 > HITS_UPPER_BOUNDARY) {
185+
RecordLog.info("TimeUtil switches to PREPARE for better performance, reads={}/s, writes={}/s", qps.r1, qps.r2);
186+
this.state = STATE.PREPARE;
187+
} else if (this.state == STATE.RUNNING && qps.r1 < HITS_LOWER_BOUNDARY) {
188+
RecordLog.info("TimeUtil switches to IDLE due to not enough load, reads={}/s, writes={}/s", qps.r1, qps.r2);
189+
this.state = STATE.IDLE;
190+
}
191+
}
192+
193+
private long currentTime(boolean innerCall) {
194+
long now = this.currentTimeMillis;
195+
Statistic val = this.statistics.currentWindow(now).value();
196+
if (!innerCall) {
197+
val.getReads().increment();
198+
}
199+
if (this.state == STATE.IDLE || this.state == STATE.PREPARE) {
200+
now = System.currentTimeMillis();
201+
this.currentTimeMillis = now;
202+
if (!innerCall) {
203+
val.getWrites().increment();
204+
}
205+
}
206+
return now;
207+
}
208+
209+
/**
210+
* Current timestamp in milliseconds.
211+
*
212+
* @return
213+
*/
214+
public long getTime() {
215+
return this.currentTime(false);
216+
}
217+
218+
public static TimeUtil instance() {
219+
return INSTANCE;
220+
}
221+
49222
public static long currentTimeMillis() {
50-
return currentTimeMillis;
223+
return INSTANCE.getTime();
51224
}
52225
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright 1999-2020 Alibaba Group Holding Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.alibaba.csp.sentinel.util;
17+
18+
import static org.junit.Assert.assertEquals;
19+
import static org.junit.Assert.assertTrue;
20+
21+
import java.util.concurrent.atomic.AtomicBoolean;
22+
import java.util.concurrent.atomic.AtomicInteger;
23+
24+
import org.junit.Before;
25+
import org.junit.Test;
26+
27+
import com.alibaba.csp.sentinel.log.RecordLog;
28+
import com.alibaba.csp.sentinel.util.function.Tuple2;
29+
30+
/**
31+
* @author jason
32+
*
33+
*/
34+
public class TimeUtilTest {
35+
@Before
36+
public void initLogging() {
37+
System.setProperty("csp.sentinel.log.output.type", "console");
38+
}
39+
40+
private void waitFor(int step, int seconds) throws InterruptedException {
41+
for (int i = 0; i < seconds; i ++) {
42+
Tuple2<Long, Long> qps = TimeUtil.instance().currentQps(TimeUtil.currentTimeMillis());
43+
RecordLog.info("step {} qps: reads={}, writes={}", step, qps.r1, qps.r2);
44+
Thread.sleep(1000);
45+
}
46+
}
47+
48+
@Test
49+
public void test() throws InterruptedException {
50+
final AtomicInteger delayTime = new AtomicInteger(50);
51+
final AtomicBoolean shouldShutdown = new AtomicBoolean();
52+
for (int i = 0; i < 10; i ++) {
53+
new Thread(new Runnable() {
54+
55+
@Override
56+
public void run() {
57+
long last = 0;
58+
while (true) {
59+
if (shouldShutdown.get()) {
60+
break;
61+
}
62+
long now = TimeUtil.currentTimeMillis();
63+
int delay = delayTime.get();
64+
if (delay < 1) {
65+
if (last > now) {
66+
System.err.println("wrong value");
67+
}
68+
last = now;
69+
continue;
70+
}
71+
try {
72+
Thread.sleep(delay);
73+
} catch (InterruptedException e) {
74+
}
75+
if (last > now) {
76+
System.err.println("incorrect value");
77+
}
78+
last = now;
79+
}
80+
}
81+
}).start();
82+
}
83+
Tuple2<Long, Long> qps;
84+
waitFor(1, 4);
85+
// initial state
86+
assertEquals(TimeUtil.STATE.IDLE, TimeUtil.instance().getState());
87+
qps = TimeUtil.instance().currentQps(TimeUtil.currentTimeMillis());
88+
assertTrue(qps.r1 < 1000);
89+
assertTrue(qps.r2 < 1000);
90+
91+
// to RUNNING
92+
delayTime.set(0);
93+
// wait statistics to be stable
94+
waitFor(2, 8);
95+
qps = TimeUtil.instance().currentQps(TimeUtil.currentTimeMillis());
96+
assertEquals(TimeUtil.STATE.RUNNING, TimeUtil.instance().getState());
97+
assertTrue(qps.r1 > 1000);
98+
assertTrue(qps.r2 <= 1000);
99+
100+
// back
101+
delayTime.set(100);
102+
// wait statistics to be stable
103+
waitFor(3, 8);
104+
qps = TimeUtil.instance().currentQps(TimeUtil.currentTimeMillis());
105+
assertEquals(TimeUtil.STATE.IDLE, TimeUtil.instance().getState());
106+
assertTrue(qps.r1 < 1000);
107+
assertTrue(qps.r2 < 1000);
108+
shouldShutdown.set(true);
109+
}
110+
111+
}

0 commit comments

Comments
 (0)