Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
98163c6
fix: 당일 취소한 수동 체크도 이틀 뒤 「실패로 확정됐어요」 알림을 받는다
selee1012 Oct 14, 2026
3b47ce9
fix: 종료가 시작되면 모더레이션 큐 폴링을 멈춘다
selee1012 Oct 14, 2026
0d51d1d
fix: 배포 이미지에 MALLOC_ARENA_MAX=2 — stg 하드 메모리 80% 경보 반복
selee1012 Oct 14, 2026
24d684a
Merge pull request #310 from RuleUp-ASM/fix/verification-result-dedup…
selee1012 Oct 14, 2026
1213836
Merge pull request #311 from RuleUp-ASM/fix/moderation-poll-shutdown
selee1012 Oct 14, 2026
c19e18f
Merge pull request #312 from RuleUp-ASM/fix/malloc-arena-max
selee1012 Oct 14, 2026
43f8e5c
feat: 장소 피하기(GPS_AVOID) 유형을 제거한다
selee1012 Oct 15, 2026
8a22f4f
feat: 기상 루틴을 잠금 해제가 아니라 일어나 걸은 걸음으로 판정한다
selee1012 Oct 15, 2026
08d0224
Merge pull request #314 from RuleUp-ASM/feat/wake-by-steps
selee1012 Oct 15, 2026
ded0327
fix: 마지막 멤버가 나가면 봇방장으로 넘기지 않고 방을 닫는다
selee1012 Oct 15, 2026
9d0a964
Merge pull request #316 from RuleUp-ASM/fix/empty-room-closes-on-leave
selee1012 Oct 15, 2026
10436af
feat: 경보를 1분 평가로 줄이고 RDS CPU·메모리를 P0 로 올린다
selee1012 Oct 15, 2026
491d749
feat: 운영 대시보드에 JVM 런타임 패널(OPS-6)을 더한다
selee1012 Oct 15, 2026
7ab8a0f
feat: P0 온콜을 1차 담당 → 10분 미응답 시 팀원 순서로 PagerDuty 에 잇는다
selee1012 Oct 15, 2026
6efdc3a
Merge pull request #319 from RuleUp-ASM/feat/monitoring-pagerduty-oncall
selee1012 Oct 15, 2026
ff1ef28
chore: PagerDuty 개통 — 원장에 에스컬레이션 정책·서비스·사용자 ID 와 SNS 토픽을 적는다
selee1012 Oct 15, 2026
42d7d45
fix: 온콜 시험이 Slack 까지 실제 P0 와 같은 길을 타게 한다
selee1012 Oct 15, 2026
75478e6
docs: 온콜 시험 절차에 Slack 확인을 넣는다
selee1012 Oct 15, 2026
cbb0edb
Merge pull request #321 from RuleUp-ASM/chore/pagerduty-ledger
selee1012 Oct 15, 2026
f60eaea
Merge pull request #322 from RuleUp-ASM/fix/oncall-test-full-path
selee1012 Oct 15, 2026
3642d81
feat: stg 부하테스트용 로그인 우회 스위치를 둔다
selee1012 Oct 15, 2026
23cedc8
feat: AI 초안 분당 한도를 설정값으로 뺀다
selee1012 Oct 15, 2026
a4eeb34
Merge pull request #323 from RuleUp-ASM/feat/loadtest-oauth-switch
selee1012 Oct 15, 2026
73f74e9
test: 요청 하나의 DB 작업(커넥션·커밋·SQL)을 세는 계측을 둔다
selee1012 Oct 15, 2026
385b650
perf: 마지막 활동일을 하루 한 번만 쓰고 로그인의 중복 기록을 없앤다
selee1012 Oct 15, 2026
454a749
fix: 기동 직후 탐색 Redis 첫 연결을 재시도해 5분 폴백을 막는다
selee1012 Oct 15, 2026
391f1b9
perf: AI 초안의 LLM 호출 동안 DB 커넥션을 잡지 않는다
selee1012 Oct 15, 2026
834466e
fix: sync 저장·판정 트랜잭션이 데드락으로 지면 다시 돈다
selee1012 Oct 15, 2026
cdbb280
perf: MySQL 드라이버 성능 옵션을 접속 주소가 아니라 풀 설정에 둔다
selee1012 Oct 15, 2026
7d3a249
fix: 예약 작업 스레드를 4개로 늘려 작업끼리 밀리고 cron 이 중복 실행되는 것을 막는다
selee1012 Oct 15, 2026
83c2ac6
fix: 시작일 도달 활성화를 1분 200건 상한 없이 끝까지 돈다
selee1012 Oct 15, 2026
ebdcfc0
fix: Redis 명령 타임아웃을 2초로 늘려 CPU 포화를 장애로 오판하지 않는다
selee1012 Oct 15, 2026
4a5c436
perf: X-Ray API 트레이스를 초당 10건 넘으면 10%만 기록한다
selee1012 Oct 15, 2026
665da66
fix: 통계 재계산이 방 행을 먼저 잠그고 센다
selee1012 Oct 15, 2026
5350337
test: RuleUp 사용 패턴 기준 k6 부하·성능 시나리오를 둔다
selee1012 Oct 15, 2026
8e06890
Merge pull request #324 from RuleUp-ASM/perf/loadtest-2026-10
selee1012 Oct 15, 2026
1197fa3
fix: 요청 전체에 EntityManager 를 걸지 않는다(open-in-view 끔)
selee1012 Oct 16, 2026
312a2b7
fix: ALB 헬스체크가 DB 를 보지 않게 한다
selee1012 Oct 16, 2026
20af89b
perf: 사용자를 필요한 만큼만 읽는다 — 멤버 목록 한 문장, 로그인은 id 만
selee1012 Oct 16, 2026
82e8e1b
perf: 닉네임 점유 확인이 users 전체를 훑지 않는다
selee1012 Oct 16, 2026
f2408a9
perf: 멤버가 나갈 때 외부 랭킹을 방 수와 상관없이 몇 문장으로 다시 매긴다
selee1012 Oct 16, 2026
dd7fc1e
perf: 같은 방의 통계 재계산은 하나만 돌리고 몰린 요청은 한 번으로 합친다
selee1012 Oct 16, 2026
401b1ef
perf: 탐색·인기의 방별 표시값을 Redis 왕복 한 번에 읽는다
selee1012 Oct 16, 2026
1e660f7
perf: sync 가 같은 날의 신호를 두 번 읽지 않는다
selee1012 Oct 16, 2026
fa6b6ee
fix: 요청 스레드의 SQS 보내기·S3 호출에 시간 제한을 둔다
selee1012 Oct 16, 2026
82d6923
perf: 배치가 멤버·가입 사건 표 전체를 훑거나 잠근 채 외부 호출하지 않는다
selee1012 Oct 16, 2026
8aa31b8
test: k6 에 핵심 화면 SLO·업무 성공 건수·단계/급증 시나리오를 더한다
selee1012 Oct 16, 2026
5e501ec
perf: X-Ray API 트레이스를 초당 1건 + 5% 로 줄인다
selee1012 Oct 16, 2026
6c9eac5
perf: 인기 마감 판정은 마감 근처 방만 세고, 1분 생명주기 배치가 방 전체를 잠그지 않는다
selee1012 Oct 16, 2026
05d6465
perf: 인기 점수 배치는 값이 바뀐 방만 쓴다
selee1012 Oct 16, 2026
15a18a5
perf: 가입·로그인에서 새 동의 상태·소셜 토큰을 merge 없이 바로 저장한다
selee1012 Oct 16, 2026
97152c8
test: k6 가 허용한 거절(sync 429 등)도 업무 거절로 센다
selee1012 Oct 16, 2026
0b81b9a
perf: 인기 목록은 저장값으로 이미 찬 방을 다시 세지 않는다
selee1012 Oct 16, 2026
674a2d0
perf: DB 에 닿지 못하면 5초 안에 503 + Retry-After 로 돌려보낸다
selee1012 Oct 16, 2026
e8f431d
fix: Redis 를 잃은 뒤 탐색 재구성이 바로 끝나고, 죽은 태스크의 잠금에 30분 묶이지 않는다
selee1012 Oct 16, 2026
92099f0
fix: 탐색 전수 재구성이 정렬 멤버를 1,000개씩 나눠 읽는다
selee1012 Oct 16, 2026
76878f6
perf: 읽기 전용 트랜잭션마다 접근 모드 SET 두 번을 DB 로 보내지 않는다
selee1012 Oct 16, 2026
684a93f
perf: 방마다 멤버 마지막 변경 시각을 인덱스 끝 한 칸으로 구한다(V16)
selee1012 Oct 16, 2026
04543fa
feat: 태스크당 동시 API 요청 상한을 두고, 넘치면 1초 뒤 503 + Retry-After 로 돌려보낸다(기본 끔)
selee1012 Oct 16, 2026
d778367
fix: 기상 걸음이 기기 기본 출처에서도 인정되고, 갱신된 걸음 기록은 마지막 값으로 센다
selee1012 Oct 16, 2026
9f62a1b
fix: 스크린타임이 화면 전환으로 사용 시간을 잃거나, 끊긴 세션을 자정까지 늘리지 않는다
selee1012 Oct 16, 2026
ecdaca9
chore: prod 사양 조정 스크립트(1 vCPU·헬스체크 관리 포트·메모리 정책 삭제·X-Ray) — main 머지 전…
selee1012 Oct 16, 2026
4b98363
test: joinhot 이 가입된 사용자로 로그인해 두고 같은 순간에 일제히 참여한다
selee1012 Oct 16, 2026
28c9305
fix: 계정 상태 필터에서 DB 를 못 쓰면 401 대신 503 + Retry-After 로 돌려보낸다
selee1012 Oct 16, 2026
d6075fe
test: k6 sync 가 VU 마다 여러 사용자를 돌아가며 써 한 사람에게 신호가 몰리지 않게 한다
selee1012 Oct 16, 2026
294dcd4
chore: prod 사양 스크립트가 CPU 는 --cpu 를 줄 때만 바꾼다
selee1012 Oct 16, 2026
083ca0b
perf: 다른 귀속일 중복 확인이 (userId, dedupKey) 로 키마다 한 번 찾는다(V17)
selee1012 Oct 16, 2026
a5de144
perf: sync 가 사용자 방을 한 번에 읽고, 신호는 판정할 때만 읽는다
selee1012 Oct 16, 2026
81d155e
Merge pull request #325 from RuleUp-ASM/fix/wake-screentime-verification
selee1012 Oct 17, 2026
ca55206
Merge pull request #326 from RuleUp-ASM/perf/dau2-2026-10
selee1012 Oct 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ jobs:
run: python3 -m unittest discover -s infra/monitoring/lambdas/tests -v
- name: Alarm catalog
run: python3 -c "import sys,types; sys.modules['boto3']=types.ModuleType('boto3'); sys.path.insert(0,'infra/monitoring/alerting'); import unittest; r=unittest.main(module='tests_catalog', argv=['x','-v'], exit=False); sys.exit(not r.result.wasSuccessful())"
- name: On-call (PagerDuty)
run: python3 -c "import sys,types; sys.modules['boto3']=types.ModuleType('boto3'); sys.path.insert(0,'infra/monitoring/alerting'); import unittest; r=unittest.main(module='tests_pagerduty', argv=['x','-v'], exit=False); sys.exit(not r.result.wasSuccessful())"
- name: Deployment preflight
run: python3 -m unittest discover -s .github/scripts -p 'test_*.py' -v

Expand Down
4 changes: 4 additions & 0 deletions Dockerfile.deploy
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,9 @@ ARG ADOT_JAVA_AGENT_VERSION=v2.31.0
ADD https://github.com/aws-observability/aws-otel-java-instrumentation/releases/download/${ADOT_JAVA_AGENT_VERSION}/aws-opentelemetry-agent.jar /app/otel/aws-opentelemetry-agent.jar
COPY --from=build /src/build/libs/*.jar app.jar
COPY infra/monitoring/tracing/entrypoint.sh /app/entrypoint.sh
# glibc 는 스레드마다 64MB 단위 malloc 아레나를 늘려 가며 돌려주지 않는다. stg(1GB 태스크)에서 JVM RSS 800MB 중
# 힙은 256MB 였고 아레나가 ~200MB 를 차지해, 하루쯤마다 하드 메모리 80% 경보(ECS-06-80)를 다시 울렸다(2026-10-14 smaps 실측).
# 아레나 수를 묶으면 네이티브 할당이 몇 개 아레나를 같이 쓰게 되어 RSS 가 천천히 차오르지 않는다.
ENV MALLOC_ARENA_MAX=2
EXPOSE 8080
ENTRYPOINT ["sh","/app/entrypoint.sh"]
100 changes: 100 additions & 0 deletions infra/capacity/apply_prod_dau2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""prod 사양 조정 — DAU 2만 부하테스트(2026-10-16~17, 노션 「부하테스트 및 성능개선」 9절) 결론. 재실행 안전.
사용자가 직접 실행한다(main 머지 **전에**, 온콜 중지 상태에서): python3 infra/capacity/apply_prod_dau2.py [--cpu 1024] [--max-inflight 64]

main CD 는 서비스의 현재 태스크 정의를 그대로 두고 이미지만 바꿔 다시 띄운다. 그래서 사양은 여기서 먼저 바꿔 둔다.
1. (선택 --cpu 1024) ECS 1 vCPU — 0.5 vCPU 는 피크 전에 CPU 100% 로 막힌다(1차 시험). 2026-10-17 결정: 지금 규모(prod CPU
평균 5%)에선 올리지 않고, 지연·오류가 생기면 **가장 먼저** 이걸로 올린다. 기본은 현재 CPU 를 그대로 둔다.
2. 헬스체크를 관리 포트(8081)로 분리 — 급증 때 요청 스레드 200개가 다 차면 /actuator/health 도 줄을 서 5초를 넘기고,
ECS 가 멀쩡한 태스크를 연달아 교체했다(2차 급증 시험). 관리 포트는 자기 Tomcat 커넥터를 따로 쓴다.
끊김 없이 옮기는 순서: ALB→ECS 8081 허용 → 헬스체크 경로를 옛·새 태스크 모두 응답하는 /health/public 으로
→ MANAGEMENT_SERVER_PORT=8081 로 배포 → 헬스체크를 8081 /actuator/health 로. 순서를 바꾸면 전부 unhealthy 다.
3. 메모리 75% 오토스케일 정책 삭제 — 부하를 한 번 받은 태스크는 힙을 다 커밋한 채 메모리 90% 에 머물러(누수 아님)
CPU 가 한가해도 태스크를 늘렸다. 태스크를 늘리면 RDS 커넥션만 늘어 오히려 느려진다(2차: 4대에서 RDS CPU 96%).
4. X-Ray API 규칙 초당 1건 + 5% — DAU 2만이면 초당 10건 + 10% 로 월 ~$135(코드 커밋 5e501ec 와 같은 값).
5. (선택) --max-inflight N: 태스크당 동시 API 요청 상한(APP_HTTP_MAX_INFLIGHT). 넘치면 1초 뒤 503 + Retry-After.
RDS 는 바꾸지 않는다 — t4g.medium 도 2 vCPU 라 CPU 병목에 효과가 없었다(2차 D1·D2)."""
import argparse, time, boto3

R = 'ap-northeast-2'
CLUSTER, SERVICE, TG_NAME = 'ruleup-prod-cluster', 'ruleup-prod-api', 'ruleup-prod-tg'
ALB_SG, ECS_SG = 'sg-03f6935fe4c111e77', 'sg-0ce610d9387070705'
MGMT = 8081

ap = argparse.ArgumentParser()
ap.add_argument('--cpu', default=None, help='태스크 CPU 단위(예: 1024). 생략하면 현재 값 유지')
ap.add_argument('--max-inflight', type=int, default=None)
args = ap.parse_args()

ecs = boto3.client('ecs', region_name=R); elb = boto3.client('elbv2', region_name=R)
ec2 = boto3.client('ec2', region_name=R); aas = boto3.client('application-autoscaling', region_name=R)
xray = boto3.client('xray', region_name=R)
tg = elb.describe_target_groups(Names=[TG_NAME])['TargetGroups'][0]
TG = tg['TargetGroupArn']


def wait_healthy(label):
for _ in range(40):
states = [t['TargetHealth']['State'] for t in elb.describe_target_health(TargetGroupArn=TG)['TargetHealthDescriptions']]
if states and all(s == 'healthy' for s in states):
print(f' {label}: healthy {len(states)}대'); return
time.sleep(15)
raise SystemExit(f'중단 — {label} 뒤 대상이 healthy 가 아니다: {states}. 헬스체크를 traffic-port·/actuator/health 로 되돌리고 확인할 것.')


# 1) ALB → ECS 8081
try:
ec2.authorize_security_group_ingress(GroupId=ECS_SG, IpPermissions=[{'IpProtocol': 'tcp', 'FromPort': MGMT, 'ToPort': MGMT,
'UserIdGroupPairs': [{'GroupId': ALB_SG, 'Description': 'ALB health check (management port)'}]}])
print('SG: ALB → ECS 8081 허용')
except ec2.exceptions.ClientError as e:
if 'Duplicate' not in str(e): raise
print('SG: 이미 허용됨')

# 2) 옛·새 태스크 모두 응답하는 경로로
if str(tg.get('HealthCheckPort')) != str(MGMT):
elb.modify_target_group(TargetGroupArn=TG, HealthCheckPort='traffic-port', HealthCheckPath='/health/public')
print('헬스체크: traffic-port /health/public (전환용)'); time.sleep(40); wait_healthy('전환 경로')

# 3) 태스크 정의: 관리 포트 · (선택) CPU · (선택) 동시 상한
svc = ecs.describe_services(cluster=CLUSTER, services=[SERVICE])['services'][0]
td = ecs.describe_task_definition(taskDefinition=svc['taskDefinition'])['taskDefinition']
c = next(x for x in td['containerDefinitions'] if x['name'] == 'api')
env = {e['name']: e['value'] for e in c.get('environment', [])}
want = {'MANAGEMENT_SERVER_PORT': str(MGMT)}
if args.max_inflight: want['APP_HTTP_MAX_INFLIGHT'] = str(args.max_inflight)
cpu = args.cpu or td['cpu']
ports = c.get('portMappings', [])
if td['cpu'] != cpu or any(env.get(k) != v for k, v in want.items()) or not any(p['containerPort'] == MGMT for p in ports):
env.update(want); c['environment'] = [{'name': k, 'value': v} for k, v in sorted(env.items())]
if not any(p['containerPort'] == MGMT for p in ports): ports.append({'containerPort': MGMT, 'hostPort': MGMT, 'protocol': 'tcp'})
c['portMappings'] = ports
if c.get('cpu'): c['cpu'] = int(cpu)
keep = ['family', 'taskRoleArn', 'executionRoleArn', 'networkMode', 'containerDefinitions', 'volumes', 'placementConstraints',
'requiresCompatibilities', 'runtimePlatform', 'ephemeralStorage', 'pidMode', 'ipcMode', 'proxyConfiguration']
kw = {k: td[k] for k in keep if td.get(k) not in (None, [])}
new = ecs.register_task_definition(cpu=cpu, memory=td['memory'], **kw)['taskDefinition']['taskDefinitionArn']
ecs.update_service(cluster=CLUSTER, service=SERVICE, taskDefinition=new)
print(f'ECS: {new.split("/")[-1]} 배포 — CPU {cpu}, 관리 포트 {MGMT}' + (f', 동시 상한 {args.max_inflight}' if args.max_inflight else ''))
ecs.get_waiter('services_stable').wait(cluster=CLUSTER, services=[SERVICE], WaiterConfig={'Delay': 15, 'MaxAttempts': 60})
wait_healthy('새 태스크(전환 경로)')
else:
print('ECS: 이미 반영됨')

# 4) 헬스체크를 관리 포트로
if str(tg.get('HealthCheckPort')) != str(MGMT):
elb.modify_target_group(TargetGroupArn=TG, HealthCheckPort=str(MGMT), HealthCheckPath='/actuator/health')
print(f'헬스체크: {MGMT} /actuator/health'); time.sleep(40); wait_healthy('관리 포트')

# 5) 메모리 오토스케일 정책 삭제(CPU 60% 는 유지)
for p in aas.describe_scaling_policies(ServiceNamespace='ecs', ResourceId=f'service/{CLUSTER}/{SERVICE}')['ScalingPolicies']:
if p['TargetTrackingScalingPolicyConfiguration']['PredefinedMetricSpecification']['PredefinedMetricType'] == 'ECSServiceAverageMemoryUtilization':
aas.delete_scaling_policy(PolicyName=p['PolicyName'], ServiceNamespace='ecs', ResourceId=p['ResourceId'],
ScalableDimension=p['ScalableDimension'])
print(f'오토스케일: {p["PolicyName"]} 삭제')

# 6) X-Ray API 규칙
rule = next((r['SamplingRule'] for r in xray.get_sampling_rules()['SamplingRuleRecords'] if r['SamplingRule']['RuleName'] == 'ruleup-api'), None)
if rule and (rule['FixedRate'] != 0.05 or rule['ReservoirSize'] != 1):
xray.update_sampling_rule(SamplingRuleUpdate={'RuleName': 'ruleup-api', 'FixedRate': 0.05, 'ReservoirSize': 1})
print('X-Ray: ruleup-api 초당 1건 + 5%')
print('완료 — 이제 main 머지(CD 가 새 이미지로 같은 태스크 정의를 다시 띄운다)')
48 changes: 48 additions & 0 deletions infra/loadtest/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 부하·성능 테스트 (k6)

2026-10 부하테스트에서 쓴 시나리오. 결과·개선 내역은 노션 「부하테스트 및 성능개선」.

## 준비 (stg 만)

| 무엇 | 어떻게 |
|---|---|
| 로그인 우회 | 태스크 정의 env `OAUTH_LOAD_TEST_ENABLED=true` — IdP 호출만 빼고 실제 로그인 트랜잭션을 탄다(`LoadTestOAuthGate`, prod 프로파일엔 빈이 없다). `code=loadtest-<id>`, `codeVerifier=<DEV_TOKEN_SECRET>` |
| AI 초안 한도 | `APP_CHALLENGE_DRAFT_RATE_LIMIT_PER_MINUTE` (기본 10) |
| prod 와 같은 조건 | 태스크 0.5 vCPU/2GB·2대(오토스케일 2~4, CPU 60%·메모리 75%), `JAVA_TOOL_OPTIONS=-XX:MaxRAMPercentage=60`, Hikari 15, RDS Multi-AZ, **DB 접속 `sslMode=REQUIRED`**(stg 기본 주소는 `useSSL=false` 라 그대로 재면 prod 보다 좋게 나온다) |
| Cloudflare 건너뛰기 | `-e ALB_IP=<stg ALB IP>` — TLS SNI·Host 는 도메인 그대로. 서버만 잰다 |

끝나면 반드시 되돌린다 — 우회 스위치 끄기, 스펙·Multi-AZ·오토스케일 원복.

## 실행

```sh
export LT_SECRET=... # stg 시크릿 ruleup-stg/app 의 DEV_TOKEN_SECRET. 저장소에 두지 않는다
k6 run -e LT_SECRET=$LT_SECRET -e ALB_IP=3.37.221.3 -e RUN_ID=lt2 -e USERS=3000 -e SCENARIO=seed -e VUS=25 infra/loadtest/k6/ruleup.js
k6 run ... -e SCENARIO=seedjoin ... # 가입·방 생성 뒤 남의 방 2곳 가입(사용자당 참여 방 3개 안팎)
k6 run ... -e SCENARIO=peak -e SCALE=1 -e DURATION=5m infra/loadtest/k6/ruleup.js
```

| SCENARIO | 내용 |
|---|---|
| `smoke.js` | 주요 API 를 한 번씩 — 계약 오류부터 거른다 |
| `seed` / `seedjoin` | 가짜 사용자 가입·공개 방 생성 / 남의 방 가입 |
| `login` | 아침 몰림 — 로그인 + 홈 + 리프레시, 초당 `RATE` |
| `browse` | 앱 열기·탐색(같은 정렬로 2페이지)·상세·내 방·마이페이지, 동시 `VUS` |
| `create` | 템플릿 초안 → 생성, 초당 `RATE` |
| `aidraft` | AI 초안(실제 LLM), 동시 `VUS` |
| `sync` | 건강정보 동의 → 인증 신호 업로드(backlog=true, 사용자당 10초 간격 — 429 는 정상 거절) |
| `peak` | 위를 실사용 비율로 섞음: 평상시(5분) → 피크(동시 400·로그인 15/s·sync 20/s·생성 3/s·가입 4/s, `DURATION`) → 마무리. `SCALE=2` 면 2배 |
| `breakpoint` | 동시 사용자를 200→2000 까지 2분씩 올리며 오류 5% 에서 멈춘다 |

임계값은 노션 SLO(로그인 3초·리프레시 1초·탐색/상세 1초·가입 1.5초·sync 3초·오늘/진행률 1초, p95)와 5xx 0건.

## 함정

- 시작일이 「내일」인 방을 대량으로 만들면 **자정에 한꺼번에 ACTIVE** 가 되어 그 뒤 측정의 데이터 모양이 바뀐다. 비교는 같은 데이터 상태에서 연달아 돈다.
- 같은 사용자 묶음으로 피크를 여러 번 돌리면 사용자당 참여 방이 계속 늘어난다(생성·가입). 비교마다 새 `RUN_ID` 로 시드하거나 순서를 붙여 돈다.
- Hikari `connection-timeout` 은 env 로 넣으면 「pool is sealed」로 기동 실패한다(풀이 먼저 시작됨).
- 비교 중에는 오토스케일 최대를 2로 묶어 2대에서 출발시키고, 시험 직전에 4로 푼다(앞 시험이 늘린 태스크가 남으면 출발점이 달라진다).
- 측정 중 로컬에서 gradle·docker 빌드를 돌리지 않는다(부하 발생기와 CPU 를 나눠 쓴다).
- 액세스 토큰은 30분이다. VU 가 오래 쥐는 세션은 `mine()` 이 25분마다 갱신한다 — 안 그러면 30분 넘는 시험 끝물이 전부 401 이다.
- 부하를 한 번 크게 받은 태스크는 힙을 다 커밋한 채 메모리 90% 에 머문다(누수 아님 — 장시간 시험에서 평평). 메모리 75% 오토스케일 정책은 그 뒤 CPU 가 한가해도 태스크를 늘린다. 태스크 수 비교는 새로 뜬 태스크에서 한다.
- 피크에서 X-Ray 전송이 계정 한도(429)에 걸리고 에이전트 스팬 큐가 넘친다 — `ruleup-api` 샘플링 규칙이 100% 면 그렇다.
Loading
Loading