使用 Redis 實現 SQL 伸縮

jopen 10年前發布 | 14K 次閱讀 Redis NoSQL數據庫

我喜歡Redis。這是目前的技術當中唯一讓你奇怪為什么需要這么長時間編譯它的技術。可預測的,高性能并且適應性強,這是我過去幾年越來越多使用它的原因。Sentry主要在PostgreSQL上運行已經不是秘密(盡管目前它還依賴于一系列其它技術)

一個多星期前,我在 Python Nordeste 上作了主題演講。某種程度上而言我只能作一些快速的總結,我決定去找一些黑客來探討大量使用Sentry,特別是Redis技術。這篇文章是一個5分鐘討論的擴充。

緩解行之間的爭奪

我們采用了早在哨兵發展的東西是什么成為著名的sentry.buffers。這是一個簡單的系統,使我們能夠實現非常有效的緩沖計數器,一個簡單的上次寫入贏的策略。重要的是要注意,我們完全與此幾乎杜絕任何形式的耐用性(這是非常可以接受的哨兵的工作方式)。

該操作是相當簡單的,每當一個更新進來,我們做到以下幾點:

    1.創建綁定到給定實體散列鍵

    2.增量'反'使用HINCRBY

    3.HEST各種不同LWW數據(如“最后一次見到”)

    4.ZADD散列鍵到'掛起'使用當前時間戳設置

現在,每個刻度(在哨兵的情況下,這是10秒),我們要轉儲這些緩沖區和扇出的寫入。這看起來像下面這樣:

    1.開始使用ZRANGE所有鍵

    2. 火了一個作業分成RabbitMQ的每一個懸而未決的關鍵

   3.  ZREM給定的鍵

現在RabbitMQ作業將能夠讀取和清除哈希表,和“懸而未決”更新已經彈出了一套。有幾件事情需要注意:

  • 在下面我們想要只彈出一個設置的數量的例子中我們將使用一組排序(舉例來說我們需要那100個舊集合)。

    </li>

  • 假使我們為了處理一個鍵值來結束多道排序的作業,這個人會得到no-oped由于另一個已經存在的處理和清空哈希的過程。

    </li>

  • 該系統能夠在許多Redis節點上不斷擴展下去僅僅是通過在每個節點上安置把一個'懸置'主鍵來實現

    </li> </ul>

    我們有了這個處理問題的模型之后,能夠確保“大部分情況下”每次在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)     currentrate,  = pipe.execute()

        return int(current_rate) > limit</pre>

    我們所闡明的限制速率的方法是 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</pre>

    事實上我們最終采取這種策略可以使哨兵追蹤不同事件的短期數據。在這種情況下,我們通常對用戶數據進行排序以便可以在最短的時間內找到最活躍用戶的數據。

    基本鎖

    雖然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)</pre>

    而鎖()內的哨兵利用的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,
        }}

    一個可修改模型可能僅使用簡單的鍵并且僅在存儲區上增加一些增量寄存器。

       "1:1399958363:0:1": 53

    我們選擇哈希映射模型基于以下兩個原因:

    • 我們可以將所有的鍵設為一次性的(這也可能產生負面影響,但是目前為止是穩定的)

      </li>

    • 大幅壓縮鍵值,這是相當重要的處理

      </li> </ul>

      此外,離散的數字鍵允許我們在將虛擬的離散鍵值映射到固定數目的鍵值上,并在此分配單一存儲區(我們可以使用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)</pre>

      歸結如下:

      • 生成所必須的鍵。

        </li>

      • 使用工作區,提取所有連接操作的最小結果集(Nydus負責這些)。

        </li>

      • 給出結果,并且基于指定的時間間隔內和給定的鍵值將它們映射到當前的存儲區內。

        </li> </ul>

        簡單的選擇

        我是一個喜歡用簡單的方案解決問題的人,在這個范疇里使用Redis無疑是很適合的。它的文檔是那樣讓人驚訝,那是因為(閱讀)其文檔的門檻非常的低。雖然他也有折衷(主要是如果你使用持久化),但是他們工作地很好并且比較直觀。

        那么Redis為您解決什么問題呢?

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!