diff --git a/README.ko.md b/README.ko.md index 5c7993c..eb0de89 100644 --- a/README.ko.md +++ b/README.ko.md @@ -177,6 +177,19 @@ WARN k.d.s.core.UrlPolicy : ssrf-guard: Host not allowed: evil.com (reason=block 태그가 bounded — Prometheus / Datadog / CloudWatch 문제 없음. +## 성능 + +인터셉터의 allowed 경로 비용은 **요청당 ~5 μs** (JMH, JDK 21) — 100 ms 외부 API 호출 대비 **0.005% 오버헤드**, 실질적으로 invisible. + +| Hot path | 평균 비용 | 비고 | +|---|---:|---| +| `UrlPolicy.validate` allowed | ~5 μs | 프로덕션 트래픽의 99%+ | +| `UrlPolicy.validate` blocked | 5-12 μs | early-exit (IP 리터럴)이 late-exit (whitelist)보다 저렴 | +| `JsonToolInputGuard` 작은 JSON | ~6 μs | URL 하나짜리 LLM 툴 입력 | +| `JsonToolInputGuard` ~2 KB JSON | ~24 μs | URL 3개 있는 RAG-augmented 툴 입력 | + +전체 방법론, stdev 포함 케이스별 숫자, 해석 가이드는 [`BENCHMARKS.md`](./BENCHMARKS.md). 직접 재현: `./gradlew :ssrf-guard-benchmarks:jmh`. + ## 자동 구성이 등록하는 빈 (RestClient 모듈) `ssrf.guard.enabled=true`(기본값)이면 RestClient 자동설정이 활성화되어 다음을 등록: diff --git a/README.md b/README.md index 3e079f9..400141a 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,19 @@ WARN k.d.s.core.UrlPolicy : ssrf-guard: Host not allowed: evil.com (reason=block Tags are bounded (`reason` is an enum, `scheme` is http/https) — Prometheus / Datadog / CloudWatch ingest happily. +## Performance + +The interceptor's allowed-path cost is **~5 μs per request** (JMH, JDK 21) — **0.005% overhead** on a 100 ms remote API call, invisible in practice. + +| Hot path | Avg cost | Notes | +|---|---:|---| +| `UrlPolicy.validate` allowed | ~5 μs | 99%+ of production traffic | +| `UrlPolicy.validate` blocked | 5-12 μs | Cheaper if early-exit (IP literal) than late-exit (whitelist) | +| `JsonToolInputGuard` small JSON | ~6 μs | LLM tool input with one URL | +| `JsonToolInputGuard` ~2 KB JSON | ~24 μs | RAG-augmented tool input with 3 URLs | + +Full methodology, per-case numbers with stdev, and how to read them: see [`BENCHMARKS.md`](./BENCHMARKS.md). Reproduce yourself with `./gradlew :ssrf-guard-benchmarks:jmh`. + ## What auto-configuration registers (RestClient module) When `ssrf.guard.enabled=true` (the default), the RestClient autoconfig activates and registers: diff --git a/docs/guides/performance.ko.md b/docs/guides/performance.ko.md new file mode 100644 index 0000000..0930799 --- /dev/null +++ b/docs/guides/performance.ko.md @@ -0,0 +1,73 @@ +# 성능 + +인터셉터의 allowed 경로 비용은 JDK 21 기준 **요청당 ~5 μs** — 100 ms 외부 API 호출 대비 0.005% 오버헤드, 실질적으로 invisible. 아래 숫자들은 실제 사용자가 비용을 지불하는 hot path에 대한 JMH 마이크로벤치마크. 로컬 재현: `./gradlew :ssrf-guard-benchmarks:jmh`. + +## 무엇을 측정하나 + +| 모듈 | 메서드 | 왜 중요한가 | +|---|---|---| +| `ssrf-guard-core` | `UrlPolicy.validate(URI)` | 모든 HTTP 클라이언트 인터셉터 (RestClient / RestTemplate / WebClient / Feign / OkHttp)가 요청마다 한 번 호출 | +| `ssrf-guard-llm` | `JsonToolInputGuard.checkOrFormatError(json)` | `-springai`와 `-langchain4j`가 LLM 툴 호출마다 한 번 호출 | + +DNS 시점 게이트 (`-httpclient5`의 `SafeDnsResolver`, OkHttp `Dns`, `-webclient`의 reactor-netty `AddressResolverGroup`)는 마이크로벤치마크 대상 아님 — DNS 해석은 네트워크 I/O가 지배. 모킹된 resolver로 측정해봐야 모킹을 측정하는 셈. + +## 결과 + +JMH 1.37, JDK 21, single-fork, 5×1s warmup + 5×1s measurement. + +### `UrlPolicy.validate(URI)` — 요청당 + +| 벤치마크 | Score | Error | 비고 | +|---|---:|---:|---| +| `allowed` | 5,285 ns | ± 924 ns | 해피 패스 — 프로덕션 트래픽의 99%+ | +| `blockedIpLiteral` | 4,888 ns | ± 750 ns | allowed보다 **빠름** — IP 리터럴 검사가 화이트리스트 lookup보다 먼저 | +| `blockedHost` | 11,822 ns | ± 2,035 ns | 가장 비싼 차단 경로 — 전체 체인 + `SsrfGuardException` | + +### `JsonToolInputGuard` — LLM 툴 호출당 + +| 벤치마크 | Score | Error | 비고 | +|---|---:|---:|---| +| `small_allowed` | 5,629 ns | ± 486 ns | `{"url": "..."}` — `fetch_url` 류 툴의 floor 비용 | +| `medium_blocked` | 6,722 ns | ± 290 ns | URL이 2단계 깊이로 중첩, IP 리터럴 검사에서 차단 | +| `large_allowed` | 24,228 ns | ± 4,958 ns | ~2 KB JSON, URL 3개 + 비 URL 필드 40개 (RAG 형태) | + +JSON 가드는 대략 `Jackson 파싱 + N × UrlPolicy.validate` 스케일. + +## 실용적 해석 + +| 본인 요청 레이턴시 | ssrf-guard `allowed` 비용 | 오버헤드 | +|---|---|---| +| 1 ms (클러스터 내부 호출) | ~5 μs | ~0.5% | +| 10 ms (리전 내부 호출) | ~5 μs | ~0.05% | +| 100 ms (외부 API) | ~5 μs | ~0.005% | +| 500 ms (LLM 툴 호출) | ~5 μs | ~0.001% | + +LLM 에이전트가 툴 호출 (LLM 왕복 + 실제 fetch 포함 100 ms - 5 s end-to-end) 하나 할 때, `JsonToolInputGuard`는 **~6-24 μs** 추가 — LLM 비용 대비 invisible. + +## 숫자 해석 가이드 + +- **"steady-state 오버헤드가 얼마?" 라는 질문엔 `allowed` 인용**. 차단 경로 비용은 거부 시점에는 실재하지만 production에서는 드묾 — 차단된 요청은 설정 미스 또는 실제 공격, 일반적이지 않음. +- **실제 요청 레이턴시와 비교**. 5 μs라도 어떤 out-of-process 호출의 network jitter noise floor 안에 들어감. +- **`JsonToolInputGuard`와 `UrlPolicy`를 직접 비교 금지** — JSON 가드는 Jackson 파싱 + 트리 walk + 차단 페이로드 직렬화 (거부 시). URL-time 인터셉터와는 apples-to-oranges. +- **차단 경로가 허용 경로보다 비쌈** — 예외 생성이 측정에 포함됨. 현실적임 — 차단 비용은 공격을 막는 대가. + +## 측정 안 한 것 + +의도적으로 범위 밖 (그리고 이 숫자에서 추론하면 안 되는 것): + +- **DNS 시점 게이트** — I/O 지배; 모킹 없이 마이크로벤치마크 의미 없음 +- **Spring 자동 설정 시작 비용** — 앱 부트 시 1회 비용, 요청당 비용 아님 +- **Micrometer 메트릭 오버헤드** — 벤치마크는 `NoOpSsrfGuardMetrics` 사용해서 가드 자체 비용 격리. 실제 Micrometer는 `Counter.increment()`당 ~50 ns 추가 +- **네이티브 이미지 (GraalVM) 성능** — 다른 형태; 위의 JIT 숫자가 상한. AOT 비교는 `nativeRun` 아래에서 같은 JMH harness 실행. + +## 재현 + +```bash +./gradlew :ssrf-guard-benchmarks:jmh +``` + +출력: `ssrf-guard-benchmarks/build/results/jmh/results.txt`. + +정식 숫자 (블로그 글이나 커밋 메시지용)는 [`ssrf-guard-benchmarks/build.gradle.kts`](https://github.com/devslab-kr/ssrf-guard/blob/main/ssrf-guard-benchmarks/build.gradle.kts)에서 `fork = 3`으로 올려서 JIT 분산 캡처. + +정식 원본 — 방법론, 새 벤치마크 기여 가이드 — [`BENCHMARKS.md`](https://github.com/devslab-kr/ssrf-guard/blob/main/BENCHMARKS.md). diff --git a/docs/guides/performance.md b/docs/guides/performance.md new file mode 100644 index 0000000..e3f7e8d --- /dev/null +++ b/docs/guides/performance.md @@ -0,0 +1,73 @@ +# Performance + +The allowed-path cost of the interceptor is **~5 μs per request** on JDK 21 — 0.005% overhead on a 100 ms remote API call, invisible in practice. The numbers below come from JMH micro-benchmarks against the actual hot paths consumers pay for. Reproduce locally with `./gradlew :ssrf-guard-benchmarks:jmh`. + +## What's measured + +| Module | Method | Why it matters | +|---|---|---| +| `ssrf-guard-core` | `UrlPolicy.validate(URI)` | What every HTTP-client interceptor (RestClient / RestTemplate / WebClient / Feign / OkHttp) invokes once per request | +| `ssrf-guard-llm` | `JsonToolInputGuard.checkOrFormatError(json)` | What `-springai` and `-langchain4j` invoke once per LLM tool call | + +The DNS-time gates (`SafeDnsResolver` in `-httpclient5`, OkHttp `Dns`, reactor-netty `AddressResolverGroup` in `-webclient`) aren't micro-benchmarked here — DNS resolution is dominated by network I/O, not by the policy code that runs around it. Measuring with a mock resolver would just measure the mock. + +## Results + +JMH 1.37, JDK 21, single-fork, 5×1s warmup + 5×1s measurement. + +### `UrlPolicy.validate(URI)` — per-request + +| Benchmark | Score | Error | Notes | +|---|---:|---:|---| +| `allowed` | 5,285 ns | ± 924 ns | The happy path — what 99%+ of production traffic costs | +| `blockedIpLiteral` | 4,888 ns | ± 750 ns | **Faster** than allowed — IP-literal check fires before whitelist lookup | +| `blockedHost` | 11,822 ns | ± 2,035 ns | The most expensive blocked path — full chain + `SsrfGuardException` | + +### `JsonToolInputGuard` — per-LLM-tool-call + +| Benchmark | Score | Error | Notes | +|---|---:|---:|---| +| `small_allowed` | 5,629 ns | ± 486 ns | `{"url": "..."}` — floor cost for a `fetch_url`-style tool | +| `medium_blocked` | 6,722 ns | ± 290 ns | Nested URL two levels deep, blocked at IP-literal check | +| `large_allowed` | 24,228 ns | ± 4,958 ns | ~2 KB JSON, 3 URLs + 40 non-URL fields (RAG-shaped) | + +The JSON guard scales roughly as `Jackson parse + N × UrlPolicy.validate`. + +## Practical takeaway + +| Your request latency | ssrf-guard `allowed` cost | Overhead | +|---|---|---| +| 1 ms (in-cluster call) | ~5 μs | ~0.5% | +| 10 ms (in-region call) | ~5 μs | ~0.05% | +| 100 ms (remote API) | ~5 μs | ~0.005% | +| 500 ms (LLM tool call) | ~5 μs | ~0.001% | + +For an LLM-driven agent making a tool call (100 ms - 5 s end-to-end including the LLM round-trip), `JsonToolInputGuard` adds **~6-24 μs**, which is invisible against the LLM cost. + +## How to read these numbers + +- **Quote `allowed` when answering "what's the steady-state overhead?"**. The blocked-path costs are realistic on rejection but rare — a blocked request in production is a misconfiguration or an actual attack, not the common case. +- **Compare against your real request latency**. Even at 5 μs per check, that's well inside the noise floor of network jitter on any out-of-process call. +- **Don't compare `JsonToolInputGuard` to `UrlPolicy` directly** — the JSON guard parses Jackson + walks the tree + serialises a block payload on rejection. Apples-to-oranges with the URL-time interceptor. +- **The block path is more expensive than the allow path** for both benchmarks, because exception construction is in the measurement. This is realistic — paying for a block is the price of stopping an attack. + +## What this doesn't measure + +Deliberately out of scope (and what would be wrong to extrapolate from these numbers): + +- **DNS-time gates** — dominated by I/O; meaningless to micro-benchmark without mocking +- **Spring auto-configuration startup cost** — one-time tax at app boot, not per-request +- **Micrometer metrics overhead** — benchmarks use `NoOpSsrfGuardMetrics` to isolate the guard's own cost. Real Micrometer adds ~50 ns per `Counter.increment()` on top +- **Native-image (GraalVM) performance** — different shape; JIT numbers above are an upper bound. Run the same JMH harness under `nativeRun` for AOT. + +## Reproducing + +```bash +./gradlew :ssrf-guard-benchmarks:jmh +``` + +Output lands at `ssrf-guard-benchmarks/build/results/jmh/results.txt`. + +For canonical numbers (e.g. a blog post or a commit message), bump `fork = 3` in [`ssrf-guard-benchmarks/build.gradle.kts`](https://github.com/devslab-kr/ssrf-guard/blob/main/ssrf-guard-benchmarks/build.gradle.kts) to capture JIT variance. + +Full canonical source — methodology in narrative form, contribution guide for new benchmarks — at [`BENCHMARKS.md`](https://github.com/devslab-kr/ssrf-guard/blob/main/BENCHMARKS.md). diff --git a/mkdocs.yml b/mkdocs.yml index 7a97273..b0ba213 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -66,6 +66,7 @@ plugins: Guides: 가이드 Security Model: 보안 모델 Configuration: 설정 + Performance: 성능 Changelog: 변경 이력 Contributing: 기여하기 @@ -120,5 +121,6 @@ nav: - Guides: - Security Model: guides/security-model.md - Configuration: guides/configuration.md + - Performance: guides/performance.md - Changelog: changelog.md - Contributing: contributing.md