memcached / MemcacheDBのデータ構造。 パート1

多くの場合、データをmemcachedまたはMemcacheDBに保存する必要があります。 これは比較的単純なデータ、たとえばデータベースからのキャッシュされたサンプルである場合があり、いくつかのプロセスから同時に更新されるより複雑なデータ構造を保存および処理し、高速なデータ読み取りなどを行う必要がある場合があります そのようなデータ構造の実装は、memcachedのget



/ set



コマンドの組み合わせにはもはや適合しません。 この記事では、いくつかのデータ構造をmemcachedに保存する方法を、コード例と基本的なアイデアとともに説明します。



この記事のMemcachedとMemcacheDBは、共通のアクセスインターフェイスを持ち、ほとんどのデータ構造のロジックが同じであるため、一緒に検討されます。以下、単に「memcached」と呼びます。 なぜmemcachedにデータ構造を保存する必要があるのですか? ほとんどの場合、異なるプロセス、異なるサーバーなどからのデータへの分散アクセス用。 また、MemcacheDBが提供するインターフェイスでデータストレージの問題を解決できる場合があり、DBMSを使用する必要がない場合もあります。



プロジェクトは、未割り当てのケース(単一サーバー内で動作する)向けに最初に開発される場合がありますが、将来のスケーリングの必要性を前提として、スケーリングが容易なこのようなアルゴリズムとデータ構造をすぐに使用することをお勧めします。 たとえば、データが単純にプロセスメモリに格納される場合でも、それらにアクセスするためのインターフェイスがmemcachedのセマンティクスを繰り返している場合、分散スケーラブルアーキテクチャに切り替えると、内部ストレージへの呼び出しをサーバー(またはサーバークラスター)memcachedへの呼び出しに置き換えるだけで十分です。



合計で、6つのデータ構造が考慮されます。

  1. ブロッキング;
  2. カウンター
  3. 訪問者カウンター;
  4. イベントログ
  5. 配列;
  6. テーブル。




この部分の最初から3番目まで、次の部分では4番目から6番目まで。



サンプルコードはPythonで記述され、以下を使用します。

memcachedアクセスインターフェイス:



 class Memcache(object): def get(self, key): """   . @param key:  @type key: C{str} @return:   @raises KeyError:   """ def add(self, key, value, expire=0): """   ,     . @param key:  @type key: C{str} @param value:   @param expire: " "    (0 — ) @type expire: C{int} @raises KeyError:    """ def incr(self, key, value=1): """     . @param key:  @type key: C{str} @param value:  @type value: C{int} @return:    ( ) @rtype: C{int} @raises KeyError:    """ def append(self, key, value): """      . @param key:  @type key: C{str} @param value:  @raises KeyError:    """ def delete(self, key): """  . @param key:  @type key: C{str} """
      
      





すべてのデータ構造は、次の形式の基本クラスの継承者になります。



 class MemcacheObject(object): def __init__(self, mc): """ . @param mc:  memcached @type mc: L{Memcache} """ self.mc = mc
      
      







ロックする



挑戦する



ロック(この状況ではミューテックス、またはむしろスピンロック)はデータ構造ではありませんが、memcachedで複雑なデータ構造を構築する際の「ブリック」として使用できます。 ロックは特定のキーへの同時アクセスを除外するために使用され、ロック機能はmemcached APIには存在しないため、他の操作を使用してエミュレートされます。



そのため、ロックはその名前によって決定され、分散した方法で動作します(つまり、memcachedにアクセスできるプロセスから)、自動的に停止します(ロックを設定したプロセスが、たとえば、異常終了のためにロックを削除できない場合)。



ロック操作:







解決策



 class MCCounter(MemcacheObject): def __init__(self, mc, name): """ . @param name:   @type name: C{str} """ super(MCCounter, self).__init__(mc) self.key = 'counter' + name def increment(self, value=1): """      . @param value:  @type value: C{int} """ while True: try: self.mc.incr(self.key, value) return except KeyError: pass try: self.mc.add(self.key, value, 0) return except KeyError: pass def value(self): """   . @return:    @rtype: C{int} """ try: return self.mc.get(self.key) except KeyError: return 0
      
      







議論



ロックの操作の正確さは、「追加」操作に基づいています。これにより、1つのプロセスが「最初に「キー値」を設定できる」ことが保証され、他のすべてのプロセスはエラーを受け取ります。 上記のクラスは、たとえばPython with-handlerを使用して 、より便利にラップできます。 ロックの問題については、キャッシュの同時再構築に関する投稿で詳しく説明しました。



アトミックカウンター



挑戦する



異なるサーバーに分散して発生するイベントの数を計算し、現在のカウンター値を取得する必要があります。 同時に、カウンターはイベントを「失い」、「余分な」値を追加するべきではありません。



データ型:整数。



初期値:ゼロ。



操作:







解決策



 def time(): """         (, UNIX Epoch). @return:     @rtype: C{int} """ class MCVisitorCounter(MemcacheObject): def __init__(self, mc, name, T): """ . @param name:   @type name: C{str} @param T:  ,  @type T: C{int} """ super(MCVisitorCounter, self).__init__(mc) self.keys = ('counter' + name + '_0', 'counter' + name + '_1') def _active(self): """    . @return: 0  1 """ return 0 if (time() % (2*T)) < T else 1 def increment(self): """   . """ active = self._active() while True: try: self.mc.incr(self.keys[1-active], 1) return except KeyError: pass try: self.mc.add(self.keys[1-active], 1, 2*T) #  43 return except KeyError: pass def value(self): """   . @return:    @rtype: C{int} """ try: return self.mc.get(self.keys[self._active()]) except KeyError: return 0
      
      







議論



カウンターの実装は非常に明白であり、最も難しい瞬間は、 increment



メソッドの「永遠の」サイクルです。 競争力のあるアクセス条件でカウンター値を正しく初期化する必要があります。 キーが見つからないというエラーでincr



操作が失敗した場合、カウンターキーを作成する必要がありますが、複数のプロセスが同時にこのコードを実行でき、そのうちの1つはadd



に成功し、2つ目はincr



を使用してエラーを受け取り、既に作成されたカウンターをインクリメントします ループは不可能です incr



操作またはadd



操作のいずれかが成功add



必要があります。memcachedでキーを作成した後、キーの全期間にわたってincr



操作が成功します。



memcachedからのカウンターを持つキーが失われた場合はどうですか? キーが消える場合、次の2つの状況があります。

  1. memcachedには、他のキーを収容するための十分なメモリがありません。
  2. memcachedプロセスがクラッシュしました(サーバーがクラッシュしました)。




(MemcacheDB、レプリケーションの構成などの場合、サーバーのクラッシュによってキーが失われることはありません。)最初のケースでは、memcachedを適切に維持するだけで済みます。カウンターに十分なメモリが必要です。そうしないと、値が失われます。この状況では受け入れられません(ただし、たとえば、memcachedにキャッシュを保存するのは完全に正常です。 2番目のケースは常に可能です。この状況が頻繁に発生する場合、複数のmemcachedサーバーでカウンターを複製できます。



来客カウンター



挑戦する



Tの期間にわたるサイトへのユニークアクセス数を計算する必要があります。たとえば、Tは5分に等しい場合があります。 特定の訪問者のサイトへの最後の訪問が-T分以上前またはそれ以下(つまり、T期間中に一意であるかどうか)だったと言えます。



カウンター操作:







解決策



 def time(): """         (, UNIX Epoch). @return:     @rtype: C{int} """ class MCVisitorCounter(MemcacheObject): def __init__(self, mc, name, T): """ . @param name:   @type name: C{str} @param T:  ,  @type T: C{int} """ super(MCVisitorCounter, self).__init__(mc) self.keys = ('counter' + name + '_0', 'counter' + name + '_1') def _active(self): """    . @return: 0  1 """ return 0 if (time() % (2*T)) < T else 1 def increment(self): """   . """ active = self._active() while True: try: self.mc.incr(self.keys[1-active], 1) return except KeyError: pass try: self.mc.add(self.keys[1-active], 1, 2*T) #  43 return except KeyError: pass def value(self): """   . @return:    @rtype: C{int} """ try: return self.mc.get(self.keys[self._active()]) except KeyError: return 0
      
      







議論







このタスクでmemcachedのカウンターを操作する基本構造は、アトミックカウンターの問題の解決策を繰り返します。 基本的な考え方は、シャドウと現在のカウンターを使用することです:シャドウカウンターは更新を受信(増加)し、現在のカウンターは訪問者数の値を取得するために使用されます(その値はシャドウであったときに累積されました)。 期間Tごとに、現在のカウンタとシャドウカウンタが交換されます。 シャドウカウンターのキーは、2 * T秒の有効期間で作成されます。つまり、その有効期間はシャドウ(および増加)であるT秒であり、値が読み取りに使用されている間のT秒です。 このキーが再びシャドウカウンターになるまでに、memcachedによって破棄する必要があります。 有効期限が切れています。 影と現在のカウンターのアイデアは、たとえば、ゲームに表示されるときのダブルバッファリングに似ています。



_active()



関数に注意する必要があります:現在の時間を秒単位で割った余りを計算することで正確に実装され、たとえば、定期的に(T秒に1回) active



の値を変更するのではなく、 すべてのサーバーの時間が適切な精度で同期されている場合、 _active()



関数の結果はすべてのサーバーで同じになります。これは、このデータ構造の正しい分散機能にとって重要です。



説明したアプローチは、 _active()



関数の値の次の変更と_active()



関数の呼び出しとの間の時間間隔が可能な限り短い場合に、十分な数の訪問者に対してのみ正しく機能します。 つまり、 increment()



が頻繁に呼び出される場合、つまり、T = 3分の期間で1000人の訪問者が多い場合などです。 そうでない場合、キーの作成と破棄は_active()



値が_active()



た瞬間に同期されず、シャドウカウンターに蓄積された訪問者は消え始め、値の蓄積中に破棄されて再作成されます。 この状況では、彼の2 * Tライフタイムは、瞬間0、時間()%(2 * T)関数のTに対して「シフト」されます。 memcachedは秒単位でキーの有効期限を個別に(1秒以下の精度で)考慮するため、キーの有効期間のシフトは1.2、...、T-1秒(シフトなし-0秒)になります。 有効期限の離散性は、キーが25秒で作成された場合、有効期間が10秒の31ミリ秒で、35ミリ秒(または36ミリ秒)で破壊されることを意味します(31ミリ秒ではありません)。 整数秒のシフトを補正するには、例の43行目を次のように修正する必要があります。



 self.mc.add(self.keys[1-active], 1, 2*T - time() % T)
      
      







訪問者のカウンターの値は期間Tの間変化しません。より快適な表示のために、出力時にカウンター値にカウンター値の約5%の数学的な期待値を少し分散して正規分布値を追加できます。



このようなカウンターの少し複雑なバージョン(時間補間付き)が、まったく同じアイデアで、 memcachedの操作とカウンターの原子性に関する投稿で説明されました。



UPD :記事の2番目の部分



All Articles