EDA365电子论坛网
标题:
这是一个诡异的时间问题追查
[打印本页]
作者:
mutougeda
时间:
2020-3-9 10:17
标题:
这是一个诡异的时间问题追查
5 A% A( S( w$ |& T
问题引入:
7 Q! D. l ~3 L. j" N* z8 }
问题是我们重装了一台电脑之后,发现apache
( _; N! K( ?8 z1 [' ?: i s
的日志里突然增加了很多服务时间超长的请求,如下面两条:
9 j5 C6 u [( B# H
** - - 4913229 11227 [29/Mar/2012:09:59:58 +0800] "GET*****
6 ^$ P' ~0 q1 u: Q# q9 N
** - - 4793564 9846 [29/Mar/2012:09:59:58 +0800] "GET *****
a* J% }2 Z% {) r
请求服务时间都在4s以上!这绝对无法忍受啊!
8 m$ g9 e# f9 b5 e) s8 M, ]
D: U8 M" E C8 y0 I: D% C
分析过程:
( k$ ~5 I( K7 g
7 j4 N8 J% U% m
但是,当我们:
8 G* r) o- K' h- t+ J5 u" U, k
* 手动访问这台服务器之后,发现基本上请求都是正常响应的;
/ u0 Z3 z, c- o8 q) I4 g
* 从前端反向代理的日志来看,服务时间也是正常的;
# f* D8 a8 L$ S' h' i) |
那这样推断,真相只有一个,那就是apache的日志时间记录有问题!
! Y# F2 M8 _7 ]
+ T' _) E# B% x# _+ c! M
什么问题呢?不清楚,但是apache
9 {9 l7 @3 v5 k: h% \3 R; a
的时间记录不外乎是在收到请求时记个时间,请求处理完成之后再记一个时间,既然是时?
# o" D( i3 p$ ^2 V
涞奈侍猓敲淳涂纯聪低呈奔浒桑幼罴虻サ?date 开始,多执行几次:
- R* e6 a" R! d! U6 X% F& P
apache2 # date
7 r2 I8 T* c V4 W) c7 H4 x8 | J
Wed Mar 28 17:25:08 CST 2012
6 r1 V( @; N ?
apache2 # date
3 W8 H2 V/ S4 ]; M1 D/ W
Wed Mar 28 17:25:04 CST 2012 时间减少了?
1 a8 o. \) W! t9 [
apache2 # date
$ j" H4 _" L, U/ N" c0 Z' Y
Wed Mar 28 17:25:06 CST 2012
; n, a3 I& W r- @
apache2 # date
3 S5 w2 m5 e8 [; G! _* L- @8 H
Wed Mar 28 17:25:12 CST 2012
( q- e1 i/ L1 h7 w! ]
apache2 # date
' V3 u5 T _ ^
Wed Mar 28 17:25:13 CST 2012
2 y+ `$ g% G9 |. _% Z5 F [9 Y
apache2 # date
, s% r2 P. e P( j
Wed Mar 28 17:25:09 CST 2012 时间减少了?
* e' G& x$ n2 ?* T/ l
apache2 # date
# \4 L6 K! |% s0 a) v- C
Wed Mar 28 17:25:09 CST 2012
9 F" d' q. O; g, W. h& M
apache2 # date
, u* Z$ i) y0 N u/ A
Wed Mar 28 17:25:15 CST 2012
) w* v' R* }( i& |+ z% g q
那么第一个怀疑就是系统里有进程在自己不断的调时间,比如ntpdate, ntpd
2 I) {! E# C/ G
,那么我们首先把 ntpd给停掉;然后系统也没有任何地方会调用ntpdate;
: T8 f9 B* \$ _; x" ?+ Z2 s0 I
继续观察,发现停掉ntpd后,现象依旧!
1 m9 F0 S+ e. `
还有什么可能呢? date
8 [! J$ s* [8 {% V0 Y( J6 ~
程序是从内核里面读取时间还是说需要读取硬件时间?到底是怎么计算的呢?
6 K+ P% _7 R- ^3 m! p/ p, y
请教下同事们,据说 sys_time 是读的kernel里的时间,而 sys_gettimeofday
( h B ~, s m% @! ]' A9 C" `
是需要读硬件时钟的。
6 L& C6 J$ B) h. ?: L
那么就测试一把吧:
1 S8 N; |+ G0 t: ?3 C0 \6 K
先写个程序调用 time(), 然后打印结果,结果是这样的:
o% d0 {. s$ G# v$ [
1332986690
( q! ?! R: ~2 p: [1 ~2 }; X
1332986692
9 m! q8 A7 Q$ b% P* k% }' V- P
1332986693
+ R0 {8 r; m: A+ u8 J
1332986693
( b( B, l: D' }4 b+ c
1332986694
2 E1 A/ F; z2 U: V3 |: g
1332986694
: s! S- e4 F& ]& X, b& x/ }
1332986695
1 B8 f8 @2 G8 j9 i
1332986695
6 O: [, n# N) O0 ]5 E; g
1332986696
7 r' k7 J' _: p- Q2 n# z
1332986696
9 s7 x5 j& \& F; E" V6 w! z* `
1332986697
, ` n- b5 o3 N5 Z4 m8 S
1332986697
& ], C- ~+ z% v9 I
1332986698
4 n/ C& N3 Q, {+ d
1332986698
. A4 E- s! v$ O% S4 b+ b
1332986699
( @2 l' a" L- w9 ?) t+ Y
确实没有问题了!看来有可能是硬件问题了,再看看clock_gettime 的调用:
* Q- n' U; {5 E9 ~1 F o7 v" ?
{
3 i( o( v6 |% f* {& a+ i( T, y- S1 n
struct timespec ts;
0 H ]; R0 {) ~9 n7 ~
int ret = 0;
' n5 E& n1 i9 } N {' V3 L; i: @
ret = clock_gettime(CLOCK_REALTIME, ts);
' g0 n, _( \: t
printf("ret: %d, time: %d %d", ts.tv_sec, ts.tv_ns
) d/ A: Q8 U O, d( \
}
% c- J, R# d2 \* I
编译: gcc -g -o clock_gettime getdate.c -lrt
8 s) e3 Q0 e0 ]6 G- r
" x8 S3 V+ v% m1 f( L, Z/ Q4 a
~ # ./clock_gettime
* G: }8 v- {3 O
ret: 1332991087, time: 594076000 -1
5 o2 g& z8 S+ C- Z& z
ret: 1332991088, time: 379009000 -1
* g& O% |# P' Y8 {) s; Y; P- g
ret: 1332991089, time: 68561000 -1
7 w) p! F) f5 x3 B6 N' _/ Z% x
ret: 1332991089, time: 714013000 -1
( ]* ]7 }, f1 s! ^
ret: 1332991086, time: 511250000 -1 时间在跳
' v9 m! f8 p6 u- K
ret: 1332991091, time: 945626000 -1
; _: G" V- J- n1 T$ o, L0 W
ret: 1332991092, time: 650479000 -1
. y0 M @" s$ }8 m P" a- ~8 P
ret: 1332991088, time: 734780000 -1
( M8 i% B# G! P: P& I+ {) M
ret: 1332991094, time: 506114000 -1时间在跳
: i% n5 Y( N) h+ ^% u
看来果然有可能是硬件问题!
" i' l6 S$ U# Q& W0 c4 U' p
那么,检查一下,直接调用一下 hwclock --show ,看看是不是可以验证这个问题:
* T1 S/ t( f& u: w/ T0 S
~ # hwclock --show
8 ]& r6 a# n, d9 \* |( c
Thu 29 Mar 2012 10:22:21 AM CST -0.948923 seconds
( O5 V1 c$ u& f2 `% _* C
~ # hwclock --show
1 K$ I9 D. y2 l; U% X* h+ B1 X( D
Thu 29 Mar 2012 10:22:22 AM CST -0.188950 seconds
& g2 X. C5 x7 U: S8 e% m# r
~ # hwclock --show
0 ? F2 S5 ~: E+ r, a$ x
Thu 29 Mar 2012 10:22:23 AM CST -0.244766 seconds
5 `9 Z6 i/ S4 I5 G7 H' Q6 Q
~ # hwclock --show
5 J$ \4 R# V" P* q
Thu 29 Mar 2012 10:22:24 AM CST -0.336868 seconds
0 C! t' o q- Q& W
~ # hwclock --show
- Q0 x6 u! ?9 T6 }7 _& A
Thu 29 Mar 2012 10:22:25 AM CST 4.237159 seconds
" B3 U% }/ w, w1 {+ |
~ # hwclock --show
0 ~2 U. h2 c5 Z, E1 `" S& h+ `
Thu 29 Mar 2012 10:22:26 AM CST 4.238672 seconds
; C9 M$ }- u. p
~ # hwclock --show
- L+ V4 y& Q. [% k/ E
Thu 29 Mar 2012 10:22:27 AM CST -0.379418 seconds
7 B5 y% A- C- c, w! r
时间居然都是正确的!显然问题没有那么简单。。。那到底是怎么回事??还是从date
4 J5 j3 E$ B/ Y8 G, _
开始查起吧:
/ B( |* ?+ I1 A
date是一个glibc里面的程序,那它的时间源是从哪里来的?查看一下glibc
' ~9 l) Y: B2 O8 @0 N
的源码,它的时间从这里来:
' \3 k" w8 D/ C
// glibc coreutils gettime.c
9 I3 K5 M0 } V1 _5 v: k
/* Get the system time into *TS. */
$ h$ K" }8 o# L, f. V
void
5 S' C% M* l+ r6 ]0 m
gettime (struct timespec *ts)
( i, c3 \1 m% |8 n. |5 t
{
& A* H* t. e' [' D A. d! q a
#if HAVE_NANOTIME
* J3 O) {8 A, `$ u
nanotime (ts);
* q. X+ e# V' l% h( ?' n6 [
#else
1 ^+ ?/ c1 z( w
8 {+ V8 @! N& [- p
# if defined CLOCK_REALTIME && HAVE_CLOCK_GETTIME
" U; `+ Q2 f/ s# y8 p
if (clock_gettime (CLOCK_REALTIME, ts) == 0)
* u W5 H0 B# r: e' F
return;
8 y! l% ~' {# g* @+ T/ x% M
# endif
' S( {9 k3 [0 X9 F; R! n
# G I+ N* q6 {5 f" K/ {0 S1 r2 g i
# if HAVE_GETTIMEOFDAY
f1 m: }1 @+ {9 ?9 k) X3 V
{
6 S6 s/ G9 {8 U9 s5 ?' ?. a+ A7 y
struct timeval tv;
0 w/ \8 P. R$ J; S/ R" S) c; S& e6 H
gettimeofday (&tv, NULL);
: X% o7 r" ^' ]- m* I! [
ts->tv_sec = tv.tv_sec;
- ^) @3 H$ [3 H5 {- h2 \! Y
ts->tv_nsec = tv.tv_usec * 1000;
6 |6 l( O% B- e" w" ~3 `
}
& M1 I3 y! |, s% t. A" B# ^
# else
: z- z) J) H7 M. v% n% s! y
ts->tv_sec = time (NULL);
: ^; r" t$ p% c
ts->tv_nsec = 0;
2 [0 J. p: t# M: X: W9 O
# endif
) f6 T' L- u3 O$ V: t+ \( l
% L1 a# n. J/ u) g( k" A: p* G& ~2 O
#endif
' u- P8 _& d$ V3 F
}
& \7 L1 y. ]3 C" c
按照 nanotime, clock_gettime, gettimeofday的顺序去读取,我们这个系统中就是
' e0 x/ {- R# q
clock_gettime,从上面的小程序中也可以看到clock_gettime的时间是有问题的,那么
8 V! V7 |3 ^5 \( t/ W
clock_gettime的时间源是怎么来的呢?
$ J. o9 ^) \* ? ?9 y: F
// linux kernel 2.6.18 arch/x86_64/kernel/time.c
0 Z, W/ l7 c& ]% ~
void do_gettimeofday(struct timeval *tv)
4 N$ a" X j2 C. i# J- D
{
5 T8 T {* @# y6 u
unsigned long seq, t;
: x" v# `; r) q' a
unsigned int sec, usec;
" }7 r4 E2 g4 a! e
! r5 a4 Z, |$ C* C$ W; J
do {
- q( N, S- @) d7 {
seq = read_seqbegin(&xtime_lock);
# K6 G/ g" E1 X5 x4 D4 d; s7 Q
* B" C1 |* L( n6 _
sec = xtime.tv_sec;
" J+ l9 o5 g9 w8 |
usec = xtime.tv_nsec / NSEC_PER_USEC;
+ e- }' T& T# x( E2 E5 w
t = (jiffies - wall_jiffies) * USEC_PER_TICK +
) a, G) ~* k0 ^. @1 t% @) s7 Y
do_gettimeoffset();
. w- H' g; |. P
usec += t;
/ d6 D$ u5 A! T) D
% c8 ]5 O) M$ i, p9 H# T! g% S# Z
} while (read_seqretry(&xtime_lock, seq));
- [: j$ O* m; ^! I
: S. ^- g- w) d/ E8 B$ Z# [
tv->tv_sec = sec + usec / USEC_PER_SEC;
$ |8 o& B5 _/ V
tv->tv_usec = usec % USEC_PER_SEC;
4 o" ]% }4 H5 {% C3 `! f
}
; n+ e. H. F2 ~
反正就是一通计算,其中比较重要的是 do_gettimeoffset
* K8 T* ~) f: b, s0 M$ Q3 {
,这个函数的目的是计算上一次更新 xtime
7 ^% A8 K2 g/ S; } ^
到当前执行这一段时间的偏移量,然后用这个偏移量去加到 xtime
) p% F0 U& _0 y, c( z
上去,就是当前的时间(因为xtime的精度不够,clock_gettime
% ^) m) R) U4 t( d/ t
的精度要求更高,所以必须计算一个偏移量)。
* w: d% O6 z+ [+ Z8 S) s) `
那么,这个 do_gettimeoffset又是从哪来的?同样的文件里面有:
$ T- X1 ^( l4 G, S
static inline unsigned int do_gettimeoffset_tsc(void)
0 @2 ^; _! E8 _! t( i9 I* _
{
; D, t3 B3 \" Z4 v9 }
unsigned long t;
$ u# m2 R m$ A, T, r: T. j! j
unsigned long x;
1 }2 K I$ T* }; g N& Q
t = get_cycles_sync();
& G! q0 y1 A5 U/ h+ L2 f
if (t < vxtime.last_tsc)
. w9 Z% o. P- ] x0 U/ [3 C k x
t = vxtime.last_tsc; /* hack */
4 E( |4 F6 E7 Y" M
x = ((t - vxtime.last_tsc) * vxtime.tsc_quot) >> US_SCALE;
1 u8 ^3 y) G# M8 g4 U& M
return x;
/ Q( \+ V8 {% y1 ]; \6 P
}
4 l0 G2 \3 [! C& H( F
; Z$ ?# N! H' ~
static inline unsigned int do_gettimeoffset_hpet(void)
# b% e: I9 M' M. U% l9 V4 j# A9 \
{
; `, t C: A" r0 [2 l" x
/* cap counter read to one tick to avoid inconsistencies */
4 {% Z" L; k/ C, O8 ^4 K
unsigned long counter = hpet_readl(HPET_COUNTER) - vxtime.last;
! A' Z, V5 p% G! x( a# J
return (min(counter,hpet_tick) * vxtime.quot) >> US_SCALE;
/ a7 {5 o1 A$ U5 H& K
}
& O! H3 _! O4 w' J- J
& l* K' V8 O9 [ ]
unsigned int (*do_gettimeoffset)(void) = do_gettimeoffset_tsc;
# ?! p- F9 a! I" {0 ^( a2 a
也就是说,go_gettimeoffset 是从 tsc寄存器从拿到的,这又是神马玩意呢?
+ N- z0 E& d7 G
参考一下下面的几篇文章【1】【2】【3】,基本就明白了:
" N, R5 }3 j7 N0 F9 N- d% l2 V
tsc是一个cpu内的寄存器,用来累加从系统启动到目前,所有的时钟周期数,全称是
# }# z# z! o! a. K2 n8 T& o" ~
Timestamp Counter;
( S5 D. s! [, d6 g
而且,最重要的是我们发现了一个重要的线索,那就是:
; z7 ~7 j! h: u; W' L. F5 ~5 a3 @
在多核cpu的架构中,tsc寄存器是很有可能不一致的,特别是在intel的比较老的多核CPU
$ ]# T( s8 E, [( J X5 R( C5 r- r
中!坑爹....
% o- k2 {, L0 T. R* F3 e2 q
很多文章提到过这个问题,比如【1】【2】【4】,显然如果不同cpu
9 M4 P# F% F# w( c4 X
读到的值不同的话,多次执行,显然时间是不一致的。
* s* p& F; t/ x$ P: x, m' h6 V- v
看起来就是这个问题了,那么就来验证一把:
" O; @, l) V& H' c* H" k
#include<stdlib.h>
( f: A: e( n0 ]9 M& ^( v
#include<stdio.h>
( B7 Y; D/ r: n1 o Q, I2 G
#include<sys/types.h>
: \9 X- v3 G: x
#include<sys/sysinfo.h>
$ u3 D$ U8 t; b4 U0 w2 Y% _( y
#include<unistd.h>
7 o0 P M- O- S' ~! @
#include<time.h>
7 A+ W4 a& D* U( Z% ~% E
Q) |' F, n5 Y$ e) {/ Y
#define __USE_GNU
+ r, o% V6 l1 L; ~5 }& A6 |2 ?
#include<sched.h>
- c# a5 g( ]$ C8 y
#include<ctype.h>
; m/ S3 d- `! G% b& X; Q, S
#include<string.h>
6 @6 x3 Y% f" n9 {# L
5 b% P$ a! @0 H& t! L
8 \& P1 O f; G
! X# R9 q6 L0 m8 I9 {
int main(int argc, char **argv){
9 V# t/ A" k0 V! Y- ^+ z4 w
+ p p5 N* U% w% a _
cpu_set_t cs;
, U& O/ ], Z- ]6 V$ O! X
CPU_ZERO(&cs);
# }* {" L+ b9 w. u4 i8 O
CPU_SET(0, &cs);
; T! {8 h9 V4 i* Y! I; O
$ P+ v/ B$ F2 ?4 @- D) J
sched_setaffinity(0, sizeof(cpu_set_t), &cs);
( w, |, w! z2 o k& m: r
& e. w9 W/ a9 Q% F
struct timespec ts;
7 h8 p+ Q4 Z9 g$ M/ l
int ret = 0;
3 a1 Q5 X. u/ M) p( {! \
ret = clock_gettime(CLOCK_REALTIME, &ts);
1 m N6 \' ]# }; g* Y4 |& `
printf("ret: %d, time: %lld %lld", ret, ts.tv_sec, ts.tv_nsec);
- f& X! S5 T! x- X# H. K0 d$ Z2 J* o3 w
- U, L `8 m7 @6 A* V
}
3 s! x# v' D. I& d) c7 F* S2 `
3 q% G6 @6 c1 V% |" K2 U
gcc -g -o cpu0_clock_gettime cpu_getdate.c -lrt
& [ N) x+ d4 _
. P, ^/ ?1 `4 x) V0 Y3 d2 F
ret: 0, time: 1332992815 537481000
+ {0 O$ D; n, }& Q/ t
ret: 0, time: 1332992816 506704000
3 c2 V/ ]- E0 K) Z9 R" T
ret: 0, time: 1332992817 9910000
% Z2 Y# E* B6 X8 Q K
ret: 0, time: 1332992817 480431000
2 U9 J( S9 X& S5 ]6 |1 O9 O
ret: 0, time: 1332992817 936965000
1 v1 K* ~: x. S% R% R
ret: 0, time: 1332992818 416634000
8 B6 H" o! r- {
ret: 0, time: 1332992818 850319000
9 D+ W% e# T S
ret: 0, time: 1332992819 305075000
3 X2 N! ]* {7 o9 a
ret: 0, time: 1332992819 729108000
' {" B( G% Q6 h
ret: 0, time: 1332992820 164726000
" o5 f8 z. u, p: ~ \: I
ret: 0, time: 1332992820 577573000
& G, Y+ a' \5 u+ A3 m
ret: 0, time: 1332992820 985025000
* k' @& _6 A8 n- P$ Q
ret: 0, time: 1332992821 394070000
$ m, T& _/ C. e0 {, j' a5 \
ret: 0, time: 1332992821 816708000
4 }) M8 ?/ |5 y( D, w. x& D
ret: 0, time: 1332992822 216811000
8 M. F: w, g4 R' k& F. m
ret: 0, time: 1332992822 616901000
9 b+ Y4 Q. w* m) D
ret: 0, time: 1332992823 18564000w
" u/ d4 }! A1 V& G3 i7 M2 i
ret: 0, time: 1332992823 727585000
: O0 y" {4 d2 L) O
通过把程序的调度固定在某一个具体的cpu中,那么clock_gettime的结果也是正确的!
3 }! {4 |. D: h0 s4 Z2 S2 {5 l
经过几次测试,很容易找到两个cpu时间不一致的情况,具体这个例子来说就是 cpu0和
9 r& O, X6 e4 l; G2 i r1 d" c
cpu5,把上面的程序修改一下,可以看到结果基本上每次都会跳一把。
# z/ D( D4 w" N$ Y: t
问题解决:
. o* y: c( v- D
那么,tsc在多核cpu中的值不同,应该怎么解决呢? 如 【4】所示,在高版本的linux
0 z. C7 E* Z0 ~5 C5 b5 k
内核中,有人提交了补丁,可以在内核启动时检查并且同步一把,那么升级一下内核到 2
+ C( J; \ L$ n& k
.6.38。
+ [) ]! G% h5 e. e8 r% u
重新启动到新内核之后,问题解决!
4 t4 J2 z# q* C- D$ M
总结一下:
( s9 Z9 h& Y- Z1 r
1. apache的时间统计方式,没具体看实现,但是显然前后的两个时间可能是在不同的cpu
+ h" o# G7 H9 m+ v: m- X9 @
核上计算出来的。
# X. [! N) S" y' U; \
2. tsc的值,并不能保证是同步的,特别是在某些Intel cpu
& ]% u1 w/ P4 M/ s. n: W6 f2 i
的情况下;但是新的内核会做一些检查和resync的工作;
- M2 B4 Z: h! L3 l1 h
3. time()调用直接读取内核的xtime值,不会触及硬件;但是 gettimeofday(),
3 b* U7 F4 L; q% c. L h0 A) Z( i
clock_gettime() 在 x86_64下都会读取tsc值;
$ c. t" d% |/ u& e
4. hwclock读的硬件时钟显然不是 tsc,应该是实时时钟RTC;
5 w/ ~3 \& l7 F7 `
【1】 http://en.wikipedia.org/wiki/Time_Stamp_Counter
, p6 r& c7 f8 y7 C* ~: q
【2】 http://juliusdavies.ca/posix_clocks/clock_realtime_linux_faq.html
; b! L. k* v: l# D$ T- {
【3】http://www.linuxforum.net/forum/gshowflat.php?Cat=&Board=driver&Number=
2 B7 b/ w9 I9 s$ F r% J0 R, Q! L* ^
385219&page=0&view=collapsed&sb=5&o=all&fpart , linux时钟中断机制,Copyright
7 o8 ]; |* m: N) p6 g& h7 S
2003 by 詹荣开
+ Y0 Q! S. O# @3 j8 C
【4】http://lwn.net/Articles/211051/ x86: unify/rewrite SMP TSC sync code
作者:
CCxiaom
时间:
2020-3-9 17:20
时间问题追查,牛逼了
欢迎光临 EDA365电子论坛网 (https://bbs.eda365.com/)
Powered by Discuz! X3.2