Skip to content
isdnetworks
Go back

배치가 표를 잡고 있었다

수집 서버에서 INSERT 가 간헐적으로 실패했고 남은 오류는 시간 초과였다. 확인해 보니 그때마다 같은 시각에 통계 배치가 함께 돌고 있었다.

Table of contents

Open Table of contents

무엇이 잡고 있는지 봤다

문제가 나는 시각에 접속해서 확인했다.

SHOW PROCESSLIST;
Id     Time   State                  Info
1042   183    Sending data           UPDATE device_data SET flag=1 WHERE ...
1103    28    Waiting for table lock  INSERT INTO device_data ...
1104    27    Waiting for table lock  INSERT INTO device_data ...

배치의 UPDATE 가 183초째 돌고 INSERTWaiting for table lock 에 걸려 있다. 저장이 실패한 것이 아니라 자기 차례를 못 받은 것이었다.

SELECT * FROM information_schema.INNODB_TRX\G

시작 시각과 잠근 행 수가 나온다. 오류 메시지도 InnoDB 가 잠금을 기다리다 innodb_lock_wait_timeout 에 걸렸다는 쪽이었다.

한 번에 너무 많이 바꾸고 있었다

배치 쿼리를 봤다.

UPDATE device_data
SET processed = 1
WHERE processed = 0 AND reg_date < DATE_SUB(NOW(), INTERVAL 1 HOUR);

processed = 0 인 행이 200만 개였고 한 문장으로 200만 행을 바꾼다. 그동안 그 표에 들어오는 저장이 기다린다.

나눠서 처리했다

UPDATE device_data
SET processed = 1
WHERE processed = 0 AND reg_date < DATE_SUB(NOW(), INTERVAL 1 HOUR)
LIMIT 5000;

LIMIT 5000 으로 나눠서 바꾸고 rowcount 가 0이 될 때까지 반복한다.

while True:
    cur.execute(sql)
    n = cur.rowcount
    conn.commit()
    if n == 0:
        break
    time.sleep(0.1)

각 묶음 사이에 짧게 쉬고 그 틈에 저장이 들어간다. 전체 시간은 조금 늘었지만 다른 요청이 안 막힌다.

인덱스와 그 작업의 영향

5,000행씩 해도 조건 조회가 느리면 소용없다.

EXPLAIN UPDATE device_data SET processed=1
WHERE processed=0 AND reg_date < '2016-09-12 12:00' LIMIT 5000;
type: ALL   rows: 4210000

typeALL 이라 전체를 훑는다.

ALTER TABLE device_data ADD INDEX idx_proc_date (processed, reg_date);

processed 를 앞에 뒀는데 값이 0과 1뿐이라 선택도가 낮지만 0인 행이 전체의 5%라 유효했다.

type: range   key: idx_proc_date   rows: 8420

인덱스 추가 자체가 표를 잠글 것으로 보고 시험 서버에서 시간을 쟀다.

시험 서버(같은 행 수): 4분 12초

문서를 보니 보조 인덱스를 더하는 ALTER TABLE 은 제자리에서 돌고 표를 다시 만들지 않으며 그 사이 읽기와 쓰기를 그대로 받는다고 적혀 있다. 그래도 새벽에 했고 그동안 들어오는 자료를 어떻게 할지 정해 뒀다.

try:
    save_to_db(data)
except OperationalError:
    save_to_file(data)      # 나중에 재처리

표를 새로 만드는 종류의 변경이었으면 이 준비가 없을 때 수집이 통째로 멈췄을 자리다.

검증 — 읽기 분리와 감시

통계 배치는 읽기가 대부분이라 읽기만 하는 것은 다른 서버에서 하게 했다.

STAT_DB = 'db-replica'   # 읽기 전용
MAIN_DB = 'db-master'

읽기 부하가 주 서버에서 빠졌다. 복제본은 Seconds_Behind_Master 에 찍히는 만큼 늦으므로 통계는 몇 초 늦어도 되지만 방금 저장한 것을 바로 읽어야 하는 조회는 주 서버에 남겼다.

같은 문제를 미리 알 수 있게 감시도 넣었다.

SELECT COUNT(*) FROM information_schema.PROCESSLIST
WHERE COMMAND != 'Sleep' AND TIME > 30;

30초 넘게 도는 쿼리 수를 1분마다 기록했다. 평소 0이고 배치 시간에 1~2이며 5를 넘으면 알렸다.

SELECT ID, TIME, LEFT(INFO, 100) FROM information_schema.PROCESSLIST
WHERE COMMAND != 'Sleep' ORDER BY TIME DESC LIMIT 3;

가장 오래 도는 쿼리도 같이 기록하니 알림이 왔을 때 무엇이 원인인지 바로 나온다. 숫자만 오면 그때 다시 조사를 시작해야 한다.

정리


Share this post on:

Previous Post
배포 절차를 문서로 만들며
Next Post
하드웨어에 디버거를 붙이며