defrag.works

海外の技術ブログで気になったものを日本語訳してメモしています

Rails 8の計画

fly.io

RailsGithub8.0のマイルストーンを公開し、次のRailsのリリースに向けた目標を示しています。新しいバックグラウンドワーカー、キャッシュバックエンド、アセットパイプライン、モバイルデバイスに通知をプッシュするフレームワーク、そして大量の開発ツールが同梱されています。

Railsの信念のひとつは「モダンなWebアプリケーションの複雑さを圧縮する」であり、これは技術的な言い方をすれば「Webアプリケーションをもっと簡単に構築できるようにする」ということです。Rails 8はこの伝統を受け継ぎ、Railsを本番環境で実行するのに必要なインフラや「可動部分」を減らしています。また、Rails開発者がネイティブ・モバイル・アプリケーションに匹敵する機能を備えたプログレッシブWebアプリケーションを簡単に公開できるようにすることで、アプリストアの産業的な複雑さに対抗しています。

プロダクションでの可動部分が少ない

Redisは高速で信頼性の高いソフトウェアであり、長年Railsスタックの定番となっています。その安定性にもかかわらず、Redisは監視が必要で障害が発生する可能性のある依存関係であることに変わりはありません。キーエビクションという言葉を聞いたことがありますか?デフォルトのRails 8アプリを公開すれば、その意味を知る必要はありません。

Rails 8ではNVMeソリッドステートドライブのおかげではるかに高速になっていることを前提に、Redisからデータベースに多くのワークロードをシフトすることで、Redisへの依存を減らしています。

Solid CacheとSolid Queue

Solid CacheSolid QueueRails 8に同梱される2つの新しいソフトウェアライブラリです。どちらもデータベース上に構築されており、キャッシュやバックグラウンドジョブにRedisを使用するライブラリを置き換えるように設計されています。

これらはRedisでバックアップされたワーカーキューやキャッシュと同じくらい速いのでしょうか?いいえ、一般的にはRedisがメモリにデータを保存するのに対し、データベースはディスクにデータをコミットする必要があるので遅くなりますが、NVMeソリッドステートドライブのおかげでデータベースのディスクははるかに高速になったので、ほとんどのRailsアプリのキャッシュとワーカーキューのワークロードを処理するには「十分速い」です。

HTTPリクエストのレート制限など、まだRedisの必要性がある場合もありますが、ほとんどの場合では本番のRailsデプロイメントではRedisをまったく使わなくても大丈夫です。

データベースをバックアップしたActionCable WebSocketブロードキャスト

PostgresMySQLにはWebSocket経由でWebブラウザにメッセージをブロードキャストする機能が搭載されています。Rails 8にはデータベースを使用して接続されているすべてのクライアントにメッセージをブロードキャストする新しいActionCableアダプタが同梱され、Redisへの依存がなくなります。

データベースはpub/subを主なユースケースとして設計されていないため、制限がある場合があります。たとえば、Postgresのデフォルトの最大ペイロードサイズは8000バイトであり、これは大きなHTMLペイロードをワイヤ経由でプッシュしようとするアプリケーションにとって問題になる可能性があります。

幸運にもRails 8にはTurbo 8ページモーフも同梱されており、モデルが変更されたという事実を公開するだけなので、デフォルトのペイロードサイズ制限に達する可能性は低く、サーバはHTTP経由でHTMLを再読み込みする必要があります。

Sprocketsに代わるPropshaft

WebアプリケーションはCSSJavaScript、画像アセットを迅速かつ効率的に提供する必要があります。これを行う最も効果的な方法の1つはアセットを"フィンガープリント"し、有効期限のないキャッシュヘッダでそのフィンガープリントを提供することです。

これはまさにPropshaftが行うことです。/images/foo.pngの画像をダイジェストし、/images/foo-586e0b396456675e08bba21db5941a3099b07766084e51400576f7622c5217cc.pngのようなURLを生成します。

Sprocketsは2009年にRailsに搭載されたオリジナルのRailsアセットパイプラインで、webpackのようなJSベースのアセットパイプラインよりも前のものです。そのためSprocketsはSCSSをCSSコンパイルしたりJavaScriptを最小化したりといった今日のesbuildのようなアセットパイプラインが行う重い作業をすべて行わなければなりませんでした。

今は2024年でほとんどのブラウザがアセットパイプラインなしでJavaScriptスタイルシートのアセットを効率的にダウンロードできる、あるいはesbuildのようなより高性能なパイプラインが使える世界です。Railsに必要なのは画像や動画、その他のメディアファイルのフィンガープリントURLを生成することだけです。Propshaftが導入されたのはそのためです。CSSJavaScriptコンパイル作業から手を引き、最新のブラウザかesbuildのようなより高性能なアセットコンパイラに任せるのです。

ネイティブアプリの代わりにプログレッシブウェブアプリを公開

macOS Sonoma、iOS 17、iPadOS 17はネイティブアプリを必要とせず、ウェブアプリケーションオペレーティングシステムとよりよく統合することを可能にする「プログレッシブ・ウェブ・アプリケーション」機能を搭載して公開されました。

プッシュ通知

ネイティブアプリケーションを公開する大きな理由の1つは通知でした。たとえば、Railsのチャットアプリでは@userで言及されたときにユーザーに通知してメッセージに返信できるようにする必要があるかもしれません。

Safariの最新バージョンではついにWeb通知機能が導入され、Railsフレームワークに組み込むことが可能になりました。つまり、ネイティブのiOSアプリケーションをインストールすることなく、モバイルのウェブ通知が可能になりました。

ウェブ通知が既存のgemを使うのか、Railsチームが独自に開発するのかはまだ明らかになっていません。

ホーム画面またはドックへのウェブページの追加

iPhoneiPadのホーム画面にウェブページを追加する方法が改善され、ドックにウェブページを追加してアプリとして表示できるようになりました。

これはWindowsAndroidではしばらくの間行われていたことですが、Appleがついにきちんとしたサポートを提供し、アプリを配布するための現実的なチャネルとなったのは喜ばしいことです。

豊富な開発ツール

Rails 8にはRailsアプリケーションの開発とデプロイを容易にするデフォルトツールが多数追加されています。

Kamalを使ったデプロイ

Kamalrails newを実行するときのデフォルトとして採用されています。Kamalはデプロイツールで、ロードバランサやHTTPS証明書、Dockerなどの管理に慣れていれば、Railsアプリケーションをメタルサーバにデプロイできるようになります。

Fly.ioへのデプロイにKamalは必要ありません。Fly.ioはHTTP証明書を扱うために必要なロードバランサとツールを提供しているからです。

HTTPベーシック認証ジェネレータ

Rails 8にはRailsアプリケーションにHTTPベーシック認証を追加するコードジェネレータが同梱されます。これまではサードパーティのgemや数行のRubyコードで可能だったので、フレームワーク初心者で実装の指針がもっと必要な人にとっては嬉しい追加となるでしょう。

ベンチマークツール

共通のベンチマークツールはホスト間の限定的な比較(apples-to-apples)を可能にします。これらのベンチマークの目的は特定のアプリケーションが本番環境でどのように動作するかを理解することではありません。むしろディスク速度、ネットワーク、その他のサーバ性能の側面をテストするスイートであり、開発者がRailsアプリをどこにデプロイするかについてより多くの情報に基づいた決定を下すのに役立ちます。

Rails 8の言語サーバー

言語サーバーはZed EditorやVS CodeなどのIDEに対して、コーディング中にオートコンプリートのポップオーバーに何を表示するかを指示します。Rails 8ではActiveRecordバリデーションなどのRailsコンストラクトのオートコンプリートを提供する言語サーバーが公開される予定です。

Rubocop

リンターが好きな人のために、Rails 8にはRuboCop Omakaseが同梱される予定です。あなたやあなたのチームが好きなRubyのフレーバーにカスタマイズできます。

Githubとの統合

Rails 8のプロジェクトには.githubフォルダーが用意され、CIやプルリクエストテンプレートなどGithubでの一般的なワークフローを実行するためのすべての設定が行われるようになりました。

Devcontainers

Devcontainersは、Dockerファイルと設定ファイルを介してローカルワークステーションに開発環境をセットアップするスクリプトのセットです。

macOSの場合、ファイル数の多い大規模なRailsプロジェクトではDocker Desktopのハイパーバイザーのパフォーマンスによって開発環境が桁違いに遅くなるという問題が発生する可能性があるので注意してください。

HTTP/2サポートの向上

Railsはいくつかのアップグレードにより、より良いHTTP/2サポートへのゆっくりとした歩みを続けています。

Rack 3

RackはRailsのようなRubyウェブフレームワークとPumaやFalconのようなウェブアプリケーションサーバーの間に位置する低レベルのHTTP APIです。Rack 3はHTTP/2用のより良いAPIを搭載しており、以前のバージョンのRackでHTTP/2を動作させるために必要だったハックのいくつかを一掃しています。

ThrusterでのHTTP/2

ThrusterはRailsアプリ用のDockerfileで、HTTP/2をサポートしていないウェブホスト用のHTTP/2プロキシが含まれています。Fly.ioにアプリをデプロイする場合はFlyのアプリケーションプロキシが追加の設定なしでHTTP/2をサポートしているので、これは必要ありません。

ActiveModel のドキュメント

ActiveModelはActiveRecordから抽出されたAPIで、RubyのオブジェクトをActiveRecord のモデルのように動作させることができます。このAPIは以前から存在していましたが、Rails 8でようやくそれにふさわしいドキュメントが提供されるようになりました。

まとめ

Rails 8はRailsアプリに含まれるデフォルトのツールを大幅に拡張し、Redisのようなプロダクションサービスの依存関係を不要にする新しいフレームワークを導入する大きなリリースになりそうです。これまで通り、新しいRailsアプリを作成するときや後でgemsを取り出すとき、気に入らない部分を削除することはできます。

全体として、Rails 8で提供される機能のほとんどはアプリの初期段階で本番インフラについて考える必要性を遅らせるでしょう。そのため、Web開発に慣れていない人にとって決断すべきことが少なくなり、Railsがより親しみやすくなります。

Turbo 8を8分で

fly.io

Turbo 8はライブ更新するRailsアプリケーションの開発を簡素化します。Turbo FramesとTurbo Streamsのレスポンスを手作業でコーディングする必要性を最小限に抑えることで、以前のバージョンから飛躍的に進歩しています。この進歩によってRailsアプリケーションの開発と保守の両方が簡素化され、生産性がさらに向上します。

Turboをご存じない方もいらっしゃるかもしれませんが、これはRuby on Railsアプリケーションで広く使用されているライブラリで、ページを部分的に更新してシングルページのJavaScriptアプリケーションと同じように応答性を感じさせるものです。HTMXStimulusReflexPhoenix LiveViewLaravel LiveWireなどのフレームワークに似ています。

Turbo 8は本当にスマートなページリローダーだと考えてください

これは単純化しすぎですが、この例えはTurbo 8の仕組みをよりよく理解するのに役立ちます。考え方はこうです:

  1. Railsはデータが変更されるとパブリッシュします。 broadcasts_refreshesを持つRailsモデルはActionCableを介してモデルが作成、更新、破棄されるとパブリッシュします。
  2. ページが気になるデータ変更を購読します。 ページが読み込まれると、Turbo JavaScriptがスキャンして<turbo-cable-stream-source/>タグを探します。各タグにはActionCable経由のデータ変更通知を購読するために使用されるモデルクラスとIDが記述されています。
  3. モデルが更新されるとサブスクライブされたページは何かが変更されたという通知を受け取ります。 TurboはバックグラウンドでHTMLページ全体をHTTP経由でリクエストし、新しいHTMLと現在ロードされている古いHTMLを比較します。HTMLファイル間に差異がある場合、ページ全体をリロードすることなく、差異のみをページに適用します。

それだけです。これがフレームワークです。印象的なのは、その印象の薄さです。私がこれまで見てきたチュートリアルのほとんどはTurbo 7とTurbo 8の比較にとらわれていますが、それは理解を難しくしていると思うのでTurboの古いバージョンについて知っていることはすべて忘れて、ベータ版を試すことで今後どのように動作するかを見てみましょう。

Turbo 8ベータ版のインストール

以下をGemfileに追加するか既存のturbo-railsエントリーを更新して、Turbo 8 gemをインストールしてください。

# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
gem "turbo-rails", "~> 2.0.0.pre.beta"

Railsサーバーを再起動して準備を整えましょう!

ページの先頭にTurboタグを追加する

最初に行う必要があるのはアプリケーションレイアウトの<head/>タグにタグを追加することです。

<%= turbo_refreshes_with method: :morph, scroll: :preserve  %>
<%= content_for :head %>

これによりTurbo 8の動作がページを「モーフィング」してスクロール位置が保持されるように設定されます。Turboの「古い」動作はページ全体を「置き換え」スクロール位置を「リセット」します。

コンテンツページを読み込むには依然としてTurboの「従来の」動作が必要です。コンテンツページのようにすべてにmethod: :morph, scroll: preserveを適用すると、ユーザーがコンテンツをクリックしたときにページがトップではなく真ん中から始まるという奇妙な動作が発生します。

ページがモデルに対してサブスクライブすることで変更を常に通知し、それに応じてリロードします。

ビューを更新したいとき、アプリケーションのビューファイルからサブスクライブします。例えば作者が変更を公開したときに更新したいブログ記事がある場合、./app/views/post/show.html.erbの先頭に次のように追加します。

<%= turbo_stream_from @post %>
<h1><%= @post.title %></h1>

このヘルパーは、HTMLにこのようなタグを出力します:

<turbo-cable-stream-source channel="Turbo::StreamsChannel" signed-stream-name="IloybGtPaTh2YzJWeWRtVnlMMVZ6WlhJdk1RIg==--b4bcfff51ae4074540fdefbada55a237d68206bf960bd30a6684b310a255656c" connected=""></turbo-cable-stream-source>

不可解に見えるかもしれませんがsigned-stream-name属性はモデルクラス内にあり、Turboが変更を購読するIDです。投稿が更新されるとTurboはサーバーから「このブログの投稿が変更されました」というシグナルを受け取ります。そしてJavaScript経由で現在のHTMLページにHTTPリクエストを行い、新しいHTMLをすでにロードされているDOMに差分し、ページをリロードする必要がないように2つの変更をマージします。

素晴らしい!これで変更をサーバーにサブスクライブするクライアントができました。Postモデルに変更を公開するようRailsに指示する必要があるので、Postモデルに以下を追加します。

class Post < ApplicationRecord
  # When the model instance is changed, a message will sent over
  # ActionCable that notifies the page to reload.
  broadcasts_refreshes
end

これでPostモデルを作成、更新、破棄するとRailsはそれをActionCableでパブリッシュし、変更があった場合はすべての関連ページにリロードするよう通知します。

コレクションを更新する方法

通常、コレクションはアプリケーション内のいずれかに属しています。例えばブログにはたくさんの投稿があります。ブログの全投稿を一覧表示するビューが./app/views/blog/posts/index.html.erbのどこかにあるでしょう。

<%= turbo_stream_from @blog %>
<%= render @blog.posts %>

そしてBlogに属するという関連付けをPostモデルに追加します。重要なのはtouch: trueを追加することです。

class Post
  # Touch will update the timestamp on the blog when
  # a post is created, updated, or destroyed.
  belongs_to :blog, touch: true

  # When the model is changed, a message will sent over ActionCable.
  broadcasts_refreshes
end

それからBlogモデルは更新をブロードキャストする必要があります:

class Blog
  has_many :posts
  broadcasts_refreshes
end

投稿が作成、更新、削除されるとBlogモデルはタイムスタンプを更新して、Blogインスタンスへの変更をリッスンしているページの更新をトリガーします。

何にも属さないコレクションについてはどうでしょうか?

実際にはこれはアプリケーションではまれなことです。例えばブログはおそらくユーザーやアカウントに属しており、上記と同様に "タッチ "することができます。以下はその例です。

class Blog < ApplicationRecord
  # Code from above removed for clarity
  belongs_to :user, touch: true
end

そしてUserモデルにbroadcast_refreshesを追加します。

class User < ApplicationRecord
  has_many :blogs
  # Blog will touch the account when something is changed.
  broadcasts_refreshes
end

そして全ユーザーのブログが一覧表示されるダッシュボード・ページで、アカウントの変更を確認します:

<%= turbo_stream_from current_user %>
<%= render @current_user.blogs %>

アプリケーションに親オブジェクトでモデル化できないコレクションがある場合でも、Turbo Streamsを使用してリストに追加することができます。

PostgresまたはSQLiteを使用して変更をブロードキャストする

Turbo 8ではHTMLペイロードをWebSocket経由でプッシュする必要がないため、Postgres ActionCableアダプタの8000バイト制限はもはや問題ではありません。小規模から中規模のアプリケーションでRedisまたはActionCable pub-subのみを使用している場合、依存関係としてRedisを排除することでアプリケーションのインフラストラクチャを簡素化できます。

SQLiteアプリケーションを本番環境にデプロイする場合、RailsアプリケーションにLitestackをインストールし、Litecableを使ってTurbo 8上で変更通知を発行することができます。

Rails開発の未来にもう一度ワクワクしよう

Railsアプリケーションを自動更新するために必要な労力は驚くほどわずかです。Turbo 8はまだベータ版であり、Railsアプリケーションを構築するためのこのアプローチには考慮すべきエッジケースがたくさんありますが、この技術はRailsアプリケーション開発をさらに簡素化する有望な方法になりつつあります。

Turbo 8より前のバージョンでTurbo Framesに大きく投資している場合、おそらく移行で一番大変なのはコントローラコード内のformat.turbo_streamブロックとビューのturbo_frameタグをすべて削除することでしょう。何千行ものコードを削除してgitコミットすることが生産的だと考えているのであれば、非常に生産的な開発の日々が待っていることを覚悟してください。

UUIDとは何か、何に使うのか?

www.cockroachlabs.com

データベースを扱う場合、テーブルの各行に対して一意の識別子を与えるために、ある種のidフィールドを使用するのが一般的な習慣です。

例えば、customersテーブルを想像してください。複数の顧客が同じ名前を持っていたり、同じ住所を共有していたりする可能性があるため、nameaddressといったフィールドを一意の識別子として使用することはありません。

その代わりに、各行には何らかのユニークな識別子を割り当てるのがよいでしょう。その1つがUUIDの使用です。

UUIDとは何でしょうか?

UUIDとは、Universally Unique IDentifierの略で、36文字の英数字で情報(テーブルの行など)を識別するために使用される文字列です。

以下はUUIDの一例です: acde070d-8c4c-4f0d-9d8a-162843c10333

UUIDが広く使われているのは世界的に一意である可能性が高いからです。つまり、私たちの行のUUIDは私たちのデータベーステーブルで一意であるだけでなく、おそらくどこのシステムでもそのUUIDを持つ唯一の行であるということです。

技術的には、私たちが生成したUUIDが他の場所で使用される可能性もないわけではありませんが、340,282,366,920,938,463,463,374,607,431,768,211,456通りのUUIDの可能性があるので、その限りではないのです...。

UUIDは何に使うのか?

この質問に答えるために私たちがeコマースの書店を運営していると仮定しましょう。注文が入るとその注文にID番号を付けて、その番号を使ってordersテーブルに格納したいと思います。

最初に来た注文は12、...というように、連続したIDを設定することができます:

id item buyer price
1 The Years of Rice and Salt Sue $14
2 A Darkling Sea Al $20
3 Too Like the Lightning Mei $25

そしてこの方法は私たちの規模が小さければ、少なくともしばらくの間はうまくいくかもしれません。しかし、この方法にはいくつかの大きな欠点があります:

第一に、テーブルの結合や新しいデータのインポートを行う際に、上記のid値がユニークでないため、混乱が生じやすくなります。これは複数のテーブルで同じIDシステムを使用している場合、内部でも問題が生じますが、外部のあらゆるデータを扱うようになると本当に厄介なことになります。

例えば、私たちの小さな本屋が大きくなり、別のオンライン書店を買収したとします。ordersテーブルを統合しようとしたところ、同じシステムを使用していることがわかりました。この問題を解決するには、統合する2つのデータベースのうち少なくとも1つのデータベースのIDをすべて更新する必要があります。最良のシナリオであっても、これは非常に面倒なことです。

第二に、シーケンシャルなアプローチはINSERTコマンドを1つずつ実行しなければならないため、あらゆる種類の分散システムでうまく機能しないことです。この制限により規模が大きくなるとパフォーマンスに大きな問題が生じる可能性があります。アプリケーションで厳密なID順序が必要な場合でも、CockroachDBのChange Data Captureのような機能を使用すれば、UUIDを使用しながらも要件を満たすことができ、順次順序付けられたIDに起因するパフォーマンスの打撃を受けずに済むかもしれません。

また、SERIALでランダムなIDを生成するような従来のユニークIDのアプローチも、分散システムでは同じ時期に生成された値は似たような値を持つため、テーブルのストレージで互いに近い場所に配置されるため、ホットスポットにつながることがあります。

www.youtube.com

UUIDはこれらの問題をすべて解決します:

  • グローバルに一意であるため外部データであっても重複するIDに遭遇する可能性が非常に低いです。
  • UUIDは中央のノードと照合することなく生成できるため、分散システムでは各ノードが自律的にUUIDを生成でき、重複や整合性の問題を心配する必要がありません。

理由1だけでもほとんどすべてのデータベースシステムでUUIDを使用するための十分な論拠となります。また、理由2は分散型データベースはスケーラビリティと耐障害性に優れているため、大規模な運用を目指すビジネスとして、私たちブックショップに非常に適しています。

UUIDのデメリット

UUIDの唯一の重大な欠点はメモリに128ビット(メタデータを含めるともう少し多い)を占有することです。ストレージスペースを最小限に抑えることが絶対に必要な場合、36文字の英数字を保存するよりも、シーケンシャルID(おそらく1~10文字の間のどこかであろう)を保存する方が効率的であることは明らかです。

しかし、ほとんどの場合でシーケンシャルIDのようなものを使用することのデメリットは、UUIDを使用することで生じるストレージコストの最小限の増加を大幅に上回ります。UUIDは非常に人気があり、さまざまな異なる識別目的で広く使用されています。私たちはかなり素晴らしいデータベースを作っているので、この記事ではデータベースの例に焦点を当てましたが、UUIDは分析システム、ウェブやモバイルアプリケーションなどでも使用されています。

UUIDの例

UUIDにはいくつかの種類があります:

バージョン1とバージョン2です。 タイムベースUUIDと呼ばれることもあるこれらのIDはUUIDが生成される時間を反映したdatetime値、ランダム値、UUIDを生成するデバイスMACアドレスの一部の組み合わせを使って生成されます。

その内訳を視覚的に説明すると次のようになります:

このようにUUIDを生成することで、同一のUUIDを持つことはほとんど不可能になります。同じデバイスによって全く同じ時間に生成され、全く同じランダムな16ビットシーケンスを生成していなければならないのです。

UUID v1およびUUID v2のIDは、生成デバイスMACアドレスの一部を含んでいるため、(例えば)どのデータベースノードがそのIDを生成したかを識別するために使用することができます。これは一般的には問題ではなく、分散システムでは利点となることもあります。

UUID v1とv2の違いはUUID v2にはローカルドメイン番号のセグメントも含まれていることです。多くの理由から、これはほとんどのアプリケーションに最適ではないので、UUID v2は広く使用されていません。

バージョン3およびバージョン5。 この2つのバージョンのUUIDは、名前空間識別子と名前をハッシュ化することで生成されます。完全にランダムではなく既存のデータを使って生成されるという点ではタイムベースのUUIDと似ていますが、日付データとデバイスMACアドレスではなく、名前空間データと名前データを使用します。

名前空間データはそれ自体がUUIDであり、名前データは実際には任意の文字列を使用することができますが、通常はUUIDの使用方法(例えばアカウント名や製品IDなど)に関連します。しかし、使用される2つの値が何であれ、ハッシュ化されて36文字の英数字の文字列が最終的なUUIDとなります。

UUIDのバージョン3と5は、主に異なるハッシュアルゴリズムを使用している点で異なります。UUID v3はMD5を使用し、UUID v5はSHA-1を使用しています。

バージョン4。 これらのUUIDは単にランダムに生成された36文字の文字列です。v4 UUIDであることを示す1つの4を除けば、各文字は単にa~zのランダムな文字、または0~9の整数です。

生成は完全にランダムであるため、ユニークである可能性は極めて高いです。また、日付、MACアドレス、名前データなどの識別情報は含まれていません(これは特定のユースケースによって利点にも欠点にもなり得ます)。

バージョン6、7、8。 この記事を書いている時点ではこれらのバージョンは存在しませんが、提案されており、今後数年のうちにUUID標準に追加される可能性があります。これらの提案されている新しいUUIDについては、こちらで詳細をご覧いただけます。

UUIDの生成方法

UUIDは複雑そうに見えるかもしれませんが、最新のアプリケーション開発の文脈ではUUIDの生成は実はとても簡単なのです。一般的なプログラミング言語には関数を呼び出すだけで簡単にUUIDを生成できるライブラリが用意されています。

例えばJavaScriptではuuidパッケージをインポートしてuuid.v1()を呼び出すだけでUUID v1が生成されます。Pythonも非常に似ていてuuidをインポートしてuuid.uuid1()でUUID v1が生成されます。

データベース、特に分散型データベースではUUIDの生成が組み込まれている場合があります。例えばCockroachDBでは行の識別子としてUUIDを使うことを推奨していますが、これを行うにはgen_random_uuid()関数を使うだけと簡単です。

そのため、例えばSQLを使ってテーブルを作成する際に各行に対してUUID v4を自動生成するようにすることができます:

CREATE TABLE users (
        id UUID NOT NULL DEFAULT gen_random_uuid(),
        city STRING NOT NULL,
        name STRING NULL,
        address STRING NULL,
        credit_card STRING NULL,
        CONSTRAINT "primary" PRIMARY KEY (city ASC, id ASC),
        FAMILY "primary" (id, city, name, address, credit_card)
);

このテーブルに行が挿入されるたびにid値は自動生成されたUUIDとなります。

UUIDの生成と使用は、一般的に非常に簡単です。具体的な実装の詳細は、使用するデータベース技術やプログラミング言語によって多少異なりますが、ほとんどの場合、generate_uuid()関数のようなものを呼び出すだけです。

分散データベースにおけるUUID

分散型SQLデータベースはNoSQLデータベースの弾力的なスケールと回復力、SQLデータベースのトランザクションの一貫性と親しみやすさを組み合わせた強力な組み合わせを提供します。

しかし、分散システムを扱うということは、いくつかの点で異なるアプローチが必要であることを意味します。例えば行IDを生成する際に整数に1を足して234と識別するような従来のやり方は分散システムではうまく機能しません。これらのアプローチは「ホットスポット」ノードにつながり、パフォーマンスのボトルネックになります。

UUIDはデータベースの各ノードが他のノードと照合することなく、自律的に完全にユニークなUUIDを生成できるため、多くの分散ワークロードに優れた選択肢を提供します。

しかし、これはUUIDが常に最良の選択であることを意味するものではありません。CockroachDBではUUIDを使うとうまくいくことが多いですが、マルチカラムの主キーを使うと、状況によっては優れたパフォーマンスを発揮することがあります(ただし、セットアップとテストがより複雑になります)。

ハンズオンに行く

分散型データベースでUUIDを扱うことを自分で試してみたいと思いませんか?CockroachDB Serverlessは無料で、新しいクラスタをスピンアップして、5分以内にテーブルの作成とデータの挿入を行うことができます。ぜひ試してみてください!

www.youtube.com

高可用性の簡単な歴史

www.cockroachlabs.com

以前、「営業時間」が記載されているウェブサイトを見たことがあるのですが、実店舗の電気が点いているときだけ「営業」しているのです。コンピュータは毎日一日中稼働できるのに、なぜそうでないのかと戸惑い、少し不満に思いました。私はインターネットの驚異的な稼働率保証に慣れきっていました。

しかし、インターネットが普及する以前は24時間365日の高可用性は「あるもの」ではなかったのです。可用性は望ましいものでしたが、根本的な権利として感じるものではありませんでした。コンピュータは必要なときにだけ使うものであり、依頼が来るのをぼんやりと待っているようなものではなかったのです。インターネットが普及するにつれ、それまで珍しかった現地時間午前3時の依頼が世界中に広がるビジネスアワーになり、コンピュータがその依頼に応えられるかどうかが重要になりました。

しかし、多くのシステムではリクエストに対応するコンピュータが1台しかなく、単一障害点となっていました。このような事態を回避するためには、ニーズを満たすことができる複数のコンピュータに負荷を分散させる必要がありました。しかし、分散計算にはそのよく知られた長所もあれば、特に同期やシステム内の部分的な故障を許容する(耐障害性)という鋭い側面もあります。エンジニアの各世代は時代のニーズに合わせてこれらの解決策を反復してきました。

データベースがどのように流通するようになったかは、コンピュータサイエンスの他の分野に比べて発展がずっと遅れている難問であるため、特に関心が高いです。確かにソフトウェアは何らかの分散計算の結果をローカルデータベースで追跡していましたが、データベースの状態自体は1台のマシンに保持されていました。なぜでしょうか?マシン間で状態を複製するのは難しいからです。

この記事では分散データベースが歴史的にどのように耐障害性を扱ってきたか、また、高いレベルでの高可用性とはどのようなものかについて見ていきます。また、さまざまなタイプの高可用性システムを紹介し、ダウンタイムに脆弱なアーキテクチャの運用コスト(および財務コスト)についても言及します。

耐障害性 vs 高可用性

高可用性の多彩な歴史に触れる前に、しばしば同義語として扱われるこの2つの用語の違いを明確にしておきたいと思います。この2つの用語は非常に密接に関連していますが、同じではありません。

耐障害性はサービスの中断をゼロにすることを意味します。どこかで障害が発生した場合、システムは即座にバックアップソリューションに切り替わり、サービスは中断することなく継続されます。一方、高可用性はサービスが高可用性であることを意味しますが、常に利用できるわけではありません。システムは高可用性であっても耐障害でない場合もあります。私は一般的に高可用性は耐障害性の一側面だと考えています。つまり、ある種の「障害」(可用性)に対処するものであり、他の側面については必ずしも言及しません。

この例はやや作為的なものですが、基本的に誰もがストリーミングコンテンツを視聴するため、視聴者が特定のビデオを視聴できるかどうかを決定するデジタル著作権管理サービスを考えてみましょう。このサービスは常にクエリを提供し返すという意味で、高可用性になるように構成することができます。しかし、特定のバックエンドデータを正しく処理できず、エラーを返したりすべてのリクエストを拒否したりする状態になることがあります。この場合、可用性は高い(到達可能で答えを返している)のですが、システム内の何かが原因で誤動作しているため、耐障害ではありません。

この例の注意点は「バグ」と耐障害性は紙一重であるということです。 しかし、耐障害性の考え方はシステムが予期せぬ出来事を優雅に処理し、優れたユーザー体験を提供し続けることができることです。 (この例に興味を持たれた方はエンジニアがインスタンス障害に強いサービスを実装するために、Netflixのカオスモンキーが本番でインスタンスをランダムに終了させる方法をご覧になることをお勧めします。)

では、高可用性データベースの例を見ていきましょう。

高可用性データベースの種類は?

高可用性データベースは一般的に2つのカテゴリーに分けられますが、最近では第3のカテゴリーが一般的になってきています:

  1. アクティブ・パッシブ・データベース: リクエストを処理するアクティブなノードと災害時にすぐに使えるホットスペアを持つデータベース
  2. アクティブ・アクティブ・データベース: データベースがデータをシャードし、データベースへの書き込みを実行するアクティブノードを持つ場合
  3. マルチ・アクティブ・データベース: データベースには少なくとも3つのアクティブノードがあり、各ノードはクラスタ内のあらゆるデータに対して競合を発生させることなく読み取りと書き込みを行うことができます

アクティブ・パッシブ・アベイラビリティとは?

アクティブ・パッシブ・アベイラビリティとはデータベースがリクエストを処理するアクティブなノードを持ち、災害時に備えてホットスペアを持つことを意味します。アクティブ・パッシブ・アベイラビリティ・モデルは1つのノードがすべてのリクエストを受け取り、それをフォロワーにレプリケートするという2つのノードの概念で動作します。

昔はデータベースは1台のマシンで動作していました。ノードが1つしかなく、そのノードがすべての読み込みと書き込みを処理していました。部分的な障害というものは存在せず、データベースは稼働しているか停止しているかのどちらかでした。

1つはコンピュータが24時間アクセスされるため、ダウンタイムがユーザーに直接影響する可能性が高いこと、もう1つはコンピュータが常に要求される状態に置かれるため、故障する可能性が高くなることです。この問題を解決するにはリクエストを処理できるコンピュータを複数台用意することが必要であり、ここから分散データベースの物語が始まります。

シングルノードの世界では1つのノードで読み書きを行い、その状態をセカンダリのパッシブなマシンに同期させるというのが最も自然な解決策でした。こうして生まれたのがアクティブ・パッシブ・レプリケーションです。

アクティブ・パッシブは最新のバックアップによる高可用性を実現するための初期のステップでした。アクティブノードに障害が発生した場合、パッシブノードへのトラフィック誘導を開始することでアクティブノードに昇格させることができました。できる限りダウンしたサーバーを新しいパッシブ・マシンで置き換えます(そして、その間にアクティブ・マシンが故障しないことを祈ります)。

当初、アクティブノードからパッシブノードへのレプリケーションは同期的な手順でした。つまり、パッシブノードがそれを承認するまで変換はコミットされませんでした。しかし、パッシブノードがダウンした場合にどうするかは不明でした。確かにバックアップシステムが利用できない場合にシステム全体がダウンするのは理にかなっていませんが、同期レプリケーションではそれが起きてしまうのです。

さらに可用性を高めるためにデータを非同期で複製することも可能です。アーキテクチャは同じですが、データベースの可用性に影響を与えることなく、アクティブノードとパッシブノードのいずれかがダウンしても対応できるようになっています。

非同期のアクティブ・パッシブは新たな一歩を踏み出しましたが、まだ大きな欠点がありました:

  • アクティブ・ノードが停止するとパッシブ・ノードにまだレプリケートされていないデータが失われる可能性があります(クライアントにはデータが完全にコミットされていると信じられていたにもかかわらず)。
  • トラフィックを処理するために1台のマシンに依存することで、1台のマシンの最大利用可能リソースに縛られることになります。

99.999%の高可用性を求める: 多くのマシンへの拡張

インターネットが普及するにつれてビジネスのニーズは規模と複雑さを増していきました。データベースにとっては1つのノードで処理しきれないほどのトラフィックを処理する能力が必要であり、「常時接続」の高可用性を提供することが必須となりました。

多くのエンジニアが他の分散技術に携わった経験を持つようになった今、データベースがシングルノードのアクティブ・パッシブセットアップを越えて、多くのマシンにデータベースを分散できることは明らかでした。

シャーディング

そこで、アクティブ・パッシブレプリケーションをよりスケーラブルなものにするために、シャーディングを開発しました。

この方式ではクラスタのデータを何らかの値(行数や主キーのユニーク値など)で分割し、それらのセグメントを複数のサイトに分散させ、それぞれのサイトにアクティブ・パッシブのペアを持たせます。そして、クラスタの前に何らかのルーティング技術を追加して、クライアントをリクエストに応じた適切なサイトに誘導します。

シャーディングはワークロードを多くのマシンに分散させスループットを向上させるだけでなく、より多くの部分的な障害に耐え、単一障害点を排除することでより高い耐障害性を実現することができます。

このような利点がある一方で、システムのシャーディングは複雑でチームにとって大きな運用負担となります。シャードを意図的に管理することはアプリケーションのビジネスロジックにルーティングが入り込んでしまうほど大変なことでした。さらに悪いことに、システムのシャード化の方法を変更する必要がある場合(スキーマの変更など)、それを達成するために多大な(あるいは途方もない)エンジニアリングが必要になることがよくありました。

シングルノードのアクティブ・パッシブシステムはトランザクションのサポートも提供していました(強力な一貫性ではないにしても)。しかし、シャード間でトランザクションを調整するのは非常に難しく複雑なため、多くのシャードシステムはトランザクションを完全に見送ることを決定しました。

www.youtube.com

アクティブ・アクティブ・アベイラビリティとは?

アクティブ・アクティブ・アベイラビリティとはデータベースが少なくとも2つのアクティブなノードを持ち、データをシャードしてデータベースへの書き込みを実行することを意味します。アクティブ・アクティブ・アベイラビリティはアクティブ・パッシブから発展したもので、クラスタ内のノードが読み取りと書き込みを行うことで、データベースを単一のマシンを超えて拡張することができます。

シャード化されたデータベースは管理が難しく機能も十分でないため、エンジニアは少なくとも問題の1つを解決するシステムの開発に着手しました。その結果、トランザクションをサポートしないものの、管理が劇的に容易になるシステムが誕生しました。アプリケーションのアップタイムに対する要求が高まる中、チームのSLA達成を支援することは賢明な判断でした。

これらのシステムの背景にあるのは各サイトがクラスタのデータの一部(またはすべて)を格納し、そのデータの読み取りと書き込みを行うことができるというアイデアです。あるノードが書き込みを受けると、そのコピーを必要とする他のすべてのノードに変更が伝搬されます。2つのノードが同じキーに対する書き込みを受け取った状況を処理するために、コミットする前に他のノードの変換が競合解決アルゴリズムに供給されました。各サイトが「アクティブ」であることから、アクティブ・アクティブと呼ばれるようになりました。

各サーバーがすべてのデータの読み取りと書き込みを処理できるため、シャーディングはアルゴリズム的に達成しやすく、デプロイの管理も容易になりました。

可用性という点ではアクティブ・アクティブが優れていました。ノードに障害が発生した場合、クライアントはデータを保存している別のノードにリダイレクトさせればよかったのです。データのレプリカが1つでも生きていれば、読み込みも書き込みも可能です。

この方式は高可用性では優れていますが、一貫性やデータの正確性とは根本的に相反する設計になっています。各サイトはキーに対する書き込みを処理できるため(フェイルオーバーのシナリオではそうなる)、処理中のデータを完全に同期させることは非常に困難です。その代わりに不整合を「平滑化」する方法について粗い粒度の決定を行う競合解決アルゴリズムによって、サイト間の競合を調停するのが一般的なアプローチです。

この解決は、クライアントがプロシージャに関する回答を受け取った後、理論的にはその回答に基づいて他のビジネスロジックを実行した後に行われるため、アクティブ-アクティブ・レプリケーションによってデータに異常が発生しやすくなります。

しかし、アップタイムの重要性を考えると、ダウンタイムのコストは潜在的な異常のコストよりも大きいと判断され、アクティブ・アクティブがレプリケーションの主流となりました。

規模に応じた正しさ: コンセンサスとマルチ・アクティブ・アベイラビリティ

アクティブ・アクティブは高可用性を提供するというインフラが抱える大きな問題に対処しているように見えました。しかし、それはトランザクションを放棄することで実現したに過ぎず、強力な一貫性を必要とするシステムには説得力のある選択肢がないままでした。

例えば、Googleは広告ビジネスに巨大で複雑なシャード化されたMySQLシステムを使用しており、SQLの表現力に大きく依存してデータベースへのクエリを任意に行うことができました。これらのクエリはパフォーマンスを向上させるためにセカンダリインデックスに依存することが多いため、その元となるデータとの整合性を完全に保つ必要がありました。

やがてシステムの規模が大きくなり、シャード化されたMySQLに問題が生じるようになったため(Spencer Kimball氏はこのポッドキャストでシャード化されたMySQLAdWordsに関する実体験を語っています)、同社のエンジニアは大規模な拡張性を持つシステムとビジネスが求める強力な一貫性を両立する問題をどう解決するかを想像し始めました。アクティブ・アクティブはトランザクションをサポートしていないためそのような選択肢はなく、新しいものを設計する必要がありました。その結果、コンセンサスレプリケーションをベースとした一貫性を保証しながらも高可用性も実現できるシステムが完成しました。

コンセンサスレプリケーションではあるノードに書き込みが提案されると他のいくつかのノードにレプリケートされます。過半数のノードが書き込みを承認すると、書き込みをコミットすることができます。

コンセンサスと高可用性

コンセンサスレプリケーションは同期レプリケーションと非同期レプリケーションの間のスイートスポットに位置するというのがここでの重要な考え方です。つまり、システムの可用性に影響を与えることなく少数派のノードがダウンしてもクラスタは許容できるのです。(ダウンしたマシンのトラフィックを処理するための注意事項があります。)

しかし、コンセンサスの代償として書き込みを行うために他のノードと通信する必要があります。ノード間で発生するレイテンシを減らすために、同じアベイラビリティゾーンにノードを配置するなどの方法がありますが、これは高可用性とトレードオフになります。

例えばすべてのノードが同じデータセンターにある場合、ノード間の通信は高速ですがデータセンター全体がオフラインになることには耐えられません。ノードを複数のデータセンターに分散させると書き込みに必要なレイテンシは増加しますが、データセンター全体がオフラインになってもアプリケーションを停止させずに済むため、可用性を向上させることができます。

マルチ・アクティブ・アベイラビリティとは?

マルチ・アクティブ・アベイラビリティはデータベースが少なくとも3つのアクティブなノードを持ち、各ノードがクラスタ内の任意のデータに対して競合を発生させずに読み取りと書き込みを実行できることを要求します。

CockroachDBはGoogle Spannerの論文から学んだことの多くを実装しています(ただし原子時計は必要ありません)。その中にはコンセンサスレプリケーションの他にも可用性をよりシンプルにする機能があります。この仕組みを説明しアクティブ・アクティブと区別するために、マルチ・アクティブ・アベイラビリティという用語を作りました。

アクティブ・アクティブ vs マルチ・アクティブ

アクティブ・アクティブはクラスタ内のどのノードでもキーの読み取りと書き込みを行うことができ、書き込みをコミットした後にのみ受け入れた変更を他のノードに伝搬させることで可用性を実現します。

一方、マルチ・アクティブ・アベイラビリティではどのノードでも読み取りと書き込みを行うことができますが、書き込みの際にはレプリカの大部分が同期するようにし、読み取りは最新バージョンのレプリカからしか行えません。

高可用性という点ではアクティブ・アクティブは1つのレプリカが読み書きともに利用可能であればよく、マルチ・アクティブは過半数のレプリカがオンラインでなければコンセンサスを得られません(システム内の部分故障は依然として許容されます)。

しかし、これらのデータベースの可用性の下流にあるのは一貫性の違いです。アクティブ・アクティブデータベースはほとんどの状況において書き込みを受け入れるように努力しますが、その後、クライアントがそのデータを現在または将来にわたって読むことができるかどうかについては保証されません。一方、マルチ・アクティブデータベースはそのデータが後で一貫性を持って読めることが保証される場合にのみ書き込みを受け付けます。

ここから先は?

過去30年の間にデータベースのレプリケーションと可用性は大きく進歩し、今では世界中に広がる展開をサポートし、決してダウンすることがないように感じられるようになりました。この分野の最初の進出はアクティブ・パッシブレプリケーションによって重要な基礎を築きましたが、最終的にはより優れた可用性とより大きなスケールが必要になりました。

そこで業界はデータベースの2つの主要なパラダイムを開発しました: アクティブ・アクティブは書き込みを素早く受け付けることを第一に考えるアプリケーション向けで、マルチ・アクティブは一貫性を必要とするアプリケーション向けです。

量子もつれを利用して分散状態を管理する次のパラダイムに移行できる日が来ることを私たちは楽しみにしています。

データベースのレプリケーションアベイラビリティの次のフェーズを定義することがあなたのコーヒーブレークの白昼夢であるなら、ここで私たちのオープンポジションをチェックしてみてください。

さらに読む