오늘 배운 것
- 발생 가능 장애
- DB Lock 관련 장애
- DB 복제 지연
- 메모리 릭
- 캐시 압력
발생 가능 장애
실제 서비스를 진행하면 많은 장애 상황을 마주할 수 있습니다.
다양한 장애 상황이 발생했을 때 어떻게 해결해야 하는지 아는 것이 중요합니다.
이보다 더 중요한 것은 이런 장애 상황이 발생하지 않도록 개발을 진행할 때 어떤 것에 신경을 써야 하는지 아는 것입니다.
살펴볼 장애 상황들은 아래와 같습니다.
- DB Lock 관련 장애
- DB 복제 지연
- 메모리 릭
- 캐시 압력
DB Lock 관련 장애
데이터베이스는 여러 사용자나 시스템이 동시에 데이터를 읽고 쓰는 환경에서 운영된다.
따라서 트랜잭션이라는 DB의 상태를 변경시키는 작업의 단위가 있다.
이런 환경에서 문제가 발생할 수 있는 대표적인 사례가 있다.
- Dirty Read (더티 리드)
- 한 트랜잭션이 데이터 수정 중일 때 다른 트랜잭션이 데이터를 읽는 상황.
- 첫번째 트랜잭션이 롤백된다면 두번째 트랜잭션은 잘못된 데이터를 읽은 것이 됨.
- Non-repeatable Read (반복 불가능한 읽기)
- 한 트랜잭션이 데이터를 읽은 후, 다른 트잭션이 데이터 수정하고 커밋한 상황.
- 첫번째 트랜잭션이 동일한 데이터를 다시 읽을 때 값이 달라지게 됨.
- Lost Update (업데이트 손실)
- 두개의 트랜잭션이 동시에 같은 데이터를 수정하려는 상황.
- 한 트랜잭션의 수정 내용이 다른 트랜잭션에 의해 덮어쓰여져 사라지게 됨.
이러한 문제점들이 발생할 수 있다.
이러한 문제점들을 사전에 막으려면 DB Lock이라는 것이 필요하다.
DB Lock이란?
DB 락(Database Lock)은 데이터베이스에서 여러 트랜잭션이 동시에 같은 데이터에 접근할 때, 데이터의 무결성(일관성)을 보장하기 위해 사용되는 메커니즘이다.
한 트랜잭션이 특정 데이터에 대해 작업을 하고 있을 때 다른 트랜잭션이 그 데이터에 접근하지 못하도록 잠그는 것이다.
이로써 데이터의 일관성을 유지하고, 동시에 발생할 수 있는 충돌을 방지할 수 있다.
DB Lock을 통해 데이터에 대한 접근을 제어하면 위의 문제점들에서 발생할 수 있는 데이터 무결성 문제를 예발할 수 있다.
DB Lock의 종류
- 공유 Lock (Shared Lock)
- 데이터베이스에서 데이터를 읽을 때 사용한다.
- 여러 트랜잭션이 동시에 같은 데이터를 읽을 수 있지만, 공유 락이 걸린 동안에는 데이터를 수정할 수 없다.
- 배타 Lock (Exclusive Lock)
- 데이터를 수정할 때 사용한다.
- 배타 락이 걸린 데이터는 다른 트랜잭션이 읽거나 수정할 수 없다.
- 비관적 Lock (Pessimistic Lock)
- 데이터를 읽을 때부터 락을 걸어 다른 트랜잭션이 접근하지 못하도록 하는 방식이다.
- 데이터 충돌 가능성이 높을 때 유용하다.
- 낙관적 Lock (Optimistic Lock)
- 데이터를 수정하기 전까지 락을 걸지 않고, 수정 시점에만 충돌을 확인하는 방식이다.
- 주로 데이터의 버전 번호를 사용하여 동시성 문제를 해결한다.
- 명명된 Lock (Named Lock)
- 특정 이름으로 락을 설정하여, 동시에 하나의 프로세스만 특정 리소스에 접근하도록 하는 방식이다.
- 주로 특정 리소스나 작업에 대한 접근을 직관적으로 제어하기 위해 사용한다.
- 분산 Lock (Distributed Lock)
- 여러 시스템이나 인스턴스에서 동시에 동일한 자원에 접근할 때, 자원의 일관성 유지하기 위해 사용되는 락이다.
- Redis와 같은 분산 시스템을 사용하여 구현된다.
DB 복제 지연
복제 지연은 데이터가 쓰기DB에서 읽기DB로 복제되는 과정에서 발생하는 시간지연 의미한다.
쓰기 작업이 발생한 후 그 변경 사항이 읽기 DB에 반영되기까지의 지연 시간을 말한다.
복제 지연으로 인해 읽기 DB에서 최신 상태의 데이터를 읽지 못하고 이전 상태의 데이터를 읽게 되는 문제 발생할 수 있다.
이런 문제점을 해결하기 위해서는 아래의 방법들을 사용할 수 있다.
- 지역 적용 알고리즘 (Lag Compensating Logic)
- 쓰기 후 즉시 읽기를 시도하는 경우, 잠시 지연을 두고 읽기를 재시도하는 방법
- 지연된 시간만큼의 시간이 지나면, 대부분의 경우 읽기 DB가 최신 데이터 반영하는 방법
- 쓰기 DB로의 직접 읽기 (Read-after-Write)
- 중요한 데이터에 대해 쓰기 직후 즉시 읽어야 하는 경우, 해당 읽기 작업을 쓰기 DB에서 직접 수행하게 하는 방법
- 읽기 DB와 쓰기 DB 간의 복제 지연 최소화
- DB 복제 지연을 최소화하도록 DB 설정 조절하는 방법
- DB마다 복제 설정을 조절하고 최적화 할 수 있음
- 그러나 물리적인 네트워크 지연이나 시스템 부하 등으로 인한 지연은 완전히 제거 불가능
- CQRS 패턴
- Command Query Responsibility Segregation (CQRS) 패턴은 데이터 저장소로부터 읽기와 쓰기 작업을 분리하는 패턴
- CQRS 패턴을 적용하여, 쓰기 작업과 읽기 작업을 명확히 분리하는 방법
- 읽기 작업에 대해 일관성을 보장하는 별도의 메커니즘을 적용 가능
- 캐시 사용
- 많이 사용하는 방법이고 쓰기 지연 일어나지 않게 하는 가장 쉬운 방법
- 캐시 시스템을 도입하여, 쓰기 작업 후 바로 캐시를 갱신하고, 그 이후의 읽기 작업은 캐시에서 제공하는 방식을 사용
- 캐시는 일관성을 보장하기 위해 일정 시간 동안(예: 1초) 쓰기 DB의 데이터를 캐시하는 방법을 사용 가능
메모리 릭
메모리 릭이란 프로세스가 더 이상 필요하지 않은 메모리를 할당한 후 이를 해제하지 않은 것이다.
따라서 해당 메모리가 지속적으로 점유된 상태로 남아있는 현상이다.
이러한 메모리 릭은 메모리 사용량을 지속적으로 증가시킨다.
결국에는 시스템의 메모리를 고갈시켜 성능 저하 또는 애플리케이션의 충돌을 유발할 수 있다.
메모리 릭의 일반적인 원인은 아래와 같다.
- 이벤트 리스너 및 콜백 등록 후 해제 X
- 이런 것들을 해제하지 않으면 객체가 계속 메모리에 남아 메모리 릭 발생 가능
- 전역 변수 사용
- 전역 변수로 객체 참조하면 해당 객체가 프로그램 종료 시점까지 해제되지 않을 수 있음
- 캐시 관리 실패
- 캐시된 객체가 필요 없을 때에도 계속해서 메모리에 유지되면 메모리 릭 발생 가능
- 잘못된 데이터 구조 관리
- 객체 저장 후 필요 없을 때 객체 제거하지 않으면 메모리 릭 발생 가능
스프링에서의 메모리 릭이 일어날 수 있는 상황은 아래와 같다.
- 스프링 빈의 라이프 사이클 관리 문제
- 빈은 애플리케이션 컨텍스트 살아있는 동안 메모리에 유지됨
- 빈이 내부적으로 많은 메모리 점유하는 객체 가지고 있으면 메모리 릭 발생 가능
- @Autowired로 주입된 객체의 잘못된 관리
- @Autowired로 주입된 객체가 예상치 못한 참조 순환 일으키면 메모리 릭 발생 가능
- @Autowired로 주입된 객체가 의도적으로 제거되지 않는다면 메모리 릭 발생 가능
- ThreadLocal의 잘못된 사용
- ThreadLocal 사용해 스레드별로 데이터 저장하는 경우 스레드 종료 시 해당 스레드와 관련된 모든 데이터 제거해야 함
- 이런 데이터 명시적으로 제거하지 않으면 메모리 릭 발생 가능
- 이벤트 리스너의 잘못된 관리
- 스프링에서 이벤트 리스너 등록 후 리스너 필요하지 않은 경우 제거해야 함
- 제거하지 않으면 리스너가 계속해 메모리를 점유해 메모리 릭 발생 가능
이런 메모리 릭을 해결하려면 아래의 것들에 대해 생각해야 한다.
- 위의 상황들을 알고 있어야 한다.
- 위의 상황들에 해당하는 것들을 의도적으로 메모리에서 제거해줘야 한다.
- 캐시에 TTL 시간 조절에 캐시에서 메모리 릭 발생하지 않도록 해야 한다.
- 개발자가 객체의 라이프 사이클을 명확하게 이해하고 있어야 한다.
캐시 압력
캐시 압력은 캐시 메모리가 부족해지면서 발생하는 문제다.
캐시가 가득 차서 더 이상 새로운 데이터를 저장할 수 없고, 이로 인해 기존 데이터를 삭제해야 하는 상황 발생할 수 있다.
캐시 압력이 증가하면, 캐시 항목을 삭제하거나 새 데이터를 캐시에 저장하는 동안 대기 상태가 발생할 수 있다.
캐시 압력이 증가하면 새로운 데이터를 저장할 수 없어 요청이 지연되고 대기 상태로 빠지는 문제 발생할 수 있다.
캐시 압력이 증가하면 필요한 데이터가 캐시에 존재하지 않아, 다시 데이터베이스에서 가져오는 상황이 빈번해질 수 있다.
캐시 전략을 제대로 수립하지 않으면 캐시 압력이 발생할 수 있다.
필요 없거나 자주 사용하지 않는 데이터를 캐싱해 낮은 캐시 히트율을 가진다면 캐시 압력이 발생할 수 있다.
낮은 캐시 히트율은 캐시의 효율성을 떨어뜨리고 메모리 낭비로 이어져 캐시 압력을 증가 시킨다.
이런 캐시 압력의 해결 방안으로는 아래와 같은 것들이 있다.
- 캐시 제한 설정
- TTL
- 캐시에 TTL 설정해 일정 시간 후 자동으로 만료되도록 설정
- 최대 캐시 크기 설정
- 캐시의 최대 크기를 설정해 용량 초과되면 오래된 데이터 자동으로 제거하도록 설정
- TTL
- 페이징 전략 최적화
- 부분적 캐싱
- 모든 페이지 캐싱하기보다는 자주 요청되는 특정 페이지만 캐싱
- 데이터베이스 인덱스 최적화
- 캐시 사용 대신 데이터베이스 인덱스 최적화해 페이징 성능 향상
- 부분적 캐싱
- 분산 캐시 사용
- 분산 캐시 시스템 사용해 여러 서버에 캐시 데이터 분산
- 한 캐시의 메모리 용량 한계 극복, 캐시 성능 향상 가능
- 캐시 우회 전략
- 특정 조건에서 캐시를 우회하고 직접 데이터베이스에서 데이터 가져오도록 전략 수립
'TIL' 카테고리의 다른 글
24.08.28 - JWT in Spring (6) | 2024.08.30 |
---|---|
24.08.27 TIL - JWT (0) | 2024.08.28 |
24.08.23 TIL - 모니터링 (0) | 2024.08.26 |
24.08.22 TIL - 캐싱 (0) | 2024.08.23 |
24.08.21 TIL - session clustering (0) | 2024.08.23 |