부하테스트를 통한 실시간 스트리밍 및 대시보드 서비스 NUVION 개선기

실시간 스트리밍, 장치 이벤트, 대시보드 조회가 함께 들어오는 NUVION 백엔드에서 부하테스트로 병목을 특정하고, 코드와 실행 조건을 함께 정리해 개선한 과정.
서지호's avatar
Mar 10, 2026
부하테스트를 통한 실시간 스트리밍 및 대시보드 서비스 NUVION 개선기

NUVION의 백엔드는 단순한 CRUD API 서버가 아니다. 장치가 보내는 이벤트를 실시간으로 받아 저장하고, 대시보드 조회 요청을 처리하고, viewer에게 스트리밍 signaling을 중계한다. 평소에는 각각의 경로가 그럭저럭 버티더라도, 이 셋이 한꺼번에 겹치면 병목의 성격이 완전히 달라진다.

이번 글은 "성능이 안 좋았다"는 후기보다는, 혼합 부하에서 무너지는 시스템을 어떻게 측정 가능한 단위로 분해했고 어떤 경로가 실제 병목이었는지를 어떻게 판별했는지를 정리한 기록이다. 핵심은 감으로 튜닝한 것이 아니라 k6 + Artillery 기반 부하테스트를 먼저 만들고, 그 결과를 근거로 코드 경로를 줄였다는 점이다.

결론부터 말하면, 가장 심했던 구간에서 mixed load의 p95는 30초 안팎이었고 HTTP error rate는 98.97%였다. 이후 read path, report aggregation, websocket auth/session 경로를 순서대로 줄인 뒤에는 같은 스케일에서 mixed dashboard/logs/reports p95가 1.x초대로 내려왔고 HTTP error rate도 0%까지 회복됐다.

NUVION은 어떤 서비스인가

NUVION은 제조 현장의 장치, 실시간 모니터링 화면, 운영용 대시보드를 하나의 흐름으로 묶는 서비스다. 현장 장치는 카메라나 센서 기반 입력을 받아 상태와 이벤트를 계속 전송하고, 운영자는 대시보드에서 공간, 라인, 공정, 장치 상태를 확인하며, 필요할 때는 실시간 방송을 열어 장치 화면을 직접 본다.

조금 더 기능별로 나누면 역할은 다음과 같다.

  • Device / Agent

    • 장치 상태, 연결 상태, 생산 로그, 이상 이벤트를 전송

    • 필요 시 실시간 방송 uplink를 시작

    • 이상 클립 같은 미디어를 업로드

  • NUV-BE

    • 인증과 권한을 처리

    • 이벤트를 저장하고 조회 API를 제공

    • 대시보드용 realtime topic과 viewer signaling을 중계

    • Redis, MySQL, mediasoup, ingest bridge와 연결

  • Dashboard / Viewer

    • 공간, 라인, 공정, 장치 상태를 조회

    • 로그와 이상 이력을 확인

    • 특정 장치의 실시간 방송을 시청

이 글에서 다루는 성능 문제는 단순히 "API가 느리다" 수준의 문제가 아니었다. 같은 백엔드가 조회 요청도 처리하고, 장치 이벤트도 받아 적재하고, 실시간 signaling도 중계하니, 여러 종류의 부하가 동시에 겹칠 때 병목이 터지는 구조였다.

아키텍처를 단순화하면 아래와 같다.

Device / Agent
  |- state, log, anomaly, connectivity -> SockJS/STOMP -> NUV-BE
  |- live uplink -----------------------> mediasoup / ingest bridge
  |- clip upload -----------------------> Object Storage

Dashboard / Viewer
  |- REST ------------------------------> NUV-BE
  |- WebSocket signaling ---------------> NUV-BE
  |- WebRTC media <--------------------- mediasoup / ingest bridge

NUV-BE
  |- MySQL
  |- Redis
  |- mediasoup / ingest bridge
  |- Object Storage

실험 환경은 어떤 스펙이었나

성능 수치를 해석하려면, 어떤 자원 위에서 측정했는지도 같이 봐야 한다. 이번 측정은 dev Kubernetes 환경에서 진행했고, 첫 baseline을 찍기 시작했을 때의 클러스터 조건은 아래와 같았다.

클러스터 구성은 단순했다.

  • control-plane 1대

  • worker node 3대

  • worker node allocatable: 각 노드당 4 vCPU, 약 31.2 GiB memory

  • control-plane allocatable: 4 vCPU, 약 15.5 GiB memory

첫 baseline 시점의 애플리케이션 배포는 아래 기준이었다.

Component

Namespace

Replica

Requests/Limits

Notes

nuvion-be

nuvion-be-dev

1

없음

Spring Boot app container 1개 + Istio sidecar 1개

redis

nuvion-be-dev

1

없음

redis:7, persistence 비활성화

여기서 중요한 점은 두 컴포넌트 모두 Kubernetes requests, limits가 명시되지 않았다는 것이다. namespace에도 LimitRangeResourceQuota가 없었다. 즉 첫 번째 결과는 "CPU 500m, 메모리 1Gi로 제한된 애플리케이션"이 아니라, node headroom 안에서 실행되는 dev 배포 기준 수치에 가까웠다.

참고로 글을 정리하던 시점의 실행 스냅샷은 대략 이 정도였다.

  • nuvion-be app container: 약 9m CPU, 2473Mi memory

  • istio-proxy: 약 4m CPU, 52Mi memory

  • redis: 약 5m CPU, 5Mi memory

이 숫자는 부하테스트가 없는 순간의 관측값이라 성능 결과 자체를 설명하는 수치는 아니다. 다만 적어도 첫 baseline이 아주 빡빡한 cgroup 제한 안에서 나온 값은 아니라는 점은 보여준다.

어떤 부하에서 문제가 터졌나

문제가 드러난 조건은 아래와 같았다.

  • 장치 50대

  • viewer 10명

  • REST 조회 20 RPS

  • line 4개

  • process 4개

REST 트래픽은 네 가지로 섞었다.

  • dashboard 40%

  • logs 30%

  • anomalies 20%

  • reports 10%

여기에 장치 쪽 realtime 부하를 같이 걸었다.

  • state: 5초마다

  • log: 1초마다

  • connectivity: 10초마다

  • anomaly: 30초마다

여기서 말하는 mixed load는 REST 조회 트래픽과 websocket 기반 실시간 트래픽이 동시에 들어오는 상황을 뜻한다. 즉 dashboard, logs, anomalies, reports 같은 읽기 요청만 때리는 것이 아니라, 장치 이벤트 저장과 viewer signaling까지 함께 겹쳐지는 부하다.

이 조합이 중요한 이유는, REST only에서는 보이지 않던 문제가 mixed load에서만 터졌기 때문이다. 장치 이벤트 저장, viewer signaling, 대시보드 조회가 같은 DB, Redis, Hikari pool을 공유하니 특정 경로의 작은 비효율이 전체 시스템을 무너뜨렸다.

가장 초기 baseline의 mixed 결과는 아래와 같았다.

Metric

Baseline

Dashboard p95

30097.51ms

Logs p95

30102.18ms

Anomalies p95

30108.37ms

Reports p95

30110.07ms

HTTP error rate

98.97%

여기서 p95는 전체 요청 중 95%가 이 시간 안에 끝났다는 뜻이다. 반대로 말하면 가장 느린 5%의 요청은 이보다 더 오래 걸렸다. 평균 응답시간보다 p95를 더 중요하게 본 이유는, 실제 사용자 체감은 평균보다 느린 꼬리 구간에서 먼저 망가지기 때문이다. HTTP error rate는 전체 요청 중 실패한 요청의 비율이다.

이 수치는 "느리다" 수준이 아니다. 대부분의 요청이 사실상 timeout 경계까지 밀리거나 실패하고 있다는 뜻이다.

먼저 측정 체계를 만들었다

병목을 고치기 전에 한 일은 최적화가 아니라 계측과 재현 환경 구축이었다.

  • k6로 REST 부하 생성

  • Artillery로 SockJS, STOMP 기반 device, viewer 시나리오 생성

  • perf 전용 owner, viewer, space, device를 별도로 생성

  • bootstrap -> prefill -> baseline -> compare -> cleanup 순서로 반복 가능하게 정리

여기서 perf는 performance의 줄임말로, 성능 측정을 위한 전용 데이터와 계정, 장치 묶음을 뜻한다. 운영 데이터와 섞지 않고 반복 측정할 수 있도록 따로 만든 테스트용 공간이라고 보면 된다.

하네스(harness)는 이런 테스트를 한 번 돌릴 때 필요한 준비, 데이터 적재, 부하 실행, 결과 수집, 정리 과정을 묶어둔 실행 틀이다. 쉽게 말해 사람이 매번 수동으로 하지 않아도 같은 조건으로 다시 실행할 수 있게 만든 테스트 실행 세트다.

여기서 k6Artillery를 같이 쓴 이유를 먼저 짚고 넘어갈 필요가 있다.

k6는 HTTP API 부하를 재현하는 데 강한 도구다. 목표 RPS를 안정적으로 유지하면서 p95, p99, error rate 같은 지표를 바로 뽑아내기 좋고, 어떤 엔드포인트에 얼마나 비중을 줄지도 명시적으로 설계할 수 있다. 이번 테스트에서는 dashboard, logs, anomalies, reports 네 가지 REST 조회를 섞어 때리는 역할을 k6가 맡았다.

반면 Artillery는 연결을 유지하는 실시간 시나리오를 만들기에 더 적합했다. NUVION에서는 장치가 SockJS, STOMP로 접속해 state, log, anomaly, connectivity 이벤트를 보내고, viewer는 signaling 메시지를 주고받으며 방송을 시청한다. 이런 흐름은 단순한 HTTP RPS만으로는 재현되지 않는다. 그래서 device storm, viewer signaling, observer fan-out latency 측정은 Artillery로 구성했다.

정리하면 이번 부하테스트에서 두 도구의 역할은 명확했다.

  • k6: 대시보드와 조회 API의 읽기 부하 측정

  • Artillery: websocket 연결 유지, STOMP 송수신, viewer, device 시나리오 재현

RESTrealtime을 같은 도구로 억지로 처리하지 않고, 각 경로에 맞는 도구를 따로 써서 혼합 부하를 구성했다.

테스트는 세 가지 suite로 나눴다.

  • rest-only

  • realtime-only

  • mixed

이 구분이 중요했다. rest-only는 HTTP 조회만 때리는 테스트이고, realtime-only는 websocket 연결과 STOMP 송수신만 재현하는 테스트다. mixed는 이 둘을 동시에 건다. 어느 구간에서만 깨지는지 분리하지 않으면 원인을 틀리게 짚기 쉽다.

아키텍처 흐름은 단순화하면 아래와 같다.

Device -> SockJS/STOMP -> NUV-BE -> Redis / MySQL / mediasoup(ingest bridge) -> Viewer signaling / Dashboard REST

실제 하네스는 perf 전용 계정과 디바이스를 생성한 뒤, prefill 단계에서 로그와 이상 이벤트를 먼저 쌓아 두고, 그 위에서 baseline을 측정하는 구조였다. 이렇게 해야 조회 API도 의미 있는 데이터를 읽게 된다. 여기서 prefill은 본 측정 전에 테스트용 데이터를 미리 채워 넣는 단계, baseline은 개선 전 기준 측정, compare는 개선 후 같은 조건으로 다시 재는 단계다.

첫 번째 병목: 조회 경로가 너무 비쌌다

가장 먼저 걸린 건 dashboard와 log, anomaly list 경로였다.

DashboardService

문제는 device 수만큼 Redis를 반복 조회하는 구조였다. online 여부, broadcasting 여부, state snapshot을 개별적으로 확인하다 보니 장치 수가 늘수록 호출 수가 그대로 늘어났다.

핵심 수정은 배치 조회였다.

List<String> deviceIds = devices.stream()
        .map(Device::getDeviceId)
        .toList();
Set<String> onlineDeviceIds = lookupPresentKeys(ONLINE_STATUS_KEY_PREFIX, deviceIds);
Set<String> broadcastingDeviceIds = lookupPresentKeys(IS_BROADCASTING_KEY_PREFIX, deviceIds);
Map<String, DeviceStateSnapshot> stateSnapshots = deviceStateService.getStates(deviceIds);

lookupPresentKeys()DeviceStateService.getStates()는 내부에서 multiGet을 사용하도록 바꿨다. 구조는 단순하지만 mixed load에서는 이런 작은 반복 비용이 누적되면 곧바로 대시보드 p95로 튄다.

EventLogService / AnomalyLogService

로그 목록도 비슷했다. 페이지의 각 row를 응답으로 바꾸는 과정에서 line, process 이름을 row마다 다시 확인하고, device를 lazy 하게 만지면서 추가 조회를 만들고, signed URL도 반복 생성하고 있었다.

그래서 응답 변환에 page-scope context를 도입했다.

LogResponseContext context = buildResponseContext(page.getContent());
return page.map(log -> toResponse(log, context));

context 안에는 다음이 들어간다.

  • page에 등장한 lineId -> lineName

  • page에 등장한 processId -> processName

  • objectName -> signedUrl cache

여기에 @EntityGraph(attributePaths = "device")를 추가해 device hydration도 한 번에 가져오도록 바꿨다. anomaly 경로는 spec에서 root.fetch("device")를 적용했다.

결과적으로 이 구간은 "쿼리 한 번이 느린 문제"라기보다 "작은 조회와 변환이 페이지 크기만큼 반복되는 문제"였다.

두 번째 병목: 리포트는 애플리케이션보다 DB가 더 잘하는 일이었다

리포트 경로는 성격이 달랐다. dashboard, logs가 주로 반복 조회와 응답 매핑 비용의 문제였다면, report는 집계 책임을 어디에 둘지의 문제였다.

초기 구조는 애플리케이션 메모리에서 날짜별, 공정별 집계를 많이 수행했다. 이 방식은 로직을 Java에서 읽기 쉽게 만들 수는 있지만, 기간이 길어지고 데이터가 쌓일수록 애플리케이션이 해야 할 일이 너무 많아진다.

그래서 이 부분은 SQL aggregation으로 옮겼다.

SELECT DATE(e.created_at) AS report_date,
       e.process_id AS process_id,
       COUNT(*) AS total
FROM event_log e
WHERE e.space_id = :spaceId
  AND e.type = 'ANOMALY'
  AND e.created_at >= :from
  AND e.created_at < :to
GROUP BY DATE(e.created_at), e.process_id
ORDER BY report_date, process_id

리포트 집계는 애플리케이션이 다 끌고 와서 루프를 도는 것보다, DB가 그룹화해서 가져오게 하는 편이 더 자연스럽다. 이 변경으로 report 경로는 다른 read path 개선과 함께 mixed 환경에서 안정화됐다.

세 번째 병목: websocket 인증과 세션 경로였다

read path를 줄인 뒤에도 mixed load에서는 여전히 이상한 지점이 남았다. CPU는 높지 않은데 응답이 밀리고, Hikari active 수치가 차오르는 구간이 있었다. 이때 의심한 것이 websocket 인증과 permission check 경로였다.

STOMP CONNECT가 DB를 타고 있었다

초기에는 websocket CONNECT가 들어올 때 JWT를 검증한 뒤, 사용자 정보를 다시 DB에서 복원하는 흐름이 있었다. 이 구조는 개별 요청에서는 괜찮아 보여도, viewer가 몰리면 CONNECT 자체가 DB-bound가 된다.

그래서 JWT에 uid claim을 넣고, claim만으로 Authentication을 재구성할 수 있게 바꿨다.

JwtBuilder accessTokenBuilder = Jwts.builder()
        .setSubject(authentication.getName())
        .claim(AUTHORITIES_KEY, authorities)
        .setExpiration(tokenExpiresIn);
if (principalId != null) {
    accessTokenBuilder.claim(USER_ID_KEY, principalId);
}

그리고 토큰 복원도 claim 우선으로 바꿨다.

Long principalId = extractPrincipalId(claims);
UserDetails userDetails = principalId != null
        ? new CustomUserDetails(principalId, username, "", authoritiesFromToken)
        : customUserDetailsService.loadUserByUsername(username);

이 변경으로 websocket CONNECT 경로의 불필요한 DB hit를 크게 줄일 수 있었다.

PermissionService와 DeviceRealtimeController도 반복 조회를 줄였다

viewer signaling에서는 permission check가 자주 호출되고, device 쪽 realtime handler에서는 principal에서 장치를 다시 조회하는 일이 반복됐다. 그래서 websocket session 단위 캐시를 도입했다.

Boolean cachedPermission = WebSocketSessionCache.getPermission(deviceId, requiredLevel);
if (cachedPermission != null) {
    return cachedPermission;
}

device lookup도 세션 캐시를 우선 쓰게 바꿨다.

Device cached = WebSocketSessionCache.getDevice(deviceId);
if (cached != null) {
    return cached;
}

Device device = deviceRepository.findWithTopologyByDeviceId(deviceId)
        .orElseThrow(...);
WebSocketSessionCache.putDevice(deviceId, device);

이 수정이 의미 있었던 이유는, mixed load에서 viewer signaling과 device 이벤트 처리 모두 같은 시점에 permission, device context를 반복 확인하고 있었기 때문이다. 즉, 여기서는 "한 번의 조회를 더 빠르게"가 아니라 "같은 조회를 다시 하지 않게" 만드는 것이 맞는 해법이었다.

부수 경로도 병목이었다

성능 개선에서 자주 놓치는 구간이 있다. 핵심 기능이 아니라 부가 기능처럼 보이는 side-effect 경로다.

이번에는 broadcaster connect, disconnect 시점의 presence 처리와 email alert 경로가 그랬다.

  • presence log 저장

  • realtime topic publish

  • email alert 발송

  • notification settings 생성

초기에는 이들이 connect, disconnect 흐름과 너무 가깝게 붙어 있었다. 그래서 다음 작업을 했다.

  • presence side-effect를 @Async로 분리

  • email recipient resolve 결과를 짧은 TTL cache로 보관

  • notification settings 생성 race를 retry 가능하게 수정

  • async 경로에서 SpaceMembership.user lazy access가 터지지 않도록 eager load 추가

이 경로가 중요했던 이유는, websocket과 auth 경로를 줄인 뒤에도 connect, disconnect 주변의 비용이 남아 있었기 때문이다. 결국 실시간 시스템에서는 핵심 요청 처리뿐 아니라 그 주변에서 붙는 부가 작업도 같은 수준으로 다뤄야 했다.

코드만이 아니라 실행 조건도 고정했다

코드 경로를 줄인 뒤에는 한 단계가 더 남아 있었다. 성능 개선이 실제 효과인지 보려면, 애플리케이션이 어떤 자원 한도 안에서 실행되는지도 고정해야 했다. 첫 baseline 시점에는 nuvion-beredis 모두 Kubernetes requests, limits가 없었고, Spring Boot JVM heap도 명시적으로 제한하지 않고 있었다. 이런 상태에서는 pod가 node의 남는 자원을 경쟁적으로 사용하기 때문에, 같은 부하를 걸어도 결과 해석이 흐려진다.

그래서 다음 단계에서는 코드 최적화와 별개로 실행 조건을 정리했다.

  • nuvion-be

    • requests: 250m CPU, 2Gi memory

    • limits: 2000m CPU, 5Gi memory

  • istio-proxy

    • requests: 50m CPU, 128Mi memory

    • limits: 500m CPU, 256Mi memory

  • redis

    • requests: 50m CPU, 128Mi memory

    • limits: 250m CPU, 256Mi memory

  • JVM

    • JAVA_TOOL_OPTIONS=-Xms1024m -Xmx3072m -XX:MaxDirectMemorySize=512m -XX:+ExitOnOutOfMemoryError

여기서 한 번의 조정이 더 필요했다. 처음에는 nuvion-be의 CPU request를 1000m로 잡았는데, 실제 dev 클러스터에서는 새 pod가 Pending 상태에 머물렀다. 이유는 단순했다. worker node 3대 모두 예약된 CPU request가 이미 높아서, 롤링 업데이트가 추가 pod를 먼저 띄우는 순간 스케줄링 여유가 부족했다. 그래서 request를 250m로 낮췄고, 그 뒤에야 새 pod가 정상적으로 스케줄되고 rollout도 끝까지 진행됐다.

이 과정은 단순한 값 조정이 아니었다. 리소스 설정은 "충분히 크게" 잡는 문제가 아니라, 현재 클러스터가 실제로 예약할 수 있는 수준과 맞아야 한다는 점을 보여줬다. limit는 애플리케이션이 사용할 상한을 정하고, request는 스케줄러가 자리를 배정할 수 있는 현실적인 예약량이 되어야 했다.

설정 후에는 같은 부하를 다시 걸었다. 조건은 바꾸지 않았다. 여전히 50 devices + 10 viewers + 20 RPS였고, warmup 60초, measure 180초, cooldown 30초로 유지했다. 이 재측정은 코드 최적화 뒤에 리소스 경계를 명시했을 때 읽기 경로가 얼마나 안정되는지 보기 위한 것이었다.

결과는 아래와 같았다.

Metric

Before Resource Bounds

After Resource Bounds

Change

Dashboard p95

1803.99ms

422.33ms

-76.59%

Logs p95

1180.30ms

422.49ms

-64.21%

Anomalies p95

1391.95ms

627.76ms

-54.90%

Reports p95

1597.42ms

649.09ms

-59.37%

HTTP error rate

2.22%

1.22%

-44.98%

여기서 주목할 점은 requests/limits와 JVM heap을 명시한 뒤에도 성능이 나빠지지 않았다는 것이다. 오히려 REST 경로의 p95는 전반적으로 더 낮아졌고, error rate도 줄었다. 즉 이번 변경은 애플리케이션을 과도하게 조이는 제한이 아니라, 실행 조건을 더 예측 가능하게 만든 조정에 가까웠다.

같은 시점의 부하 후 스냅샷을 보면 nuvion-be app container는 약 1575m CPU, 566Mi memory를 사용했고, istio-proxy5m CPU, 31Mi memory, redis4m CPU, 3Mi memory 수준이었다. 이 값만 보면 메모리 limit는 충분히 여유가 있었고, CPU는 순간적으로 1.5 core 정도까지 사용할 수 있었다. 그래서 2 CPU / 5Gi limit는 과도하게 낮은 값이 아니었고, 반대로 request는 스케줄 가능한 수준으로 유지하는 편이 더 중요했다.

이 지점에서 HPA와 VPA도 같이 검토했다. 하지만 바로 autoscaling을 넣는 것이 정답은 아니었다. 당시 worker node들의 CPU request는 이미 96~98% 수준이라, replica를 더 늘리려 해도 새 pod가 다시 Pending 될 가능성이 컸다. 즉 scale-out보다 먼저 해야 할 일은 pod 하나가 안정적으로 배포되고, 같은 부하를 예측 가능한 자원 경계 안에서 소화하도록 만드는 것이었다. VPA 역시 검토 대상이었지만, 클러스터에 CRD 자체가 설치돼 있지 않았고, 도입하더라도 자동 적용보다 recommendation 모드로 시작하는 것이 맞는 단계였다.

결과는 어떻게 달라졌나

본문에서 메인 비교로 사용할 수 있는 가장 강한 숫자는 아래 두 지점이다.

  • 최악의 baseline mixed

  • websocket session cache까지 반영한 follow-up mixed

Metric

Baseline

Optimized Follow-up

Change

Mixed dashboard p95

30097.51ms

1477.69ms

-95.09%

Mixed logs p95

30102.18ms

1236.67ms

-95.89%

Mixed anomalies p95

30108.37ms

1600.78ms

-94.68%

Mixed reports p95

30110.07ms

1470.45ms

-95.12%

Mixed HTTP error rate

98.97%

0.00%

recovered

보조 지표로 보면 gated rest-only 검증도 충분히 좋아졌다.

Metric

Rest-only Follow-up

Dashboard p95

758.44ms

Logs p95

791.96ms

Anomalies p95

927.21ms

Reports p95

1024.57ms

이 변화는 머신 스펙을 올려서 얻은 것이 아니다. 같은 dev 클러스터, 같은 replica 수에서 코드 경로를 줄였더니 나온 변화다. 그래서 이번 개선을 "인프라 튜닝"보다 코드 병목 제거로 해석하는 게 맞다.

코드 병목은 어떻게 판단했나

이번 개선의 핵심 판단 기준은 아래 세 가지였다.

  1. 리소스를 늘리지 않았는데 지표가 크게 좋아졌는가 - 그렇다면 코드 경로가 병목이었을 가능성이 높다.

  2. CPU가 낮은데 Hikari나 DB contention 신호가 올라가는가 - 그렇다면 연산량보다 I/O 경합이나 반복 조회 문제일 가능성이 높다.

  3. rest-only, realtime-only, mixed 중 어디에서만 깨지는가 - mixed에서만 깨지면 공유 의존성 경합을 의심해야 한다.

이 기준 덕분에 병목이 연산 자체인지, 반복 조회와 공유 자원 경합인지, 아니면 특정 경로에만 국한된 문제인지를 더 분명하게 볼 수 있었다.

마무리

이번 작업으로 해결한 것

  • dashboard read path의 반복 Redis 조회

  • logs, anomalies response mapping의 page-row 비용

  • report aggregation의 메모리 중심 처리

  • websocket auth, permission, device lookup의 반복 DB 접근

  • presence, alert 경로의 connect, disconnect 부하

Share article

플래드