Node.js에서 읽기 전용 PostgreSQL 복제본으로 쿼리 라우팅

4196 단어 nodepostgres
데이터베이스 확장은 어렵습니다. 그러나 아마도 가장 낮은 성과는 읽기 전용 복제본을 도입하는 것일 것입니다.

일반적인 부하 분산 요구 사항은 모든 "논리적"읽기 전용 쿼리를 읽기 전용 인스턴스로 라우팅하는 것입니다. 이 요구 사항은 두 가지 방법으로 구현할 수 있습니다.
  • 두 개의 데이터베이스 클라이언트(읽기-쓰기 및 읽기 전용)를 만들고 필요에 따라 응용 프로그램에 전달합니다.
  • 미들웨어를 사용하여 쿼리 자체를 기반으로 연결 풀에 쿼리를 할당합니다.

  • 옵션 1: 두 개의 서로 다른 클라이언트



    첫 번째 옵션이 가장 명시적이므로 선호됩니다. 그러나 구현하는 데 가장 많은 오버헤드가 있습니다. 기존 코드베이스에 이를 추가하면 수천 줄의 코드를 수정해야 할 수 있습니다. 이 접근 방식의 다른 단점은 읽기 전용 쿼리가 읽기-쓰기 데이터베이스 클라이언트를 사용할 때 간과하기 쉽다는 것입니다. 이것이 주요 문제는 아니지만 그 반대가 참이라면 좋을 것입니다. 쿼리에 대해 잘못된 연결을 사용하는 경우 경고를 받게 됩니다. 쿼리를 라우팅하는 미들웨어를 사용하면 가능할 수 있습니다.

    옵션 2: 미들웨어



    두 번째 옵션은 쿼리 자체와 쿼리가 시작된 컨텍스트를 기반으로 쿼리를 라우팅합니다. 쿼리를 읽기 전용 인스턴스로 안전하게 라우팅하려면 다음이 참이어야 합니다.
  • SELECT 쿼리여야 합니다.
  • 트랜잭션의 일부가 아니어야 합니다.
  • 휘발성 함수를 실행하면 안 됩니다.

  • 처음 두 요구 사항은 구현하기가 상대적으로 간단합니다. 세 번째는 우리가 컨벤션을 도입할 것을 요구합니다.

    휘발성 함수 처리



    PostgreSQL의 휘발성 함수는 부작용(예: 데이터 쓰기)이 있거나 단일 테이블 스캔 내에서도 출력을 변경할 수 있는 함수(예: random())입니다. 우리의 맥락에서 휘발성 기능의 첫 번째 부분만 관련이 있습니다. 그러나 쿼리를 보는 것만으로는 휘발성인지 여부를 알 수 있는 방법이 없습니다.

    SELECT foo() # Is foo() volatile?
    


    따라서 이러한 쿼리를 읽기 전용 라우팅에 대해 안전하지 않은 것으로 표시하기 위한 규칙이 필요합니다. 이를 수행하는 방법은 여러 가지가 있지만 쿼리를 휘발성으로 표시하는 매직 주석이라는 매우 간단한 접근 방식을 선택했습니다. 쿼리(주석)의 아무 곳에나 @volatile를 추가하여 부작용이 있음을 나타내고 미들웨어가 해당 키워드를 확인하도록 합니다.

    여기에서 역으로 수행하고 @readOnly 키워드를 사용하여 읽기 전용 인스턴스로 라우팅하기에 안전한 쿼리를 표시하는 규칙을 사용할 수도 있습니다. 이렇게 하면 다른 키워드에 대한 쿼리를 모두 확인하지 않고 엔지니어가 계측하는 것에 전적으로 의존할 수 있습니다. 그러나 이 접근 방식은 #1 옵션과 동일한 단점이 있습니다. 기회를 놓치기 쉽고 모든 쿼리를 하나씩 편집해야 합니다.

    Slonik을 사용하여 쿼리 라우팅 구현



    우리는 프로젝트에서 Slonik PostgreSQL 클라이언트를 사용하고 있으며 운 좋게도 Slonik에는 3가지 요구 사항을 모두 구현하는 데 사용할 수 있는 beforePoolConnection middleware이 있습니다. 즉, 쿼리에 연결이 할당되기 직전에 beforePoolConnection가 호출됩니다. 세 가지 조건이 모두 충족되면 두 번째 읽기 전용 풀을 만들고 해당 읽기 전용 풀로 쿼리를 라우팅하기만 하면 됩니다. 코드는 매우 간단합니다.

    const readOnlyPool = await createPool('postgres://read-only');
    const pool = await createPool('postgres://main', {
      interceptors: [
        {
          beforePoolConnection: (connectionContext) => {
            if (!connectionContext.query?.sql.trim().toUpperCase().startsWith('SELECT ')) {
              // Returning null falls back to using the DatabasePool from which the query originates.
              return null;
            }
    
            // This is a convention for the edge-cases where a SELECT query includes a volatile function.
            // Adding a @volatile comment anywhere into the query bypasses the read-only route, e.g.
            // sql`
            //   # @volatile
            //   SELECT write_log()
            // `
            if (connectionContext.query?.sql.includes('@volatile')) {
              return null;
            }
    
            // Returning an instance of DatabasePool will attempt to run the query using the other connection pool.
            // Note that all other interceptors of the pool that the query originated from are short-circuited.
            return readOnlyPool;
          }
        }
      ]
    });
    
    // This query will use `postgres://read-only` connection.
    pool.query(sql`SELECT 1`);
    
    // This query will use `postgres://main` connection.
    pool.query(sql`UPDATE 1`);
    


    위의 코드에 대해 명확히 할 가치가 있는 몇 가지 사항은 다음과 같습니다.
  • 이 미들웨어에서는 SELECT INTO를 처리하지 않습니다. 사용 사례에 필요하지 않다는 것을 알고 있으므로 생략했습니다. SELECT INTO 를 사용하는 경우 쿼리에 INTO 키워드가 포함되어 있는지 확인하는 간단한 수정이 될 수 있습니다.
  • 클라이언트가 pool#connect() 또는 pool#transaction() , then connectionContext.query is null`을 사용하여 연결을 시작하는 경우 즉, 기본 풀을 사용하도록 폴백한다는 점을 강조할 필요가 있습니다.

  • 그리고 그게 다야! 짧고 달다. 이제 기본 인스턴스의 로드를 줄이고 서비스를 더 잘 확장할 수 있는 efficient query routing mechanism이 있습니다.

    좋은 웹페이지 즐겨찾기