Asana에서는 웹앱의 중추 역할을 하는 LunaDb라는 데이터 로딩 시스템을 구축했습니다. 이름과 달리 데이터베이스가 아닙니다. 오히려 선언적으로 데이터를 가져오는 GraphQL과 유사한 시스템입니다. 기본적으로 최신 버전의 데이터와 향후 모든 업데이트를 로드하는 방법입니다.
당사는 처음에 2015년에 백엔드 인프라¹를 근본적으로 재작성하여 LunaDb를 출시했습니다. 이 새로운 시스템의 중심 구성 요소는 클라이언트 동기화에서 데이터 로딩 및 액세스 제어에 이르기까지 모든 작업을 수행하는 모놀리스인 동기화 서버였습니다. 상당한 변경 없이 이 초기 아키텍처는 초기 예상 수준을 훨씬 뛰어넘어 수백만 명의 주간 활성 사용자와 수십억 건의 일일 쿼리에 이르기까지 확장되었습니다.
성능은 여전히 우수했지만 트래픽과 기능의 복잡성이 증가함에 따라 동기화 서버의 제약으로 인해 LunaDb를 운영하고 개선하기가 점점 더 어려워졌습니다.
데이터 로딩 인프라 개요
운영이 어려웠던 이유는 무엇인가요?
트래픽 이동은 비용이 많이 듭니다
동기화 서버는 클라이언트에 대한 영구 웹소켓 연결을 직접 관리했습니다. 각 웹소켓은 상태 저장 클라이언트 세션에 의해 뒷받침되었습니다. 연결이 끊어지면 이 모든 상태가 삭제되고 클라이언트는 관심 있는 모든 데이터를 다시 구독하게 됩니다. 많은 세션에서 이런 일이 발생하면 금방 비용이 많이 듭니다. 따라서 재연결로 인한 작업의 급격한 증가를 고려하여 이러한 연결을 전환할 때 주의해야 했습니다.
트래픽 이동
동기화 서버를 배포한다는 것은 트래픽을 이동한다는 것을 의미합니다.
물론 트래픽 이동을 영구적으로 방지할 수는 없습니다. 새로운 코드를 푸시하거나 스케일 업/다운하려는 경우 항상 동기화 서버를 해제해야 하며, 이를 위해서는 종료 인스턴스에서 모든 트래픽을 이동해야 합니다.
지속적인 업데이트
동기화 서버는 시작 시 성능이 좋지 않습니다
동시에 동기화 서버는 상당한 양의 프로세스 워밍이 이루어진 후에만 성능을 발휘하게 됩니다. 이 두 가지 문제를 모두 관리하는 것은 상당히 취약한 균형이었으며 과거에는 광범위한 엔지니어링 작업이 필요했습니다.
동기화 서버는 복잡하고 모니터링하기 어렵습니다
마지막으로, 동기화 서버는 (사용자 지정 서버 측 기능을 통해) 많은 임의의 제품 코드를 실행했기 때문에 원인을 파악하기 어려운 노이지 네이버 기반 성능 저하에 매우 취약했습니다².
노이시 네이버 문제
“동기화 서버가 복잡하고 시작 시 성능이 좋지 않은 이유는 무엇인가요?”라는 합리적인 질문이 있습니다.
이러한 문제의 일반적인 원인은 서버 측 제품 코드입니다. 동기화 서버 코드의 주요 부분은 Scala로 작성됩니다. 세션 상태 관리 및 Luna 프레임워크의 다양한 측면과 관련된 일부 복잡성에도 불구하고 이 프레임워크/플랫폼 코드는 대부분 예상대로 작동합니다(운영 및 성능 문제는 비교적 적음).
반면에 이러한 제품 서버 계산 값(SCV라고 하지만 사용자 지정 해결자로 생각하세요)은 Typescript로 작성됩니다. 두 코드 세트는 Truffle 프레임워크를 통해 여러 언어를 사용할 수 있는 폴리글랏 VM인 GraalVM에서 함께 실행됩니다. SCV는 Typescript로 작성되었기 때문에 기본적으로 시작 시 해석되며, 이로 인해 성능과 CPU 사용량이 용납할 수 없는 수준으로 높아질 것으로 예상됩니다. GraalVM은 호출된 SCV에 대해 JIT 컴파일을 수행하려고 합니다. 좋습니다! GraalVM/Truffle은 성능을 크게 최적화할 수 있지만 그렇게 하는 것은 무료가 아닙니다. SCV 컴파일은 상당히 비용이 많이 들 수 있습니다(cpu, 코드 캐시 등).
다국어 VM 설정
두 가지 언어가 필요한 이유
SCV에 대한 첫 번째 디자인은 전적으로 Scala로 이루어졌습니다. 반면에, 당사의 변이 및 비동기 작업 시스템은 Javascript/Typescript로 작성되었습니다. Scala 기반 SCV는 작동했지만, 당사의 변이 및 비동기 작업 시스템과 LunaDb 간의 비즈니스 로직 중복과 제품 엔지니어링 팀이 Scala에 익숙하지 않은 것이 제품 속도에 상당한 걸림돌이 되었습니다.
GraalVM을 사용해야 하는 이유
구독 결과의 계산(및 재계산) 속도를 높이기 위해 프로세스 중인 많은 데이터를 캐시합니다. GraalVM을 사용하면 Scala 및 TypeScript 부분을 별도의 컨테이너로 분할하여 발생할 수 있는 정확성 또는 성능 문제 없이 이러한 캐시를 여러 언어 간에 간단하게 공유할 수 있습니다.
개선하기 어려운 이유는 무엇인가요?
서버가 너무 많은 일을 수행하고 운영하기에 상대적으로 취약했기 때문에 더 큰 변경 사항은 피하는 경향이 있었습니다. 코드의 복잡성 때문일 뿐만 아니라 새로운 변경 사항을 안전하게 롤아웃하는 데 드는 높은 오버헤드 때문이기도 합니다.
예, 저는 질문하는 것을 좋아합니다.
동기화 서버를 운영하고 개선하는 데 어려움이 있었기 때문에 아키텍처를 변경하기로 어려운 결정을 내렸습니다. 주로 모놀리식 동기화 서버를 두 가지의 더 작은 구성 요소로 대체하기로 결정했습니다.
클라이언트 연결 및 상태 해결을 관리하는 세션 브로커
데이터 로��을 담당하는 동기화 가능한 로더 (즉, 세션 브로커의 쿼리 처리)
세션 브로커 및 동기화 가능한 로더
이것이 도움이 되는 이유는 무엇인가요?
이 새로운 아키텍처는 즉시 웹소켓 트래픽 이동(예: 세션 브로커 배포)과 새로운 프로세스 워밍(예: 동기화 가능한 로더 배포)을 분리합니다. 그 결과, 세션 브로커와 별도로 동기화 가능한 로더를 배포하여 중단을 최소화할 수 있습니다.
이러한 새로운 구성 요소는 각각 더 간단합니다.
세션 브로커는 훨씬 가벼우며, 프로세스 워밍이 필요하지 않으며, 제품 코드를 실행하지 않습니다. 그 결과, 세션 브로커를 자주 배포할 필요가 없으며 배포할 때도 상당히 간단합니다.
동기화 가능한 로더는 표준 Kubernetes 수평 Pod 자동 크기 조정에 더 쉽게 적용할 수 있는 더 간단한 인터페이스(스테이트리스 구독 요청)를 가지고 있습니다. 이렇게 하면 워밍업도 더 빠르고 간단해집니다. 세션 브로커와 동기화 가능한 로더 간에 흐르는 주변 요청에 트래픽 미러링을 사용하기만 하면 됩니다.
새로운 아키텍처를 통해 제품 개발 프로세스를 크게 간소화할 수 있습니다. 초기부터 서버 측 제품 코드와 클라이언트 측 제품 코드 호출의 독립적인 배포 일정은 속도를 저해하고 운영상의 수고를 유발하는 요인이었습니다(버전 호환성 문제로 인해). 이제 동기화 가능한 로더가 제품 코드를 실행하는 유일한 프로세스이며 이를 배포하는 것이 더 이상 지장을 주지 않으므로 새로운 제품 코드를 푸시할 때마다 이를 다시 배포할 수 있습니다.
이 새로운 아키텍처를 사용하면 다양한 워크로드 유형(예: 수신함, 작업, 목표 등과 같은 서로 다른 기능)에 대해 서로 다른 동기화 가능한 로더 풀을 배포하여 새로운 기능으로 더 잘 확장할 수 있습니다. 세션 브로커는 데이터 쿼리가 다양한 업스트림 동기화 가능 로더로 전달되는 방식을 직접 제어할 수 있는 서비스 게이트웨이 역할을 합니다.
좋아요! 이 새로운 아키텍처는 훨씬 더 좋아 보이지만, 어떻게 실현했나요? 동기화 서버는 기본적으로 여러 기능을 포함하는 모놀리스이며, 모놀리스를 분할하는 것은 거의 항상 까다로운 일입니다. 저희의 경우, 몇 가지 주요 설계 장애물을 극복해야 했습니다.
PubSub 분할
반응성을 구현하는 시스템인 PubSub는 새로운 데이터를 로드하여 클라이언트로 보내는 단일 프로세스(동기화 서버)를 중심으로 설계되었습니다. 이제 독립적인 두 가지 프로세스 유형(동기화 가능한 로더 및 세션 브로커)에서 정확성을 보장하는 방식으로 PubSub를 재설계해야 했습니다.
동기화 서버에서 이것이 어떻게 구현되는지 간단히 살펴보겠습니다. 참고: 무효화 파이프라인에 대한 이전 게시물을 읽는 것이 도움이 될 수 있지만, 시스템에 대한 사전 조건 없는 보기를 제공합니다.
동기화 서버에서는 세션별로 구독을 추적합니다. 무효화 파이프라인을 사용하여 구독에 대한 업데이트를 지속적으로 모니터링합니다. 새로운 무효화 메시지마다 동기화 서버는 영향을 받는 모든 구독을 다시 로드합니다.
동기화 서버는 DB 객체/쿼리 결과, 사용자 지정 해결자 결과, 이전 구독 결과를 대량으로 캐시하여 구독 재로드를 최적화합니다(예: 리드 스루 패턴 사용). 캐시된 아티팩트는 구독에 사용되는 것과 동일한 무효화 파이프라인에 의해 수동적으로 무효화됩니다. 캐시된 데이터를 사용하려고 할 때마다 유효성을 확인하고 필요에 따라 재계산으로 대체합니다.
구독 재로드와 캐시된 데이터 무효화 간에 명확한 종속성을 관찰할 수 있습니다. 무효화를 수신한 후 캐시된 데이터가 무효화되기 전에 구독을 다시 로드하면 오래된 결과를 계산할 가능성이 있습니다. 데이터 로딩과 구독 관리가 동일한 프로세스에서 발생하는 경우 이 종속성을 보장하는 것은 간단합니다. 다시 로드하기 전에 캐시를 무효화하기만 하면 됩니다.
레이스 컨디션은 업데이트 시 데이터가 최신 상태가 아닐 수 있도록 할 수 있습니다
제안된 새로운 아키텍처에서 세션 브로커와 동기화 가능한 로더는 모두 무효화 파이프라인의 독립적인 컨슈머입니다. 그렇다면 구독을 다시 로드하기 전에 캐시를 무효화하도록 어떻게 강제할 수 있을까요?
요청 및 응답 버전 관리
무효화 파이프라인이 메시지를 동시에 전달하도록 하여 이 문제를 해결할 수 있었습니다. 또는 동기화 가능한 로더보다 먼저 세션 브로커에 무효화 파이프라인 메시지가 도착하지 않도록 순서 보장을 시행하는 메커니즘을 구축할 수도 있었습니다. 그러나 이 두 솔루션 모두 이상적인 절충안을 제공하지 못했습니다. 특히 세션 브로커와 동기화 가능한 로더의 결합을 증가시켰습니다.
대신 무효화 스트림의 상대적인 진행 상황을 기반으로 요청 및 응답 버전을 사용하여 데이터 로딩 프로토콜을 보강함으로써 이 문제를 해결했습니다. 스트림은 데이터베이스에 대한 업데이트의 총 순서를 나타내므로 스트림 진행 상태는 전역 버전 카운터처럼 사용할 수 있습니다.
요청 및 응답 버전
무효화 재로드
동기화 서버는 사이트에 대한 모든 읽기의 대부분인 엄청난 양의 데이터를 로드합니다. 새로운 아키텍처에서는 세션 브로커와 동기화 가능한 로더가 네트워크를 통해 많은 데이터를 교환해야 합니다. 신규 구독의 경우, 이 네트워크 오버헤드는 비교적 무시해도 될 정도입니다. 그러나 이는 무효화 재로드에 특히 비효율적입니다. 왜냐하면 종종 전체 응답을 반환할 필요가 없으며 업데이트된 데이터만 반환하면 되기 때문입니다³. 특정 경우는 특히 좋지 않습니다. 한 사용자가 프로젝트에서 10,000개의 작업을 페이징한 경우를 상상해 보세요. 다른 사용자가 이 프로젝트에서 작업 설명을 지속적으로 변경하는 경우, 무효화될 때마다 모든 작업을 전송해야 합니다! 업데이트된 데이터만 다시 보내는 것이 이상적인 것은 분명하지만, 이를 어떻게 효율적으로 구현할 수 있을까요?
무효화 재로드
핑거프린팅
동기화 가능한 로더가 업데이트된 데이터의 델타를 계산하려면 요청자가 이미 보유한 데이터를 알아야 합니다. 그러나 요청과 함께 최신 데이터를 전달하는 것은 전체 결과를 반환하는 것만큼 비용이 많이 듭니다. 데이터를 보다 공간 효율적인 방식으로 표현해야 합니다.
해싱은 공간을 절약하는 훌륭한 방법입니다. 우리가 중요하게 생각하는 세분화된 데이터의 각 비트를 Syncable이라고 합니다. 각 직렬화된 동기화 가능 항목의 128비트 머머 해시를 계산하여 지문으로 사용할 수 있습니다 ⁴. 구체적으로, 이 지문은 해당 버전의 동기화 가능 항목에 대한 식별자입니다.
동기화 가능한 데이터를 추적하는 모든 곳에서 대신 해당 지문을 사용할 수 있습니다. 이제 전체 구독 응답을 추적하려는 경우 전체 데이터를 전달할 필요 없이 일련의 지문을 사용하기만 하면 됩니다!
참고: 동기화 가능한 항목이란 무엇이며 구독과 어떤 관련이 있나요?
Syncable은 구독 결과의 콘텐츠입니다. 구독을 로드하면 결과가 일련의 동기화 가능한 항목으로 반환됩니다. 보다 구체적으로, 동기화 가능 항목은 개체, 쿼리 또는 SCV 결과일 수 있습니다.
구독 매핑에 동기화 가능
명확하게, 각 구독은 여러 동기화 항목에 매핑됩니다. 그러나 동기화 가능한 항목은 여러 구독에서 공유할 수 있습니다(중복되는 데이터를 로드하는 경우). 따라서 실제로 구독과 동기화 가능 항목 간에 다대다 매핑이 있습니다.
세션 브로커로부터의 각 요청과 함께 이러한 지문 집합을 전달합니다. 동기화 가능한 로더에서 전체 응답을 계산하고, 해당 응답의 지문 집합을 계산하고, 요청과 중복되는 모든 데이터를 제외하고, 델타를 반환합니다.
변경의 규모와 중요성을 고려하여 롤아웃을 대략 4단계로 나누었습니다.
1단계 - 모놀리스 리팩터링
고도로 결합된 세션 관리 및 데이터 로딩 코드를 독립적인 구성 요소로 분할
2단계 - 로컬 동기화 가능 로더
새로운 데이터 로딩 구성 요소를 사용하여 로컬 gRPC 서버를 생성하고 데이터 로딩을 통해 마이그레이션
3단계 - 원격 syncable-loader
새로운 syncable-loader 바이너리 및 배포 생성
모든 sync-server 데이터 로딩을 새로운 syncable-loader 배포로 마이그레이션
4단계 - 별도의 session-broker 바이너리
새 session-broker 바이너리 및 배포 생성
모든 트래픽을 동기화 서버에서 세션 브로커로 마이그레이션
너무 많습니다. 어디서부터 시작해야 할까요?
대규모 응답
우리가 빠르게 직면한 문제 중 하나는 큰 응답 크기였습니다. 동기화 서버에 데이터를 로드하는 것은 모두 동일한 프로세스 내에 있었기 때문에 지금까지는 큰 문제가 되지 않았습니다⁵. 그러나 로컬 gRPC 경계를 넘어 데이터를 로드하기 시작하자 많은 문제를 겪기 시작했습니다.
저희는 처음부터 일부 응답이 크다고 의심했지만, 이 문제를 조사하기 시작하면서 정말 놀라운 결과를 발견했습니다. 하루에 수천 건의 로드가 정기적으로 100MiB를 초과했습니다! 단일 gRPC 메서드를 통해 그렇게 큰 응답을 물리적으로 반환할 수 없었습니다(http2 최대 프레임 크기에 도달하기 시작합니다). 무엇을 해야 할까요?
이 문제를 체계적으로 해결하기 위한 몇 가지 방법을 고려했지만, 궁극적으로 근본 원인을 해결해야 한다는 결론을 내렸습니다. gRPC 서버 측 스트리밍을 구현할 수도 있었지만, 이로 인해 발생하는 높은 직렬화 비용과 증가된 소켓 경합은 대기 시간과 처리량에 상당히 부정적인 영향을 미칠 것입니다. 이러한 응답을 거부할 수도 있었지만, 발생률이 너무 높아서 이는 허용될 수 없었습니다.
우리는 모든 대용량 응답을 표시하고, 각 문제 사례를 분석 및 제거한 다음 응답 크기에 대한 엄격한 상한을 적용하는 3단계 접근 방식을 채택했습니다.
1MB를 초과하는 모든 로드를 표시하고 소스, 사용량, 데이터 분류에 대한 자세한 이벤트를 기록했습니다. 비용이 많이 드는 몇 가지 사용 사례가 있었지만, 가장 악명 높은 것은 아마도 첨부 파일 썸네일 Blob이었을 것입니다. 이들이 base64 문자열로 인코딩되어 직렬화된 응답에 포함되어 있었던 것으로 밝혀졌습니다. 소수일 때는 감당할 수 있었지만, 대량으로 로드되면 빠르게 방대해졌습니다. 예를 들어, 각 작업에 대한 첨부 파일 썸네일을 렌더링하는 그리드 기반 보기를 로드하는 경우와 같습니다.
대용량 응답에 대한 썸네일을 제한하고, 더 작은 썸네일을 사용하고, 결국 바이너리 데이터를 응답에서 제거함으로써 이와 같은 문제를 점진적으로 해결할 수 있었습니다. 이와 같은 간단한 완화 조치 후에는 대규모 응답 수가 사라졌고, 이후 프레임워크 수준의 응답 크기 제한을 적용할 수 있었습니다⁶.
주제 충돌
또 다른 이상한 문제는 Pubsub 토픽 충돌이었습니다. 도메인에 관계없이 동일한 구독 주제를 생성하는 규정 미준수 프레임워크 사용이 있었던 것으로 밝혀졌습니다. Pubsub가 단일 프로세스 유형에서만 발생했을 때 그 영향은 비교적 양호했습니다. 일반적으로 단일 pubsub 주제는 단일 도메인의 데이터에 해당합니다. 그러나 이제 pubsub이 세션 브로커와 동기화 가능한 로더로 분할되었기 때문에 두 프로세스 유형이 특정 주제의 도메인에 대해 의견이 일치하지 않을 수 있었습니다. 이러한 일이 발생하면 이 '도메인 불일치'로 인해 무효화 재로드의 비율이 꾸준히 상승하는 것을 볼 수 있었습니다. 다행히도 수정은 상당히 간단했지만, 이 버그가 감지되지 않고 프레임워크에서 이렇게 오랫동안 지속된 것은 흥미롭습니다.
워크로드 재조정
세션 브로커와 동기화 가능한 로더는 기존 동기화 서버와 상당히 다른 워크로드를 가지고 있습니다. 세션 브로커는 세션 관리에만 책임이 있으며 동기화 가능한 로더는 데이터 로딩에만 책임이 있습니다.
이것이 리소스 요구 사항에 어떤 영향을 미칠지 정확히 알 수 없었기 때문에 유사한 리소스(cpu/mem) 요청과 HPA(horizontal pod autoscaler) 설정으로 두 가지 모두를 시작했습니다.
session-brokers
우리가 관찰한 바에 따르면 세션 브로커가 상당히 더 가벼운 것이 분명해졌습니다. 매우 낮은 CPU에서 안정적으로 작동했습니다(만약 있다면 훨씬 더 많은 메모리에 구속되었습니다⁷). 인프라 셀 전체의 트래픽을 처리하기 위해 몇 개의 복제본이면 충분한 것 같았습니다. 그러나 실제로 HPA의 minReplicas를 줄였을 때 데이터 재로드 및 재로드 대기 시간이 급증하는 것을 관찰했습니다. 무슨 일이 있었던 걸까요?
간단히 말해, 캐시 크기 조정 및 스로틀러에 대한 모든 공유 설정을 고려하지 않았습니다. 복제본이 몇 개에 불과한 상태에서 각 세션 브로커는 일반적인 동기화 서버보다 Pod당 훨씬 더 많은 세션(약 3.5배)을 처리하고 있었습니다. 각각 훨씬 더 많은 데이터를 보고 있었기 때문에 pubsub 주제 ⇔ 구독 캐시를 완전히 채웠고, 각 제거는 (안전상의 이유로) 재로드를 트리거했습니다. 이 임계값을 적절하게 약 6배 증가시키면 증가된 캐시 제거 및 재로드 비율이 수정되었습니다. 마찬가지로, 계층적 재로드 스로틀러가 새로운 트래픽 속도에 맞지 않게 잘못 구성되어 있다는 것을 발견했습니다. 이러한 스로틀러 설정을 적정하게 조정하는 것도 마찬가지로 재로드 대기 시간과 엔드투엔드 반응성 지연(즉, 웹 앱이 자체 쓰기를 확인하는 데 걸리는 시간)을 약 5~10배나 크게 줄이는 데 기여했습니다.
syncable-loaders
반면에 동기화 가능한 로더는 예상보다 상당히 무거웠습니다. 각 서버는 동등한 리소스를 갖춘 동기화 서버보다 초당 더 많은 구독(약 1.5배)을 로드합니다. 세션 브로커와 달리 훨씬 더 CPU에 의존적이었습니다 ⁸.
흥미롭게도, CPU의 상당한 부분이 TS SCV 코드와 관련된 Truffle 디옵티마이제이션의 증가로 인한 것이었습니다. 가장 가능성이 높은 원인은 각 동기화 가능한 로더가 더 많은 SCV 코드에 액세스했기 때문입니다. 그럼에도 불구하고 코드 캐시 크기를 약간 늘려야 했습니다⁹.
프로세스 워밍
데이터 로딩을 위한 프로세스 워밍은 역사적으로 어려웠습니다. 다행히도 새로운 아키텍처에서는 조금 더 간단합니다. 당사의 syncable-loader 메인 인터페이스는 스테이트리스 데이터 쿼리이므로 세션 브로커와 syncable-loader 간의 기존 트래픽을 재생하거나 미러링하기만 하면 워밍할 수 있습니다.
반면에, 우리는 여전히 동기화 서버 워밍과 동일한 많은 어려움에 직면해 있습니다. 주로 프로세스를 워밍하는 데 많은 CPU가 소모되며, 이로 인해 시작 시 모든 종류의 노이지 네이버 문제(noisy neighbor problem)가 발생합니다(우리에게는 k8s 제한 한도에 도달하지 않기 때문에 여기에서 관련 지표는 부분 정지입니다). 여기서 우리가 이룬 좋은 개선 사항은 인플레이스 포드 크기 조정을 사용하여 시작 시 syncable-loader의 리소스를 제한하는 동시에 시작 후 버스트를 허용하는 것이었습니다.
그럼에도 불구하고 각 Pod를 워밍하는 데는 여전히 몇 분이 소요됩니다. JFR 프로파일을 살펴본 결과, 주요 병목 현상은 워밍 중 TS SCV 코드의 컴파일이 불충분한 데 있다고 생각하며, 여기에 더 많은 여유가 있다고 생각합니다. 저희는 더 타겟팅된 방법으로 관련 경로를 더 정확하게 워밍하고 더 나은 컴파일을 위해 TS 인터페이스¹⁰를 재구성하는 방법을 적극적으로 모색하고 있습니다.
세션 브로커는 데이터 로딩을 담당하지 않으며 실제로 어떤 종류의 워밍도 필요하지 않았습니다.
새로운 아키텍처는 운영 복잡성을 크게 줄이고, 배포 및 코드 속도를 높이며, 향후 속도 및 확장 기회를 열어주었습니다.
새로운 아키텍처는 복잡한 트래픽 관리를 필요로 하지 않고 총 읽기 트래픽의 변화에 따라 자동으로 확장됩니다. 각 종류의 트래픽 이동 작업은 단순히 Pod를 다시 시작하여 깔끔하게 처리됩니다. 모든 세션을 부팅해야 하나요? 모든 세션 브로커를 전환하세요. 캐시를 지워야 하나요? 모든 동기화 가능한 로더를 전환하세요. 두 작업 모두 가동 시간과 관련하여 안전합니다.
이전에는 동기화 서버를 배포하는 것이 제품 개발 속도의 주요 병목 현상이었습니다. 새로운 환경에서는 제품 코드를 배포하기 위해 동기화 가능한 로더만 배포하면 됩니다. 이 작업은 동기화 가능한 로더를 자체 배포 가능한 셀로 이동하여 수행했으며, 이 셀은 더 자주 배포하기 위해 노력하고 있습니다(그리고 궁극적으로 제품 코드와 함께 배포할 예정입니다). 동기화 가능한 로더를 배포하는 것은 이미 동기화 서버보다 약 40% 더 빠릅니다(약 20분 더 빠름). 훨씬 더 빠르게 안전하게 서지할 수 있으며, 앞으로 더 큰 증가를 목표로 하고 있습니다.
특히, 성능 개선은 이 작업의 목표가 아니었지만, 설계 및 구현에 얼마나 많은 부분이 포함되었는지 고려할 때 논의할 가치가 있습니다. 새로운 시스템에서는 쿼리 결과 계산 대기 시간이 실제로 개선되었습니다 (로드 밸런싱/튜닝/자동 스케일링이 개선되었기 때문일 가능성이 높음). 이에 따라 초기 구독 대기 시간이 눈에 띄게 개선되었습니다. 반면에 엔드투엔드 변이 반영 대기 시간(즉, 변경 사항이 다른 세션으로 배포되는 데 걸리는 시간)은 거의 동일합니다. 추적에 따르면 이는 세션 브로커와 동기화 가능한 로더 간에 PubSub 스트림을 사용하는 데 있어 편차가 있는 것이 원인인 것으로 보입니다(동기화 가능한 로더는 요청 버전이 최신 상태가 될 ���까지 요청을 처리할 수 없음).
현재 시스템은 훨씬 개선되었지만, 여전히 모든 릴리스에서 이전/향후 호환성에 대한 고려가 필요하여 제품 속도가 제한됩니다. 그러나 배포 속도가 전반적으로 개선됨에 따라 이제 모든 데이터 모델 변경 사항을 함께 배포할 수 있으므로 이전/후방 호환성에 대해 고려할 필요가 없습니다. 가까운 시일 내에 이를 구축할 수 있도록 적극적으로 검토하고 있습니다.
이 작업을 수행하는 주된 동기 중 하나는 원인을 파악하기 어려운 성능 저하였습니다. 특히, 이 작업의 결과로 상당한 개선이 이루어지지 않은 영역 중 하나입니다. 긍정적인 측면으로는, 이제 앞으로 해결해야 할 주요 문제 중 하나가 되었습니다. 여기서 논의된 대부분의 작업과 달리, 이는 제품 도메인, 데이터 모델, 프레임워크, 인프라 고려 사항을 포함하는 훨씬 더 교차 기능적인 문제입니다.
기능별 동기화 가능한 로더 작업자 풀
플랫폼 인프라(예: 기능별 작업자 풀), 플랫폼 프레임워크, 플랫폼 도구(예: 블랙 박스/화이트 박스 테스트)에 걸쳐 솔루션을 모색하게 되어 기쁩니다.
Arvind Vijayakumar는 LunaDb 팀의 엔지니어로, 사용자가 Asana의 웹 앱과 API 전반에 걸쳐 항상 정확하고 반응형이며 초고속으로 업데이트를 볼 수 있도록 하는 핵심 인프라인 Asana의 핵심 데이터 로딩 플랫폼을 구축하고 확장하는 데 도움을 주고 있습니다.
LunaDb를 확장하기 위한 이 작업은 지난 몇 년에 걸쳐 장기적으로 진행된 팀 작업으로, Brandon Zhang, Alex Matevish, Sean Wentzel, Eric Walton, Spencer Yu, Sophia Yao, George Ong, Tyler Prete, Koushik Ghosh, Natan Dubitski, Vinodh Chandra Murthy 등 현재 및 과거 LunaDb의 많은 구성원이 참여했습니다.
특히 Facebook이 GraphQL을 오픈 소스화하기 전에 이를 구축했습니다.
프로세스 워밍에 대한 이전 게시물에 더 많은 세부 정보가 있습니다. 하지만 요약하자면, 로컬 트래픽 급증에 빠르게 대응하기 위해 동기화 Pod가 임의로 버스트할 수 있도록 하는 방식을 사용합니다.
또한 (과거의 이유로) 필드별이 아닌 객체별 세분성으로 업데이트를 수행합니다. 이로 인해 무효화 재로드에 관련된 데이터 양이 증가합니다.
충돌을 방지하기 위해 128비트 머머 해시를 사용합니다.
특히, 저희는 매우 오랫동안 웹소켓 압축을 사용해 왔으며, 이로 인해 클라이언트가 인지하는 영향이 완화될 가능성이 높습니다.
특히, 원래는 네트워크 홉의 오버헤드에도 몇 가지 문제가 있었습니다. 조사를 진행한 결과, 대부분의 오버헤드는 실제로 서비스 메시(Istio/Envoy)로 인한 것으로 확인되었으며, 이에 따라 성능을 개선하기 위해 몇 가지 집중적인 조정을 실시했습니다.
보유 메모리의 중요한 요인은 서버 측 동기화 가능한 저장소를 데이터 해시만 보유하도록 다시 작성한 것입니다. 간단한 문제가 아니었기 때문에 이에 대한 후속 게시물을 게시할 수도 있습니다! 이것이 없으면 저장된 클라이언트 데이터로 인해 세션 브로커가 훨씬 더 많은 메모리를 사용하게 됩니다.
이전에는 모든 동기화 서버가 메모리 최적화 인스턴스에서 실행되었습니다. 반면, 동기화 가능한 로더는 혼합 인스턴스와 같이 CPU가 더 풍부한 노드 유형의 이점을 누릴 수 있습니다.
이러한 방식으로 GraalVM에서 TS를 실행할 때 주목할 만한 속성은 보다 표준적인 Scala/Java JVM 애플리케이션에서 일반적으로 사용하는 것보다 훨씬 더 많은 코드 캐시를 사용한다는 점입니다.
우리가 알 수 있는 한, 높은 다형성은 TS 컴파일 비용을 더 많이 발생시킵니다. 모노모픽 인터페이스로 변경하고 보고 다형 특수화를 활용하는 방안을 검토하고 있습니다.