測快 5 倍的場景與邊界)
1. 從一次搜索延遲排查說起為什么我開始重新審視搜索引擎選型去年幫一個(gè)做內(nèi)容社區(qū)的朋友處理線上問題他們的檢索服務(wù)在晚高峰頻繁超時(shí)。技術(shù)棧是典型的 Elasticsearch 單集群扛全文檢索數(shù)據(jù)量其實(shí)不算夸張——兩千多萬條帖子加評論但每次帶過濾條件的模糊查詢 P99 能飆到 1.8 秒以上。運(yùn)維同學(xué)第一反應(yīng)是加節(jié)點(diǎn)從 3 個(gè)數(shù)據(jù)節(jié)點(diǎn)擴(kuò)到 6 個(gè)內(nèi)存翻倍結(jié)果延遲只降了不到 20%。這個(gè)投入產(chǎn)出比讓人很難接受。后來我們把查詢?nèi)罩纠鰜碇饤l分析發(fā)現(xiàn)問題根本不在集群規(guī)模而在于查詢模式大量請求是關(guān)鍵詞匹配 多字段過濾 按時(shí)間排序 分頁而且過濾字段的基數(shù)很高。Elasticsearch 在這種場景下需要把倒排索引命中的文檔逐個(gè)回表取字段、算分、排序再走一遍 fetch 階段開銷主要花在協(xié)調(diào)節(jié)點(diǎn)和數(shù)據(jù)節(jié)點(diǎn)之間的往返以及打分計(jì)算上。換句話說不是它不夠快而是它的架構(gòu)決定了這類查詢天然偏重。這次排查讓我開始認(rèn)真對比幾個(gè)主流方案其中 Redis Search 給我的印象最深。在同樣的數(shù)據(jù)集和查詢模式下它的響應(yīng)時(shí)間穩(wěn)定在 200 到 300 毫秒差不多是原來 Elasticsearch 的五分之一。標(biāo)題里說比 ES 快 5 倍這個(gè)數(shù)字不是拍腦袋來的而是在特定查詢形態(tài)下實(shí)測出來的量級(jí)差異。當(dāng)然這不代表 Redis Search 全面碾壓 Elasticsearch兩者定位不同適用的場景也不同。這篇文章我想把這次選型對比的完整思路、實(shí)測方法、踩過的坑和最終結(jié)論都攤開講清楚給正在做檢索選型或者被 ES 延遲困擾的同行一個(gè)參考。需要先說明的是本文討論的搜索引擎指的是面向應(yīng)用的結(jié)構(gòu)化/半結(jié)構(gòu)化數(shù)據(jù)檢索服務(wù)不是那種爬全網(wǎng)做索引的通用搜索引擎。Redis Search 和 Elasticsearch 都屬于前者只是設(shè)計(jì)哲學(xué)差異很大。下面我會(huì)從原理、實(shí)測、配置、優(yōu)化幾個(gè)層面展開。2. Redis Search 與 Elasticsearch 的架構(gòu)差異到底在哪2.1 一個(gè)內(nèi)存優(yōu)先一個(gè)磁盤優(yōu)先理解性能差異得先理解兩者對數(shù)據(jù)的態(tài)度。Elasticsearch 建立在 Lucene 之上核心是倒排索引 段合并segment merge的磁盤存儲(chǔ)模型。文檔寫入后先進(jìn)內(nèi)存 buffer再 refresh 成 segment 落到文件系統(tǒng)緩存最終 fsync 到磁盤。查詢時(shí)如果數(shù)據(jù)不在 page cache 里就要走磁盤 IO。它的設(shè)計(jì)目標(biāo)是海量數(shù)據(jù)的持久化檢索能扛 TB 級(jí)甚至 PB 級(jí)數(shù)據(jù)代價(jià)是查詢路徑長、延遲下限高。Redis Search 則是純內(nèi)存索引。它作為 Redis 的一個(gè)模塊運(yùn)行索引結(jié)構(gòu)直接放在內(nèi)存里查詢時(shí)不需要磁盤 IO也沒有 segment 合并帶來的寫放大。數(shù)據(jù)持久化靠 Redis 自身的 RDB/AOF 機(jī)制兜底但檢索路徑本身是內(nèi)存操作。這就解釋了為什么它的延遲能壓到毫秒級(jí)——沒有磁盤這一層物理上限就高出一截。打個(gè)比方Elasticsearch 像是一個(gè)藏書百萬冊的圖書館你要找書得先查目錄卡、再去書架取書流程規(guī)范但慢Redis Search 像是一個(gè)把所有書都攤在桌面上的書房伸手就能拿到但桌面大小有限書太多就放不下。2.2 查詢執(zhí)行路徑的差異具體到一次查詢的執(zhí)行差異更明顯。Elasticsearch 的查詢分兩個(gè)階段query phase和fetch phase。query phase 在每個(gè)分片上執(zhí)行算出命中文檔的 doc id 和排序值返回給協(xié)調(diào)節(jié)點(diǎn)協(xié)調(diào)節(jié)點(diǎn)歸并排序后再發(fā)起 fetch phase 去各分片取實(shí)際字段。跨分片通信、打分BM25 或自定義、歸并都是開銷。即使你用了 filter 不走打分fetch 階段的網(wǎng)絡(luò)往返依然存在。Redis Search 的查詢在單個(gè) Redis 實(shí)例內(nèi)完成集群模式下按 key 分片沒有獨(dú)立的協(xié)調(diào)節(jié)點(diǎn)做二次歸并。它用的是自己的索引結(jié)構(gòu)支持全文、數(shù)值、標(biāo)簽、地理等多種字段類型查詢時(shí)直接在內(nèi)存索引上做交集、并集運(yùn)算然后返回結(jié)果。路徑短、無跨節(jié)點(diǎn)歸并、無磁盤 IO這是它快的根本原因。2.3 兩者的能力邊界對比不能只看速度得看能不能滿足需求。我整理了一張對比表維度Redis SearchElasticsearch數(shù)據(jù)規(guī)模受內(nèi)存限制通常千萬級(jí)可擴(kuò)展到 TB/PB 級(jí)查詢延遲毫秒到百毫秒級(jí)百毫秒到秒級(jí)全文檢索能力支持分詞器生態(tài)較弱極強(qiáng)分詞、同義詞、高亮完善聚合分析支持基礎(chǔ)聚合非常強(qiáng)大適合 OLAP 場景持久化依賴 Redis RDB/AOF原生持久化副本機(jī)制成熟運(yùn)維復(fù)雜度低復(fù)用 Redis 運(yùn)維體系高需要專門的集群運(yùn)維成本內(nèi)存成本高存儲(chǔ)成本低內(nèi)存需求相對小這張表的核心結(jié)論是Redis Search 適合數(shù)據(jù)量可控、查詢延遲敏感、以過濾和排序?yàn)橹鞯膱鼍癊lasticsearch 適合數(shù)據(jù)量大、檢索邏輯復(fù)雜、需要深度聚合的場景。選型不是比誰快而是比誰匹配。3. 實(shí)測對比我是怎么測出 5 倍差距的3.1 測試環(huán)境與數(shù)據(jù)集構(gòu)造為了讓對比有說服力我搭了一套盡量公平的環(huán)境。兩臺(tái)機(jī)器配置相同8 核 16GSSD 盤千兆內(nèi)網(wǎng)。一臺(tái)跑 Elasticsearch 7.17 單節(jié)點(diǎn)一臺(tái)跑 Redis 7.2 加 Redis Stack內(nèi)置 RediSearch 2.8。數(shù)據(jù)集用腳本生成 2000 萬條模擬帖子每條包含標(biāo)題、正文、作者 ID、分類標(biāo)簽、發(fā)布時(shí)間、點(diǎn)贊數(shù)等字段。Elasticsearch 的索引 mapping 里標(biāo)題和正文用 text 類型配 ik 分詞標(biāo)簽和作者用 keyword時(shí)間用 date點(diǎn)贊數(shù)用 integer。Redis Search 這邊用 FT.CREATE 建索引標(biāo)題正文用 TEXT 類型標(biāo)簽作者用 TAG時(shí)間用 NUMERIC點(diǎn)贊數(shù)也用 NUMERIC。兩邊字段語義對齊保證查詢可比。提示數(shù)據(jù)集生成時(shí)要注意字段值的分布。如果所有標(biāo)簽都集中在少數(shù)幾個(gè)值上過濾的選擇性會(huì)很差測出來的延遲不能反映真實(shí)場景。我特意讓標(biāo)簽分布接近長尾模擬真實(shí)社區(qū)的分類分布。3.2 查詢場景設(shè)計(jì)與壓測方法我設(shè)計(jì)了四類查詢覆蓋典型使用場景純關(guān)鍵詞匹配在標(biāo)題和正文里搜一個(gè)詞返回前 20 條。關(guān)鍵詞 標(biāo)簽過濾 時(shí)間排序搜詞限定某個(gè)分類按發(fā)布時(shí)間倒序。多標(biāo)簽過濾 數(shù)值范圍 分頁限定多個(gè)標(biāo)簽點(diǎn)贊數(shù)大于某值翻到第 10 頁。聚合統(tǒng)計(jì)按分類統(tǒng)計(jì)命中數(shù)量。壓測工具用 wrk 和自寫的 Python 腳本每類查詢預(yù)熱 1000 次后正式跑 60 秒記錄 P50、P95、P99 和 QPS。并發(fā)從 10 逐步加到 200觀察延遲曲線。3.3 實(shí)測數(shù)據(jù)與結(jié)果解讀結(jié)果如下表單位毫秒查詢類型ES P50ES P99Redis Search P50Redis Search P99純關(guān)鍵詞45320835關(guān)鍵詞過濾排序180180022210多標(biāo)簽范圍分頁260240035280聚合統(tǒng)計(jì)12090018150可以看到查詢越復(fù)雜、過濾條件越多Redis Search 的優(yōu)勢越明顯。純關(guān)鍵詞查詢差距約 5 到 9 倍帶過濾排序的復(fù)雜查詢差距能到 8 倍以上。標(biāo)題里說的快 5 倍是個(gè)保守說法實(shí)際在復(fù)雜查詢下差距更大。但這里有個(gè)關(guān)鍵前提Redis Search 的數(shù)據(jù)全部在內(nèi)存里而 Elasticsearch 的數(shù)據(jù)量已經(jīng)超過了 page cache 能緩存的范圍。如果 ES 的數(shù)據(jù)集小到能全量放進(jìn)內(nèi)存差距會(huì)縮小。所以這個(gè)對比的準(zhǔn)確表述是在數(shù)據(jù)量超過內(nèi)存緩存能力、且查詢以過濾排序?yàn)橹鞯膱鼍跋翿edis Search 比 Elasticsearch 快約 5 倍。3.4 內(nèi)存占用與成本核算快是有代價(jià)的。2000 萬條數(shù)據(jù)Redis Search 索引加原始數(shù)據(jù)占了約 11G 內(nèi)存。同樣數(shù)據(jù)在 Elasticsearch 里堆內(nèi)存給了 8G磁盤占用約 6G但查詢時(shí)大量依賴 page cache實(shí)際機(jī)器內(nèi)存占用也在 10G 以上。單看內(nèi)存Redis Search 略高但考慮到它省掉了獨(dú)立的集群協(xié)調(diào)節(jié)點(diǎn)和副本開銷在小規(guī)模場景下總成本反而可能更低。如果數(shù)據(jù)量繼續(xù)漲到 1 億條Redis Search 需要的內(nèi)存會(huì)到 50G 以上這時(shí)候單機(jī)就吃不消了得上集群分片。而 Elasticsearch 在這個(gè)量級(jí)依然從容。所以成本拐點(diǎn)大概在幾千萬條數(shù)據(jù)、內(nèi)存 32G 到 64G 這個(gè)區(qū)間超過之后 ES 的性價(jià)比開始反超。4. 把 Redis Search 跑起來從建索引到查詢的完整操作4.1 環(huán)境準(zhǔn)備與模塊加載Redis Search 現(xiàn)在通過 Redis Stack 分發(fā)最省事的方式是用官方 Docker 鏡像docker run -d --name redis-stack \ -p 6379:6379 \ -p 8001:8001 \ redis/redis-stack:latest8001 端口是 RedisInsight 可視化界面調(diào)試索引很方便。如果你用源碼編譯需要單獨(dú)加載 RediSearch 模塊在 redis.conf 里加loadmodule /path/to/redisearch.so。我建議直接用 Redis Stack省去模塊版本兼容的麻煩。驗(yàn)證模塊是否加載成功redis-cli MODULE LIST輸出里應(yīng)該能看到search模塊及其版本號(hào)。如果沒看到說明模塊沒加載上后面所有 FT.* 命令都會(huì)報(bào)錯(cuò)。4.2 用 FT.CREATE 設(shè)計(jì)索引結(jié)構(gòu)建索引是核心步驟字段類型選錯(cuò)了后面查詢會(huì)很別扭。以帖子場景為例FT.CREATE post_idx ON HASH PREFIX 1 post: \ SCHEMA \ title TEXT WEIGHT 2.0 \ content TEXT \ author TAG \ category TAG \ publish_ts NUMERIC SORTABLE \ likes NUMERIC SORTABLE幾個(gè)關(guān)鍵點(diǎn)解釋一下。ON HASH表示索引的是 Redis Hash 類型的數(shù)據(jù)PREFIX 1 post:表示只索引 key 以post:開頭的 Hash。WEIGHT 2.0讓標(biāo)題在全文檢索里權(quán)重更高符合標(biāo)題匹配更重要的直覺。SORTABLE標(biāo)記的字段才能用于排序沒標(biāo) SORTABLE 的字段排序會(huì)報(bào)錯(cuò)這是新手最容易踩的坑。TAG 類型適合精確匹配的枚舉值比如分類、作者它比 TEXT 更省內(nèi)存也更快。4.3 數(shù)據(jù)寫入與索引同步數(shù)據(jù)寫入有兩種方式。一種直接寫 HashRediSearch 會(huì)自動(dòng)索引HSET post:1001 title Redis Search 實(shí)戰(zhàn) content ... \ author u123 category tech publish_ts 1700000000 likes 42另一種是批量導(dǎo)入用FT.CREATE配合 pipeline 批量 HSET效率高很多。我實(shí)測 2000 萬條數(shù)據(jù)用 pipeline 分批寫入大概 15 分鐘能完成單條寫入則要幾個(gè)小時(shí)。注意寫入時(shí)如果 Hash 的字段名和索引 schema 對不上該字段不會(huì)被索引但也不會(huì)報(bào)錯(cuò)。排查為什么搜不到時(shí)第一件事就是用FT.INFO post_idx看索引里的文檔數(shù)和字段配置。4.4 查詢語法與分頁實(shí)踐基礎(chǔ)查詢FT.SEARCH post_idx title:Redis category:{tech} \ SORTBY publish_ts DESC LIMIT 0 20字段:值是字段限定語法{tech}是 TAG 匹配多個(gè) TAG 用|表示或。數(shù)值范圍用likes:[100 inf]。分頁靠LIMIT offset num但要注意深分頁性能會(huì)下降因?yàn)?offset 越大需要跳過的結(jié)果越多。深分頁的優(yōu)化思路是用游標(biāo)式分頁記錄上一頁最后一條的排序值下一頁用范圍查詢接著取FT.SEARCH post_idx category:{tech} publish_ts:[(1700000000 inf] \ SORTBY publish_ts ASC LIMIT 0 20這樣每次都是從索引的某個(gè)位置開始取不隨頁碼增大而變慢。這個(gè)技巧在 ES 里叫 search_after思路是一樣的。5. 那些文檔里不會(huì)寫的踩坑記錄5.1 中文分詞的現(xiàn)實(shí)困境Redis Search 內(nèi)置的分詞器對中文支持有限默認(rèn)按空格和標(biāo)點(diǎn)切分中文長句會(huì)被當(dāng)成一個(gè)整體導(dǎo)致搜搜索引擎匹配不到推薦一個(gè)搜索引擎。解決辦法有兩個(gè)一是寫入前用外部分詞庫如 jieba預(yù)處理把詞用空格拼好再存二是用 Redis Search 2.6 之后支持的自定義分詞器接口。我選的是第一種在應(yīng)用層做分詞。代價(jià)是索引里存的是分詞后的文本原文需要另存一份用于展示。這個(gè)取舍要提前想清楚不要指望 Redis Search 開箱即用地處理好中文這是它和 Elasticsearch 差距最大的地方。5.2 內(nèi)存碎片與索引膨脹跑了一段時(shí)間后發(fā)現(xiàn)內(nèi)存漲得比預(yù)期快。用MEMORY DOCTOR一看碎片率到了 1.6。原因是頻繁更新文檔時(shí)舊索引項(xiàng)不會(huì)立即回收會(huì)留下碎片。解決辦法是配置activedefrag yes開啟主動(dòng)碎片整理或者定期用FT.CREATE重建索引。另外TAG 字段如果基數(shù)極高比如把用戶 ID 做成 TAG索引體積會(huì)膨脹得厲害。TAG 適合基數(shù)在幾千到幾萬級(jí)別的字段超過這個(gè)量級(jí)要考慮是否真的需要索引它。5.3 持久化與數(shù)據(jù)安全的平衡Redis Search 的數(shù)據(jù)在內(nèi)存里一旦進(jìn)程崩潰且沒有持久化索引就沒了。生產(chǎn)環(huán)境必須開 AOFappendfsync everysec是延遲和安全的折中點(diǎn)。但要注意AOF 重放時(shí) RediSearch 會(huì)重建索引數(shù)據(jù)量大時(shí)啟動(dòng)會(huì)很慢。我實(shí)測 2000 萬條數(shù)據(jù) AOF 重放要 8 分鐘左右這段時(shí)間服務(wù)不可用。如果對可用性要求高得做主從復(fù)制從節(jié)點(diǎn)預(yù)熱好再切流量。或者接受索引可重建的思路把原始數(shù)據(jù)存在 MySQL 等持久化存儲(chǔ)里Redis 只作為檢索加速層崩了就從源頭重建。5.4 集群模式下的分片陷阱單機(jī)內(nèi)存扛不住時(shí)會(huì)上 Redis Cluster。但 RediSearch 在集群模式下每個(gè)分片只能搜自己那部分?jǐn)?shù)據(jù)跨分片的聚合和排序需要客戶端自己做。這意味著你沒法像單機(jī)那樣一條命令拿到全局 Top 20得從每個(gè)分片各取 20 條再歸并。這個(gè)邏輯得在應(yīng)用層實(shí)現(xiàn)復(fù)雜度不低。我的建議是能用單機(jī)大內(nèi)存解決就別上集群。現(xiàn)在單機(jī) 256G 內(nèi)存的機(jī)器很常見能撐到上億條數(shù)據(jù)比集群運(yùn)維省心得多。6. 什么場景該選它什么場景該繞道6.1 優(yōu)先考慮 Redis Search 的幾種情況如果你的場景符合下面幾條Redis Search 值得認(rèn)真評估數(shù)據(jù)量在千萬級(jí)以內(nèi)內(nèi)存放得下查詢以關(guān)鍵詞 過濾 排序 分頁為主聚合需求簡單已經(jīng)在用 Redis想復(fù)用現(xiàn)有運(yùn)維體系不想再維護(hù)一套 ES 集群對延遲敏感P99 要求控制在幾百毫秒內(nèi)團(tuán)隊(duì)規(guī)模小沒有專職的搜索運(yùn)維。我那個(gè)內(nèi)容社區(qū)的朋友最后就是切到了 Redis Search把 ES 集群下線機(jī)器成本降了一半延遲還更穩(wěn)了。他們的數(shù)據(jù)量正好在 Redis Search 的舒適區(qū)。6.2 老老實(shí)實(shí)用 Elasticsearch 的場景反過來這些情況別硬上 Redis Search數(shù)據(jù)量上億甚至更多內(nèi)存成本扛不住需要復(fù)雜的分詞、同義詞、拼寫糾錯(cuò)、高亮有大量聚合分析、報(bào)表類查詢需要跨字段的相關(guān)性打分調(diào)優(yōu)數(shù)據(jù)必須強(qiáng)持久化不能接受重建索引的窗口。Elasticsearch 在這些方面積累深厚不是 Redis Search 短期能替代的。選型的本質(zhì)是匹配不是追新。6.3 混合架構(gòu)其實(shí)可以都要還有一種思路是兩者結(jié)合。用 Elasticsearch 做全量數(shù)據(jù)的深度檢索和分析用 Redis Search 做熱數(shù)據(jù)的快速檢索。比如社區(qū)場景里最近 7 天的帖子放 Redis Search 保證實(shí)時(shí)性歷史數(shù)據(jù)放 ES 做歸檔查詢。查詢時(shí)先查 Redis沒有再回源 ES。這種架構(gòu)的復(fù)雜度在于數(shù)據(jù)同步和一致性維護(hù)適合有一定工程能力的團(tuán)隊(duì)。如果團(tuán)隊(duì)小我建議先單用一個(gè)把場景跑透再考慮混合。7. 性能調(diào)優(yōu)的幾個(gè)實(shí)操參數(shù)7.1 索引層面的關(guān)鍵配置建索引時(shí)幾個(gè)參數(shù)對性能影響很大。MAXTEXTFIELDS允許索引更多 TEXT 字段但會(huì)增加內(nèi)存NOOFFSETS關(guān)閉詞偏移量存儲(chǔ)省內(nèi)存但失去高亮能力NOHL關(guān)閉高亮NOFIELDS關(guān)閉字段級(jí)過濾。如果不需要高亮加上NOOFFSETS NOHL能省 20% 到 30% 內(nèi)存。FT.CREATE post_idx ON HASH PREFIX 1 post: \ NOOFFSETS NOHL \ SCHEMA title TEXT content TEXT category TAG publish_ts NUMERIC SORTABLE7.2 查詢層面的優(yōu)化技巧盡量用 TAG 代替 TEXT 做過濾TAG 是哈希匹配比全文匹配快得多避免前綴通配符查詢title:red*這類查詢無法用索引會(huì)退化成掃描限制返回字段用RETURN 2 title publish_ts只取需要的字段減少網(wǎng)絡(luò)傳輸合理設(shè)置 LIMIT不要一次取幾千條深分頁用游標(biāo)。7.3 監(jiān)控指標(biāo)該看什么生產(chǎn)環(huán)境要盯幾個(gè)指標(biāo)FT.INFO里的indexing狀態(tài)是否在重建、num_docs文檔數(shù)是否異常、hash_indexing_failures索引失敗次數(shù)。Redis 層面看used_memory、mem_fragmentation_ratio、evicted_keys。如果出現(xiàn) evicted_keys 大于 0說明內(nèi)存不夠開始淘汰數(shù)據(jù)了索引會(huì)不完整這是最危險(xiǎn)的信號(hào)。我一般會(huì)配一個(gè)告警碎片率超過 1.5 或者淘汰鍵數(shù)大于 0 就通知。這兩個(gè)指標(biāo)能提前發(fā)現(xiàn)大部分內(nèi)存相關(guān)問題。8. 我個(gè)人的選型體會(huì)折騰完這一輪我最大的感受是沒有銀彈只有匹配。Redis Search 快是因?yàn)樗褦?shù)據(jù)放在內(nèi)存、砍掉了磁盤和跨節(jié)點(diǎn)歸并這些環(huán)節(jié)代價(jià)是內(nèi)存成本和規(guī)模上限。Elasticsearch 慢一點(diǎn)換來的是海量數(shù)據(jù)能力和豐富的檢索功能。標(biāo)題說比 ES 快 5 倍這個(gè)結(jié)論成立的前提是場景對得上——數(shù)據(jù)量可控、查詢以過濾排序?yàn)橹?、能接受?nèi)存成本。如果你正被 ES 的延遲困擾我的建議是先別急著換把查詢?nèi)罩纠鰜矸治鲆幌?。很多時(shí)候延遲高是因?yàn)椴樵儗懛ㄓ袉栴}比如該用 filter 的地方用了 query、該用 keyword 的地方用了 text、深分頁沒優(yōu)化。這些改完可能就夠用了。如果確實(shí)是架構(gòu)層面的瓶頸再考慮 Redis Search并且一定要用真實(shí)數(shù)據(jù)做壓測別信任何脫離場景的性能數(shù)字包括我這篇里的。最后分享一個(gè)小技巧遷移時(shí)不要一次性切先用雙寫的方式讓兩邊數(shù)據(jù)同步查詢走灰度對比一段時(shí)間的結(jié)果一致性和延遲表現(xiàn)確認(rèn)沒問題再切流量。這樣即使 Redis Search 那邊有問題也能快速回滾到 ES風(fēng)險(xiǎn)可控。