diff --git a/.gitignore b/.gitignore index 2c68eeab2..d11041834 100644 --- a/.gitignore +++ b/.gitignore @@ -123,3 +123,6 @@ run !test/ctx_register.js .egg/ + +# Benchmark test files +benchmark/stream_download/nginx/50mb_ones.txt diff --git a/benchmark/stream_download/.gitignore b/benchmark/stream_download/.gitignore new file mode 100644 index 000000000..fb9a6d1af --- /dev/null +++ b/benchmark/stream_download/.gitignore @@ -0,0 +1,4 @@ +tmp +coredumps +*.heapsnapshot +core.* diff --git a/benchmark/stream_download/COREDUMP_ANALYSIS.md b/benchmark/stream_download/COREDUMP_ANALYSIS.md new file mode 100644 index 000000000..f18036c20 --- /dev/null +++ b/benchmark/stream_download/COREDUMP_ANALYSIS.md @@ -0,0 +1,188 @@ +# Node.js Coredump Analysis Guide + +This document describes how to analyze Node.js coredump files for memory leak detection. + +## Prerequisites + +- Docker container with `gdb` and `procps` installed +- Core dump file generated with `ulimit -c unlimited` +- Node.js built with debug symbols (optional but helpful) + +## Generating Coredump + +```bash +# Run benchmark and generate coredump +./run-benchmark-with-coredump.sh 60 + +# Or manually: +# 1. Start benchmark +docker exec -d nginx-benchmark-server bash -c "cd /root/workspace && node --expose-gc --heapsnapshot-signal=SIGUSR2 benchmark.js" + +# 2. Get PID +docker exec nginx-benchmark-server cat /tmp/benchmark.pid + +# 3. Generate heap snapshot (optional) +docker exec nginx-benchmark-server kill -SIGUSR2 + +# 4. Generate coredump +docker exec nginx-benchmark-server kill -SIGABRT + +# 5. Copy coredump +./copy-coredump.sh +``` + +## Analysis Methods + +### Method 1: Benchmark Log Analysis + +Extract memory stats from benchmark log: + +```bash +# Get memory trend +grep -E "(rss:|heapUsed:|external:|arrayBuffers:)" benchmark.log | paste - - - - | awk '{ + gsub(/,/,"",$0); + printf "RSS=%3.0fMB, heapUsed=%2.0fMB, external=%2.0fMB, arrayBuffers=%2.0fMB\n", + $2/1024/1024, $4/1024/1024, $6/1024/1024, $8/1024/1024; +}' + +# Calculate statistics +grep "rss:" benchmark.log | awk '{gsub(/,/,"",$2); print $2}' | sort -n | tail -5 +``` + +### Method 2: String Extraction from Coredump + +```bash +# Find error patterns +strings core.58 | grep -E "(Error|ENOMEM|EMFILE|leak)" | head -50 + +# Find memory stats captured in core +strings core.58 | grep -E "(heapTotal|heapUsed|external|arrayBuffers|rss):" | tail -20 + +# Count object references (e.g., temp files by UUID) +strings core.58 | grep -oE "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" | wc -l + +# Find connection/socket patterns +strings core.58 | grep -E "(Socket|Stream|Pool|Agent|keepAlive)" | sort | uniq -c | sort -rn | head -20 + +# Find error codes +strings core.58 | grep -E "(ECONNRESET|ETIMEDOUT|ENOTFOUND|ECONNREFUSED|EPIPE)" | head -20 + +# Find loaded modules +strings core.58 | grep -E "node_modules/.+\.js" | sort | uniq -c | sort -rn | head -30 +``` + +### Method 3: LLDB Analysis (macOS) + +```bash +# Load coredump +lldb -c core.58 + +# Commands in lldb: +(lldb) bt all # Backtrace all threads +(lldb) thread list # List all threads +(lldb) memory region --all # Show memory regions +(lldb) process status # Process state +``` + +### Method 4: GDB Analysis (Linux/Docker) + +```bash +# Copy coredump to container +docker cp core.58 nginx-benchmark-server:/tmp/core.58 + +# Analyze with GDB +docker exec -it nginx-benchmark-server gdb /usr/local/bin/node /tmp/core.58 + +# GDB commands: +(gdb) bt # Backtrace +(gdb) info threads # List threads +(gdb) thread apply all bt # Backtrace all threads +(gdb) info registers # Register state +``` + +### Method 5: llnode (Node.js LLDB Plugin) + +```bash +# Install llnode +npm install -g llnode + +# Analyze V8 heap +lldb -c core.58 +(lldb) plugin load /path/to/llnode.dylib +(lldb) v8 bt # V8-aware backtrace +(lldb) v8 findjsobjects # Find JS objects by type +(lldb) v8 findjsinstances Array # Find Array instances +``` + +## Memory Metrics Reference + +| Metric | Description | Normal Range | +| -------------- | ---------------------------------------- | ---------------------------- | +| `rss` | Resident Set Size (total process memory) | Varies, should stabilize | +| `heapTotal` | V8 heap allocated | Grows then stabilizes | +| `heapUsed` | V8 heap actually used | Should not continuously grow | +| `external` | Memory for C++ objects bound to JS | Fluctuates with I/O | +| `arrayBuffers` | Memory for ArrayBuffer/TypedArray | Fluctuates with I/O | + +## Memory Leak Indicators + +### Leak Detected: + +- `heapUsed` continuously growing without returning to baseline +- `rss` continuously growing over time +- ENOMEM errors in strings output +- EMFILE (too many open files) errors +- Thousands of duplicate object references + +### No Leak (Healthy): + +- `heapUsed` fluctuates but returns to baseline +- `rss` stabilizes after initial growth +- `external` and `arrayBuffers` fluctuate with I/O operations +- GC running regularly (check GC stats in log) + +## Example Analysis Report + +``` +=== Memory Analysis Report === + +Sample count: 147 +Duration: 60 seconds +Operations: 9200 download/upload cycles + +Memory State: +- Initial RSS: 235 MB +- Final RSS: 328 MB +- Max RSS: 360 MB +- Growth: 93 MB (40%) - NORMAL (initial allocation) + +V8 Heap (heapUsed): 12-20 MB - STABLE (no leak) +External Memory: 5-85 MB - FLUCTUATING (normal for I/O) +ArrayBuffers: 0-74 MB - FLUCTUATING (normal for file ops) + +Conclusion: NO MEMORY LEAK DETECTED +``` + +## Troubleshooting + +### Coredump not generated + +```bash +# Check ulimit +docker exec container ulimit -c + +# Set unlimited +docker run --ulimit core=-1 --privileged ... +``` + +### Architecture mismatch (Rosetta) + +If running on Apple Silicon with x86_64 container: + +- Use `strings` extraction method +- Or run native ARM64 container: `--platform linux/arm64` + +### Missing symbols in GDB/LLDB + +- Use Node.js debug build +- Or rely on string extraction methods diff --git a/benchmark/stream_download/Dockerfile b/benchmark/stream_download/Dockerfile new file mode 100644 index 000000000..5f04ec9e9 --- /dev/null +++ b/benchmark/stream_download/Dockerfile @@ -0,0 +1,50 @@ +FROM node:24.12.0 + +# 安装 nginx 和其他必要工具 +RUN apt-get update && apt-get install -y \ + nginx \ + curl \ + vim \ + gdb \ + procps \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get clean + +# 配置 coredump +RUN echo "ulimit -c unlimited" >> /etc/bash.bashrc \ + && mkdir -p /tmp/cores \ + && chmod 777 /tmp/cores + +# 创建 nginx 配置目录 +RUN mkdir -p /etc/nginx/conf.d + +# 复制 nginx 配置文件 +COPY nginx.conf /etc/nginx/sites-available/default + +# 创建 nginx 工作目录 +RUN mkdir -p /var/www/html + +# 创建启动脚本 +COPY start-nginx.sh /usr/local/bin/start-nginx.sh +RUN chmod +x /usr/local/bin/start-nginx.sh + +# 暴露端口 +EXPOSE 80 9229 + +# 设置工作目录 +WORKDIR /var/www/html + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost/health || exit 1 + +RUN mkdir -p /root/workspace + +COPY gc.js /root/workspace/gc.js +COPY benchmark.js /root/workspace/benchmark.js +COPY benchmark_undici.js /root/workspace/benchmark_undici.js + +RUN cd /root/workspace && npm i urllib --registry https://registry.npmmirror.com + +# 启动命令 +CMD ["/usr/local/bin/start-nginx.sh"] \ No newline at end of file diff --git a/benchmark/stream_download/README.md b/benchmark/stream_download/README.md new file mode 100644 index 000000000..e31ef7d27 --- /dev/null +++ b/benchmark/stream_download/README.md @@ -0,0 +1,115 @@ +# Nginx 下载/上传测试服务器 + +## 快速开始 + +> **注意**: 请先切换到 `benchmark/stream_download` 目录下执行以下命令 + +### 构建镜像 + +```bash +docker build --platform linux/amd64 -t nginx-node-benchmark . +``` + +### 运行容器 + +```bash +docker run --rm -d --platform linux/amd64 \ + --name nginx-node-benchmark \ + -p 8080:80 \ + -v $(pwd)/nginx:/var/www/html \ + nginx-node-benchmark +``` + +### 测试 + +```bash +# 下载测试 +curl -v http://localhost:8080/download/test-file.txt + +# 上传测试 +curl -v -X POST -d "test" http://localhost:8080/upload/ +``` + +### 停止 + +```bash +docker stop nginx-node-benchmark && docker rm nginx-node-benchmark +``` + +### 运行生成大文件 + +```bash +sh generate_50mb_file.sh +``` + +### 运行 node 测试 + +```bash +docker exec -ti nginx-node-benchmark bash + +cd /root/workspace +node benchmark.js +``` + +## 内存分析 (Memory Leak Analysis) + +### 运行 benchmark 并生成 coredump + +运行 benchmark 60 秒后自动生成 heap snapshot 和 coredump: + +```bash +# 使用默认 60 秒 +./run-benchmark-with-coredump.sh + +# 或指定运行时间(秒) +./run-benchmark-with-coredump.sh 120 +``` + +### 手动复制 coredump 文件 + +如果需要手动复制 coredump 和 heap snapshot 文件: + +```bash +./copy-coredump.sh +``` + +### 分析 heap snapshot + +1. 打开 Chrome DevTools -> Memory 标签 +2. 点击 "Load" 加载 `coredumps/*.heapsnapshot` 文件 +3. 分析内存分配情况 + +### 分析 coredump + +详细的 coredump 分析指南请参考 [COREDUMP_ANALYSIS.md](./COREDUMP_ANALYSIS.md) + +快速分析方法: + +```bash +# 使用 strings 提取内存信息 +strings coredumps/core.* | grep -E "(heapUsed|rss|external):" | tail -20 + +# 查找内存相关错误 +strings coredumps/core.* | grep -E "(ENOMEM|EMFILE|leak)" | head -20 + +# 统计对象引用数量 +strings coredumps/core.* | grep -oE "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" | wc -l + +# 在容器内使用 gdb 分析 +docker exec -it nginx-benchmark-server gdb /usr/local/bin/node /tmp/core.* + +# 或将 coredump 复制到本地后使用 lldb 分析 +lldb -c coredumps/core.* +``` + +### 手动触发 heap snapshot + +在 benchmark 运行时发送 SIGUSR2 信号生成 heap snapshot: + +```bash +# 获取 benchmark 进程 PID +docker exec nginx-benchmark-server cat /tmp/benchmark.pid + +# 发送信号生成 heap snapshot +docker exec nginx-benchmark-server kill -SIGUSR2 +``` diff --git a/benchmark/stream_download/benchmark.js b/benchmark/stream_download/benchmark.js new file mode 100644 index 000000000..ff2fb0b2d --- /dev/null +++ b/benchmark/stream_download/benchmark.js @@ -0,0 +1,74 @@ +const { HttpClient } = require('urllib'); +const { Agent } = require('undici'); +const fs = require('fs'); +const crypto = require('crypto'); +const path = require('path'); +const { setTimeout } = require('timers/promises'); + +require('./gc.js'); + +const tmp = path.join(__dirname, 'tmp'); +fs.mkdirSync(tmp, { recursive: true }); + +const BASE_URL = process.argv[2] || 'http://127.0.0.1'; + +// Create HttpClient with smaller connection pool +const POOL_SIZE = parseInt(process.env.POOL_SIZE || '2', 10); +const agent = new Agent({ + connections: POOL_SIZE, // Max connections per origin + pipelining: 1, // Disable pipelining + keepAliveTimeout: 4000, // 4 seconds + keepAliveMaxTimeout: 10000, // 10 seconds max +}); +const httpClient = new HttpClient(); +httpClient.setDispatcher(agent); + +console.log(`Using connection pool size: ${POOL_SIZE}`); +let count = 0; +async function downloadAndUpload() { + const tmpFilePath = path.join(tmp, `${crypto.randomUUID()}.txt`); + const downloadResponse = await httpClient.request(`${BASE_URL}/download/50mb_ones.txt`, { + writeStream: fs.createWriteStream(tmpFilePath), + }); + const uploadResponse = await httpClient.request(`${BASE_URL}/upload/`, { + method: 'POST', + stream: fs.createReadStream(tmpFilePath), + }); + await fs.promises.rm(tmpFilePath); + count++; + if (count % 100 === 0) { + console.log( + `Downloaded and uploaded ${count} times, downloadResponse: ${downloadResponse.status}, uploadResponse: ${uploadResponse.status}`, + ); + } +} + +let downloading = true; +(async () => { + while (true) { + if (downloading) { + await Promise.all([downloadAndUpload(), downloadAndUpload(), downloadAndUpload()]); + } else { + await setTimeout(100); + if (globalThis.gc) { + globalThis.gc(); + } + } + } +})(); +(async () => { + while (true) { + if (downloading) { + await Promise.all([downloadAndUpload(), downloadAndUpload(), downloadAndUpload()]); + } else { + await setTimeout(100); + if (globalThis.gc) { + globalThis.gc(); + } + } + } +})(); + +process.on('SIGUSR2', () => { + downloading = !downloading; +}); diff --git a/benchmark/stream_download/benchmark_undici.js b/benchmark/stream_download/benchmark_undici.js new file mode 100644 index 000000000..e73994c6c --- /dev/null +++ b/benchmark/stream_download/benchmark_undici.js @@ -0,0 +1,39 @@ +const { request } = require('undici'); +const fs = require('fs'); +const crypto = require('crypto'); +const path = require('path'); +const { pipeline } = require('stream/promises'); + +async function downloadAndUpload() { + const tmpFilePath = path.join(__dirname, `${crypto.randomUUID()}.txt`); + + // Download file + const downloadResponse = await request('http://127.0.0.1/download/50mb_ones.txt'); + await pipeline(downloadResponse.body, fs.createWriteStream(tmpFilePath)); + + // Upload file + await request('http://127.0.0.1/upload/', { + method: 'POST', + body: fs.createReadStream(tmpFilePath), + }); + + await fs.promises.rm(tmpFilePath); +} + +let downloading = true; +(async () => { + while (true) { + if (downloading) { + await downloadAndUpload(); + } else { + await setTimeout(100); + if (globalThis.gc) { + globalThis.gc(); + } + } + } +})(); + +process.on('SIGUSR2', () => { + downloading = !downloading; +}); diff --git a/benchmark/stream_download/copy-coredump.sh b/benchmark/stream_download/copy-coredump.sh new file mode 100755 index 000000000..6a982c407 --- /dev/null +++ b/benchmark/stream_download/copy-coredump.sh @@ -0,0 +1,106 @@ +#!/bin/bash + +# Copy coredump and heap snapshot files from Docker container +set -e + +CONTAINER_NAME="nginx-benchmark-server" +OUTPUT_DIR="$(pwd)/coredumps" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +echo -e "${GREEN}=== Copy Coredump and Heap Snapshots ===${NC}" + +# Create output directory +mkdir -p "$OUTPUT_DIR" + +# Check if container is running +if ! docker ps -a | grep -q "$CONTAINER_NAME"; then + echo -e "${RED}Error: Container $CONTAINER_NAME not found${NC}" + exit 1 +fi + +# Copy heap snapshots +echo -e "${YELLOW}Looking for heap snapshots...${NC}" +HEAP_FILES=$(docker exec "$CONTAINER_NAME" bash -c "find /root/workspace -name '*.heapsnapshot' 2>/dev/null" || true) +if [ -n "$HEAP_FILES" ]; then + echo "$HEAP_FILES" | while read -r file; do + if [ -n "$file" ]; then + FILENAME=$(basename "$file") + docker cp "$CONTAINER_NAME:$file" "$OUTPUT_DIR/$FILENAME" + echo -e "${GREEN}Copied heap snapshot: $FILENAME${NC}" + fi + done +else + echo "No heap snapshots found" +fi + +# Copy core dumps from /tmp +echo -e "${YELLOW}Looking for core dumps in /tmp...${NC}" +CORE_FILES=$(docker exec "$CONTAINER_NAME" bash -c "find /tmp -name 'core*' -type f 2>/dev/null" || true) +if [ -n "$CORE_FILES" ]; then + echo "$CORE_FILES" | while read -r file; do + if [ -n "$file" ]; then + FILENAME=$(basename "$file") + echo -e "${GREEN}Found core dump: $file${NC}" + docker cp "$CONTAINER_NAME:$file" "$OUTPUT_DIR/$FILENAME" + echo -e "${GREEN}Copied: $FILENAME${NC}" + fi + done +else + echo "No core dumps found in /tmp" +fi + +# Copy core dumps from /tmp/cores +echo -e "${YELLOW}Looking for core dumps in /tmp/cores...${NC}" +CORE_FILES=$(docker exec "$CONTAINER_NAME" bash -c "find /tmp/cores -name 'core*' -type f 2>/dev/null" || true) +if [ -n "$CORE_FILES" ]; then + echo "$CORE_FILES" | while read -r file; do + if [ -n "$file" ]; then + FILENAME=$(basename "$file") + echo -e "${GREEN}Found core dump: $file${NC}" + docker cp "$CONTAINER_NAME:$file" "$OUTPUT_DIR/$FILENAME" + echo -e "${GREEN}Copied: $FILENAME${NC}" + fi + done +else + echo "No core dumps found in /tmp/cores" +fi + +# Copy core dumps from workspace +echo -e "${YELLOW}Looking for core dumps in /root/workspace...${NC}" +CORE_FILES=$(docker exec "$CONTAINER_NAME" bash -c "find /root/workspace -name 'core*' -type f 2>/dev/null" || true) +if [ -n "$CORE_FILES" ]; then + echo "$CORE_FILES" | while read -r file; do + if [ -n "$file" ]; then + FILENAME=$(basename "$file") + echo -e "${GREEN}Found core dump: $file${NC}" + docker cp "$CONTAINER_NAME:$file" "$OUTPUT_DIR/$FILENAME" + echo -e "${GREEN}Copied: $FILENAME${NC}" + fi + done +else + echo "No core dumps found in /root/workspace" +fi + +# Copy benchmark log if exists +if docker exec "$CONTAINER_NAME" test -f /tmp/benchmark.log 2>/dev/null; then + docker cp "$CONTAINER_NAME:/tmp/benchmark.log" "$OUTPUT_DIR/benchmark.log" + echo -e "${GREEN}Copied benchmark.log${NC}" +fi + +echo "" +echo -e "${GREEN}=== Copy Complete ===${NC}" +echo "Output files are in: $OUTPUT_DIR" +ls -la "$OUTPUT_DIR" 2>/dev/null || echo "No files copied" + +echo "" +echo -e "${YELLOW}To analyze heap snapshot:${NC}" +echo " 1. Open Chrome DevTools -> Memory tab" +echo " 2. Click 'Load' and select the .heapsnapshot file" +echo "" +echo -e "${YELLOW}To analyze core dump with gdb:${NC}" +echo " docker exec -it $CONTAINER_NAME gdb /usr/local/bin/node /tmp/core.*" diff --git a/benchmark/stream_download/curl-format.txt b/benchmark/stream_download/curl-format.txt new file mode 100644 index 000000000..be3b2faca --- /dev/null +++ b/benchmark/stream_download/curl-format.txt @@ -0,0 +1,16 @@ +time_namelookup: %{time_namelookup}\n + time_connect: %{time_connect}\n + time_appconnect: %{time_appconnect}\n + time_pretransfer: %{time_pretransfer}\n + time_redirect: %{time_redirect}\n + time_starttransfer: %{time_starttransfer}\n + ----------\n + time_total: %{time_total}\n + ----------\n + size_download: %{size_download}\n + speed_download: %{speed_download}\n + size_request: %{size_request}\n + speed_upload: %{speed_upload}\n + content_type: %{content_type}\n + num_connects: %{num_connects}\n + num_redirects: %{num_redirects} \ No newline at end of file diff --git a/benchmark/stream_download/docker-compose.nginx.yml b/benchmark/stream_download/docker-compose.nginx.yml new file mode 100644 index 000000000..e36414a33 --- /dev/null +++ b/benchmark/stream_download/docker-compose.nginx.yml @@ -0,0 +1,23 @@ +version: '3.8' + +services: + nginx-benchmark: + image: nginx:alpine + container_name: nginx-benchmark-server + ports: + - '8080:80' + volumes: + - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro + - ./nginx:/usr/share/nginx/html:ro + restart: unless-stopped + + # 可选:使用 openresty 支持 Lua 模块 + # openresty-benchmark: + # image: openresty/openresty:alpine + # container_name: openresty-benchmark-server + # ports: + # - "8080:80" + # volumes: + # - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro + # - ./nginx:/usr/share/nginx/html:ro + # restart: unless-stopped diff --git a/benchmark/stream_download/gc.js b/benchmark/stream_download/gc.js new file mode 100644 index 000000000..6d44c0667 --- /dev/null +++ b/benchmark/stream_download/gc.js @@ -0,0 +1,79 @@ +const { PerformanceObserver, constants } = require('node:perf_hooks'); + +const gcStats = { + totalGCDuration: 0, // ms + count: 0, + byKind: { + scavenge: 0, // minor GC + markSweepCompact: 0, // major GC + incremental: 0, + weakc: 0, + unknown: 0, + }, +}; + +// kind meaning: https://nodejs.org/api/perf_hooks.html#performancegc_kind +// 1: scavenge +// 2: mark-sweep-compact +// 4: incremental +// 8: weak callbacks +function kindToString(kind) { + switch (kind) { + case constants.NODE_PERFORMANCE_GC_MAJOR: + return 'markSweepCompact'; + case constants.NODE_PERFORMANCE_GC_MINOR: + return 'scavenge'; + case constants.NODE_PERFORMANCE_GC_INCREMENTAL: + return 'incremental'; + case constants.NODE_PERFORMANCE_GC_WEAKCB: + return 'weakc'; + default: + return 'unknown'; + } +} + +const obs = new PerformanceObserver((list) => { + const entries = list.getEntries(); + for (const entry of entries) { + gcStats.totalGCDuration += entry.duration; + gcStats.count += 1; + + const kindCode = entry.detail?.kind; + const kind = kindToString(kindCode); + if (!gcStats.byKind[kind]) gcStats.byKind[kind] = 0; + gcStats.byKind[kind] += entry.duration; + } +}); + +obs.observe({ entryTypes: ['gc'] }); + +// for other modules to use +function getGCStats() { + return { + totalGCDuration: gcStats.totalGCDuration, + count: gcStats.count, + avgDuration: gcStats.count ? gcStats.totalGCDuration / gcStats.count : 0, + byKind: { ...gcStats.byKind }, + }; +} + +// only print GC stats if the GC environment variable is set +if (process.env.GC || true) { + setInterval(() => { + const stats = getGCStats(); + console.log(''); + console.log( + '[GC]', + 'total(ms)=', + stats.totalGCDuration.toFixed(2), + 'count=', + stats.count, + 'avg(ms)=', + stats.avgDuration.toFixed(2), + 'byKind=', + stats.byKind, + ); + // process memory usage + console.log('process memory usage=', process.memoryUsage()); + }, 2000); +} diff --git a/benchmark/stream_download/generate_50mb_file.sh b/benchmark/stream_download/generate_50mb_file.sh new file mode 100644 index 000000000..340d102b6 --- /dev/null +++ b/benchmark/stream_download/generate_50mb_file.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# 生成一个50MB的txt文件,内容都是1 +# 文件名为: 50mb_ones.txt + +OUTPUT_FILE="nginx/50mb_ones.txt" +TARGET_SIZE_MB=50 +TARGET_SIZE_BYTES=$((TARGET_SIZE_MB * 1024 * 1024)) + +# 检查文件是否已存在 +if [ -f "$OUTPUT_FILE" ]; then + echo "文件 $OUTPUT_FILE 已存在,正在删除..." + rm -f "$OUTPUT_FILE" +fi + +echo "正在生成 $TARGET_SIZE_MB MB 的文件,内容都是1..." + +# 使用dd命令生成文件,每块1KB,共50*1024块 +dd if=/dev/zero bs=1024 count=$((TARGET_SIZE_MB * 1024)) | tr '\0' '1' > "$OUTPUT_FILE" + +# 验证文件大小 +ACTUAL_SIZE=$(stat -f%z "$OUTPUT_FILE" 2>/dev/null || stat -c%s "$OUTPUT_FILE" 2>/dev/null) +if [ "$ACTUAL_SIZE" -eq "$TARGET_SIZE_BYTES" ]; then + echo "成功生成文件: $OUTPUT_FILE" + echo "文件大小: $(ls -lh "$OUTPUT_FILE" | awk '{print $5}')" +else + echo "警告: 文件大小不匹配,期望: $TARGET_SIZE_BYTES 字节,实际: $ACTUAL_SIZE 字节" +fi + +echo "文件路径: $(pwd)/$OUTPUT_FILE" \ No newline at end of file diff --git a/benchmark/stream_download/nginx.conf b/benchmark/stream_download/nginx.conf new file mode 100644 index 000000000..39a08eb98 --- /dev/null +++ b/benchmark/stream_download/nginx.conf @@ -0,0 +1,78 @@ +server { + listen 80; + server_name localhost; + + # 设置根目录为 nginx 文件夹 + root /var/www/html/; + + # 禁用缓存以支持流式下载测试 + sendfile off; + tcp_nopush off; + tcp_nodelay on; + keepalive_timeout 65; + + # 下载路径 - GET /download/ + location /download/ { + # 映射到 nginx 目录中的文件 + alias /var/www/html/; + autoindex on; + autoindex_exact_size off; + + # 支持断点续传 + add_header Accept-Ranges bytes; + + # 设置合适的缓存头用于测试 + expires -1; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + + # 允许跨域访问(测试需要) + add_header Access-Control-Allow-Origin *; + add_header Access-Control-Allow-Methods "GET, OPTIONS"; + add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range"; + + if ($request_method = 'OPTIONS') { + return 204; + } + } + + # 上传路径 - POST /upload/ + location /upload/ { + # 只允许 POST 方法 + limit_except POST { + deny all; + } + + # 将上传内容重定向到 /dev/null + client_body_in_file_only clean; + client_body_temp_path /tmp/nginx_temp; + client_max_body_size 0; # 不限制上传大小 + + # 使用 Lua 模块或直接返回 201 + return 201; + + # 如果需要更复杂的处理,可以使用 Lua + # content_by_lua_block { + # ngx.req.read_body() + # local data = ngx.req.get_body_data() + # -- 数据已经被读取,但不做任何处理 + # ngx.status = 201 + # ngx.say("{\"status\":\"uploaded\",\"bytes_received\":" .. ngx.req.get_headers()["content-length"] .. "}") + # ngx.exit(201) + # } + } + + # 健康检查端点 + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + + # 错误页面 + error_page 404 /404.html; + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} \ No newline at end of file diff --git a/benchmark/stream_download/nginx/test-file.txt b/benchmark/stream_download/nginx/test-file.txt new file mode 100644 index 000000000..251dc74ce --- /dev/null +++ b/benchmark/stream_download/nginx/test-file.txt @@ -0,0 +1,16 @@ +这是一个测试文件,用于 nginx 下载测试。 + +文件内容: +- 测试文件大小:约 1KB +- 用途:验证 /download/ 路径的文件下载功能 +- 创建时间:2025-12-24 + +可以通过以下方式测试: +1. GET http://localhost:8080/download/test-file.txt +2. 使用 curl: curl -O http://localhost:8080/download/test-file.txt +3. 使用 wget: wget http://localhost:8080/download/test-file.txt + +测试上传功能: +1. POST http://localhost:8080/upload/ +2. 使用 curl: curl -X POST -d "test data" http://localhost:8080/upload/ +3. 使用 curl 上传文件: curl -X POST --data-binary @test-file.txt http://localhost:8080/upload/ \ No newline at end of file diff --git a/benchmark/stream_download/package.json b/benchmark/stream_download/package.json new file mode 100644 index 000000000..5bbefffba --- /dev/null +++ b/benchmark/stream_download/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/benchmark/stream_download/run-benchmark-with-coredump.sh b/benchmark/stream_download/run-benchmark-with-coredump.sh new file mode 100755 index 000000000..064f29a98 --- /dev/null +++ b/benchmark/stream_download/run-benchmark-with-coredump.sh @@ -0,0 +1,132 @@ +#!/bin/bash + +# Run benchmark for 60 seconds and generate coredump + heap dump for memory analysis +set -e + +CONTAINER_NAME="nginx-benchmark-server" +BENCHMARK_DURATION=${1:-60} # Default 60 seconds +OUTPUT_DIR="$(pwd)/coredumps" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +echo -e "${GREEN}=== Run Benchmark with Coredump ===${NC}" +echo "Duration: ${BENCHMARK_DURATION} seconds" + +# Create output directory +mkdir -p "$OUTPUT_DIR" + +# Cleanup old files +echo -e "${YELLOW}Cleaning up old core files and heap snapshots...${NC}" +rm -f "$OUTPUT_DIR"/core.* "$OUTPUT_DIR"/*.heapsnapshot "$OUTPUT_DIR"/benchmark.log 2>/dev/null || true +echo "Local cleanup done" + +# Check if container is running +if ! docker ps | grep -q "$CONTAINER_NAME"; then + echo -e "${RED}Error: Container $CONTAINER_NAME is not running${NC}" + echo "Please run ./start-docker.sh first" + exit 1 +fi + +# Cleanup old files in container +echo -e "${YELLOW}Cleaning up old files in container...${NC}" +docker exec "$CONTAINER_NAME" bash -c "rm -f /tmp/core.* /tmp/benchmark.log /tmp/benchmark.pid /root/workspace/*.heapsnapshot /root/workspace/tmp/*.txt 2>/dev/null || true" +echo "Container cleanup done" + +# Enable core dumps in the container +echo -e "${YELLOW}Enabling core dumps in container...${NC}" +docker exec "$CONTAINER_NAME" bash -c "ulimit -c unlimited && echo '/tmp/core.%p' > /proc/sys/kernel/core_pattern 2>/dev/null || true" + +# Start the benchmark with --expose-gc and --heapsnapshot-signal flags +echo -e "${GREEN}Starting benchmark...${NC}" +docker exec -d "$CONTAINER_NAME" bash -c "cd /root/workspace && ulimit -c unlimited && exec node --expose-gc --heapsnapshot-signal=SIGUSR2 benchmark.js http://127.0.0.1 > /tmp/benchmark.log 2>&1" + +# Wait for process to start +sleep 2 + +# Get the PID using pgrep to find the actual node process running benchmark.js +BENCHMARK_PID=$(docker exec "$CONTAINER_NAME" pgrep -f "node.*benchmark.js" 2>/dev/null | head -1 || echo "") +if [ -z "$BENCHMARK_PID" ]; then + echo -e "${RED}Error: Failed to get benchmark PID${NC}" + echo "Checking running processes..." + docker exec "$CONTAINER_NAME" ps aux | grep -E "(node|benchmark)" || true + exit 1 +fi +# Save PID for other scripts to use +docker exec "$CONTAINER_NAME" bash -c "echo $BENCHMARK_PID > /tmp/benchmark.pid" +echo -e "${GREEN}Benchmark started with PID: $BENCHMARK_PID${NC}" + +# Monitor memory usage while running +echo -e "${YELLOW}Running benchmark for $BENCHMARK_DURATION seconds...${NC}" +ELAPSED=0 +while [ $ELAPSED -lt $BENCHMARK_DURATION ]; do + sleep 10 + ELAPSED=$((ELAPSED + 10)) + echo -e "${GREEN}[$ELAPSED/$BENCHMARK_DURATION seconds]${NC} Checking memory..." + docker exec "$CONTAINER_NAME" bash -c "ps -o pid,rss,vsz,comm -p $BENCHMARK_PID 2>/dev/null || echo 'Process info not available'" + + # Show last few lines of benchmark log + docker exec "$CONTAINER_NAME" tail -5 /tmp/benchmark.log 2>/dev/null || true +done + +echo -e "${YELLOW}Benchmark duration complete. Generating heap snapshot...${NC}" + +# Generate heap snapshot by sending SIGUSR2 +docker exec "$CONTAINER_NAME" kill -SIGUSR2 $BENCHMARK_PID 2>/dev/null || true +sleep 3 + +# Copy any heap snapshots generated +echo -e "${YELLOW}Copying heap snapshots...${NC}" +docker exec "$CONTAINER_NAME" bash -c "find /root/workspace -name 'Heap.*.heapsnapshot' -o -name '*.heapsnapshot'" 2>/dev/null | while read -r file; do + if [ -n "$file" ]; then + FILENAME=$(basename "$file") + docker cp "$CONTAINER_NAME:$file" "$OUTPUT_DIR/$FILENAME" + echo -e "${GREEN}Copied heap snapshot: $FILENAME${NC}" + fi +done + +# Generate coredump by sending SIGABRT +echo -e "${YELLOW}Generating coredump (SIGABRT)...${NC}" +docker exec "$CONTAINER_NAME" kill -SIGABRT $BENCHMARK_PID 2>/dev/null || true +sleep 2 + +# Copy benchmark log +echo -e "${YELLOW}Copying benchmark log...${NC}" +docker cp "$CONTAINER_NAME:/tmp/benchmark.log" "$OUTPUT_DIR/benchmark.log" 2>/dev/null || true + +# Try to find and copy core dump files +echo -e "${YELLOW}Looking for core dumps...${NC}" +docker exec "$CONTAINER_NAME" bash -c "find /tmp -name 'core*' -type f 2>/dev/null" | while read -r corefile; do + if [ -n "$corefile" ]; then + FILENAME=$(basename "$corefile") + echo -e "${GREEN}Found core dump: $corefile${NC}" + docker cp "$CONTAINER_NAME:$corefile" "$OUTPUT_DIR/$FILENAME" + echo -e "${GREEN}Copied: $FILENAME${NC}" + fi +done + +# Also check for core files in workspace +docker exec "$CONTAINER_NAME" bash -c "find /root/workspace -name 'core*' -type f 2>/dev/null" | while read -r corefile; do + if [ -n "$corefile" ]; then + FILENAME=$(basename "$corefile") + echo -e "${GREEN}Found core dump: $corefile${NC}" + docker cp "$CONTAINER_NAME:$corefile" "$OUTPUT_DIR/$FILENAME" + echo -e "${GREEN}Copied: $FILENAME${NC}" + fi +done + +echo "" +echo -e "${GREEN}=== Benchmark Complete ===${NC}" +echo "Output files are in: $OUTPUT_DIR" +ls -la "$OUTPUT_DIR" + +echo "" +echo -e "${YELLOW}To analyze heap snapshot:${NC}" +echo " 1. Open Chrome DevTools -> Memory tab" +echo " 2. Click 'Load' and select the .heapsnapshot file" +echo "" +echo -e "${YELLOW}To analyze core dump:${NC}" +echo " lldb -c $OUTPUT_DIR/core.* -- \$(which node)" diff --git a/benchmark/stream_download/start-docker.sh b/benchmark/stream_download/start-docker.sh new file mode 100755 index 000000000..2c4d67509 --- /dev/null +++ b/benchmark/stream_download/start-docker.sh @@ -0,0 +1,92 @@ +#!/bin/bash + +# Docker 启动脚本 +set -e + +# 设置变量 +IMAGE_NAME="nginx-node-benchmark" +CONTAINER_NAME="nginx-benchmark-server" +HOST_PORT="8080" +CONTAINER_PORT="80" +MOUNT_DIR="$(pwd)/nginx" +CONTAINER_MOUNT_DIR="/var/www/html" + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${GREEN}=== 启动 Docker 容器 ===${NC}" + +# 检查 Docker 是否运行 +if ! docker info > /dev/null 2>&1; then + echo -e "${RED}错误: Docker 未运行或未安装${NC}" + exit 1 +fi + +# 检查 nginx 目录是否存在 +if [ ! -d "$MOUNT_DIR" ]; then + echo -e "${YELLOW}警告: nginx 目录不存在,正在创建...${NC}" + mkdir -p "$MOUNT_DIR" +fi + +# 检查是否有测试文件,如果没有就创建一些 +if [ ! -f "$MOUNT_DIR/test-file.txt" ]; then + echo -e "${YELLOW}创建测试文件...${NC}" + cp nginx/test-file.txt "$MOUNT_DIR/" 2>/dev/null || echo "测试文件已存在" +fi + +if [ ! -f "$MOUNT_DIR/large-test-file.bin" ]; then + echo -e "${YELLOW}创建大测试文件...${NC}" + cp nginx/large-test-file.bin "$MOUNT_DIR/" 2>/dev/null || dd if=/dev/zero of="$MOUNT_DIR/large-test-file.bin" bs=1M count=10 +fi + +# 停止并删除已存在的容器 +echo "检查并停止已存在的容器..." +docker stop "$CONTAINER_NAME" > /dev/null 2>&1 || true +docker rm "$CONTAINER_NAME" > /dev/null 2>&1 || true + +# 构建 Docker 镜像 +echo "构建 Docker 镜像..." +docker build -t "$IMAGE_NAME" . + +# 启动容器 (添加 ulimit 和 privileged 模式用于 coredump) +echo "启动容器..." +docker run -d \ + --name "$CONTAINER_NAME" \ + -p "$HOST_PORT:$CONTAINER_PORT" \ + -v "$MOUNT_DIR:$CONTAINER_MOUNT_DIR:ro" \ + --ulimit core=-1 \ + --privileged \ + --restart unless-stopped \ + "$IMAGE_NAME" + +# 等待容器启动 +echo "等待容器启动..." +sleep 3 + +# 检查容器状态 +if docker ps | grep -q "$CONTAINER_NAME"; then + echo -e "${GREEN}容器启动成功!${NC}" + echo -e "${GREEN}访问地址: http://localhost:$HOST_PORT${NC}" + echo -e "${GREEN}下载测试: http://localhost:$HOST_PORT/download/${NC}" + echo -e "${GREEN}上传测试: http://localhost:$HOST_PORT/upload/${NC}" + echo -e "${GREEN}健康检查: http://localhost:$HOST_PORT/health${NC}" + + # 显示容器信息 + echo "" + echo "容器信息:" + docker ps | grep "$CONTAINER_NAME" + + echo "" + echo "测试命令:" + echo "下载测试: curl -O http://localhost:$HOST_PORT/download/test-file.txt" + echo "上传测试: curl -X POST -d 'test' http://localhost:$HOST_PORT/upload/" + +else + echo -e "${RED}容器启动失败!${NC}" + echo "查看日志:" + docker logs "$CONTAINER_NAME" + exit 1 +fi \ No newline at end of file diff --git a/benchmark/stream_download/start-nginx.sh b/benchmark/stream_download/start-nginx.sh new file mode 100755 index 000000000..d05b71b5a --- /dev/null +++ b/benchmark/stream_download/start-nginx.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +# nginx 启动脚本 +set -e + +echo "=== 启动 nginx 服务 ===" + +# 检查 nginx 配置 +nginx -t + +# 创建必要的目录 +mkdir -p /var/log/nginx +mkdir -p /var/lib/nginx/body +mkdir -p /var/lib/nginx/proxy +mkdir -p /var/lib/nginx/fastcgi + +# 设置 nginx 目录权限 +# chown -R www-data:www-data /var/www/html +# chmod -R 755 /var/www/html + +# 启动 nginx 前台进程 +echo "正在启动 nginx..." +nginx -g 'daemon off;' & + +NGINX_PID=$! + +# 等待 nginx 启动 +sleep 2 + +# 检查 nginx 是否成功启动 +if ! kill -0 $NGINX_PID 2>/dev/null; then + echo "nginx 启动失败" + exit 1 +fi + +echo "nginx 启动成功,PID: $NGINX_PID" +echo "访问地址: http://localhost" +echo "下载测试: http://localhost/download/" +echo "上传测试: http://localhost/upload/" +echo "健康检查: http://localhost/health" + +# 处理信号 +handle_signal() { + echo "接收到信号,正在停止 nginx..." + nginx -s quit + wait $NGINX_PID + echo "nginx 已停止" + exit 0 +} + +# 设置信号处理 +trap handle_signal SIGTERM SIGINT + +# 等待 nginx 进程 +wait $NGINX_PID \ No newline at end of file diff --git a/benchmark/stream_download/status-docker.sh b/benchmark/stream_download/status-docker.sh new file mode 100755 index 000000000..6d2f36e5c --- /dev/null +++ b/benchmark/stream_download/status-docker.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +# Docker 状态检查脚本 + +CONTAINER_NAME="nginx-benchmark-server" +IMAGE_NAME="nginx-node-benchmark" + +# 颜色输出 +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +echo -e "${BLUE}=== Docker 容器状态 ===${NC}" + +# 检查容器状态 +if docker ps | grep -q "$CONTAINER_NAME"; then + echo -e "${GREEN}容器状态: 运行中${NC}" + echo "" + echo "容器信息:" + docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep "$CONTAINER_NAME" + + echo "" + echo "资源使用:" + docker stats --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}" | grep "$CONTAINER_NAME" || echo "无法获取资源信息" + + echo "" + echo "访问测试:" + if curl -s http://localhost:8080/health > /dev/null; then + echo -e "${GREEN}✓ HTTP 服务正常${NC}" + else + echo -e "${RED}✗ HTTP 服务异常${NC}" + fi + +elif docker ps -a | grep -q "$CONTAINER_NAME"; then + echo -e "${YELLOW}容器状态: 已停止${NC}" + echo "" + echo "最后状态:" + docker ps -a --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep "$CONTAINER_NAME" +else + echo -e "${RED}容器状态: 不存在${NC}" +fi + +# 检查镜像 +if docker images | grep -q "$IMAGE_NAME"; then + echo "" + echo -e "${BLUE}镜像信息:${NC}" + docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.CreatedAt}}" | grep "$IMAGE_NAME" +else + echo "" + echo -e "${RED}镜像不存在${NC}" +fi + +# 端口检查 +if netstat -tulnp 2>/dev/null | grep -q ":8080" || lsof -i :8080 2>/dev/null; then + echo "" + echo -e "${GREEN}端口 8080 已占用${NC}" +else + echo "" + echo -e "${YELLOW}端口 8080 空闲${NC}" +fi + +# 显示测试命令 +echo "" +echo -e "${BLUE}测试命令:${NC}" +echo "健康检查: curl -I http://localhost:8080/health" +echo "下载测试: curl -O http://localhost:8080/download/test-file.txt" +echo "上传测试: curl -X POST -d 'test' http://localhost:8080/upload/" \ No newline at end of file diff --git a/benchmark/stream_download/stop-docker.sh b/benchmark/stream_download/stop-docker.sh new file mode 100755 index 000000000..ddf8fdcb8 --- /dev/null +++ b/benchmark/stream_download/stop-docker.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# Docker 停止脚本 +set -e + +CONTAINER_NAME="nginx-benchmark-server" +IMAGE_NAME="nginx-node-benchmark" + +# 颜色输出 +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' + +echo -e "${GREEN}=== 停止 Docker 容器 ===${NC}" + +# 停止容器 +if docker ps | grep -q "$CONTAINER_NAME"; then + echo "正在停止容器 $CONTAINER_NAME..." + docker stop "$CONTAINER_NAME" + echo -e "${GREEN}容器已停止${NC}" +else + echo "容器 $CONTAINER_NAME 未运行" +fi + +# 删除容器 +if docker ps -a | grep -q "$CONTAINER_NAME"; then + echo "正在删除容器 $CONTAINER_NAME..." + docker rm "$CONTAINER_NAME" + echo -e "${GREEN}容器已删除${NC}" +fi + +# 可选:删除镜像 +echo "" +read -p "是否删除镜像 $IMAGE_NAME? (y/N): " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + echo "正在删除镜像 $IMAGE_NAME..." + docker rmi "$IMAGE_NAME" || echo "镜像不存在或正在使用" + echo -e "${GREEN}镜像已删除${NC}" +fi + +echo -e "${GREEN}清理完成${NC}" \ No newline at end of file