이코에코(Eco²) ext-authz 성능 튜닝: Redis PoolSize, HPA


이코에코(Eco²) ext-authz: Stress Test 에서 밝혔듯 ext-authz의 주된 병목은 Redis PoolSize와 cpu-limits다.
당시 Redis PoolSize는 10으로 users:2000, ramp-ups:200 지점부터 점진적으로 부하가 나타나다가,
users:2500, ramp-ups:250부터 본격적으로 avg latency 증가, RPS 42로 급감하며 포화상태를 보였다.
Eco² 클러스터 Redis 구성 현황

ubuntu@k8s-master:~$ kubectl get pods -n redis -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
dev-redis-node-0 3/3 Running 0 4d22h 192.168.37.134 k8s-redis <none> <none>
dev-redis-node-1 3/3 Running 0 4d22h 192.168.37.135 k8s-redis <none> <none>
ubuntu@k8s-master:~$ kubectl describe nodes k8s-redis
...
Addresses:
InternalIP: 10.0.2.235
Hostname: k8s-redis
Capacity:
cpu: 2
ephemeral-storage: 30297152Ki
hugepages-1Gi: 0
hugepages-2Mi: 0
memory: 3928856Ki
pods: 110
...
Allocated resources:
(Total limits may be over 100 percent, i.e., overcommitted.)
Resource Requests Limits
-------- -------- ------
cpu 1080m (54%) 5300m (265%)
memory 1656Mi (44%) 5248Mi (140%)
ephemeral-storage 100Mi (0%) 4Gi (15%)
hugepages-1Gi 0 (0%) 0 (0%)
hugepages-2Mi 0 (0%) 0 (0%)
Events: <none>
| 소스 | 연결 수 | 용도 | Redis 연산 |
|---|---|---|---|
| ext-authz (Go) | 10 | JWT 블랙리스트 조회 | EXISTS blacklist:{jti} |
| auth-api (FastAPI) | ~1 | 블랙리스트 등록/삭제 | SET, DEL |
| Sentinel (HA) | 4 | Primary 감시 (2노드 × 2연결) | PING, SUBSCRIBE |
| Replica | 1 | 데이터 동기화 (node-1 → node-0) | REPLCONF |
| 합계 | 26 |
현재 이코에코는 Redis Sentinel로 구성되어 있다.
Sentinel은 Primary의 상태를 지속 감시하며, 장애 감지 시 Replica를 자동으로 Primary로 승격한다.
각 Pod에 sidecar로 배포된 Sentinel은 Primary에 2개 연결(cmd + pubsub)을 유지하며 상태 확인(PING)과 failover 이벤트 구독(SUBSCRIBE)을 수행한다. 비용 효율을 위해 단일 노드(k8s-redis)에서 Pod 단위 HA를 적용 중이다. 노드 자체 장애 시에는 Redis 서비스 전체가 다운될 위험은 있지만, 프로세스 단위의 크래시나 Pod 재시작 시 자동 복구를 보장한다.
현재 메모리 사용량은 1.6GB으로 capacity 대비 부하를 감당할 여유분은 2GB 정도다.
클라이언트 연결용 메모리로 500MB 한정하고 connection을 10KB로 추산하면 추가 연결 가능분은 50,000+다. 커넥션 풀과 MaxClient를 늘릴 리소스는 충분하다.
Redis PoolSize 튜닝

| 설정 | 기본값 | 튜닝값 | 환경변수 |
|---|---|---|---|
| PoolSize | 20 | 100 | REDIS_POOL_SIZE |
| MinIdleConns | 0 | 20 | REDIS_MIN_IDLE_CONNS |
| PoolTimeout | 4s | 2s | REDIS_POOL_TIMEOUT_MS |
| ReadTimeout | 3s | 1s | REDIS_READ_TIMEOUT_MS |
| WriteTimeout | 3s | 1s | REDIS_WRITE_TIMEOUT_MS |
앞서 밝혔듯 k8s-redis 노드의 스펙은 여유로운 상황이고 Redis의 Pod 레벨 HA, 도메인과 Redis 간 노드 격리 또한 보장된 상황이다. Redis PoolSize를 Idle: 10->20, Max: 20->100까지 올려, ext-authz의 성능 지표가 개선되는지부터 확인했다.
users: 2000, ramp-ups: 200, time: 5m, Redis PoolSize [Idle: 20, Max: 100]
https://snapshots.raintank.io/dashboard/snapshot/ZwODTmIBu8gb4wHEpvZ9ksH0q7tZzNhK
Grafana
If you're seeing this Grafana has failed to load its application files 1. This could be caused by your reverse proxy settings. 2. If you host grafana under subpath make sure your grafana.ini root_url setting includes subpath. If not using a reverse proxy m
snapshots.raintank.io
스크린샷으로 나열하기엔 대시보드가 방대해져서 메트릭을 스냅샷 링크로 공유한다.
위 링크에 접속하면 당시 측정한 대시보드 값을 얻을 수 있다.
현재로선 부하 테스트로 얻은 grafana 대시보드 값을 실성능으로 가정하고 이어가야 하니..
라이브 트래픽을 보내서 이코에코의 실성능 테스트하고 싶다면 frontend.dev.growbin.app을 열고 맘껏 써주길 바란다.
| 메트릭 | AS-IS (PoolSize=20) | 튜닝 후 (PoolSize=100) | 변화 |
|---|---|---|---|
| Avg Latency | 57-80ms | 16-22ms | 📉 72% 개선 |
| Success Rate | 90-94% | 98.25-99.5% | 📈 +5-9%p |
| redis_error rate | 11-54 req/s | 3-8 req/s | 📉 85% 감소 |
| redis_error 비율 | 6-10% | 0.3-0.9% | 📉 90% 감소 |
| p99 JWT Verify | 0.7-0.95ms | 0.45-0.48ms | ➡️ 유사 |
| p99 Redis Lookup | 100ms 초과 | 100ms 초과 | 메트릭 개선 필요 |
| Peak RPS | ~884 | ~900 | ➡️ 유사 |
| CPU Usage | 0.01-0.19 cores | 0.06-0.19 cores | ➡️ 유사 |
| Memory Usage | 17-25MB | 20-35MB | ➡️ 유사 |
p99, avg latency, success rate, RPS 전방면에 걸쳐 지표가 개선되었다.
PoolSize 100에서의 Success Rate를 살피면 초기 warm-ups(20)에서 0.5-2%가량의 요청이 드랍된다.
부하가 급증하며 풀이 확장하는 구간(20->100) 중 일부 요청이 PoolTimeout(2s)을 초과했다.
RPS 또한 900으로 2000 users, 200 ramp-ups, wait time 1-3초인 현재 테스트 환경의 임계 처리량 근사하다.
HPA[min:2, max:5]를 적용할 경우 RPS는 950까지 올릴 수 있다. ext/authz 파드의 cpu 사용량이 limits인 0.2 cores(ext-authz Stress Test 참고)까지 도달한 상황이라 더 강한 부하를 위해선 추가 조정이 필요하다.
Kubernetes Deployment CPU requests/limits

ext-authz deployments의 cpu limits를 늘리기 전에 K8s Deployments의 리소스 관리 특성과 유의할 점에 대해 먼저 살펴보자.
관련 내용은 Sysdig 블로그를 참고했다.
핵심 개념
| 구분 | requests | limits |
|---|---|---|
| 초과 시 | 다른 Pod 영향 | Throttling (CPU) / OOM Kill (Memory) |
CPU 특성 (Compressible Resource)
CPU는 압축 가능한 리소스다.
→ 부족 시 프로세스가 죽지 않고 Throttling (대기)
→ 성능 저하만 발생
1000m = 1 core
500m = 0.5 core = 100ms/200ms 사용 가능
Throttling 발생 조건:
ext-authz limit: 200m (0.2 cores)
= 매 100ms 주기 중 20ms만 CPU 사용 가능
= 20ms 초과 시 나머지 시간 대기 (Throttling)
ext-authz 리소스 스펙 (/workloads/domains/ext-authz/base)
resources:
requests:
cpu: 50m # 스케줄링용 (0.05 cores)
memory: 64Mi
limits:
cpu: 200m # 최대 0.2 cores ← 병목
memory: 128Mi
문제점
| 항목 | 현재값 | 상태 |
|---|---|---|
| Memory limit | 128Mi | 충분 (실사용 20-35MB) |
| RPS | ~900 | CPU 한계로 정체 |
CPU limits w.HPA
Best Practice
| QoS 클래스 | requests | limits | 비고 |
|---|---|---|---|
| Burstable QoS | 100m | 500m | 권장 (평시 절약, 피크 시 버스트 허용) |
| No Limits | 100m | - | 최대 유연성이나 노드 안정성 위험 |
HPA 시나리오

Single Pod (requests=100m, limits=500m):
├── 평상시: ~50m 사용 (50%)
├── 부하 증가: ~100m 사용 → HPA 70% 초과 감지
├── 스케일 아웃: 2 pods
└── 각 Pod: ~50m 사용 (안정화)
버스트 상황:
├── 순간 트래픽 급증
├── CPU 500m까지 버스트 허용 (Throttling 없음)
└── HPA가 추가 Pod 생성할 시간 확보
k8s-api-auth 노드 리소스 분석
| 항목 | 값 | 상태 |
|---|---|---|
| Memory | 1.77GB (1853Mi) | Allocatable |
| 현재 CPU 사용 | 67m (3%) | 매우 여유 |
| 현재 Memory 사용 | 1258Mi (69%) | ⚠️ 주의 |
현재 Pod 구성 (auth namespace)
| Pod | Container | CPU req | CPU lim | Mem req | Mem lim | 실사용 |
|---|---|---|---|---|---|---|
| auth-api | istio-proxy | 100m | 2000m | 128Mi | 1Gi | 3m / 57Mi |
| ext-authz | ext-authz | 50m | 200m | 64Mi | 128Mi | 1m / 20Mi |
| ext-authz | istio-proxy | 100m | 2000m | 128Mi | 1Gi | 4m / 86Mi |
| 합계 | - | 400m | 4.7 Core | 576Mi | 2.6 Gi | ~10m / ~242Mi |
HPA 최댓값 계산
권장 설정 적용 시 (ext-authz: 100m req / 500m lim):
Pod당 requests:
├── ext-authz: 100m CPU, 64Mi Memory
└── istio-proxy: 100m CPU, 128Mi Memory
= 200m CPU, 192Mi Memory per Pod
노드 여유분 (auth-api 제외):
├── CPU: 2000m - 250m(auth-api+istio) = 1750m
├── Memory: 1853Mi - 384Mi(auth-api+istio) = 1469Mi
ext-authz 최대 Pod 수:
├── CPU 기준: 1750m / 200m = 8.75 → 8 pods
└── Memory 기준: 1469Mi / 192Mi = 7.6 → 7 pods
→ Memory가 제한 요소: 최대 7 pods
안전 마진 고려
현재 노드 메모리 69% 사용 중 (시스템 + 캐시 포함)
→ 실제 여유분은 계산보다 적음
안전한 HPA 설정:
├── minReplicas: 2 (가용성 보장)
├── maxReplicas: 5 (보수적)
└── 여유분 확보로 OOM 방지
5 Pods 시 리소스 예상치
| 항목 | 계산 | 비율 |
|---|---|---|
| Memory Requests | 384Mi + (5×192Mi) = 1344Mi | 73% ⚠️ |
| Redis 연결 | 5 × 200 = 1000 | 2% (maxclients 50K) ✅ |
Redis Pool + HPA 리소스 예상치 (2000 users, 200 ramp-ups)
현재 설정
| 항목 | 값 |
|---|---|
| MinIdleConns | 50 / Pod |
| HPA min | 2 pods |
| HPA max | 5 pods |
| Redis maxclients | 50,000 |
연결 수 예상치
| 상태 | Pods | 연결 수 | Pool 사용률 |
|---|---|---|---|
| 평상시 | 2 | 2 × 200 = 400 | 16% |
| 스케일 아웃 | 5 | 5 × 200 = 1,000 | 40% |
| + 기타 (auth-api, Sentinel) | - | +10 | - |
| 최대 총합 | 5 | ~1,010 | ~40% |
메모리 소모 예상치
| 상태 | 연결 수 | 연결당 메모리 | 총 메모리 |
|---|---|---|---|
| 평상시 | 400 | ~10KB | ~4MB |
| 최대 (5 pods) | 1,000 | ~10KB | ~10MB |
Max PoolSize 200 기준으로 연결 범위는 400-1,000이다. (현재 Go의 grpc 동시성 설정은 끈 상태다.)
Max Connection Pool을 50,000까지 확장해 둔 현재 시점을 기준으로 2% 내외의 사용량을 보인다.
메모리 여유 또한 2GB로 k8s-redis 노드의 임계치까지 도달하기엔 여전히 적은 사용량이다. (10KB * 50,000 = 500MB)
HPA를 한계치까지 올려둔 상태니, 가용 유저수를 늘릴 때 임계 PoolSize를 조정하며 찾아보자.
ext-authz 최종 스펙
# workloads/domains/ext-authz/base/deployment.yaml
...
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 500m
memory: 256Mi
# workloads/domains/ext-authz/base/hpa.yaml
spec:
minReplicas: 2 # 가용성 (1 pod 장애 대응)
maxReplicas: 5 # 노드 메모리 제약
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # requests 100m의 70% = 70m
# workloads/domains/ext-authz/base/destination-rule.yaml
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: ext-authz
namespace: auth
spec:
host: ext-authz.auth.svc.cluster.local
trafficPolicy:
loadBalancer:
simple: ROUND_ROBIN
connectionPool:
http:
h2UpgradePolicy: UPGRADE
http2MaxRequests: 1000
maxRequestsPerConnection: 100 # 100 요청마다 연결 종료, gRPC 로드밸런싱 불균형 완화
# workloads/domains/ext-authz/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: auth
resources:
- deployment.yaml
- service.yaml
- servicemonitor.yaml
- hpa.yaml
- destination-rule.yaml
labels:
- includeSelectors: true
pairs:
app.kubernetes.io/part-of: ecoeco-backend
domain: ext-authz
tier: business-logic
# workloads/domains/character/base/hpa.yaml
...
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: character-api
minReplicas: 1
maxReplicas: 3 # 더미 엔드포인트인 character의 병목 고려
ext-authz 배포 현황


ubuntu@k8s-master:~$ kubectl get pods -n auth -l app=ext-authz
NAME READY STATUS RESTARTS AGE
ext-authz-67595bd97-vfckh 2/2 Running 0 76s
ext-authz-67595bd97-xxtnm 2/2 Running 0 4m46s
ubuntu@k8s-master:~$ kubectl get hpa -n auth ext-authz
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
ext-authz Deployment/ext-authz 2%/70% 2 5 2 4m54s
ubuntu@k8s-master:~$ kubectl top pods -n auth -l app=ext-authz
NAME CPU(cores) MEMORY(bytes)
ext-authz-67595bd97-vfckh 3m 48Mi
ext-authz-67595bd97-xxtnm 3m 47Mi
users: 2000, ramp-ups: 200, time: 30m, PoolSize [Idle: 200, Max: 500], HPA [Min:2, Max:5]
ubuntu@k8s-master:~$ watch -n 1 "kubectl exec -n redis dev-redis-node-0 -c redis -- redis-cli INFO clients | grep connected"
Every 1.0s: kubectl exec -n redis dev-redis-node-0 -c redis -- redis-cli INFO clients | grep connected k8s-master: Mon Dec 15 13:18:05 2025
connected_clients:108
ubuntu@k8s-master:~$ watch -n 1 "kubectl exec -n redis dev-redis-node-0 -c redis -- redis-cli INFO clients | grep connected"
Every 1.0s: kubectl exec -n redis dev-redis-node-0 -c redis -- redis-cli INFO clients | grep connected k8s-master: Mon Dec 15 15:00:52 2025
connected_clients:5494
# 좀비 커넥션이 쌓인 모습이다. RTT를 측정한 뒤, 그 값을 기준으로 connection TTL을 조정하자.
https://snapshots.raintank.io/dashboard/snapshot/P5uoWRrXqFYBvoc9AvXP4kYF1M6jpfeG
Grafana
If you're seeing this Grafana has failed to load its application files 1. This could be caused by your reverse proxy settings. 2. If you host grafana under subpath make sure your grafana.ini root_url setting includes subpath. If not using a reverse proxy m
snapshots.raintank.io
이 테스트부터 메트릭이 개선됐다. 보다 촘촘한 시간선을 볼 수 있다.
Observability가 잡히니 확실히 눈이 편하다.
| 지표 | 최소 | 최대 | 안정값 |
|---|---|---|---|
| RPS | 350 | 950 | ~850-900 |
| p99 Latency | 100ms | 450ms | ~250-350ms |
| Avg Latency | 7.5ms | 30ms | ~15-20ms |
| p99 JWT Verify | 2ms | 3.4ms | ~2.5ms |
| p99 Redis Lookup | 150ms | 450ms | ~250-350ms |
| redis_error | 0 | 3.5 req/s | ~1-2 req/s |
| requests_in_flight | 0 | 180 | ~20-60 |
| Pod Count | 3 | 5 | 5 |
리소스 사용량
| 지표 | 최소 | 최대 | 안정값 |
|---|---|---|---|
| istio-proxy CPU (cores) | 0.3 | 0.8 | ~0.65-0.75 |
| Memory (MB) | 250 | 475 | ~400-450 MB |
개선 지표
| 항목 | AS-IS → TO-BE | 개선율 |
|---|---|---|
| Avg Latency | 57-80ms → 7.5-30ms | ~70-87% |
| redis_error | 6-10% → <0.2% | 98% |
| 안정성 | 단일 Pod | ext-authz: 3-5 Pods (HPA), character-api: 1-3 Pods (HPA) |
튜닝 단계별 성능 비교
| 메트릭 | AS-IS (PoolSize=20) | 1차 튜닝 (PoolSize=100) | 2차 튜닝 (PoolSize=500 + HPA) | 변화 |
|---|---|---|---|---|
| Avg Latency | 57-80ms | 16-22ms | 7.5-30ms | 최대 87% 개선 |
| Success Rate | 90-94% | 98.25-99.5% | 99.55-100% | +6-10%p |
| redis_error rate | 11-54 req/s | 3-8 req/s | 0-3.5 req/s | 93% 감소 |
| redis_error 비율 | 6-10% | 0.3-0.9% | <0.2% | 98% 감소 |
| p99 JWT Verify | 0.7-0.95ms | 0.45-0.48ms | 2-3.4ms | 유사 |
| p99 Redis Lookup | 100ms+ (상한) | 100ms+ (상한) | 150-450ms | 메트릭 개선 완료 |
| Peak RPS | ~884 | ~900 | ~950 | +7% |
| requests_in_flight | 230 | - | 20-180 | 분산 처리 |
| Pod Count | 1 | 1 | 3-5 (HPA) | 수평 확장 |
users: 2500, ramp-ups: 250, time: 30m, PoolSize [Idle: 200, Max: 500], HPA [Min:2, Max:5]
https://snapshots.raintank.io/dashboard/snapshot/1qhkHr5rWubb29VtWCAXYB66bHMmN5Ad
Grafana
If you're seeing this Grafana has failed to load its application files 1. This could be caused by your reverse proxy settings. 2. If you host grafana under subpath make sure your grafana.ini root_url setting includes subpath. If not using a reverse proxy m
snapshots.raintank.io


주요 지표는 p99 latency와 p99 redis lookup이다.
대시보드 스냅샷을 살펴보면 확인할 수 있지만 측정된 메트릭 수치가 동일해서 redis lookup의 병목이 그대로 latency가 된다. ext-authz가 클러스터 내에서 가장 성능이 빠르긴 하지만..
클러스터의 베이스라인으로 묶이기엔 어딘가 아쉽다. 5000 RPS도 시원하게 뚫어줬으면 하는 맘이다.
물리적인 Redis 노드 증설없이 욕심일 수 있으나.. MQ로 관리하면 또 모르겠다.
현재 구조에서 임계 성능을 뽑는 게 이번 튜닝의 목적이니 지표부터 살펴보겠다.
| 지표 | 최소 | 최대 | 안정값 |
|---|---|---|---|
| RPS | 300 | 1200 | ~900-1100 |
| p99 Latency | 150ms | 400ms | ~200-300ms |
| Avg Latency | 10ms | 24ms | ~15-20ms |
| p99 JWT Verify | 2.7ms | 3.7ms | ~3.3-3.5ms |
| p99 Redis Lookup | 150ms | 400ms | ~250-350ms |
| redis_error | 0.75 | 4 req/s | ~1-2 req/s |
| requests_in_flight | 0 | 180 | ~20-60 |
| Pod Count | 3 | 5 | 5 |
리소스 사용량
| 지표 | 최소 | 최대 | 안정값 |
|---|---|---|---|
| istio-proxy CPU (cores) | 0.2 | 0.9 | ~0.65-0.8 |
| Memory (MB) | 225 | 475 | ~400-450 |
개선 지표
| 항목 | AS-IS (5m 테스트) | TO-BE (30m 테스트) | 개선율 |
|---|---|---|---|
| Avg Latency | ~125ms | 10-24ms | ~84% |
| Peak RPS | ~42 | ~1,200 | ~28배 |
| redis_error | ~6 req/s | 0.75-4 req/s | ~67% |
| 안정성 | 단일 Pod, CPU 병목 | HPA 3-5 Pods | 수평 확장 |
정리 [2500, 250, wait_time: 1-3s]
- 30분 장기 테스트에서 안정성 확인: 튜닝 전 스트레스 테스트(5m) 대비 Success Rate가 86% → 99.8%로 대폭 개선, 장시간 부하에도 안정적 유지
- RPS 처리량 극대화: PoolSize 튜닝 + HPA 적용으로 RPS가 ~42 → ~1,200까지 약 28배 증가
- Latency 개선: Avg Latency가 125ms → 15-20ms로 84% 개선, p99 470ms → 200-300ms로 57% 감소
- 리소스 분산 효과: 단일 Pod 기준 CPU 0.2 cores 한계에서 5개 Pod로 분산되어 총 0.4-0.5 cores로 여유 있게 처리
- HPA: 30분 테스트에서 Redis Command Queue에 적재된 부하에 따라 Pod가 3 - 5개로 탄력적으로 운영
추가 개선 방향에 대한 고민
PoolSize 500 + HPA 튜닝으로 2500 users, 250 ramp-ups, wait 1-3초 환경에서 안정적인 처리(Success Rate 99.8%, RPS 1100)를 확보했으나, p99 Redis Lookup이 250-350ms로 전체 Latency의 주요 병목으로 남았다.

현 Redis Latency의 주된 병목은 Redis Command Queue 과적재다. Redis는 싱글 스레드 이벤트 루프 기반으로 동작한다.
Redis is, mostly, a single-threaded server from the POV of commands execution. It uses I/O multiplexing to handle many clients efficiently.
모든 명령이 단일 스레드에서 순차 처리되며 동시 요청 시 대기 Queue에 적재된다. (반쪽짜리 비동기..)
최대 부하를 기준으로 단일 스레드에 5 Pods X 500 Pool = 2500 Connection이 붙는다.
io-threads should be set to a value less than or equal to the number of CPU cores.
Redis 6.0+부터 멀티 스레딩을 지원하지만(이코에코 Redis의 경우 v7.4.1이다.) 권장 설정은 CPU 코어 이하다.
현재 2 cores인 상황에서 유의미한 성능 향상을 보기 어렵다.

다른 선택지로 Bloom Filter를 로컬 캐시로 활용하는 방안이 있다.
Bloom Filter는 확률적 자료구조로, 특정 원소가 집합에 "확실히 없음" 또는 "있을 수 있음"을 해싱으로 판별한다.
False Negative가 없어 블랙리스트 토큰을 놓치지 않으므로, 로컬 캐시의 보안 Trade-off를 완화할 수 있다.
다만 동기화 병목을 고려해야 한다. 블랙리스트에 새 JWT가 적재될 때마다 ext-authz 파드에 전파가 필요하다.
Bloom Filter 동기화 시점을 줄이려면 MQ 기반 실시간 전파가 필요하기에 현재 인프라에선 택하기 어렵다.
Bloom Filter는 삭제를 지원하지 않는다는 점도 리스크다. JWT는 만료 후 무효화되므로 적재된 데이터가 곧 더미가 된다.

Counting Bloom Filter라고 삭제를 지원하는 형태도 있지만..
삭제를 하려면 우선 lookup을 거쳐야 한다. 결국 key로 사용할 JWT가 필요해지고, 이러면 다시 Redis Blacklist에 의존하게 된다.
무엇보다 로컬 캐시는 ext-authz 노드의 리소스를 써야 한다. 일정 크기를 넘어가면 결국 분리해야 한다.
더구나 로컬 노드의 여유 리소스는 HPA로 부하 분산이 가능한 파드수를 결정하는 주요 팩터다.
현재 구조에서 JWT Blacklist 로컬 캐시안은 얻는 이점은 미약한데 비해 내어줘야할 Trade-off가 많아 반려했다.
JWT Blacklist는 로그인/로그아웃이 확실히 보장되는 만큼 동작 신뢰도가 높지만 stateless가 강점인 웹 서버를 state로 묶어버리는 점이 항상 발목을 잡는다.
그렇지만 시스템 로그인 / 로그아웃을 state 없이 처리하는 것도 넌센스라..
JWT의 유효시간을 극단적으로 짧게 가져가면 해결이 될 여지는 있으나, 그렇게 되면 클라이언트 측에서 주기적으로 refresh를 호출해야한다.
‘로그인/로그아웃을 온전히 백엔드 주도로 관리‘ 한다는 이점 아래 너무 많은 걸 내주는 면도 없지 않아 있다.
서비스 API의 Auth를 다루면 보다 나은 선택지가 보일텐데.. JWT 검증 서버가 클러스터의 baseline이 되는 건 달갑지 않다. (Go로 임계 성능을 올려두는 것도 이 때문이다.)
=== Pure Network RTT Measurement ===
Measuring 1000 PINGs...
Samples: 1000
--- Latency Distribution ---
Min: 1.000 ms
p50: 1.208 ms
p90: 1.420 ms
p95: 1.539 ms
p99: 2.761 ms
Avg: 1.299 ms
StdDev: 1.227 ms
도메인 노드와 Redis 노드가 물리적으로 분리된 만큼 단일 요청으로 쌓이는 RTT도 부하가 될 수 있다는 생각에 측정을 진행했다.
배치 파이프라인을 구성해 N개의 요청을 모아 단일 요청을 보내면 현 클러스터를 기준으로 1.3*(N-1)ms만큼 줄일 여지는 존재하나, p99 Redis Lookup이 250-350ms인 걸 감안하면 0.3%로 비중이 현저히 낮다. 무엇보다 배치 작업이 Redis 큐 부하를 해소하진 못한다.
돌고 돌아 Redis 스케일업 또는 단일 Redis 노드를 Cluster로 전환이 근본적인 해결이나, 로깅, MQ 노드 등 증설 피쳐가 남은 상황에서 'Redis에 리소스를 추가로 투입할 가치가 있는가'는 여전히 의문이다.
현재 k8s-ext-authz, k8s-redis 단일 노드로 1100 RPS, Success Rate 99.8%를 확보했으니 Observability 고도화와 MQ 도입 후 추가 최적화를 검토해도 좋겠다.