日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

redis延遲問題全面排障指南

作者:kevine

前言

在 Redis 的實際使用過程中,我們經常會面對以下的場景:

  • 在 Redis 上執行同樣的命令,為什么有時響應很快,有時卻很慢;
  • 為什么 Redis 執行 GET、SET、DEL 命令耗時也很久;
  • 為什么我的 Redis 突然慢了一波,之后又恢復正常了;
  • 為什么我的 Redis 穩定運行了很久,突然從某個時間點開始變慢了。

這時我們還是需要一個全面的排障流程,不能無厘頭地進行優化;全面的排障流程可以幫助我們找到真正的根因和性能瓶頸,以及實施正確高效的優化方案。

這篇文章我們就從可能導致 Redis 延遲的方方面面開始,逐步深入排障深水區,以提供一個「全面」的 Redis 延遲問題排查思路。

需要了解的詞

  • Copy On WriteCOW是一種建立在虛擬內存重映射技術之上的技術,因此它需要 MMU的硬件支持, MMU會記錄當前哪些內存頁被標記成只讀,當有進程嘗試往這些內存頁中寫數據的時候, MMU就會拋一個異常給操作系統內核,內核處理該異常時為該進程分配一份物理內存并復制數據到此內存地址,重新向 MMU發出執行該進程的寫操作。
  • 內存碎片操作系統負責為每個進程分配物理內存,而操作系統中的虛擬內存管理器保管著由內存分配器分配的實際內存映射 如果我們的應用程序需求1GB大小的內存,內存分配器將首先嘗試找到一個 連續的內存段來存儲數據;如果找不到連續的段,則分配器必須將進程的 數據分成多個段,從而導致內存開銷增加。
  • SWAP顧名思義,當某進程向 OS 請求內存發現不足時,OS 會把內存中暫時不用的數據交換出去,放在SWAP分區中,這個過程稱為 SWAP OUT。 當某進程又需要這些數據且 OS 發現還有空閑物理內存時,又會把 SWAP分區中的數據交換回物理內存中,這個過程稱為 SWAP IN,詳情可參考這篇文章。
  • redis 監控指標合理完善的監控指標無疑能大大助力我們的排障,本篇文章中提到了很多的 redis 監控指標,詳情可以參考這篇文章: redis 監控指標
排除無關原因

當我們發現從我們的業務服務發起請求到接收到Redis的回包這條鏈路慢時,我們需要先排除其它的一些無關 Redis自身的原因,如:

  • 業務自身準備請求耗時過長;
  • 業務服務器到 Redis服務器之間的網絡存在問題,例如網絡線路質量不佳,網絡數據包在傳輸時存在延遲、丟包等情況; 網絡和通信導致的固有延遲:客戶端使用 TCP/IP連接或 Unix域連接連接到 Redis,在 1 Gbit/s網絡下的延遲約為 200 us,而 Unix域Socket的延遲甚至可低至 30 us,這實際上取決于網絡和系統硬件;在網絡通信的基礎之上,操作系統還會增加了一些額外的延遲(如 線程調度、CPU緩存、NUMA等);并且在虛擬環境中,系統引起的延遲比在物理機上也要高得多的結果就是,即使 Redis 在亞微秒的時間級別上能處理大多數命令,網絡和系統相關的延遲仍然是不可避免的。
  • Redis實例所在的機器帶寬不足 / Docker網橋性能問題等。

排障事大,但咱也不能冤枉了Redis;首先我們還是應該把其它因素都排除完了,再把焦點關注在業務服務到 Redis這條鏈路上。如以下的火焰圖就可以很肯定的說問題出現在 Redis 上了:

在排除無關因素后,如何確認 Redis 是否真的變慢了?

測試流程

排除無關因素后,我們可以按照以下基本步驟來判斷某一 Redis 實例是否變慢了:

  1. 監控并記錄一個相對正常的 Redis 實例(相對低負載、key 存儲結構簡單合理、連接數未滿)的相關指標;
  2. 找到認為表現不符合預期的 Redis 實例(如使用該實例后業務接口明顯變慢),在相同配置的服務器上監控并記錄這個實例的相關指標;
  3. 若表現不符合預期的 Redis 實例的相關指標明顯達不到正常 Redis 實例的標準(延遲兩倍以上、OPS僅為正常實例的 1/3、 內存碎片率較高等),即可認為這個 Redis 實例的指標未達到預期。

確認是 Redis 實例的某些指標未達到預期后,我們就可以開始逐步分析拆解可能導致 Redis 表現不佳的因素,并確認優化方案了。

快速清單

I've little time, give me the checklist

在線上發生故障時,我們都沒有那么多時間去深究原因,所以在深入到排障的深水區前,我們可以先從最影響最大的一些問題開始檢查,這里是一份「會對redis基本運行造成嚴重影響的問題」的 checklist:

  • 確保沒有運行阻塞服務器的緩慢命令;使用 Redis 的耗時命令記錄功能來檢查這一點;
  • 對于 EC2 用戶,請確保使用基于HVM的現代 EC2 實例,如 m3.dium等,否則, fork系統調用帶來的延遲太大了;
  • 禁用透明內存大頁。使用echo never > /sys/kernel/mm/transparent_hugepage/enabled來禁用它們,然后重新啟動 Redis 進程;
  • 如果使用的是虛擬機,則可能存在與 Redis 本身無關的固有延遲;使用redis-cli --intrinsic-latency 100檢查延遲,確認該延遲是否符合預期(注意:您需要在服務器上而不是在客戶機上運行此命令);
  • 啟用并使用 Redis 的延遲監控功能,更好的監控 Redis 實例中的延遲事件和原因。
  •  
導致 Redis Latency 的具體原因

如果使用我們的快速清單并不能解決實際的延遲問題,我們就得深入 redis 性能排障的深水區,多方面逐步深究其中的具體原因了。

使用復雜度過高的命令 / 「大型」命令

要找到這樣的命令執行記錄,需要使用 Redis 提供的耗時命令統計的功能,查看 Redis 耗時命令之前,我們需要先在redis.conf中設置耗時命令的閾值;如:設置耗時命令的閾值為 5ms,保留近 500 條耗時命令記錄:

# The following time is expressed in microseconds, so 1000000 is equivalent# to one second. Note that a negative number disables the slow log, while# a value of zero forces the logging of every command.slowlog-log-slower-than 10000# There is no limit to this length. Just be aware that it will consume memory.# You can reclAIm memory used by the slow log with SLOWLOG RESET.slowlog-max-len 128

或是直接在redis-cli中使用 CONFIG命令配置:

# 命令執行耗時超過 5 毫秒,記錄耗時命令CONFIG SET slowlog-log-slower-than 5000# 只保留最近 500 條耗時命令CONFIG SET slowlog-max-len 500

通過查看耗時命令記錄,我們就可以知道在什么時間點,執行了哪些比較耗時的命令。

如果應用程序執行的 Redis 命令有以下特點,那么有可能會導致操作延遲變大:

  1. 經常使用 O(N) 以上復雜度的命令,例如 SORT, SUNION, ZUNIONSTORE 等聚合類命令
  2. 使用 O(N) 復雜度的命令,但 N 的值非常大

第一種情況導致變慢的原因是 Redis 在操作內存數據時,時間復雜度過高,要花費更多的 CPU 資源。

第二種情況導致變慢的原因是 處理「大型」redis 命令(大請求包體 / 大返回包體的 redis 請求),對于這樣的命令來說,雖然其只有兩次內核態與用戶態的上下文切換,但由于 redis 是單線程處理回調事件的,所以后續請求很有可能被這一個大型請求阻塞,這時可能需要考慮業務請求拆解盡量分批執行,以保證 redis 服務的穩定性。

Bigkey

bigkey 一般指包含大量數據或大量成員和列表的 key,如下所示就是一些典型的 bigkey(根據 Redis 的實際用例和業務場景,bigkey 的定義可能會有所不同):

  • value 大小為 5 MB(數據太大)的 String
  • 包含 20000 個元素的List(列表中的元素數量過多)
  • 有 10000 個成員的ZSET密鑰(成員數量過多)
  • 一個大小為 100 MB 的Hash key,即便只包含 1000 個成員(key 太大)

在上一節的耗時命令查詢中,如果我們發現榜首并不是復雜度過高的命令,而是 SET / DEL 等簡單命令,這很有可能就是 redis 實例中存在 bigkey導致的。

bigkey 會導致包括但不限于以下的問題:

  • Redis 的內存使用量不斷增長,最終導致實例 OOM,或者因為達到最大內存限制而導致寫入被阻塞和重要 key 被驅逐;
  • 訪問偏差導致的資源傾斜,bigkey的存在可能會導致某個 Redis 實例達到性能瓶頸,從而導致整個集群也達到性能瓶頸;在這種情況下,Redis 集群中一個節點的內存使用量通常會因為對 bigkey的訪問需求而遠遠超過其他節點,而 Redis 集群中數據遷移時有一個最小粒度,這意味著該節點上的 bigkey 占用的內存無法進行 balance;
  • 由于將 bigkey 請求從 socket 讀取到 Redis 占用了幾乎所有帶寬,Redis 的其它請求都會受到影響;
  • 刪除 BigKey 時,由于主庫長時間阻塞(釋放 bigkey 占用的內存)導致同步中斷或主從切換。

如何定位 bigkey

  1. 使用 redis-cli 提供的—-bigkeys參數 redis-cli提供了掃描 bigkey 的 option —-bigkeys,執行以下命令就可以掃描 redis 實例中 bigkey 的分布情況,以 key 類型維度輸出結果: $ redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.01

    [00.00%] Biggest string found so far

    ...

    [98.23%] Biggest string found so far

    -------- summary -------

    Sampled 829675 keys inthe keyspace!

    Total key length inbytes is 10059825 (avg len 12.13)

    Biggest string found 'key:291880'has 10 bytes

    Biggest list found 'mylist:004'has 40 items

    Biggest setfound 'myset:2386'has 38 members

    Biggest hashfound 'myhash:3574'has 37 fields

    Biggest zset found 'myzset:2704'has 42 members

    36313 strings with 363130 bytes (04.38% of keys, avg size 10.00)

    787393 lists with 896540 items (94.90% of keys, avg size 1.14)

    1994 sets with 40052 members (00.24% of keys, avg size 20.09)

    1990 hashs with 39632 fields (00.24% of keys, avg size 19.92)

    1985 zsets with 39750 members (00.24% of keys, avg size 20.03)

    從輸出結果我們可以很清晰地看到,每種數據類型所占用的最大內存 / 擁有最多元素的 key 是哪一個,以及每種數據類型在整個實例中的占比和平均大小 / 元素數量。 bigkey 掃描實際上是 Redis 執行了 SCAN 命令,遍歷整個實例中所有的 key,然后針對 key 的類型,分別執行 STRLEN, LLEN, HLEN, SCARD和ZCARD命令,來獲取 String 類型的長度,容器類型(List, Hash, Set, ZSet)的元素個數。 ??NOTICE: 當執行 bigkey 掃描時,要注意 2 個問題:以下是 bigkey 掃描實際用到的命令的時間復雜度:

    1. 對線上實例進行 bigkey 掃描時,Redis 的 OPS 會突增,為了降低掃描過程中對 Redis 的影響,最好控制一下掃描的頻率,指定 -i參數即可,它表示掃描過程中每次掃描后休息的時間間隔(秒);
    2. 掃描結果中,對于容器類型(List, Hash, Set, ZSet)的 key,只能掃描出元素最多的 key;但一個 key 的元素多,不一定表示內存占用也多,我們還需要根據業務情況,進一步評估內存占用情況。

  1. 使用開源的 redis-rdb-tools通過 redis-rdb-Tools,我們可以根據自己的標準準確分析 Redis 實例中所有密鑰的實際內存使用情況,同時它還可以避免中斷在線服務,分析完成后,您可以獲得簡潔、易于理解的報告。 redis-rdb-Tools對 rdb文件的分析是離線的,對在線的 redis 服務沒有影響;這無疑是它對比第一種方案最大的優勢,但也正是因為是離線分析,其分析結果的實時性可能達不到某些場景下的標準,對大型 rdb文件的分析可能需要較長的時間。

針對 bigkey 問題的優化措施:

  1. 上游業務應避免在不合適的場景寫入 bigkey(夸張一點:用String存儲大型 binary file),如必須使用,可以考慮進行 大key拆分,如:對于 string 類型的 Bigkey,可以考慮拆分成多個 key-value;對于 hash 或者 list 類型,可以考慮拆分成多個 hash 或者 list。
  2. 定期清理HASH key中的無效數據(使用 HSCAN和 HDEL),避免 HASH key中的成員持續增加帶來的 bigkey 問題。
  3. Redis ≥ 4.0中,用 UNLINK命令替代 DEL,此命令可以把釋放 key 內存的操作,放到后臺線程中去執行,從而降低對 Redis 的影響。
  4. Redis ≥ 6.0中,可以開啟 lazy-free 機制(lazyfree-lazy-user-del = yes),在執行 DEL 命令時,釋放內存也會放到后臺線程中執行。
  5. 針對消息隊列 / 生產消費場景的 List, Set 等,設置過期時間或實現定期清理任務,并配置相關監控以及時處理突發情況(如線上流量暴增,下有服務無法消費等產生的消費積壓)。

即便我們有一系列的解決方案,我們也要盡量避免在實例中存入 bigkey。這是因為 bigkey 在很多場景下,依舊會產生性能問題;例如,bigkey 在分片集群模式下,對于數據的遷移也會有性能影響;以及資源傾斜、數據過期、數據淘汰、透明大頁等,都會受到 bigkey 的影響。

Hotkey

在討論 bigkey 時,我們也經常談到 hotkey ,當訪問某個密鑰的工作量明顯高于其他密鑰時,我們可以稱之為 hotkey;以下就是一些 hotkey 的例子:

  • 在一個 QPS 10w 的 Redis 實例中,只有一個 key 的 QPS 達到了 7000 次;
  • 擁有數千個成員、總大小為 1MB 的哈希鍵每秒會收到大量的 HGETALL 請求(在這種情況下,我們將其稱為熱鍵,因為訪問一個鍵比訪問其他鍵消耗的帶寬要大得多);
  • 擁有數萬個 member 的 ZSET 每秒處理大量的 ZRANGE 請求(cpu時間明顯高于用于其他 key 請求的 cpu時間。同樣,我們可以說這種消耗大量 CPU 的 Key 就是 HotKey)。

hotkey 通常會帶來以下的問題:

  • hotkey 會導致較高的 CPU 負載,并影響其它請求的處理;
  • 資源傾斜,對 hotkey 的請求會集中在個別 Redis 節點/機器上,而不是shard到不同的 Redis 節點上,導致 內存/CPU 負載集中在這個別的節點上,Redis 集群利用率不能達到預期;
  • hotkey 上的流量可能在流量高峰時突然飆升,導致 redis CPU 滿載甚至緩存服務崩潰,在緩存場景下導致緩存雪崩,大量的請求會直接命中其它較慢的數據源,最終導致業務不可用等不可接受的后果。

如何定位 hotkey:

  1. 使用redis-cli提供的 —hotkeys參數 Redis 從 4.0版本開始在 redis-cli中提供 hotkey 參數,以方便實例粒度的 hotkey 分析;它可以返回所有 key 被訪問的次數,但需要先將 maxmemory policy設置為 allkey-LFU。# Scanning the entire keyspace to find hot keys as well as

    # average sizes per key type. You can use -i 0.1 to sleep 0.1 sec

    # per 100 SCAN commands (not usually needed).

    Error: ERR An LFU maxmemory policy is not selected, access frequency not tracked. Please note that when switching between policies at runtime LRU and LFU data will take some time to adjust.

  2. 使用monitor命令 Redis 的 monitor命令可以實時輸出 Redis 接收到的所有請求,包括 訪問時間、客戶端 IP、命令和 key;我們可以短時間執行 monitor 命令,并將輸出重定向到文件;結束后,可以通過對文件中的請求進行分類和分析來找到這段時間的 hotkey。 monitor命令會消耗大量 CPU、內存和網絡資源;因此,對于本身就負載較大的 Redis 實例來說, monitor命令可能會讓性能問題進一步惡化;同時,這種異步采集分析方案的時效性較差,分析的準確性依賴于 monitor命令的執行時長;因此,在大多數無法長時間執行該命令的在線場景中,結果的準確性并不好。
  3. 上游服務針對 redis 請求進行監控 所有的 redis 請求都來自于上游服務,上游服務可以在上報時進行相關的指標監控、匯總及分析,以定位 hotkey ;但這樣的方式需要上游服務支持,并不獨立。

針對 hotkey 問題的優化方案:

1.使用pipeline

在一些非實時的 bigkey 請求場景下,我們可以使用 pipeline來大幅度降低 Redis 實例的 CPU 負載。

首先我們要知道,Redis 核心的工作負荷是一個單線程在處理,這里指的是——網絡 IO命令執行是由一個線程來完成的;而 Redis 6.0 中引入了多線程,在 Redis 6.0之前,從網絡 IO 處理到實際的讀寫命令處理都是由單個線程完成的,但隨著網絡硬件的性能提升,Redis 的性能瓶頸有可能會出現在網絡 IO 的處理上,也就是說單個主線程處理網絡請求的速度跟不上底層網絡硬件的速度。針對此問題,Redis 采用多個 IO 線程來處理網絡請求,提高網絡請求處理的并行度,但多 IO 線程只用于處理網絡請求,對于命令處理,Redis 仍然使用單線程處理

而 Redis 6.0 以前的單線程網絡 IO 模型的處理具體的負載在哪里呢?雖然 Redis 利用epoll機制實現 IO 多路復用(即使用 epoll監聽各類事件,通過事件回調函數進行事件處理),但 I/O 這一步驟是無法避免且始終由單線程串行處理的,且涉及用戶態/內核態的切換,即:

  • 從socket中讀取請求數據,會從內核態將數據拷貝到用戶態 ( read調用)
  • 將數據回寫到socket,會將數據從用戶態拷貝到內核態 ( write調用)

高頻簡單命令請求下,用戶態/內核態的切換帶來的開銷被更加放大,最終會導致redis-servercpu滿載 → redis-serverOPS不及預期 → 上游服務海量請求超時 → 最終造成類似 緩存穿透的結果,這時我們就可以使用 pipeline來處理這樣的場景了:redis pipeline。

眾所周知,redis pipeline可以讓 redis-server一次接收一組指令(在內核態中存入輸入緩沖區,收到客戶端的 Exec指令再調用 read syscall)后再執行,減少 I/O(即 accept -> read -> write)次數,在 高頻可聚合命令的場景下使用 pipeline可以大大減少 socket I/O帶來的 內核態與用戶態之間的上下文切換開銷

下面我們進行跑一組基于golang redis客戶端的簡單高頻命令的 Benchmark測試(不使用 pipeline和使用 pipeline對比),同時使用perf對 Redis 4 實例監控上下文切換次數:

  • Set without Pipeline(redis 4.0.14)perf stat-p 15537 -e context-switches -a sleep 10

    Performance counter stats forprocess id '15537':

    96,301 context-switches

    10.001575750 seconds time elapsed

  • Set using Pipeline(redis 4.0.14)perf stat-p 15537 -e context-switches -a sleep 10

    Performance counter stats forprocess id '15537':

    17 context-switches

    10.001722488 seconds time elapsed

可以看到在不使用pipeline執行高頻簡單命令時產生了大量的上下文切換,這無疑會占用大量的 cpu時間。

另一方面,pipeline雖然好用,但是每次 pipeline組裝的命令個數不能沒有節制,否則一次組裝 pipeline數據量過大,一方面會增加客戶端的等待時間,另一方面會造成一定的網絡阻塞,可以將一次包含大量命令的 pipeline拆分成多次較小的 pipeline來完成,比如可以將 pipeline的總發送大小控制在內核輸入輸出緩沖區大小之內(內核的輸入輸出緩沖區大小一般是 4K-8K,在不同操作系統中有所差異,可配置修改),同時控制在 單個 TCP 報文最大值 1460 字節之內。

最大傳輸單元(MTU — Maximum Transmission Unit)在以太網中的最大值是1500 字節,扣減 20 個字節的 IP頭和 20 個字節的 TCP頭,即 1460 字節

2.MemCache當 hotkey 本身可預估,且總大小可控時,我們可以考慮使用MemCache直接存儲:

  • 省去了 Redis 接入
  • 直接的內存讀取,保證高性能
  • 擺脫帶寬限制

但同時它也帶來了新的問題:

  • 在像k8s這樣的高可用多實例架構下,多 pod間的同步以及和原始數據庫的同步是一個大問題,很有可能導致 臟讀
  • 同樣是在多實例的情況下,會帶來很多的內存浪費。

同時 MemCache 相比于 Redis 也少了很多 feature ,可能不能滿足業務需求

FeatureRedisMemCache原生支持不同的數據結構??原生支持持久化??橫向擴展(replication)??聚合操作??支持高并發??

3.Redis 讀寫分離

當對 hotkey 的請求僅僅集中在讀上時,我們可以考慮讀寫分離的 Redis 集群方案(很多公有云廠商都有提供),針對 hotkey 的讀請求,新增read-only replica來承擔讀流量,原 replica作為熱備不提供服務,如下圖所示(鏈式復制架構)。

這里我們不展開講讀寫分離的其它優勢,僅針對讀多寫少的業務場景來說,使用讀寫分離的 Redis 提供了更多的選擇,業務可以根據場景選擇最適合的規格,充分利用每一個read-only replica的資源,且讀寫分離架構還有比較好的橫向擴容能力、客戶端友好等優勢。

規格QPS帶寬1 master8-10 萬讀寫10-48 MB1 master + 1 read-only replica10 萬寫 + 10 萬讀20-64 MB1 master + 3 read-only replica10 萬寫 + 30 萬讀40-128 MBn _ master + m _ read-only replican _ 100,000 write + m _ 100,000 read10(m+n) MB - 32(m+n) MB

當然我們也不能忽略讀寫分離架構的缺點,在有大量寫請求的場景中,讀寫分離架構將不可避免地產生延遲,這很有可能造成臟讀,所以讀寫分離架構不適用于讀寫負載都較高以及實時性要求較高的場景。

Key 集中過期

當 Redis 實例表現出的現象是:周期性地在一個小的時間段出現一波延遲高峰時,我們就需要 check 一下是否有大批量的 key 集中過期;那么為什么 key 集中過期會導致 Redis 延遲變大呢?

我們首先來了解一下 Redis 的過期策略是怎樣的,Redis 處理過期 key 的方式有兩種——被動方式和主動方式

被動方式

key過期的時候不刪除,每次從 Redis 獲取 key時檢查是否過期,若過期,則刪除,返回 null。

優點:刪除操作只發生在從數據庫取出 key 的時候發生,而且只刪除當前key,所以對 CPU 時間的占用是比較少的。

缺點:若大量的key在超出超時時間后,很久一段時間內,都沒有被獲取過,此時的無效緩存是永久暫用在內存中的,那么可能發生內存泄露(無效 key占用了大量的內存)。

主動方式

Redis 每100ms執行以下步驟:

  1. 抽樣檢查附加了TTL的 20 個隨機 key(環境變量 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP,默認為 20);
  2. 刪除抽樣中所有過期的key;
  3. 如果超過25%的 key過期,重復步驟 1。

優點:通過限制刪除操作的時長和頻率,來限制刪除操作對 CPU 時間的占用;同時解決被動方式中無效 key存留的問題。

缺點: 仍然可能有最高達到25%的無效key存留;在 CPU時間友好方面,不如 被動方式,主動方式會 block住主線程。

難點: 需要合理設置刪除操作的執行時長(每次刪除執行多長時間)和執行頻率(每隔多長時間做一次刪除,這要根據服務器運行情況和實際需求來決定)。

如果 Redis 實例配置為上面的主動方式的,當 Redis 中的 key 集中過期時,Redis 需要處理大量的過期 key;這無疑會增加 Redis 的 CPU 負載和內存使用,可能會使 Redis 變慢,特別當 Redis 實例中存在 bigkey 時,這個耗時會更久;而且這個耗時不會被記錄在slow log中:

解決方案:

為了避免這種情況,可以考慮以下幾種方法:

  • 盡量避免 key 集中過期,如果需要批量插入 key(如批量插入一批設置了同樣ExpireAt的 key),可以通過額外的小量隨機過期時間來打散 key 的過期時間;
  • 在 Redis 4.0 以上的版本中提供了 lazy-free 選項,當刪除過期 key 時,把釋放內存的操作放到后臺線程中執行,避免阻塞主線程。
lazyfree-lazy-expire yes

從監控的角度出發,我們還需要建立對expired_keys的實時監控和突增告警,以及時發出告警幫助我們定位到業務中的相關問題。

觸及 maxmemory

當我們的 Redis 實例達到了設置的內存上限時,我們也會很明顯地感知到 Redis 延遲增大。

究其原因,當 Redis 達到 maxmemory 后,如果繼續往 Redis 中寫入數據,Redis 將會觸發內存淘汰策略來清理一些數據以騰出內存空間,這個過程需要耗費一定的 CPU 和內存資源,如果淘汰過程中產生了大量的 Swap 交換或者內存回收,將會導致 Redis 變慢,甚至可能導致 Redis 崩潰。

常見的驅逐策略有以下幾種:

  • noeviction: 不刪除策略,達到最大內存限制時,如果需要更多內存,直接返回錯誤信息;大多數寫命令都會導致占用更多的內存(有極少數會例外, 如 DEL );
  • allkeys-lru: 所有 key 通用; 優先刪除最長時間未被使用(less recently used ,LRU) 的 key;
  • volatile-lru: 只限于設置了 expire 的部分; 優先刪除最長時間未被使用(less recently used ,LRU) 的 key;
  • allkeys-random: 所有 key 通用; 隨機刪除一部分 key;
  • volatile-random: 只限于設置了 expire 的部分; 隨機刪除一部分 key;
  • volatile-ttl: 只限于設置了 expire 的部分; 優先刪除剩余時間(time to live,TTL) 短的 key;
  • volatile-lfu: added in Redis 4, 從設置了 expire 的 key 中刪除使用頻率最低的 key;
  • allkeys-lfu: added in Redis 4, 從所有 key 中刪除使用頻率最低的 key。

最常用的驅逐策略是allkeys-lru / volatile-lru。

?? 需要注意的是:Redis 的淘汰數據的邏輯與刪除過期 key 的一樣,也是在命令真正執行之前執行的,也就是說它也會增加我們操作 Redis 的延遲,并且寫 OPS 越高,延遲也會越明顯。

另外,如果 Redis 實例中還存儲了 bigkey,那么在淘汰刪除 bigkey 釋放內存時,也會耗時比較久

解決方案:

為了避免 Redis 達到 maxmemory 后變慢,可以考慮以下幾種解決方案:

  1. 設置合理的 maxmemory,可以根據實際情況設置 Redis 的 maxmemory,避免 Redis 在運行過程中出現內存不足的情況(大白話就是加錢加內存);
  2. 開啟 Redis 的持久化功能,以將 Redis 中的數據持久化到磁盤中,避免數據丟失,并且在 Redis 重啟后可以快速地恢復加載數據;
  3. 使用 Redis 的分區功能,將 Redis 中的數據分散到多個 Redis 實例中,以減輕單個 Redis 實例內存淘汰的負載壓力;
  4. 與刪除過期 key 一樣,針對淘汰 key 也可以開啟layz-free,把淘汰 key 釋放內存的操作放到后臺線程中執行。
lazyfree-lazy-eviction yes 持久化耗時

為了保證 Redis 數據的安全性,我們可能會開啟后臺定時 RDB 和 AOF rewrite 功能:

而為了在后臺生成RDB文件,或者在啟用 AOF持久化的情況下追加寫只讀 AOF文件,Redis 都需要 fork一個子進程, fork操作(在主線程中運行)本身可能會導致延遲。

下圖分別是AOF持久化和 RDB持久化的流程圖:

在大多數類 Unix 系統上,fork的成本都很高,因為它涉及復制與進程相關聯的許多對象,尤其是與虛擬內存機制相關聯的 頁表。

例如,在linux/AMD64系統上,內存被劃分為 4kB的頁(如不開啟內存大頁);而為了將虛擬地址轉換為物理地址,每個進程存儲了一個頁表,該頁表包含該進程的地址空間每一頁的至少一個指針;一個大小為 24 GB的 Redis 實例就會需要一個 24 GB / 4 kB * 8 = 48 MB的頁表。

在執行后臺持久化時,就需要fork此實例,也就需要為頁表分配和復制 48MB的內存;這無疑會耗費大量 CPU 時間,特別是在部分虛擬機上,分配和初始化大內存塊本身成本就更高。

可以看到在Xen上運行的某些 VM的 fork耗時比在物理機上要高一個數量級到兩個數量級。

如何查看 fork 耗時:

我們可以在 redis-cli上執行 INFO 命令,查看 latest_fork_usec項:

INFO latest_fork_usec# 上一次 fork 耗時,單位為微秒latest_fork_usec:59477

這個時間就是主進程在 fork子進程期間,整個實例阻塞無法處理客戶端請求的時間;這個時間對于大多數業務來說無疑是不能過高的(如達到秒級)。

除了定時的數據持久化會生成 RDB之外,當主從節點第一次建立數據同步時,主節點也會創建子進程生成 RDB,然后發給從節點進行一次全量同步,所以,這個過程也會對 Redis 產生性能影響。

解決方案:

  1. 更改持久化模式 如果 Redis 的持久化模式為RDB,我們可以嘗試使用 AOF模式來減少持久化的耗時的突增(AOF rewrite 可以是多次的追加寫)。
  2. 優化寫入磁盤的速度 如果 Redis 所在的磁盤寫入速度較慢,我們可以嘗試將 Redis 遷移到寫入速度更快的磁盤上。
  3. 控制 Redis 實例的內存 用作緩存的 Redis 實例盡量在 10G 以下,執行 fork 的耗時與實例大小有關,實例越大,耗時越久。
  4. 避免虛擬化部署 Redis 實例不要部署在虛擬機上,fork 的耗時也與系統也有關,虛擬機比物理機耗時更久。
  5. 合理配置數據持久化策略 于低峰期在 slave 節點執行 RDB 備份;而對于丟失數據不敏感的業務(例如把 Redis 當做純緩存使用),可以關閉 AOF 和 AOF rewrite。
  6. 降低主從庫全量同步的概率 適當調大 repl-backlog-size參數,避免主從全量同步。
開啟內存大頁

在上面提到的定時 RDB 和 AOF rewrite 持久化功能中,除了fork本身帶來的頁表復制的耗時外,還會有 內存大頁帶來的延遲。

內存頁是用戶應用程序向操作系統申請內存的單位,常規的內存頁大小是 4KB,而 Linux 內核從 2.6.38 開始,支持了 內存大頁機制,該機制允許應用程序以 2MB大小為單位,向操作系統申請內存。

在開啟內存大頁的機器上調用bgsave或者 bgrewriteaoffork 出子進程后,此時 主進程依舊是可以接收寫請求的,而此時處理寫請求,會采用 Copy On Write(寫時復制)的方式操作內存數據(兩個進程共享內存大頁,僅需復制一份 頁表)。

在寫負載較高的 Redis 實例中,不斷處理寫命令將導致命令針對幾千個內存大頁(哪怕只涉及一個內存大頁上的一小部分數據更改),導致幾乎整個進程內存的COW,這將造成這些寫命令巨大的延遲,以及巨大的額外峰值內存。

同樣的,如果這個寫請求操作的是一個 bigkey,那主進程在拷貝這個 bigkey 內存塊時,涉及到的內存大頁會更多,時間也會更久,十惡不赦的 bigkey在這里又一次影響到了性能。

無疑在開啟AOF / RDB時,我們需要關閉內存大頁。我們可以使用以下命令查看是否開啟了內存大頁:

$ cat /sys/kernel/mm/transparent_hugepage/enabled[always] madvise never

如果該文件的輸出為 [always]或 [madvise],則透明大頁是啟用的;如果輸出為 [never],則透明大頁是禁用的

在 Linux 系統中,可以使用以下命令來關閉透明大頁:

typeCopy codeecho never > /sys/kernel/mm/transparent_hugepage/enabledecho never > /sys/kernel/mm/transparent_hugepage/defrag

第一行命令將透明大頁的使用模式設置為 never,第二行命令將透明大頁的碎片整理模式設置為 never;這樣就可以關閉透明大頁了。

AOF 和磁盤 I/O 造成的延遲

針對AOF(Append Only File)持久化策略來說,除了前面提到的 fork子進程追加寫文件會帶來性能損耗造成延遲。

首先我們來詳細看一下AOF的實現原理,AOF 基本上依賴兩個 系統調用來完成其工作;一個是 WRITE(2),用于將數據寫入 Append Only文件,另一個是 fDataync(2),用于刷新磁盤上的 內核文件緩沖區,以確保用戶指定的持久性級別,而 WRITE(2)和 fDatync(2)調用都可能是延遲的來源。

對 WRITE(2)來說,當系統范圍的磁盤緩沖區同步正在進行時,或者當輸出緩沖區已滿并且內核需要刷新磁盤以接受新的寫入時, WRITE(2)都會因此阻塞。

對fDataync(2)來說情況更糟,因為使用了許多內核和文件系統的組合,我們可能需要幾毫秒到幾秒的時間才能完成 fDataync(2),特別是在某些其它進程正在執行 I/O 的情況下;因此, Redis 2.4之后版本會盡可能在另一個線程執行 fDataync(2)調用。

解決方案:

最直接的解決方案當然是從 redis 配置出發,那么有哪些配置會影響到這兩個系統調用的執行策略呢。我們可以使用appendfsync配置,該配置項提供了三種磁盤緩沖區刷新策略

  • no當appendfsync被設置為 no時,redis 不會再執行 fsync,在這種情況下唯一的延遲來源就只有 WRITE(2)了,但這種情況很少見,除非磁盤無法處理 Redis 接收數據的速度(不太可能),或是磁盤被其他 I/O 密集型進程嚴重減慢。 這種方案對 Redis 影響最小,但當 Redis 所在的服務器宕機時,會丟失一部分數據,為了數據的安全性,一般我們也不采取這種配置。
  • everysec當appendfsync被設置為 everysec時,redis 每秒執行一次 fsync,這項工作在非主線程中完成?? 需要注意的是:對于用于追加寫入 AOF文件的 WRITE(2)系統調用,如果執行時 fsync仍在進行中,Redis 將使用一個緩沖區將 WRITE(2)調用延遲兩秒(因為在 Linux上,如果正在對同一文件進行 fsync, WRITE就會阻塞);但如果 fsync花費的時間太長,即使 fsync仍在進行中,Redis 最終也會執行 WRITE(2)調用,造成延遲。針對這種情況,Redis 提供了一個配置項,當子進程在追加寫入 AOF文件期間,可以讓后臺子線程不執行刷盤(不觸發 fsync 系統調用)操作,也就是相當于在追加寫 AOF期間,臨時把 appendfsync設置為了 no,配置如下: # AOF rewrite 期間,AOF 后臺子線程不進行刷盤操作

    # 相當于在這期間,臨時把 appendfsync 設置為了 none

    no-appendfsync-on-rewrite yes

    當然,開啟這個配置項,在追加寫 AOF期間,如果實例發生宕機,就會丟失更多的數據。

  • always當appendfsync被設置為 always時,每次寫入操作時都執行 fsync,完成后才會發送 response回客戶端(實際上,Redis 會嘗試將同時執行的多個命令聚集到單個 fsync 中)。在這種模式下,性能通常非常差,如果一定要達到這個持久化的要求并使用這個模式,就需要使用能夠在短時間內執行 fsync的 高速磁盤以及 文件系統實現。

大多數 Redis 用戶使用no或 everysec

并且為了最小化AOF帶來的延遲,最好也要 避免其他進程在同一系統中執行 I/O;當然,使用 SSD磁盤也會有所幫助(加 ),但通常情況下,即使是非 SSD 磁盤,如果磁盤沒有被其它進程占用,Redis 也能在寫入 Append Only File時保持良好的性能,因為 Redis 在寫入 Append Only File時不需要任何 seek操作。

我們可以使用strace命令查看 AOF帶來的延遲:

sudo strace -p $(pidof redis-server) -T -e trace=fdatasync

上面的命令將展示 Redis 在主線程中執行的所有fdatync(2)系統調用,但當 appendfsync配置選項設置為 everysec時,我們監控不到 后臺線程執行的 fdatync(2);為此我們需將 -f option加到上述命令中,這樣就可以看到子線程執行的 fdatync(2)了。

如果需要的話,我們還可以將write添加到 trace項中以監控 WRITE(2)系統調用:

sudo strace -p $(pidof redis-server) -T -e trace=fdatasync,write

但是,由于WRITE(2)也用于將數據寫入客戶端 socket以回復客戶端請求,該命令也會顯示許多與磁盤 I/O 無關的內容;為了解決這個問題我們可以使用以下命令:

sudo strace -f -p $(pidof redis-server) -T -e trace=fdatasync,write 2>&1 | grep -v '0.0' | grep -v unfinished SWAP 導致的延遲

Linux(以及許多其它現代操作系統)能夠將內存頁面從內存重新定位到磁盤,反之亦然,以便有效地使用系統內存。

如果內核將 Redis 內存頁從內存移動到SWAP 分區,則當存儲在該內存頁中的數據被 Redis 使用時(例如,訪問存儲在該內存頁中的 key),內核將停止 Redis 進程,以便將該內存頁移回內存;這是一個涉及 隨機I/O的緩慢磁盤操作(與訪問已在內存中的內存頁相比慢一到兩個數量級),并將導致 Redis 客戶端的異常延遲。

Linux 內核執行 SWAP主要有以下三個原因:

  • 系統已使用內存達到內存上限,有可能是 Redis 使用的內存超過了系統可用內存,也可能是其它進程導致的;
  • Redis 實例的數據集或數據集的一部分幾乎是完全空閑的(客戶端從未訪問過),因此內核可以交換內存中的空閑內存頁到磁盤;這種問題非常少見,因為即使是中等速度的實例也會經常接觸所有內存頁,迫使內核將所有內存頁保留在內存中;
  • 一些進程在系統上產生大量讀寫 I/O。因為文件通常是緩存的,所以它往往會給內核帶來增加文件系統緩存的壓力,從而產生SWAP;當然,這里說的進程也包括可能產生大文件的 Redis RDB和 AOF后臺線程。

我們可以通過以下命令查看 Redis 的SWAP情況:

首先我們獲取到 redis-server的 pid:

$ redis-cli info | grep process_idprocess_id:9

接下來查看Redis Swap的使用情況:

# $pid改為剛剛獲取到的redis-server的pidcat /proc/$pid/smaps | egrep '^(Swap|Size)'

產生類似下面的輸出:

Size: 316 kBSwap: 0 kBSize: 4 kBSwap: 0 kBSize: 8 kBSwap: 0 kBSize: 40 kBSwap: 0 kBSize: 132 kBSwap: 0 kBSize: 720896 kBSwap: 12 kBSize: 4096 kBSwap: 156 kBSize: 4096 kBSwap: 8 kBSize: 4096 kBSwap: 0 kBSize: 4 kBSwap: 0 kBSize: 1272 kBSwap: 0 kBSize: 8 kBSwap: 0 kBSize: 4 kBSwap: 0 kBSize: 16 kBSwap: 0 kBSize: 84 kBSwap: 0 kBSize: 4 kBSwap: 0 kBSize: 4 kBSwap: 0 kBSize: 8 kBSwap: 4 kBSize: 8 kBSwap: 0 kBSize: 4 kBSwap: 0 kBSize: 4 kBSwap: 4 kBSize: 144 kBSwap: 0 kBSize: 4 kBSwap: 0 kBSize: 4 kBSwap: 4 kBSize: 12 kBSwap: 4 kBSize: 108 kBSwap: 0 kBSize: 4 kBSwap: 0 kBSize: 4 kBSwap: 0 kBSize: 272 kBSwap: 0 kBSize: 4 kBSwap: 0 kB

每一行 Size 表示 Redis 所用的一塊內存大小,Size 下面的 Swap 就表示這塊 Size 大小的內存有多少數據已經被換到磁盤上了。

但如果存在SWAP比例較大的輸出,那么 Redis 的延遲很大可能就是 SWAP導致的。我們可以使用 vmstat命令進一步驗證:

$ vmstat 1procs -----------memory---------- ---swap-- -----io---- -system-- ----cpu---- r b swpd free buff cache si so bi bo in cs us sy id wa 0 0 3980 697932 147180 1406456 0 0 2 2 2 0 4 4 91 0 0 0 3980 697428 147180 1406580 0 0 0 0 19088 16104 9 6 84 0 0 0 3980 697296 147180 1406616 0 0 0 28 18936 16193 7 6 87 0 0 0 3980 697048 147180 1406640 0 0 0 0 18613 15987 6 6 88 0 2 0 3980 696924 147180 1406656 0 0 0 0 18744 16299 6 5 88 0 0 0 3980 697048 147180 1406688 0 0 0 4 18520 15974 6 6 88 0

我們看到si和 so這兩列,它們分別是內存中 SWAP到文件的 Size 以及從文件中 SWAP到內存的 Size;如果這兩列中存在非零值,則表示系統中存在 SWAP活動。

最后我們還可以使用IOStat命令查看系統的全局 I/O活動:

$ iostat -xk 1avg-cpu: %user %nice %system %iowait %steal %idle 13.55 0.04 2.92 0.53 0.00 82.95Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await svctm %utilsda 0.77 0.00 0.01 0.00 0.40 0.00 73.65 0.00 3.62 2.58 0.00sdb 1.27 4.75 0.82 3.54 38.00 32.32 32.19 0.11 24.80 4.24 1.85

解決方案:

這種情況下基本沒有什么可以多說的解決方案,無非就兩方面:

  1. 加內存(加 ),沒有什么是懟資源無法解決的;
  2. 減少業務側對 Redis 的使用量,包括調整過期時間、優化數據結構、調整緩存策略等等;

另一方面自然是做好 Redis 機器的內存監控以及SWAP事件監控,在內存不足及 SWAP事件激增時及時告警。

內存碎片

Redis 內存碎片率(used_memory_rss / used_memory)大于 1表示正在發生碎片,內存碎片率超過 1.5 表示碎片過多,Redis 實例消耗了其實際申請的物理內存的 150%的內存;另一方面,如果 內存碎片率低于 1,則表示 Redis 需要的內存多于系統上的可用內存,這會導致 SWAP 操作,其中內存交換到磁盤的 CPU 時間成本將導致 Redis 延遲顯著增加。

為什么會產生內存碎片:

主要有兩大原因:

  • redis自己實現的內存分配器:在 redis中新建 key-value值時, redis需要向操作系統申請內存,一般的進程在不需要使用申請的內存后,會直接釋放掉、歸還內存;但 redis不一樣, redis在使用完內存后并不會直接歸還內存,而是放在 redis自己實現的內存分配器中管理,這樣就不需要每次都向操作系統申請內存了,實現了高性能;但另一方面,未歸還的內存自然也就造成了 內存碎片。
  • value的更新: redis的每個 key-value對初始化的內存大小是最適合的,當這個 value改變的并且原來內存塊不適用的時候,就需要重新分配內存了;而重新分配之后,就會有一部分內存 redis無法正常回收,造成了 內存碎片。

我們可以通過執行 INFO 命令快速查詢到一個 Redis 實例的內存碎片率(mem_fragmentation_ratio):

[db0] > INFO memory# Memoryused_memory:215489640used_memory_human:205.51M...mem_fragmentation_ratio:1.13mem_fragmentation_bytes:27071448...

理想情況下,操作系統將在物理內存中分配一個連續的段,Redis 的內存碎片率等于 1 或略大于 1;碎片率過大會導致內存無法有效利用,進而導致 redis 頻繁進行內存分配和回收,從而導致用戶請求延遲,并且這個延遲是不會計入slowlog的。

如何清理內存碎片:

若在Redis < 4.0的版本,如果內存碎片率高于 1.5,直接重啟 Redis 實例就可以讓操作系統恢復之前因碎片而無法使用的內存;但在這種情況下,也許監控并發出告警就足夠了,直接重啟在大多數場景下并不適用;但當內存碎片率低于 1 時,我們就需要一個高級別的告警,以快速增加可用內存或減少內存使用量。

Redis ≥ 4.0開始,當 Redis 配置為使用包含的 jemalloc副本時,可以使用主動碎片整理功能;它可以配置為在碎片達到一定百分比時啟動,將數據復制到 連續的內存區域并釋放舊數據,從而減少內存碎片。

redis-cli開啟自動內存碎片清理:

127.0.0.1:6379[6]> config set activedefrag yesOK

redis.conf中相關的配置項:

# Enabled active defragmentation# 碎片整理總開關# activedefrag yes# Minimum amount of fragmentation waste to start active defrag# 內存碎片達到多少的時候開啟整理active-defrag-ignore-bytes 100mb# Minimum percentage of fragmentation to start active defrag# 碎片率達到百分之多少開啟整理active-defrag-threshold-lower 10# Maximum percentage of fragmentation at which we use maximum effort# 碎片率小余多少百分比開啟整理active-defrag-threshold-upper 100

當然,在面對一些復雜的場景時我們希望能根據自己設計的策略來進行內存碎片清理,redis也提供了手動內存碎片清理的命令:

127.0.0.1:6379> memory purgeOK 綁定 CPU 單核

很多時候,我們在部署服務時,為了提高服務性能,降低應用程序在多個 CPU 核心之間的上下文切換帶來的性能損耗,通常采用的方案是進程綁定 CPU 的方式提高性能。

但 Vanilla Redis并不適合綁定到單個 CPU 核心上。一般現代的服務器會有多個 CPU,而每個 CPU 又包含多個 物理核心,每個 物理核心又分為多個 邏輯核心,每個物理核下的邏輯核共用 L1/L2 Cache。而 Redis 會 fork出非常消耗 CPU 的后臺任務,如 BGSAVE或 BGREWRITEAOF、異步釋放 fd、異步 AOF 刷盤、異步 lazy-free 等等。如果把 Redis 進程只綁定了一個 CPU 邏輯核心上,那么當 Redis 在進行數據持久化時,fork 出的子進程會 繼承父進程的 CPU 使用偏好

此時子進程就要占用大量的 CPU 時間,與主進程發生 CPU 爭搶,進而影響到主進程服務客戶端請求,訪問延遲變大。

解決方案:

  1. 綁定多個邏輯核心如果你確實想要綁定 CPU,可以優化的方案是,不要讓 Redis 進程只綁定在一個 CPU 邏輯核上,而是綁定在多個邏輯核心上,而且,綁定的多個邏輯核心最好是同一個物理核心,這樣它們還可以共用 L1/L2 Cache。當然,即便我們把 Redis 綁定在多個邏輯核心上,也只能在一定程度上緩解主線程、子進程、后臺線程在 CPU 資源上的競爭,因為這些子進程、子線程還是會在這多個邏輯核心上進行切換,依舊存在性能損耗。
  2. 針對各個場景綁定固定的 CPU 邏輯核心Redis 6.0 以上的版本中,我們可以通過以下配置,對主線程、后臺線程、后臺 RDB 進程、AOF rewrite 進程,綁定固定的 CPU 邏輯核心:# Redis Server 和 IO 線程綁定到 CPU核心 0,2,4,6

    server_cpulist 0-7:2

    # 后臺子線程綁定到 CPU核心 1,3

    bio_cpulist 1,3

    # 后臺 AOF rewrite 進程綁定到 CPU 核心 8,9,10,11

    aof_rewrite_cpulist 8-11

    # 后臺 RDB 進程綁定到 CPU 核心 1,10,11

    # bgsave_cpulist 1,10-1

    如果使用的正好是 Redis 6.0 以上的版本,就可以通過以上配置,來進一步提高 Redis 性能;但一般來說,Redis 的性能已經足夠優秀,除非對 Redis 的性能有更加嚴苛的要求,否則不建議綁定 CPU。

總結

Redis 排障是一個循序漸進的復雜流程,涉及到 Redis 運行原理,設計架構以及操作系統,網絡等等。

作為業務方的 Redis 使用者,我們需要了解 Redis 的基本原理,如各個命令的時間復雜度、數據過期策略、數據淘汰策略以及讀寫分離架構等,從而更合理地使用 Redis 命令,并結合業務場景進行相關的性能優化。

Redis 在性能優秀的同時,又是脆弱的;作為 Redis 的運維者,我們需要在部署 Redis 時,需要結合實際業務進行容量規劃,預留足夠的機器資源,配置良好的網絡支持,還要對 Redis 機器和實例做好完善的監控,以保障 Redis 實例的穩定運行。

分享到:
標簽:Redis
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定