YiiフレームワークでのMySQLシャーディング

そもそも、私たちのプロジェクトは開発の初期段階にあり、その立ち上げは11月1日に予定されています。 また、時期尚早な最適化に関するあらゆる可能性のある批判を即座に遮断するために、チームは突然の負荷ショック(1000から50,000など)に対処できるアプリケーションの開発を任されたと言います。 この点で、ハードウェアのおかげでシステムのパフォーマンスを簡単かつ迅速に高めることができる拡張性の高いアーキテクチャを構築することが決定されました(スケールアウトの原則に基づいて)。













プラットフォーム


プラットフォームを迅速に選択しました-Yiiフレームワークに基づいてプロジェクトを開発することが決定されました。 簡単に言えば、一年半前に私はよく知られた開発者のレビューによって研究することに触発され、このプロジェクトを開発するためのプラットフォームとしてのyiiの選択は、長いグーグル、テスト、および比較特性( www.yiiframework.com/performanceなど )のおかげで行われたと言えます



Yii ActiveRecordをORMとして使用することを拒否しないことを決定しました。これは、開発の観点から非常に便利であり、その後直接ボトルネックを修正して直接データベースクエリを使用する可能性があるためです。 初期の段階では、データストレージ構造自体がボトルネックのように見えました。これは、MySQL(データストレージ用に選択)がすぐに1つのサーバーでは処理できない非常に大きなクエリストリームを受信することを示唆しているためです。



複製


解決策としてのレプリケーションはすぐに失敗しました。 開発の初期段階(最初の年)では、負荷の問題を忘れることができますが、将来的には、常にロードされるウィザードの新たに発生する問題を解決する必要があります(ところで、これらの問題は、マスターのプロキシ、スレーブのマスターとしてのスレーブなど、非常に泥だらけの方法で解決されますなど)。 はい。また、データベース内の関連データを維持し、ログをドラッグする管理者の継続的な作業は、人的要因が取り消されていないため、悲惨な結果を招く可能性があります。



シャーディング


選択は、mysqlの水平スケーリング(シャーディング)にかかっていました。 このデータ配置の原則の詳細については説明しません。1つのエンティティのインスタンス(1つのテーブルの行)が異なるサーバーに分散していると言えます( ここで詳しく説明します )。 シャードに分割されたテーブルは、「最も頻繁にアクセスされる」原則、つまり、多くのクエリが存在するテーブル(タイプに関係なく)に従って選択されます。



今回のケースでは、いくつかの基本的なテーブルが選択され、サーバー間で水平に分散することにしました。 数百万人が予想されるユーザーのテーブルの例を紹介します。 そのため、ユーザーテーブル(tbUser)が複数のサーバーに分散されており、要求されたユーザーごとに、どのサーバーにあるかを知る必要があります。 たとえば、UIDをサーバーの数などで除算する残りの部分を使用するなど、この状況に対する多くの解決策があります。しかし、この場合、新しいサーバーを追加するときに問題が発生し、テーブルまたは他の何かをブロックする追加のユーザー転送スクリプトを記述する必要があります。 したがって、ユーザーが保存されているサーバーを特定するために、タイプUID => SERVER_IDのサードパーティデータストレージのオプションを選択し、ストレージ自体としてRedisを使用しました。



Rediskaを構成する


Redisと対話するために、 Rediskaライブラリがインストールされました。 Yiiと統合することは難しくありません。たとえば、ライブラリをダウンロードして、/ protected / vendor / rediskaに配置し、Rediska.phpのヘッダーに次のように記述する必要があります。



<?php spl_autoload_unregister(array('YiiBase','autoload')); require_once dirname(__FILE__) . '/Rediska/Autoloader.php'; Rediska_Autoloader::register(); spl_autoload_register(array('YiiBase', 'autoload')); class Rediska extends Rediska_Options
      
      







このソリューションは、Yiiの自動ロードを一時的に無効にし、Radiskovskyをオンにします(それ以外の場合、自動ロードは単に競合します)。



ディレクトリ/ protected / componentsで、このライブラリを使用して独自のコンポーネントを作成しました:



 <?php require_once dirname(__FILE__).'/../vendor/rediska/Rediska.php'; class RediskaConnection { public $options = array(); private $_rediska; public function init() { $this->_rediska = new Rediska($this->options); } public function getConnection() { return $this->_rediska; } }
      
      







...そしてmain.phpにプラグインしました



 'RediskaConnection'=>array( 'class'=>'application.components.RediskaConnection', 'options'=>array( 'servers' => array( 'server1' => array( 'host'=>'192.168.0.131', 'port'=>'6379', 'timeout'=>'3', // in seconds 'readTimeout'=>'3', // in seconds ), ), 'serializerAdapter'=>'json', ), ),
      
      







Rediskaの設定はすべてで説明されています。 サイト。 新しいコンポーネントを構成したら、get / setsで確認するだけで十分です。



 Yii::app()->RediskaConnection->getConnection()->set('key', 'value'); echo Yii::app()->RediskaConnection->getConnection()->get('key');
      
      







データベースの準備


Redisでの小さなトラブルがすべて終了したら、同じデータベースを使用して2つのMySQLインスタンスを作成しました。



  'shard1' => array( 'class' => 'CDbConnection', 'connectionString' => 'mysql:host=192.168.0.131;dbname=dbshard', 'username' => 'dbuser', 'password' => 'dbpass', 'autoConnect' => false, 'charset' => 'utf8', ), 'shard2' => array( 'class' => 'CDbConnection', 'connectionString' => 'mysql:host=192.168.0.61;dbname=dbshard', 'username' => 'dbuser', 'password' => 'dbpass', 'autoConnect' => false, 'charset' => 'utf8', ),
      
      







シャーディングのソフトウェア実装


これらのベースに、後で混乱しないように、シャーディングの対象となるテーブル(tbUserを含む)のみを配置しました。 ここで、受信したサーバーにデータを配信するメカニズムを検討する必要がありました。 これを行うために、CSActiveedRecordクラスを作成することが決定されました。このクラスは、基本的にCActiveRecordを拡張し、必要なメソッドをオーバーライドします。 動作の原理は次のとおりです:beforeSave()を再定義し、新しいレコードを保存するためのサーバーを定義し(UIDをSERVER_QUANTITYで除算した後の同じ余り)、カウント値をRedisに保存し、保存するためのレコードIDは、 Redisインクリメント()を支援します(これにより、すべてのデータベースでエンドツーエンドのID番号付けが可能になります)。 findByPk()を再定義しました。これにより、選択が行われるサーバー番号も取得されます。 すべてのコードは次のとおりです。



 <?php /** * Sharded active record */ class CShardedActiveRecord extends CActiveRecord { /** * Used in find by PK and * @var integer */ private $_pk = null; /** * Used connection * @var CDbConnection */ private $_connection = null; /** * @see db/ar/CActiveRecord#getDbConnection() */ public function getDbConnection() { if (!is_null($this->_connection)) return $this->_connection; if (is_null($this->_pk)) { $serverName = Yii::app()->params->servers['serverNames'][0]; } else { $serverId = $this->getServerId($this->_pk); $serverName =empty(Yii::app()->params->servers['serverNames'][$serverId]) ? Yii::app()->params->servers['serverNames'][0] : Yii::app()->params->servers['serverNames'][$serverId]; } $this->_connection = Yii::app()->{$serverName}; return $this->_connection; } private function removeConnection() { $this->_connection = null; } private function getRedisKey($key) { return $this->tableName() . '_' . $key; } /** * @return server id or false, for null $pk */ private function getServerId($pk) { if (is_null($pk)) return false; $serverId = Yii::app()->RediskaConnection->getConnection()->get($this->getRedisKey($pk)); return $serverId; } public function findByPk($pk, $condition = '', $params = array()) { if (!is_integer($pk)) throw new Exception ('primary key must be integer'); $this->_pk = $pk; $this->removeConnection(); return parent::findByPk($pk, $condition, $params); } /** * Set unique id for new record * @return boolean */ protected function beforeSave() { if (!parent::beforeSave()) return false; if ($this->getIsNewRecord()) { $key = $this->tableName().'_counter'; $this->id = $this->_pk = Yii::app()->RediskaConnection->getConnection()->increment($key); $serverId = $this->id % Yii::app()->params->servers['serverCount']; $result = Yii::app()->RediskaConnection->getConnection()->set($this->getRedisKey($this->id), $serverId); $this->removeConnection(); } return true; } }
      
      







まとめ


したがって、シャーディングテーブル用の独自のActiveRecordが取得され、CActiveRecordからではなくCShardedActiveRecordから機能を継承するモデルのみを作成する必要がありました。 オーバーライドされたメソッドで十分です。 データベースからのサンプリングのほとんどはPKを使用して行われ、データベース検索はSphinxを使用して実装されます(Sphinxには、DataProviderでのみ動作するYiiに統合されたウィジェットを使用できるように独自のDataProviderを作成しました)。 さらに、テーブルへの単純な挿入と選択を通じて、コンポーネントの動作をテストし、作業の結果に満足しました。



All Articles