구독해서 새 게시물에 대한 알림을 받으세요.

D1 구축하기: 글로벌 데이터베이스

2024-04-01

8분 읽기
이 게시물은 English, 繁體中文, Français, Deutsch, 日本語, Español简体中文로도 이용할 수 있습니다.

Worker 앱을 구축하는 개발자는 필요한 인프라에 대해 걱정하지 않고 구축 중인 앱에만 집중하고 Cloudflare 네트워크의 이점을 활용할 수 있습니다. 개인 프로젝트부터 비즈니스 크리티컬 워크로드에 이르기까지 많은 앱에는 영구적인 데이터가 필요합니다. Workers는 키-값 및 개체 스토리지와 같이 개발자의 필요에 맞는 다양한 데이터베이스 및 스토리지 옵션을 제공합니다.

오늘날 많은 앱은 관계형 데이터베이스를 기반으로 구축됩니다. 이제 모든 사용자는 Cloudflare의 관계형 데이터베이스를 보완하는 D1을 이용할 수 있습니다. 2022년 말 알파 버전에서 2024년 4월 정식 출시(GA) 버전에 이르는 여정은 개발자가 관계형 데이터와 SQL에 익숙한 상태에서 프로덕션 워크로드를 구축할 수 있도록 하는 데 중점을 두었습니다.

D1이란 무엇인가요?

D1은 Cloudflare의 기본 제공 서버리스 관계형 데이터베이스입니다. Worker 앱의 경우, D1은 SQLite의 SQL 방언을 사용하는 SQL의 표현성과 Drizzle ORM과 같은 객체-관계 매퍼(ORM)를 비롯한 개발자 도구 통합을 제공합니다. D1은 Workers 또는 HTTP API를 통해 액세스할 수 있습니다.

서버리스는 프로비저닝이 필요 없고 Time Travel을 통한 기본적인 재해 복구와 사용량 기반 요금제를 의미합니다. D1에는 개발자가 프로덕션으로 전환하기 전에 D1을 실험해 볼 수 있는 넉넉한 무료 티어가 포함되어 있습니다.

데이터를 글로벌화하는 방법은 무엇일까요?

D1 GA는 안정성과 개발자 경험의 만족도를 높이는 데 주력해 왔습니다. 이제 Cloudflare는 전 세계에 분산되어 있는 앱에 더 나은 지원을 제공하기 위해 D1을 확장할 계획입니다.

Workers 모델에서 요청이 수신되면 가장 가까운 데이터 센터에서 서버리스 실행을 호출합니다. Worker 앱은 사용자 요청에 따라 전 세계적으로 확장할 수 있습니다. 그러나 앱 데이터는 중앙 집중식 데이터베이스에 저장되며, 글로벌 사용자 트래픽은 데이터 위치에 액세스하기 위해 왕복해야 합니다. 예를 들어, 오늘날 D1 데이터베이스는 단일 위치에 있습니다.

Workers는 자주 액세스하는 데이터 위치를 고려하기 위해 Smart Placement를 지원합니다. Smart Placement는 데이터베이스와 같은 중앙 집중식 백엔드 서비스에 더 가까운 곳에 있는 Worker를 호출하여 대기 시간을 줄이고 앱 성능을 개선합니다. Cloudflare는 글로벌 앱에서 Worker 배치를 다뤘지만, 데이터 배치 문제도 처리해야 합니다.

그렇다면 Cloudflare의 기본 제공 데이터베이스 솔루션인 D1이 어떻게 글로벌 앱의 데이터 배치를 더욱 효과적으로 지원할 수 있을까요? 해답은 비동기 읽기 복제에 있습니다.

비동기 읽기 복제란 무엇인가요?

Postgres, MySQL, SQL Server 또는 Oracle과 같은 데이터베이스에는 읽기 복제본이라는 서버 유형이 있습니다. 이 서버는 거의 최신 상태의 읽기 전용 사본 역할을 하는 별도의 기본 데이터베이스 서버입니다. 관리자는 주 서버의 스냅샷에서 새 서버를 시작하고 주 서버가 복제본 서버에 업데이트를 비동기적으로 전송하도록 구성하여 읽기 복제본을 만듭니다. 업데이트가 비동기적으로 이루어지기 때문에 읽기 복제본은 주 서버의 현재 상태보다 늦어질 수 있습니다. 기본 서버와 복제본 서버 사이의 이러한 지연을 복제본 지연이라고 하며, 읽기 복제본을 두 개 이상 보유할 수 있습니다.

비동기 읽기 복제는 데이터베이스 성능을 개선하기 위해 긴 시간 동안 검증된 솔루션입니다.

  • 여러 복제본에 부하를 분산하여 처리량을 늘릴 수 있습니다.

  • 복제본이 쿼리를 수행하는 사용자와 가까운 곳에 있으면 쿼리 대기 시간을 줄일 수 있습니다.

일부 데이터베이스 시스템은 동기 복제 기능도 제공합니다. 동기 복제 시스템에서는 모든 복제본이 쓰기를 확인할 때까지 기다려야 합니다. 동기 복제 시스템은 가장 느린 복제본과 같은 속도로 실행될 수 있으며 복제본에 장애가 발생하면 작업이 중단될 수 있습니다. 따라서 글로벌 규모로 성능을 개선하고 싶다면 동기 복제 사용을 최소화하는 것이 좋습니다!

일관성 모델 및 읽기 복제본

대부분의 데이터베이스 시스템은 구성에 따라 읽기 커밋(read committed), 스냅샷 격리 또는 직렬화할 수 있는 일관성 모델을 제공합니다. 예를 들어, Postgres는 읽기 커밋 모드를 기본값으로 사용하지만 더 강력한 모드를 사용하도록 구성할 수 있습니다. SQLite는 WAL 모드에서 스냅샷 격리를 지원합니다. 스냅샷 격리 또는 직렬화 가능과 같은 강력한 모드를 사용하면, 허용되는 시스템 동시성 시나리오와 프로그래머가 고민해야 하는 동시성 경쟁 조건이 제한되기 때문에 프로그래밍이 더욱 쉬워집니다.

읽기 복제본은 독립적으로 업데이트되기 때문에 각 복제본의 내용은 언제든지 달라질 수 있습니다. 주 복제본이든 읽기 복제본이든 모든 쿼리가 동일한 서버로 전송되는 경우 기본 데이터베이스가 지원하는 일관성 모델에 따라 결과가 일관성 있게 유지되어야 합니다. 읽기 복제본을 사용하는 경우 결과는 다소 오래된 것일 수도 있습니다.

읽기 복제본이 있는 서버 기반 데이터베이스를 사용할 때는 세션 내의 모든 쿼리에 동일한 서버를 일관되게 사용하는 것이 중요합니다. 동일한 세션 내에서 서로 다른 읽기 복제본으로 전환하게 되면 앱에서 설정된 일관성 모델이 손상될 수 있습니다. 이로 인해 데이터베이스 작동 방식에 대한 가정을 위반하여 앱에서 잘못된 결과가 반환될 수도 있습니다!

예시A와 B라는 두 개의 복제본이 있다고 가정해 보겠습니다. 복제본 A는 기본 데이터베이스보다 100밀리초가, 복제본 B는 2초 지연됩니다. 사용자가 다음과 같은 상황을 원한다고 가정해 보겠습니다.

  1. 쿼리 1 실행1a. 쿼리 1 결과를 기반으로 일부 계산

  2. (1a)의 계산 결과를 기반으로 쿼리 2 실행

시간 t=10초가 되면 쿼리 1이 복제본 A로 이동하여 반환됩니다. 쿼리 1은 t=9.9초에서 기본 데이터베이스의 상태를 반영합니다. 계산을 처리하는 데 500밀리초가 걸린다고 가정하면, t=10.5초에서 쿼리 2가 복제본 B로 전송됩니다. 복제본 B는 기본 데이터베이스보다 2초 지연되므로, t=10.5초가 되면 쿼리 2는 t=8.5초의 데이터베이스 상태를 반영합니다. 앱의 관점에서 보면 쿼리 2의 결과는 마치 데이터베이스가 시간을 거슬러 올라간 것처럼 보입니다!

공식적으로 쿼리는 커밋된 데이터만 확인하기 때문에 이를 읽기 커밋된 일관성(read committed consistency)이라고 합니다. 그러나 다른 보장은 없으며, 심지어 사용자가 직접 작성한 데이터를 읽을 수도 없습니다. 읽기 커밋은 유효한 일관성 모델이지만, 읽기 커밋 모델이 허용하는 모든 잠재적 경쟁 조건을 추론하기는 어렵기 때문에 앱을 올바르게 작성하기 어렵습니다.

D1의 일관성 모델 및 읽기 복제본

기본적으로 D1은 SQLite가 지원하는 스냅샷 격리 기능을 제공합니다.

스냅샷 격리는 대부분의 개발자가 잘 알고 있으면서 쉽게 사용할 수 있는 일관성 모델입니다. Cloudflare는 D1 데이터베이스의 활성 복제본을 최대 하나로 유지하고 모든 HTTP 요청을 해당 단일 데이터베이스로 라우팅하는 방식으로 이 모델을 D1에 구현했습니다. D1 데이터베이스의 활성 복제본이 하나만 있도록 하는 것은 분산 시스템에서 복잡한 문제를 야기하지만, Cloudflare는 Durable Objects를 사용하여 D1을 구축함으로써 이 문제를 성공적으로 해결했습니다. Durable Objects는 전 세계적인 유일성을 보장하기 때문에 Cloudflare가 Durable Objects를 사용하게 되면, HTTP 요청을 D1 Durable Objects로 전송함으로써 쉽게 라우팅할 수 있습니다.

이 방법은 데이터베이스의 활성 복제본이 여러 개 있는 경우에는 효과가 없습니다. 이러한 경우, 수신되는 일반 HTTP 요청을 매번 동일한 복제본으로 일관되게 라우팅하는 100% 신뢰할 수 있는 방법이 없기 때문입니다. 이전 섹션의 예시에서 살펴본 것처럼, 관련 요청을 100% 동일한 복제본으로 라우팅하지 못하면 Cloudflare가 제공할 수 있는 최상의 일관성 모델은 읽기 커밋이 되는 결과를 초래합니다.

특정 복제본으로 일관되게 라우팅할 수 없는 경우, 또 다른 접근 방식은 요청을 임의의 복제본으로 라우팅하고 선택한 복제본이 프로그래머에게 ‘논리적인’ 일관성 모델에 따라 요청에 응답하도록 하는 것입니다. Cloudflare가 요청에 램포트 타임스탬프를 포함하면 어떤 복제본을 사용하든 ‘순차적 일관성’을 구현할 수 있습니다. 순차적 일관성 모델에는 ‘내가 쓴 것 읽기’, ‘읽기 작업 이후에 쓰기’와 같은 중요한 속성과 전체 쓰기 순서라는 속성이 있습니다. 전체 쓰기 순서는 모든 복제본이 동일한 순서로 트랜잭션이 커밋되는 것을 목격한다는 의미이며, 이는 Cloudflare가 트랜잭션 시스템에서 원하는 것과 정확히 일치합니다. 순차적 일관성에는 시스템의 개별 엔터티가 임의로 최신 상태가 아닐 수 있기에 주의해야 하지만, Cloudflare가 API를 설계할 때 복제본 지연을 고려할 수 있다는 점에서 이점이 되기도 합니다.

D1이 모든 데이터베이스 쿼리마다 앱에 램포트 타임스탬프를 제공하고 해당 앱이 마지막으로 확인한 램포트 타임스탬프를 D1에 알리면, Cloudflare는 각 복제본이 순차적 일관성 모델에 따라 쿼리의 작동 방식을 결정하도록 할 수 있다는 아이디어입니다.

복제본의 순차적 일관성을 달성하는 간단하면서도 효과적인 방법은 다음과 같습니다.

  • 데이터베이스에 대한 모든 단일 요청에 램포트 타임스탬프를 할당합니다. 이 경우 값이 감소하기보다는 항상 단조적으로 증가하는(monotonically) 커밋 토큰이 원활하게 작동합니다.

  • 전체 쓰기 작업 순서를 유지하려면 모든 쓰기 쿼리를 메인 데이터베이스로 전송합니다.

  • 모든 복제본에 읽기 쿼리를 전송하되, 복제본이 쿼리의 램포트 타임스탬프 이후에 발생한 기본 데이터베이스의 업데이트를 받을 때까지 쿼리 서비스를 지연해야 합니다.

이 구현의 장점은 특히 읽기 중심 워크로드를 동일한 복제본으로 일관되게 전송하는 일반적인 경우에 속도가 빠르며, 다른 복제본으로 라우팅하는 요청도 처리할 수 있다는 점입니다.

미리 보기: 세션을 통해 D1에 읽기 복제 지원하기

D1에 읽기 복제를 도입하기 위해 세션이라는 새로운 개념을 통해 D1 API를 확장할 예정입니다. 세션은 앱의 단일 논리적 세션에 속하는 모든 쿼리를 캡슐화하는 역할을 합니다. 예를 들어, 세션은 특정 웹 브라우저나 모바일 앱에서 발생하는 모든 요청을 나타낼 수 있습니다. 세션을 사용하는 경우 쿼리에서는 기본 데이터베이스나 가까운 복제본 중 요청에 가장 적합한 D1 데이터베이스 복제본이 사용됩니다. D1의 세션 구현은 세션 내의 모든 쿼리에 대해 순차적 일관성을 보장합니다.

세션 API는 D1의 일관성 모델을 변경하므로 개발자는 새로운 API를 사용하도록 옵트인해야 합니다. 기존 D1 API 메서드는 동일하게 유지되며 이전과 동일한 스냅샷 격리 일관성 모델을 계속 사용할 수 있습니다. 그러나 신규 세션 API를 사용하여 만든 쿼리만 복제본을 사용합니다.

다음은 D1 세션 API의 예시입니다.

D1은 세션 구현 시 커밋 토큰을 사용합니다.  커밋 토큰은 데이터베이스에 커밋된 특정 쿼리를 식별합니다.  세션 내에서 D1은 커밋 토큰을 사용하여 쿼리가 올바른 순서로 정렬되도록 보장합니다.  위 예시에서, Cloudflare가 대기 기간 동안 복제본을 전환하더라도 D1 세션은 신규 순서의 “INSERT” 이후에 “SELECT COUNT(*)” 쿼리가 실행되도록 합니다.

export default {
  async fetch(request: Request, env: Env) {
    // When we create a D1 Session, we can continue where we left off
    // from a previous Session if we have that Session's last commit
    // token.  This Worker will return the commit token back to the
    // browser, so that it can send it back on the next request to
    // continue the Session.
    //
    // If we don't have a commit token, make the first query in this
    // session an "unconditional" query that will use the state of the
    // database at whatever replica we land on.
    const token = request.headers.get('x-d1-token') ?? 'first-unconditional'
    const session = env.DB.withSession(token)


    // Use this Session for all our Workers' routes.
    const response = await handleRequest(request, session)


    if (response.status === 200) {
      // Set the token so we can continue the Session in another request.
      response.headers.set('x-d1-token', session.latestCommitToken)
    }
    return response
  }
}


async function handleRequest(request: Request, session: D1DatabaseSession) {
  const { pathname } = new URL(request.url)


  if (pathname === '/api/orders/list') {
    // This statement is a read query, so it will execute on any
    // replica that has a commit equal or later than `token` we used
    // to create the Session.
    const { results } = await session.prepare('SELECT * FROM Orders').all()


    return Response.json(results)
  } else if (pathname === '/api/orders/add') {
    const order = await request.json<Order>()


    // This statement is a write query, so D1 will send the query to
    // the primary, which always has the latest commit token.
    await session
      .prepare('INSERT INTO Orders VALUES (?, ?, ?)')
      .bind(order.orderName, order.customer, order.value)
      .run()


    // In order for the application to be correct, this SELECT
    // statement must see the results of the INSERT statement above.
    // The Session API keeps track of commit tokens for queries
    // within the session and will ensure that we won't execute this
    // query until whatever replica we're using has seen the results
    // of the INSERT.
    const { results } = await session
      .prepare('SELECT COUNT(*) FROM Orders')
      .all()


    return Response.json(results)
  }


  return new Response('Not found', { status: 404 })
}

Workers 가져오기 핸들러에서 세션을 시작하는 방법에는 몇 가지 옵션이 있습니다. db.withSession(<condition>) 을 사용하면 다음과 같은 인수를 사용할 수 있습니다.

condition 인수

condition argument

Behavior

<commit_token>

(1) starts Session as of given commit token

(2) subsequent queries have sequential consistency

first-unconditional

(1) if the first query is read, read whatever current replica has and use the commit token of that read as the basis for subsequent queries.  If the first query is a write, forward the query to the primary and use the commit token of the write as the basis for subsequent queries.

(2) subsequent queries have sequential consistency

first-primary

(1) runs first query, read or write, against the primary

(2) subsequent queries have sequential consistency

null or missing argument

treated like first-unconditional 

동작

<commit_token>

(1) 주어진 커밋 토큰으로 세션 시작

(2) 후속 쿼리가 순차적으로 일관성이 있는 경우

first-unconditional

(1) 첫 번째 쿼리가 읽기인 경우, 현재 복제본이 무엇이든 해당 쿼리를 읽고 해당 읽기의 커밋 토큰을 후속 쿼리의 기준으로 사용합니다.  첫 번째 쿼리가 쓰기인 경우, 쿼리를 기본으로 전달하고 쓰기의 커밋 토큰을 후속 쿼리의 기준으로 사용합니다.

(2) 후속 쿼리가 순차적으로 일관성이 있는 경우

first-primary

(1) 기본 쿼리(읽기 또는 쓰기)에 대해 첫 번째 쿼리 실행

(2) 후속 쿼리가 순차적으로 일관성이 있는 경우

npx wrangler d1 create northwind-traders

# omit --remote to run on a local database for development
npx wrangler d1 execute northwind-traders --remote --file=./schema.sql

npx wrangler d1 execute northwind-traders --remote --file=./data.sql

null 또는 누락된 인수

# database schema & data
npx wrangler d1 export northwind-traders --remote --output=./database.sql

# single table schema & data
npx wrangler d1 export northwind-traders --remote --table='Employee' --output=./table.sql

# database schema only
npx wrangler d1 export <database_name> --remote --output=./database-schema.sql --no-data=true

first-unconditional 처럼 취급

# To find top 10 queries by average execution time:
npx wrangler d1 insights <database_name> --sort-type=avg --sort-by=time --count=10

세션의 마지막 쿼리에서 커밋 토큰을 ‘왕복(round-tripping)’하고 이를 사용하여 신규 세션을 시작하면 한 세션이 여러 요청에 걸쳐 있도록 할 수 있습니다.  이렇게 하면 웹 앱이나 모바일 앱과 같은 개별 사용자 에이전트가 사용자에게 일관된 순서로 쿼리를 표시할 수 있습니다.

D1의 읽기 복제는 추가 사용량이나 스토리지에 따른 비용 없이 기본으로 제공되며 복제본 구성이 필요하지 않습니다. Cloudflare는 앱의 D1 트래픽을 모니터링하고 데이터베이스 복제본을 자동으로 생성하여 사용자 트래픽을 사용자와 가까운 여러 서버로 분산합니다. Cloudflare의 서버리스 모델에 따라, D1 개발자는 복제본 프로비저닝 및 관리에 대해 걱정할 필요가 없습니다. 대신 개발자는 복제 및 데이터 일관성 절충안을 위한 앱 설계에 집중해야 합니다.

Cloudflare는 글로벌 읽기 복제 및 앞서 언급한 제안을 실현하기 위해 적극적으로 노력하고 있습니다(Cloudflare Developer Discord의 #d1 채널에서 피드백을 공유해 주세요). 그동안 D1 GA에는 몇 가지 흥미로운 새 기능이 추가될 예정입니다.

D1 GA 확인하기

2023년 10월 D1의 오픈 베타 이후, Cloudflare는 핵심 서비스에 필요한 안정성, 확장성, 개발자 경험에 집중해 왔습니다. 당사는 개발자가 D1으로 앱을 더 빠르게 빌드하고 디버깅할 수 있도록 몇 가지 새로운 기능에 투자했습니다.

더 큰 규모의 데이터베이스로 더 크게 구축하기Cloudflare는 더 큰 데이터베이스가 필요하다는 개발자들의 의견에 귀를 기울였습니다. 그 결과, D1은 이제 최대 10GB 크기의 데이터베이스를 지원하며, Workers 유료 요금제에서는 사용자가 최대 50,000개의 데이터베이스를 보유할 수 있습니다. D1의 수평적 확장을 통해 앱은 각 비즈니스 엔터티의 데이터베이스 사용 사례를 모델링할 수 있습니다. 특히, 새로운 D1 데이터베이스는 베타 출시 이후 특정 기간 내에 D1 알파 데이터베이스에 비해 40배 더 많은 요청을 처리하는 것으로 나타났습니다.

가져오기 및 대용량 데이터 내보내기개발자는 다음과 같은 여러 가지 이유로 데이터를 가져오거나 내보냅니다.

  • 서로 다른 데이터베이스 시스템 간 데이터베이스 마이그레이션 테스트

  • 로컬 개발 또는 테스트를 위한 데이터 복사

  • 규정 준수와 같은 사용자 지정 요구 사항을 위한 수동 백업

이전에는 D1에 대해 SQL 파일을 실행할 수 있었습니다. 그러나 Cloudflare는 wrangler d1 execute –file=<filename> 을 개선하여 데이터베이스가 불완전한 상태로 남지 않도록 보장하고 있습니다. 또한 원격 프로덕션 데이터베이스를 보호하기 위해 이제 wrangler d1 execute 가 로컬 우선으로 기본 설정됩니다.

Cloudflare의 Northwind Traders 데모 데이터베이스를 가져오려면 스키마데이터를 다운로드하고 SQL 파일을 실행하면 됩니다.

다음 방법을 통해 D1 데이터베이스 데이터 및 스키마, 스키마 전용 또는 데이터 전용을 SQL 파일로 내보낼 수 있습니다.

쿼리 성능 디버깅SQL 쿼리 성능을 이해하고 느린 쿼리를 디버깅하는 것은 프로덕션 워크로드에서 매우 중요한 단계입니다. Cloudflare는 개발자가 GraphQL API를 통해서도 쿼리 성능 메트릭을 분석할 수 있도록 실험적인 `wrangler d1 insights`를 추가했습니다.

개발자 도구D1은 다양한 커뮤니티 개발자 프로젝트의 지원을 받고 있습니다. 이제 버전 5.12.0에서는 Prisma ORM을 비롯한 새로운 기능이 추가되어 Workers와 D1을 지원합니다.

다음 단계

현재 글로벌 읽기 복제 설계와 함께 정식 출시(GA)를 통해 제공되는 기능은 개발자 앱의 SQL 데이터베이스 요구 사항을 충족하기 위한 시작에 불과합니다. 아직 D1을 사용해 보지 않으셨다면 지금 바로 시작하시거나, D1의 개발자 문서를 방문하여 아이디어를 얻거나, Cloudflare Developer Discord의 #d1 채널에 참여하여 다른 D1 개발자 및 당사의 제품 엔지니어링 팀과 이야기를 나눌 수 있습니다.

Cloudflare에서는 전체 기업 네트워크를 보호하고, 고객이 인터넷 규모의 애플리케이션을 효과적으로 구축하도록 지원하며, 웹 사이트와 인터넷 애플리케이션을 가속화하고, DDoS 공격을 막으며, 해커를 막고, Zero Trust로 향하는 고객의 여정을 지원합니다.

어떤 장치로든 1.1.1.1에 방문해 인터넷을 더 빠르고 안전하게 만들어 주는 Cloudflare의 무료 앱을 사용해 보세요.

더 나은 인터넷을 만들기 위한 Cloudflare의 사명을 자세히 알아보려면 여기에서 시작하세요. 새로운 커리어 경로를 찾고 있다면 채용 공고를 확인해 보세요.
Developer Week (KO)개발자Developer PlatformD1 (KO)Database (KO)

X에서 팔로우하기

Vy Ton|@vaiton13
Cloudflare|@cloudflare

관련 게시물

2024년 10월 31일 오후 1:00

Moving Baselime from AWS to Cloudflare: simpler architecture, improved performance, over 80% lower cloud costs

Post-acquisition, we migrated Baselime from AWS to the Cloudflare Developer Platform and in the process, we improved query times, simplified data ingestion, and now handle far more events, all while cutting costs. Here’s how we built a modern, high-performing observability platform on Cloudflare’s network....

2024년 10월 25일 오후 1:00

Elephants in tunnels: how Hyperdrive connects to databases inside your VPC networks

Hyperdrive (Cloudflare’s globally distributed SQL connection pooler and cache) recently added support for directing database traffic from Workers across Cloudflare Tunnels. We dive deep on what it took to add this feature....