본문 바로가기

Dot Database/Concept

[DB] 대기와 Lock에 대해 알아보자

    대기와 Lock 구조를 알아야 하는 이유

    시스템을 운영하다 보면 많은 시스템에서 데이터베이스 안에 대기가 발생해 성능이 제대로 나오지 않거나 처리가 지연되기도 한다. 대기와 구조를 제대로 이해하지 않으면 튜닝도 못 하고, 지연을 해소할 수도 없다. 나아가 'Lock 대기', 'Deadlock(교착 상태)'과 같은 장애를 만날 수도 있다. 이런 경우도 대기나 Lock의 구조를 제대로 이해하고 있지 않으면 대처도 어렵고 애플리케이션 담당자에게 설명하기도 어렵다. (해당 글은 오라클을 기준으로 작성하였다)

     

    데이터베이스에 Lock이 필요한 이유

    데이터베이스를 창고로 비유하고 오라클을 창고 업자에 비유해서 Lock이 왜 필요한지 이야기해보자.

    고객이 창고에 맡긴 물건(ID:1)의 내용에 숫자 1을 더하고 싶다고 요청했다. 이 경우 대부분 다음과 같은 로직을 떠올릴 것이다.

    물건의 내용을 변경하는 순서는?

    1. 우선 물건(숫자)을 알고 싶으니깐 물건을 꺼내자.(SELECT)
    2. 그 후 꺼낸 값에 1을 더하여 다시 맡기자.(UPDATE)
    SELECT counter FROM counter_table WHERE id =1;
    UPDATE counter_table SET counter = <새로운 값> WHERE id=1;

    SELECT한 값을 사용해서 UPDATE하는 방식


    위의 방식으로 충분할 것 같아 보이지만 문제가 있다. 힌트는 오라클의 중요 키워드 '병렬 처리를 가능케 하고 높은 처리량을 실현한다'에서 병렬 처리를 가능케 한다는 점이다. 병렬 처리를 실행하는 시점에 따라서는 값이 늘어나지 않는 현상이 발생할 수 있다. (멀티쓰레드 환경에서 발생하는 에러라고 생각하면 된다.)

    SELECT한 값을 사용해서 UPDATE하는 방식에서 발생하는 문제


    이때 변경 후의 값은 3이어야 한다. 하지만 변경 후의 데이터는 2가 되어버렸고, 그 원인은 데이터를 변경할 때 데이터가 보호받지 못했기 때문이다. 즉, 데이터를 변경하는 작업 도중에는 변경되기 전 데이터를 조회하는 작업이 아닌 작업들은 접근할 수 없도록 해야 한다. 고객 A가 데이터를 변경하고 있는 동안에는 고객 A 이외에는 해당 데이터를 변경할 수 없도록 변경하는 데이터에 Lock을 걸어 보호해야만 한다.

    Lock의 본질'다중 처리를 구현하기 위해 데이터를 보호한다'는 것이다. SQL에서는 다음과 같이 SELECT 단계에서 Lock을 거는 것도 가능하다. '이제부터 내가 이 데이터를 사용할 거니까 아무도 건들지 마!'라는 Lock이라고 생각하면 된다.

    # SELECT ~ FOR UPDATE문 
    SELECT counter FROM counter_table WHERE id =1 FOR UPDATE; # FOR UPADTE문으로 인해 Lock이 걸린다.
    UPDATE counter_table SET counter = <새로운 값> WHERE id=1;


    첫 SELECT에 의해 id가 1인 로우에 Lock이 걸린다. 로우 Lock이 걸렸기 때문에 동일한 로우를 대상으로 UPDATE, DELETE, SELECT FOR UPDATE문을 수행할 수 없어 기다려야 한다. 로우 Lock은 Lock을 건 세션에서 커밋 또는 롤백이 수행될 때 해제된다.

    + 조금 더 설명하면 한 개의 UPDATE문으로도 '값을 1 늘린다'라는 작업은 충분히 처리할 수 있다.

    UPDATE counter_table SET counter = counter+1 WHERE id =1;

    이 SQL문은 아무 문제가 없을까? 실은 UPDATE문 등의 DML(Data Manipulation Language, 데이터 조작어)은 자동으로 로우 Lock을 걸기 때문에 문제가 발생하지 않으며, 병렬로 처리하더라도 정상적으로 처리된다.

    하지만 '이 UPDATE문을 대량으로 동시에 수행하면 id의 값이 1인 로우에 Lock이 발생해서 성능이 나쁘지 않을까?' 라고 생각했다면 정답이다.

    • 예상한 대로 UPDATE문을 대량으로 수행하면 Lock 대기(Lock에 의한 대기)가 발생한다. Lock 대기는 데이터를 보호하기 위해 발생할 수 밖에 없으며, 오라클에서는 이를 대처할 만한 개선 방안이 없으므로 대량의 DML을 실행해야 할 때는 애플리케이션에서 최대한 이런 부분을 감안하고 작업(프로그램)을 개선할 수 밖에 없다.

     

    대기와 Lock 대기

    계속해서 Lock 대기의 구조에 관한 이야기를 하기 전에, 먼저 '대기'와 '대기 이벤트(wait event)'에 관해 이야기해보자.

    일반적으로 대기는 성능을 나쁘게 한다는 좋지 않은 이미지가 있지만, 실제로 대기는 '기다린다(wait)'는 것을 표시하는 것일 뿐이다. 또한, 대기 이벤트를 간단히 말하면 '기다리게 만드는 작업'이라 할 수 있다. 대기로 인해 SQL의 처리가 늦어지는 경우도 있으므로 일반적으로는 이미지가 나쁘지만, 조금만 생각해보면 알 수 있듯이 대기에는 다음과 같은 종류들이 있다.

    • 처리할 것이 없어서 쉬고 있는 대기 (Idle 대기)
    • 이유가 있어 어쩔 수 없이 하는 대기 (Non-Idle 대기)
    • 이상 상태 등 쓸데없이 SQL을 기다리게 하는 대기 (Idle 대기)

     

    Idle 대기

    다음 그림을 보면 알 수 있듯이, Idle 대기 이벤트는 SQL의 처리를 기다리게 하지 않는다. 따라서 일반적으로 성능을 분석할 때는 신경을 쓰지 않아도 된다.

    자주 보이는 대기 이벤트와 그 의미


    자주 보이는 Idle 대기 이벤트는 다음과 같다.

    • SQL*Net message from client(클라이언트에서 오는 SQL문 등을 기다리고 있다.)
    • smon timer, pmon timer, rdbms ipc message, wakeup time manager, Queue Monitor wait 등

     

    Non-Idle 대기는 주의

    문제는 Non-Idle 대기 이벤트이다. 이유가 있어서 어쩔 수 없이 기다리는 정상적인 예로 디스크 I/O대기가 있다. 오라클은 SQL 처리 도중에 데이터가 필요할 때는 디스크에서 블록을 읽어오며, 그때 대기가 발생한다. 이것은 SQL 처리에 필요한 대기라 할 수 있다.

    이상 상태 등 쓸데없이 SQL을 기다리게 하는 대기는 판단하기 어렵다. 이상 상태라는 것은 '한 사용자가 어떤 테이블에 Lock을 걸어 버린 후에 식사하러 갔다.' 등의 경우를 말한다. 이렇게 되면 다른 사용자가 해당 테이블의 데이터를 변경할 수 없으므로 Lock을 통해 데이터는 보호되지만 쓸데없는 대기라 할 수 있다.

    • 평상시에는 I/O 처리에 10밀리초가 걸리지만 디스크 장애 등으로 인해 100밀리초가 걸렸다고 가정해보자. 그러면 대기 시간은 10배가 되며, 그만큼 SQL 처리가 지연된다. 이는 엔드 유저 입장에서 봤을 때 정상일 경우보다 느리므로 쓸데없는 대기이다. 같은 대기 이벤트라도 이런 사례들처럼 '어쩔 수 없는 것'과 '이상한(쓸데없는) 것'이 있다.
    • 또한, 어쩔 수 없는 것이라 하더라도 애플리케이션에서의 처리를 줄임으로써 전체 대기 시간을 단축할 수 있는 경우가 있다.


    SQL의 처리 과정을 튜닝한다는 관점으로 바라보면, 'Non-Idle 대기 이벤트 + SQL 처리에 사용하는 CPU 시간'SQL에 걸린 시간이므로 이 부분은 매우 중요한 개념이다. 튜닝을 할 때 이들 항목에는 항상 신경을 쓸 필요가 있다.

    * 대기 이벤트는 스태츠팩(또는 AWR)이나 v$session_wait에서 볼 수 있다.

    Lock에 의한 대기는?

    다음으로 Lock으로 인해 발생하는 대기에 관하여 이야기하겠다. Lock을 걸었다는 것 자체만으로는 대기가 발생하지 않으며, Lock이 걸려있는 대상에 다시 Lock을 걸려고 했을 때 대기가 발생한다.

    로우 Lock의 메커니즘


    자주 볼 수 있는 Lock은 'TX'와 'TM'이다. TX는 로우와 관련된 Lock이며, TM은 테이블에 거는 Lock이다. 'MODE'는 동시성 제어를 위한 것으로 Lock이 어떤 형태로 걸려있는지 표시해준다.

    Deadlock의 구조

    Deadlock(고장난 열쇠)은 이름에서도 알 수 있듯이, 고장나서 작동하지 않는 열쇠라고 생각하면 된다. 구체적으로는 서로가 상대방이 보유하고 있는 Lock을 기다리느라 영원히 작업 처리를 진행할 수 없는 상태를 말한다.

    Deadlock의 구조


    Deadlock일 때는 한쪽의 처리가 오라클에 의해 자동으로 롤백되며, alert 파일과 트레이스 파일에 정보가 표시된다. 오라클의 버전에 따라서는 기록되는 정보의 양이 다를 수 있지만, 9i 이후 버전이라면 Deadlock이 발생한 SQL문을 양쪽 모두 알 수 있어서 애플리케이션을 수정할 때 도움이 된다.

    Latch의 구조

    Latch도 다중 처리를 구현하기 위한 Lock이다. 일반적인 Lock과 다른 부분은 Latch는 오라클 내부에서 자동으로 얻으며, SQL을 한 번 실행하기 위해서는 여러 Latch를 얻고 해제하는 것을 반복한다는 것이다. Latch는 메모리나 데이터를 조작할 때 상호 배타적으로 처리하지 않아 데이터가 손상되는 것을 방지하기 위해 사용된다.

     

    Latch의 구조


    스태츠팩(또는 AWR)을 보면 Latch는 수십, 수백 개가 존재한다. 이렇게 많은 양의 Latch가 존재하는 이유는 '병렬 처리가 가능케 하고 높은 처리량을 실현'하기 위함이다. Lock 용도에 따라 최대한 잘게 쪼개서 Lock(latch)의 종류와 수를 늘리는 방법으로 다른 세션들과 경합할 가능성을 줄이고 있는 것이다. 이런 여러 장치들 덕분에 오라클에서는 Latch로 인한 경합은 최소화되고 있다.

    하지만 현실에서는 Latch 경합을 많은 시스템에서 볼 수 있는데 이는 CPU와 OS가 관련이 있다. 최근 OS에서는 여러 처리를 동시에 실행하는 멀티태스킹이 당연시 되고 있으며, 선점(preemptive)이라고 불리는 처리 중인 CPU를 가로채는 동작도 존재한다.

    • 그러면 이런 OS의 동작으로 인해 Lock(latch)을 가진 세션(프로세스)에서 CPU를 가로채면 어떻게 될까? 이런 때는 Latch를 가진 세션은 CPU를 사용할 수 없어서 처리를 진행하지 못하고, CPU를 사용할 수 있는 세션은 Latch를 얻지 못해 처리를 진행하지 못하는 상황이 발생한다.
    • 이 모습을 사람에 비유하면 '어떤 사람이 중요한 작업을 처리하는 도중에 직장 상사가 갑자기 새로운 일을 시키는 바람에 중요한 작업 결과를 원하던 사람들이 애꿎게 마냥 기다리는 상황이 되었다'와 비슷할 것이다.


    또한, 이러한 현상은 CPU의 스케줄링 외에도 OS의 페이징 등의 바람직하지 않은 현상으로 인해 오라클 프로세스 처리가 멈춰버리는 상황도 발생한다. 중/대규모 시스템에서 발생하는 Latch 경합은 대부분 CPU 자원의 부족이나 페이징이 그 원인이었다.

    테스트 운영할 때 이런 Latch 경합이 보인다면 우선은 CPU 대기가 발생하지 않도록 개선을 시도하자.

     

    정리

    설명한 내용을 정리하면 다음과 같다.

    • 대기는 단순히 기다리고 있는 상황을 표시하는 것에 지나지 않는다.
    • Idle 대기 이벤트와 Non-Idle 대기 이벤트가 있다.
    • Lock은 데이터를 보호하기 위해 존재한다.
    • Deadlock은 상대방이 소유하고 있는 Lock을 요청해서 작업의 처리를 진행하지 못하는 상태다.
    • Lock 경합을 해소하기 위해서는 애플리케이션 측에서 대처해야 할 때가 많다.
    • Latch는 오라클 내부의 중요한 것(특히 메모리)을 보호하기 위해 존재한다.
    • 대형 시스템이 아닌데 Latch 경합이 심하다면 CPU 자원이 부족하거나 페이징이 발생하는 등의 바람직하지 않은 상태인지를 확인해본다.
    • 오라클을 정상적으로 운영하기 위해서는 토대가 되는 OS도 정상적인 상태여야만 한다.

     


    참고

    스기타 아츠시 외 4인 - 오라클 구조