kafka 실시간 처리를 하며 발생한 문제이다. 문제에 앞서 먼저 실시간 처리의 흐름을 설명하자면,
학생활동 업데이트 이벤트 발생 > 프로듀서에서 학생활동 ID 발송 > 컨슈머에서 ID로 해당 시점의 값들 전부 조회(MySQL) > 변환 및 저장(PostgreSQL)
이런 흐름을 가지고 있으며, 컨슈머는 EKS pod으로 올려서 사용하고 있다. 또한 dependency injector로 서비스에 사용되는 모든 DB connection을 선언하고 컨테이너에 주입하여 서비스를 호출하는 구조를 띄고 있다.
문제점
학생학습활동이 변경됨으로 감지하고 이벤트 발송했는데 컨슈머에서 ID로 조회를 할 때 변경된 값이 아닌 이전 값이 조회되어 제대로 업데이트가 반영되지 않는 문제가 발생했다. 확인해보니 DB connection이 연결된 시점이후로 새롭게 생기는 데이터가 조회되지 않음을 알 수 있었다.
원인 파악
DB Transaction 격리수준
이번 문제는 DB Transaction 격리수준과 관련이 있었다. 격리수준이란 하나의 트랜잭션이 다른 트랙잭션들에 의해 어디까지 영향을 받게 할지 제어하는데 사용되는 개념이다. 격리수준은 다음과 같이 네가지 단계로 나뉘어져 있고 순서대로 점점 높은 격리가 일어난다.
격리 수준 | Dirty Read | Non-Repeatable Read | Phantom Read | 설명 |
READ UNCOMMITTED | 가능 | 가능 | 가능 | 다른 트랜잭션에서 커밋되지 않은 데이터를 읽을 수 있음 |
READ COMMITTED | 불가능 | 가능 | 가능 | 다른 트랜잭션에서 커밋된 데이터만 읽을 수 있음 대부분 데이터베이스의 기본 설정 |
REPEATABLE READ | 불가능 | 불가능 | 가능 | 트랜잭션 내에서 같은 쿼리를 반복해서 수행하더라도 첫번째 쿼리의 결과값과 동일한 데이터 보장 새로 삽입된 데이터를 볼 수 있음 |
SERIALIZABLE | 불가능 | 불가능 | 불가능 | 완벽한 격리 제공 트랜잭션들이 마치 순차적으로 실행된 것처럼 보장, 다른 트랜잭션의 영향을 전혀 받지 않음 |
- Dirty Read : 한 트랜잭션에서 아직 커밋되지 않은 데이터를 다른 트랜잭션이 읽는 현상.
- Non-Repeatable Read : 한 트랜잭션 내에서 같은 쿼리를 두 번 실행했을때, 두 결과가 다르게 나오는 현상. 다른 트랜잭션이 두 번 쿼리를 실행하는 그 사이에 데이터를 변경하여 변경 전, 후 가 둘 다 읽힌다.
- Phantom Read : 한 트랜잭션 내에서 같은 쿼리를 두 번 실행했을때, 첫번째 쿼리에는 없던 새로운 row가 두번째 쿼리 결과에 포함되는 현상. 다른 트랜잭션이 그 사이에 새로운 데이터를 삽입하여 추가된 행이 읽힌다.
격리 수준이 높아질수록 동시성이 감소하고 성능이 저하되지만, 데이터 일관성은 증가한다. 반대로 격리 수준이 낮아질 수록 동시성이 증가하고 성능이 향상되지만, 데이터의 일관성이 감소할 수 있다.
pymysql의 트랜잭션 운영
운영 DB에서 값을 조회할 때 pymysql을 사용 하고 있는데 디폴트로 autocommit = False 이다. pymysql은 cursor 클래스를 선언해서 execute를 실행할 때 트랜잭션이 시작되고, 새로운 cursor 클래스를 선언해도 처음 생성된 트랜잭션 내에서 계속 작동을 하게 된다. 아래 경고문처럼 단순 select 문을 실행해도 트랜잭션이 실행되니, 실행주기가 긴 프로그램은 트랜잭션 종료를 명시적으로 해주거나 autocommit을 사용하라고 뜬다.
해결
현재 운영DB는 develop, stage, production 세개의 환경으로 운영하고 있다.
- develop : REPEATABLE-READ
- stage, production: READ-COMMITTED
왜 인지 모르게 develop 환경만 REPEATABLE-READ로 격리수준이 한단계 높았다. develop 환경에서 테스트를 하는데 connection을 연결한 후로 하나의 트랜잭션으로 운영을 하다보니 새롭게 들어오는 데이터를 확인 할 수 없었던 것이다. 이후 조치는 다음과 같다.
- 백엔드 팀에 격리수준 다른 점 알리기 및 히스토리 파악.
- stage, production 환경에서는 동일 테스트시 같은 문제가 발생하지 않음 확인.
- autocommit = True로 설정하여 각 쿼리마다 별도 트랜잭션으로 처리하고 쿼리 종료 후 자동으로 커밋하도록 설정. 이후 다음 쿼리 실행시 새로운 트랜잭션이 열리게 되고 다른 트랜잭션에서 업데이트한 값을 읽을 수 있게 됨.
'이슈 해결 일지' 카테고리의 다른 글
Airbyte 배포부터 ClickHouse 연결과 실행 - 문제 해결기 (0) | 2025.01.15 |
---|---|
Kafka 파이프라인에 따른 DB 부하줄이기 여정 (0) | 2024.03.02 |
Redash 인프라 개선 (0) | 2024.01.14 |
Kafka offset이 뒤로 돌아가는 문제 (0) | 2023.10.17 |
Route 53과 Externel DNS (feat. 회고) (1) | 2023.07.31 |