같은 초에 들어온 주문의 순서를 가려야 했다. 저장된 시각을 열어 보니 이랬다.
Table of contents
Open Table of contents
증상 — 같은 초에 들어온 셋
세 건의 시각이 이렇다.
2018-03-04 14:22:07
2018-03-04 14:22:07
2018-03-04 14:22:07
14:22:07 이 셋 다 같아서 어느 것이 먼저인지 알 수 없다.
같은 초에 여러 건이 들어오는 것이 드문 일이 아니었다. reg_date 를 초 단위로 저장하는 한 이 물음에는 답할 수 없다.
원인 — 컬럼 타입이 초까지였다
컬럼 정의를 봤다.
`reg_date` datetime NOT NULL
datetime 은 자릿수를 안 주면 초까지만 담고 그 아래는 버린다.
ALTER TABLE orders MODIFY reg_date datetime(6) NOT NULL;
(6) 을 주면 마이크로초까지 들어간다.
다만 ALTER TABLE 을 해도 이미 들어간 값의 소수부는 전부 .000000 이다. 앞으로 들어올 것만 가릴 수 있다.
값을 만드는 쪽의 자릿수
컬럼을 바꿨는데 새로 들어오는 값도 소수부가 0이었다.
$row['reg_date'] = date('Y-m-d H:i:s');
date('Y-m-d H:i:s') 가 값을 만드는 자리에서 이미 잘라 내고 있었다.
$row['reg_date'] = (new DateTimeImmutable())->format('Y-m-d H:i:s.u');
.u 를 붙여야 마이크로초가 문자열에 실린다.
저장 자리를 넓히는 것과 값을 만드는 쪽을 고치는 것이 둘 다 필요했다. 한쪽만 고치면 아무것도 안 바뀌는데 datetime(6) 만 보고 끝냈으면 그대로 넘어갔을 것이다.
제약 — 시각은 순서의 근거로 약하다
마이크로초까지 가도 같은 값이 나올 수 있다. 서버가 둘 이상이면 사정이 더 나쁘다.
서버마다 시계가 조금씩 다르다
시각 동기화가 시계를 뒤로 당길 수 있다
ntpd 가 시계를 뒤로 당기면 나중에 들어온 것이 더 이른 reg_date 를 갖는다.
순서가 필요한 자리에는 증가하는 번호를 쓰는 쪽으로 갔다.
`seq` bigint NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`seq`)
시각은 언제인지 보는 용도로 남기고 순서는 seq 로 가린다.
datetime(6) 은 같은 값이 나올 확률을 줄일 뿐 없애지 못한다. 순서를 묻는 질문에는 순서를 보장하는 값으로 답해야 했다.
설정 — 어느 시계를 쓸지
값을 만드는 자리가 셋이었다.
date('Y-m-d H:i:s') // 애플리케이션 서버 시계
NOW() // DB 서버 시계
$request->input('client_time') // 보낸 쪽 시계
date() 와 NOW() 와 client_time 이 서로 다른 시계를 보므로 값이 조금씩 다르게 나온다.
무엇을 쓸지 정해서 문서에 적었다. 저장 시각은 DB 서버 시계를 쓰고 보낸 쪽 시각은 따로 컬럼에 남기되 순서 판단에는 쓰지 않기로 했다.
`reg_date` datetime(6) NOT NULL, -- DB 시계
`client_date` datetime(6) DEFAULT NULL -- 보낸 쪽이 적은 값
보낸 쪽 시각을 버리지 않은 것은 지연을 볼 때 필요해서다. 두 값이 함께 있으면 우리 쪽에 닿기까지 얼마나 걸렸는지가 나온다.
대응 — 저장과 표시의 분리
저장은 한 기준으로 하고 보여 줄 때 바꾸게 했다.
`reg_date` datetime(6) NOT NULL COMMENT 'UTC'
$dt = new DateTimeImmutable($row['reg_date'], new DateTimeZone('UTC'));
echo $dt->setTimezone(new DateTimeZone('Asia/Seoul'))->format('Y-m-d H:i:s.v');
Asia/Seoul 로 저장하면 그 값만 보고는 어느 기준인지 알 수 없게 된다.
무엇으로 저장했는지를 컬럼 주석에 적어 둔 것은 그 때문이다. 주석 한 줄이 없어서 나중에 값을 잘못 해석하는 일이 자주 생긴다.
마지막으로 서버 시계가 실제로 얼마나 벌어져 있는지 쟀다.
$ ntpq -p
remote refid st t when poll reach delay offset jitter
*ntp-01 ... 2 u 41 64 377 0.412 -0.038 0.055
ntpq 의 offset 이 커지는 것을 지켜보고 임계를 넘으면 알림이 가게 뒀다.
여러 서버가 각자 시각을 기록하면 벌어진 만큼이 그대로 오차가 된다. 밀리초 단위 순서를 다루면서 시계가 수백 밀리초 벌어져 있으면 앞의 노력이 전부 의미가 없다.
정리
datetime은 자릿수를 안 주면 초까지만 담는다datetime(6)으로 바꿔도 지나간 값의 소수부는 0이다- 컬럼을 바꿔도 만드는 쪽이 초까지만 넘기면 값이 안 들어온다
- 저장 자리와 만드는 쪽을 같이 봐야 한다
- 정밀도를 올려도 같은 값이 나올 가능성은 남는다
- 시각 동기화가 시계를 뒤로 당길 수 있다
- 순서가 필요하면 증가하는 번호를 쓴다
- 애플리케이션 시계와 DB 시계와 보낸 쪽 시계 중 무엇을 쓸지 정해 적는다
- 보낸 쪽 시각은 따로 남기면 지연을 볼 수 있다
- 저장은 한 기준으로 하고 보여 줄 때 바꾼다
- 시계가 벌어진 만큼이 그대로 오차가 된다