24P by xguru 2달전 | favorite | 댓글 6개
  • 3년 전 Notion은 SQLite 데이터베이스를 사용하여 클라이언트에 데이터를 캐싱함으로써 Mac과 Windows용 Notion 앱의 속도를 성공적으로 향상시킴
  • 이번에는 브라우저를 통해 Notion에 접속하는 사용자들에게도 같은 개선 사항을 제공할 수 있게 되었음
  • 이 글은 WebAssembly(WASM) 구현의 sqlite3를 사용하여 브라우저에서 Notion의 성능을 개선한 방법을 심층 분석한 내용
  • SQLite를 사용하면 모든 최신 브라우저에서 페이지 탐색 시간이 20% 개선되었음
    • 특히 인터넷 연결 등의 외부 요인으로 인해 API 응답 시간이 특히 느린 사용자의 경우 그 차이가 더욱 두드러졌음
    • 예를 들어 호주 사용자의 경우 페이지 탐색 시간이 28%, 중국 사용자는 31%, 인도 사용자는 33% 빨라졌음

핵심 기술: OPFS와 Web Workers

  • WASM SQLite 라이브러리는 데이터를 세션 간에 유지하기 위해 Origin Private File System(OPFS)이라는 최신 브라우저 API를 사용함
  • OPFS는 Web Workers에서만 사용 가능함
  • Web Worker는 브라우저에서 대부분의 JavaScript가 실행되는 메인 스레드와 다른 별도의 스레드에서 실행되는 코드로 생각할 수 있음
  • Notion은 Webpack과 함께 번들링되는데, Web Worker를 로드하기 위한 사용하기 쉬운 구문을 제공함
  • Web Worker를 설정하여 OPFS를 사용하여 SQLite 데이터베이스 파일을 만들거나 기존 파일을 로드하도록 했고, 이 Web Worker에서 기존 캐싱 코드를 실행함
  • Comlink 라이브러리를 사용하여 메인 스레드와 Worker 간에 메시지 전달을 쉽게 관리함

SharedWorker 기반 접근 방식

  • 최종 아키텍처는 Roy Hashimoto가 GitHub 토론에서 제시한 새로운 솔루션을 기반으로 함
    • 한 번에 하나의 탭만 SQLite에 액세스하면서 다른 탭에서도 SQLite 쿼리를 실행할 수 있도록 하는 접근 방식
  • 이 새로운 아키텍처는 어떻게 작동하는가?
    • 간단히 말해, 각 탭에는 SQLite에 쓸 수 있는 전용 Web Worker가 있음
    • 그러나 실제로 Web Worker를 사용할 수 있는 것은 한 탭뿐임
    • SharedWorker는 "활성 탭"이 무엇인지 관리하는 역할을 함
    • 활성 탭이 닫히면 SharedWorker는 새로운 활성 탭을 선택해야 한다는 것을 알고 있음
  • SQLite 쿼리를 실행하려면 각 탭의 메인 스레드가 해당 쿼리를 SharedWorker로 보내고, SharedWorker는 활성 탭의 전용 Worker로 리디렉션함
  • 탭은 원하는 만큼 동시에 SQLite 쿼리를 실행할 수 있으며, 항상 단일 활성 탭으로 라우팅됨
  • 각 Web Worker는 모든 주요 브라우저에서 작동하는 OPFS SyncAccessHandle Pool VFS 구현을 사용하여 SQLite 데이터베이스에 액세스함

간단한 접근 방식이 작동하지 않은 이유

  • 앞에서 설명한 아키텍처를 구축하기 전에, 탭마다 전용 Web Worker를 두고 각 Web Worker가 SQLite 데이터베이스에 쓰는 보다 간단한 방식으로 WASM SQLite를 구동하려고 시도함
  • 그러나 어느 것도 그대로 사용하기에는 Notion의 요구 사항에 충분하지 않았음

걸림돌 #1: 크로스 오리진 격리

  • OPFS via sqlite3_vfs를 사용하려면 사이트가 "크로스 오리진 격리" 상태여야 함
  • 크로스 오리진 격리를 페이지에 추가하려면 로드할 수 있는 스크립트를 제한하는 몇 가지 보안 헤더를 설정해야 함
  • 이 헤더를 설정하는 것은 상당한 작업이 될 수 있음
  • Notion은 웹 인프라의 다양한 기능을 구동하기 위해 많은 제3자 스크립트에 의존하고 있어 완전한 크로스 오리진 격리를 달성하려면 각 벤더에게 새로운 헤더를 설정하고 iframe 작동 방식을 변경하도록 요청해야 했음 - 이는 현실적으로 어려운 요구 사항이었음
  • 테스트에서는 Chrome과 Edge 브라우저에서 사용 가능한 SharedArrayBuffer용 Origin Trials를 사용하여 사용자 하위 집합에 이 변형을 제공함으로써 중요한 성능 데이터를 얻을 수 있었음
  • 이러한 Origin Trials를 사용하면 크로스 오리진 격리 요구 사항을 일시적으로 우회할 수 있었음

걸림돌 #2: 손상 문제

  • OPFS via sqlite3_vfs를 소수의 사용자에게 제공했을 때, 일부 사용자에게 심각한 버그가 발생하기 시작함
    • 이 사용자들은 페이지에 잘못된 데이터를 보게 됨
    • 예를 들어 잘못된 동료에게 할당된 댓글이나 미리 보기가 완전히 다른 페이지인 새 페이지에 대한 링크 등
  • 이 버그의 영향을 받은 사용자의 데이터베이스 파일을 보면 SQLite 데이터베이스가 어떤 식으로든 손상된 패턴이 있었음
    • 특정 테이블의 행을 선택하면 오류가 발생했고, 행 자체를 검사했을 때 동일한 ID를 가진 여러 행에 서로 다른 내용이 있는 등의 데이터 일관성 문제가 발견됨
  • SQLite 데이터베이스가 어떻게 그런 상태가 되었는지에 대해, 동시성 문제로 인해 발생한 것으로 추측함
    • 여러 탭이 열려 있고, 각 탭에는 SQLite 데이터베이스에 대한 활성 연결이 있는 전용 Web Worker가 있었기 때문
    • Notion 애플리케이션은 서버에서 업데이트를 받을 때마다, 즉 탭이 동시에 같은 파일에 쓰게 될 때마다 캐시에 자주 씀
  • 이미 SQLite 쿼리를 함께 일괄 처리하는 트랜잭션 접근 방식을 사용하고 있었지만, OPFS API 측의 동시성 처리가 부족하여 손상이 발생한 것으로 강하게 의심됨
  • 그래서 손상 오류를 로깅하기 시작했고 Web Locks를 추가하고 포커스된 탭만 SQLite에 쓰도록 하는 등의 몇 가지 땜빵 접근 방식을 시도함
    • 이러한 조정으로 손상률은 낮아졌지만, 프로덕션 트래픽에 다시 기능을 켤 수 있을 만큼 충분하지는 않았음
    • 그래도 동시성 문제가 손상에 상당히 기여하고 있다는 것을 확인할 수 있었음
  • Notion 데스크톱 앱에서는 이 문제가 발생하지 않았음
    • 해당 플랫폼에서는 단일 부모 프로세스만 SQLite에 씀
    • 앱에서 원하는 만큼 많은 탭을 열 수 있지만 항상 단일 스레드만 데이터베이스 파일에 액세스함

걸림돌 #3: 대안은 한 번에 하나의 탭에서만 실행될 수 있음

  • OPFS SyncAccessHandle Pool VFS 변형도 평가했음
    • 이 변형은 SharedArrayBuffer가 필요하지 않아 Safari, Firefox 및 SharedArrayBuffer용 Origin Trial이 없는 기타 브라우저에서 사용할 수 있음
  • 이 변형의 단점은 한 번에 하나의 탭에서만 실행될 수 있다는 것
    • 후속 탭에서 SQLite 데이터베이스를 열려고 하면 단순히 오류가 발생함
  • 한편으로 이는 OPFS SyncAccessHandle Pool VFS가 OPFS via sqlite3_vfs 변형의 동시성 문제가 없다는 것을 의미함
    • 소수의 사용자에게 제공했을 때 손상 문제가 발견되지 않은 것으로 이를 확인함
  • 다른 한편으로는 모든 사용자 탭이 캐싱의 혜택을 받기를 원했기 때문에 이 변형을 그대로 출시할 수 없었음

문제 해결

  • 어느 변형도 그대로 사용할 수 없다는 사실이 위에서 설명한 SharedWorker 아키텍처를 구축하게 된 계기가 되었음
  • 이 아키텍처는 이 SQLite 변형 중 하나와 호환됨
  • OPFS via sqlite3_vfs 변형을 사용할 때는 한 번에 하나의 탭만 쓰기 때문에 손상 문제를 피할 수 있음
  • OPFS SyncAccessHandle Pool VFS 변형을 사용하면 SharedWorker 덕분에 모든 탭에서 캐싱이 가능함
  • 이 아키텍처가 두 변형에서 모두 작동하고, 측정 지표에서 성능 향상이 눈에 띄며, 손상 문제가 없다는 것을 확인한 후, 어떤 변형을 제공할지 최종 선택해야 할 때가 되었음
  • OPFS SyncAccessHandle Pool VFS를 선택했는데, 크로스 오리진 격리 요구 사항이 없어 Chrome과 Edge 이외의 브라우저로 출시하는 것을 막지 않았기 때문

성능 저하 완화

  • 이 개선 사항을 사용자에게 제공하기 시작했을 때, 로드 시간이 느려지는 등 수정해야 할 몇 가지 성능 저하가 발견됨

페이지 로드가 느려짐

  • 첫 번째 발견은 Notion 페이지 간 이동은 더 빨라졌지만 초기 페이지 로드는 더 느려졌다는 것
    • 프로파일링 결과, 페이지 로드는 일반적으로 데이터 가져오기에 병목 현상이 발생하지 않는다는 것을 깨달음
    • Notion의 앱 부팅 코드는 API 호출이 완료되기를 기다리는 동안 다른 작업(JS 파싱, 앱 설정 등)을 실행하므로 탐색만큼 SQLite 캐싱의 혜택을 받지 못함
  • 느려진 이유는 사용자가 WASM SQLite 라이브러리를 다운로드하고 처리해야 했기 때문
    • 이는 페이지 로드 프로세스를 차단하여 다른 페이지 로드 작업이 동시에 발생하지 않도록 함
    • 이 라이브러리의 크기가 몇 백 킬로바이트이기 때문에 추가 시간이 측정 지표에서 눈에 띄게 나타남
  • 이를 해결하기 위해 라이브러리 로드 방식을 약간 수정함
    • WASM SQLite를 완전히 비동기식으로 로드하고 페이지 로드를 차단하지 않도록 함
    • 이는 초기 페이지 데이터가 SQLite에서 로드되는 경우는 거의 없을 것이라는 의미였음
    • 이는 괜찮았는데, SQLite에서 초기 페이지를 로드하여 얻는 속도 향상이 라이브러리 다운로드로 인한 속도 저하보다 크지 않다는 것을 객관적으로 판단했기 때문
  • 변경 사항을 적용한 후 초기 페이지 로드 측정 지표는 실험의 테스트 그룹과 대조 그룹 사이에 동일해짐

느린 기기는 캐싱의 혜택을 받지 못함

  • 측정 지표에서 발견한 또 다른 현상은 Notion 페이지에서 다른 페이지로 이동하는 중간값 시간은 더 빨라졌지만, 95번째 백분위수 시간은 더 느려졌다는 것
    • Notion을 가리키는 브라우저가 있는 모바일 폰과 같은 특정 기기는 캐싱의 혜택을 받지 못했고 오히려 더 나빠졌음
  • 이 수수께끼에 대한 답을 모바일 팀에서 실행한 이전 조사에서 발견함
    • 네이티브 모바일 애플리케이션에서 이 캐싱을 구현했을 때 구형 Android 폰과 같은 일부 기기는 디스크에서 매우 느리게 읽었음
    • 따라서 디스크 캐시에서 데이터를 로드하는 것이 API에서 동일한 데이터를 로드하는 것보다 더 빠를 것이라고 가정할 수 없었음
  • 모바일 조사 결과, 페이지 로드에는 이미 두 개의 비동기 요청(SQLite와 API)을 서로 "경쟁"시키는 일부 로직이 있었음
    • 탐색 클릭을 위한 코드 경로에서 이 로직을 단순히 다시 구현함
    • 이는 실험의 두 그룹 간 탐색 시간의 95번째 백분위수를 동일하게 만들어 줌

결론

  • 브라우저에서 Notion에 SQLite의 성능 개선 사항을 제공하는 것은 나름의 어려움이 있었음
  • 특히 새로운 기술과 관련하여 일련의 알 수 없는 문제에 직면했고 그 과정에서 몇 가지 교훈을 얻었음:
    • OPFS는 기본적으로 동시성을 우아하게 처리하지 않음. 개발자는 이를 인식하고 그에 맞게 설계해야 함
    • Web Workers와 SharedWorkers(그리고 이 글에서 언급되지 않은 사촌 Service Workers)는 서로 다른 기능을 가지고 있으며, 필요한 경우 이들을 결합하는 것이 유용할 수 있음
    • 2024년 봄 현재 정교한 웹 애플리케이션에서 크로스 오리진 격리를 완전히 구현하는 것은 쉽지 않음. 특히 타사 스크립트를 사용하는 경우 더욱 그러함
  • 사용자를 위해 브라우저에 SQLite로 데이터를 캐싱한 결과, 앞서 언급한 탐색 시간 20% 향상을 보았고 다른 측정 지표가 저하되는 것은 보지 못했음
    • 중요한 것은 SQLite 손상으로 인한 문제가 관찰되지 않았다는 것
    • 이 최종 접근 방식의 성공과 안정성은 SQLite의 공식 WASM 구현을 담당한 팀, 그리고 대중에게 실험적 접근 방식을 제공한 Roy Hashimoto 덕분이라고 생각함

구글이나 페이스북 등의 경우 오래되고 느린 인터넷 환경의 접근성을 위해 저사양/저데이터 모드라는 서비스를 제공했었는데... 요즘은 구글이나 페북도 다 없애버렸죠.
물론 최신 기술은 성능이나 기능적 면에서 혜택은 크지만, 문제는 그로 인한 저사양 및 구형 모델의 차별이라는 과제가 남아 있습니다.
우리가 그토록 혐오하는 IE도 그 문제 중 하나지만, IE는 보안 문제 때문에 정당화될 수는 없었죠. 하지만 여전히 많은 기업에서는 IE를 주력으로 쓰고, 2024년 현재도 일부 업체에서는 윈도우 XP에 IE8로 업무를 봅니다. 근데 그들은 무시해도 되는 수준이긴 하지만 특히 금융 쪽에서는 이들 무시하면 X됩니다. 그래서 금융 및 공공 일부 프로젝트가 빡신 이유 중 하나가 이 IE 지원이라는 거대한 장벽이죠.

이래서 서드파티랑 협력해야 하는 서비스는 첫 출시부터 교차출처격리 활성화 하고 나가야...

오잉 cometkim님 반갑습니당

제 firefox에서 노션 페이지를 열면 먹통이 되어 쓸 수가 없는데 이거 때문이려나요.. (노션앱은 잘 동작해서 일단 그걸로 쓰고 있음)

아마 그럴겁니다. Enda도 크롬&엣지만 로컬 파일 쓰기를 지원하더라구요

예전 오래된 리눅스 랩탑에서 이런 적이 있었는데, 비공개 모드로 켜면 되더라구요