Skip to content
isdnetworks
Go back

검색으로 안 잡히는 잠금

(수정됨)

상품 복제 작업이 도는 동안 카테고리 편집 화면이 멈춰서 잠금 경합으로 보였다. 어느 코드가 categories 를 잠그는지부터 찾아야 했다.

Table of contents

Open Table of contents

배경 — 갱신 코드가 0건인데 멈추는 화면

멈추는 화면이 건드리는 테이블을 확인하고 grep -rn "categories" | grep -i "update\|insert" 로 갱신하는 코드를 전부 검색했지만 문제의 작업 쪽에서는 한 건도 나오지 않았다. 그 작업은 그 테이블을 고치는 코드를 갖고 있지 않았다.

그런데 실제로는 그 작업이 도는 동안에만 화면이 멈추므로 시간 상관관계는 분명했다. 코드에는 없고 현상에는 있는 상태에서 조사가 막혔다.

주의 — 시간 상관관계를 인과로 단정한 보고

앞선 장애 보고서에는 그 작업이 카테고리 테이블을 갱신하면서 배타 잠금을 걸었다고 원인이 적혀 있었다. 그러나 코드 어디에도 그 갱신이 없으므로 그 문장은 관측이 아니라 추론이었다.

그것을 멈추니 풀렸다는 관측에서 그것이 갱신해서 잠갔다는 인과로 한 단계를 건너뛴 서술이었다. 시간 상관관계는 무엇이 관여했는지는 말해 주지만 어떤 방식으로 관여했는지는 말해 주지 않는다.

원인 — 제약조건이 거는 암묵적 잠금

검토를 받으면서 외래키 제약이 거는 잠금이 아니냐는 지적을 받고 SHOW CREATE TABLE 로 관계를 확인했다. product_categories 가 categories 를 참조하는 FOREIGN KEY 가 실제로 걸려 있었다.

MySQL 매뉴얼대로 외래키 제약이 있으면 제약 검사가 필요한 INSERT 가 검사 대상 부모 행에 공유 레코드 잠금을 걸고 그것이 커밋까지 유지된다. 삽입 한 번이 부모 행을 잠그는 것인데 그 코드에는 카테고리를 갱신하는 문장이 한 줄도 없다.

검증 — 잠금을 걸고 나서 하는 긴 작업

작업 구조를 보니 DB::transaction 안에서 product_categories 를 먼저 넣고 그다음에 product_options 수십 개를 복제하고 있었다. 잠금을 먼저 잡은 뒤에 오래 걸리는 일을 하는 순서였다. 고치는 길은 두 가지가 있었는데 옵션 복제를 트랜잭션 밖으로 빼는 것과 순서만 바꿔 카테고리 연결을 마지막에 넣는 것이다. 뒤쪽을 골랐다. 이유는 트랜잭션을 쪼개면 옵션이 반만 복제된 상품이 남을 수 있어서다. 대신 순서 변경이 다른 참조 무결성 검사 순서에 영향을 주는지 한 번 더 검증해야 하는 것을 감수했다.

그동안 다른 쪽에서 그 카테고리를 편집하려 하면 X 잠금을 요청하는데 S 와 X 는 함께 잡히지 않으므로 대기한다. 많이 쓰이는 카테고리일수록 그 충돌이 자주 일어난다.

조치 — 진단 순서와 결함의 개수

이후로 잠금 경합을 볼 때 갱신 코드를 먼저 찾고 안 나오면 information_schema.KEY_COLUMN_USAGE 로 그 테이블을 참조하는 외래키를 확인하고 마지막으로 트랜잭션 범위를 보는 순서로 진행한다. 두 번째 단계를 빼면 이런 종류의 원인은 검색으로 끝까지 안 나온다.

파고 보니 독립된 결함이 셋이었는데 잠금 뒤의 긴 작업이라는 구조 문제와 그 안의 무거운 조회라는 성능 문제와 실제로 안 바뀌는 값이 변경으로 인식되는 표면 문제였다. 앞선 보고서는 세 번째만 원인으로 지목하고 있었고 표면에서 멈추면 구조가 안 보인다.

정리


Share this post on:

Previous Post
게이트가 둘일 때의 절반 통과
Next Post
무엇을 허용해야 하나