본문으로 바로가기 본문 바로가기
WIKI 기술 지식 베이스

셀 기반 아키텍처 (Cell-based architecture)

원문 보기 위키 갱신

셀 기반 아키텍처는 이전에 shared-nothing-architecture(공유 없음 아키텍처)로 알려졌던 개념을 Amazon이 재발견하고 셀 기반이라는 이름으로 다시 브랜딩한 것이에요. 이 블루프린트는 사용자 한 명마다 새로운 Machine을 하나씩 배정하는 시나리오를 어떻게 구현하는지 보여줘요. 이 Machine들은 기본적으로 사용하지 않을 때 자동으로 멈추고, 접근하면 다시 시작되므로, 주요 비용은 볼륨과 실제 사용량에서 발생해요.

경고: 이 예시를 따라가면 데이터베이스가 복제되지 않은 채 앱이 배포돼요. 이는 앱을 볼륨과 Machine 장애에 노출시킵니다. 백업은 당신의 책임이 돼요. 이 요구사항을 해결할 몇 가지 접근 방식이 이 페이지의 하단에 나열되어 있어요.

여기서 드는 예시는 Rails 기반이지만, 이 접근 방식은 모든 프레임워크에 적용할 수 있어요.

출처: 문서

본문

단일 Machine 앱으로 시작하기

아직 없다면 다음을 사용하면 돼요:

rails new blog --css tailwind
cd blog
bin/rails generate scaffold Post title:string body:text
fly launch --name blog-$USER-$RANDOM

앱 이름은 원하는 대로 바꿔도 괜찮아요.

라우트 구성하기

config/routes.rb를 수정하세요. 라우트를 다음과 같이 감싸세요:

scope ENV.fetch("FLY_MACHINE_ID", "") do
...
end

get "up" 라인이 있다면 이 블록 밖으로 옮기세요.

root 라인의 주석을 해제하세요.

최종 결과는 대략 다음과 같을 거예요:

Rails.application.routes.draw do
  scope ENV.fetch("FLY_MACHINE_ID", "") do
    resources :posts
    # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

    # Defines the root path route ("/")
    root "posts#index"
  end

  # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
  # Can be used by load balancers and uptime monitors to verify that the app is live.
  get "up" => "rails/health#show", as: :rails_health_check
end

이렇게 하면 앱 전체가(헬스 체크 엔드포인트 제외) fly.io에 배포될 때 하나의 네임스페이스 안에 자리 잡게 돼요. 앱에 접근하려면 Machine id를 접두사로 붙여야 해요. FLY_MACHINE_ID 환경 변수가 설정되지 않은 환경에서 실행될 때는 접두사가 필요 없어요.

Machine id 목록은 다음 명령으로 얻을 수 있어요:

fly machines list -q

두 번째 Machine 시작하기

여러 Machine 배포를 준비하려면 각 요청이 올바른 Machine으로 라우팅되도록 해야 해요. fly-replay 응답 헤더를 사용하는 미들웨어로 이를 구현할 수 있어요.

다음을 config/initializers/fly_router.rb에 넣으세요:

if ENV["FLY_MACHINE_ID"]
  class FlyInstanceRouter
    def initialize(app)
      @app = app
    end

    def call(env)
      segments = env["PATH_INFO"].split('/')
      instance = segments[1]

      if instance =~ /^[0-9a-f]{14}$/ and instance != ENV["FLY_MACHINE_ID"]
        return [409, {"Fly-Replay" => "instance=#{instance}"}, [""]]
      end

      @app.call(env)
    end
  end

  Rails.application.config.middleware.use(FlyInstanceRouter)
end

이 변경을 fly deploy로 배포한 뒤, 다음 명령으로 두 번째 Machine을 만드세요:

fly machine clone

이 시점에 blog 앱의 인스턴스 두 개가 생기고, 각각은 자신만의 Machine, 볼륨, sqlite3 데이터베이스를 갖게 돼요.

fly machine clone에는 region, VM 크기, 볼륨의 고유 영역(unique zone) 필요 여부 같은 것을 지정할 수 있는 많은 옵션이 있다는 점을 참고하세요.

특히 region을 활용하면 앱과 데이터를 모두 사용자와 가까운 곳에 배치할 수 있어요.

라우트 최적화하기

이 섹션은 선택 사항이며, Rails의 Hotwire 구현의 독특한 기능에 의존해요.

이 시점에서 많은 요청이 두 번의 이동을 하게 돼요. 먼저 가까운 Machine으로, 그다음 원하는 Machine으로 이동하는 거죠. 이는 응답의 지연 시간을 늘리며, 특히 첫 요청을 처리하기 위해 Machine 두 개를 깨워야 할 때 그렇습니다.

첫 요청에서 이를 피할 방법은 별로 없지만, 이후 요청은 fly-force-instance-id 요청 헤더를 사용해 올바른 Machine으로 라우팅할 수 있어요.

다음을 app/javascript/controllers/fly_router_controller.js에 넣으세요:

import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="fly-router"
export default class extends Controller {
  connect() {
    console.log('fly router connected')
    this.instance = this.element.dataset.instance;

    document.documentElement.addEventListener(
     'turbo:before-fetch-request',
     this.beforeFetchRequest
    )
  }

  disconnect() {
    document.documentElement.removeEventListener(
     'turbo:before-fetch-request',
     this.beforeFetchRequest
    )
  }

  beforeFetchRequest = event => {
    console.log('injecting ' + this.instance);
    event.detail.fetchOptions.headers['fly-force-instance-id'] = this.instance;
  }
}

마지막으로 app/views/layouts/application.html.erb의 <body> 라인을 수정하세요:

  <body<% if ENV['FLY_MACHINE_ID'] %> data-instance="<%= ENV['FLY_MACHINE_ID'] %>" data-controller="fly-router"<% end %>>

이 변경을 fly deploy로 배포하세요.

다음 단계

위 내용은 단지 개념 증명에 불과해요. 프로덕션 앱은 다음 중 하나 이상을 사용할 가능성이 높아요:

  • Machine id를 URL 경로에 넣는 것도 효과적이지만, 그다지 사용하기 편하지는 않아요. 더 나은 해결책은 경로와 연결된 Machine의 레지스트리를 갖는 거예요.
  • 미들웨어에서 라우팅을 구현해도 일은 되지만, nginx 같은 앱으로 별도 프로세스에 분리하면 여러 장점이 있어요:
    • fly-replay는 1MB 이하의 요청 페이로드로 제한되어 있어, 파일 업로드 같은 기능에 영향을 주죠. 리버스 프록시가 더 나은 해결책일 수 있어요.
    • 경로 네임스페이스와 스코프 대신 사용자 정의 하위 도메인을 지원해요.
    • 인증과 정적 자산 제공을 앱 외부에서 수행할 수 있어요.
    • Phusion Passenger 같은 애드온으로 하나의 Machine에서 앱 인스턴스를 여러 개 실행할 수 있어요.
  • 수백 개, 아니면 수십 개의 독립 Machine이 생기면 모니터링이 더욱 중요해져요. Fly.io가 로그를 합쳐 주긴 하지만, 이는 많은 문제를 해결해 주지만 앱이나 네트워크 문제로 인해 누락된 로그를 처리하는 데는 도움이 되지 않아요. 앱이 하트비트를 보내고, 누락된 하트비트를 찾아 문제로 보고하는 모니터링 소프트웨어를 작성하는 것을 고려해 보세요.
  • 단일 Machine의 모든 서비스가 자체 포함되어 있을 때 업데이트를 적용하는 것은, 프로덕션에서 실행 중인 상호 의존적인 서비스를 업그레이드하는 것보다 쉬운 문제예요. 다만 그 대가가 있죠: 블루/그린 배포가 불가능하므로 시작 시간을 줄이는 데 집중해야 해요.
  • 앱을 위한 관리용 웹 인터페이스 구축을 고려해 보세요. 사용자 등록과 Machine 배정은 여러분의 워크플로에서 밀접하게 연관되어 있을 가능성이 높아요. Fly.io가 Machines API를 제공하지만, flyctl 명령을 사용한 옛날 방식의 스크립팅으로도 멀리 갈 수 있어요.

이 아이디어 중 많은 것을 구현해 프로덕션에서 실행 중인 앱의 예시로 Showcase Architecture를 참조하세요.

백업

앞서 언급했듯이 백업은 매우 중요해요. 살펴볼 항목들:

  • LiteFS - 분산 SQLite. 그 자체로 장애 조치 상황을 자연스럽게 지원해요. 추가 재해 복구 옵션도 있어요.
  • Sqlite3 데이터베이스는 그저 파일일 뿐이에요. 백업을 호스팅하는 별도 앱을 만들고 앱이 주기적으로 그쪽에 복사본을 POST하도록 하세요.
  • Rsync는 Linux 배포판에서 사용할 수 있는 유틸리티로, Machine 간 변경 사항을 효율적으로 복사하는 데 쓸 수 있어요.
  • 셀당 여러 Machine을 실행해 전통적인 고가용성 구성의 모든 이점을 누리세요. 관리용 웹 UI를 구현했다면 새 클러스터 배포를 쉽게 자동화할 수 있어요.

더 알아보기 (Learn more)