這篇文章主要介紹了利用Redis實現SQL伸縮的方法,包括講到了鎖和時間序列等方面來提升傳統資料庫的效能,需要的朋友可以參考下。
緩解行競爭
我們在Sentry開發的早起採用的是sentry.buffers。 這是一個簡單的系統,它允許我們以簡單的Last Write Wins策略來實現非常有效緩衝計數器。 重要的是,我們藉助它完全消除了任何形式的耐久性 (這是Sentry工作的一個非常可接受的方式)。
操作非常簡單,每當一個更新進來我們就做如下幾步:
- 建立一個綁定到傳入實體的雜湊鍵(hash key)
- 使用HINCRBY使計數器值增加
- HSET所有的LWW資料(比如 "最後一次見到的")
- 用目前時間戳ZADD雜湊鍵(hash key)到一個"掛起" set
現在每一個時幅 (在Sentry中為10秒鐘) 我們要轉儲(dump)這些緩衝區並且扇出寫道(fanout the writes)。 看起來像下面這樣:
- 使用ZRANGE擷取所有的key
- 為每一個掛起的key發起一個作業到RabbitMQ
現在RabbitMQ作業將能夠讀取和清除雜湊表,和“懸而未決”更新已經彈出了一套。有幾件事情需要注意:
- 在下面我們想要只彈出一個設定的數量的例子中我們將使用一組排序(舉例來說我們需要那100箇舊集合)。
- 假使我們為了處理一個索引值來結束多道排序的作業,這個人會得到no-oped由於另一個已經存在的處理和清空雜湊的過程。
- 該系統能夠在許多Redis節點上不斷擴充下去僅僅是通過在每個節點上安置把一個'懸置'主鍵來實現。
我們有了這個處理問題的模型之後,能夠確保“大部分情況下”每次在SQL中只有一行能夠被馬上更新,而這樣的處理方式減輕了我們能夠預見到的鎖問題。考慮到將會處理一個突然產生且所有最終組合在一起進入同一個計數器的資料的情境,這種策略對Sentry用處很多。
速度限制
出於哨兵的局限性,我們必須終結持續的拒絕服務的攻擊。我們通過限制連線速度來應對這種問題,其中一項是通過Redis支援的。這無疑是在sentry.quotas範圍內更直接的實現。
它的邏輯相當直接,如同下面展示的那般:
def incr_and_check_limit(user_id, limit): key = '{user_id}:{epoch}'.format(user_id, int(time() / 60)) pipe = redis.pipeline() pipe.incr(key) pipe.expire(key, 60) current_rate, _ = pipe.execute() return int(current_rate) > limit
我們所闡明的限制速率的方法是 Redis在快取服務上最基本的功能之一:增加空的鍵字。在快取服務中實現同樣的行為可能最終使用這種方法:
def incr_and_check_limit_memcache(user_id, limit): key = '{user_id}:{epoch}'.format(user_id, int(time() / 60)) if cache.add(key, 0, 60): return False current_rate = cache.incr(key) return current_rate > limit
事實上我們最終採取這種策略可以使哨兵追蹤不同事件的短期資料。在這種情況下,我們通常對使用者資料進行排序以便可以在最短的時間內找到最活躍使用者的資料。
基本鎖
雖然Redis的是可用性不高,我們的用例鎖,使其成為工作的好工具。我們沒有使用這些在哨兵的核心了,但一個樣本用例是,我們希望盡量減少並發性和簡單無操作的操作,如果事情似乎是已經在運行。這對於可能需要執行每隔一段時間類似cron任務非常有用,但不具備較強的協調。
在Redis的這樣使用SETNX操作是相當簡單的:
from contextlib import contextmanagerr = Redis()@contextmanagerdef lock(key, nowait=True): while not r.setnx(key, '1'): if nowait: raise Locked('try again soon!') sleep(0.01) # limit lock time to 10 seconds r.expire(key, 10) # do something crazy yield # explicitly unlock r.delete(key)
而鎖()內的哨兵利用的memcached的,但絕對沒有理由我們不能在其切換到Redis。
時間序列資料
近來我們創造一個新的機制在Sentry(包含在sentry.tsdb中) 儲存時間序列資料。這是受RRD模型啟發,特別是Graphite。我們期望一個快速簡單的方式儲存短期(比如一個月)時間序列數,以便於處理高速寫入資料,特別是在極端情況下計算潛在的短期速率。儘管這是第一個模型,我們依舊期望在Redis儲存資料,它也是使用計數器的簡單範例。
在目前的模型中,我們使用單一的hash map來儲存全部時間序列資料。例如,這意味所有資料項目在都將同一個雜湊鍵擁有一個資料類型和1秒的生命週期。如下所示:
{ "<type enum>:<epoch>:<shard number>": { "<id>": <count> }}
因此在這種狀況,我們需要追蹤事件的數目。事件類型映射到枚舉類型"1".該判斷的時間是1s,因此我們的處理時間需要以秒計。散列最終看起來是這樣的:
{ "1:1399958363:0": { "1": 53, "2": 72, }}
一個可修改模型可能僅使用簡單的鍵並且僅在儲存區上增加一些增量寄存器。
我們選擇雜湊映射模型基於以下兩個原因:
我們可以將所有的鍵設為一次性的(這也可能產生負面影響,但是目前為止是穩定的)
大幅壓縮索引值,這是相當重要的處理
此外,離散的數字鍵允許我們在將虛擬離散索引值映射到固定數目的索引值上,並在此分配單一儲存區(我們可以使用64,映射到32個物理結點上)
現在通過使用 Nydus和它的map()(依賴於一個工作區)(),資料查詢已經完成。這次操作的代碼是相當健壯的,但幸好它並不龐大。
def get_range(self, model, keys, start, end, rollup=None): """ To get a range of data for group ID=[1, 2, 3]: Start and end are both inclusive. >>> now = timezone.now() >>> get_keys(tsdb.models.group, [1, 2, 3], >>> start=now - timedelta(days=1), >>> end=now) """ normalize_to_epoch = self.normalize_to_epoch normalize_to_rollup = self.normalize_to_rollup make_key = self.make_key if rollup is None: rollup = self.get_optimal_rollup(start, end) results = [] timestamp = end with self.conn.map() as conn: while timestamp >= start: real_epoch = normalize_to_epoch(timestamp, rollup) norm_epoch = normalize_to_rollup(timestamp, rollup) for key in keys: model_key = self.get_model_key(key) hash_key = make_key(model, norm_epoch, model_key) results.append((real_epoch, key, conn.hget(hash_key, model_key))) timestamp = timestamp - timedelta(seconds=rollup) results_by_key = defaultdict(dict) for epoch, key, count in results: results_by_key[key][epoch] = int(count or 0) for key, points in results_by_key.iteritems(): results_by_key[key] = sorted(points.items()) return dict(results_by_key)
歸結如下:
- 產生所必須的鍵。
- 使用工作區,提取所有串連操作的最小結果集(Nydus負責這些)。
- 給出結果,並且基於指定的時間間隔內和給定的索引值將它們映射到當前的儲存區內。
以上就是如何利用Redis實現SQL伸縮的方法,希望對大家的學習有所協助。