기능 개발을 하며 개발 DB에서 작업 중에 의도치 않은 테이블 락이 걸려 장애가 있는 문제가 있었다. 해당 테이블에는 통계를 위한 롱 쿼리가 실행 중으로 읽기 락이 걸려있는 상황이었고, 해당 테이블의 PK를 FK로 가지고 있는 테이블에 컬럼을 추가하는 DDL이 테이블 락을 위해 대기를 하고 있었다(PostgreSQL에서 FK는 트리거로 구현되어 있기 때문에 영향을 준다). DDL은 대기 중임에도 불구하고 테이블 락을 잡고 있었고, 따라서 그 이후로 발생한 모든 쿼리가 대기 상태로 DB 커넥션을 잡고 있어 결국 커넥션 소진으로 장애가 발생했다. 직관적으로는 읽기 락은 테이블락과 충돌나지 않기 때문에 문제가 없을 것이라고 생각했지만, 이는 전혀 틀린 직관이었다 ;(
잠시 찾아보니 모든 PostgreSQL의 락은 큐를 가지고 있다고 한다. 트랜잭션 A가 락을 잡고 있고, 트랜잭션 B가 A와 충돌하는 락을 얻으려고하면, B는 큐에서 대기한다. 이때 새로 들어온 트랜잭션 C가 실행되기 위해서는, 트잭 A와 충돌이 나지 않아야할 뿐만 아니라, 큐에 있는 트잭 B와도 충돌나지 않아야 한다.
참고 : https://www.citusdata.com/blog/2018/02/22/seven-tips-for-dealing-with-postgres-locks/
다시 위의 상황에 빗대어 보자면, 롱 쿼리로 인해 DDL이 큐에서 기다렸고, 이후 들어온 쿼리들이 DDL의 락 수준과 충돌하기 때문에 문제가 발생했던 것이다. 사실 생각해 보면 당연하다. 락 큐에 FIFO로 대기하지 않는다면, 락 수준이 높은 쿼리는 이후 조회 쿼리들만 있더라도 영원히 실행될 수 없다. 프로덕션에 롱 쿼리가 있는 것 자체가 문제일 수도 있지만, 만약 어쩔 수 없다면 최소한 트랜잭션에 락 타임아웃을 설정하도록 하자
SET lock_timeout TO ‘2s’
ALTER TABEL table_name ADD COLUMN col_name int4;
SET lock_timeout은 실행문이 락을 얻을때까지 대기하는 시간을 설정하는 문법이다
참고 : https://postgresqlco.nf/doc/en/param/lock_timeout/
PostgreSQL과 함께 대표적으로 많이 사용되는 DB 밴더인 MySQL에서는 어떻게 처리하는지 궁금해서 찾아봤다. 최소한 InnoDB 기준 MySQL도 동일하게 FIFO로 락 큐가 동작한다.