3센터 A-A-A RTGS 실시간총액결제 프로토타입 최초 버전관리 시작. - backend: Kotlin/Gradle 멀티모듈(sequencer, common, 채널/센터 모듈) - frontend: Vite + TS - infra: docker-compose, prometheus/grafana, ELK 네이티브 스크립트 - loadtest(k6), sim(장애/순서/정합성 시나리오), docs(PoC 보고서/복원력방안)
95 lines
4.0 KiB
JavaScript
95 lines
4.0 KiB
JavaScript
import http from 'k6/http';
|
|
import { check } from 'k6';
|
|
|
|
// RTGS 부하 스크립트 - pacs.008(ISO 20022, pacs.008.001.08) 실시간 생성 -> Chanel 접수.
|
|
// 참가기관(Dbtr/Cdtr Agent MmbId) 무작위 선정, 이체금액 1~200원(개요/PoC 기준).
|
|
// 프로세스 (0) 사전확인(개인계좌 잔액/수신계좌)은 참가기관 몫이므로 여기(부하생성기=송신기관)가 담당.
|
|
//
|
|
// ※ Chanel은 pacs.008.001.08 XSD로 원전문을 검증한다. 반드시 정식 <Document> 구조여야
|
|
// 접수(TxSts=RCVD)된다. 구식 <RtgsPacs008> 간이 포맷은 XSD 검증 실패로 RJCT.
|
|
// 핵심 추출 필드: EndToEndId(BMI), DbtrAgt/CdtrAgt MmbId(송/수신), Dbtr/CdtrAcct Othr Id, IntrBkSttlmAmt.
|
|
//
|
|
// 실행 예:
|
|
// steady : k6 run -e RATE=50 -e DURATION=20s loadtest/pacs008.k6.js
|
|
// spike : k6 run -e MODE=spike loadtest/pacs008.k6.js
|
|
// 대시보드: k6 run --out influxdb=http://localhost:8096/k6 ... (관측 프로필)
|
|
|
|
const BASE = __ENV.CHANEL || 'http://localhost:8091';
|
|
const DATE = __ENV.BIZDATE || '20260709';
|
|
const ORGS = ['1001','1002','1003','1004','1005','1006','1007','1008','1009','1010',
|
|
'1011','1012','1013','1014','1015','1016','1017','1018','1019'];
|
|
|
|
function scenarios() {
|
|
if (__ENV.MODE === 'spike') {
|
|
// 스파이크(peak) 산정: 도착률을 단계적으로 올려 최대 접수 처리건수 측정(리뷰어2 B).
|
|
return {
|
|
spike: {
|
|
executor: 'ramping-arrival-rate',
|
|
startRate: Number(__ENV.START || 100),
|
|
timeUnit: '1s',
|
|
preAllocatedVUs: 100, maxVUs: 2000,
|
|
stages: [
|
|
{ target: Number(__ENV.PEAK || 1000), duration: __ENV.RAMP || '30s' },
|
|
{ target: Number(__ENV.PEAK || 1000), duration: '20s' },
|
|
{ target: 0, duration: '10s' },
|
|
],
|
|
},
|
|
};
|
|
}
|
|
return {
|
|
steady: {
|
|
executor: 'constant-arrival-rate',
|
|
rate: Number(__ENV.RATE || 50),
|
|
timeUnit: '1s',
|
|
duration: __ENV.DURATION || '20s',
|
|
preAllocatedVUs: 50, maxVUs: 500,
|
|
},
|
|
};
|
|
}
|
|
|
|
export const options = { scenarios: scenarios() };
|
|
|
|
function pad(n, len) { return String(n).padStart(len, '0'); }
|
|
|
|
export default function () {
|
|
const s = ORGS[Math.floor(Math.random() * ORGS.length)];
|
|
let r = ORGS[Math.floor(Math.random() * ORGS.length)];
|
|
while (r === s) r = ORGS[Math.floor(Math.random() * ORGS.length)];
|
|
|
|
const amount = 1 + Math.floor(Math.random() * 200);
|
|
const serial = pad((__VU * 1000000 + __ITER) % 10000000000, 10);
|
|
const bmi = DATE + s + serial; // 22자리 EndToEndId = 8+4+10
|
|
const creDtTm = new Date().toISOString().slice(0, 19); // pacs.008 ISODateTime (초 단위, TZ 생략)
|
|
|
|
// pacs.008.001.08 정식 전문 (요소 순서는 XSD 시퀀스 준수 - 정상 샘플과 동일 구조).
|
|
const xml =
|
|
`<?xml version="1.0" encoding="UTF-8"?>` +
|
|
`<Document xmlns="urn:iso:std:iso:20022:tech:xsd:pacs.008.001.08">` +
|
|
`<FIToFICstmrCdtTrf>` +
|
|
`<GrpHdr>` +
|
|
`<MsgId>${bmi}</MsgId>` +
|
|
`<CreDtTm>${creDtTm}</CreDtTm>` +
|
|
`<NbOfTxs>1</NbOfTxs>` +
|
|
`<SttlmInf><SttlmMtd>CLRG</SttlmMtd></SttlmInf>` +
|
|
`</GrpHdr>` +
|
|
`<CdtTrfTxInf>` +
|
|
`<PmtId><EndToEndId>${bmi}</EndToEndId></PmtId>` +
|
|
`<IntrBkSttlmAmt Ccy="KRW">${amount}</IntrBkSttlmAmt>` +
|
|
`<ChrgBr>SLEV</ChrgBr>` +
|
|
`<Dbtr><Nm>BANK-${s}</Nm></Dbtr>` +
|
|
`<DbtrAcct><Id><Othr><Id>ACC-${s}</Id></Othr></Id></DbtrAcct>` +
|
|
`<DbtrAgt><FinInstnId><ClrSysMmbId><MmbId>${s}</MmbId></ClrSysMmbId></FinInstnId></DbtrAgt>` +
|
|
`<CdtrAgt><FinInstnId><ClrSysMmbId><MmbId>${r}</MmbId></ClrSysMmbId></FinInstnId></CdtrAgt>` +
|
|
`<Cdtr><Nm>BANK-${r}</Nm></Cdtr>` +
|
|
`<CdtrAcct><Id><Othr><Id>ACC-${r}</Id></Othr></Id></CdtrAcct>` +
|
|
`</CdtTrfTxInf>` +
|
|
`</FIToFICstmrCdtTrf>` +
|
|
`</Document>`;
|
|
|
|
const res = http.post(`${BASE}/pay/customer`, xml, { headers: { 'Content-Type': 'application/xml' } });
|
|
check(res, {
|
|
'status 200': (x) => x.status === 200,
|
|
'accepted (RCVD)': (x) => x.body && x.body.includes('RCVD'),
|
|
});
|
|
}
|