Skip to content

Commit 7246816

Browse files
committed
feat(server): introduce ByteBufferPool and integrate into connection handlers for reduced allocation overhead
- Added ByteBufferPool for efficient buffer reuse across server components - Supports configurable small(2KB), medium(8KB), and large(32KB) buffer pools - Thread-safe via ConcurrentLinkedQueue and capacity-limited to prevent leaks - Tracks allocation, acquisition, and release statistics for profiling - Integrated ByteBufferPool into DefaultConnectionManager - Replaced per-connection temporary buffer allocation with pooled buffer - Reduced GC pressure from high-frequency accept operations - Integrated ByteBufferPool into HttpConnectionHandler - Pooled 8KB read buffers per connection, released on close - Planned for pooled write buffer support via HttpUtils - Updated HttpUtils to provide overload of createResponseBuffer(ResponseEntity, ByteBufferPool) - Enables response buffer creation using pooled ByteBuffer - Preserves backward compatibility with existing static utility calls - Overall: significant reduction of ByteBuffer allocation rate and GC churn under high load
1 parent 8a3f164 commit 7246816

6 files changed

Lines changed: 119 additions & 12 deletions

File tree

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ java {
1111
}
1212

1313
group = 'org.example'
14-
version = '1.0-SNAPSHOT'
14+
version = '1.1-SNAPSHOT'
1515

1616
repositories {
1717
mavenCentral()

profile_gatling.sh

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/bin/bash
2+
# Sprout 서버 부하테스트 + Async Profiler 통합 스크립트
3+
4+
SERVER_PORT=8080
5+
DURATION=30
6+
ASYNC_PROFILER_HOME=/Users/mac/IdeaProjects/async-profiler/build
7+
SIMULATION_CLASS=benchmark.HelloWorldSimulation
8+
9+
PID=$(lsof -i :$SERVER_PORT -t)
10+
if [ -z "$PID" ]; then
11+
echo "서버가 실행 중이 아닙니다. 먼저 Sprout 서버를 실행하세요."
12+
exit 1
13+
fi
14+
echo "Sprout 서버 PID = $PID"
15+
16+
ASPROF="$ASYNC_PROFILER_HOME/bin/asprof"
17+
if [ ! -f "$ASPROF" ]; then
18+
echo "asprof 실행 파일을 찾을 수 없습니다. 경로를 확인하세요."
19+
exit 1
20+
fi
21+
22+
echo "1) Gatling 부하테스트 시작..."
23+
./gradlew gatlingRun --simulation=benchmark.HelloWorldSimulation &
24+
GATLING_PID=$!
25+
26+
sleep 3
27+
28+
echo "2) Async Profiler로 $DURATION초간 프로파일링..."
29+
env DYLD_LIBRARY_PATH=$ASYNC_PROFILER_HOME/lib $ASPROF -d $DURATION -e cpu -o flamegraph -f cpu-flamegraph.html $PID
30+
env DYLD_LIBRARY_PATH=$ASYNC_PROFILER_HOME/lib $ASPROF -d $DURATION -e alloc -o flamegraph -f alloc-flamegraph.html $PID
31+
32+
wait $GATLING_PID
33+
34+
echo "완료! 결과 파일"
35+
echo " - cpu-flamegraph.html"
36+
echo " - alloc-flamegraph.html"
37+
open cpu-flamegraph.html
38+
open alloc-flamegraph.html

src/main/java/sprout/server/HttpUtils.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,4 +227,52 @@ public static ByteBuffer createResponseBuffer(ResponseEntity<?> res) {
227227

228228
return buffer;
229229
}
230+
231+
public static ByteBuffer createResponseBuffer(ResponseEntity<?> res, ByteBufferPool pool) {
232+
if (res == null) return null;
233+
234+
byte[] bodyBytes = res.getBody() != null
235+
? res.getBody().toString().getBytes(StandardCharsets.UTF_8)
236+
: new byte[0];
237+
238+
StringBuilder header = new StringBuilder();
239+
header.append("HTTP/1.1 ")
240+
.append(res.getStatusCode().getCode())
241+
.append(" ")
242+
.append(res.getStatusCode().getMessage())
243+
.append("\r\n");
244+
245+
header.append("Content-Type: ")
246+
.append(res.getContentType())
247+
.append("\r\n");
248+
249+
header.append("Content-Length: ")
250+
.append(bodyBytes.length)
251+
.append("\r\n");
252+
253+
header.append("Connection: keep-alive\r\n");
254+
header.append("Keep-Alive: timeout=5, max=1000\r\n");
255+
256+
if (res.getHeaders() != null) {
257+
for (Map.Entry<String, String> entry : res.getHeaders().entrySet()) {
258+
header.append(entry.getKey())
259+
.append(": ")
260+
.append(entry.getValue())
261+
.append("\r\n");
262+
}
263+
}
264+
265+
header.append("\r\n");
266+
267+
byte[] headerBytes = header.toString().getBytes(StandardCharsets.UTF_8);
268+
int totalSize = headerBytes.length + bodyBytes.length;
269+
270+
// 풀에서 버퍼 대여
271+
ByteBuffer buffer = pool.acquire(totalSize);
272+
buffer.put(headerBytes);
273+
buffer.put(bodyBytes);
274+
buffer.flip();
275+
return buffer;
276+
}
277+
230278
}

src/main/java/sprout/server/builtins/DefaultConnectionManager.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
package sprout.server.builtins;
22

33
import sprout.beans.annotation.Component;
4-
import sprout.server.AcceptableProtocolHandler;
5-
import sprout.server.ConnectionManager;
6-
import sprout.server.ProtocolDetector;
7-
import sprout.server.ProtocolHandler;
4+
import sprout.server.*;
85

96
import java.nio.ByteBuffer;
107
import java.nio.channels.SelectionKey;
@@ -18,10 +15,12 @@ public class DefaultConnectionManager implements ConnectionManager {
1815

1916
private final List<ProtocolDetector> detectors;
2017
private final List<ProtocolHandler> handlers;
18+
private final ByteBufferPool bufferPool;
2119

22-
public DefaultConnectionManager(List<ProtocolDetector> detectors, List<ProtocolHandler> handlers) {
20+
public DefaultConnectionManager(List<ProtocolDetector> detectors, List<ProtocolHandler> handlers, ByteBufferPool bufferPool) {
2321
this.detectors = detectors;
2422
this.handlers = handlers;
23+
this.bufferPool = bufferPool;
2524
}
2625

2726
@Override
@@ -30,10 +29,11 @@ public void acceptConnection(SelectionKey selectionKey, Selector selector) throw
3029
SocketChannel clientChannel = serverChannel.accept();
3130
clientChannel.configureBlocking(false);
3231

33-
ByteBuffer buffer = ByteBuffer.allocate(2048);
32+
ByteBuffer buffer = bufferPool.acquire(ByteBufferPool.SMALL_BUFFER_SIZE);
3433
int bytesRead = clientChannel.read(buffer);
3534

3635
if (bytesRead <= 0) {
36+
bufferPool.release(buffer);
3737
clientChannel.close();
3838
return;
3939
}
@@ -54,6 +54,7 @@ public void acceptConnection(SelectionKey selectionKey, Selector selector) throw
5454

5555
if ("UNKNOWN".equals(detectedProtocol) || detectedProtocol == null) {
5656
System.err.println("Unknown protocol detected. Closing socket: " + clientChannel.socket());
57+
bufferPool.release(buffer);
5758
clientChannel.close();
5859
return;
5960
}
@@ -64,9 +65,10 @@ public void acceptConnection(SelectionKey selectionKey, Selector selector) throw
6465
((AcceptableProtocolHandler) handler).accept(clientChannel, selector, buffer);
6566
return; // 핸들러를 찾았으므로 종료
6667
}
67-
break;
68+
6869
}
6970
}
7071

72+
bufferPool.release(buffer);
7173
}
7274
}

src/main/java/sprout/server/builtins/HttpConnectionHandler.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,22 @@ public class HttpConnectionHandler implements ReadableHandler, WritableHandler {
2020
private final RequestDispatcher dispatcher;
2121
private final HttpRequestParser parser;
2222
private final RequestExecutorService requestExecutorService;
23+
private final ByteBufferPool bufferPool;
2324

24-
private final ByteBuffer readBuffer = ByteBuffer.allocate(8192);
25+
private final ByteBuffer readBuffer;
2526
private volatile ByteBuffer writeBuffer;
2627
private HttpConnectionStatus currentState = HttpConnectionStatus.READING;
2728

28-
public HttpConnectionHandler(SocketChannel channel, Selector selector, RequestDispatcher dispatcher, HttpRequestParser parser, RequestExecutorService requestExecutorService, ByteBuffer initialBuffer) {
29+
public HttpConnectionHandler(SocketChannel channel, Selector selector, RequestDispatcher dispatcher, HttpRequestParser parser, RequestExecutorService requestExecutorService, ByteBufferPool bufferPool, ByteBuffer initialBuffer) {
2930
this.channel = channel;
3031
this.selector = selector;
3132
this.dispatcher = dispatcher;
3233
this.parser = parser;
3334
this.requestExecutorService = requestExecutorService;
35+
this.bufferPool = bufferPool;
36+
37+
// 버퍼 풀에서 8KB 버퍼 대여
38+
this.readBuffer = bufferPool.acquire(ByteBufferPool.MEDIUM_BUFFER_SIZE);
3439

3540
if (initialBuffer != null && initialBuffer.hasRemaining()) {
3641
this.readBuffer.put(initialBuffer);
@@ -43,6 +48,7 @@ public HttpConnectionHandler(SocketChannel channel, Selector selector, RequestDi
4348
public void read(SelectionKey key) throws Exception {
4449
System.out.println("Try to read from " + channel.socket() + " with current state: " + currentState + " and buffer size: " + readBuffer.remaining() + " bytes");
4550
if (currentState != HttpConnectionStatus.READING) return;
51+
4652
System.out.println("Read from " + channel.socket() + " with current state: " + currentState + " and buffer size: " + readBuffer.remaining() + " bytes");
4753
int bytesRead = channel.read(readBuffer);
4854
if (bytesRead == -1) {
@@ -77,7 +83,7 @@ public void read(SelectionKey key) throws Exception {
7783
dispatcher.dispatch(req, res);
7884

7985
// 5. 응답 준비 및 쓰기 상태 전환
80-
this.writeBuffer = HttpUtils.createResponseBuffer(res.getResponseEntity());
86+
this.writeBuffer = HttpUtils.createResponseBuffer(res.getResponseEntity(), bufferPool);
8187
this.currentState = HttpConnectionStatus.WRITING;
8288

8389
// 6. Selector에 쓰기 이벤트 감지 요청
@@ -92,6 +98,9 @@ public void read(SelectionKey key) throws Exception {
9298

9399
// 버퍼 초기화 (다음 요청을 위해)
94100
readBuffer.clear();
101+
} else {
102+
// 아직 요청이 완전하지 않으면 다음 read 대기
103+
readBuffer.compact();
95104
}
96105

97106
}
@@ -102,6 +111,11 @@ private void closeConnection(SelectionKey key) {
102111
channel.close();
103112
} catch (IOException e) {
104113
e.printStackTrace();
114+
} finally {
115+
bufferPool.release(readBuffer);
116+
if (writeBuffer != null) {
117+
bufferPool.release(writeBuffer);
118+
}
105119
}
106120
}
107121

@@ -114,6 +128,8 @@ public void write(SelectionKey key) throws IOException {
114128
if (!writeBuffer.hasRemaining()) {
115129
// 버퍼의 모든 데이터를 전송 완료
116130
this.currentState = HttpConnectionStatus.READING;
131+
132+
bufferPool.release(writeBuffer);
117133
this.writeBuffer = null;
118134

119135
// keep-alive 지원: 다음 요청을 기다리기 위해 READ 모드로 전환

src/test/java/sprout/server/builtins/DefaultConnectionManagerTest.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import org.mockito.Mock;
88
import org.mockito.junit.jupiter.MockitoExtension;
99
import sprout.server.AcceptableProtocolHandler;
10+
import sprout.server.ByteBufferPool;
1011
import sprout.server.ProtocolDetector;
1112

1213
import java.nio.ByteBuffer;
@@ -37,12 +38,14 @@ class DefaultConnectionManagerTest {
3738
private ServerSocketChannel mockServerChannel;
3839
@Mock
3940
private SocketChannel mockClientChannel;
41+
@Mock
42+
private ByteBufferPool mockByteBufferPool;
4043

4144
private DefaultConnectionManager connectionManager;
4245

4346
@BeforeEach
4447
void setUp() {
45-
connectionManager = new DefaultConnectionManager(List.of(mockDetector), List.of(mockHandler));
48+
connectionManager = new DefaultConnectionManager(List.of(mockDetector), List.of(mockHandler), mockByteBufferPool);
4649
}
4750

4851
@Test

0 commit comments

Comments
 (0)