排他制御の失敗から学ぶ一意性を守るAPI設計の要点
目次
はじめに
業務の中で、以下のような機能を持つAPIを実装して利用していました。
- リクエストの入力に基づき、一意のリソースを生成して返す
- そのリソースがすでに存在する場合、初回に生成した結果を返す
初めてこのエンドポイントにアクセスした場合、サーバー側で新しいリソースが生成され、その結果が返されます。2回目以降のアクセスでは、1回目と同じ結果が返されます。
一見するとシンプルな仕組みですが、このAPIエンドポイントで予期せぬ挙動が発生しました。具体的には、1回目のアクセスと2回目以降のアクセスで異なる結果が返されることがありました。この問題は非常に稀に発生したため、原因を特定するまでに時間を要しました。
この記事では、この問題の発生原因と解決策、さらに今回の対応方針について解説します。
なぜこのような問題が発生したのか
この問題の原因は、APIに排他制御が実現されていなかったことにあります。
2度目以降のアクセスで1度目と同じ結果を返すためには、その結果を保存しておく必要があります。しかし、1度目のアクセスとほぼ同時に2度目のアクセスが発生した場合、両方のリクエストが「前の結果が保存されていない」と判断してしまい、それぞれ新しい一意のリソースを生成して返してしまいました。その結果、想定外の動作が発生していたのです。
この問題の回避策
この問題を回避するには、以下のような方法が考えられます。
1. RDBのロック機能(トランザクション)を利用する
RDBはデータの一意性を担保する仕組みを提供しており、トランザクションを活用することでAPIの結果の一意性を保証できます。 システムが RDB を使っているのであれば、最もシンプルかつ強力な選択肢です。
2. その他のシステムのロック機能を利用する
Redis、Zookeeper、DynamoDBなど、RDB以外のシステムを使用してロックを実現する方法もあります。
今回のケースでは、AWS上で稼働しているという環境を踏まえ、DynamoDBを利用するのが現実的で効率的です。DynamoDBの条件付き書き込みやトランザクションを用いることで、APIが返すデータの一意性を担保できます。 これらの機能を利用した楽観ロックにより、コストを抑えながら問題を解決できます。 DynamoDB 以外にも、S3 の条件付き書込みを利用することも考えられます。
これらの書込みで失敗した場合に保存済みデータの再取得を行うようにAPIの実装を修正することで、この問題を回避可能です。
3. APIの冪等性を実現する
入力パラメータに基づいて出力を決定する設計にすることで、APIがどのタイミングで呼び出されても同じ結果を返すように改修する方法です。この方法では、S3にデータが複数回保存されても、同じ内容が上書きされるだけで特に問題にはなりません。
ただし、生成されるデータが簡単に推測可能なものである場合、セキュリティ上の懸念が生じる可能性があります。これを防ぐために、ハッシュ関数やソルトなどを活用して内部データを推測しにくくする設計が必要です。
今回どのような手法で解決したか
今回のケースでは、APIの動作をAWS Lambda上で行い、結果をS3に保存していました。このため、1. RDSを利用する解決策は主にコストの観点から見送りました。
APIで返すデータはランダムなバイナリデータであり、呼び出し時のパラメータには複数のUUIDが含まれていました。この特性を活用し、システム独自のソルトを設定した上で、パラメータ内のUUIDを元にハッシュ関数を用いてバイナリデータを生成する仕組みを導入しました。
このアプローチにより、APIの冪等性を実現しました。同じパラメータでリクエストされた場合、生成されるバイナリデータは常に同じ内容になるため、競合を防ぎつつ一貫性のある応答を返すことが可能になりました。
まとめ
APIの設計において、「存在しなければ生成」「同じパラメータで再度アクセスされた場合には初回の結果を返す」というシンプルな要件でも、排他制御が不十分だと予期せぬ問題が発生することがあります。本記事では、AWS Lambdaを利用したAPIでの問題事例をもとに、発生原因と解決策を考察しました。
近年、AWS Lambdaなどのサーバーレス環境を活用することで、システムを手軽に動作させることが可能になりました。しかし、その一方で、排他制御や一意性の確保といった課題に対しては注意が必要です。これらの問題は、適切なツールを活用したり、冪等性を持つ生成ロジックを実装することで効果的に回避できます。
本記事が、API設計や排他制御の課題解決に役立つヒントとなれば幸いです。