redisに保存されたデータの自動圧縮

問題は、ピーク時にネットワークインターフェイスが送信されたデータ量に対応できないことです。

利用可能なソリューションオプションのうち、保存されたデータの圧縮が選択されました

tl; dr:メモリーを50%以上節約し、ネットワークを50%節約します。 これは、 redisに送信する前にデータを自動的に圧縮するpredis プラグインです。



ご存じのとおり、redisはバイナリセーフテキストプロトコルを使用し、データは元の形式で保存されます。 このアプリケーションでは、redisはシリアル化されたphpオブジェクトとhtmlコードを格納します。これは、圧縮の概念に非常に適しています。データは同種であり、文字の繰り返しグループが多数含まれています。



解決策を見つける過程で、グループ内で議論が見つかりました -開発者はプロトコルに圧縮を追加する予定はありません...それで、私たちはそれを自分で行います。



したがって、概念:redisに保存するために転送されるデータのサイズがNバイトを超える場合、保存する前にgzipを使用してデータを圧縮します。 redisからデータを受信する場合、データの最初のバイトでgzipヘッダーの存在を確認し、見つかった場合はアプリケーションに渡す前にデータを解凍します。

predisを使用してredisを操作するため、プラグインはredisで作成されています。



小さく始めて、圧縮を扱うためのメカニズムCompressorInterface



するかどうかを決定するためのメソッド、圧縮、それ自体をアンパックおよびアンパックするかどうかを決定しましょう。 クラスコンストラクターは、圧縮が有効になっている場所から始まるしきい値をバイト単位で受け取ります。 このインターフェイスを使用すると、チューブWinRARなど、お気に入りの圧縮アルゴリズムを自分で実装できます。



入力データのサイズをチェックするロジックは、各実装で重複しないようにAbstractCompressor



クラスで実行されます。

AbstractCompressor
 abstract class AbstractCompressor implements CompressorInterface { const BYTE_CHARSET = 'US-ASCII'; protected $threshold; public function __construct(int $threshold) { $this->threshold = $threshold; } public function shouldCompress($data): bool { if (!\is_string($data)) { return false; } return \mb_strlen($data, self::BYTE_CHARSET) > $this->threshold; } }
      
      





mbstring.func_overload



およびシングルバイトエンコーディングで起こりうる問題を克服するためにmb_strlen



を使用して、データからエンコーディングを自動的に決定する試みを防ぎます。



\x1f\x8b\x08"



等しいマジックバイトを持つ圧縮のgzencodeベースの実装作成しています(これにより、文字列を展開する必要があることがわかります)。

Gzipcompressor
 class GzipCompressor extends AbstractCompressor { public function compress(string $data): string { $compressed = @\gzencode($data); if ($compressed === false) { throw new CompressorException('Compression failed'); } return $compressed; } public function isCompressed($data): bool { if (!\is_string($data)) { return false; } return 0 === \mb_strpos($data, "\x1f" . "\x8b" . "\x08", 0, self::BYTE_CHARSET); } public function decompress(string $data): string { $decompressed = @\gzdecode($data); if ($decompressed === false) { throw new CompressorException('Decompression failed'); } return $decompressed; } }
      
      







素敵なボーナス-RedisDesktopManagerを使用すると、表示時にgzipが自動的に解凍されます。 プラグインの結果を確認しようとしましたが、この機能が見つかるまで、プラグインが機能しないと思いました:)



predisには、リポジトリに転送する前にコマンドの引数を変更できるProcessorメカニズムがあり、これを使用します。 ところで、このメカニズムに基づいて、標準のpredisパッケージには、すべてのキーに文字列を動的に追加できるプレフィックスがあります。



 class CompressProcessor implements ProcessorInterface { private $compressor; public function __construct(CompressorInterface $compressor) { $this->compressor = $compressor; } public function process(CommandInterface $command) { if ($command instanceof CompressibleCommandInterface) { $command->setCompressor($this->compressor); if ($command instanceof ArgumentsCompressibleCommandInterface) { $arguments = $command->compressArguments($command->getArguments()); $command->setRawArguments($arguments); } } } }
      
      





プロセッサーは、インターフェースの1つを実装するコマンドを探しています。

1. CompressibleCommandInterface



コマンドが圧縮をサポートしていることを示し、チームがCompressorInterface



の実装を取得する方法を説明します。

2. ArgumentsCompressibleCommandInterface



最初のインターフェイスの継承者は、コマンドが引数圧縮をサポートすることを示します。



論理がおかしいことが判明したと思いませんか? 引数の圧縮が明示的に行われ、プロセッサによって呼び出されるのはなぜですか?しかし、答えを展開するためのロジックはそうではありませんか? predis( \Predis\Profile\RedisProfile::createCommand()



)を使用するコマンドを作成するためのコードを見てください:



 public function createCommand($commandID, array $arguments = array()) { //       $command = new $commandClass(); $command->setArguments($arguments); if (isset($this->processor)) { $this->processor->process($command); } return $command; }
      
      





このロジックのため、いくつかの問題があります。

1つ目は、引数を既に受け取った後にのみ、プロセッサがコマンドに影響を与えることができるということです。 これにより、外部への依存関係を渡すことができません(この例ではGzipCompressorですが、predisを使用して外部から初期化する必要がある他のメカニズム、たとえば、暗号化システムやデータに署名するためのメカニズム)も可能です。 このため、引数を圧縮する方法を備えたインターフェイスが登場しました。

2番目の問題は、プロセッサがサーバー応答コマンドの処理に影響を与えないことです。 このため、アンパックロジックはCommandInterface::parseResponse()



に置かれますが、これは完全に正しいわけではありません。



一緒に、これらの2つの問題は、展開メカニズムがチーム内に格納され、展開ロジック自体が明示的ではないという事実につながりました。 predisのプロセッサは、プリプロセッサ(引数をサーバーに送信する前に変換するため)とポストプロセッサー(サーバーからの応答を変換するため)の2つのステージに分割する必要があると思います。 これらの考えをpredis開発者と共有しました。



典型的なセットコマンドコード
 use CompressibleCommandTrait; use CompressArgumentsHelperTrait; public function compressArguments(array $arguments): array { $this->compressArgument($arguments, 1); return $arguments; }
      
      



一般的なGetコマンドコード
 use CompressibleCommandTrait; public function parseResponse($data) { if (!$this->compressor->isCompressed($data)) { return $data; } return $this->compressor->decompress($data); }
      
      





クラスターインスタンスのいずれかのグラフでのプラグインのアクティブ化の結果について:







インストール方法と使用開始方法:
 composer require b1rdex/predis-compressible
      
      





 use B1rdex\PredisCompressible\CompressProcessor; use B1rdex\PredisCompressible\Compressor\GzipCompressor; use B1rdex\PredisCompressible\Command\StringGet; use B1rdex\PredisCompressible\Command\StringSet; use B1rdex\PredisCompressible\Command\StringSetExpire; use B1rdex\PredisCompressible\Command\StringSetPreserve; use Predis\Client; use Predis\Configuration\OptionsInterface; use Predis\Profile\Factory; use Predis\Profile\RedisProfile; // strings with length > 2048 bytes will be compressed $compressor = new GzipCompressor(2048); $client = new Client([], [ 'profile' => function (OptionsInterface $options) use ($compressor) { $profile = Factory::getDefault(); if ($profile instanceof RedisProfile) { $processor = new CompressProcessor($compressor); $profile->setProcessor($processor); $profile->defineCommand('SET', StringSet::class); $profile->defineCommand('SETEX', StringSetExpire::class); $profile->defineCommand('SETNX', StringSetPreserve::class); $profile->defineCommand('GET', StringGet::class); } return $profile; }, ]);
      
      





UpdGitHubのプラグインへのリンク



All Articles