대기열 시스템에서 동시성 문제 (4) - Lock 사용하기

개발세발·2024년 12월 17일

5. Lock

Lock이란?

  • 동시 접근을 제어하기 위해 사용되는 동기화 메커니즘. 한 번에 하나의 주체만 접근할 수 있도록 제한한다.
  • 락 획득 → 자원 접근 및 작업 수행 → 락 해제

Lock 종류

  • 뮤텍스(Mutex, Mutual Exclusion)
    • 한 번에 한 스레드만 락을 획득할 수 있습니다.
    • 락을 획득한 스레드가 작업을 마치고 락을 해제해야 다른 스레드가 접근할 수 있습니다.
    • 임계 구역에 접근 시 락을 획득하고, 임계 구역에서 나가면 락을 반환.
  • 스핀락(Spin Lock)
    • 락이 해제될 때까지 계속 반복해서 확인합니다. (대기하면서 CPU를 사용)
    • 짧은 시간 내에 락이 해제될 때 유리하지만, 장시간 대기 시 CPU 리소스를 낭비할 수 있습니다.
  • 분산 락(Distributed Lock)
    • 여러 노드나 시스템에서 자원을 제어하기 위해 사용됩니다.
    • 네트워크를 통해 락을 관리

낙관적 락

  • 데이터를 읽을 때는 락을 걸지 않고, 업데이트할 때만 이전 데이터와 현재 데이터를 비교하여 충돌 여부를 판단한다.
  • 충돌이 발생하지 않을 것이라고 가정하고, 충돌이 발생한 경우를 대비하는 방식!
  • JPA에서 @Version을 이용하여 구현할 수 있다.
  • 단점: 충돌이 빈번하게 발생하는 경우 성능이 저하될 수 있으며, 재시도 비용이 커질 수 있다.

비관적 락

  • 데이터를 읽을 때부터 해당 데이터에 대한 락을 걸어 다른 트랜잭션이 해당 데이터를 변경할 수 없게 한다. 일관성 유지에 더욱 초점을 맞춘다!!
  • 충돌이 발생할 확률이 높다고 가정하고, 데이터에 접근하기 전에 먼저 락을 걸어 충돌을 예방하는 방법이다.
  • JPA에서 LockModeType을 사용하여 구현할 수 있다:
    • PESSIMISTIC_READ: 읽기만 허용, 쓰기 불가.
    • PESSIMISTIC_WRITE: 읽기와 쓰기 모두 불가.
    • PESSIMISTIC_FORCE_INCREMENT: 락을 걸면서 버전을 증가시킴.
  • 단점: 성능 저하 및 교착 상태에 빠질 수 있다. 이를 방지하기 위해 타임아웃 설정 등이 필요.

분산 락과 Redis를 사용한 분산 락

  • 분산 락: 여러 노드에서 동일한 자원에 대한 접근을 제어하는 방법이다. 자원에 접근할 때가 아닌 로직의 시작점에서 락을 획득하는 것이 일반적인 분산 락의 사용 방식이다.
    • 분산 락을 사용하면 동시에 같은 자원에 접근하는 것을 방지할 수 있고, 데이터 일관성 문제나 충돌을 방지할 수 있다.
    • 멀티 인스턴스 환경에서도 공통된 락을 사용할 수 있다.
    • “분산”의 의미: 여러 시스템(노드)에서 이루어지기 때문이다. 락이 분산되어 있다는 의미!
  • Redis를 사용한 분산 락
    • 다양한 방식으로 구현 가능:
      • SETNX (SET if Not Exists): 락을 획득할 때 사용.
      • Expire (TTL): 락 자동 해제 시간 설정.
      • LuaScript: 락 소유 여부를 확인하며 락을 원자적으로 해제.
    • Redisson 라이브러리를 사용하면 락 인터페이스를 통해 간편하게 구현 가능하며, Watchdog 기능으로 TTL 만료 문제를 해결할 수 있다.
    • Pub/Sub 방식을 이용해 락 해제 이벤트를 다른 클라이언트에게 알림.
    • Lettuce는 분산락 구현 시 SETNX(SET if Not Exists)를 사용하여 락이 없을 경우에만 키를 생성하여 락 획득한다. 그리고 EXPIRE를 사용하여 TTL(Time-to-Live)을 설정해 락이 일정 시간이 지나면 자동으로 해제되도록 설정. 해제 시에는 락 소유 여부를 확인한 후에만 락을 해제해야 함.
      그리고 이 과정에서 Lettuce는 스핀락(무작정 반복적으로 Lock이 반환될 때까지 확인하며 대기) 방식으로 동작합니다.
  • Lettuce vs Redisson
    • Lettuce: 비동기(non-blocking) 방식으로 Redis와 통신하는 클라이언트입니다. Netty 기반으로 구현되어 있으며, Reactive 프로그래밍을 지원합니다. Spring Data Redis에서 기본 클라이언트로 사용합니다.
      • Netty 기반:
        • 네트워크 I/O 처리를 위한 Netty 라이브러리를 사용하여 비동기 이벤트 기반 모델로 동작합니다.
      • 비동기 및 반응형(reactive) API:
        • 비동기 API(CompletableFuture), 반응형 스트림(Reactive Streams), 동기 API를 모두 지원합니다.
        • 비동기 방식 덕분에 I/O 작업을 기다리는 동안 스레드가 블록되지 않아 높은 성능을 제공합니다.
      • 싱글 커넥션 기반:
        • 하나의 연결에서 여러 작업을 비동기적으로 처리합니다.
        • 커넥션 풀(Connection Pool)을 사용하지 않고, 대신 비동기 처리로 성능을 보완합니다.
      • 트랜잭션과 Pub/Sub 지원:
        • Redis의 트랜잭션, Pub/Sub 기능도 지원합니다.
    • Redisson: 동기식(blocking)과 비동기식(non-blocking) 작업을 모두 지원하며, 다양한 분산 객체동시성 도구를 제공합니다.
      • 커넥션 풀:
        • Redisson은 내부적으로 커넥션 풀을 사용하여 여러 연결을 효율적으로 관리합니다.
      • 분산 데이터 구조:
        • Redis를 이용해 Java의 표준 데이터 구조(List, Map, Set 등)를 분산 환경에서 사용할 수 있습니다.
        • 예를 들어, RMap, RList, RLock 같은 Redisson의 객체를 통해 분산 환경에서도 동기화된 데이터 구조를 활용할 수 있습니다.

Redisson고수준 분산 기능을 제공하며 멀티 노드 환경에서 동기화 메커니즘이 필요할 때 유용합니다.

Lettuce는 가볍고 성능이 뛰어나며, 간단한 캐시 작업이나 저수준 데이터 접근에 적합합니다.

  • Redisson과 Lettuce의 분산 락

    • Redisson은 고수준의 기능을 제공하며, Redlock 알고리즘을 구현하고 있습니다. Redlock은 여러 Redis 노드에 락을 걸어 분산 환경에서도 안전하게 동기화를 보장하려는 알고리즘입니다. 이 알고리즘에서는 과반수 이상의 노드에서 락을 획득해야 유효한 락으로 간주됩니다. Redlock의 목표는 단일 노드 장애에 대비하여 락의 신뢰성을 높이는 것입니다.
    • Lettuce는 저수준 Redis 클라이언트로, 자체적으로 고급 분산 락 기능을 제공하지는 않습니다. 락이 필요한 경우, 직접 명령어 수준에서 Redis 명령을 사용하여 구현해야 합니다. Lettuce는 동기식, 비동기식, 반응형 프로그래밍 모델을 지원하며, 세밀하게 Redis와의 상호작용을 제어할 수 있습니다. 기본적으로 락의 분산 기능을 제공하지 않고, 락을 구현하려면직접 Lua 스크립트나 Redis 명령어를 사용해야 합니다.
  • 분산 락 in Java를 위한 Redis 기반 도구

    • Lock 인터페이스
      • TheRLock interface는 Java의 java.util.concurrent.locks.Lock 인터페이스를 구현합니다. Rlock을 획득한 Redisson 인스턴스가 충돌하면 잠금이 획득된 상태로 멈출 수 있습니다. 따라서 “watchdog”을 유지하여 잠금 시간을 연장할(기본 30초) 수 있습니다.
      • RLock은 Lock 인터페이스를 구현하기 때문에 잠금을 소유한 스레드만 리소스의 잠금을 해제할 수 있습니다. 그렇지 않은 시도는 IllegalMonitorStateException을 만나게 됩니다.
    • FairLock
      • RLock과 유사하게, RFairLock 도 구현한다. 대기 순서에 따라 잠금을 획득하는 공정한 방식이다. 공정성을 제공하지만 성능 오버헤드가 발생할 수 있다. 즉, 스레드가 리소스를 요청한 순서대로 리소스를 획득하도록 보장할 수 있습니다(즉, “선입선출” 대기열).
    • ReadWriteLock
      • RReadWriteLock도 구현한다. Java에서 읽기/쓰기 잠금은 실제로 여러 스레드가 동시에 소유할 수 있는 읽기 전용 잠금과 한 번에 단일 스레드만 소유할 수 있는 쓰기 잠금의 두 가지 잠금의 조합입니다. 읽는 건 다 가능, 쓰기는 하나의 스레드만!
    • RedLock
      • RLock과 동일하게 사용한다.(Redisson 내의 getRedLock() 메서드는 @Deprecated 되었고, getLock()를 통해 사용해야 한다. 독립적이고 동등한 지위를 가진 N개의 Redis 노드에서 동작하고, 알고리즘은 동일한 키 이름과 임의의 값을 사용하여 이러한 각 인스턴스에서 순차적으로 잠금을 획득하려고 시도한다.
    • MultiLock
      • 여러 잠금을 하나로 묶어서 관리한다. 여러 리소스에 대해 동시 접근이 가능하지만, 하나라도 획득 실패 시 전체 작업이 실패한다.
      • 여러 개의 개별 RLock 인스턴스를 그룹화하여 단일 엔티티로 관리할 수 있으며, 각RLock 객체는 서로 다른 Redisson 인스턴스에 속할 수 있다. 이는 차례로 다른 Redis 데이터베이스에 연결될 수 있다.
  • Redis 분산 락의 단점

    • 락 소실 가능성: TTL 설정이 적절하지 않으면 예상치 못하게 락이 해제될 수 있다.
    • 병목 지점: Redis가 단일 장애 지점(SPOF, Single Point of Failure)이 될 수 있음.
    • 보완 방법: Redlock 알고리즘 사용.

Redlock 알고리즘

  • Redis가 권장하는 Lock을 제공하는 방법
  • N대의 Redis 서버가 있다고 가정할 때, 과반 수 이상의 노드에서 Lock을 획득했다면 Lock을 획득한 것으로 간주한다.
  • RedLock 알고리즘 절차
    • Client는 Lock을 획득하기 위해 모든 Redis 서버에게 Lock을 요청. 과반 수 이상의 Redis 서버에게 Lock을 획득하면 Lock을 획득
    • 실패했다면 모든 Redis 서버에게 Lock 해제를 요청하고 일정 시간 후에 Lock을 획득하기 위한 재 시도를 한다.
  • 완벽한 알고리즘은 아니다.
    • 네트워크 분할 문제 (Network Partition): 네트워크 분할(Partition)이 발생하면 일부 노드와의 통신이 단절될 수 있다.
      • 예를 들어 전체 Redis 노드가 5개일 때, 3개 노드의 네트워크가 분리되면 두 개의 클러스터(2개 노드와 3개 노드)가 각각 존재하게 됩니다.
      • 두 클러스터가 독립적으로 락을 획득하여 동시에 동일한 리소스에 접근할 가능성이 생깁니다.
      • 이는 Redlock이 네트워크 분할 환경에서의 일관성 보장을 완벽히 해결하지 못함을 의미합니다.
    • 락의 TTL과 클라이언트 동기화 문제: 락의 TTL(Time-to-Live)이 너무 짧거나, 클라이언트의 작업이 TTL을 초과할 경우, 락이 만료되어 다른 클라이언트가 락을 획득할 수 있습니다.
      • 예를 들어, 클라이언트 A가 락을 획득하고 작업 중인데 TTL이 만료되면, 클라이언트 B가 락을 획득할 수 있습니다. 하지만 클라이언트 A는 여전히 락이 유효하다고 간주하고 작업을 진행 중일 수 있습니다.
      • 이는 리소스 충돌을 초래할 수 있습니다.
      • 락 연장(Renewal)을 도입하여 일부 문제를 완화할 수 있지만, 이 역시 분산 환경에서 완벽히 동기화되기 어렵습니다.
    • 락 획득 실패 시 비용 문제: Redlock은 과반수 이상에서 락을 획득하지 못하면 모든 노드에서 락 해제 요청을 보내야 하며, 일정 시간 후 재시도합니다.
      • 락 해제와 재시도 과정에서 불필요한 네트워크 비용 및 지연이 발생할 수 있습니다.
      • 특히, 노드가 많아지면 이 문제는 더욱 심화됩니다.

💡가능하면, 락을 사용하지 않는 것이 성능면에서는 좋다.
락을 사용한다는 것은, 병목 지점을 만드는 것이다. 병목 지점이 생기면 교착상태에 빠질 수 있고, 스레드가 작업을 변환해야 하므로 컨텍스트 스위칭 비용이 증가할 수 있다. 이는 당연하게도 성능 저하로 이어진다.
따라서, 락을 사용하지 않고 동시성 문제를 해결할 수 있다면, 락 없이 문제를 해결하는 것이 좋다.

0개의 댓글