앞장에서 설명한 PersistentDurableStateActor 은 상태있는 서비스에서 분산을 했을때 AKKA를 이용한 분산상태 개발방식입니다.
상태없는 개발방식과 없는 방식의 차이를 먼저알아고보 AKKA의 액터모델이 아닌, 코틀린 순수액터모델을 이용해 유사하게 구현해보고
읽기성능이 얼마나 빨라질수 있는지 확인을 해보겠습니다.
상태없는 서비스 VS 상태 있는서비스 장단점 요약
특성 | 상태없는 서비스 | 상태 있는 서비스 |
---|---|---|
병목 원인 | 데이터베이스, 외부 API, 캐시 사용으로 부하 증가 | 상태 동기화, 세션 관리, 중앙 상태 저장소로 부하 증가 |
확장성 | 수평 확장이 용이하며 병목을 분산 처리 가능 | 상태 동기화 필요로 인해 확장성이 제한됨 |
장애 복구 | 장애 복구가 상대적으로 단순 | 병목 지점의 상태 복구가 복잡 |
최적화 방향 | 요청 간 상태 독립성 활용, 캐싱 및 병목 지점 분산 | 상태 동기화 최적화, 병목 지점 클러스터링 |
적합한 사례 | 대규모 웹 애플리케이션, REST API, 서버리스 설계 | 게임 서버, 채팅, 실시간 세션 기반 시스템 |
일반적인 웹개발에서 상태없는 서비스로 작성하는것이 이득이 있지만 , 게임또는 채팅이 진행되는동안 상태업데이트가 지속 일어나는경우
상태있는 객체를 설계해야할수도 있습니다.
간단한 게임및 비교적 간단한 채팅의 경우 상태없는 서비스로도 충분히 설계가 가능할수 있으며 꼭 어느 한가지방식만 장점이 있을수 있다고 볼수 없으며
다음과 같이 트레이드 오프가 발생할수 있습니다.
상태관리 객체를 만드는 가장 큰 단점은 구현의 난이도에 있습
니다.
성능 트레이드오프
장점
- 로컬 상태 관리로 요청 처리 속도가 빨라짐.
- 네트워크 비용 감소와 높은 동시성 처리.
- 읽기와 쓰기의 독립적 최적화.
- 데이터베이스 성능 의존을 줄일수 있음
단점
- 상태 동기화와 장애 복구(예: 인스턴스 장애 시 상태 손실) 복잡성 증가.
- 상태 저장소와 메모리 사용량 증가.
- 서버 장애 시 상태 복구를 위한 이벤트 로그 또는 스냅샷 관리 필요.
액터모델은 기본적으로 상태를 관리하며 라이프 사이클이 웹(REST API)에서 요청하는 사이클보다 긴 사이클의 상태관리를 할때 활용할수 있습니다.
시도되는 코드
- 조건에 따른 상태저장 : 특정 사용자가 Hello를 하면 사용자별 카운트 1증가 , 기분이 나쁜 상태일때는 Hello를 거부 카운트 증가없음
- 상태읽기 : 특정 사용자 Click 카운트를 확인 하기 위해 1000번 조회후 성능측정
- 추가 미션 : 마지막 상태값 저장뿐만 아니라 상태 로그를 저장해 시계열분석이 가능하도록 데이터 처리
성능표
- Total time for 100 Hello commands for Write: 73 ms
- Average time per Hello command for Write: 0.73 ms
- Total time for 1000 HelloCount for Read commands: 75 ms
- Average time per HelloCount command: 0.075 ms
성능표를 먼저 공유하면~ 저장시 마지막 상태는 Redis에 저장을 하고, 로그성은 Kafka에 저장합니다.
해당 객체가 상태를 가지고 있기때문에 읽기시 Redis로부터 값을 읽을 필요없이 로컬에서의 상태값값을 반환합니다.
Redis가 아무리 고성능 장치라고 하지만 1000회조회에 75ms 이내로 수행하기는 어렵습니다. 다음과 같은 결정적인 이유가 있기때문입니다.
- Redis 를 1000번 호출한다는것은 인메모리에서 1000번 호출할것같지만 네트워크 호출 1000회가 포함되어 있습니다.
- 상태없는 프로그래밍에서 최적화 지점은 Redis가 고성능이라고 믿고 있지만 네트워크 호출 횟수조차 줄여~ 단일지점 저장소의 부하를 어플리케이션에 분산하는것에 있습니다.
이것을 모델 다이어그램으로 정리하면 다음과 같이 동작하게 됩니다.
- Hello Count 를 질의하기위해 영속장치를 접근할 필요없이 , 인메모리(상태프로그래밍) 에서 정확한 값을 바로 응답할수 있습니다.
- Redis에 마지막 값을 항상 유지함으로 ~ 업데이트 또는 장애복구시 Actor가 초기화될시 마지막값으로 상태를 복원해 시작할수 있습니다.
- 마지막 상태값 유지를 위해, 꼭 Redis일 필요없으며, 인메모리 기능을 이미 가지고 있기때문에 RDB에 단지 마지막 값을 유지할수도 있습니다.
- 이 모델은 Read를 위해 매번 RDB 조회할 필요가 없으며, Read의 책임있는 DB를 인메모리가 아닌 다른곳에 위임할수도 있습니다.
- 이벤트의 변화를 Kafka에 기록해둠으로 누군가는 이것을 소비해 시계열 기반 분석 기능을 작성할수도 있습니다. ( 이벤트 소싱패턴 활용한 다양한 기능을 구현 )
위 방식이 전통적인 CRUD 보다 분명 복잡하고 고려해야할 사항들은 더 있을수 있으며 위 방식을 단지 CRUD방식으로 풀어서 비교해보겠습니다.
전통적인 RDB를 이용한 CRUD 방식
CREATE TABLE user_state ( user_id VARCHAR(255) PRIMARY KEY, state ENUM('HAPPY', 'ANGRY') NOT NULL, hello_count BIGINT NOT NULL, hello_total_count BIGINT NOT NULL ); DELIMITER // CREATE PROCEDURE increment_hello_count( IN p_user_id VARCHAR(255), IN p_amount BIGINT ) BEGIN UPDATE user_state SET hello_count = hello_count + p_amount WHERE user_id = p_user_id AND state = 'HAPPY'; END // DELIMITER ; DELIMITER // CREATE PROCEDURE get_user_state( IN p_user_id VARCHAR(255) ) BEGIN SELECT state, hello_count, hello_total_count FROM user_state WHERE user_id = p_user_id; END // DELIMITER ;
전통적인 DB에서 쓰기와 읽기를 분리한다고 했을때 이것이 CQRS라고 생각하면 큰 착각이다. 우선 DB의 Read성능을 높이기위해 확장하는것은 DB1개를 더 두는것이기때문에 아주 값비싼 확장방식이다.
더욱이 사용자의 1카운트를 증가하기위해 Update또는 Create만 발생하는것이 아니라~ 기존 값을 확인(Read)한후 증가하기때문에 Read와 Write(Update)비용이 증가함과 동시에 동시성 처리를 위해
사용자단위로 LockFree하지 않은 방식이 사용되었습니다.
CRUD가 항상 단점이 있는것은 아니며 다음과 CQRS대비 장단점이 존재합니다.
장점: CRUD
- 데이터 영속성: RDB는 내구성 있는 저장소를 제공하여 애플리케이션이 충돌하더라도 데이터가 손실되지 않습니다.
- ACID 트랜잭션: RDB는 원자성, 일관성, 고립성, 내구성을 지원하여 신뢰할 수 있는 트랜잭션을 보장합니다.
- 유연한 쿼리: SQL을 사용하여 복잡한 쿼리와 조인을 수행할 수 있어 데이터 검색 및 조작이 용이합니다.
- 확장성: RDB는 대용량 데이터셋을 처리할 수 있으며 샤딩과 복제를 통해 수평 확장을 지원합니다.
- 백업 및 복구: RDB는 데이터 백업 및 복구를 위한 내장 메커니즘을 가지고 있습니다.
단점: CQRS
- 지연 시간: RDB 작업은 디스크 I/O 및 네트워크 지연을 수반하므로 메모리 내 작업에 비해 느릴 수 있습니다.
- 동시성: 높은 동시성을 처리하는 것은 도전적일 수 있으며, 병목 현상을 피하기 위해 신중한 트랜잭션 관리가 필요합니다.
- 복잡성: 스키마 관리, 인덱스 최적화 및 쿼리 최적화는 애플리케이션에 복잡성을 추가할 수 있습니다.
- 오버헤드: RDB는 ACID 속성을 유지하고 데이터 무결성을 보장하기 위해 오버헤드를 도입합니다.
- 확장성: RDB는 확장 가능하지만, 분산 메모리 내 액터 시스템의 선형 확장성을 따라가지 못할 수 있습니다.