안녕하세요 AI 네이티브 옵저버빌리티 플랫폼, 와탭랩스입니다. 와탭랩스는 웹 기반으로 다양한 모니터링 서비스를 제공하고 있습니다. 그중 로그 모니터링은 하루 최대 24억 건의 데이터를 수집하고 조회할 수 있을 만큼 대규모 데이터를 처리합니다.
이번 글에서는 애플리케이션, 서버, 데이터베이스, 쿠버네티스, 네트워크 장비 등에서 수집된 로그를 실시간으로 확인할 수 있는 ‘라이브 테일(Live Tail)’ 화면의 UX를 개선하기 위해, 우리가 정의했던 문제와 그 해결 과정을 공유하려고 합니다.
위 영상을 보셨을 때 어떤 생각이 드셨나요? 몇몇 고객분들이 아래와 같은 문제들을 제기하였고, 저 또한 이 문제에 공감했습니다.
로그 모니터링 고객은 터미널에서 tail -f 명령어를 입력하여 사용하는 UX에 익숙하여 이를 참고하기로 결정했습니다.
이러한 동작이 가능한 이유는 터미널과 브라우저의 로그 처리 방식의 차이에 있습니다.터미널은 로그를 GC(Garbage Collection) 하지 않고 단순히 로그 파일에 계속 쌓는 구조를 갖고 있습니다.
반면, 와탭의 브라우저 기반 로그 모니터링 서비스는 서버로부터 전달받은 로그 JSON 데이터를 무한정 저장할 경우 OOM(Out of Memory) 이 발생할 수 있기 때문에, 최대 2만 개까지만 로그를 유지하고 그 이상은 자동으로 GC하여 메모리를 관리합니다.
사용자가 과거 데이터를 확인하기 위해 스크롤을 위로 올리면 자동 스크롤이 일시 중지되어 로그를 안정적으로 읽을 수 있습니다. 이후 사용자가 스크롤을 최하단으로 내리면, 자동으로 최신 로그 스트리밍이 다시 시작됩니다.
export type ScrollStatus = 'auto' | 'manual';
동일한 브라우저 이벤트를 사용하고 있어, 구분이 어려웠습니다.
const handleMouseDown = (e) => {
const isScrollbar = e.offsetX > e.currentTarget.clientWidth;
if (!isScrollbar) {
return;
}
updateScrollStatus('manual');
stopScrollAnimation();
};
와탭의 로그 데이터는 하루 최대 24억 건을 수용할 수 있기에 virtualized 기법을 사용하여 화면에 보이는 요소만 렌더링하고 있습니다.
로그 데이터는 데이터마다 컨텐츠의 크기가 다르다는 특성이 있습니다.
일반적인 정적 높이 가상화 리스트에서는 element.scrollTop = element.scrollHeight - element.clientHeight 로 최하단 이동이 가능합니다. 하지만, 동적 높이의 가상화 리스트에서는 DOM 요소들이 동적으로 생성/제거되고 높이가 실시간으로 변경되기 때문에 이 방법이 제대로 작동하지 않습니다.
그리하여 rAF를 활용하여 auto 모드일 때, 지속적으로 바닥으로 자동 스크롤하도록 설계했습니다.
function startContinuousScroll() {
function continuousScrollLoop() {
if (scrollMode !== 'auto' || isUserInteracting) {
return;
}
// 매 프레임마다 최신 scrollHeight 계산
const maxScrollTop = element.scrollHeight - element.clientHeight;
element.scrollTop = maxScrollTop;
// 다음 프레임에서 다시 실행
requestAnimationFrame(() => {
continuousScrollLoop();
});
}
requestAnimationFrame(continuousScrollLoop);
}
실시간 로그 모니터링 시스템에서는 끊임없이 새로운 로그 데이터가 추가됩니다. 하지만 브라우저의 메모리는 무한하지 않기 때문에, 사용자가 현재 보고 있는 로그는 보존하면서도 메모리를 효율적으로 관리하는 가비지 컬렉션(GC) 알고리즘이 필요합니다.
하지만 터미널의 UX를 제공하기 위해서는 실시간으로 로그가 업데이트되면서, 애니메이션이 동작하면서, 자동 스크롤을 멈추었다면 멈춰서 해당 로그를 볼 수 있게 해야합니다.
자동 스크롤을 멈춘 경우: 스크롤 위치가 최하단이 아닌 경우
사용자가 자동 스크롤을 멈추었다면 새로운 로그 데이터가 갱신되면서, 메모리 최적화를 위해 GC를 하면서 보고 있던 데이터가 유지되도록 하는 방법이 고민이었습니다.
기존 방법대로 항상 최신 로그 데이터 2만개를 유지하면 사용자가 자동 스크롤을 멈춘 위치의 화면을 보고 있을 때, 새로운 데이터로 갱신되는 현상이 생깁니다.
새로운 데이터가 추가되어야 즉, 이동할 스크롤이 있어야 애니메이션이 발생합니다. 2만개가 되었을 때, 스크롤 위치도 바닥에 있으면 애니메이션이 동작하지 않습니다.
현재 데이터 개수가 15,000개이면 버퍼 추가 알고리즘을 통해 동적으로 max 데이터 크기가 늘어나면서 최대 2만개까지 데이터를 유지하도록 설계했습니다.
15,000 → 15,500 → … → 20,000 → 15,000 → 15,500 → …
export const linear = (startValue: number, endValue: number, t: number): number => {
return startValue + (endValue - startValue) * t;
};
export const easeInOutQuad = (t: number): number => {
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
};
다섯 가지 문제들을 해결함으로써,
제약 사항을 해결하며, 목표로 했던 사용자 경험을 제공할 수 있었습니다.
추가적으로 하루 최대 24억 건의 실시간 로그 인터랙션을 제공하기 위해서 성능도 고민해야 하고, 복잡한 문제를 해결한만큼 확장성 또한 고민해야 하는데요. 이 경험은 다음 편에서 소개드리겠습니다. 감사합니다.