๐์ฑ๋ฅ ํ ์คํธ ๋ฐ ์ต์ ํ
Sprout ์๋ฒ์ ์ฑ๋ฅ ํน์ฑ์ ํ์ ํ๊ณ ๋ณ๋ชฉ ์ง์ ์ ๊ฐ์ ํ ์ ์ฒด ๊ณผ์ ์ ๋ค๋ฃน๋๋ค. ์ด๊ธฐ ๋ฒค์น๋งํน๋ถํฐ ํ๋กํ์ผ๋ง ๋ถ์, ์ฝ๋ ๋ฆฌํฉํ ๋ง, ๊ทธ๋ฆฌ๊ณ ์ต์ข ์ฑ๋ฅ ๊ฐ์ ๊น์ง์ ์ฌ์ ์ ๋จ๊ณ๋ณ๋ก ์ ๋ฆฌํฉ๋๋ค.
๊ฐ์โ
Sprout๋ Java NIO ๊ธฐ๋ฐ์ ๊ณ ์ฑ๋ฅ ์น ์๋ฒ๋ก, BIO(Blocking I/O)์ NIO(Non-blocking I/O)๋ฅผ ๋ชจ๋ ์ง์ํ๋ฉฐ ํ๋ซํผ ์ค๋ ๋์ ๊ฐ์ ์ค๋ ๋ ์ค ์ ํํ ์ ์์ต๋๋ค. ์ด๋ฌํ ๊ตฌ์กฐ์ ์ ์ฐ์ฑ ๋๋ถ์ ๋ค์ํ ์กฐํฉ์์์ ์ฑ๋ฅ ํน์ฑ์ ๋น๊ตํ๊ณ ์ต์ ํํ ์ ์์์ต๋๋ค.
ํ ์คํธ ํ๊ฒฝโ
| ํญ๋ชฉ | ์ฌ์ |
|---|---|
| CPU | 10 Cores |
| Memory | 32GB |
| OS | macOS Sequoia 15.6.1 |
| JDK | OpenJDK 21 |
| Tool | Gatling 3.x |
์๋ฒ ๊ตฌ์ฑ ์กฐํฉโ
Sprout๋ ์คํ ๋ชจ๋์ ์ค๋ ๋ ํ์ ์ ์กฐํฉํ์ฌ 4๊ฐ์ง ๊ตฌ์ฑ์ผ๋ก ๋์ํฉ๋๋ค.
| I/O ๋ชจ๋ | ์ค๋ ๋ ํ์ | ์ค๋ช |
|---|---|---|
| Hybrid (BIO) | Platform Threads | HTTP๋ BIO๋ก ๋์, ๊ณ ์ ํฌ๊ธฐ ์ค๋ ๋ ํ ์ฌ์ฉ (150๊ฐ) |
| Hybrid (BIO) | Virtual Threads | HTTP๋ BIO๋ก ๋์, ๊ฐ์ ์ค๋ ๋ ์ฌ์ฉ |
| NIO | Platform Threads | HTTP๊ฐ NIO๋ก ๋์, ๊ณ ์ ํฌ๊ธฐ ์ค๋ ๋ ํ ์ฌ์ฉ |
| NIO | Virtual Threads | HTTP๊ฐ NIO๋ก ๋์, ๊ฐ์ ์ค๋ ๋ ์ฌ์ฉ |
์ค์ ์์
server:
execution-mode: nio # ์คํ ๋ชจ๋: nio ๋๋ hybrid
thread-type: virtual # ์ค๋ ๋ ์ข
๋ฅ: virtual ๋๋ platform
thread-pool-size: 150 # platform ์ค๋ ๋์ผ ๊ฒฝ์ฐ ์ฌ์ฉํ ์ค๋ ๋ ํ ํฌ๊ธฐ
Phase 1: ์ด๊ธฐ ๋ฒค์น๋งํนโ
๋ฒค์น๋งํฌ ์๋๋ฆฌ์คโ
์ธ ๊ฐ์ง ์๋๋ฆฌ์ค๋ก ์๋ฒ์ ํน์ฑ์ ์ธก์ ํ์ต๋๋ค
1. HelloWorld ์๋๋ฆฌ์ค (์ฝ 8,000 ์์ฒญ)โ
@GetMapping("/hello")
public String hello() {
return "Hello, World!";
}
๊ฐ์ฅ ๊ฐ๋จํ ์๋ต์ผ๋ก ์์ ์๋ฒ ์ฑ๋ฅ์ ์ธก์ ํฉ๋๋ค.
2. CPU Intensive ์๋๋ฆฌ์ค (์ฝ 2,000 ์์ฒญ)โ
@GetMapping("/cpu")
public String cpu(@RequestParam(required = false, defaultValue = "35") String n) {
int num = Integer.parseInt(n);
long result = fibonacci(num);
return "Fibonacci(" + num + ") = " + result;
}
@GetMapping("/cpu-heavy")
public String cpuHeavy(@RequestParam(required = false, defaultValue = "10000") String limit) {
int max = Integer.parseInt(limit);
int primeCount = countPrimes(max);
return "Primes up to " + max + ": " + primeCount;
}
CPU ๋ฐ์ด๋ ์์ ์์์ ์๋ฒ ์ฒ๋ฆฌ ๋ฅ๋ ฅ์ ์ธก์ ํฉ๋๋ค.
3. Latency ์๋๋ฆฌ์ค (์ฝ 20,000 ์์ฒญ)โ
@GetMapping("/latency")
public String latency(@RequestParam(required = false, defaultValue = "100") String ms) {
int delay = Integer.parseInt(ms);
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return "Interrupted";
}
return "Delayed response after " + delay + "ms";
}
I/O ๋ธ๋กํน์ด ๋น๋ฒํ ์ํฉ์ ์๋ฎฌ๋ ์ด์ ํฉ๋๋ค.
์ด๊ธฐ ๋ฒค์น๋งํน ๊ฒฐ๊ณผโ
| ์กฐํฉ | HelloWorld | CPU | Latency | ํน์ฑ ์์ฝ |
|---|---|---|---|---|
| Hybrid + Platform | 84% | 53% | 95.6% | ์์ ์ ์ด๋ Warm-up ์์กด์ |
| Hybrid + Virtual | 87% | 47% | 94.7% | ์ด๋ฐ ์ค๋ฒํค๋ ์์ผ๋ ์๋ต ๋น ๋ฆ |
| NIO + Platform | 82% | 45% | 93.4% | ์ด๊ธฐ Selector ๋ณ๋ชฉ ๋ฐ์ |
| NIO + Virtual | 69% | 64.9% | 92.2% | CPU ๋ถํ์์ ์ต์ , I/O์ ๋ถ์ |
์ฃผ์ ๋ฐ๊ฒฌ ์ฌํญโ
1. Warm-up ๋ฌธ์ โ
๋ชจ๋ ์กฐํฉ์์ ์ด๋ฐ 5-10์ด ๊ตฌ๊ฐ์ ์์ฒญ ์คํจ(KO)๊ฐ ์ง์ค๋๊ณ ์ดํ ์์ ํ๋๋ ํจํด์ด ๊ด์ฐฐ๋์์ต๋๋ค. ๊ทธ๋ํ๋ฅผ ๋ณด๋ฉด ํน์ ์์ (์ฝ 300๊ฑด์ ์์ฒญ ์ฒ๋ฆฌ ํ)๋ถํฐ ์ฑ๋ฅ์ด ๊ธ์ฆํ๋๋ฐ, ์ด๋ JIT ์ปดํ์ผ๋ฌ๊ฐ "hot" ์ํ๋ก ์ ํ๋ ์์ ์ ์๋ฏธํฉ๋๋ค.
์์ธ ๋ถ์
- JIT ์ปดํ์ผ์ด ์๋ฃ๋๊ธฐ ์ ๊น์ง ์ธํฐํ๋ฆฌํฐ ๋ชจ๋๋ก ์คํ๋์ด ๋๋ฆผ
- ํนํ NIO ๊ตฌ์กฐ๋ Selector, Channel, ByteBuffer ๋ฑ ๋ณต์กํ ๋ฃจํ ๋๋ฌธ์ JIT threshold ๋๋ฌ์ ๋ ๋ง์ ๋ฐ๋ณต ํ์
- BIO๋ ๋จ์ socket read/write๋ผ ๋นจ๋ฆฌ ์ต์ ํ ๊ฐ๋ฅ
2. NIO + Virtual Thread์ ํน์ดํ ํจํดโ
HelloWorld ์๋๋ฆฌ์ค์์๋ ๊ฐ์ฅ ๋ฎ์ ์ฑ๊ณต๋ฅ (69%)์ ๋ณด์์ง๋ง, CPU Intensive ์๋๋ฆฌ์ค์์๋ ๊ฐ์ฅ ๋์ ์ฑ๊ณต๋ฅ (64.9%)์ ๊ธฐ๋กํ์ต๋๋ค.
ํด์
- CPU ์ง์ฝ์ ์์ : NIO์ ์ด๋ฒคํธ ๋ถ์ฐ๊ณผ Virtual Thread์ ๊ฐ๋ฒผ์์ด ์๋์ง๋ฅผ ๋
- I/O ์ง์ฐ ์์ : ์๋์ ์ง์ฐ์ด NIO์ ๋น๋๊ธฐ ์ฒ๋ฆฌ์ ๋ง์ง ์์ ์คํ๋ ค ์ค๋ฒํค๋ ๋ฐ์
3. Hybrid + Virtual Thread์ ์์ ์ฑโ
๋๋ถ๋ถ์ ์๋๋ฆฌ์ค์์ ๊ฐ์ฅ ๊ท ํ์กํ ์ฑ๋ฅ์ ๋ณด์ฌ์คฌ์ต๋๋ค. BIO์ ๋จ์ํจ๊ณผ Virtual Thread์ ๊ฒฝ๋ ํน์ฑ์ด ์ ์กฐํฉ๋์ด ์์ ์ ์ธ ๊ฒฐ๊ณผ๋ฅผ ์ ๊ณตํฉ๋๋ค.
Phase 2: ๋ณ๋ชฉ ์ง์ ๋ถ์โ
์ด๊ธฐ ๋ฒค์น๋งํน์์ Warm-up ๋ฌธ์ ๋ฅผ ํ์ธํ ํ, ์ ํํ ๋ณ๋ชฉ ์ง์ ์ ์ฐพ๊ธฐ ์ํด ํ๋กํ์ผ๋ง ๋๊ตฌ๋ฅผ ํ์ฉํ์ต๋๋ค.
async-profiler ๋ถ์โ
async-profiler๋ฅผ ์ฌ์ฉํ์ฌ CPU, ๋ฉ๋ชจ๋ฆฌ ํ ๋น, Wall-clock ํ๋กํ์ผ๋ง์ ์งํํ์ต๋๋ค.
# async-profiler ์คํ ์คํฌ๋ฆฝํธ
env DYLD_LIBRARY_PATH=$ASYNC_PROFILER_HOME/lib $ASPROF \
-d 30 -e cpu -o flamegraph -f cpu-flamegraph.html $PID
env DYLD_LIBRARY_PATH=$ASYNC_PROFILER_HOME/lib $ASPROF \
-d 30 -e alloc -o flamegraph -f alloc-flamegraph.html $PID
env DYLD_LIBRARY_PATH=$ASYNC_PROFILER_HOME/lib $ASPROF \
-d 30 -e wall -o flamegraph -f wall-flamegraph.html $PID
CPU ํ๋กํ์ผ ๊ฒฐ๊ณผโ
- JIT ์ปดํ์ผ:
C2Compiler::compile_method,compiler_thread_loop๋ฑ์ด ๊ฐ์ฅ ๋ง์ CPU ์๊ฐ ์๋น - JMX ์ค๋ฒํค๋:
DefaultMBeanServerInterceptor.getAttribute๊ฐ ์๋นํ CPU ์ฌ์ฉ - ๋คํธ์ํฌ ์ฒ๋ฆฌ:
DefaultConnectionManager.acceptConnection์์ ๋๊ธฐ ๊ด๋ จ ํจ์(__psynch_cvwait) ๋ฐ๊ฒฌ
๋ฉ๋ชจ๋ฆฌ ํ ๋น ํ๋กํ์ผ ๊ฒฐ๊ณผโ
๊ฐ์ฅ ์ค์ํ ๋ฐ๊ฒฌ์ด์์ต๋๋ค
์ฃผ์ ํ ๋น ์ง์ :
- HTTP ์์ฒญ ํ์ฑ (์ฝ 18%):
HttpHeaderParser.parseโByteBuffer.allocate - HTTP ์๋ต ๋ฒํผ ์์ฑ (์ฝ 32%):
HttpUtils.createResponseBuffer - ์์ฒญ ๋ผ์ฐํ
/ํํฐ๋ง:
FilterChain.doFilter,HandlerMappingImpl.findHandler - ๋ฌธ์์ด ํ์ฑ/์ ๊ท์:
Pattern.matcher,Pattern.split
์ดํ: ์ ์ฒด ๋ฉ๋ชจ๋ฆฌ ํ ๋น์ ์ฝ 50%๊ฐ ByteBuffer ์์ฑ์ ์ฌ์ฉ๋๊ณ ์์์ต๋๋ค.
Wall-clock ํ๋กํ์ผ ๊ฒฐ๊ณผโ
์ ์ฒด ์คํ ์๊ฐ ์ค 95% ์ด์์ด ์ค๋ ๋์ ๋๊ธฐ(Waiting) ๋๋ ํด๋ฉด(Idle) ์ํ์์ต๋๋ค. ์ด๋ ์ ์์ ์ธ ํจํด์ผ๋ก, ์ค์ ์์ ์์๋ ์์ ๋ฐ๊ฒฌํ ๋ณ๋ชฉ ์ง์ ๋ค์ด ๋ฌธ์ ๊ฐ ๋ฉ๋๋ค.
JMC(JDK Mission Control) ๋ถ์โ
# JVM ์ต์
์ผ๋ก JFR ๊ธฐ๋ก ํ์ฑํ
-XX:StartFlightRecording=filename=jit-profile/recording.jfr,duration=300s,settings=profile
-XX:+UnlockDiagnosticVMOptions
-XX:+LogCompilation
-XX:LogFile=jit-profile/hotspot_%p.log
-XX:+PrintInlining
-XX:+PrintCompilation
JIT ์ปดํ์ผ ์๊ฐ ๋ถ์โ
๊ฐ์ฅ ๊ธด ์ปดํ์ผ ์๊ฐ(์ฝ 4.75ms)์ ์๋นํ ๋ฉ์๋๋ค
HttpUtils.readRawRequest- HTTP ์์ฒญ ์ฝ๊ธฐClassLoader๊ด๋ จ ๋ฉ์๋ - ํด๋์ค ๋ก๋ฉObjectOutputStream.writeOrdinaryObject- ๊ฐ์ฒด ์ง๋ ฌํ
GC ๋ถ์โ
Young Collection Total Time: 55.848 ms (123ํ)
Old Collection Total Time: 25.299 ms (1ํ)
Total GC Time: 81.146 ms
์ ์ฒด ์คํ ์๊ฐ ๋๋น GC ๋น์จ: 0.054%
GC ๋ถํ๋ ๊ฑฐ์ ์๋ ์์ค์ด์์ง๋ง, ByteBuffer ํ ๋น ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ฉด ๋ ๊ฐ์ ๋ ์ฌ์ง๊ฐ ์์์ต๋๋ค.
JITWatch ๋ถ์โ
# ์์ธํ JIT ๋ก๊ทธ ์์ฑ
-XX:+LogCompilation
-XX:+PrintInlining
-XX:+PrintAssembly
HttpUtils.readRawRequest ๋ถ์โ
JITWatch๋ฅผ ํตํด ์ ํํ ๋ฌธ์ ์ ์ ๋ฐ๊ฒฌํ์ต๋๋ค
๋ฐ๊ฒฌ๋ ๋ฌธ์
- "callee is too large": ๋ฉ์๋ ํฌ๊ธฐ๊ฐ JIT ์ปดํ์ผ๋ฌ์ ์ธ๋ผ์ด๋ ํ๊ณ(~325 ๋ฐ์ดํธ) ์ด๊ณผ
- "unpredictable branch": ๋ถ๊ธฐ ์์ธก๋ฅ 50% (chunked vs content-length)
- "callee uses too much stack":
new String(bytes, UTF_8)๋ฐ๋ณต ์์ฑ์ผ๋ก ์คํ ์๋ฐ
// ๋ฌธ์ ๊ฐ ์๋ ์๋ณธ ์ฝ๋ (93์ค, ์ฝ 450 ๋ฐ์ดํธ์ฝ๋)
public static String readRawRequest(ByteBuffer initial, InputStream in) throws IOException {
StringBuilder sb = new StringBuilder();
// ... ํค๋ ์ฝ๊ธฐ ๋ก์ง ...
// ๋ถ๊ธฐ ์์ธก ์คํจ ์์ธ
if (chunked) {
bodyStart += readChunkedBody(bin);
} else if (contentLength > -1) {
// content-length ์ฒ๋ฆฌ
}
return headers + "\r\n\r\n" + bodyStart;
}
Phase 3: ์ฑ๋ฅ ๊ฐ์ 1์ฐจ - ByteBufferPool ๋์ โ
๋ฉ๋ชจ๋ฆฌ ํ ๋น ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ByteBuffer ํ๋ง์ ๋์ ํ์ต๋๋ค.
ByteBufferPool ๊ตฌํโ
@Component
public class ByteBufferPool implements InfrastructureBean {
// ์์ฃผ ์ฐ์ด๋ ๋ฒํผ ํฌ๊ธฐ ์ฌ์ ์ ์
public static final int SMALL_BUFFER_SIZE = 2048; // 2KB โ ํ๋กํ ์ฝ ํ์ง์ฉ
public static final int MEDIUM_BUFFER_SIZE = 8192; // 8KB โ ์ผ๋ฐ ์์ฒญ ์ฝ๊ธฐ์ฉ
public static final int LARGE_BUFFER_SIZE = 32768; // 32KB โ ๋์ฉ๋ ์๋ต์ฉ
private final ConcurrentHashMap<Integer, PoolConfig> pools;
public ByteBufferPool() {
this.pools = new ConcurrentHashMap<>();
initializePool(SMALL_BUFFER_SIZE, 500);
initializePool(MEDIUM_BUFFER_SIZE, 500);
initializePool(LARGE_BUFFER_SIZE, 100);
}
public ByteBuffer acquire(int size) {
int poolSize = findPoolSize(size);
PoolConfig config = pools.get(poolSize);
ByteBuffer buffer = config.pool.poll();
if (buffer != null) {
buffer.clear();
return buffer;
}
return allocateBuffer(poolSize);
}
public void release(ByteBuffer buffer) {
if (buffer == null) return;
PoolConfig config = pools.get(buffer.capacity());
if (config == null) return;
if (config.pool.size() >= config.maxPoolSize) return;
buffer.clear();
config.pool.offer(buffer);
}
}
์ ์ฉ ๊ฒฐ๊ณผโ
์ฑ๋ฅ ํฅ์
- ์ฑ๊ณต๋ฅ : 67% โ 81% (14% ํฅ์)
- ByteBuffer ํ ๋น: ์ ์ฒด ํ ๋น์ 50% โ ๊ฑฐ์ 0%๋ก ๊ฐ์
๋ฉ๋ชจ๋ฆฌ ํ๋กํ์ผ ๋ณํ
ByteBuffer.allocate๊ด๋ จ ์คํ์ด ๊ฑฐ์ ์ฌ๋ผ์ง- GC ๋ถ๋ด ๊ฐ์๋ก JMX ์ค๋ฒํค๋๋ ํจ๊ป ๊ฐ์
- CPU ํ๋กํ์ผ์์ JMX ๊ด๋ จ CPU ์ฌ์ฉ๋ ์์ ์ ๊ฑฐ
Phase 4: ์ฑ๋ฅ ๊ฐ์ 2์ฐจ - JIT ์นํ์ ์ฝ๋ ๋ฆฌํฉํ ๋งโ
JITWatch ๋ถ์ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํ์ผ๋ก HttpUtils.readRawRequest ๋ฉ์๋๋ฅผ ๋ฆฌํฉํ ๋งํ์ต๋๋ค.
๋ฆฌํฉํ ๋ง ์์นโ
- ๋ฉ์๋ ๋ถ๋ฆฌ: ๋จ์ผ ์ฑ ์ ์์น + JIT ์ธ๋ผ์ด๋ ์ต์ ํ (< 325 ๋ฐ์ดํธ)
- ์กฐ๊ธฐ ๋ฆฌํด: ๋น๋ ๋์ ์ผ์ด์ค ์ฐ์ ์ฒ๋ฆฌ๋ก ๋ถ๊ธฐ ์์ธก๋ฅ ํฅ์
- ์ ๋ก ์นดํผ: ๋ถํ์ํ String ์์ฑ ์ ๊ฑฐ
- BufferedInputStream ์ฌ์ฌ์ฉ: ๋ฐ์ดํฐ ์์ค ๋ฐฉ์ง
๋ฉ์๋ ๋ถ๋ฆฌโ
Before: ๋จ์ผ ๊ฑฐ๋ ๋ฉ์๋ (93์ค, ~450 ๋ฐ์ดํธ์ฝ๋)โ
public static String readRawRequest(ByteBuffer initial, InputStream in) {
// 43์ค์ ๋ณต์กํ ๋ก์ง
// - ํค๋ ์ฝ๊ธฐ
// - ํ์ฑ
// - ๋ฐ๋ ์ฝ๊ธฐ (content-length/chunked)
}
After: 3๊ฐ์ ์์ ๋ฉ์๋๋ก ๋ถ๋ฆฌโ
// 1. ์กฐํฉ ๋ฉ์๋ (30์ค, ~200 ๋ฐ์ดํธ์ฝ๋)
public static String readRawRequest(ByteBuffer initial, InputStream in) throws IOException {
BufferedInputStream bin = new BufferedInputStream(in);
String headerPart = readHeadersFromStream(initial, bin);
int headerEnd = headerPart.indexOf("\r\n\r\n");
if (headerEnd < 0) return headerPart;
String headers = headerPart.substring(0, headerEnd);
String bodyStart = headerPart.substring(headerEnd + 4);
// ์กฐ๊ธฐ ๋ฆฌํด ํจํด (๋น๋ ์์)
int contentLength = parseContentLength(headers);
if (contentLength > 0) {
String body = readBodyWithContentLength(bin, contentLength, bodyStart);
return headers + "\r\n\r\n" + body;
}
if (contentLength == 0) {
return headers + "\r\n\r\n" + bodyStart;
}
if (isChunked(headers)) {
String chunkedBody = readChunkedBody(bin);
return headers + "\r\n\r\n" + bodyStart + chunkedBody;
}
return headers + "\r\n\r\n" + bodyStart;
}
// 2. ํค๋ ์ฝ๊ธฐ ์ ์ฉ (18์ค, ~120 ๋ฐ์ดํธ์ฝ๋)
private static String readHeadersFromStream(ByteBuffer initial, BufferedInputStream bin) {
StringBuilder sb = new StringBuilder();
if (initial != null && initial.hasRemaining()) {
byte[] arr = new byte[initial.remaining()];
initial.get(arr);
sb.append(new String(arr, StandardCharsets.UTF_8));
}
while (!sb.toString().contains("\r\n\r\n")) {
int ch = bin.read();
if (ch == -1) break;
sb.append((char) ch);
}
return sb.toString();
}
// 3. ๋ฐ๋ ์ฝ๊ธฐ ์ ์ฉ (10์ค, ~80 ๋ฐ์ดํธ์ฝ๋)
private static String readBodyWithContentLength(BufferedInputStream bin,
int contentLength,
String bodyStart) {
int alreadyRead = bodyStart.getBytes(StandardCharsets.UTF_8).length;
int remaining = contentLength - alreadyRead;
if (remaining <= 0) return bodyStart;
byte[] bodyBytes = bin.readNBytes(remaining);
return bodyStart + new String(bodyBytes, StandardCharsets.UTF_8);
}
๊ฐ์ ํจ๊ณผ:
- ๋ชจ๋ ๋ฉ์๋๊ฐ 325 ๋ฐ์ดํธ ์ดํ๊ฐ ๋์ด C2 ์ปดํ์ผ๋ฌ ์ธ๋ผ์ด๋ ๋์์ด ๋จ
- ๋ฉ์๋๋ณ ์ฑ ์์ด ๋ช ํํด์ ธ ํ ์คํธ์ ์ ์ง๋ณด์ ์ฉ์ด
์กฐ๊ธฐ ๋ฆฌํด ํจํดโ
Before: ๋ณต์กํ if-else ์ค์ฒฉ (๋ถ๊ธฐ ์์ธก๋ฅ 50%)โ
if (chunked) {
// chunked ์ฒ๋ฆฌ (์ค์ ๋ก๋ 10% ๋ฏธ๋ง)
} else if (contentLength > -1) {
// content-length ์ฒ๋ฆฌ (์ค์ ๋ก๋ 80% ์ด์)
}
After: ๋น๋ ์์๋๋ก ์กฐ๊ธฐ ๋ฆฌํด (์์ ๋ถ๊ธฐ ์์ธก๋ฅ 80%+)โ
// 1. Content-Length > 0 (80%+ ์ผ์ด์ค) โ ์ฆ์ ๋ฆฌํด
int contentLength = parseContentLength(headers);
if (contentLength > 0) {
String body = readBodyWithContentLength(bin, contentLength, bodyStart);
return headers + "\r\n\r\n" + body;
}
// 2. Content-Length == 0 (5% ์ผ์ด์ค) โ ์ฆ์ ๋ฆฌํด
if (contentLength == 0) {
return headers + "\r\n\r\n" + bodyStart;
}
// 3. Chunked (10% ๋ฏธ๋ง) โ ์ฆ์ ๋ฆฌํด
if (isChunked(headers)) {
String chunkedBody = readChunkedBody(bin);
return headers + "\r\n\r\n" + bodyStart + chunkedBody;
}
๊ฐ์ ํจ๊ณผ
- CPU ๋ถ๊ธฐ ์์ธก ์ฑ๊ณต๋ฅ ์ด 50% โ 80%๋ก ํฅ์
- ํ์ดํ๋ผ์ธ ํ๋ฌ์ ๋น๋ ๊ฐ์
ํค๋ ํ์ฑ ์ต์ ํโ
Before: split() + toLowerCase() (์์ฒญ๋น ~40๊ฐ ๊ฐ์ฒด ์์ฑ)โ
private static int parseContentLength(String headers) {
for (String line : headers.split("\r\n")) { // String[] ๋ฐฐ์ด ์์ฑ
if (line.toLowerCase().startsWith("content-length:")) { // String ๋ณต์ฌ
return Integer.parseInt(line.split(":")[1].trim()); // ๋ ๋ฐฐ์ด ์์ฑ
}
}
return -1;
}
After: indexOf() + ์ง์ ๋ฌธ์ ๋น๊ต (0~1๊ฐ ๊ฐ์ฒด ์์ฑ)โ
private static int parseContentLength(String headers) {
int pos = 0;
int headersLength = headers.length();
while (pos < headersLength) {
int lineEnd = headers.indexOf("\r\n", pos);
if (lineEnd < 0) lineEnd = headersLength;
// "content-length:" ๋์๋ฌธ์ ๋ฌด์ ๋น๊ต (15์)
if (regionMatchesIgnoreCase(headers, pos, "content-length:", 15)) {
int colonIdx = headers.indexOf(':', pos);
if (colonIdx < 0 || colonIdx >= lineEnd) {
pos = lineEnd + 2;
continue;
}
// ๊ฐ ์ถ์ถ (trim ์์ด ์ง์ ์ฒ๋ฆฌ)
int valueStart = colonIdx + 1;
while (valueStart < lineEnd && headers.charAt(valueStart) == ' ') {
valueStart++;
}
int valueEnd = lineEnd;
while (valueEnd > valueStart && headers.charAt(valueEnd - 1) == ' ') {
valueEnd--;
}
try {
return Integer.parseInt(headers.substring(valueStart, valueEnd));
} catch (NumberFormatException e) {
return -1;
}
}
pos = lineEnd + 2;
}
return -1;
}
๊ฐ์ ํจ๊ณผ
- ๋ฉ๋ชจ๋ฆฌ ํ ๋น: ์์ฒญ๋น
40๊ฐ โ 01๊ฐ ๊ฐ์ฒด (97.5% ๊ฐ์) - String[] ๋ฐฐ์ด ์์ฑ ์ ๊ฑฐ
- toLowerCase() ๋ณต์ฌ ์ ๊ฑฐ
- split(":") ๋ฐฐ์ด ์์ฑ ์ ๊ฑฐ
๋ฆฌํฉํ ๋ง 2์ฐจ ๊ฒฐ๊ณผโ
Full Warm-up ํ ์คํธ (์ฝ 120,000 ์์ฒญ)โ
- ์ฑ๊ณต๋ฅ : 99.73% โ 99.84% (0.11% ํฅ์)
- JIT ์ปดํ์ผ ์๊ฐ:
readRawRequest๋ณ๋ชฉ ์ง์ ์์ ํ ์ ๊ฑฐ - JITWatch ์ ์ ์ฌํญ: 0๊ฐ (๋ชจ๋ ์ต์ ํ ์๋ฃ)
GC ์ํฅ ๋ถ์โ
Young GC Count: 123 โ 127 (+3.2%)
Young GC Total Time: 55.848 ms โ 62.710 ms (+12.3%)
Average GC Time: 0.454 ms โ 0.494 ms (+8.8%)
GC ์๋ ฅ์ด ์ฝ๊ฐ ์ฆ๊ฐํ์ง๋ง ๋ฌด์ ๊ฐ๋ฅํ ์์ค์
๋๋ค. substring() ์ฌ์ฉ์ผ๋ก ์ธํ ๋จ๊ธฐ ๊ฐ์ฒด ์ฆ๊ฐ๊ฐ ์์ธ์ผ๋ก ์ถ์ ๋ฉ๋๋ค.
์ต์ข ์ฑ๋ฅ ๊ฐ์ ๊ฒฐ๊ณผโ
Partial Warm-up ํ ์คํธ (์ฝ 8,000 ์์ฒญ)โ
์ค์ ์ด์ ํ๊ฒฝ์ ๊ฐ๊น์ด ์งง์ Warm-up ์๋๋ฆฌ์ค๋ก ์ต์ข ์ฑ๋ฅ์ ์ธก์ ํ์ต๋๋ค.
| I/O ๋ชจ๋ธ | Executor ํ์ | ๊ธฐ์กด ์ฑ๊ณต๋ฅ (%) | ๊ฐ์ ํ ์ฑ๊ณต๋ฅ (%) | ๊ฐ์ ํญ(ฮ%) |
|---|---|---|---|---|
| Hybrid (BIO) | Platform Threads | 83.95 | 96.54 | +12.59 |
| Hybrid (BIO) | Virtual Threads | 88.00 | 96.25 | +8.25 |
| NIO | Platform Threads | 72.79 | 96.28 | +23.49 |
| NIO | Virtual Threads | 69.00 | 98.00 | +29.00 |
์ฃผ์ ๊ฐ์ ์ฌํญโ
1. Warm-up ์๊ฐ ๋จ์ถโ
- ์์ ํ๊น์ง ์์ ์๊ฐ: ์ฝ 60์ด โ ์ฝ 20์ด
- JIT ์ปดํ์ผ ๋ณ๋ชฉ ์ ๊ฑฐ๋ก ์ด๊ธฐ ์๋ต ์คํจ์จ ๋ํญ ๊ฐ์
2. NIO ๊ตฌ์กฐ์ ๊ทน์ ์ธ ๊ฐ์ โ
- NIO + Virtual Thread: 69% โ 98% (+29%)
- NIO + Platform Thread: 72.79% โ 96.28% (+23.49%)
- NIO ๊ตฌ์กฐ๊ฐ cold-start์ ๊ฐ์ฅ ๋ฏผ๊ฐํ์ผ๋, ๊ฐ์ ํ ๊ฐ์ฅ ํฐ ํญ์ ํฅ์
3. ๋ฉ๋ชจ๋ฆฌ ํจ์จ ๊ฐ์ โ
- ByteBuffer ํ ๋น: ์ ์ฒด ํ ๋น์ 50% โ ๊ฑฐ์ 0%
- GC ๋ถ๋ด ๊ฐ์๋ก ์์ ์ ์ธ ์๋ต ์๊ฐ ์ ์ง
- JMX ์ค๋ฒํค๋ ์์ ์ ๊ฑฐ
4. CPU ํจ์จ ํฅ์โ
- JIT ์ปดํ์ผ ์๊ฐ ๊ฐ์
- ๋ถ๊ธฐ ์์ธก ์ฑ๊ณต๋ฅ ํฅ์
- ๋ฉ์๋ ์ธ๋ผ์ด๋ ์ฑ๊ณต
๊ถ์ฅ ๊ตฌ์ฑโ
ํ ์คํธ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํ์ผ๋ก ์ฌ์ฉ ์ฌ๋ก๋ณ ๊ถ์ฅ ๊ตฌ์ฑ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค
๋์ ์ฒ๋ฆฌ๋์ด ํ์ํ API ์๋ฒโ
server:
execution-mode: hybrid
thread-type: virtual
ํน์ง
- ๊ฐ๋จํ ์ฝ๋๋ก ๋์ ์ฒ๋ฆฌ๋
- ์๋ ๋ฐฑํ๋ ์
- ๊ฐ์ ์ค๋ ๋์ ์ ์๋
์ต๋ ํ์ฅ์ฑ์ด ํ์ํ ์๋ฒ (์ฐ๊ฒฐ ์ง์ฝ์ )โ
server:
execution-mode: nio
thread-type: virtual
ํน์ง
- cold-start์์ ์ต๊ณ ๊ฐ์ ํญ (+29%)
- ์์ ํ ํ ๊ฐ์ฅ ๋์ ์ฑ๊ณต๋ฅ (98%)
- ์์ฒ ๊ฐ์ ๋์ ์ฐ๊ฒฐ ์ฒ๋ฆฌ ๊ฐ๋ฅ
์์ ์ฑ ์ฐ์ ์๋ฒโ
server:
execution-mode: hybrid
thread-type: platform
thread-pool-size: 200
ํน์ง
- ๊ฐ์ฅ ์์ธก ๊ฐ๋ฅํ ๋์
- ๋ชจ๋ ์๋๋ฆฌ์ค์์ ๊ท ํ์กํ ์ฑ๋ฅ
- ๋ ๊ฑฐ์ ํธํ์ฑ
์ถ๊ฐ ์ต์ ํ ๊ณ ๋ ค์ฌํญโ
1. JVM ์ต์ ํ๋โ
Tiered Compilationโ
# C2 ์ปดํ์ผ๋ฌ๋ง ์ฌ์ฉ (์ต๋ ์ฑ๋ฅ, ๋๋ฆฐ ์์)
-XX:TieredStopAtLevel=4
# C1 ์ปดํ์ผ๋ฌ๋ง ์ฌ์ฉ (๋น ๋ฅธ ์์, ๋ฎ์ ํผํฌ ์ฑ๋ฅ)
-XX:TieredStopAtLevel=1
JIT ์ปดํ์ผ ์๊ณ๊ฐ ์กฐ์ โ
# ๋ ๋น ๋ฅธ JIT ์ปดํ์ผ ํธ๋ฆฌ๊ฑฐ
-XX:CompileThreshold=5000
# ์ธ๋ผ์ด๋ ํ๊ณ ์ฆ๊ฐ
-XX:MaxInlineSize=500
-XX:FreqInlineSize=500
2. ์ถ๊ฐ ํ๋ง ๋์โ
ํ์ฌ ByteBuffer๋ง ํ๋งํ๊ณ ์์ง๋ง, ๋ค์ ๊ฐ์ฒด๋ค๋ ํ๋ง ๊ฐ๋ฅํฉ๋๋ค
// HttpRequest/HttpResponse ๊ฐ์ฒด ํ๋ง
@Component
public class HttpObjectPool {
private final Queue<HttpRequest<?>> requestPool = new ConcurrentLinkedQueue<>();
private final Queue<HttpResponse> responsePool = new ConcurrentLinkedQueue<>();
public HttpRequest<?> borrowRequest() { /* ... */ }
public void returnRequest(HttpRequest<?> request) { /* ... */ }
}
3. ๋ผ์ฐํ ์บ์ ๊ฐ์ โ
ํ์ฌ ๊ฐ๋จํ ์บ์ฑ๋ง ์ ์ฉ๋์ด ์์ต๋๋ค
// ํ์ฌ ๊ตฌํ
private final Map<String, PathPattern> pathPatterns = new ConcurrentHashMap<>();
// ๊ฐ์ ๊ฐ๋ฅ ๋ฐฉํฅ: LRU ์บ์
private final Cache<String, RequestMappingInfo> routingCache =
CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterAccess(10, TimeUnit.MINUTES)
.build();
4. GC ํ๋โ
# G1 GC ์ต์ ํ
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=16m
# ๋๋ ์ ์ง์ฐ GC (Shenandoah, ZGC)
-XX:+UseShenandoahGC
# -XX:+UseZGC
์ฑ๋ฅ ๋ชจ๋ํฐ๋งโ
ํ๋กํ์ผ๋ง ์คํฌ๋ฆฝํธโ
ํ๋ก์ ํธ์ ํฌํจ๋ ์ฑ๋ฅ ํ ์คํธ ์คํฌ๋ฆฝํธ๋ฅผ ํ์ฉํ ์ ์์ต๋๋ค
# async-profiler๋ฅผ ์ฌ์ฉํ ํ๋กํ์ผ๋ง
./run-performance-tests.sh
# JIT ๋ถ์์ ์ํ ์์ธ ๋ก๊ทธ
./jit-benchmark.sh
Wireshark๋ฅผ ํตํ ๋คํธ์ํฌ ๋ถ์โ
# HTTP ํธ๋ํฝ ์บก์ฒ ๋ฐ ๋ถ์
./wireshark-analysis.sh
๊ฒฐ๋ก โ
Sprout ์๋ฒ๋ ์ฒด๊ณ์ ์ธ ์ฑ๋ฅ ๋ถ์๊ณผ ๊ฐ์ ์์ ์ ํตํด ๋ค์๊ณผ ๊ฐ์ ์ฑ๊ณผ๋ฅผ ๋ฌ์ฑํ์ต๋๋ค
์ฃผ์ ์ฑ๊ณผโ
- Warm-up ์๊ฐ 67% ๋จ์ถ: 60์ด โ 20์ด
- ์ด๊ธฐ ์ฑ๊ณต๋ฅ ์ต๋ 29% ํฅ์: NIO + VT ์กฐํฉ์์ 69% โ 98%
- ๋ฉ๋ชจ๋ฆฌ ํจ์จ 97.5% ๊ฐ์ : ์์ฒญ๋น ๊ฐ์ฒด ์์ฑ 40๊ฐ โ 0~1๊ฐ
- JIT ์ปดํ์ผ ๋ณ๋ชฉ ์์ ์ ๊ฑฐ: ์ฃผ์ hot path ๋ฉ์๋ ์ธ๋ผ์ด๋ ์ฑ๊ณต
ํต์ฌ ๊ตํโ
- ํ๋กํ์ผ๋ง์ ์ค์์ฑ: async-profiler, JMC, JITWatch๋ฅผ ์กฐํฉํ์ฌ ์ ํํ ๋ณ๋ชฉ ์ง์ ํ์
- JIT ์นํ์ ์ฝ๋ฉ: ๋ฉ์๋ ํฌ๊ธฐ, ๋ถ๊ธฐ ์์ธก, ์กฐ๊ธฐ ๋ฆฌํด ํจํด์ด ์ฑ๋ฅ์ ํฐ ์ํฅ
- ๊ฐ์ฒด ํ๋ง: GC ์ธ์ด์์ ๋ฉ๋ชจ๋ฆฌ ์ฃผ๋๊ถ์ ์ก๋ ํจ๊ณผ์ ์ธ ๋ฐฉ๋ฒ
- ๊ตฌ์กฐ์ ์ ์ฐ์ฑ: ๋ค์ํ I/O ๋ชจ๋ธ๊ณผ ์ค๋ ๋ ์ ๋ต์ผ๋ก ์ฌ์ฉ ์ฌ๋ก๋ณ ์ต์ ํ ๊ฐ๋ฅ
์ด์ ๊ถ์ฅ์ฌํญโ
- ์ผ๋ฐ์ ์ธ ์น ์ ํ๋ฆฌ์ผ์ด์ : Hybrid + Virtual Thread
- ๊ณ ๋ถํ ์ค์๊ฐ ์๋น์ค: NIO + Virtual Thread
- ์์ ์ฑ ์ฐ์ : Hybrid + Platform Thread
์ ์ฑ๋ฅ ๊ฐ์ ์ ์ ๋ํ ๋ถ๊ฐ์ ์ธ ์ฌํญ์ ์ ๊ฐ์ธ ๋ธ๋ก๊ทธ์ ๊ธฐ์ฌ๋์ด ์์ต๋๋ค.