728x90
반응형
SMALL
Redis를 단일 노드로 사용하는 경우 간단하게 구축할 수 있는 장점이 있지만, 장애가 발생했을 때 서비스 전체가 영향을 받는 위험이 존재합니다. 이를 개선하고자 Redis Sentinel을 도입하여 고가용성(High Availability)을 확보한 경험을 정리해 봅니다. 이 글에서는 Redis Sentinel의 도입 배경, Cluster와의 비교, 실제 적용 방법과 기대 효과까지 다뤄보겠습니다.
왜 Redis Sentinel이 필요했을까?
기존에는 Redis를 단일 노드로 구성하여 세션 캐시, 임시 데이터 저장, 큐 등 다양한 역할을 수행해 왔습니다. 하지만 서비스가 점점 중요해짐에 따라 Redis 장애 시 전체 기능에 영향을 미치는 위험이 커졌고, 고가용성 구조가 필요해졌습니다.
기존 구성의 문제점
- Redis 프로세스 또는 서버 장애 시 수동 복구 필요
- 장애 시 다운타임이 발생하며 기능 일부 중단
- 단일 실패 지점(SPOF) 존재
이러한 문제를 해결하기 위한 첫 번째 개선책이 바로 Redis Sentinel 도입이었습니다.
Redis Sentinel vs Redis Cluster 비교 분석
1. 목적 및 사용 시나리오
항목 | Redis Sentinel | Redis Cluster |
주요 목적 | 고가용성 | 확장성 + 고가용성 |
특징 | 장애 감지 + 자동 복구 | 데이터 샤딩 + 노드 간 통신 |
대표 사례 | 세션, 캐시 | 대용량 분산 저장 |
2. 구성 방식과 특징
항목 | Redis Sentinel | Redis Cluster |
구성 노드 | Master + Replica + Sentinel | 여러 샤드(Master + Replica) |
데이터 분산 | ❌ 불가능 | ✅ 가능 (해시 슬롯 기반) |
클라이언트 호환성 | 대부분 지원 | 전용 클러스터 클라이언트 필요 |
장애 감지 및 복구 | Sentinel에서 수행 | 클러스터 내 노드들이 상호 처리 |
3. 설정 및 운영 난이도
항목 | Redis Sentinel | Redis Cluster |
설정 난이도 | 낮음 | 높음 (슬롯 관리 등) |
클라이언트 로직 | 단순 | 키 기반 분산 라우팅 필요 |
장애 복구 방식 | Sentinel이 Replica 승격 | 샤드별 복제본이 자동 승격 |
Sentinel이 적합한 경우
- 데이터 트래픽과 용량이 상대적으로 크지 않으며,
- 클러스터 수준의 샤딩이 필요하지 않고,
- 고가용성이 가장 중요한 경우
복잡한 클러스터 설정 없이도 장애 대응과 자동 복구를 가능하게 하는 Sentinel은 이러한 상황에 적합한 선택지입니다.
Redis Sentinel 구성 예제 (Docker Compose 기반)
간단한 실습 및 테스트를 위해 Docker Compose를 활용한 예제를 구성했습니다.
디렉토리 구조
redis-sentinel/
├── docker-compose.yml
└── conf/
└── sentinel1
└── sentinel.conf
└── sentinel2
└── sentinel.conf
└── sentinel3
└── sentinel.conf
docker-compose.yml
version: '3'
services:
redis-master:
image: redis:latest
container_name: redis-master
ports:
- "6379:6379"
command: redis-server /etc/conf/redis.conf
volumes:
- ./conf/redis-master.conf:/etc/conf/redis.conf
networks:
- redis-net
redis-slave1:
image: redis:latest
container_name: redis-slave1
ports:
- "6380:6379"
command: redis-server /etc/conf/redis.conf --replicaof redis-master 6379
volumes:
- ./conf/redis-slave1.conf:/etc/conf/redis.conf
depends_on:
- redis-master
networks:
- redis-net
redis-slave2:
image: redis:latest
container_name: redis-slave2
ports:
- "6381:6379"
command: redis-server /etc/conf/redis.conf --replicaof redis-master 6379
volumes:
- ./conf/redis-slave2.conf:/etc/conf/redis.conf
depends_on:
- redis-master
networks:
- redis-net
sentinel1:
image: redis:latest
container_name: sentinel1
ports:
- "26379:26379"
command: redis-sentinel /etc/conf/sentinel.conf
depends_on:
- redis-master
- redis-slave1
- redis-slave2
networks:
- redis-net
volumes:
- ./conf/sentinel1:/etc/conf
sentinel2:
image: redis:7
container_name: sentinel2
ports:
- "26380:26379"
command: redis-sentinel /etc/conf/sentinel.conf
depends_on:
- redis-master
- redis-slave1
- redis-slave2
networks:
- redis-net
volumes:
- ./conf/sentinel2:/etc/conf
sentinel3:
image: redis:7
container_name: sentinel3
ports:
- "26381:26379"
command: redis-sentinel /etc/conf/sentinel.conf
depends_on:
- redis-master
- redis-slave1
- redis-slave2
networks:
- redis-net
volumes:
- ./conf/sentinel3:/etc/conf
networks:
redis-net:
driver: bridge
conf/sentinel1/sentinel.conf
3개의 sentinel.conf은 전부 동일
port 26379
sentinel monitor mymaster redis-master 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 10000
sentinel parallel-syncs mymaster 1
sentinel resolve-hostnames yes
클라이언트 연동 예시
Spring Boot
implementation("org.springframework.boot:spring-boot-starter-data-redis")
spring:
redis:
sentinel:
master: mymaster
nodes:
- localhost:26379
- localhost:26380
- localhost:26381
timeout: 3000
@RestController
@RequestMapping("/api/redis")
class RedisSentinelController(
private val redisRepository: RedisRepository,
private val redisConnectionFactory: RedisConnectionFactory
) {
@GetMapping("/status")
fun getStatus(): Map<String, Any> {
val testKey = "sentinel-test-${LocalDateTime.now()}"
val testValue = "테스트 값"
try {
// Redis에 테스트 데이터 쓰기
redisRepository.setValue(testKey, testValue)
// Redis에서 데이터 읽기
val retrievedValue = redisRepository.getValue(testKey)
// 결과 생성
val result = mutableMapOf<String, Any>()
result["status"] = "정상"
result["timestamp"] = LocalDateTime.now().toString()
result["testKey"] = testKey
result["testValue"] = testValue
result["retrievedValue"] = retrievedValue ?: "값 없음"
result["connection"] = "연결됨"
return result
} catch (e: Exception) {
return mapOf(
"status" to "오류",
"timestamp" to LocalDateTime.now().toString(),
"error" to e.message.toString(),
"connection" to "연결 실패"
)
}
}
}
@Repository
public class RedisRepository {
private final StringRedisTemplate redisTemplate;
public RedisRepository(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void setValue(String key, String value) {
redisTemplate.opsForValue().set(key, value);
}
public String getValue(String key) {
return redisTemplate.opsForValue().get(key);
}
}
FastAPI
pip install fastapi redis-py
from fastapi import FastAPI, HTTPException, status
from redis.sentinel import Sentinel
import uvicorn
app = FastAPI()
# Redis Sentinel 연결 설정
SENTINEL_SERVERS = [
('localhost', 26379),
('localhost', 26380),
('localhost', 26381)
]
SENTINEL_MASTER_NAME = 'mymaster'
# Sentinel 객체 생성 및 Redis 마스터 연결
try:
sentinel = Sentinel(SENTINEL_SERVERS, socket_timeout=0.1)
redis_master = sentinel.master_for(SENTINEL_MASTER_NAME, socket_timeout=0.1)
except Exception as e:
# 연결 실패 시 로그 출력하고 이후 요청에서 에러 처리하도록 함
print("Redis Sentinel 연결 오류:", e)
redis_master = None
@app.get("/")
def root():
return {"message": "FastAPI Redis Sentinel 예제입니다."}
@app.get("/set/{key}/{value}")
def set_key(key: str, value: str):
if not redis_master:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Redis 연결이 되어 있지 않습니다."
)
try:
redis_master.set(key, value)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"키 설정 중 오류 발생: {e}"
)
return {"message": f"'{key}'가 '{value}'로 설정되었습니다."}
@app.get("/get/{key}")
def get_key(key: str):
if not redis_master:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Redis 연결이 되어 있지 않습니다."
)
try:
value = redis_master.get(key)
if value is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="해당 키가 존재하지 않습니다."
)
# Redis는 값을 bytes로 반환하므로 디코드 처리
value = value.decode('utf-8')
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"키 조회 중 오류 발생: {e}"
)
return {"key": key, "value": value}
if __name__ == "__main__":
# 파일명이 main.py라고 가정(파일명에 맞게 수정 필요)
uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True)
기대 효과 및 마무리
Redis Sentinel 적용 후 다음과 같은 효과를 기대할 수 있습니다:
- 장애 발생 시 자동 복구로 서비스 중단 최소화
- 운영 자동화 및 복잡도 감소
- Redis Cluster 수준의 복잡도 없이 고가용성 확보
무엇보다도 현재 서비스 규모와 필요를 충족하면서도 미래 확장을 고려한 구성이라는 점에서 만족스러운 선택이었습니다. Redis Sentinel 도입을 고려하고 계신 분들께 도움이 되길 바랍니다.
728x90
반응형
LIST
'웹 (Web) 개발' 카테고리의 다른 글
Redis Sentinel 장애 대응 및 복구 전략 정리 (0) | 2025.04.14 |
---|---|
Redis 관리 도구 비교 및 RedisInsight 설정 가이드 (1) | 2025.04.09 |
스프링 부트에서 N+1 문제 해결하기 - 성능 최적화를 위한 첫걸음 (0) | 2025.04.01 |
Vite: 현대적인 프론트엔드 빌드 도구의 진화 (0) | 2024.10.21 |
React: 모던 웹 개발의 필수 도구 (1) | 2024.10.18 |