ロックフリーmemcache API

こんにちは、ハラジテリ!

この投稿は、何時間にもわたるリフレクション、ペーパークリップ、コードスケッチ、そして最終的には実際に稼働しているコードの概要です。

私たちのサイト(および以降は単にサイト)は、ホットデータにMemekesを積極的に使用しています。 memekeshを埋めるコードは非常に長い時間(0.5秒は長い時間)動作し、同時にユーザーリクエストはさらに100の更新手順を開始することができます。 結果は理解できますが、長い間、総負荷のレベルでそれらに気付くことはありませんでした。 リクエストを処理するための時間のバーストを見たときにのみ(負荷の増加により、MySQLにもSLOW_QUERIES_LOGが入りました)、作業が沸騰し始めました。





問題は明らかです



キーが「腐敗」しているか、まだインストールされていない場合、memkeshからキーを要求するシナリオをさらに詳しく考えてみましょう。

問題を理解するために、私は小さな図を描きました:



データ要求のワークフロー



更新ロジックはコードによって制御されます。 アプリケーション自体がFALSEを返したかどうかを知っているので、キーを再生成する必要があります。 図からわかるように、最初のリクエストのデータに「準備」する時間がなかった時点で問題が発生し、すでに同じデータを要求しています。

正確な定義を提供するevilbloodydemon habruiserのおかげで、この状況は「犬パイル効果」と呼ばれています。



解決策



多数のバージョンが提案されています-これを回避する方法。

まず、ロックシステムと更新キューについて考えました。 しかし、このシナリオは恐ろしく遅いです。

次に、コードでデータを再生成できる場合は、それを実行させます。 FALSEに戻すだけです。 そしてすぐに、古いデータを再インストールします。 合計:更新手順は1回起動され、再生成プロセスの終了までアプリケーションに返されるデータは、再生成の間だけ「腐敗」します。

これを行うには、データ自体だけでなく、タイムアウトとキーが無効化される時間もmemkeshに追加する必要があります。 (確かに)2倍の大きさの配列は、実際のmemkeshに入ります。 ドキュメントには、最長のキー保存期間は30日間であると書かれています。 つまり 「ラッパー」データを15日間-忠実度のために1秒だけ入力します。 タイムアウト= 0(つまり、混み合うまで永遠に)のキーにも同じことが当てはまります。 memkeshのデータが15日に1回必要な状況-私は会いませんでした。 これがあなたに起こった場合、何かを変更する必要があります。

また、インクリメントの問題にもすぐに気付きました。 たとえば、すべてのインクリメントキーが「_inc」で終わることに同意する必要がありました。 そして、そのようなキーが見つかると、memeksh自体がインクリメントした必要なデータを取得するだけです。 *このフォークまたはMemcache_Proxy :: get()メソッドを削除しました。



コード



コードは必要な場所に文書化されています:)事前にコードシートをおaびしますが、それ以上カットすることはできません。

class MC { private static $_proxy; // Singleton for our class, extended of native Memcache class private static function _proxy() { if (is_null(self::$_proxy) || self::$_proxy->closed) self::$_proxy = new Memcache_Proxy; return self::$_proxy; } public static function get($key = '') { return self::_proxy()->get($key); } public static function set($key = '', $data = NULL, $flag = FALSE, $timeout = 3600) { return self::_proxy()->set($key, $data, $flag, $timeout); } public static function delete($key = '') { return self::_proxy()->delete($key); } public static function increment($key = '', $increment = 1) { return self::_proxy()->increment($key, $increment); } }
      
      







MSクラスは、memcacheへの接続を明示的に宣言する必要なく、コード全体でmemcacheの1つのインスタンスと通信するために必要です。 このクラスで目的のメソッドを初めて呼び出すときに作成されます。



 class Memcache_Proxy extends Memcache { public $closed = false; public function __construct() { $this->connect(MEMCACHE_HOST, MEMCACHE_PORT, null); $this->closed = false; } function __destruct() { $this->close(); $this->closed = true; } /** * Mirror for $memcache->get() method */ public function get($key = '') { if (empty($key)) return FALSE; $data = parent::get($key); if ($data !== FALSE && $this->_is_valid_cache($data)) { if (!isset($data['_dc_cache'])) $data['_dc_cache'] = NULL; //check lifetime if (time() > $data['_dc_life_end']) { //expired, save the same for a longer time for other connections $this->set($key, $data['_dc_cache'], FALSE, $data['_dc_cache_time']); return FALSE; } else { //still alive return $data['_dc_cache']; } } return FALSE; } /** * Mirror for $memcache->set() method */ public function set($key = '', $data, $flag = FALSE, $timeout = 3600) { if (empty($key)) return FALSE; // Place here "_inc" key check if (is_int($data) || $data === FALSE) parent::delete($key . '_increment'); // Maximum timeout = 15 days - 1 second if ((int)$timeout == 0 || (int)$timeout > 1295999) $timeout = 1295999; return $this->_set($key, $data, $flag, $timeout * 2); } /** * Mirror for $memcache->delete() method */ public function delete($key = '') { if (empty($key)) return FALSE; // Magic for increment. Place here "_inc" key check parent::delete($key . '_increment'); return parent::delete($key); } public function increment($key, $increment = 1) { $inc_value = parent::increment($key . '_increment', $increment); $data = parent::get($key); if ($data === FALSE) return FALSE; if ($this->_is_valid_cache($data)) { if ($inc_value === FALSE) { $inc_value = $data['_dc_cache'] + $increment; parent::set($key . '_increment', $inc_value, FALSE, $data['_dc_cache_time'] * 2); } $time = $data['_dc_life_end'] - time(); if ($time > 0) { $this->_set($key, $inc_value, FALSE, $time); return $inc_value; } } return $inc_value; } private function _set($key = '', $data, $flag = FALSE, $timeout = 3600) { $cache = array('_dc_cache' => $data, '_dc_life_end' => time() + $timeout, '_dc_cache_time' => $timeout); return parent::set($key, $cache, $flag, $timeout); } // Maybe we have pure Memcache data, not our array structure private function _is_valid_cache($value) { return (is_array($value) && isset($value['_dc_life_end']) && isset($value['_dc_cache_time']) && !empty($value['_dc_life_end']) && !empty($value['_dc_cache_time']) ) ? TRUE : FALSE; } }
      
      







使用例



コード、ただのコード。 データが酸っぱい場合は、リクエスターのみにFALSEを返すことで生成を開始し、同じデータを同時に再インストールします。 したがって、最初のプロセスが生成を終了し、MC :: set()を実際のデータで実行するまで、次のリクエスターは古いデータを受け取ります。 その直後に、すべてのプロセスが関連データを受け取ります。



 $data = MC::get('some_key'); if ($data === FALSE) { //     $data = huge_generate_func_call(); MC::set('some_key', $data, FALSE, 3600); }
      
      







つまり 以前と同様にmemkeshを使用し続けます。 memkeshにアクセスするためのラッパーがあった場合、それを修正できますが、アプリケーションコードには何も触れないでください。 ちなみに、これは要件の1つでした。新しいクラスのmemkeshを導入するための最小限のリファクタリングです。



まとめ



タイムスタンプとタイムアウトを保存するオーバーヘッドに目を向けることができ、メモリは安価になりました。

データがデータ生成の時間に等しい時間だけ「悪化」するという事実は致命的ではなく許容できるものではありませんが、同じデータを生成するための新しいフローは作成されません。 CTD!



PS

提案やコメントを歓迎します! スペル-PMで、基本的に-コメントで!



UPD。

同志マイナス-あなたの選択を主張します。 誰もがプーシキンとストラウストラップの才能を持って生まれたわけではありません!



UPD。 2

私たちは短所を扱います:

1。

MCクラスは、コードを変更しないようにするために必要です。 絶対に。 memekeshのラッパーの名前(ある場合)に置き換えます。 そうでない場合、ほとんどのIDEはリファクタリング->クラス名の変更をサポートしています。

2。

クラスMSは静的です。 これは歴史的に起こりました-リファクタリングの最小-主な要件。 Habrのコードをやり直さなかった-主なアイデアはそこに反映されています。



All Articles