diff --git a/docker-compose.yml b/docker-compose.yml index 24de4bc..d9d7d2c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,8 @@ services: SPRING_DATA_REDIS_HOST: redis SPRING_DATA_REDIS_PORT: 6379 + REDIS_CONNECT_TIMEOUT: ${REDIS_CONNECT_TIMEOUT:-200ms} + REDIS_COMMAND_TIMEOUT: ${REDIS_COMMAND_TIMEOUT:-200ms} CACHE_ENABLED: ${CACHE_ENABLED:-true} diff --git a/docs/01-requirements.md b/docs/01-requirements.md index d04e277..eb7ca6e 100644 --- a/docs/01-requirements.md +++ b/docs/01-requirements.md @@ -43,7 +43,7 @@ URL Shortener는 긴 URL을 짧은 코드로 변환하고, 단축 URL 요청이 | FR-004 | 서로 다른 URL에 동일한 단축 코드를 할당하지 않는다. | Must | | FR-005 | 유효하지 않은 URL 입력에는 400 응답을 반환한다. | Must | | FR-006 | 존재하지 않는 단축 코드에는 404 응답을 반환한다. | Must | -| FR-007 | Hash, 순차 ID 기반 Base62, 난수 Base62 생성 방식을 비교한다. | Should | +| FR-007 | Sequence, Hash, Snowflake 기반 Base62 생성 방식을 비교한다. | Should | | FR-008 | Redis를 적용해 반복적인 원본 URL 조회를 캐시한다. | Should | | FR-009 | Bloom Filter 또는 Negative Cache로 잘못된 코드의 반복 조회를 줄인다. | Could | | FR-010 | 사용자가 생성된 URL을 수정하거나 삭제하는 기능은 제공하지 않는다. | Won't | @@ -70,9 +70,11 @@ URL Shortener는 긴 URL을 짧은 코드로 변환하고, 단축 URL 요청이 ### 가용성 -* Redis 장애 시 MySQL 조회로 우회한다. -* 캐시 데이터가 유실되어도 원본 URL을 복구할 수 있어야 한다. -* 초기 구조의 단일 장애 지점은 Spring Boot, MySQL, Redis이다. +- Redis 조회 실패 시 MySQL 원본 저장소로 Fallback한다. +- Redis 장애 중에도 리다이렉트 요청의 오류율을 1% 미만으로 유지한다. +- Redis 복구 후 Cache Aside 조회 경로로 자동 복귀한다. +- 캐시 데이터가 유실돼도 MySQL을 통해 원본 URL을 복구할 수 있어야 한다. +- 현재 구조의 단일 장애 지점은 Spring Boot, MySQL, Redis이다. ### 확장성 @@ -109,7 +111,7 @@ URL Shortener는 긴 URL을 짧은 코드로 변환하고, 단축 URL 요청이 * Redis Cache-Aside * k6 부하 테스트 * 개선 전후 동일 조건 재측정 -* Redis 장애 및 Hot Key 실험 +* Redis 장애 시 MySQL Fallback 실험 ### 제외 diff --git a/docs/03-architecture.md b/docs/03-architecture.md index 2749eba..987af93 100644 --- a/docs/03-architecture.md +++ b/docs/03-architecture.md @@ -6,8 +6,8 @@ * 가장 중요한 비기능 요구사항: 단축 코드 유일성, 읽기 성능, 확장 가능성 * 설계에서 우선한 요소: 단순한 초기 구조와 측정 가능한 베이스라인 * 감수한 트레이드오프: 초기에는 모든 조회 요청이 MySQL에 집중된다. -* 초기 코드 생성 전략: 순차 ID 기반 Base62 -* 최종 코드 생성 전략: Hash 충돌 해소 및 난수 Base62 방식과 비교한 뒤 결정한다. +* 초기 코드 생성 전략: Sequence ID + Base62 +* 최종 설계: 단일 인스턴스에서는 Sequence를 기본으로 사용하고, 다중 인스턴스에서는 Snowflake 적용을 검토한다. ## 2. 전체 아키텍처 @@ -15,19 +15,24 @@ flowchart LR Client[Client] API[Spring Boot API] + Redis[(Redis)] DB[(MySQL)] - Metrics[Prometheus] - Dashboard[Grafana] - LoadTest[k6] + Prometheus[Prometheus] + Grafana[Grafana] + k6[k6] Client --> API + API --> Redis API --> DB - Metrics --> API - Dashboard --> Metrics - LoadTest --> API + Prometheus --> API + Grafana --> Prometheus + k6 --> API ``` -초기 구조에는 Redis와 Message Broker를 포함하지 않는다. +MySQL은 원본 데이터를 보관하는 Source of Truth이며, +Redis는 리다이렉트 조회 성능을 위한 보조 저장소로 사용한다. + +Redis Cache Miss 또는 연결 실패 시 MySQL을 조회한다. ```text 요구사항 정의 @@ -47,6 +52,7 @@ flowchart LR | Prometheus | 애플리케이션 지표 수집 | 현재는 단일 인스턴스 | 성능 지표 수집 불가 | | Grafana | 성능 지표 시각화 | 현재는 단일 인스턴스 | 대시보드 조회 불가 | | k6 | 부하 테스트 실행 | VU 단계적 증가 | 서비스에는 영향 없음 | +| Redis | 원본 URL 조회 캐시 | Sentinel, Cluster | 장애 시 MySQL Fallback으로 지연 증가 | ## 4. 요청 흐름 @@ -60,24 +66,26 @@ flowchart LR ### URL 리다이렉트 -1. 클라이언트가 단축 코드로 요청한다. -2. API 서버가 Base62 코드를 숫자 ID로 변환한다. -3. MySQL에서 ID를 이용해 원본 URL을 조회한다. -4. 원본 URL을 `302 Found`로 반환한다. +1. `shortCode`로 Redis를 조회한다. +2. Cache Hit이면 원본 URL을 반환한다. +3. Cache Miss이면 MySQL의 `short_code` 인덱스로 조회하고 Redis에 저장한다. +4. Redis 조회에 실패하면 MySQL로 Fallback한다. +5. 원본 URL을 `302 Found`로 반환한다. ### 실패 흐름 1. URL 형식이 잘못된 경우 `400 Bad Request`를 반환한다. 2. 단축 코드 형식이 잘못됐거나 데이터를 찾을 수 없으면 `404 Not Found`를 반환한다. -3. DB 연결에 실패하면 `503 Service Unavailable`을 반환한다. +3. DB 연결에 실패하면 `503 Service Unavailable`을 반환한다. +4. Redis 연결에 실패하면 MySQL 조회로 전환한다. ## 5. 데이터 모델 ### 주요 엔티티 -| 엔티티 | 주요 필드 | 설명 | -| -------- | ---------------------- | -------------------- | -| ShortUrl | id, longUrl, createdAt | 원본 URL과 생성 정보를 저장한다. | +| 엔티티 | 주요 필드 | 설명 | +|---|---|---| +| ShortUrl | id, shortCode, longUrl, createdAt | 단축 코드와 원본 URL을 저장한다. | ### 관계 @@ -87,18 +95,14 @@ flowchart LR erDiagram SHORT_URL { BIGINT id PK + VARCHAR short_code UK VARCHAR long_url DATETIME created_at } ``` -초기 구조에서는 단축 코드를 별도 컬럼에 저장하지 않는다. - -```text -shortCode = Base62(id) -``` - -Redirect 요청에서는 단축 코드를 다시 ID로 변환해 Primary Key로 조회한다. +모든 생성 전략이 동일한 조회 경로를 사용하도록 `short_code`에 Unique Index를 적용했다. +Base62의 대소문자를 구분하기 위해 `ascii_bin` Collation을 사용한다. Hash와 난수 Base62 방식에서는 생성된 코드를 저장해야 하므로 이후 `short_code` 컬럼과 Unique Index를 사용하는 별도 구조를 적용한다. @@ -106,8 +110,8 @@ Hash와 난수 Base62 방식에서는 생성된 코드를 저장해야 하므로 | Method | Endpoint | 설명 | 멱등성 | | ------ | -------------- | ----------------- | --- | -| POST | `/api/v1/urls` | 긴 URL을 단축 URL로 변환 | No | -| GET | `/{shortCode}` | 원본 URL로 리다이렉트 | Yes | +| POST | `/api/v1/data/shorten` | 긴 URL을 단축 URL로 변환 | 전략에 따라 다름 | +| GET | `/api/v1/{shortCode}` | 원본 URL로 리다이렉트 | Yes | 동일한 원본 URL을 여러 번 요청하면 서로 다른 단축 URL이 생성될 수 있다. @@ -189,20 +193,9 @@ Redirect 요청이 계속 서버에 전달되므로 301보다 서버 부하가 | 전략 | 생성 방식 | 주요 확인 항목 | | --------------- | ------------------------------ | --------------------- | -| Sequence Base62 | Auto Increment ID를 Base62로 변환 | 처리량, 조회 성능, 예측 가능성 | -| Hash 충돌 해소 | URL Hash를 Base62로 표현하고 7자리로 제한 | 충돌 수, DB 조회 수, 재시도 횟수 | -| Random Base62 | 난수 기반 고정 길이 코드 생성 | 충돌 수, 확장성, 예측 가능성 | - -Hash 방식은 다음 순서로 동작한다. - -```text -원본 URL -→ SHA-256 Hash -→ Hash 결과를 Base62로 표현 -→ 7자리 코드 생성 -→ 중복 확인 -→ 충돌 시 다시 생성 -``` +| Sequence + Base62 | INSERT → ID 발급 → UPDATE | 단순하고 충돌 없음 | +| Hash + Base62 | 중복 조회 → SHA-256 → INSERT | 고정 길이, 충돌 재시도 필요 | +| Snowflake + Base62 | 분산 ID 생성 → INSERT | DB ID 비의존, nodeId 관리 필요 | 초기 구현은 Sequence Base62로 진행하고, 이후 세 방식의 RPS, p95, DB 조회 수와 충돌 횟수를 비교한다. @@ -224,12 +217,19 @@ Hash와 난수 방식에서는 `short_code`에 Unique Constraint를 적용해 | DB 장애 | URL 생성 및 조회 불가 | Connection 오류, Actuator | 503 반환, DB 복구 | | Prometheus 장애 | 지표 수집 불가 | Scrape 상태 | 컨테이너 재시작 | | Grafana 장애 | 대시보드 조회 불가 | 컨테이너 상태 | 컨테이너 재시작 | +| Redis 장애 | 응답 지연 및 DB 부하 증가 | Cache Error, Fallback 지표 | MySQL Fallback 후 자동 복귀 | 초기 구조에서는 DB 장애 시 요청을 처리할 대체 저장소가 없다. +Redis GET에 실패하면 MySQL로 Fallback한다. + +Redis 장애가 확인된 요청에서는 Redis SET을 생략해 Timeout이 중복되지 않도록 했다. + +이는 기능 지속을 위한 Graceful Degradation이며 Redis 자체의 고가용성을 구성한 것은 아니다. + ## 11. 단일 장애 지점 -* 현재 존재하는 SPOF: Spring Boot 단일 인스턴스, MySQL 단일 인스턴스 +* 현재 존재하는 SPOF: Spring Boot 단일 인스턴스, MySQL, Redis * 프로젝트 범위에서 허용한 이유: 로컬 환경에서 초기 구조의 병목을 확인하기 위한 실험이기 때문이다. * 운영 환경에서의 개선 방법: Load Balancer, 다중 API 서버, MySQL Replica와 장애 조치 구성 @@ -251,8 +251,6 @@ Prometheus와 Grafana도 단일 인스턴스지만 서비스 요청 처리에는 ### 캐시 확장 -초기 구조에서는 캐시를 사용하지 않는다. - 부하 테스트를 통해 DB 조회 병목이 확인되면 Redis Cache-Aside를 적용한다. ```text @@ -262,9 +260,11 @@ Client → Cache Miss 시 MySQL ``` -* 캐시 키: `short-url:{shortCode}` -* 만료 정책: TTL 기반 -* Cache Stampede 대응: TTL Jitter 또는 요청 병합 +- 전략: Redis Cache Aside +- 키: `short-url:{shortCode}` +- TTL: 1시간 +- Redis 장애: MySQL Fallback +- 한계: 장애 중 DB 부하와 응답 지연 증가 ## 13. 보안 @@ -293,6 +293,8 @@ Client * HikariCP Pending Connection * 코드 충돌 횟수 * 코드 생성 재시도 횟수 +* Redis Cache Error 수 +* MySQL Fallback 수 ### Logs diff --git a/docs/04-experiment.md b/docs/04-experiment.md index 8231bb2..dc7fb97 100644 --- a/docs/04-experiment.md +++ b/docs/04-experiment.md @@ -503,8 +503,51 @@ DB Auto Increment 의존성이 없는 Snowflake 방식을 적용할 수 있다. 단, 전략별 한 번만 측정했으며 Sequence와 Snowflake의 차이가 작으므로 이번 결과만으로 Snowflake의 성능 우위를 일반화할 수는 없다. +## 17. Redis 장애 시 MySQL Fallback -## 17. 실험 한계 +Redis 장애가 리다이렉트 전체 장애로 이어지지 않도록 +Redis GET 실패 시 MySQL로 전환하는 Fallback을 적용했다. + +| 항목 | 조건 | +|---|---| +| VU | 100 | +| 실행 시간 | 120초 | +| 정상 | 0~30초 | +| Redis 중지 | 30~60초 | +| 복구 관찰 | 60~120초 | +| Redis Timeout | 200ms | +| HikariCP | 최대 10개 | + +### 결과 + +| 지표 | 결과 | +|---|---:| +| 요청 수 | 656,623 | +| 평균 RPS | 5,471.30 | +| 평균 응답 시간 | 18.09ms | +| 전체 p95 | 31.92ms | +| 최대 응답 시간 | 345.44ms | +| 실패율 | 0% | + +Redis 장애 구간에는 GET Error, MySQL Fallback과 DB Lookup이 함께 증가했다. +장애 구간의 p95는 약 200ms, p99는 약 220ms까지 증가했지만 +5xx 오류는 발생하지 않았다. + +Redis 복구 후 Error, Fallback과 DB Lookup은 다시 0으로 감소했고, +Cache Hit와 응답시간도 정상 수준으로 복귀했다. + +이를 통해 Redis 장애 시 성능 저하를 감수하면서 기능을 유지하고, +Redis 복구 후 Cache Aside 경로로 자동 전환되는 것을 확인했다. + +현재는 모든 요청이 Redis Timeout을 기다린 뒤 Fallback하므로, +향후 Circuit Breaker를 적용하면 장애 구간의 반복 대기를 줄일 수 있다. + +![Redis 장애 성능 지표](images/redis-fallback-100vu-performance.png) + +![Redis 장애 캐시 및 DB 지표](images/redis-fallback-100vu-cache-db.png) + + +## 18. 실험 한계 - 로컬 Docker 환경에서 실행했다. - k6, 애플리케이션, MySQL, Redis가 같은 장비의 자원을 사용했다. @@ -517,8 +560,10 @@ DB Auto Increment 의존성이 없는 Snowflake 방식을 적용할 수 있다. - Redis Stress Test의 처리량 한계가 애플리케이션, Redis 또는 로컬 환경 중 어디에서 발생했는지는 추가로 분리하지 않았다. - 단축 코드 생성 전략도 조건별 한 번만 측정해 Sequence와 Snowflake의 작은 차이가 실행 환경의 변동인지 확인하지 못했다. - Snowflake 전략은 단일 애플리케이션 인스턴스에서만 실행했으며, 서로 다른 nodeId를 사용하는 다중 인스턴스 환경은 검증하지 않았다. +- Redis 장애 실험은 프로세스 중지만 재현했으며 네트워크 지연과 패킷 손실은 검증하지 않았다. +- Redis 장애 중 더 높은 부하에서는 MySQL과 커넥션 풀이 포화될 수 있다. -## 18. 후속 실험 +## 19. 후속 실험 - [x] Redis Cache Aside 적용 - [x] Redis 적용 전후 부하 테스트 @@ -528,3 +573,7 @@ DB Auto Increment 의존성이 없는 Snowflake 방식을 적용할 수 있다. - [x] HikariCP Pool 크기 비교 - [x] 더 높은 VU로 Stress Test 수행 - [x] Sequence ID + Base62, Hash, Snowflake ID + Base62 비교 +- [x] Redis 장애 시 MySQL Fallback 및 자동 복구 검증 +- [ ] Circuit Breaker를 통한 Redis 장애 구간 Timeout 감소 +- [ ] Redis Sentinel 또는 Cluster 기반 고가용성 구성 +- [ ] 다중 애플리케이션 인스턴스와 장애 전환 검증 \ No newline at end of file diff --git a/docs/images/redis-fallback-100vu-cache-db.png b/docs/images/redis-fallback-100vu-cache-db.png new file mode 100644 index 0000000..8e29a85 Binary files /dev/null and b/docs/images/redis-fallback-100vu-cache-db.png differ diff --git a/docs/images/redis-fallback-100vu-performance.png b/docs/images/redis-fallback-100vu-performance.png new file mode 100644 index 0000000..18c3f27 Binary files /dev/null and b/docs/images/redis-fallback-100vu-performance.png differ diff --git a/docs/images/redis-fallback-run1-cache-db.png b/docs/images/redis-fallback-run1-cache-db.png new file mode 100644 index 0000000..3c60ad3 Binary files /dev/null and b/docs/images/redis-fallback-run1-cache-db.png differ diff --git a/docs/images/redis-fallback-run1-performance.png b/docs/images/redis-fallback-run1-performance.png new file mode 100644 index 0000000..8ea3e05 Binary files /dev/null and b/docs/images/redis-fallback-run1-performance.png differ diff --git a/k6/redis-fallback-test.js b/k6/redis-fallback-test.js new file mode 100644 index 0000000..2a8e57c --- /dev/null +++ b/k6/redis-fallback-test.js @@ -0,0 +1,39 @@ +import http from 'k6/http'; +import { check } from 'k6'; + +const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080'; +const SHORT_CODE = __ENV.SHORT_CODE; + +if (!SHORT_CODE) { + throw new Error('SHORT_CODE 환경변수가 필요합니다.'); +} + +export const options = { + vus: Number(__ENV.VUS || 100), + duration: __ENV.DURATION || '90s', + + thresholds: { + http_req_failed: ['rate<0.01'], + checks: ['rate>0.99'], + http_req_duration: ['p(95)<1000'], + }, +}; + +export default function () { + const response = http.get( + `${BASE_URL}/api/v1/${SHORT_CODE}`, + { + redirects: 0, + tags: { + experiment: 'redis-fallback', + }, + }, + ); + + check(response, { + 'redirect status is 302': (res) => res.status === 302, + 'location header exists': (res) => + typeof res.headers.Location === 'string' + && res.headers.Location.length > 0, + }); +} \ No newline at end of file diff --git a/monitoring/grafana/backups/spring-boot-overview.backup.json b/monitoring/grafana/backups/spring-boot-overview.backup.json new file mode 100644 index 0000000..a3a06b7 --- /dev/null +++ b/monitoring/grafana/backups/spring-boot-overview.backup.json @@ -0,0 +1,1928 @@ +{ + "apiVersion": "dashboard.grafana.app/v2", + "kind": "Dashboard", + "metadata": { + "name": "spring-boot-overview", + "namespace": "default", + "uid": "d8ff4239-0696-483e-ac21-919126fd4967", + "resourceVersion": "1785489867760013", + "generation": 4, + "creationTimestamp": "2026-07-30T10:33:40Z", + "labels": { + "grafana.app/deprecatedInternalID": "2113606437945344" + }, + "annotations": { + "grafana.app/createdBy": "access-policy:service", + "grafana.app/folder": "eftnup4o0j1tsd", + "grafana.app/managedBy": "classic-file-provisioning", + "grafana.app/managerId": "System Design Dashboards", + "grafana.app/sourceChecksum": "dd3f2ae3957746d44c447391e659a68b", + "grafana.app/sourcePath": "/var/lib/grafana/dashboards/spring-boot-overview.json", + "grafana.app/sourceTimestamp": "1785489862000", + "grafana.app/updatedBy": "access-policy:service", + "grafana.app/updatedTimestamp": "2026-07-31T09:24:27Z", + "grafana.app/folderTitle": "System Design", + "grafana.app/folderUrl": "/dashboards/f/eftnup4o0j1tsd/system-design" + } + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "builtIn": true, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "query": { + "datasource": { + "name": "-- Grafana --" + }, + "group": "grafana", + "kind": "DataQuery", + "spec": {}, + "version": "v0" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "prometheus" + }, + "group": "prometheus", + "kind": "DataQuery", + "spec": { + "expr": "sum(rate(http_server_requests_seconds_count{job=\"spring-boot\",uri!~\"/actuator.*\"}[$__rate_interval]))", + "legendFormat": "RPS" + }, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 1, + "links": [], + "title": "Requests Per Second", + "vizConfig": { + "group": "timeseries", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "options": { + "annotations": { + "clustering": -1, + "multiLane": false + }, + "legend": { + "calcs": [], + "displayMode": "list", + "enableFacetedFilter": false, + "overflow": "ellipsis", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + } + }, + "version": "13.1.1" + } + } + }, + "panel-10": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "prometheus" + }, + "group": "prometheus", + "kind": "DataQuery", + "spec": { + "editorMode": "code", + "expr": "jvm_threads_live_threads{\n application=\"url-shortener\"\n}", + "legendFormat": "Live Threads", + "range": true + }, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 10, + "links": [], + "title": "JVM Live Threads", + "vizConfig": { + "group": "timeseries", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "options": { + "annotations": { + "clustering": -1, + "multiLane": false + }, + "legend": { + "calcs": [], + "displayMode": "list", + "enableFacetedFilter": false, + "overflow": "ellipsis", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + } + }, + "version": "13.1.1" + } + } + }, + "panel-11": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "prometheus" + }, + "group": "prometheus", + "kind": "DataQuery", + "spec": { + "editorMode": "code", + "expr": "jvm_threads_virtual_live_threads{\n application=\"url-shortener\",\n scheduling_status=\"mounted\"\n}", + "legendFormat": "Mounted", + "range": true + }, + "version": "v0" + }, + "refId": "A" + } + }, + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "prometheus" + }, + "group": "prometheus", + "kind": "DataQuery", + "spec": { + "editorMode": "code", + "expr": "jvm_threads_virtual_live_threads{\n application=\"url-shortener\",\n scheduling_status=\"queued\"\n}", + "instant": false, + "legendFormat": "Queued", + "range": true + }, + "version": "v0" + }, + "refId": "B" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 11, + "links": [], + "title": "Virtual Threads Mounted / Queued", + "vizConfig": { + "group": "timeseries", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "annotations": { + "clustering": -1, + "multiLane": false + }, + "legend": { + "calcs": [], + "displayMode": "list", + "enableFacetedFilter": false, + "overflow": "ellipsis", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + } + }, + "version": "13.1.1" + } + } + }, + "panel-12": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "prometheus" + }, + "group": "prometheus", + "kind": "DataQuery", + "spec": { + "editorMode": "code", + "expr": "jvm_threads_virtual_pool_size_threads{\n application=\"url-shortener\"\n}", + "legendFormat": "Carrier Pool Size", + "range": true + }, + "version": "v0" + }, + "refId": "A" + } + }, + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "prometheus" + }, + "group": "prometheus", + "kind": "DataQuery", + "spec": { + "editorMode": "code", + "expr": "jvm_threads_virtual_parallelism{\n application=\"url-shortener\"\n}", + "instant": false, + "legendFormat": "Target Parallelism", + "range": true + }, + "version": "v0" + }, + "refId": "B" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 12, + "links": [], + "title": "Virtual Thread Scheduler", + "vizConfig": { + "group": "timeseries", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "annotations": { + "clustering": -1, + "multiLane": false + }, + "legend": { + "calcs": [], + "displayMode": "list", + "enableFacetedFilter": false, + "overflow": "ellipsis", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + } + }, + "version": "13.1.1" + } + } + }, + "panel-13": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "prometheus" + }, + "group": "prometheus", + "kind": "DataQuery", + "spec": { + "editorMode": "code", + "expr": "increase(\n jvm_threads_virtual_pinned_seconds_count{\n application=\"url-shortener\"\n }[$__rate_interval]\n)", + "legendFormat": "Pinned Events", + "range": true + }, + "version": "v0" + }, + "refId": "A" + } + }, + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "prometheus" + }, + "group": "prometheus", + "kind": "DataQuery", + "spec": { + "editorMode": "code", + "expr": "increase(\n jvm_threads_virtual_submit_failed_total{\n application=\"url-shortener\"\n }[$__rate_interval]\n)", + "instant": false, + "legendFormat": "Submit Failed", + "range": true + }, + "version": "v0" + }, + "refId": "B" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 13, + "links": [], + "title": "Virtual Thread Events", + "vizConfig": { + "group": "timeseries", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "annotations": { + "clustering": -1, + "multiLane": false + }, + "legend": { + "calcs": [], + "displayMode": "list", + "enableFacetedFilter": false, + "overflow": "ellipsis", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + } + }, + "version": "13.1.1" + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "prometheus" + }, + "group": "prometheus", + "kind": "DataQuery", + "spec": { + "expr": "histogram_quantile(0.95, sum by (le) (rate(http_server_requests_seconds_bucket{job=\"spring-boot\",uri!~\"/actuator.*\"}[$__rate_interval]))) * 1000", + "legendFormat": "p95" + }, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 2, + "links": [], + "title": "p95 Response Time", + "vizConfig": { + "group": "timeseries", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "options": { + "annotations": { + "clustering": -1, + "multiLane": false + }, + "legend": { + "calcs": [], + "displayMode": "list", + "enableFacetedFilter": false, + "overflow": "ellipsis", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + } + }, + "version": "13.1.1" + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "prometheus" + }, + "group": "prometheus", + "kind": "DataQuery", + "spec": { + "expr": "histogram_quantile(0.99, sum by (le) (rate(http_server_requests_seconds_bucket{job=\"spring-boot\",uri!~\"/actuator.*\"}[$__rate_interval]))) * 1000", + "legendFormat": "p99" + }, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 3, + "links": [], + "title": "p99 Response Time", + "vizConfig": { + "group": "timeseries", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "options": { + "annotations": { + "clustering": -1, + "multiLane": false + }, + "legend": { + "calcs": [], + "displayMode": "list", + "enableFacetedFilter": false, + "overflow": "ellipsis", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + } + }, + "version": "13.1.1" + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "prometheus" + }, + "group": "prometheus", + "kind": "DataQuery", + "spec": { + "expr": "100 * ((sum(rate(http_server_requests_seconds_count{job=\"spring-boot\",status=~\"5..\",uri!~\"/actuator.*\"}[$__rate_interval])) or vector(0)) / clamp_min(sum(rate(http_server_requests_seconds_count{job=\"spring-boot\",uri!~\"/actuator.*\"}[$__rate_interval])), 0.000001))", + "legendFormat": "5xx" + }, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 4, + "links": [], + "title": "5xx Error Rate", + "vizConfig": { + "group": "timeseries", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "options": { + "annotations": { + "clustering": -1, + "multiLane": false + }, + "legend": { + "calcs": [], + "displayMode": "list", + "enableFacetedFilter": false, + "overflow": "ellipsis", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + } + }, + "version": "13.1.1" + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "prometheus" + }, + "group": "prometheus", + "kind": "DataQuery", + "spec": { + "expr": "sum(jvm_memory_used_bytes{job=\"spring-boot\",area=\"heap\"})", + "legendFormat": "Heap" + }, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 5, + "links": [], + "title": "JVM Heap Used", + "vizConfig": { + "group": "timeseries", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "options": { + "annotations": { + "clustering": -1, + "multiLane": false + }, + "legend": { + "calcs": [], + "displayMode": "list", + "enableFacetedFilter": false, + "overflow": "ellipsis", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + } + }, + "version": "13.1.1" + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "prometheus" + }, + "group": "prometheus", + "kind": "DataQuery", + "spec": { + "expr": "process_cpu_usage{job=\"spring-boot\"} * 100", + "legendFormat": "CPU" + }, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 6, + "links": [], + "title": "Process CPU Usage", + "vizConfig": { + "group": "timeseries", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "options": { + "annotations": { + "clustering": -1, + "multiLane": false + }, + "legend": { + "calcs": [], + "displayMode": "list", + "enableFacetedFilter": false, + "overflow": "ellipsis", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + } + }, + "version": "13.1.1" + } + } + }, + "panel-7": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "prometheus" + }, + "group": "prometheus", + "kind": "DataQuery", + "spec": { + "editorMode": "code", + "expr": "rate(\n short_url_cache_hit_total{\n application=\"url-shortener\"\n }[$__rate_interval]\n)", + "legendFormat": "Cache Hit/s", + "range": true + }, + "version": "v0" + }, + "refId": "A" + } + }, + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "prometheus" + }, + "group": "prometheus", + "kind": "DataQuery", + "spec": { + "editorMode": "code", + "expr": "rate(\n short_url_cache_miss_total{\n application=\"url-shortener\"\n }[$__rate_interval]\n)", + "instant": false, + "legendFormat": "Cache Miss/s", + "range": true + }, + "version": "v0" + }, + "refId": "B" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 7, + "links": [], + "title": "Redis Cache Hit / Miss", + "vizConfig": { + "group": "timeseries", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "options": { + "annotations": { + "clustering": -1, + "multiLane": false + }, + "legend": { + "calcs": [], + "displayMode": "list", + "enableFacetedFilter": false, + "overflow": "ellipsis", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + } + }, + "version": "13.1.1" + } + } + }, + "panel-8": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "prometheus" + }, + "group": "prometheus", + "kind": "DataQuery", + "spec": { + "editorMode": "code", + "expr": "rate(\n short_url_db_lookup_total{\n application=\"url-shortener\"\n }[$__rate_interval]\n)", + "legendFormat": "DB Lookup/s", + "range": true + }, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 8, + "links": [], + "title": "DB Lookups Per Second", + "vizConfig": { + "group": "timeseries", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "options": { + "annotations": { + "clustering": -1, + "multiLane": false + }, + "legend": { + "calcs": [], + "displayMode": "list", + "enableFacetedFilter": false, + "overflow": "ellipsis", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + } + }, + "version": "13.1.1" + } + } + }, + "panel-9": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "prometheus" + }, + "group": "prometheus", + "kind": "DataQuery", + "spec": { + "editorMode": "code", + "expr": "hikaricp_connections_active{\n application=\"url-shortener\",\n pool=\"HikariPool-1\"\n}", + "legendFormat": "Active", + "range": true + }, + "version": "v0" + }, + "refId": "A" + } + }, + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "prometheus" + }, + "group": "prometheus", + "kind": "DataQuery", + "spec": { + "editorMode": "code", + "expr": "hikaricp_connections_pending{\n application=\"url-shortener\",\n pool=\"HikariPool-1\"\n}", + "instant": false, + "legendFormat": "Pending", + "range": true + }, + "version": "v0" + }, + "refId": "B" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 9, + "links": [], + "title": "HikariCP Active / Pending", + "vizConfig": { + "group": "timeseries", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "options": { + "annotations": { + "clustering": -1, + "multiLane": false + }, + "legend": { + "calcs": [], + "displayMode": "list", + "enableFacetedFilter": false, + "overflow": "ellipsis", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + } + }, + "version": "13.1.1" + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-1" + }, + "height": 8, + "width": 8, + "x": 0, + "y": 0 + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-2" + }, + "height": 8, + "width": 8, + "x": 8, + "y": 0 + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-3" + }, + "height": 8, + "width": 8, + "x": 16, + "y": 0 + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-4" + }, + "height": 8, + "width": 8, + "x": 0, + "y": 8 + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-5" + }, + "height": 8, + "width": 8, + "x": 8, + "y": 8 + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-6" + }, + "height": 8, + "width": 8, + "x": 16, + "y": 8 + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-7" + }, + "height": 8, + "width": 8, + "x": 0, + "y": 16 + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-8" + }, + "height": 8, + "width": 8, + "x": 8, + "y": 16 + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-9" + }, + "height": 8, + "width": 8, + "x": 16, + "y": 16 + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-10" + }, + "height": 8, + "width": 24, + "x": 0, + "y": 24 + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-11" + }, + "height": 8, + "width": 8, + "x": 0, + "y": 32 + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-12" + }, + "height": 8, + "width": 8, + "x": 8, + "y": 32 + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-13" + }, + "height": 8, + "width": 8, + "x": 16, + "y": 32 + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [ + "spring-boot", + "system-design" + ], + "timeSettings": { + "autoRefresh": "5s", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "fiscalYearStartMonth": 0, + "from": "now-15m", + "hideTimepicker": false, + "timezone": "browser", + "to": "now" + }, + "title": "Spring Boot Overview", + "variables": [] + } +} \ No newline at end of file diff --git a/monitoring/grafana/dashboards/spring-boot-overview.json b/monitoring/grafana/dashboards/spring-boot-overview.json index a3a06b7..53eac58 100644 --- a/monitoring/grafana/dashboards/spring-boot-overview.json +++ b/monitoring/grafana/dashboards/spring-boot-overview.json @@ -4,25 +4,23 @@ "metadata": { "name": "spring-boot-overview", "namespace": "default", - "uid": "d8ff4239-0696-483e-ac21-919126fd4967", - "resourceVersion": "1785489867760013", - "generation": 4, - "creationTimestamp": "2026-07-30T10:33:40Z", + "uid": "9f5e1718-e0de-4b16-82e0-fb1fdb5f47b9", + "resourceVersion": "1786000355142018", + "generation": 1, + "creationTimestamp": "2026-08-06T07:12:35Z", "labels": { - "grafana.app/deprecatedInternalID": "2113606437945344" + "grafana.app/deprecatedInternalID": "96116541288448" }, "annotations": { "grafana.app/createdBy": "access-policy:service", - "grafana.app/folder": "eftnup4o0j1tsd", + "grafana.app/folder": "cfu8feuqxi7lsb", "grafana.app/managedBy": "classic-file-provisioning", "grafana.app/managerId": "System Design Dashboards", - "grafana.app/sourceChecksum": "dd3f2ae3957746d44c447391e659a68b", + "grafana.app/sourceChecksum": "0737391e824d48cce503bc64fcc70134", "grafana.app/sourcePath": "/var/lib/grafana/dashboards/spring-boot-overview.json", - "grafana.app/sourceTimestamp": "1785489862000", - "grafana.app/updatedBy": "access-policy:service", - "grafana.app/updatedTimestamp": "2026-07-31T09:24:27Z", + "grafana.app/sourceTimestamp": "1786000349000", "grafana.app/folderTitle": "System Design", - "grafana.app/folderUrl": "/dashboards/f/eftnup4o0j1tsd/system-design" + "grafana.app/folderUrl": "/dashboards/f/cfu8feuqxi7lsb/system-design" } }, "spec": { @@ -716,6 +714,149 @@ } } }, + "panel-14": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "prometheus" + }, + "group": "prometheus", + "kind": "DataQuery", + "spec": { + "editorMode": "code", + "expr": "rate(\n short_url_cache_error_total{\n application=\"url-shortener\",\n operation=\"get\"\n }[$__rate_interval]\n)", + "legendFormat": "Redis GET Error/s", + "range": true + }, + "version": "v0" + }, + "refId": "A" + } + }, + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "prometheus" + }, + "group": "prometheus", + "kind": "DataQuery", + "spec": { + "editorMode": "code", + "expr": "rate(\n short_url_cache_fallback_total{\n application=\"url-shortener\"\n }[$__rate_interval]\n)", + "instant": false, + "legendFormat": "MySQL Fallback/s", + "range": true + }, + "version": "v0" + }, + "refId": "B" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 14, + "links": [], + "title": "Redis Error / Fallback", + "vizConfig": { + "group": "timeseries", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "options": { + "annotations": { + "clustering": -1, + "multiLane": false + }, + "legend": { + "calcs": [], + "displayMode": "list", + "enableFacetedFilter": false, + "overflow": "ellipsis", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + } + }, + "version": "13.1.1" + } + } + }, "panel-2": { "kind": "Panel", "spec": { @@ -1848,11 +1989,24 @@ "name": "panel-10" }, "height": 8, - "width": 24, + "width": 12, "x": 0, "y": 24 } }, + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-14" + }, + "height": 8, + "width": 12, + "x": 12, + "y": 24 + } + }, { "kind": "GridLayoutItem", "spec": { diff --git a/monitoring/grafana/provisioning/dashboards/dashboard.yml b/monitoring/grafana/provisioning/dashboards/dashboard.yml index 1d4785d..e79abc5 100644 --- a/monitoring/grafana/provisioning/dashboards/dashboard.yml +++ b/monitoring/grafana/provisioning/dashboards/dashboard.yml @@ -6,8 +6,8 @@ providers: folder: System Design type: file disableDeletion: false - updateIntervalSeconds: 10 + updateIntervalSeconds: 30 allowUiUpdates: false - options: - path: /var/lib/grafana/dashboards \ No newline at end of file + path: /var/lib/grafana/dashboards + foldersFromFilesStructure: false \ No newline at end of file diff --git a/results/resilience/metrics-before.txt b/results/resilience/metrics-before.txt new file mode 100644 index 0000000..4663af5 --- /dev/null +++ b/results/resilience/metrics-before.txt @@ -0,0 +1,16 @@ +# HELP short_url_cache_error_total Redis cache operation error count +# TYPE short_url_cache_error_total counter +short_url_cache_error_total{application="url-shortener",operation="get"} 17220.0 +short_url_cache_error_total{application="url-shortener",operation="set"} 0.0 +# HELP short_url_cache_fallback_total MySQL fallback count caused by Redis errors +# TYPE short_url_cache_fallback_total counter +short_url_cache_fallback_total{application="url-shortener"} 17220.0 +# HELP short_url_cache_hit_total Redis cache hit count +# TYPE short_url_cache_hit_total counter +short_url_cache_hit_total{application="url-shortener"} 393146.0 +# HELP short_url_cache_miss_total Redis cache miss count +# TYPE short_url_cache_miss_total counter +short_url_cache_miss_total{application="url-shortener"} 2.0 +# HELP short_url_db_lookup_total Short URL database lookup count +# TYPE short_url_db_lookup_total counter +short_url_db_lookup_total{application="url-shortener"} 17222.0 diff --git a/results/resilience/metrics-normal.txt b/results/resilience/metrics-normal.txt new file mode 100644 index 0000000..5eb0683 --- /dev/null +++ b/results/resilience/metrics-normal.txt @@ -0,0 +1,16 @@ +# HELP short_url_cache_error_total Redis cache operation error count +# TYPE short_url_cache_error_total counter +short_url_cache_error_total{application="url-shortener",operation="get"} 17221.0 +short_url_cache_error_total{application="url-shortener",operation="set"} 0.0 +# HELP short_url_cache_fallback_total MySQL fallback count caused by Redis errors +# TYPE short_url_cache_fallback_total counter +short_url_cache_fallback_total{application="url-shortener"} 17221.0 +# HELP short_url_cache_hit_total Redis cache hit count +# TYPE short_url_cache_hit_total counter +short_url_cache_hit_total{application="url-shortener"} 586851.0 +# HELP short_url_cache_miss_total Redis cache miss count +# TYPE short_url_cache_miss_total counter +short_url_cache_miss_total{application="url-shortener"} 2.0 +# HELP short_url_db_lookup_total Short URL database lookup count +# TYPE short_url_db_lookup_total counter +short_url_db_lookup_total{application="url-shortener"} 17223.0 diff --git a/results/resilience/metrics-outage.txt b/results/resilience/metrics-outage.txt new file mode 100644 index 0000000..087072f --- /dev/null +++ b/results/resilience/metrics-outage.txt @@ -0,0 +1,16 @@ +# HELP short_url_cache_error_total Redis cache operation error count +# TYPE short_url_cache_error_total counter +short_url_cache_error_total{application="url-shortener",operation="get"} 31067.0 +short_url_cache_error_total{application="url-shortener",operation="set"} 0.0 +# HELP short_url_cache_fallback_total MySQL fallback count caused by Redis errors +# TYPE short_url_cache_fallback_total counter +short_url_cache_fallback_total{application="url-shortener"} 31067.0 +# HELP short_url_cache_hit_total Redis cache hit count +# TYPE short_url_cache_hit_total counter +short_url_cache_hit_total{application="url-shortener"} 594712.0 +# HELP short_url_cache_miss_total Redis cache miss count +# TYPE short_url_cache_miss_total counter +short_url_cache_miss_total{application="url-shortener"} 2.0 +# HELP short_url_db_lookup_total Short URL database lookup count +# TYPE short_url_db_lookup_total counter +short_url_db_lookup_total{application="url-shortener"} 31069.0 diff --git a/results/resilience/metrics-recovered-30s.txt b/results/resilience/metrics-recovered-30s.txt new file mode 100644 index 0000000..61a7b50 --- /dev/null +++ b/results/resilience/metrics-recovered-30s.txt @@ -0,0 +1,16 @@ +# HELP short_url_cache_error_total Redis cache operation error count +# TYPE short_url_cache_error_total counter +short_url_cache_error_total{application="url-shortener",operation="get"} 34467.0 +short_url_cache_error_total{application="url-shortener",operation="set"} 0.0 +# HELP short_url_cache_fallback_total MySQL fallback count caused by Redis errors +# TYPE short_url_cache_fallback_total counter +short_url_cache_fallback_total{application="url-shortener"} 34467.0 +# HELP short_url_cache_hit_total Redis cache hit count +# TYPE short_url_cache_hit_total counter +short_url_cache_hit_total{application="url-shortener"} 745649.0 +# HELP short_url_cache_miss_total Redis cache miss count +# TYPE short_url_cache_miss_total counter +short_url_cache_miss_total{application="url-shortener"} 2.0 +# HELP short_url_db_lookup_total Short URL database lookup count +# TYPE short_url_db_lookup_total counter +short_url_db_lookup_total{application="url-shortener"} 34469.0 diff --git a/results/resilience/metrics-recovered-60s.txt b/results/resilience/metrics-recovered-60s.txt new file mode 100644 index 0000000..07e9052 --- /dev/null +++ b/results/resilience/metrics-recovered-60s.txt @@ -0,0 +1,16 @@ +# HELP short_url_cache_error_total Redis cache operation error count +# TYPE short_url_cache_error_total counter +short_url_cache_error_total{application="url-shortener",operation="get"} 34467.0 +short_url_cache_error_total{application="url-shortener",operation="set"} 0.0 +# HELP short_url_cache_fallback_total MySQL fallback count caused by Redis errors +# TYPE short_url_cache_fallback_total counter +short_url_cache_fallback_total{application="url-shortener"} 34467.0 +# HELP short_url_cache_hit_total Redis cache hit count +# TYPE short_url_cache_hit_total counter +short_url_cache_hit_total{application="url-shortener"} 1032522.0 +# HELP short_url_cache_miss_total Redis cache miss count +# TYPE short_url_cache_miss_total counter +short_url_cache_miss_total{application="url-shortener"} 2.0 +# HELP short_url_db_lookup_total Short URL database lookup count +# TYPE short_url_db_lookup_total counter +short_url_db_lookup_total{application="url-shortener"} 34469.0 diff --git a/results/resilience/redis-fallback-100vu.json b/results/resilience/redis-fallback-100vu.json new file mode 100644 index 0000000..503c7d8 --- /dev/null +++ b/results/resilience/redis-fallback-100vu.json @@ -0,0 +1,143 @@ +{ + "root_group": { + "path": "", + "id": "d41d8cd98f00b204e9800998ecf8427e", + "groups": {}, + "checks": { + "redirect status is 302": { + "name": "redirect status is 302", + "path": "::redirect status is 302", + "id": "d8a7b755aa6d54fb5eda54a6efb3e312", + "passes": 656623, + "fails": 0 + }, + "location header exists": { + "fails": 0, + "name": "location header exists", + "path": "::location header exists", + "id": "50ed0c1f734ebfa4cadbd3f1b25f92d5", + "passes": 656623 + } + }, + "name": "" + }, + "metrics": { + "iteration_duration": { + "avg": 18.256313034354186, + "min": 1.487417, + "med": 11.007875, + "max": 346.630333, + "p(90)": 22.155816600000016, + "p(95)": 32.2765253 + }, + "http_req_tls_handshaking": { + "min": 0, + "med": 0, + "max": 0, + "p(90)": 0, + "p(95)": 0, + "avg": 0 + }, + "http_req_sending": { + "med": 0.007, + "max": 67.309, + "p(90)": 0.017, + "p(95)": 0.029, + "avg": 0.0204981899809725, + "min": 0.003 + }, + "http_req_duration": { + "avg": 18.08845682377888, + "min": 1.388, + "med": 10.873, + "max": 345.437, + "p(90)": 21.892, + "p(95)": 31.919900000000023, + "thresholds": { + "p(95)<1000": false + } + }, + "http_req_waiting": { + "p(90)": 21.541, + "p(95)": 31.460900000000024, + "avg": 17.97280184824486, + "min": 1.284, + "med": 10.812, + "max": 345.399 + }, + "http_req_duration{expected_response:true}": { + "max": 345.437, + "p(90)": 21.892, + "p(95)": 31.919900000000023, + "avg": 18.08845682377888, + "min": 1.388, + "med": 10.873 + }, + "http_req_connecting": { + "p(95)": 0, + "avg": 0.017864107714776964, + "min": 0, + "med": 0, + "max": 92.721, + "p(90)": 0 + }, + "data_received": { + "count": 85484870, + "rate": 712301.0423341938 + }, + "data_sent": { + "count": 52529840, + "rate": 437703.88591160555 + }, + "checks": { + "passes": 1313246, + "fails": 0, + "thresholds": { + "rate>0.99": false + }, + "value": 1 + }, + "vus": { + "value": 100, + "min": 100, + "max": 100 + }, + "http_req_failed": { + "passes": 0, + "fails": 656623, + "thresholds": { + "rate<0.01": false + }, + "value": 0 + }, + "http_reqs": { + "count": 656623, + "rate": 5471.298573895069 + }, + "http_req_blocked": { + "med": 0.003, + "max": 92.799, + "p(90)": 0.005, + "p(95)": 0.006, + "avg": 0.023259827938061774, + "min": 0.001 + }, + "vus_max": { + "max": 100, + "value": 100, + "min": 100 + }, + "http_req_receiving": { + "p(90)": 0.065, + "p(95)": 0.106, + "avg": 0.09515678555246543, + "min": 0.011, + "med": 0.026, + "max": 70.616 + }, + "iterations": { + "count": 656623, + "rate": 5471.298573895069 + } + } +} \ No newline at end of file diff --git a/results/resilience/redis-fallback-100vu.log b/results/resilience/redis-fallback-100vu.log new file mode 100644 index 0000000..8da63a5 --- /dev/null +++ b/results/resilience/redis-fallback-100vu.log @@ -0,0 +1,419 @@ + + /\ Grafana /‾‾/ + /\ / \ |\ __ / / + / \/ \ | |/ / / ‾‾\ + / \ | ( | (‾) | + / __________ \ |_|\_\ \_____/ + + + execution: local + script: k6/redis-fallback-test.js + output: - + + scenarios: (100.00%) 1 scenario, 100 max VUs, 2m30s max duration (incl. graceful stop): + * default: 100 looping VUs for 2m0s (gracefulStop: 30s) + + +running (0m01.0s), 100/100 VUs, 2338 complete and 0 interrupted iterations +default [ 1% ] 100 VUs 0m01.0s/2m0s + +running (0m02.0s), 100/100 VUs, 6827 complete and 0 interrupted iterations +default [ 2% ] 100 VUs 0m02.0s/2m0s + +running (0m03.0s), 100/100 VUs, 12716 complete and 0 interrupted iterations +default [ 2% ] 100 VUs 0m03.0s/2m0s + +running (0m04.0s), 100/100 VUs, 19984 complete and 0 interrupted iterations +default [ 3% ] 100 VUs 0m04.0s/2m0s + +running (0m05.0s), 100/100 VUs, 26269 complete and 0 interrupted iterations +default [ 4% ] 100 VUs 0m05.0s/2m0s + +running (0m06.0s), 100/100 VUs, 33351 complete and 0 interrupted iterations +default [ 5% ] 100 VUs 0m06.0s/2m0s + +running (0m07.0s), 100/100 VUs, 41272 complete and 0 interrupted iterations +default [ 6% ] 100 VUs 0m07.0s/2m0s + +running (0m08.0s), 100/100 VUs, 50813 complete and 0 interrupted iterations +default [ 7% ] 100 VUs 0m08.0s/2m0s + +running (0m09.0s), 100/100 VUs, 60235 complete and 0 interrupted iterations +default [ 7% ] 100 VUs 0m09.0s/2m0s + +running (0m10.0s), 100/100 VUs, 69905 complete and 0 interrupted iterations +default [ 8% ] 100 VUs 0m10.0s/2m0s + +running (0m11.0s), 100/100 VUs, 79786 complete and 0 interrupted iterations +default [ 9% ] 100 VUs 0m11.0s/2m0s + +running (0m12.0s), 100/100 VUs, 89582 complete and 0 interrupted iterations +default [ 10% ] 100 VUs 0m12.0s/2m0s + +running (0m13.0s), 100/100 VUs, 95438 complete and 0 interrupted iterations +default [ 11% ] 100 VUs 0m13.0s/2m0s + +running (0m14.0s), 100/100 VUs, 104812 complete and 0 interrupted iterations +default [ 12% ] 100 VUs 0m14.0s/2m0s + +running (0m15.0s), 100/100 VUs, 112626 complete and 0 interrupted iterations +default [ 12% ] 100 VUs 0m15.0s/2m0s + +running (0m16.0s), 100/100 VUs, 116671 complete and 0 interrupted iterations +default [ 13% ] 100 VUs 0m16.0s/2m0s + +running (0m17.0s), 100/100 VUs, 120427 complete and 0 interrupted iterations +default [ 14% ] 100 VUs 0m17.0s/2m0s + +running (0m18.0s), 100/100 VUs, 127914 complete and 0 interrupted iterations +default [ 15% ] 100 VUs 0m18.0s/2m0s + +running (0m19.0s), 100/100 VUs, 131318 complete and 0 interrupted iterations +default [ 16% ] 100 VUs 0m19.0s/2m0s + +running (0m20.0s), 100/100 VUs, 136241 complete and 0 interrupted iterations +default [ 17% ] 100 VUs 0m20.0s/2m0s + +running (0m21.0s), 100/100 VUs, 138695 complete and 0 interrupted iterations +default [ 17% ] 100 VUs 0m21.0s/2m0s + +running (0m22.0s), 100/100 VUs, 145419 complete and 0 interrupted iterations +default [ 18% ] 100 VUs 0m22.0s/2m0s + +running (0m23.0s), 100/100 VUs, 152070 complete and 0 interrupted iterations +default [ 19% ] 100 VUs 0m23.0s/2m0s + +running (0m24.0s), 100/100 VUs, 156267 complete and 0 interrupted iterations +default [ 20% ] 100 VUs 0m24.0s/2m0s + +running (0m25.0s), 100/100 VUs, 161436 complete and 0 interrupted iterations +default [ 21% ] 100 VUs 0m25.0s/2m0s + +running (0m26.0s), 100/100 VUs, 168907 complete and 0 interrupted iterations +default [ 22% ] 100 VUs 0m26.0s/2m0s + +running (0m27.0s), 100/100 VUs, 173722 complete and 0 interrupted iterations +default [ 22% ] 100 VUs 0m27.0s/2m0s + +running (0m28.0s), 100/100 VUs, 179850 complete and 0 interrupted iterations +default [ 23% ] 100 VUs 0m28.0s/2m0s + +running (0m29.0s), 100/100 VUs, 187927 complete and 0 interrupted iterations +default [ 24% ] 100 VUs 0m29.0s/2m0s + +running (0m30.0s), 100/100 VUs, 194104 complete and 0 interrupted iterations +default [ 25% ] 100 VUs 0m30.0s/2m0s + +running (0m31.0s), 100/100 VUs, 201313 complete and 0 interrupted iterations +default [ 26% ] 100 VUs 0m31.0s/2m0s + +running (0m32.0s), 100/100 VUs, 201877 complete and 0 interrupted iterations +default [ 27% ] 100 VUs 0m32.0s/2m0s + +running (0m33.0s), 100/100 VUs, 202368 complete and 0 interrupted iterations +default [ 27% ] 100 VUs 0m33.0s/2m0s + +running (0m34.0s), 100/100 VUs, 202868 complete and 0 interrupted iterations +default [ 28% ] 100 VUs 0m34.0s/2m0s + +running (0m35.0s), 100/100 VUs, 203368 complete and 0 interrupted iterations +default [ 29% ] 100 VUs 0m35.0s/2m0s + +running (0m36.0s), 100/100 VUs, 203821 complete and 0 interrupted iterations +default [ 30% ] 100 VUs 0m36.0s/2m0s + +running (0m37.0s), 100/100 VUs, 204290 complete and 0 interrupted iterations +default [ 31% ] 100 VUs 0m37.0s/2m0s + +running (0m38.0s), 100/100 VUs, 204776 complete and 0 interrupted iterations +default [ 32% ] 100 VUs 0m38.0s/2m0s + +running (0m39.0s), 100/100 VUs, 205276 complete and 0 interrupted iterations +default [ 32% ] 100 VUs 0m39.0s/2m0s + +running (0m40.0s), 100/100 VUs, 205690 complete and 0 interrupted iterations +default [ 33% ] 100 VUs 0m40.0s/2m0s + +running (0m41.0s), 100/100 VUs, 206190 complete and 0 interrupted iterations +default [ 34% ] 100 VUs 0m41.0s/2m0s + +running (0m42.0s), 100/100 VUs, 206690 complete and 0 interrupted iterations +default [ 35% ] 100 VUs 0m42.0s/2m0s + +running (0m43.0s), 100/100 VUs, 207190 complete and 0 interrupted iterations +default [ 36% ] 100 VUs 0m43.0s/2m0s + +running (0m44.0s), 100/100 VUs, 207648 complete and 0 interrupted iterations +default [ 37% ] 100 VUs 0m44.0s/2m0s + +running (0m45.0s), 100/100 VUs, 208092 complete and 0 interrupted iterations +default [ 37% ] 100 VUs 0m45.0s/2m0s + +running (0m46.0s), 100/100 VUs, 208590 complete and 0 interrupted iterations +default [ 38% ] 100 VUs 0m46.0s/2m0s + +running (0m47.0s), 100/100 VUs, 209090 complete and 0 interrupted iterations +default [ 39% ] 100 VUs 0m47.0s/2m0s + +running (0m48.0s), 100/100 VUs, 209498 complete and 0 interrupted iterations +default [ 40% ] 100 VUs 0m48.0s/2m0s + +running (0m49.0s), 100/100 VUs, 209998 complete and 0 interrupted iterations +default [ 41% ] 100 VUs 0m49.0s/2m0s + +running (0m50.0s), 100/100 VUs, 210497 complete and 0 interrupted iterations +default [ 42% ] 100 VUs 0m50.0s/2m0s + +running (0m51.0s), 100/100 VUs, 210997 complete and 0 interrupted iterations +default [ 42% ] 100 VUs 0m51.0s/2m0s + +running (0m52.0s), 100/100 VUs, 211412 complete and 0 interrupted iterations +default [ 43% ] 100 VUs 0m52.0s/2m0s + +running (0m53.0s), 100/100 VUs, 211900 complete and 0 interrupted iterations +default [ 44% ] 100 VUs 0m53.0s/2m0s + +running (0m54.0s), 100/100 VUs, 212400 complete and 0 interrupted iterations +default [ 45% ] 100 VUs 0m54.0s/2m0s + +running (0m55.0s), 100/100 VUs, 212898 complete and 0 interrupted iterations +default [ 46% ] 100 VUs 0m55.0s/2m0s + +running (0m56.0s), 100/100 VUs, 213313 complete and 0 interrupted iterations +default [ 47% ] 100 VUs 0m56.0s/2m0s + +running (0m57.0s), 100/100 VUs, 213813 complete and 0 interrupted iterations +default [ 47% ] 100 VUs 0m57.0s/2m0s + +running (0m58.0s), 100/100 VUs, 214300 complete and 0 interrupted iterations +default [ 48% ] 100 VUs 0m58.0s/2m0s + +running (0m59.0s), 100/100 VUs, 214713 complete and 0 interrupted iterations +default [ 49% ] 100 VUs 0m59.0s/2m0s + +running (1m00.0s), 100/100 VUs, 215213 complete and 0 interrupted iterations +default [ 50% ] 100 VUs 1m00.0s/2m0s + +running (1m01.0s), 100/100 VUs, 215713 complete and 0 interrupted iterations +default [ 51% ] 100 VUs 1m01.0s/2m0s + +running (1m02.0s), 100/100 VUs, 216213 complete and 0 interrupted iterations +default [ 52% ] 100 VUs 1m02.0s/2m0s + +running (1m03.0s), 100/100 VUs, 216613 complete and 0 interrupted iterations +default [ 52% ] 100 VUs 1m03.0s/2m0s + +running (1m04.0s), 100/100 VUs, 217113 complete and 0 interrupted iterations +default [ 53% ] 100 VUs 1m04.0s/2m0s + +running (1m05.0s), 100/100 VUs, 217613 complete and 0 interrupted iterations +default [ 54% ] 100 VUs 1m05.0s/2m0s + +running (1m06.0s), 100/100 VUs, 218013 complete and 0 interrupted iterations +default [ 55% ] 100 VUs 1m06.0s/2m0s + +running (1m07.0s), 100/100 VUs, 218513 complete and 0 interrupted iterations +default [ 56% ] 100 VUs 1m07.0s/2m0s + +running (1m08.0s), 100/100 VUs, 223151 complete and 0 interrupted iterations +default [ 57% ] 100 VUs 1m08.0s/2m0s + +running (1m09.0s), 100/100 VUs, 229501 complete and 0 interrupted iterations +default [ 57% ] 100 VUs 1m09.0s/2m0s + +running (1m10.0s), 100/100 VUs, 231441 complete and 0 interrupted iterations +default [ 58% ] 100 VUs 1m10.0s/2m0s + +running (1m11.0s), 100/100 VUs, 237495 complete and 0 interrupted iterations +default [ 59% ] 100 VUs 1m11.0s/2m0s + +running (1m12.0s), 100/100 VUs, 243081 complete and 0 interrupted iterations +default [ 60% ] 100 VUs 1m12.0s/2m0s + +running (1m13.0s), 100/100 VUs, 249088 complete and 0 interrupted iterations +default [ 61% ] 100 VUs 1m13.0s/2m0s + +running (1m14.0s), 100/100 VUs, 256464 complete and 0 interrupted iterations +default [ 62% ] 100 VUs 1m14.0s/2m0s + +running (1m15.0s), 100/100 VUs, 259183 complete and 0 interrupted iterations +default [ 62% ] 100 VUs 1m15.0s/2m0s + +running (1m16.0s), 100/100 VUs, 264295 complete and 0 interrupted iterations +default [ 63% ] 100 VUs 1m16.0s/2m0s + +running (1m17.0s), 100/100 VUs, 267956 complete and 0 interrupted iterations +default [ 64% ] 100 VUs 1m17.0s/2m0s + +running (1m18.0s), 100/100 VUs, 274130 complete and 0 interrupted iterations +default [ 65% ] 100 VUs 1m18.0s/2m0s + +running (1m19.0s), 100/100 VUs, 282165 complete and 0 interrupted iterations +default [ 66% ] 100 VUs 1m19.0s/2m0s + +running (1m20.0s), 100/100 VUs, 286467 complete and 0 interrupted iterations +default [ 67% ] 100 VUs 1m20.0s/2m0s + +running (1m21.0s), 100/100 VUs, 292243 complete and 0 interrupted iterations +default [ 67% ] 100 VUs 1m21.0s/2m0s + +running (1m22.0s), 100/100 VUs, 300166 complete and 0 interrupted iterations +default [ 68% ] 100 VUs 1m22.0s/2m0s + +running (1m23.0s), 100/100 VUs, 309054 complete and 0 interrupted iterations +default [ 69% ] 100 VUs 1m23.0s/2m0s + +running (1m24.0s), 100/100 VUs, 317861 complete and 0 interrupted iterations +default [ 70% ] 100 VUs 1m24.0s/2m0s + +running (1m25.0s), 100/100 VUs, 323553 complete and 0 interrupted iterations +default [ 71% ] 100 VUs 1m25.0s/2m0s + +running (1m26.0s), 100/100 VUs, 332443 complete and 0 interrupted iterations +default [ 72% ] 100 VUs 1m26.0s/2m0s + +running (1m27.0s), 100/100 VUs, 340687 complete and 0 interrupted iterations +default [ 72% ] 100 VUs 1m27.0s/2m0s + +running (1m28.0s), 100/100 VUs, 349795 complete and 0 interrupted iterations +default [ 73% ] 100 VUs 1m28.0s/2m0s + +running (1m29.0s), 100/100 VUs, 359325 complete and 0 interrupted iterations +default [ 74% ] 100 VUs 1m29.0s/2m0s + +running (1m30.0s), 100/100 VUs, 368729 complete and 0 interrupted iterations +default [ 75% ] 100 VUs 1m30.0s/2m0s + +running (1m31.0s), 100/100 VUs, 378313 complete and 0 interrupted iterations +default [ 76% ] 100 VUs 1m31.0s/2m0s + +running (1m32.0s), 100/100 VUs, 386694 complete and 0 interrupted iterations +default [ 77% ] 100 VUs 1m32.0s/2m0s + +running (1m33.0s), 100/100 VUs, 396310 complete and 0 interrupted iterations +default [ 77% ] 100 VUs 1m33.0s/2m0s + +running (1m34.0s), 100/100 VUs, 406021 complete and 0 interrupted iterations +default [ 78% ] 100 VUs 1m34.0s/2m0s + +running (1m35.0s), 100/100 VUs, 415759 complete and 0 interrupted iterations +default [ 79% ] 100 VUs 1m35.0s/2m0s + +running (1m36.0s), 100/100 VUs, 424918 complete and 0 interrupted iterations +default [ 80% ] 100 VUs 1m36.0s/2m0s + +running (1m37.0s), 100/100 VUs, 433654 complete and 0 interrupted iterations +default [ 81% ] 100 VUs 1m37.0s/2m0s + +running (1m38.0s), 100/100 VUs, 443620 complete and 0 interrupted iterations +default [ 82% ] 100 VUs 1m38.0s/2m0s + +running (1m39.0s), 100/100 VUs, 453797 complete and 0 interrupted iterations +default [ 82% ] 100 VUs 1m39.0s/2m0s + +running (1m40.0s), 100/100 VUs, 461272 complete and 0 interrupted iterations +default [ 83% ] 100 VUs 1m40.0s/2m0s + +running (1m41.0s), 100/100 VUs, 470820 complete and 0 interrupted iterations +default [ 84% ] 100 VUs 1m41.0s/2m0s + +running (1m42.0s), 100/100 VUs, 480703 complete and 0 interrupted iterations +default [ 85% ] 100 VUs 1m42.0s/2m0s + +running (1m43.0s), 100/100 VUs, 490565 complete and 0 interrupted iterations +default [ 86% ] 100 VUs 1m43.0s/2m0s + +running (1m44.0s), 100/100 VUs, 500522 complete and 0 interrupted iterations +default [ 87% ] 100 VUs 1m44.0s/2m0s + +running (1m45.0s), 100/100 VUs, 509861 complete and 0 interrupted iterations +default [ 87% ] 100 VUs 1m45.0s/2m0s + +running (1m46.0s), 100/100 VUs, 519456 complete and 0 interrupted iterations +default [ 88% ] 100 VUs 1m46.0s/2m0s + +running (1m47.0s), 100/100 VUs, 528988 complete and 0 interrupted iterations +default [ 89% ] 100 VUs 1m47.0s/2m0s + +running (1m48.0s), 100/100 VUs, 539038 complete and 0 interrupted iterations +default [ 90% ] 100 VUs 1m48.0s/2m0s + +running (1m49.0s), 100/100 VUs, 548620 complete and 0 interrupted iterations +default [ 91% ] 100 VUs 1m49.0s/2m0s + +running (1m50.0s), 100/100 VUs, 558501 complete and 0 interrupted iterations +default [ 92% ] 100 VUs 1m50.0s/2m0s + +running (1m51.0s), 100/100 VUs, 568163 complete and 0 interrupted iterations +default [ 92% ] 100 VUs 1m51.0s/2m0s + +running (1m52.0s), 100/100 VUs, 577959 complete and 0 interrupted iterations +default [ 93% ] 100 VUs 1m52.0s/2m0s + +running (1m53.0s), 100/100 VUs, 587670 complete and 0 interrupted iterations +default [ 94% ] 100 VUs 1m53.0s/2m0s + +running (1m54.0s), 100/100 VUs, 597242 complete and 0 interrupted iterations +default [ 95% ] 100 VUs 1m54.0s/2m0s + +running (1m55.0s), 100/100 VUs, 607570 complete and 0 interrupted iterations +default [ 96% ] 100 VUs 1m55.0s/2m0s + +running (1m56.0s), 100/100 VUs, 616793 complete and 0 interrupted iterations +default [ 97% ] 100 VUs 1m56.0s/2m0s + +running (1m57.0s), 100/100 VUs, 626501 complete and 0 interrupted iterations +default [ 97% ] 100 VUs 1m57.0s/2m0s + +running (1m58.0s), 100/100 VUs, 636544 complete and 0 interrupted iterations +default [ 98% ] 100 VUs 1m58.0s/2m0s + +running (1m59.0s), 100/100 VUs, 646266 complete and 0 interrupted iterations +default [ 99% ] 100 VUs 1m59.0s/2m0s + +running (2m00.0s), 100/100 VUs, 656112 complete and 0 interrupted iterations +default [ 100% ] 100 VUs 2m00.0s/2m0s + + + █ THRESHOLDS + + checks + ✓ 'rate>0.99' rate=100.00% + + http_req_duration + ✓ 'p(95)<1000' p(95)=31.91ms + + http_req_failed + ✓ 'rate<0.01' rate=0.00% + + + █ TOTAL RESULTS + + checks_total.......: 1313246 10942.597148/s + checks_succeeded...: 100.00% 1313246 out of 1313246 + checks_failed......: 0.00% 0 out of 1313246 + + ✓ redirect status is 302 + ✓ location header exists + + HTTP + http_req_duration..............: avg=18.08ms min=1.38ms med=10.87ms max=345.43ms p(90)=21.89ms p(95)=31.91ms + { expected_response:true }...: avg=18.08ms min=1.38ms med=10.87ms max=345.43ms p(90)=21.89ms p(95)=31.91ms + http_req_failed................: 0.00% 0 out of 656623 + http_reqs......................: 656623 5471.298574/s + + EXECUTION + iteration_duration.............: avg=18.25ms min=1.48ms med=11ms max=346.63ms p(90)=22.15ms p(95)=32.27ms + iterations.....................: 656623 5471.298574/s + vus............................: 100 min=100 max=100 + vus_max........................: 100 min=100 max=100 + + NETWORK + data_received..................: 86 MB 712 kB/s + data_sent......................: 53 MB 438 kB/s + + + + +running (2m00.0s), 000/100 VUs, 656623 complete and 0 interrupted iterations +default ✓ [ 100% ] 100 VUs 2m0s diff --git a/results/resilience/redis-fallback-timeline.txt b/results/resilience/redis-fallback-timeline.txt new file mode 100644 index 0000000..78020fb --- /dev/null +++ b/results/resilience/redis-fallback-timeline.txt @@ -0,0 +1,8 @@ +test_start=2026-08-06T16:30:14+09:00 +short_code=pV0 +vus=100 +duration=120s +redis_stopped=2026-08-06T16:30:44+09:00 +redis_started=2026-08-06T16:31:14+09:00 +redis_ready=2026-08-06T16:31:15+09:00 +test_end=2026-08-06T16:32:15+09:00 diff --git a/results/resilience/run1/metrics-before.txt b/results/resilience/run1/metrics-before.txt new file mode 100644 index 0000000..bda77aa --- /dev/null +++ b/results/resilience/run1/metrics-before.txt @@ -0,0 +1,16 @@ +# HELP short_url_cache_error_total Redis cache operation error count +# TYPE short_url_cache_error_total counter +short_url_cache_error_total{application="url-shortener",operation="get"} 0.0 +short_url_cache_error_total{application="url-shortener",operation="set"} 0.0 +# HELP short_url_cache_fallback_total MySQL fallback count caused by Redis errors +# TYPE short_url_cache_fallback_total counter +short_url_cache_fallback_total{application="url-shortener"} 0.0 +# HELP short_url_cache_hit_total Redis cache hit count +# TYPE short_url_cache_hit_total counter +short_url_cache_hit_total{application="url-shortener"} 0.0 +# HELP short_url_cache_miss_total Redis cache miss count +# TYPE short_url_cache_miss_total counter +short_url_cache_miss_total{application="url-shortener"} 1.0 +# HELP short_url_db_lookup_total Short URL database lookup count +# TYPE short_url_db_lookup_total counter +short_url_db_lookup_total{application="url-shortener"} 1.0 diff --git a/results/resilience/run1/metrics-normal.txt b/results/resilience/run1/metrics-normal.txt new file mode 100644 index 0000000..0806851 --- /dev/null +++ b/results/resilience/run1/metrics-normal.txt @@ -0,0 +1,16 @@ +# HELP short_url_cache_error_total Redis cache operation error count +# TYPE short_url_cache_error_total counter +short_url_cache_error_total{application="url-shortener",operation="get"} 0.0 +short_url_cache_error_total{application="url-shortener",operation="set"} 0.0 +# HELP short_url_cache_fallback_total MySQL fallback count caused by Redis errors +# TYPE short_url_cache_fallback_total counter +short_url_cache_fallback_total{application="url-shortener"} 0.0 +# HELP short_url_cache_hit_total Redis cache hit count +# TYPE short_url_cache_hit_total counter +short_url_cache_hit_total{application="url-shortener"} 184967.0 +# HELP short_url_cache_miss_total Redis cache miss count +# TYPE short_url_cache_miss_total counter +short_url_cache_miss_total{application="url-shortener"} 1.0 +# HELP short_url_db_lookup_total Short URL database lookup count +# TYPE short_url_db_lookup_total counter +short_url_db_lookup_total{application="url-shortener"} 1.0 diff --git a/results/resilience/run1/metrics-outage.txt b/results/resilience/run1/metrics-outage.txt new file mode 100644 index 0000000..bf647db --- /dev/null +++ b/results/resilience/run1/metrics-outage.txt @@ -0,0 +1,16 @@ +# HELP short_url_cache_error_total Redis cache operation error count +# TYPE short_url_cache_error_total counter +short_url_cache_error_total{application="url-shortener",operation="get"} 13820.0 +short_url_cache_error_total{application="url-shortener",operation="set"} 0.0 +# HELP short_url_cache_fallback_total MySQL fallback count caused by Redis errors +# TYPE short_url_cache_fallback_total counter +short_url_cache_fallback_total{application="url-shortener"} 13820.0 +# HELP short_url_cache_hit_total Redis cache hit count +# TYPE short_url_cache_hit_total counter +short_url_cache_hit_total{application="url-shortener"} 190745.0 +# HELP short_url_cache_miss_total Redis cache miss count +# TYPE short_url_cache_miss_total counter +short_url_cache_miss_total{application="url-shortener"} 1.0 +# HELP short_url_db_lookup_total Short URL database lookup count +# TYPE short_url_db_lookup_total counter +short_url_db_lookup_total{application="url-shortener"} 13821.0 diff --git a/results/resilience/run1/metrics-recovered.txt b/results/resilience/run1/metrics-recovered.txt new file mode 100644 index 0000000..f543af7 --- /dev/null +++ b/results/resilience/run1/metrics-recovered.txt @@ -0,0 +1,16 @@ +# HELP short_url_cache_error_total Redis cache operation error count +# TYPE short_url_cache_error_total counter +short_url_cache_error_total{application="url-shortener",operation="get"} 17220.0 +short_url_cache_error_total{application="url-shortener",operation="set"} 0.0 +# HELP short_url_cache_fallback_total MySQL fallback count caused by Redis errors +# TYPE short_url_cache_fallback_total counter +short_url_cache_fallback_total{application="url-shortener"} 17220.0 +# HELP short_url_cache_hit_total Redis cache hit count +# TYPE short_url_cache_hit_total counter +short_url_cache_hit_total{application="url-shortener"} 384004.0 +# HELP short_url_cache_miss_total Redis cache miss count +# TYPE short_url_cache_miss_total counter +short_url_cache_miss_total{application="url-shortener"} 1.0 +# HELP short_url_db_lookup_total Short URL database lookup count +# TYPE short_url_db_lookup_total counter +short_url_db_lookup_total{application="url-shortener"} 17221.0 diff --git a/results/resilience/run1/redis-fallback-100vu.json b/results/resilience/run1/redis-fallback-100vu.json new file mode 100644 index 0000000..19055b1 --- /dev/null +++ b/results/resilience/run1/redis-fallback-100vu.json @@ -0,0 +1,143 @@ +{ + "root_group": { + "name": "", + "path": "", + "id": "d41d8cd98f00b204e9800998ecf8427e", + "groups": {}, + "checks": { + "redirect status is 302": { + "name": "redirect status is 302", + "path": "::redirect status is 302", + "id": "d8a7b755aa6d54fb5eda54a6efb3e312", + "passes": 410366, + "fails": 0 + }, + "location header exists": { + "path": "::location header exists", + "id": "50ed0c1f734ebfa4cadbd3f1b25f92d5", + "passes": 410366, + "fails": 0, + "name": "location header exists" + } + } + }, + "metrics": { + "iteration_duration": { + "max": 303.374333, + "p(90)": 25.440124500000003, + "p(95)": 49.41790575, + "avg": 21.912812892949017, + "min": 1.093875, + "med": 11.6351875 + }, + "http_req_blocked": { + "avg": 0.021735170555146476, + "min": 0.001, + "med": 0.003, + "max": 24.425, + "p(90)": 0.005, + "p(95)": 0.006 + }, + "http_reqs": { + "count": 410366, + "rate": 4559.0454523164435 + }, + "http_req_connecting": { + "avg": 0.016379363787448267, + "min": 0, + "med": 0, + "max": 18.118, + "p(90)": 0, + "p(95)": 0 + }, + "http_req_waiting": { + "avg": 21.649055691747744, + "min": 0.946, + "med": 11.431, + "max": 303.148, + "p(90)": 24.86, + "p(95)": 48.60325 + }, + "iterations": { + "count": 410366, + "rate": 4559.0454523164435 + }, + "http_req_tls_handshaking": { + "p(90)": 0, + "p(95)": 0, + "avg": 0, + "min": 0, + "med": 0, + "max": 0 + }, + "data_sent": { + "count": 32829280, + "rate": 364723.6361853155 + }, + "data_received": { + "count": 53424625, + "rate": 593531.8560698532 + }, + "vus_max": { + "value": 100, + "min": 100, + "max": 100 + }, + "http_req_duration": { + "max": 303.275, + "p(90)": 25.194, + "p(95)": 49.1095, + "avg": 21.75433420410081, + "min": 0.99, + "med": 11.494, + "thresholds": { + "p(95)<1000": false + } + }, + "vus": { + "value": 100, + "min": 100, + "max": 100 + }, + "http_req_receiving": { + "p(90)": 0.064, + "p(95)": 0.101, + "avg": 0.08629486117273392, + "min": 0.012, + "med": 0.027, + "max": 43.378 + }, + "checks": { + "passes": 820732, + "fails": 0, + "thresholds": { + "rate>0.99": false + }, + "value": 1 + }, + "http_req_failed": { + "passes": 0, + "fails": 410366, + "thresholds": { + "rate<0.01": false + }, + "value": 0 + }, + "http_req_duration{expected_response:true}": { + "min": 0.99, + "med": 11.494, + "max": 303.275, + "p(90)": 25.194, + "p(95)": 49.1095, + "avg": 21.75433420410081 + }, + "http_req_sending": { + "avg": 0.018983651179661293, + "min": 0.003, + "med": 0.007, + "max": 31.224, + "p(90)": 0.017, + "p(95)": 0.027 + } + } +} \ No newline at end of file diff --git a/results/resilience/run1/redis-fallback-100vu.log b/results/resilience/run1/redis-fallback-100vu.log new file mode 100644 index 0000000..95b66ac --- /dev/null +++ b/results/resilience/run1/redis-fallback-100vu.log @@ -0,0 +1,329 @@ + + /\ Grafana /‾‾/ + /\ / \ |\ __ / / + / \/ \ | |/ / / ‾‾\ + / \ | ( | (‾) | + / __________ \ |_|\_\ \_____/ + + + execution: local + script: k6/redis-fallback-test.js + output: - + + scenarios: (100.00%) 1 scenario, 100 max VUs, 2m0s max duration (incl. graceful stop): + * default: 100 looping VUs for 1m30s (gracefulStop: 30s) + + +running (0m01.0s), 100/100 VUs, 1464 complete and 0 interrupted iterations +default [ 1% ] 100 VUs 0m01.0s/1m30s + +running (0m02.0s), 100/100 VUs, 4102 complete and 0 interrupted iterations +default [ 2% ] 100 VUs 0m02.0s/1m30s + +running (0m03.0s), 100/100 VUs, 7304 complete and 0 interrupted iterations +default [ 3% ] 100 VUs 0m03.0s/1m30s + +running (0m04.0s), 100/100 VUs, 11318 complete and 0 interrupted iterations +default [ 4% ] 100 VUs 0m04.0s/1m30s + +running (0m05.0s), 100/100 VUs, 15705 complete and 0 interrupted iterations +default [ 6% ] 100 VUs 0m05.0s/1m30s + +running (0m06.0s), 100/100 VUs, 19805 complete and 0 interrupted iterations +default [ 7% ] 100 VUs 0m06.0s/1m30s + +running (0m07.0s), 100/100 VUs, 24179 complete and 0 interrupted iterations +default [ 8% ] 100 VUs 0m07.0s/1m30s + +running (0m08.0s), 100/100 VUs, 29309 complete and 0 interrupted iterations +default [ 9% ] 100 VUs 0m08.0s/1m30s + +running (0m09.0s), 100/100 VUs, 34301 complete and 0 interrupted iterations +default [ 10% ] 100 VUs 0m09.0s/1m30s + +running (0m10.0s), 100/100 VUs, 39415 complete and 0 interrupted iterations +default [ 11% ] 100 VUs 0m10.0s/1m30s + +running (0m11.0s), 100/100 VUs, 45021 complete and 0 interrupted iterations +default [ 12% ] 100 VUs 0m11.0s/1m30s + +running (0m12.0s), 100/100 VUs, 50605 complete and 0 interrupted iterations +default [ 13% ] 100 VUs 0m12.0s/1m30s + +running (0m13.0s), 100/100 VUs, 57234 complete and 0 interrupted iterations +default [ 14% ] 100 VUs 0m13.0s/1m30s + +running (0m14.0s), 100/100 VUs, 63132 complete and 0 interrupted iterations +default [ 16% ] 100 VUs 0m14.0s/1m30s + +running (0m15.0s), 100/100 VUs, 69519 complete and 0 interrupted iterations +default [ 17% ] 100 VUs 0m15.0s/1m30s + +running (0m16.0s), 100/100 VUs, 76917 complete and 0 interrupted iterations +default [ 18% ] 100 VUs 0m16.0s/1m30s + +running (0m17.0s), 100/100 VUs, 84587 complete and 0 interrupted iterations +default [ 19% ] 100 VUs 0m17.0s/1m30s + +running (0m18.0s), 100/100 VUs, 91603 complete and 0 interrupted iterations +default [ 20% ] 100 VUs 0m18.0s/1m30s + +running (0m19.0s), 100/100 VUs, 98764 complete and 0 interrupted iterations +default [ 21% ] 100 VUs 0m19.0s/1m30s + +running (0m20.0s), 100/100 VUs, 105341 complete and 0 interrupted iterations +default [ 22% ] 100 VUs 0m20.0s/1m30s + +running (0m21.0s), 100/100 VUs, 114229 complete and 0 interrupted iterations +default [ 23% ] 100 VUs 0m21.0s/1m30s + +running (0m22.0s), 100/100 VUs, 122964 complete and 0 interrupted iterations +default [ 24% ] 100 VUs 0m22.0s/1m30s + +running (0m23.0s), 100/100 VUs, 129914 complete and 0 interrupted iterations +default [ 26% ] 100 VUs 0m23.0s/1m30s + +running (0m24.0s), 100/100 VUs, 138838 complete and 0 interrupted iterations +default [ 27% ] 100 VUs 0m24.0s/1m30s + +running (0m25.0s), 100/100 VUs, 147401 complete and 0 interrupted iterations +default [ 28% ] 100 VUs 0m25.0s/1m30s + +running (0m26.0s), 100/100 VUs, 155341 complete and 0 interrupted iterations +default [ 29% ] 100 VUs 0m26.0s/1m30s + +running (0m27.0s), 100/100 VUs, 164613 complete and 0 interrupted iterations +default [ 30% ] 100 VUs 0m27.0s/1m30s + +running (0m28.0s), 100/100 VUs, 173602 complete and 0 interrupted iterations +default [ 31% ] 100 VUs 0m28.0s/1m30s + +running (0m29.0s), 100/100 VUs, 183056 complete and 0 interrupted iterations +default [ 32% ] 100 VUs 0m29.0s/1m30s + +running (0m30.0s), 100/100 VUs, 190745 complete and 0 interrupted iterations +default [ 33% ] 100 VUs 0m30.0s/1m30s + +running (0m31.0s), 100/100 VUs, 191145 complete and 0 interrupted iterations +default [ 34% ] 100 VUs 0m31.0s/1m30s + +running (0m32.0s), 100/100 VUs, 191645 complete and 0 interrupted iterations +default [ 36% ] 100 VUs 0m32.0s/1m30s + +running (0m33.0s), 100/100 VUs, 192053 complete and 0 interrupted iterations +default [ 37% ] 100 VUs 0m33.0s/1m30s + +running (0m34.0s), 100/100 VUs, 192545 complete and 0 interrupted iterations +default [ 38% ] 100 VUs 0m34.0s/1m30s + +running (0m35.0s), 100/100 VUs, 193045 complete and 0 interrupted iterations +default [ 39% ] 100 VUs 0m35.0s/1m30s + +running (0m36.0s), 100/100 VUs, 193545 complete and 0 interrupted iterations +default [ 40% ] 100 VUs 0m36.0s/1m30s + +running (0m37.0s), 100/100 VUs, 193947 complete and 0 interrupted iterations +default [ 41% ] 100 VUs 0m37.0s/1m30s + +running (0m38.0s), 100/100 VUs, 194447 complete and 0 interrupted iterations +default [ 42% ] 100 VUs 0m38.0s/1m30s + +running (0m39.0s), 100/100 VUs, 194947 complete and 0 interrupted iterations +default [ 43% ] 100 VUs 0m39.0s/1m30s + +running (0m40.0s), 100/100 VUs, 195420 complete and 0 interrupted iterations +default [ 44% ] 100 VUs 0m40.0s/1m30s + +running (0m41.0s), 100/100 VUs, 195848 complete and 0 interrupted iterations +default [ 46% ] 100 VUs 0m41.0s/1m30s + +running (0m42.0s), 100/100 VUs, 196347 complete and 0 interrupted iterations +default [ 47% ] 100 VUs 0m42.0s/1m30s + +running (0m43.0s), 100/100 VUs, 196847 complete and 0 interrupted iterations +default [ 48% ] 100 VUs 0m43.0s/1m30s + +running (0m44.0s), 100/100 VUs, 197347 complete and 0 interrupted iterations +default [ 49% ] 100 VUs 0m44.0s/1m30s + +running (0m45.0s), 100/100 VUs, 197847 complete and 0 interrupted iterations +default [ 50% ] 100 VUs 0m45.0s/1m30s + +running (0m46.0s), 100/100 VUs, 198261 complete and 0 interrupted iterations +default [ 51% ] 100 VUs 0m46.0s/1m30s + +running (0m47.0s), 100/100 VUs, 198750 complete and 0 interrupted iterations +default [ 52% ] 100 VUs 0m47.0s/1m30s + +running (0m48.0s), 100/100 VUs, 199250 complete and 0 interrupted iterations +default [ 53% ] 100 VUs 0m48.0s/1m30s + +running (0m49.0s), 100/100 VUs, 199750 complete and 0 interrupted iterations +default [ 54% ] 100 VUs 0m49.0s/1m30s + +running (0m50.0s), 100/100 VUs, 200250 complete and 0 interrupted iterations +default [ 56% ] 100 VUs 0m50.0s/1m30s + +running (0m51.0s), 100/100 VUs, 200747 complete and 0 interrupted iterations +default [ 57% ] 100 VUs 0m51.0s/1m30s + +running (0m52.0s), 100/100 VUs, 201150 complete and 0 interrupted iterations +default [ 58% ] 100 VUs 0m52.0s/1m30s + +running (0m53.0s), 100/100 VUs, 201650 complete and 0 interrupted iterations +default [ 59% ] 100 VUs 0m53.0s/1m30s + +running (0m54.0s), 100/100 VUs, 202150 complete and 0 interrupted iterations +default [ 60% ] 100 VUs 0m54.0s/1m30s + +running (0m55.0s), 100/100 VUs, 202565 complete and 0 interrupted iterations +default [ 61% ] 100 VUs 0m55.0s/1m30s + +running (0m56.0s), 100/100 VUs, 203065 complete and 0 interrupted iterations +default [ 62% ] 100 VUs 0m56.0s/1m30s + +running (0m57.0s), 100/100 VUs, 203550 complete and 0 interrupted iterations +default [ 63% ] 100 VUs 0m57.0s/1m30s + +running (0m58.0s), 100/100 VUs, 204050 complete and 0 interrupted iterations +default [ 64% ] 100 VUs 0m58.0s/1m30s + +running (0m59.0s), 100/100 VUs, 204465 complete and 0 interrupted iterations +default [ 66% ] 100 VUs 0m59.0s/1m30s + +running (1m00.0s), 100/100 VUs, 204965 complete and 0 interrupted iterations +default [ 67% ] 100 VUs 1m00.0s/1m30s + +running (1m01.0s), 100/100 VUs, 205465 complete and 0 interrupted iterations +default [ 68% ] 100 VUs 1m01.0s/1m30s + +running (1m02.0s), 100/100 VUs, 205914 complete and 0 interrupted iterations +default [ 69% ] 100 VUs 1m02.0s/1m30s + +running (1m03.0s), 100/100 VUs, 206365 complete and 0 interrupted iterations +default [ 70% ] 100 VUs 1m03.0s/1m30s + +running (1m04.0s), 100/100 VUs, 206865 complete and 0 interrupted iterations +default [ 71% ] 100 VUs 1m04.0s/1m30s + +running (1m05.0s), 100/100 VUs, 207365 complete and 0 interrupted iterations +default [ 72% ] 100 VUs 1m05.0s/1m30s + +running (1m06.0s), 100/100 VUs, 207765 complete and 0 interrupted iterations +default [ 73% ] 100 VUs 1m06.0s/1m30s + +running (1m07.0s), 100/100 VUs, 212112 complete and 0 interrupted iterations +default [ 74% ] 100 VUs 1m07.0s/1m30s + +running (1m08.0s), 100/100 VUs, 217932 complete and 0 interrupted iterations +default [ 76% ] 100 VUs 1m08.0s/1m30s + +running (1m09.0s), 100/100 VUs, 226922 complete and 0 interrupted iterations +default [ 77% ] 100 VUs 1m09.0s/1m30s + +running (1m10.0s), 100/100 VUs, 235342 complete and 0 interrupted iterations +default [ 78% ] 100 VUs 1m10.0s/1m30s + +running (1m11.0s), 100/100 VUs, 244553 complete and 0 interrupted iterations +default [ 79% ] 100 VUs 1m11.0s/1m30s + +running (1m12.0s), 100/100 VUs, 253357 complete and 0 interrupted iterations +default [ 80% ] 100 VUs 1m12.0s/1m30s + +running (1m13.0s), 100/100 VUs, 262612 complete and 0 interrupted iterations +default [ 81% ] 100 VUs 1m13.0s/1m30s + +running (1m14.0s), 100/100 VUs, 272164 complete and 0 interrupted iterations +default [ 82% ] 100 VUs 1m14.0s/1m30s + +running (1m15.0s), 100/100 VUs, 281753 complete and 0 interrupted iterations +default [ 83% ] 100 VUs 1m15.0s/1m30s + +running (1m16.0s), 100/100 VUs, 290747 complete and 0 interrupted iterations +default [ 84% ] 100 VUs 1m16.0s/1m30s + +running (1m17.0s), 100/100 VUs, 299837 complete and 0 interrupted iterations +default [ 86% ] 100 VUs 1m17.0s/1m30s + +running (1m18.0s), 100/100 VUs, 309220 complete and 0 interrupted iterations +default [ 87% ] 100 VUs 1m18.0s/1m30s + +running (1m19.0s), 100/100 VUs, 319000 complete and 0 interrupted iterations +default [ 88% ] 100 VUs 1m19.0s/1m30s + +running (1m20.0s), 100/100 VUs, 324982 complete and 0 interrupted iterations +default [ 89% ] 100 VUs 1m20.0s/1m30s + +running (1m21.0s), 100/100 VUs, 331304 complete and 0 interrupted iterations +default [ 90% ] 100 VUs 1m21.0s/1m30s + +running (1m22.0s), 100/100 VUs, 337736 complete and 0 interrupted iterations +default [ 91% ] 100 VUs 1m22.0s/1m30s + +running (1m23.0s), 100/100 VUs, 344885 complete and 0 interrupted iterations +default [ 92% ] 100 VUs 1m23.0s/1m30s + +running (1m24.0s), 100/100 VUs, 353870 complete and 0 interrupted iterations +default [ 93% ] 100 VUs 1m24.0s/1m30s + +running (1m25.0s), 100/100 VUs, 361903 complete and 0 interrupted iterations +default [ 94% ] 100 VUs 1m25.0s/1m30s + +running (1m26.0s), 100/100 VUs, 371502 complete and 0 interrupted iterations +default [ 96% ] 100 VUs 1m26.0s/1m30s + +running (1m27.0s), 100/100 VUs, 381471 complete and 0 interrupted iterations +default [ 97% ] 100 VUs 1m27.0s/1m30s + +running (1m28.0s), 100/100 VUs, 390995 complete and 0 interrupted iterations +default [ 98% ] 100 VUs 1m28.0s/1m30s + +running (1m29.0s), 100/100 VUs, 400492 complete and 0 interrupted iterations +default [ 99% ] 100 VUs 1m29.0s/1m30s + +running (1m30.0s), 100/100 VUs, 409954 complete and 0 interrupted iterations +default [ 100% ] 100 VUs 1m30.0s/1m30s + + + █ THRESHOLDS + + checks + ✓ 'rate>0.99' rate=100.00% + + http_req_duration + ✓ 'p(95)<1000' p(95)=49.1ms + + http_req_failed + ✓ 'rate<0.01' rate=0.00% + + + █ TOTAL RESULTS + + checks_total.......: 820732 9118.090905/s + checks_succeeded...: 100.00% 820732 out of 820732 + checks_failed......: 0.00% 0 out of 820732 + + ✓ redirect status is 302 + ✓ location header exists + + HTTP + http_req_duration..............: avg=21.75ms min=990µs med=11.49ms max=303.27ms p(90)=25.19ms p(95)=49.1ms + { expected_response:true }...: avg=21.75ms min=990µs med=11.49ms max=303.27ms p(90)=25.19ms p(95)=49.1ms + http_req_failed................: 0.00% 0 out of 410366 + http_reqs......................: 410366 4559.045452/s + + EXECUTION + iteration_duration.............: avg=21.91ms min=1.09ms med=11.63ms max=303.37ms p(90)=25.44ms p(95)=49.41ms + iterations.....................: 410366 4559.045452/s + vus............................: 100 min=100 max=100 + vus_max........................: 100 min=100 max=100 + + NETWORK + data_received..................: 53 MB 594 kB/s + data_sent......................: 33 MB 365 kB/s + + + + +running (1m30.0s), 000/100 VUs, 410366 complete and 0 interrupted iterations +default ✓ [ 100% ] 100 VUs 1m30s diff --git a/results/resilience/run1/redis-fallback-timeline.txt b/results/resilience/run1/redis-fallback-timeline.txt new file mode 100644 index 0000000..960b264 --- /dev/null +++ b/results/resilience/run1/redis-fallback-timeline.txt @@ -0,0 +1,8 @@ +test_start=2026-08-06T11:45:05+09:00 +short_code=pUZ +vus=100 +duration=90s +redis_stopped=2026-08-06T11:45:35+09:00 +redis_started=2026-08-06T11:46:05+09:00 +redis_ready=2026-08-06T11:46:06+09:00 +test_end=2026-08-06T11:46:37+09:00 diff --git a/scripts/run-redis-failure-test.sh b/scripts/run-redis-failure-test.sh new file mode 100755 index 0000000..311dbac --- /dev/null +++ b/scripts/run-redis-failure-test.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +BASE_URL="${BASE_URL:-http://localhost:8080}" +VUS="${VUS:-100}" +DURATION="${DURATION:-120s}" + +RESULT_DIR="results/resilience" +RESULT_JSON="${RESULT_DIR}/redis-fallback-100vu.json" +RESULT_LOG="${RESULT_DIR}/redis-fallback-100vu.log" +TIMELINE_FILE="${RESULT_DIR}/redis-fallback-timeline.txt" + +mkdir -p "$RESULT_DIR" + +cleanup() { + echo + echo "[cleanup] Redis 실행 상태 복구" + docker compose start redis >/dev/null 2>&1 || true +} + +trap cleanup EXIT INT TERM + +snapshot_metrics() { + local phase="$1" + + curl -s "${BASE_URL}/actuator/prometheus" \ + | grep -E \ + 'short_url_cache_(hit|miss|error|fallback)_total|short_url_db_lookup_total' \ + > "${RESULT_DIR}/metrics-${phase}.txt" || true +} + +wait_for_redis() { + until docker compose exec -T redis redis-cli ping 2>/dev/null \ + | grep -q PONG; do + echo "[wait] Redis 준비 대기 중..." + sleep 1 + done +} + +wait_for_app() { + until curl -sf "${BASE_URL}/actuator/health" \ + | grep -q '"status":"UP"'; do + echo "[wait] 애플리케이션 준비 대기 중..." + sleep 1 + done +} + +sleep_until() { + local target_second="$1" + + if (( SECONDS < target_second )); then + sleep $((target_second - SECONDS)) + fi +} + +echo "[1/7] Redis 실행 확인" +docker compose start redis >/dev/null +wait_for_redis + +echo "[2/7] 애플리케이션 준비 확인" +wait_for_app + +echo "[3/7] 테스트용 단축 URL 생성" + +LONG_URL="https://example.com/redis-fallback-$(date +%s)" + +RESPONSE=$( + curl -sf -X POST \ + "${BASE_URL}/api/v1/data/shorten" \ + -H 'Content-Type: application/json' \ + -d "{\"longUrl\":\"${LONG_URL}\"}" +) + +SHORT_CODE=$(echo "$RESPONSE" | jq -r '.shortCode // empty') + +if [[ -z "$SHORT_CODE" ]]; then + echo "[error] shortCode 생성 실패" + echo "$RESPONSE" + exit 1 +fi + +echo "shortCode=${SHORT_CODE}" + +echo "[4/7] 캐시 Warm-up" + +STATUS=$( + curl -s \ + -o /dev/null \ + -w '%{http_code}' \ + "${BASE_URL}/api/v1/${SHORT_CODE}" +) + +if [[ "$STATUS" != "302" ]]; then + echo "[error] Warm-up 요청 실패: HTTP ${STATUS}" + exit 1 +fi + +snapshot_metrics "before" + +{ + echo "test_start=$(date -Iseconds)" + echo "short_code=${SHORT_CODE}" + echo "vus=${VUS}" + echo "duration=${DURATION}" +} > "$TIMELINE_FILE" + +echo "[5/7] k6 테스트 시작" + +SECONDS=0 + +SHORT_CODE="$SHORT_CODE" \ +BASE_URL="$BASE_URL" \ +VUS="$VUS" \ +DURATION="$DURATION" \ +k6 run \ + --summary-export="$RESULT_JSON" \ + k6/redis-fallback-test.js \ + > "$RESULT_LOG" 2>&1 & + +K6_PID=$! + +sleep_until 30 + +snapshot_metrics "normal" + +echo "[fault] Redis 중지: $(date -Iseconds)" +echo "redis_stopped=$(date -Iseconds)" >> "$TIMELINE_FILE" + +docker compose stop redis + +sleep_until 60 + +snapshot_metrics "outage" + +echo "[recovery] Redis 시작: $(date -Iseconds)" +echo "redis_started=$(date -Iseconds)" >> "$TIMELINE_FILE" + +docker compose start redis +wait_for_redis + +echo "redis_ready=$(date -Iseconds)" >> "$TIMELINE_FILE" + +sleep_until 90 +snapshot_metrics "recovered-30s" + +sleep_until 120 +snapshot_metrics "recovered-60s" + +set +e +wait "$K6_PID" +K6_STATUS=$? +set -e + +echo "test_end=$(date -Iseconds)" >> "$TIMELINE_FILE" + +echo "[6/7] 실험 종료" +cat "$RESULT_LOG" + +echo +echo "[7/7] 결과 파일" +echo "- ${RESULT_JSON}" +echo "- ${RESULT_LOG}" +echo "- ${TIMELINE_FILE}" +echo "- ${RESULT_DIR}/metrics-before.txt" +echo "- ${RESULT_DIR}/metrics-normal.txt" +echo "- ${RESULT_DIR}/metrics-outage.txt" +echo "- ${RESULT_DIR}/metrics-recovered.txt" + +exit "$K6_STATUS" \ No newline at end of file diff --git a/src/main/java/com/backendsystemdesignlab/urlshortener/metrics/RedirectMetrics.java b/src/main/java/com/backendsystemdesignlab/urlshortener/metrics/RedirectMetrics.java index 78762c8..27cf5ff 100644 --- a/src/main/java/com/backendsystemdesignlab/urlshortener/metrics/RedirectMetrics.java +++ b/src/main/java/com/backendsystemdesignlab/urlshortener/metrics/RedirectMetrics.java @@ -9,6 +9,9 @@ public class RedirectMetrics { private final Counter cacheHitCounter; private final Counter cacheMissCounter; + private final Counter cacheGetErrorCounter; + private final Counter cacheSetErrorCounter; + private final Counter cacheFallbackCounter; private final Counter dbLookupCounter; public RedirectMetrics(MeterRegistry meterRegistry) { @@ -20,6 +23,20 @@ public RedirectMetrics(MeterRegistry meterRegistry) { .description("Redis cache miss count") .register(meterRegistry); + this.cacheGetErrorCounter = Counter.builder("short_url.cache.error") + .description("Redis cache operation error count") + .tag("operation", "get") + .register(meterRegistry); + + this.cacheSetErrorCounter = Counter.builder("short_url.cache.error") + .description("Redis cache operation error count") + .tag("operation", "set") + .register(meterRegistry); + + this.cacheFallbackCounter = Counter.builder("short_url.cache.fallback") + .description("MySQL fallback count caused by Redis errors") + .register(meterRegistry); + this.dbLookupCounter = Counter.builder("short_url.db.lookup") .description("Short URL database lookup count") .register(meterRegistry); @@ -31,6 +48,9 @@ public void recordCacheHit() { public void recordCacheMiss() { cacheMissCounter.increment(); } + public void recordCacheGetError() { cacheGetErrorCounter.increment(); } + public void recordCacheSetError() { cacheSetErrorCounter.increment(); } + public void recordCacheFallback() { cacheFallbackCounter.increment(); } public void recordDbLookup() { dbLookupCounter.increment(); } diff --git a/src/main/java/com/backendsystemdesignlab/urlshortener/service/RedirectService.java b/src/main/java/com/backendsystemdesignlab/urlshortener/service/RedirectService.java index 4bfca8e..5a63084 100644 --- a/src/main/java/com/backendsystemdesignlab/urlshortener/service/RedirectService.java +++ b/src/main/java/com/backendsystemdesignlab/urlshortener/service/RedirectService.java @@ -6,8 +6,11 @@ import com.backendsystemdesignlab.urlshortener.url.domain.ShortUrl; import com.backendsystemdesignlab.urlshortener.url.repository.ShortUrlRepository; import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataAccessException; import org.springframework.stereotype.Service; +import java.util.Optional; + @Service @RequiredArgsConstructor public class RedirectService { @@ -17,18 +20,52 @@ public class RedirectService { private final RedirectMetrics redirectMetrics; public String findLongUrl(String shortCode) { - return shortUrlCache.find(shortCode) - .orElseGet(() -> findFromDatabase(shortCode)); + try { + Optional cachedLongUrl = shortUrlCache.find(shortCode); + + if (cachedLongUrl.isPresent()) { + return cachedLongUrl.get(); + } + } catch (DataAccessException e) { + redirectMetrics.recordCacheGetError(); + redirectMetrics.recordCacheFallback(); + + /* + * Redis GET이 실패했으므로 같은 요청에서는 + * Redis SET을 다시 시도하지 않고 MySQL만 조회 + */ + return findFromDatabase(shortCode, false); + } + + /* + * Redis 조회에는 성공했지만 Cache Miss인 경우 + * MySQL 조회 후 Redis에 다시 저장 + */ + return findFromDatabase(shortCode, true); } - private String findFromDatabase(String shortCode) { + private String findFromDatabase(String shortCode, boolean saveToCache) { redirectMetrics.recordDbLookup(); ShortUrl shortUrl = shortUrlRepository.findByShortCode(shortCode) .orElseThrow(() -> new ShortUrlNotFoundException(shortCode)); - shortUrlCache.save(shortCode, shortUrl.getLongUrl()); + String longUrl = shortUrl.getLongUrl(); + + // Redis가 정상 작동할 때만 저장 + if (saveToCache) { + saveToCacheSafely(shortCode, longUrl); + } + + return longUrl; + } - return shortUrl.getLongUrl(); + private void saveToCacheSafely(String shortCode, String longUrl) { + try { + shortUrlCache.save(shortCode, longUrl); + } catch (DataAccessException e) { + // Redis 저장 실패가 사용자 리다이렉트 실패로 이어지지 않게 한다. + redirectMetrics.recordCacheSetError(); + } } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c71d33a..ca4e6d1 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -29,8 +29,8 @@ spring: redis: host: ${REDIS_HOST:localhost} port: ${REDIS_PORT:6379} - connect-timeout: 2s - timeout: 2s + connect-timeout: ${REDIS_CONNECT_TIMEOUT:200ms} + timeout: ${REDIS_COMMAND_TIMEOUT:200ms} management: endpoints: diff --git a/src/test/java/com/backendsystemdesignlab/urlshortener/service/RedirectServiceTest.java b/src/test/java/com/backendsystemdesignlab/urlshortener/service/RedirectServiceTest.java index 3b0e5ad..6b26fac 100644 --- a/src/test/java/com/backendsystemdesignlab/urlshortener/service/RedirectServiceTest.java +++ b/src/test/java/com/backendsystemdesignlab/urlshortener/service/RedirectServiceTest.java @@ -1,7 +1,6 @@ package com.backendsystemdesignlab.urlshortener.service; import com.backendsystemdesignlab.urlshortener.cache.ShortUrlCache; -import com.backendsystemdesignlab.urlshortener.encoding.Base62Encoder; import com.backendsystemdesignlab.urlshortener.exception.ShortUrlNotFoundException; import com.backendsystemdesignlab.urlshortener.metrics.RedirectMetrics; import com.backendsystemdesignlab.urlshortener.url.domain.ShortUrl; @@ -11,13 +10,13 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.redis.RedisConnectionFailureException; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.*; @ExtendWith(MockitoExtension.class) class RedirectServiceTest { @@ -53,7 +52,6 @@ class RedirectServiceTest { void 캐시에_URL이_없으면_DB를_조회하고_캐시에_저장한다() { String shortCode = "2TX"; String longUrl = "https://www.google.com"; - long id = 12345L; ShortUrl shortUrl = ShortUrl.create(longUrl); @@ -91,4 +89,47 @@ class RedirectServiceTest { then(shortUrlRepository).should().findByShortCode(shortCode); then(shortUrlCache).shouldHaveNoMoreInteractions(); } + + @Test + void Redis_조회에_실패하면_DB로_Fallback한다() { + String shortCode = "2TX"; + String longUrl = "https://www.google.com"; + + ShortUrl shortUrl = ShortUrl.create(longUrl); + + given(shortUrlCache.find(shortCode)).willThrow(new RedisConnectionFailureException("Redis connection failure")); + given(shortUrlRepository.findByShortCode(shortCode)).willReturn(Optional.of(shortUrl)); + + String result = redirectService.findLongUrl(shortCode); + + assertThat(result).isEqualTo(longUrl); + then(shortUrlCache).should().find(shortCode); + then(redirectMetrics).should().recordCacheGetError(); + then(redirectMetrics).should().recordCacheFallback(); + then(redirectMetrics).should().recordDbLookup(); + then(shortUrlRepository).should().findByShortCode(shortCode); + then(shortUrlCache).shouldHaveNoMoreInteractions(); + } + + @Test + void Redis_저장에_실패해도_DB에서_조회한_URL을_반환한다() { + String shortCode = "2TX"; + String longUrl = "https://www.google.com"; + + ShortUrl shortUrl = ShortUrl.create(longUrl); + + given(shortUrlCache.find(shortCode)).willReturn(Optional.empty()); + given(shortUrlRepository.findByShortCode(shortCode)).willReturn(Optional.of(shortUrl)); + + willThrow(new RedisConnectionFailureException("Redis connection failure")).given(shortUrlCache).save(shortCode, longUrl); + + String result = redirectService.findLongUrl(shortCode); + + assertThat(result).isEqualTo(longUrl); + then(shortUrlCache).should().find(shortCode); + then(redirectMetrics).should().recordDbLookup(); + then(shortUrlRepository).should().findByShortCode(shortCode); + then(shortUrlCache).should().save(shortCode, longUrl); + then(redirectMetrics).should().recordCacheSetError();; + } } \ No newline at end of file