9

簡単に言えば、私はシステムの一部を書き直しており、AWS SimpleDB にいくつかのヒットカウンターを保存する方法を探しています。

SimpleDB に慣れていない方のために説明すると、カウンターの格納に関する (主な) 問題は、クラウドの伝搬遅延がしばしば 1 秒を超えることです。私たちのアプリケーションは現在、毎秒最大 1,500 ヒットを取得しています。これらのすべてのヒットが同じキーにマッピングされるわけではありませんが、おおよその数字は、1 秒あたり約 5 ~ 10 回のキー更新です。これは、従来の更新メカニズム (読み取り、インクリメント、ストア) を使用すると、意図せずにかなりの数のヒットを削除してしまうことを意味します。

考えられる解決策の 1 つは、カウンターを memcache に保持し、cron タスクを使用してデータをプッシュすることです。これに関する大きな問題は、それが「正しい」方法ではないということです。Memcache は実際には永続ストレージに使用すべきではありません...結局のところ、これはキャッシュ レイヤーです。さらに、プッシュを行うときに問題が発生し、正しい要素を削除していることを確認し、それらを削除するときに競合が発生しないことを期待します (これは非常に可能性が高いです)。

もう 1 つの潜在的な解決策は、ローカル SQL データベースを維持し、そこにカウンターを書き込み、SimpleDB を多くのリクエストごとに帯域外で更新するか、cron タスクを実行してデータをプッシュすることです。これにより、タイムスタンプを含めて SimpleDB プッシュの境界を簡単に設定できるため、同期の問題が解決されます。もちろん、他にも問題はあります。これはかなりの量のハッキングで機能する可能性がありますが、最も洗練されたソリューションとは思えません。

誰かが自分の経験で同様の問題に遭遇したことがありますか、または新しいアプローチがありますか? たとえ完全に洗い流されていなくても、アドバイスやアイデアをいただければ幸いです。私はしばらくこれについて考えてきましたが、いくつかの新しい視点を使用することができました.

4

6 に答える 6

20

既存の SimpleDB API は、分散カウンターになるのに自然に適しているわけではありません。しかし、それは確かに行うことができます。

SimpleDB 内で厳密に動作させるには、2 つの方法があります。クリーンアップに cron ジョブなどを必要とする簡単な方法。または、それが進むにつれてきれいになる、はるかに複雑なテクニック。

簡単な方法

簡単な方法は、「ヒット」ごとに異なるアイテムを作成することです。キーである単一の属性を使用します。カウントを使用してドメインをすばやく簡単にポンピングします。カウントをフェッチする必要がある場合 (おそらく、はるかに少ない頻度で)、クエリを発行する必要があります。

SELECT count(*) FROM domain WHERE key='myKey'

もちろん、これによりドメインが無制限に成長し、クエリの実行に時間がかかるようになります。解決策は、キーごとにこれまでに収集されたすべてのカウントをロールアップする要約レコードです。これは、キー {summary='myKey'} の属性と、ミリ秒単位の粒度を持つ「最終更新」タイムスタンプを持つ単なるアイテムです。これには、「ヒット」アイテムに「timestamp」属性を追加することも必要です。要約レコードは同じドメインにある必要はありません。実際、セットアップによっては、別のドメインに保持するのが最適な場合があります。どちらの方法でも、キーを itemName として使用し、SELECT を実行する代わりに GetAttributes を使用できます。

カウントの取得は 2 段階のプロセスです。要約レコードを取得し、要約レコードの「最終更新」時刻よりも厳密に大きい「タイムスタンプ」を照会して、2 つのカウントを合計する必要があります。

SELECT count(*) FROM domain WHERE key='myKey' AND timestamp > '...'

また、要約レコードを定期的に更新する方法も必要になります。これは、スケジュールに従って (1 時間ごとに) 実行することも、他の基準に基づいて動的に実行することもできます (たとえば、クエリが複数のページを返すたびに通常の処理中に実行するなど)。サマリー レコードを更新するときは、結果整合性ウィンドウを過ぎた十分に過去の時間に基づいていることを確認してください。1分は安全以上です。

このソリューションは同時更新に直面しても機能します。これは、多数の要約レコードが同時に書き込まれたとしても、カウントと「最終更新」属性がそれぞれと一致するため、それらはすべて正しく、どちらが勝っても正しいままであるためです。他の。

これは、ヒット レコードで要約レコードを保持している場合でも、複数のドメイン間でうまく機能します。すべてのドメインから同時に要約レコードを取得し、すべてのドメインにクエリを並行して発行できます。これを行う理由は、1 つのドメインから取得できるよりも高いスループットがキーに必要な場合です。

これはキャッシングでうまく機能します。キャッシュに障害が発生した場合、信頼できるバックアップがあります。

誰かが戻って、古い「タイムスタンプ」値​​を持つレコードを編集/削除/追加したい時が来るでしょう。その時点で (そのドメインの) サマリー レコードを更新する必要があります。そうしないと、そのサマリーを再計算するまでカウントがオフになります。

これにより、一貫性ウィンドウ内で現在表示可能なデータと同期しているカウントが得られます。これは、ミリ秒まで正確なカウントを提供しません。

ハードウェイ

もう 1 つの方法は、通常の読み取り - インクリメント - ストア メカニズムを実行するだけでなく、値とともにバージョン番号を含む複合値を書き込むことです。使用するバージョン番号は、更新する値のバージョン番号よりも 1 大きくなります。

get(key) は属性 value="Ver015 Count089" を返します

ここでは、バージョン 15 として保存された 89 のカウントを取得します。更新を行うときは、次のように値を書き込みます。

put(key, value="Ver016 Count090")

以前の値は削除され、lamport クロックを連想させる更新の監査証跡が作成されます。

これには、いくつかの追加作業が必要です。

  1. GET を実行するたびに競合を特定して解決する機能
  2. 単純なバージョン番号は機能しません。少なくともミリ秒単位の解像度のタイムスタンプと、場合によってはプロセス ID も含める必要があります。
  3. 実際には、競合をより簡単に解決するために、値に現在のバージョン番号と更新の基になっている値のバージョン番号を含める必要があります。
  4. 1 つの項目に無限の監査証跡を保持することはできないため、古い値に対して削除を発行する必要があります。

この手法で得られるものは、分岐更新のツリーのようなものです。1 つの値があり、突然複数の更新が発生し、同じ古​​い値に基づいて一連の更新が行われますが、それらは互いに認識していません。

GET 時に競合を解決すると言うとき、アイテムを読み取って値が次のようになる場合を意味します。

      11 --- 12
     /
10 --- 11
     \
       11

実際の値が 14 であることを把握できる必要があります。更新する値のバージョンを新しい値ごとに含めると、これが可能になります。

ロケット科学であってはならない

単純なカウンターだけが必要な場合:これはやり過ぎです。単純なカウンターを作るのはロケット科学であってはなりません。これが、SimpleDB が単純なカウンターを作成するための最良の選択ではない可能性がある理由です。

これが唯一の方法ではありませんが、実際にロックする代わりに SimpleDB ソリューションを実装する場合は、これらのほとんどを行う必要があります。

誤解しないでください。ロックがなく、このカウンターを同時に使用できるプロセス数の制限が約 100 であるため、私は実際にこの方法が好きです (アイテムの属性数の制限のため)。また、いくつかの変更を加えることで、100 を超えることもできます。

ノート

しかし、これらすべての実装の詳細が隠されていて、increment(key) を呼び出すだけでよい場合は、まったく複雑ではありません。SimpleDB では、クライアント ライブラリが複雑なものをシンプルにするための鍵となります。しかし、現在、この機能を実装する公に利用可能なライブラリはありません (私の知る限り)。

于 2009-09-30T15:04:49.523 に答える
15

この問題を再考する人にとっては、Amazon は条件付きプットのサポートを追加したばかりで、カウンターの実装がはるかに簡単になります。

ここで、カウンターを実装するには、GetAttributes を呼び出し、カウントをインクリメントしてから、期待値を正しく設定して PutAttributes を呼び出すだけです。Amazon がエラーで応答した場合は、ConditionalCheckFailed操作全体を再試行してください。

PutAttributes 呼び出しごとに期待値を 1 つしか持てないことに注意してください。そのため、1 つの行に複数のカウンターを配置する場合は、バージョン属性を使用します。

疑似コード:

begin
  attributes = SimpleDB.GetAttributes
  initial_version = attributes[:version]
  attributes[:counter1] += 3
  attributes[:counter2] += 7
  attributes[:version] += 1
  SimpleDB.PutAttributes(attributes, :expected => {:version => initial_version})
rescue ConditionalCheckFailed
  retry
end
于 2010-03-02T22:59:44.940 に答える
2

あなたはすでに答えを受け入れているようですが、これは斬新なアプローチとして数えられるかもしれません。

ウェブアプリを構築している場合は、GoogleのAnalytics製品を使用してページの表示回数を追跡し(ページからドメインアイテムへのマッピングが適合する場合)、AnalyticsAPIを使用してそのデータをアイテム自体に定期的にプッシュできます。

私はこれを詳細に考えていなかったので、穴があるかもしれません。この地域での経験を踏まえると、このアプローチに関するフィードバックに非常に興味があります。

ありがとうスコット

于 2009-10-14T23:55:15.490 に答える
2

私がこれにどのように対処したかに興味がある人のために...(わずかにJava固有)

各サーブレット インスタンスで EhCache を使用することになりました。UUID をキーとして使用し、Java AtomicInteger を値として使用しました。スレッドは定期的にキャッシュを反復処理し、行を simpledb temp stats ドメインにプッシュし、キーを含む行を無効化ドメインに書き込みます (キーが既に存在する場合、サイレントに失敗します)。また、スレッドはカウンターを前の値でデクリメントし、更新中にヒットを見逃さないようにします。別のスレッドが simpledb 無効化ドメインに ping を実行し、一時ドメイン (ec2 インスタンスを使用しているため、各キーに複数の行があります) の統計をロールアップし、実際の統計ドメインにプッシュします。

少し負荷テストを行ったところ、うまくスケーリングするようです。ローカルでは、ロード テスターが壊れる前に 1 秒あたり約 500 ヒットを処理できました (サーブレットではありません)。

于 2009-10-15T16:57:16.663 に答える
1

ファインマンズバスタードへの回答:

大量のイベントを保存する場合は、kafkaaws kinesisなどの分散コミット ログ システムを使用することをお勧めします。イベントのストリームを安価でシンプルに消費できます (Kinesis の価格は、1 秒あたり 1K イベントで月額 25 ドルです)。消費者を実装するだけで (任意の言語を使用して)、前のチェックポイントからすべてのイベントを一括で読み取り、メモリ内のカウンターを集約します。次に、データを永続ストレージ (dynamodb または mysql) にフラッシュし、チェックポイントをコミットします。

イベントは nginx log を使用して単純にログに記録し、 fluentd を使用してkafka /kinesis に転送できます。これは非常に安価で、パフォーマンスが高く、シンプルなソリューションです。

于 2015-01-26T21:15:42.177 に答える
0

同様のニーズ/課題もありました。

Google アナリティクスと count.ly を使用して調べました。後者はコストが高すぎて価値がないように思われました (さらに、セッションの定義が多少混乱しています)。GA を使いたかったのですが、彼らのライブラリといくつかのサードパーティのライブラリ (gadotnet と、おそらく codeproject のもう 1 つ) を使用して 2 日間を費やしました。残念ながら、API が成功を報告した場合でも、通常のダッシュボードには表示されず、GA リアルタイム セクションにのみ表示されました。おそらく何か間違ったことをしていたのでしょうが、ga の時間予算を超えてしまいました。

以前のコメンターが述べたように、条件付き更新を使用して更新する既存の simpledb カウンターが既にありました。これはうまく機能しますが、カウントが失われる競合や同時性がある場合に問題が生じます (たとえば、最新のカウンターは、バックアップ システムに対して、3 か月間で数百万のカウントを失いました)。

この質問の回答に似た新しいソリューションを実装しましたが、はるかに単純です。

カウンターを分割/分割しました。カウンターを作成するときは、予想される同時更新の数の関数であるシャードの数を指定します。これにより、いくつかのサブカウンターが作成され、それぞれが属性として開始されたシャードカウントを持ちます:

COUNTER (w/5shards) 作成: shard0 { numshards = 5 } (情報のみ) shard1 { count = 0, numshards = 5, timestamp = 0 } shard2 { count = 0, numshards = 5, timestamp = 0 } shard3 { count = 0、numshards = 5、timestamp = 0 } shard4 { count = 0、numshards = 5、timestamp = 0 } shard5 { count = 0、numshards = 5、timestamp = 0 }

シャード書き込み シャード数がわかれば、ランダムにシャードを選択して、条件付きで書き込みを試みます。競合が原因で失敗した場合は、別のシャードを選択して再試行してください。シャード数がわからない場合は、シャードの数に関係なく、存在するルート シャードから取得します。カウンターごとに複数の書き込みをサポートするため、競合の問題が軽減されます。

シャード読み取り シャード数がわかっている場合は、すべてのシャードを読み取り、それらを合計します。シャード数がわからない場合は、ルート シャードから取得し、すべてを読み取って合計します。

更新の伝播が遅いため、読み取り中にカウントを見逃す可能性がありますが、後で取得する必要があります。これは私たちのニーズには十分ですが、これをさらに制御したい場合は、読み取り時に最後のタイムスタンプが期待どおりであることを確認して再試行できます。

于 2014-07-18T00:07:49.260 に答える