背景問題
業務用Redisのプロセスでは、例外読んでタイムアウトがあります。
トラブルシューティング
直接の原因
運用・保守に関するお問い合わせは、スロークエリログをRedisの異常時のノードが、そこに、スロークエリログをRedisのコマンドSADDが1秒を要した実行ことがわかりました。Redisのは、シングルスレッドアプリケーションであるので、しかし、単一のコマンドの実行を阻止することは読んでタイムアウトで、その結果、他のコマンドを待っているキューの原因となります。
深さの調査 - なぜそんなに遅いそれSADD
サッドなぜ遅いですか?Redisのドキュメントの複雑サッド操作を確認するためにチェックすると、注文が時折800W以上100ミリ秒の場合よりも多くなるまでO(1)、機械のドッキングウィンドウのビルドのRedisの実際の使用は、サッドスクリプトを使用してテストされています。(後でテストを参照してください)
Redisの建築環境
ドッカーRedisのアプリケーションを使用することから実行しているマシンにレイジー回線テスト、次のように
ドッキングウィンドウはRedisの#を引く使用redis3.x版本ドッキングウィンドウ-itv〜/ redis.confを実行します。/redis.conf -p 32768:6379 --name myredis5 -dのRedis Redisのサーバー/redis.conf
テストスクリプト
#コーディング= UTF-8import timeimport redisimportランダム R = redis.Redis(ホスト= 'XXXX'、ポート= XXXX、decode_responses = TRUE) K =「key4'tarr = [] ST = time.clock() ST2 = time.clock () r.sadd(K、1)#创建连接也会有耗时Iの範囲内の(1、1600000)用: T1 = time.clock()* 1000 RN = random.randint(千億、20000000000000) r.sadd (K、RN) T2 = time.clock()* 1000 C = T2 - T1 tarr.append(STR(C))C> 100の場合:印刷I、cprint time.clock() S = "\ n" .join Fとして開く(ター)( './ result.txt'、 'W'): f.write(S)
テスト結果
800Wの開始に到着すると100ミリ秒の時折の現象が必要サッド。
分析
私はRedisのデル運用の複雑さを見たとき、多くの情報を照会することはO(n)は、次のようにここでは例のタイムアウトを、より多くの背景を追加します:
スロークエリログ時間:16 00.00分01秒には、コマンドはSADD prefix_20180215で、キーは有効期限があります。
答えは、かなり明らかにされているトリガーのRedisをサッドされていない操作を削除期限切れ、およびのでdelコマンドの複雑さのため、ここを参照してください。O(n)は、期限切れのデータの削除に費やされた時間です。
テスト再現
(1、1000000)のためのレンジ内I: T1 = time.clock()* 1000 RN = random.randint(千億、20,000,000,000,000) r.sadd(K、RN) T2 = time.clock()1000 * C = T2 - T1の tarr.append(STR(C))IF C> 100:印刷I、C X = INT(time.time()) X = 10 +#遅延(10 K)毎秒10 r.expire満了しながら真: Yは、time.time()= T1 = time.clock()×1000 (1、1000000000)RN = random.randintを (K、RN)r.sadd T2 = time.clock()×1000 tarr.append(STRを( C))C> 100なら: #はサッドスロークエリケース再生 iはY> X場合C、プリントを + 5:#1 タイムアウトがBREAKのある breakprintのtime.clock()
非常に単純な再現手順、
-
十分なデータのキーSADD(百万)へ
-
相対的な有効期限を設定するためのキー。
-
サッドコマンドは、記録時間を呼び出し、呼び出しを続けました。
-
最後の観測Redisのスロークエリログ。
スロークエリログとして容疑者として、SADDコマンドを登場1秒を要しました。
ソリューションおよび概要
なぜならRedisのデル・キーが設定されているO(n)のための操作の複雑さのため、キーのセットのために、好ましくは、単一のキーの過大な値を回避するために、断片化によって提供されます。
また、redis4.0遅延は、非同期のブロックを回避するために、操作lazyfree_lazy_expire / azyfree_lazy_eviction / lazyfree_lazy_server_delを削除非同期によって達成することができるサポートの設定によって削除されました
深い読み
最後に、のはそれのキーソースを削除扱うredis3.xと4.xを見てみましょう。
Redisの三つのメカニズム、すなわち、キーを排除しました
-
delコマンド
-
パッシブ(とき対応するキー期限切れの要求コマンドを削除する)段階的に廃止しました
-
(回復メモリを排除するために重要なイニシアチブRedisの)削除するイニシアティブ
のは、上記の3つの除去機構のエントリーコードredis3.xバージョンを見てみましょう。
delコマンド - delCommand
空delCommand(クライアント* C){ int型= 0、削除、J。用(J = 1、J <C-> ARGC; J ++){ expireIfNeeded(C-> DB、C-> ARGV [J])。IF(dbDelete(C-> DB、C-> ARGV [J])){ signalModifiedKey(C-> DB、C-> ARGV [J])。 notifyKeyspaceEvent(NOTIFY_GENERIC、 "デル"、C-> ARGV [j]は、C-> DB-> ID)。 server.dirty ++; ++削除; } } addReplyLongLong(C、削除)。 }
プロセスフローは、キーの有効期限が切れているかどうか最初のチェックは非常に簡単ですし、削除dbDeleteを呼び出します
パッシブ排除 - expireIfNeeded
INT expireIfNeeded(redisDb * DB、robj *キー){mstime_t = getExpire(DB、キー)//获取过期时间 今mstime_t。IF(<0の場合)は0を返します。/ *ありません* /このキーの有効期限が切れる 読み込みながら何かを期限切れにしないでください/ *。それは、後に行われます。* / IF(server.loading)の戻り0; 私たちは、Luaのスクリプトのコンテキスト内にある場合は/ *、我々はその時間があると主張 のLuaスクリプトを開始したときにブロックされました*。この方法では、キーが期限切れになることができます *それはの真ん中にアクセスしていないだけで、初めて の奴隷/ AOF一貫性に伝播すること、*スクリプトの実行を。 *詳しくは、Githubの上で問題の#1525を参照してください。* / 今= server.lua_caller?server.lua_time_start:mstime(); //过去当前时间 / *我々はできるだけ早く返す、スレーブのコンテキストで実行している場合: *スレーブキーの有効期限がしますマスターによって制御されている *期限切れのキーのために私たちに合成されたDEL操作を送信します。 * *それでも我々は、呼び出し側に適切な情報を返すようにしようと 私たちはキーが1ならば、まだ有効であるべきだと思う場合は0、つまり* 私たちはキーが、この時点で有効期限が切れていると思います*。* / IF(server.masterhost = NULL!)今すぐ戻る>とき。このキーの有効期限が切れていない/ *戻り値* / IF(今<=の場合)リターン0; / *キー* /削除 server.stat_expiredkeysを++; propagateExpire(デシベル、キー)。//把过期时间传递出去(从库、AOF备份等) notifyKeyspaceEvent(NOTIFY_EXPIRED、 、キー、DB->「期限切れ」 //; ID) の監視を行うようのpubsubのpubsubを通過するメッセージのDB通知に生じる変化の鍵、のRedisを使用することができる )、(DBキーdbDeleteを返します。 }
排除するためのイニシアティブ - serverCron
server.cファイル
int型serverCron(構造体aeEventLoop *イベントループ、長い長いID、void *型clientData){ / ** * STH重要ではありません * / ... / *私たちは、非同期クライアント上でいくつかの操作を行う必要があります。* / clientsCron(); Redisのデータベース上の/ *ハンドルバックグラウンド処理。* / databasesCron(); / ** * STH重要ではありません * / ... server.cronloops ++; 1000 / server.hzを返します。 } / *この関数ハンドル「背景」の操作は、私たちが行うことが要求される ようなアクティブ鍵失効、サイズ変更、などのRedisデータベースにインクリメンタル* *リハッシュ。する* / void databasesCron(無効){/ *ランダムサンプリングでキーを期限切れ。奴隷には必要ありません マスターは私たちのためにDELSを合成しますと*。* / IF(server.active_expire_enabled && server.masterhost == NULL) activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW)。/ ** * STH重要ではありません * / } / *いくつかのキーをタイムアウトし期限切れにしてください。使用されるアルゴリズムは、適応され、 それ以外の場合は、いくつかの期限切れのキーがある場合*いくつかのCPUサイクルを使用します *それはあまりにも多くのメモリが使用されていることにより、回避するために、より積極的な取得する 鍵空間から削除することができます*キー。 * * CRON_DBS_PER_CALLデータベースは、すべてでテストされているよりも、これ以上 *の繰り返し。 * *コールのこの種が使用されているのRedisはtimelimit_exitであることを検出 *真、そうそこに行うにはより多くの仕事があり、我々はより漸進からそれを行う * イベントループのbeforeSleep()関数*。 * *サイクルタイプを期限切れ: * *タイプは、関数を実行しようとするACTIVE_EXPIRE_CYCLE_FASTある場合 *は「速い」EXPIRE_FAST_CYCLE_DURATIONよりも長くかかるサイクルを期限切れ *マイクロ秒、そして同じ時間前に再び繰り返されていません。 * *タイプは通常期限切れサイクルがされていること、ACTIVE_EXPIRE_CYCLE_SLOW場合 期限がREDIS_HZ期間の割合である*、実行 定義REDIS_EXPIRELOOKUPS_TIME_PERCによって指定されるよう*。* /ボイドactiveExpireCycle(int型){int型dbs_per_call = CRON_DBS_PER_CALL。/ *我々は通常で、反復ごとにCRON_DBS_PER_CALLをテストする必要があります :* 2つの例外 * 1)私たちが持っているよりも多くのDBをテストしないでください。 * 2)最後の時間は、我々は時間制限がヒットした場合、我々はすべてのDBスキャンする いくつかのDBに行うには、我々が望んでいない仕事があるので、この繰り返しで*を あまりにも多くの時間のためにメモリを使用するには、*期限切れのキーは。* / IF(dbs_per_call> server.dbnum || timelimit_exit) dbs_per_call = server.dbnum。//每次清理扫描的数据库数 / *私たちは、CPU時間の最大ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERCの割合で使用することができ 、反復ごとに*。この関数はの頻度で呼び出されますので 、以下の毎秒server.hz時間*の最大量である 私たちは、この関数の中で過ごすことができます*マイクロ秒。* / いるtimelimit = 1000000 * ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC / server.hz / 100。 timelimit_exit = 0; IF(いるtimelimit <= 0)いるtimelimit = 1。(タイプ== ACTIVE_EXPIRE_CYCLE_FAST)場合 いるtimelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION。/ *マイクロインチ * / (J = 0; J <dbs_per_callあり、j ++){int型のため期限切れ。 redisDb * DB = server.db +(current_db%server.dbnum)。/ *ので、我々は時間が不足した場合に確信している今、DBをインクリメント 現在のDBに*我々は次から再起動します。これは、することができます * DBの全体に均等に時間を配布します。* / current_db ++; / *サイクルの終わりに25%以上の場合には有効期限が切れるように続けて キーの*は有効期限が切れました。* / //如果有超过25%的键过期了则继续扫描 {unsigned long型NUM、スロットを行います。長い長い今、ttl_sum。int型ttl_samples; / *できるだけ早く次のDBを試し期限切れに何も存在しない場合。* / (== 0(NUM = dictSize(DB->の有効期限が切れた))){//当前没有需要过期的键場合 DB-> avg_ttl = 0。ブレーク; } スロット= dictSlots(DB->の有効期限が切れました); 今= mstime()。ランダム取得1%未満満たさスロットがある場合は/ * *キーは高価なので、より良い時間を待ってここで停止... *辞書はできるだけ早くリサイズされます。* / IF(NUM &&スロット> DICT_HT_INITIAL_SIZE && (NUM * 100 /スロット<1))、ブレーク。/ *メインの収集サイクル。キーの間でランダムキーをサンプル 期限切れのものをチェックし、期限切れとセットで*。* / 0 =期限切れの。 ttl_sum = 0; ttl_samples = 0; IF(NUM> ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP) NUM = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP。// 3.2.11为20次 ながら(num--){ dictEntry *デ。長い長いTTL; IF((デ= dictGetRandomKey(DB->満了する))== NULL)破ります。//随机获取一个键 TTL = dictGetSignedIntegerVal(デ)-now。(今activeExpireCycleTryExpire(デシベル、デは、))++期限が切れている場合。(TTL> 0){/ *私たちは、キーの平均TTLがまだ切れていない場合です。* / ttl_sum + = TTL; ttl_samples ++; } } / ** *这里有一些控制删除时间的逻辑和其他逻辑。 * / キーの有効期限が切れていることが判明した場合*、それはデータベースから削除され、 IF(timelimit_exit)リターン; / *私たちはそこにキーの25%ある場合は、最後の内にはない以下のサイクルを繰り返し行う *現在で見つかった* / DBを期限切れ。 }(期限切れ> ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP / 4)一方、// 20次/ 4 } } / * =======================クロン:呼び出さ100ms毎=========== ============= * // * activeExpireCycle()関数のヘルパー関数。 *この機能は、ハッシュテーブルに格納されているキー期限切れにしようとする のRedisデータベースの「期限が切れる」ハッシュテーブルのエントリ*「ド」を。 * * *パラメータは、「今」渡されたとしてミリ秒単位で現在の時刻です * 1が返されます。そうでなければ何も操作を行わないと0が返されます。 * キーの有効期限が切れている場合は*、server.stat_expiredkeysがインクリメントされます。 *機能にあまりにも多くのgettimeofday()システムコールを避けるために。* / INT activeExpireCycleTryExpire(redisDb * DB、dictEntry *ド、長い長今){長いロングT = dictGetSignedIntegerVal(デ)。(今> T){もし SDSキー= dictGetKey(デ)。 robj * keyobj = createStringObject(キー、sdslen(キー)); propagateExpire(DB、keyobj)。 dbDelete(DB、keyobj)。 notifyKeyspaceEvent(NOTIFY_EXPIREDは、 "期限切れ"、keyobj、DB->のid); decrRefCount(keyobj)。 server.stat_expiredkeys ++; 1を返します。 }他{0を返します。 } }
> databasesCron - - > activeExpireCycle - コールパスserverCron削除するためのイニシアチブ> activeExpireCycleTryExpireは、私たちは主にactiveExpireCycleTryExpireを見て。
排除するためのイニシアチブは、スロットのノードのランダムリスト次いで、それはランダム、ランダムな最初のスロットによって行われる、ランダムアルゴリズムは非常に簡単であり、ランダムサンプリングによって削除されるべきです。加えて、数および時間満了したキーの長さに基づいて削除排除するDBイニシアチブをスキャンの数及び頻度を決定するであろう。
ちなみに、serverCronはに加えて、キーを排除するために、タイマー、databasesCronイニシアチブによって登録Redisの定期的なタスク、ある、の話をするだけでなく、焼き直しか、サイズを変更し、他のもの。
低レベルの呼び出し
三つのメカニズムが異なりますが、基本的な方法は同じです--dbDelete自分のコール。
db.cファイル
辞書は、SDSの解放されません有効期限が切れるから/ *もしあれば、キー、値、および関連する有効期限のエントリを削除し、DB * / INT dbDelete(redisDb * DB、robj *キー)から、{/ *エントリを削除 *キー、それがメイン辞書で共有されているため。* / IF(dictSize(DB->満了する)> 0)dictDelete(DB->満了し、キー- > PTR)。IF(dictDelete(DB->辞書、KEY-> PTR)== DICT_OK){IF(server.cluster_enabled)slotToKeyDel(キー)。1を返します。 }他{0を返します。 } }
dict.cファイル
int型dictDelete(辞書の*のHT、CONST void *型のキー){dictGenericDelete(HT、キー、0)を返します。 } / *検索および除去要素* /静的INT dictGenericDelete(辞書の* dを、CONSTボイド*キー、int型nofree) { unsigned int型のH、IDX。 dictEntry *彼、* prevHe。 int型のテーブル。IF(D-> HT [0] .size == 0)DICT_ERRを返します。/ * D-> HT [0] .tableがNULLである* / IF(dictIsRehashing(D))_dictRehashStep(D)。 H = dictHashKey(D、キー)用(表= 0;表<= 1;表++){ IDX = H&D-> HT [表] .sizemask。 D-> HT [表] .table [IDX] = HE->次。(もし!nofree){ 彼= D-> HT [テーブル] .table [IDX]。 prevHe = NULL; しばらく(彼){場合(キー== HE->キー|| dictCompareKeys(D、キー、HE->キー)){/ * / *リストから要素をリンク解除 (prevHe)もし 次prevHe->次= HE->。他 dictFreeKey(D、彼は); dictFreeVal(D、彼は); } zfree(彼)。 D-> HT [表] .used--。DICT_OKを返します。 } prevHe =彼。 彼はHE->次=。 }もし(!dictIsRehashing(d))がブレーク。 } DICT_ERRを返します。/ * * /} / * -------------------------------マクロ---------見つかりません--------------------------- * /#\ dictFreeVal(D、エントリー)を定義する ( - >タイプ- > valDestructor(d))があれば\ (D) - >タイプ- > valDestructor((D) - > privdata、(エントリ) - > v.val)
server.c
/ * DB->辞書、キーはSDS列では、ヴァルスはRedisのオブジェクトです。* / dictType dbDictType = { dictSdsHash、/ *ハッシュ関数* / NULL、/ *キーDUP * / NULL、/ *ヴァルDUP * / dictSdsKeyCompare、/ *キーの比較* / dictSdsDestructor、/ *キーデストラクタ* / dictObjectDestructor / *ヴァルデストラクタ* /};空隙dictObjectDestructor(ボイド* privdata、ボイド*ヴァル){ DICT_NOTUSED(privdata)。(ヴァル== NULL)を返すと、NULLに設定するようにスワップアウトキーの/ *値* / decrRefCount(ヴァル)。 }
object.c
ボイドdecrRefCount(robjの* 0){(O->参照カウント<= 0)serverPanic( "参照カウントに対してdecrRefCount <= 0")であれば、IF(O->参照カウント== 1){スイッチ(O->型){ケースOBJ_STRING:freeStringObject(O)ブレーク; ケースOBJ_LIST:freeListObject(O)ブレーク; ケースOBJ_SET:freeSetObject(O)ブレーク; ケースOBJ_ZSET:freeZsetObject(O)ブレーク; ケースOBJ_HASH:freeHashObject(O)ブレーク; デフォルト:serverPanic( "不明なオブジェクトタイプ"); ブレーク; } zfree(O) }他{ O-> refcount--。 } } ボイドfreeSetObject(robj * O){スイッチ(O->エンコーディング){ケースOBJ_ENCODING_HT: dictRelease((辞書*)O-> PTR)。ブレーク; ケースOBJ_ENCODING_INTSET: zfree(O-> PTR)。ブレーク; デフォルト: serverPanic(「不明なセットのエンコードタイプ」); } }
コアは、dictFreeVal年に見られるマクロに対応することができ、削除、マクロは参照カウントによって、厳密(decrRefCountに削除対応する、dbDictType dictObjectDestructor関数で指定されvalDestructor、の対応dictTypeを呼び出します管理のライフサイクル)
該当するリリース内DecrRefCountは、各データ型に対して、我々が見て設定freeSetObjectの道を解放する方法を意味します。2つの処理の設定に基づいてデータの2種類があり、INTSETのみハッシュテーブルdictReleaseメソッドが呼び出された場合、うまくポインタを解放する必要があります。
dict.c
/ *クリア&ハッシュテーブルをリリースする* / void dictRelease(辞書の* d)は { _dictClear(D&D-> HT [0]、NULL); _dictClear(D&D-> HT [1]、NULL); zfree(D)。 } / *全体の辞書を破棄* / INT _dictClear(辞書の* dを、dicthtの*のHT、ボイド(コールバック)(ボイド*)){ unsigned long型I。/ *無料すべての要素* / 用(i = 0;> 0使用されるI <HT->サイズ&& HT->; iは++){ dictEntry *彼* nextHe。(コールバック&&(I&65535)== 0)場合にコールバック(D-> privdata)。もし((彼= HT->テーブル[I])== NULL)続けます。(HE){一方 nextHe = HE->次。 dictFreeKey(D、彼は); dictFreeVal(D、彼は); zfree(彼)。 彼はnextHeを=。 } } / *表と割り当てられたキャッシュ構造を解放* / zfree(HT->テーブル)。/ *テーブルを再初期化* / _dictReset(HT)。DICT_OKを返します。/ *} * /失敗することはありません
この時点(dictClear法)で、我々は、HTは、各要素および削除を横断する必要があり、これはO(N)プロセスであることを確認することができるので、ブロックされているRedisのリスクが存在します。(メカニズムを排除するためであってもイニシアチブ)
これは遅延を削除することによってredis4.xシリーズで解決されています。