Cloud Datastore は Entity 毎になぜ秒間1回の書き込み制約があるのか

はじめに

Cloud Datastore では 1 write / entity group / sec という制約があります (公式ドキュメント)。

確かに Datastore を使っていると、同一 Entity Group に対する書き込み頻度が高い時に一部の書き込みが失敗することは経験上よくありましたが、 なぜこの制約が存在しているのかはドキュメントに詳しくは記載されていませんでした。 そこで Cloud Datastore のバックエンドである Megastore の論文「Megastore: Providing Scalable, Highly Available Storage for Interactive Services」をベースに、その理由を調べてみました。

※公開されている情報から推測しているので、間違っていた場合ご指摘下さい。

TL;DR

先に結論を述べると、レプリカ間で書き込みデータのレプリケーションを行なう手続きに、最大で1秒ほどの時間がかかるためです。

より詳しく述べると、Cloud Datastore の以下の特徴から書き込みスループットの制約が存在します。

  • 高可用性を実現するためにデータセンターを跨いで複数のレプリカを動かしており、レプリカ間で同期的なレプリケーションを行っている
  • どのレプリカからも読み込み・書き込みが開始でき、クエリによっては Strong Consistency なデータを取得できる
  • レプリカ間でのレプリケーションには Paxos を用いており、Paxos の手続きの完了にデータセンター間で 1RTT もしくは 2RTT のネットワークレイテンシを要する

Megastore のアーキテクチャ

f:id:furuyamayuuki:20171210225222p:plain:w400

Megastore は高い可用性を実現するためにデータを地理的に離れた複数のデータセンターで保持していますが、 各ノードで一貫性のあるデータ読み込みを実現するために、ノード間で同期的なレプリケーションを行っています。

レプリケーションはマスター・スレーブのような構成ではなく、全てのノードから Read / Write を発行できるマルチマスターのようなレプリケーションとなっており、各ノードは「レプリカ」と呼ばれています。

つまり、同じ内容を保持しているデータベースが地理的に離れた箇所に複数デプロイされている形です。

Read 手順

f:id:furuyamayuuki:20171210225723p:plain:w400

Megastore での Write 手順を見る前に、参考までに Megastore からのデータ読み込みがどういう手順で行われるか見てみます。

論文の "4.6.2 Reads" から引用すると、読み込みは以下の手順で行われます。

  1. Query Local: 地理的に最も近いレプリカの Coordinator に、所持している Entity Group のデータが最新かどうかを問い合わせる。
  2. Find Position: 1番で最新だった場合、選択したレプリカから Entity Group の最新のログポジションとタイムスタンプを取得し、3番、4番の処理をスキップする。最新でなかった場合、他の全てのレプリカにログポジションを問い合わせ、過半数の結果を採用する。
  3. Catchup: 2番で取得した最新のログポジションを元に他のレプリカからログ (= Write Ahead Log) を取得し、自身のデータベースに適用していく。
  4. Validate: Coordinator に今持っている Entity Group のデータが最新であることを伝える。
  5. Query Data: 選択したレプリカから Engity Group のデータを読み出す

この手順を見ると、仮にあるレプリカが他のレプリカと比べて古いデータを保持していたとしても、一連のシーケンスの中で他のレプリカへの追いつきが走るようになっています。 つまりどのレプリカから Read しようとしても、必ずシステムに最後にコミットされた最新のデータが読み取れること (Strong Consistency) が保証されています。

Write 手順

f:id:furuyamayuuki:20171210225801p:plain:w400

次に書き込みの手順です。

書き込む値のレプリケーションPaxos を使って行われます。 通常の Paxos は合意形成に至るまでノード間の通信が少なくとも2回 (Prepare phase & Accept phase) 必要ですが、 Megastore が実装している Paxos ではノード間の通信を最短で1回のラウンドトリップで済むような拡張を入れています。

論文の "4.6.3 Writes" から引用すると、書き込みは以下のような流れで行われます。

  1. Accept Leader: リーダーレプリカに proposal number = 0 で Proposal を投げる。これが成功した場合2番をスキップする。
  2. Prepare: 全てのレプリカに Prepare メッセージを投げる。
  3. Accept: リーダー以外の全てのレプリカに Proposal を投げる。全てのレプリカが Accept した場合4番をスキップする。
  4. Invalidate: 3番で一部のレプリカが Accept しなかった場合、そのレプリカの Coordinator に対して Invalidate メッセージを送る(そのレプリカが持つ現在の値を読ませないようにするため)。
  5. Apply: 合意した変更点を実際のストレージ(Bigtable)に適用させる

1番の Accept Leader フェーズが Paxos の拡張になっており、後続の Prepare フェーズをスキップ出来るようになっています。 これは1つ前の書き込みの完了に、その次の Prepare の意味も持たしていることから来ていますが、より詳細な前提条件の説明が必要なため、詳しくは論文の "4.4.2 Fast Writes" を参照して下さい。

Write が完了するタイミング

このシーケンスのなかで、元の Client に書き込みの完了 (Ack) を伝えれるのは3番、もしくは4番が終わったタイミングになります。 よって最短で書き込みに要するネットワークレイテンシは、

# 全てのレプリカが Accept した場合
RTT(Client, A) + MAX(RTT(Client, B), RTT(Client, C))

# 一部のレプリカが Accept しなかった場合
RTT(Client, A) + MAX(RTT(Client, B), RTT(Client, C)) + RTT(Client, C)

となりますが、Client ↔  Replica A 間は地理的になるべく近い場所にいる(ように Leader を選ぶ)ため、両者の RTT は非常に短い時間だと考えられます。 よってその時間は十分無視できるとすると、

# 全てのレプリカが Accept した場合
MAX(RTT(Client, B), RTT(Client, C))

# 一部のレプリカが Accept しなかった場合
MAX(RTT(Client, B), RTT(Client, C)) + RTT(Client, C)

となり、大体1RTT、もしくは2RTTかかることになります。

仮にレプリカがアメリカの西海岸と東海岸に配置されていると仮定すると、レプリカ間のRTTは約120msです。 (参考: AT&Tが公開しているネットワークレイテンシから、サンフランシスコとニューヨーク間のレイテンシ: 62ms を持ってきました。)

よって、書き込みのレイテンシはネットワークレイテンシに実際の各レプリカの処理時間を乗せたものになるので、150ms〜300msくらいが標準的な書き込み時間になるのではないでしょうか。

そしてトランザクション内での Entity Group に対する書き込みはシリアライズ化されているので、この書き込み1回あたりのレイテンシがそのままスループット制約となります。

Google で計測されたレイテンシ

f:id:furuyamayuuki:20171210230118p:plain:w400

論文の "4.10 Production Metrics" に Google の本番環境での Megastore の読み込み・書き込みレイテンシが載っていました。 それによると、平均的なアプリケーションは大体100ms〜500msで書き込みが完了しているようです。 ただし、この値はデータセンター間の距離や、レプリカ数に依存するとも述べられているので、場合によっては1秒近くかかってしまうこともあるのでしょう。

まとめ

Cloud Datastore の 1 write / entity group / sec の制約について調べ、データセンター間での同期的なレプリケーションに要する時間がネックになっていることを説明しました。 ちなみに Megastore の論文の中でも "4.8 Write Throughput" において、書き込みのスループットが高くないことを課題として挙げています。

Our implementation of Paxos has interesting tradeoffs in system behavior. Application servers in multiple datacenters may initiate writes to the same entity group and log position simultaneously. All but one of them will fail and need to retry their transactions. The increased latency imposed by synchronous replication increases the likelihood of conflicts for a given per-entity-group commit rate.

(訳) 私達の Paxos の実装はシステムの動作に興味深いトレードオフをもたらします。複数のデータセンタ上のアプリケーションサーバから、同一 Entity Group の同一ログポジションに同時に書き込みを行おうとするかもしれません。その場合、その内の一つを除いた全てのクライアントはトランザクションをリトライする必要があります。(Megastore が行っている)同期的なレプリケーションによって生じる高いレイテンシは、ある Entity Group に対するコミットがコンフリクトしてしまう可能性を上げることになります。

このパフォーマンス上のボトルネックを解決することを1つの目的として、後続のデータベースである Spanner の開発に繋がっていったようですね。

参考

GCPコンソールのヘッダの色を変更するChrome拡張を作りました

GCP のコンソールで複数のプロジェクトを操作していると、特定のプロジェクトだけ視覚的に目立たせたいことがあります。 特に開発用のプロジェクトと本番のプロジェクトを行き来していると、たまにプロジェクトを間違えそうになってヒヤッとすることがありますよね。

そこで GCP コンソール画面のヘッダの色をプロジェクト毎に変更できる Chrome 拡張を作りました。
GCP console colorize

img1

条件は複数書けるので、プロジェクト毎に細かく設定できます。
(正規表現も使えます)

Chrome Web Store からインストールできるので是非導入してみてください。

GAE Datastore の Single-property Index と Composite Index は全く違うものと理解する

Datastore の Single-property Index と Composite Index はどちらも似たようなものだと思っていたのですが、実際のところはかなり違う性質をそれぞれ持っていることが段々とわかってきたので、現時点の自分の理解をメモしておきます。 恐らく Cloud Datastore にも該当する話だと思います。

Datastore のインデックスの種類

Datastore には2種類のインデックスが存在します。

  • Single-property Index (Built-in Index とも呼ばれる)
  • Composite Index (Custom Index とも呼ばれる)

このうち Single-property Index はデフォルトで全てのプロパティに有効になっているインデックスであり、Entity を保存した段階でそれぞれのプロパティに対応するインデックスが自動で作られます。 Composite Index はあらかじめ設定ファイルで定義しておく必要があり、それによって複数のプロパティを組み合わせた複合インデックスを作ることが出来ます。

両インデックスの違い

一番大きな違いだと思うのが、Single-property Index が Entity 単位にインデックスの ON/OFF をコントロールできる一方、Composite Index は Datastore の Kind 毎に ON/OFF されるという点です。 この性質の違いがいくつかの挙動の違いを生みます。

1. Single-property Index を途中からつけても、既存の Entity には効果がない

これはドキュメントに書かれてる内容ですが、今まで明示的に Unindex としていたプロパティに途中からインデックスを定義しようとしても、既存の Entity に対応するインデックスは作成されません。 そのため、古い Entity は MapReduce などを用いて fetch → put と再保存してインデックスを構築し直す必要があります。 これは Entity 単位にインデックスの ON/OFF が制御されるという性質上しょうがないものかと思います。

逆に Composite Index は All or Nothing なので、途中から定義したとしても既存を含む全ての Entity に作られることになります。

2. Single-property Index がないと Composite Index にエントリが追加されない

これは(自分の知る限り)アンドキュメントな内容ですが、Composite Index を設定ファイルで定義したとしても、そこで使用しているプロパティの Single-property Index が一つでも存在しない場合、Composite Index にはエントリが追加されません。 実際にその複合インデックスを使うようなクエリを投げても、単純にクエリに引っかからないような挙動になり、エラーもでないのでかなりハマります。 なぜこのような挙動になっているのか原理や理由はわかっていませんが、Composite Index は全ての Entity が対象となるので、一部の Entity をインデックスに追加しないようにするためにそういう機構があるのでしょうか。

Single-property Index は必ず定義しておくべき

上記のようなことがあるため、特別な理由がない限り Single-property Index は定義しておくべきだと考えています。 以前はインデックスの作成操作も Datastore write ops に含まれてしまっていたので必要のないインデックスはなるべく避ける傾向にあったと思いますが、今はそれらが Entity 単位の Quota になったため、あまり気にしなくていい感じになりました。 Single-property Index が既に存在していると、あとから Composite Index も張りやすくなりますし、なにより Datastore のコンソール画面からクエリを投げられるのでデバッグがやりやすくなると思います。

とはいってもインデックスの作成はその分ストレージを消費しますし、インデックスに追加されるまでの時間もインデックス数に応じて増えていくと思うので、他のインデックス追加に影響を及ぼして参照整合性が弱くなってしまうのを避けたい、といった場合には控えたほうが良さそうです。 (cf. Anti Pattern #2: Too Many Indexes - Balancing Strong and Eventual Consistency with Google Cloud Datastore)

一点実際のプログラミング時に注意が必要なのは、GAE/Java + Objectify で開発していると明示的に @Index アノテーションをプロパティに付加しておかないと setUnindexedProperty でプロパティが定義されてしまい、インデックス=OFFとなってしまう点です。 これが例えば GAE/Python + ndb だとプロパティを定義するとデフォルトでインデックス=ONとなるので、個人的には Objectify もデフォルトはインデックス=ONの挙動にしておいて欲しかった感じはあります...。

まとめ

Datastore の Single-property Index と Composite Index の性質をまとめると以下のようになります。

制御単位 インデックスの構築 注意点
Single-property Index Entity 単位 半自動 古い Entity は手動でインデックスを作る
Composite Index Kind 単位 自動 Single-property Index が作られていないとダメ

手探りで色々試している面もあるので、理解が間違っている等ありましたらご指摘下さい。

参考

GAE Task Queue をマイクロサービスのサービス間通信として使う

昨今マイクロサービスアーキテクチャに基づいたアプリケーション構築が話題ですが、Google App Engine を用いてる場合 Task Queue をサービス間の通信として便利に使用することが出来ます。

GAE の Task Queue (Push) について

背景

マイクロサービスでよくやることとして、あるサービスでイベントが発生したらそれを別のサービスに非同期に通知したいことがあります。 通常のアプリケーションの場合、何かしらの Message Queue を用いて Job Worker 経由で別のサービスに通知することが多いと思います。

+-----------+
| Service A |
+-----------+
     ↓ Push
+-----------+
|    MQ     |
+-----------+
     ↑ Pull
+-----------+
| Job Worker|
+-----------+
     ↓ HTTP
+-----------+
| Service B |
+-----------+

この場合 Job Worker は確実に Service B に通知を行なうよう、リトライ処理などをきちんと実装する必要があります。

一方 Google App Engine でアプリケーションを構築してる場合、代わりに Push 型の Task Queue を使うことで実現できます。

+-----------+
| Service A |
+-----------+
     ↓ Push
+-----------+
| Task Queue|
+-----------+
     ↓ HTTP
+-----------+
| Service B |
+-----------+

Task Queue はタスクの通知を HTTP で行なうため、サービス間に Job Worker を挟まずダイレクトに別のサービスに通信できる、という算段です。

Task Queue のコード例

以下のコードは Java で別のサービスに通信する例です。 アプリケーション特有のヘッダや、HTTP Body などを割と自由に付与できます。

// キューの取得
Queue queue = QueueFactory.getDefaultQueue();

// タスクの定義
TaskOptions task = TaskOptions.Builder
    .withMethod(TaskOptions.Method.POST)
    .url("/service_b/evetns")
    .header("X-My-Header", "foo, hoge")
    .payload("{\"eventId\": 123}".getBytes(), "application/json");

// タスクのキューイング
queue.add(task);

Task Queue だと何が嬉しいか

Job Worker となる部分を用意しなくて済むのも大きなメリットですが、他にも以下の様なメリットがあります。

  • Task Queue は App Engine のフルマネージドな部分なので、上述のリトライ処理を適切に行なってくれます。
    • タスクの種類ごとにリトライの間隔なども調整可能です。
  • 通常の MQ を用いた場合の欠点として、データベースのトランザクションの中に MQ へのキューイングを含められませんが、App Engine の場合 Datastore のトランザクションの中に Task Queue へのキューイングを含めることができます。
    • サービス自体の処理は成功したけど、別のサービスへの通知は失敗していた、などの中途半端な状態を防げれます。

注意点

Task Queue にもいくつか制約があります。

  • あくまでも非同期通信です。同期的なサービス間通信をしたい場合は、通常通りサービス内で HTTP 通信をします。
  • 使用できる HTTP メソッドは GET/POST/PUT/DELETE のみです。PATCH など別のメソッドを使いたい場合は X-HTTP-Method-Override などの Method Override を併用することになると思います。
  • Task Queue からタスクを受け取ったサービスは 200 番台のレスポンスを返さないとリトライされてしまいます。仮に 300/400 番台のレスポンスで成功を表すものがある場合は注意が必要です。
  • Task Queue で指定できる URL は、同じ App Engine アプリケーションで動いているサービスに対してのみです。App Engine 外に別のサービスを置いている場合は、一度 App Engine 内でタスクを受け取ったあとに行う必要があります。

まとめ

Task Queue を用いたサービス間通信のメリットについて書きました。 何より Task Queue によるタスクの受け渡しが HTTP で通信されるというのが大きな特徴ではないでしょうか。 マイクロサービスではサービス間通信を HTTP で行なうことが多いので、今回挙げた例のように「リトライ付きの非同期 HTTP クライアント」として Task Queue を使えるというのは非常に魅力的に思えます。

Let's Encrypt でサイトをHTTPS化する

Let's Encrypt が Public Beta になり事前申請なしで証明書の発行ができるようになったので、試しに使ってみました。

環境

事前準備

証明書を取得する

Let's Encrypt のドキュメントの通り、letsencrypt-auto スクリプト一発で証明書を取得することが出来ます。

# EC2 内において
$ git clone https://github.com/letsencrypt/letsencrypt
$ cd letsencrypt
$ ./letsencrypt-auto --debug certonly

letsencrypt-auto を実行すると、Linux の設定画面のような TUI の青い画面が表示され、対話形式でメールアドレスやドメイン名を入力していきます。

この間に裏では ACME プロトコルによるチャレンジが行われており、問題なく終了すると /etc/letsencrypt/live/{domain}/ 以下に証明書等が作られます。

作られた証明書を見てみる

$ sudo ls /etc/letsencrypt/live/{domain}
cert.pem  chain.pem  fullchain.pem  privkey.pem
  • cert.pem : 証明書
  • chain.pem : 中間証明書
  • fullchain.pem : cert.pem と chain.pem がくっついたもの
  • privkey.pem : 秘密鍵

実際に取得した証明書を見てみると、確かに Let's Encrypt の CA による署名が付いていることがわかります。 証明書の有効期限は3ヶ月と短いですね。

$ sudo openssl x509 -in /etc/letsencrypt/live/{domain}/cert.pem -text -noout
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            01:45:4d:37:ef:ab:46:11:f5:b9:8d:e5:d0:55:97:af:02:cc
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=US, O=Let's Encrypt, CN=Let's Encrypt Authority X1
        Validity
            Not Before: Dec 26 06:00:00 2015 GMT
            Not After : Mar 25 06:00:00 2016 GMT
    ...
    ...
    ...

nginx に設定する

nginx.conf の server ディレクティブに証明書と秘密鍵のパスを指定します。 nginx をリロードして、ブラウザから https でアクセスできれば成功です。

server {
    listen 443;
    server_name localhost;
    root /var/www/html;
    
    ssl on;
    ssl_certificate /etc/letsencrypt/live/{domain}/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/{domain}/privkey.pem;
    location / {
    }
}

Let's Encrypt を使うと、手作業でも最初の git clone からものの 10 分ほどでサイトをHTTPS化することができました。 いやー簡単だなー。

MySQL で JWT の中身を確認する

MySQLJSON Web Token (JWT) をデコードして表示してくれるプラグインを作りました。

addsict/mysql_jwt

こんな風に使えます。

> SET @token = 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vZXhhbXBsZS5jb20iLCJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjoiaHR0cDovL2FwcC5leGFtcGxlLmNvbSIsImV4cCI6MTQyNDQzNzQ5MSwiaWF0IjoxNDI0NDM2NTkxLCJqdGkiOiIxMjM0NTY3ODkwIn0.V0SEo1Y1kurWp2bSYU9gEQ2K9nweII_RNIlYEBRHdWY';
> SELECT decode_jwt(@token)\G
*************************** 1. row ***************************
decode_jwt(@token): {"iss":"http://example.com","sub":"1234567890","aud":"http://app.example.com","exp":1424437491,"iat":1424436591,"jti":"1234567890"}
1 row in set (0.00 sec)

decode_jwt()の第二引数に claim を指定することもできます。

> SELECT decode_jwt(@token, 'iss');
+---------------------------+
| decode_jwt(@token, 'iss') |
+---------------------------+
| http://example.com        |
+---------------------------+
1 row in set (0.00 sec)

> SELECT FROM_UNIXTIME(decode_jwt(@token, 'exp'));
+------------------------------------------+
| FROM_UNIXTIME(decode_jwt(@token, 'exp')) |
+------------------------------------------+
| 2015-02-20 22:04:51                      |
+------------------------------------------+
1 row in set (0.00 sec)

DB に格納されてる JWT の中身をささっと見たい時に便利かなと思うので、是非使ってみてください。

Web アプリケーションに HAL を適用する

HAL (Hypertext Application Language) とは Web API でやり取りされるリソースを、ハイパーメディアとしても扱えるようにするための仕様です。 ハイパーメディアとは HTML のアンカータグをイメージするとわかりやすいですが、ドキュメントといったメディアがリンクを介して繋がっている状態のことを指します。 例えば通常の Web API で取得できるリソース同士は特別何かで繋がっているわけではなく、お互いのリソースがある意味孤立した状態にありますが、そこにリンクを含めてあげることでお互いのリソース間を行き来することが出来るようになります。

HTML と HAL をハイパーメディアの観点で言うと、このような感じになるでしょうか。

  • HTML: 人間が操作するためのハイパーメディア
  • HAL: 機械が操作するためのハイパーメディア

具体的にリソースを HAL + JSON で表すとこんな感じになります。

{
    "userId": 10,
    "lastOrderDate": "Mon, 07 Jul 2014 18:26:21 GMT",
    "_links": {
        "self": { "href": "/users/10/orders" },
        "next": { "href": "/users/10/orders?page=2" }
    },
    "_embedded": {
        "orders": [{
            "orderId": 12,
            "orderDate": "Mon, 01 Jul 2014 18:26:21 GMT",
            "_links": {
                "self": { "href": "/users/10/orders/12" }
            }
        }, {
            "orderId": 11,
            "orderDate": "Mon, 01 Jul 2014 18:26:21 GMT",
            "_links": {
                "self": { "href": "/users/10/orders/11" }
            }
        }]
    }
}

userIdlastOrderDate といったリソースの情報と共に、それに付随したリンクが _links というプロパティに含まれています。 このリソースを使用するクライアントはそのリンクを「辿る」ことにより、別のリソースの操作を行おうという試みです。

こういったハイパーメディアを Web API に利用する考えは HATEOASREST Level3 など、昨今注目を集めているように思えます。

ブラウザと Web API の相性の悪さ

ここで少しブラウザと Web API*1 の関係について考えてみます。 ブラウザは人間が操作するものであり、HTML 上のリンクをクリックして別のドキュメントに遷移したりします。 しかし昨今の Web アプリはそういった画面遷移の他にも、JavaScript で Web API 用の URL を組み立て、Ajax を使って呼び出したりすることが多くなっています。 しかしそういった Ajax を使った Web API の呼び出しは、それまで HTML で表せていたハイパーメディアとしての領域をはみ出してしまいます。 リソース同士の繋がりを HTML とは別のもの(例えばクライアントサイドで定義したURLなど)を利用して表現しなければならないからです。 このように Web API が絡んでくると、クライアントサイドは既存のハイパーメディアの上にただ単純に乗っかっていれば済む話ではなくなるため、ブラウザとの相性が悪いと個人的には思っています。

ブラウザで HAL を使う

そこで、前述した HAL をブラウザでも使いたい!というのが本エントリーの主旨です。 具体的に言うと、HTML の他に HAL 形式のリソースを同時にブラウザに渡し、JavaScript ではその HAL 形式のリソースを参照します。 同じリソースを表しているのに、HTML と HAL という2つの表現として渡されてしまうという欠点はありますが、クライアントサイドは HAL に書かれているものだけを使えばいいので、サーバサイドとクライアントサイドが疎結合アーキテクチャになります。

例えば以下の様な HTML があるとします(HAL で表したリソースを type="application/hal+json" の script タグに埋め込んでおきます)。

<html>
<head>
    <script type="application/hal+json" id="hal-resource">
    {
        "_links": {
            "self": { "href": "http://example.com/users/10/orders" },
            "next": { "href": "http://example.com/users/10/orders?page=2" }
        },
        "_embedded": [{
            "id": "order-1",
            "_links": {
                "self": { "href": "http://example.com/users/10/orders/1" }
            }
        }, {
            "id": "order-2",
            "_links": {
                "self": { "href": "http://example.com/users/10/orders/2" }
            }
        }]
    }
    </script>
</head>
<body>
    <ol>
        <li id="order-1">Order 1</li>
        <li id="order-2">Order 2</li>
    </ol>
</body>
</html>

ここで次のページを Ajax で取得したいといった時、普通のやり方だと JS で URL を組み立ててそれを使ったりするでしょう。

var userId = 10;
var nextPage = 2;
var url = "/users/" + userId + "/orders?page=" + nextPage;
$.ajax({
    type: "GET",
    url: url,
    success: function (...) { ... },
    error: function (...) { ... },
});

一方、HAL を使うと、JS 側では渡されたリソースのリンクを辿るだけで次のページを取得できます。

JavaScript で HAL を扱うには halfred(https://github.com/basti1302/halfred) というライブラリがおすすめです。

var hal = $("#hal-resource").html();
var resource = halfred.parse(JSON.parse(hal));
var url = resource.link("next").href;

$.ajax({
    type: "GET",
    url: url,
    success: function (...) { ... },
    error: function (...) { ... },
});

このように単純にブラウザを HAL のクライアントとみなすことで、Web API との親和性も高くなります。

さいごに

HAL を Web アプリケーションに使用した場合の一番わかりやすい利点は、エンドポイントの管理がサーバサイドで完結し疎結合アーキテクチャになる点です。 もちろん HAL なんて使わなくても同様のことは簡単に実現できますが、標準に乗っかることでブラウザ以外の別のクライアントでも同じやり方を採用できたりするので、色々ハッピーかなと思ったりする年の瀬でした。

参考

*1:ここでは WebAPI を「HTTP プロトコルを利用してリソースを操作するためのコンピュータ用のインターフェース」として考えています。