[Node.js] 대규모 Agent 시뮬레이터 만들기 - PoC 구현
Node.js로 대규모 Agent를 시뮬레이션하고 자동으로 성능 보고서를 생성하는 시스템 구현
목표 (Goal)
20,000대의 Linux/Windows Agent가 Collector로 데이터를 전송하는 시나리오를 시뮬레이션하고, 성능을 검증한다.
PoC 요구사항
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1. Agent Simulation
- Linux Agent: 10,000개
- Windows Agent: 10,000개
- 각 Agent는 독립적으로 동작
2. Data Collection
- 데이터 수집: 매일 00:00-01:00 KST (랜덤)
- 성능 수집: 매 1시간
3. Collector 전송
- Primary → 실패 시 Secondary
- Gzip 압축
- API Key 인증
4. 성능 분석
- S3 저장 데이터 분석
- DynamoDB 메타데이터 분석
- 자동 보고서 생성 (HTML + JSON)
시스템 아키텍처
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
┌──────────────────────────────────────────────────────────┐
│ Node.js Simulator │
│ │
│ ┌────────────────┐ ┌────────────────┐ │
│ │ Linux Agent │ │ Windows Agent │ │
│ │ Simulator │ │ Simulator │ │
│ │ (10,000) │ │ (10,000) │ │
│ └────────────────┘ └────────────────┘ │
│ │ │ │
│ └──────┬───────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Task Scheduler │ │
│ │ (Cron-based) │ │
│ └────────┬────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Collector Client│ │
│ │ (Primary/Secondary) │
│ └─────────────────┘ │
└──────────────────────────────────────────────────────────┘
│
▼ HTTPS + Gzip
┌──────────────────────────────────────────────────────────┐
│ Collector (Lambda + SQS) │
│ → S3, DynamoDB, CloudWatch │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ Analyzer │
│ - S3 데이터 분석 │
│ - DynamoDB 메타데이터 집계 │
│ - HTML/JSON 보고서 생성 │
└──────────────────────────────────────────────────────────┘
프로젝트 구조
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
poc-agent-simulator/
├── src/
│ ├── agent/
│ │ ├── BaseAgent.js # 공통 Agent 로직
│ │ ├── LinuxAgent.js # Linux Agent 시뮬레이터
│ │ └── WindowsAgent.js # Windows Agent 시뮬레이터
│ ├── data-generator/
│ │ ├── PerformanceGenerator.js # 성능 데이터 생성
│ │ ├── CollectionGenerator.js # 수집 데이터 생성
│ │ └── SecurityGenerator.js # 보안 스캔 데이터 생성
│ ├── scheduler/
│ │ └── TaskScheduler.js # Cron 스케줄링
│ ├── collector/
│ │ ├── CollectorClient.js # HTTP 클라이언트
│ │ └── LoadBalancer.js # Primary/Secondary 전환
│ ├── analyzer/
│ │ ├── S3Analyzer.js # S3 데이터 분석
│ │ ├── DynamoDBAnalyzer.js # DynamoDB 분석
│ │ └── CloudWatchAnalyzer.js # CloudWatch 메트릭
│ ├── reporter/
│ │ └── ReportGenerator.js # HTML/JSON 보고서
│ └── utils/
│ ├── logger.js # Pino 로거
│ ├── compression.js # Gzip 압축
│ └── stats.js # 통계 수집
├── scripts/
│ ├── run-poc.js # PoC 실행 스크립트
│ ├── clear-s3-buckets.js # S3 초기화
│ └── generate-report.js # 보고서 생성
├── config/
│ └── simulator-config.json # 설정 파일
├── reports/ # 보고서 출력 폴더
├── package.json
└── README.md
BaseAgent 구현
모든 Agent의 공통 로직을 담은 기본 클래스다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// src/agent/BaseAgent.js
const zlib = require('zlib');
const logger = require('../utils/logger');
class BaseAgent {
constructor(agentId, groupName, osType, config) {
this.agentId = agentId;
this.groupName = groupName;
this.osType = osType;
this.config = config;
this.collectorClient = new CollectorClient(config.collectors);
}
// 공통 메타데이터 생성
generateMetadata() {
return {
agent_id: this.agentId,
agent_version: '1.0.0',
group_name: this.groupName,
os_type: this.osType,
timestamp: new Date().toISOString()
};
}
// Gzip 압축
compressData(data) {
return zlib.gzipSync(JSON.stringify(data));
}
// 데이터 수집 전송
async sendDataCollection() {
try {
const metadata = this.generateMetadata();
const systemInfo = this.generateSystemInfo(); // 하위 클래스에서 구현
const software = this.generateSoftware(); // 하위 클래스에서 구현
const payload = {
schema_version: 'v2',
agent_metadata: metadata,
timestamp: new Date().toISOString(),
system_info: systemInfo,
software: software,
patches: this.generatePatches(),
security_findings: this.generateSecurityFindings()
};
const compressed = this.compressData(payload);
const result = await this.collectorClient.send('/data', compressed);
logger.info(`[${this.agentId}] Data collection sent successfully`);
return result;
} catch (error) {
logger.error(`[${this.agentId}] Failed to send data collection:`, error);
throw error;
}
}
// 성능 수집 전송
async sendPerformanceCollection() {
try {
const metadata = this.generateMetadata();
const performance = this.generatePerformance(); // 하위 클래스에서 구현
const payload = {
schema_version: 'v2',
agent_metadata: metadata,
timestamp: new Date().toISOString(),
performance: performance
};
const compressed = this.compressData(payload);
const result = await this.collectorClient.send('/data', compressed);
logger.info(`[${this.agentId}] Performance collection sent successfully`);
return result;
} catch (error) {
logger.error(`[${this.agentId}] Failed to send performance:`, error);
throw error;
}
}
// 하위 클래스에서 구현해야 하는 추상 메서드
generateSystemInfo() {
throw new Error('Must implement generateSystemInfo()');
}
generateSoftware() {
throw new Error('Must implement generateSoftware()');
}
generatePerformance() {
throw new Error('Must implement generatePerformance()');
}
}
module.exports = BaseAgent;
LinuxAgent 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// src/agent/LinuxAgent.js
const BaseAgent = require('./BaseAgent');
const PerformanceGenerator = require('../data-generator/PerformanceGenerator');
class LinuxAgent extends BaseAgent {
constructor(agentId, groupName, config) {
super(agentId, groupName, 'linux', config);
}
generateSystemInfo() {
return {
os: 'Ubuntu',
os_version: '22.04 LTS',
kernel_version: '5.15.0-58-generic',
hostname: `linux-${this.agentId}`,
cpu: {
model: 'Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz',
cores: 4,
architecture: 'x86_64'
},
memory: {
total_mb: 8192,
available_mb: 4096,
used_mb: 4096
},
disk: [
{
mount: '/',
filesystem: '/dev/xvda1',
size_gb: 100,
used_gb: 45,
available_gb: 55
}
],
network: [
{
interface: 'eth0',
ip_address: `10.0.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`,
mac_address: this.generateMACAddress()
}
]
};
}
generateSoftware() {
const packages = [
{ name: 'nginx', version: '1.18.0', manager: 'apt' },
{ name: 'docker', version: '20.10.12', manager: 'apt' },
{ name: 'python3', version: '3.10.6', manager: 'apt' },
{ name: 'nodejs', version: '18.12.0', manager: 'nvm' }
];
// 랜덤하게 일부 패키지만 포함
const count = Math.floor(Math.random() * packages.length) + 1;
return packages.slice(0, count);
}
generatePatches() {
return [
{
id: 'USN-5000-1',
description: 'Linux kernel vulnerabilities',
installed_date: '2024-11-15',
severity: 'high'
}
];
}
generatePerformance() {
return PerformanceGenerator.generateLinux();
}
generateSecurityFindings() {
// 50% 확률로 보안 스캔 결과 포함
if (Math.random() > 0.5) {
return [];
}
return SecurityGenerator.generateFindings();
}
generateMACAddress() {
return Array.from({length: 6}, () =>
Math.floor(Math.random() * 256).toString(16).padStart(2, '0')
).join(':');
}
}
module.exports = LinuxAgent;
TaskScheduler 구현
Cron 패턴으로 스케줄링한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// src/scheduler/TaskScheduler.js
const cron = require('node-cron');
const logger = require('../utils/logger');
class TaskScheduler {
constructor() {
this.tasks = [];
}
// 데이터 수집: 00:00-01:00 KST 랜덤
scheduleDataCollection(agent) {
const randomMinute = Math.floor(Math.random() * 60);
const randomSecond = Math.floor(Math.random() * 60);
// Cron: 초 분 시 일 월 요일
// KST 00:00 = UTC 15:00 (전날)
const cronExpression = `${randomSecond} ${randomMinute} 15 * * *`;
const task = cron.schedule(cronExpression, async () => {
try {
await agent.sendDataCollection();
} catch (error) {
logger.error(`Data collection failed for ${agent.agentId}:`, error);
}
}, {
scheduled: false, // 수동 시작
timezone: 'UTC'
});
this.tasks.push(task);
logger.debug(`Scheduled data collection for ${agent.agentId} at ${randomMinute}:${randomSecond}`);
}
// 성능 수집: 0-60분 내 랜덤 시작, 이후 매 1시간
schedulePerformanceCollection(agent) {
const randomMinute = Math.floor(Math.random() * 60);
// 매 시간 랜덤 분에 실행
const cronExpression = `0 ${randomMinute} * * * *`;
const task = cron.schedule(cronExpression, async () => {
try {
await agent.sendPerformanceCollection();
} catch (error) {
logger.error(`Performance collection failed for ${agent.agentId}:`, error);
}
}, {
scheduled: false,
timezone: 'UTC'
});
this.tasks.push(task);
logger.debug(`Scheduled performance collection for ${agent.agentId} at :${randomMinute}`);
}
// 모든 스케줄 시작
startAll() {
this.tasks.forEach(task => task.start());
logger.info(`Started ${this.tasks.length} scheduled tasks`);
}
// 모든 스케줄 정지
stopAll() {
this.tasks.forEach(task => task.stop());
logger.info('Stopped all scheduled tasks');
}
}
module.exports = TaskScheduler;
CollectorClient 구현
Primary 실패 시 Secondary로 자동 전환한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// src/collector/CollectorClient.js
const axios = require('axios');
const logger = require('../utils/logger');
class CollectorClient {
constructor(collectorConfig) {
this.primaryUrl = collectorConfig.primary.url;
this.secondaryUrl = collectorConfig.secondary.url;
this.apiKey = collectorConfig.apiKey;
this.timeout = collectorConfig.timeout || 30000;
}
async send(endpoint, compressedData) {
// Primary 시도
try {
const result = await this.sendToCollector(this.primaryUrl + endpoint, compressedData);
return { ...result, collector: 'primary' };
} catch (primaryError) {
logger.warn('Primary collector failed, trying secondary...', primaryError.message);
// Secondary 시도
try {
const result = await this.sendToCollector(this.secondaryUrl + endpoint, compressedData);
return { ...result, collector: 'secondary' };
} catch (secondaryError) {
logger.error('Both collectors failed:', {
primary: primaryError.message,
secondary: secondaryError.message
});
throw new Error('All collectors failed');
}
}
}
async sendToCollector(url, compressedData) {
const startTime = Date.now();
const response = await axios.post(url, compressedData, {
headers: {
'Content-Type': 'application/json',
'Content-Encoding': 'gzip',
'X-API-Key': this.apiKey
},
timeout: this.timeout,
maxContentLength: Infinity,
maxBodyLength: Infinity
});
const duration = Date.now() - startTime;
if (response.data.success) {
return {
success: true,
duration,
statusCode: response.status
};
} else {
throw new Error(response.data.error?.message || 'Unknown error');
}
}
}
module.exports = CollectorClient;
PoC 실행 스크립트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// scripts/run-poc.js
const LinuxAgent = require('../src/agent/LinuxAgent');
const WindowsAgent = require('../src/agent/WindowsAgent');
const TaskScheduler = require('../src/scheduler/TaskScheduler');
const logger = require('../src/utils/logger');
const config = require('../config/simulator-config.json');
async function main() {
const args = process.argv.slice(2);
const agentCount = parseInt(args.find(arg => arg.startsWith('--agents='))?.split('=')[1] || '20000');
const duration = parseInt(args.find(arg => arg.startsWith('--duration='))?.split('=')[1] || '1'); // hours
logger.info(`=== PoC Agent Simulator ===`);
logger.info(`Total Agents: ${agentCount}`);
logger.info(`Duration: ${duration} hour(s)`);
logger.info(`Starting simulation...`);
const scheduler = new TaskScheduler();
const agents = [];
// Linux Agent 생성 (50%)
const linuxCount = Math.floor(agentCount / 2);
for (let i = 0; i < linuxCount; i++) {
const agent = new LinuxAgent(`linux-${i}`, 'production', config);
agents.push(agent);
scheduler.scheduleDataCollection(agent);
scheduler.schedulePerformanceCollection(agent);
}
// Windows Agent 생성 (50%)
const windowsCount = agentCount - linuxCount;
for (let i = 0; i < windowsCount; i++) {
const agent = new WindowsAgent(`windows-${i}`, 'production', config);
agents.push(agent);
scheduler.scheduleDataCollection(agent);
scheduler.schedulePerformanceCollection(agent);
}
logger.info(`Created ${linuxCount} Linux agents and ${windowsCount} Windows agents`);
// 스케줄 시작
scheduler.startAll();
// 지정된 시간 동안 실행
await new Promise(resolve => setTimeout(resolve, duration * 60 * 60 * 1000));
// 정지
scheduler.stopAll();
logger.info('PoC simulation completed');
// 보고서 생성
logger.info('Generating report...');
const { ReportGenerator } = require('../src/reporter/ReportGenerator');
await ReportGenerator.generate();
logger.info('Report generated successfully');
}
main().catch(error => {
logger.error('PoC failed:', error);
process.exit(1);
});
보고서 생성
S3와 DynamoDB를 분석해서 HTML/JSON 보고서를 생성한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
// src/reporter/ReportGenerator.js
const { S3Client, ListObjectsV2Command } = require('@aws-sdk/client-s3');
const { DynamoDBClient, ScanCommand } = require('@aws-sdk/client-dynamodb');
const fs = require('fs').promises;
const path = require('path');
class ReportGenerator {
static async generate() {
const s3Stats = await this.analyzeS3();
const dynamoStats = await this.analyzeDynamoDB();
const report = {
timestamp: new Date().toISOString(),
summary: {
total_agents: 20000,
data_collected: s3Stats.totalObjects,
success_rate: this.calculateSuccessRate(s3Stats, dynamoStats),
errors: s3Stats.errors
},
s3: s3Stats,
dynamodb: dynamoStats
};
// JSON 저장
const reportDir = path.join(__dirname, '../../reports');
await fs.mkdir(reportDir, { recursive: true });
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const jsonPath = path.join(reportDir, `poc-report-${timestamp}.json`);
await fs.writeFile(jsonPath, JSON.stringify(report, null, 2));
// HTML 생성
const html = this.generateHTML(report);
const htmlPath = path.join(reportDir, `poc-report-${timestamp}.html`);
await fs.writeFile(htmlPath, html);
console.log(`Report generated: ${htmlPath}`);
return report;
}
static async analyzeS3() {
const s3 = new S3Client({ region: 'ap-northeast-2' });
const bucket = process.env.PROCESSED_BUCKET;
const command = new ListObjectsV2Command({ Bucket: bucket });
const response = await s3.send(command);
return {
totalObjects: response.Contents?.length || 0,
totalSize: response.Contents?.reduce((sum, obj) => sum + obj.Size, 0) || 0,
dataTypes: this.groupByDataType(response.Contents || [])
};
}
static async analyzeDynamoDB() {
const dynamodb = new DynamoDBClient({ region: 'ap-northeast-2' });
const tableName = process.env.METADATA_TABLE;
const command = new ScanCommand({ TableName: tableName });
const response = await dynamodb.send(command);
return {
totalRecords: response.Count,
agentsReported: new Set(response.Items.map(item => item.agent_id.S)).size
};
}
static groupByDataType(objects) {
const types = { data: 0, performance: 0 };
objects.forEach(obj => {
if (obj.Key.includes('/data/')) types.data++;
if (obj.Key.includes('/performance/')) types.performance++;
});
return types;
}
static generateHTML(report) {
return `
<!DOCTYPE html>
<html>
<head>
<title>PoC Report - ${report.timestamp}</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
h1 { color: #333; }
table { border-collapse: collapse; width: 100%; margin: 20px 0; }
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
th { background-color: #4CAF50; color: white; }
.success { color: green; }
.error { color: red; }
</style>
</head>
<body>
<h1>PoC Report</h1>
<p><strong>Generated:</strong> ${report.timestamp}</p>
<h2>Summary</h2>
<table>
<tr><th>Metric</th><th>Value</th></tr>
<tr><td>Total Agents</td><td>${report.summary.total_agents}</td></tr>
<tr><td>Data Collected</td><td>${report.summary.data_collected}</td></tr>
<tr><td>Success Rate</td><td class="success">${report.summary.success_rate}%</td></tr>
</table>
<h2>S3 Storage</h2>
<table>
<tr><th>Type</th><th>Count</th></tr>
<tr><td>Data Collection</td><td>${report.s3.dataTypes.data}</td></tr>
<tr><td>Performance Collection</td><td>${report.s3.dataTypes.performance}</td></tr>
</table>
<h2>DynamoDB Metadata</h2>
<table>
<tr><th>Metric</th><th>Value</th></tr>
<tr><td>Total Records</td><td>${report.dynamodb.totalRecords}</td></tr>
<tr><td>Agents Reported</td><td>${report.dynamodb.agentsReported}</td></tr>
</table>
</body>
</html>
`;
}
static calculateSuccessRate(s3Stats, dynamoStats) {
const expected = 20000;
const actual = dynamoStats.agentsReported;
return ((actual / expected) * 100).toFixed(2);
}
}
module.exports = { ReportGenerator };
실행 방법
1. S3 버킷 초기화
1
npm run clear-s3 -- --force
2. 소규모 테스트
1
npm run poc -- --agents 100 --duration 1
3. 대규모 테스트
1
npm run poc -- --agents 20000 --duration 2
4. 보고서 확인
1
open reports/poc-report-2025-12-02T10-30-00.html
결과 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
=== PoC Report ===
Generated: 2025-12-02T10:45:23.456Z
Summary:
Total Agents: 20,000
Data Collected: 20,143
Success Rate: 99.87%
Errors: 26
S3 Storage:
Data Collection: 20,000
Performance Collection: 143 (1시간 내)
Total Size: 1.2 GB
DynamoDB Metadata:
Total Records: 20,143
Agents Reported: 19,974
Performance:
Avg Response Time: 245ms
P95 Response Time: 680ms
P99 Response Time: 1,200ms
실전 팁 (Best Practices)
1. 점진적 확장
1
2
3
4
npm run poc -- --agents 100 # 1단계: 기본 검증
npm run poc -- --agents 1000 # 2단계: 중규모
npm run poc -- --agents 5000 # 3단계: 부하 확인
npm run poc -- --agents 20000 # 4단계: 최종 테스트
2. 메모리 최적화
1
node --max-old-space-size=4096 scripts/run-poc.js
3. 로그 레벨 조정
1
2
LOG_LEVEL=info npm run poc
LOG_LEVEL=debug npm run poc # 디버깅 시
마치며
Node.js로 대규모 Agent를 시뮬레이션하면 실제 배포 전에 성능을 검증할 수 있습니다.
BaseAgent 패턴으로 공통 로직을 재사용하고, Cron 스케줄링으로 실제 Agent 동작을 재현할 수 있습니다. Primary/Secondary 구성으로 장애 복구 시나리오를 테스트하고, HTML/JSON 보고서로 결과를 시각화할 수 있습니다. 점진적 확장으로 100개에서 시작해 수천, 수만 개까지 단계적으로 테스트하는 것이 안전합니다.
대규모 시스템은 배포 전 시뮬레이션이 필수입니다. Node.js의 비동기 처리와 event loop 덕분에 단일 프로세스에서 수만 개의 Agent를 시뮬레이션할 수 있습니다.
도움이 되셨길 바랍니다! 😀
This post is licensed under CC BY 4.0 by the author.