作者: Anton Borisov
開源無國界,在本期「StarRocks 全球用户精選案例」中,我們走進 Fresha——全球領先的美業、健康與自我護理行業一站式平台,服務於全球數以百萬計的消費者與商家。
隨着業務規模的快速增長,Fresha 曾面臨典型的架構失配挑戰:Postgres 頻繁因 OLAP 需求過載,而 Snowflake 在應對高頻準實時分析時又面臨成本與時效性限制。為此,Fresha 引入了 StarRocks,在保持 Lakehouse 為唯一事實源的前提下,構建了兼具“聯邦查詢”與“內部表加速”的混合架構。
自 2025 年春季上線以來,Fresha 成為英國最早在生產環境規模化落地 StarRocks 的先行者之一。本文將深度拆解其選型邏輯、落地架構以及性能優化等方面的實戰經驗。
現狀與挑戰
到 2024 年中期,Fresha 的數據平台呈現出一種極其矛盾的狀態:雖然原有的技術棧勉強能跑通,但每個組件都在承擔着超出其設計初衷的工作:
- Postgres (OLTP) :原本用於支撐面向用户的業務系統,卻承擔了大量的 Ad-hoc 和產品儀表盤需求。寬表 Join 和重度聚合導致了 Head-of-line blocking 和 Noisy neighbor效應,甚至偶爾會引發“為什麼下單接口變慢了?”這種生產事故。
- Snowflake (BI/數據導出) :雖然能很好地處理傳統 BI 看板和大規模數據導出,但在應對高頻交互、準實時的產品及運營分析時,無論在成本還是響應速度上都難以為繼。
這種架構失配導致高峯期系統響應變慢、儀表盤延遲波動。我們意識到,必須尋找一個能夠同時填補兩個缺口的分析引擎:
- 在不消耗 Postgres 資源的前提下,能夠高效處理海量歷史數據。
- 支持標準協議以降低遷移成本,且隨着業務增長,性能與擴展性需保持高度可預測。
核心訴求:填補拼圖的缺口
為此,我們為理想的分析工具劃定了幾個硬性約束:
- 將歷史分析需求從 OLTP 路徑中剝離。
- 堅持開放格式優先(基於對象存儲的 Iceberg/Paimon),將 Lakehouse 作為唯一事實源,在不增加 Postgres 存儲負擔的前提下處理歷史數據。
- 支持 MySQL 協議、標準驅動,儘量減少改造與工具替換成本。
- 擴展能力可預期:能從容應對流量高峯,而非耗費數天進行容量規劃。
- 核心鏈路達到秒級至分鐘級時延,其餘鏈路保持分鐘級。
- 低運維複雜度:減少定製化管道與額外系統。
為什麼選擇 StarRocks?
基於上述要求,StarRocks 憑藉其混合查詢模式脱穎而出:它既能通過外部 Catalog 實現對開放格式數據的聯邦查詢(保證廣度),又支持將時序敏感的指標直接接入內部列存表(保證深度與性能)。
- 原生列式存儲:支持支持明細、聚合及主鍵模型,並支持高吞吐寫入(如 Flink 或 Routine Load)。這是實現核心指標“準實時”可用的最短路徑。
- 湖倉加速能力:通過 Catalog 直接讀 Iceberg / Paimon / Hive 等開放表格式,並將 Filter與 Projection 下推以減少對象存儲掃描開銷——這是處理大規模歷史數據的理想方案。
- 物化視圖自動查詢改寫:可定義增量彙總或預關聯,優化器會自動將符合條件的查詢改寫為命中對應的物化視圖。
- 存算分離架構:計算資源可按需彈性擴縮,無需在節點間重新平衡數據,確保了業務高峯期成本與時延的可預測性。
- MySQL 協議與生態兼容:與常見 BI 工具及主流客户端庫開箱即用,工程師可以快速接入與落地。
(StarRocks 採用存算分離架構:客户端通過 MySQL 協議連接到 FE 節點(Leader、Follower/Observer),由 FE 負責 Catalog 管理與查詢協調;CN 節點承擔實際查詢執行並進行數據緩存。持久化數據存放在分佈式存儲中,因此擴展算力時只需要新增 CN 節點,無需對存儲數據做重分佈。)
新架構一覽
你可以將整個平台想象成一條統一的數據攝取主幹,並延伸出三條鏈路:一條是進入 StarRocks 內部表的實時鏈路,一條是進入 Iceberg/Paimon 的歷史鏈路,以及一條進入 Elasticsearch 的搜索鏈路。StarRocks 居中作為統一的 SQL 入口。工程師通過標準的 MySQL 協議接入,即可實現跨三條鏈路的關聯查詢,而無需關注數據的存儲位置。
(Fresha 的高層數據流如下:以 Postgres 為主的數據源通過 Debezium + Schema Registry 接入 Kafka;計算層使用 Flink 與 Spark;湖倉層採用 Iceberg + Paimon;下游由多個 Sink 承接,其中 StarRocks 作為統一的 SQL 查詢入口。StarRocks 通過外部 Catalog 訪問湖倉數據,計算層則分別服務實時與歷史鏈路,對湖倉進行讀寫。)
- 寫入主幹(Ingestion spine)。 它實時捕獲 Postgres 的 CDC 變更事件並流向 Kafka,同時配合 Schema Registry 使用 Avro 格式進行序列化。這為我們提供了一個強類型、可平滑演進的事件封裝層,既滿足了 CDC 需求,也為下游消費者構建了一個單一、可靠的數據主幹。
Kafka 在這裏承擔了扇出點(Fan-out point)的角色:Flink 與 Spark 從同一個事實源獲取數據,並根據不同的訪問模式,將數據寫入到最適合的存儲引擎中。
- 實時鏈路(StarRocks 內部表)。針對時效性達“秒級”、且用户體驗極度依賴尾部延遲(Tail Latency)穩定性的場景,Flink 會將數據直接寫入 StarRocks 的內部列存表。
在表模型選擇上,我們針對不同業務場景進行了適配:主鍵模型(Primary Key)用於承載需要實時保新的變更流;聚合模型(Aggregate Key)用於執行指標預計算(如 Sum/Count/Min/Max);而明細模型(Duplicate Key)則負責接收那些後續需要進行 Compaction 或異步彙總的流式數據。
這種設計刻意壓縮了數據路徑:即“Kafka → Flink → StarRocks → Dashboard/API”的極短鏈路。通過將對象存儲從核心路徑中剝離,我們能夠依靠 StarRocks 的橫向擴展來應對流量峯值,而不必受限於遠程存儲的 List 或 Get 請求。
在這些內部表之上,我們為常用的聚合與預關聯定義了物化視圖。StarRocks 的優化器會自動將符合條件的原始查詢透明改寫,使其直接命中這些物化視圖。這使得我們的研發團隊只需編寫最基礎的 SQL 即可。
- 歷史鏈路(Iceberg/Paimon)。並非所有查詢都具有極高的緊迫性,而且幾乎沒有哪類查詢僅關注“當下”。我們將業務側 CDC 數據落地到 Paimon;同時,Flink 和 Spark 負責將長期的事實表與緩慢變化維(SCD)寫入對象存儲上的 Iceberg。其中,Spark 處理更為繁重的工作: backfill、repair、compaction ,以及生成跨大跨度時間範圍的一致性快照。
這種模式為我們提供了低成本且持久的歷史存儲,並支持完善的 Schema 演進和分區機制;同時也確保了 Lakehouse 作為唯一事實源的地位。StarRocks 通過外部 Catalog 直接接入 Iceberg 和 Paimon,使得歷史查詢能夠在不遷移數據的情況下,直接在開放格式上進行聯邦查詢。當回灌數據落地後,我們可以重建或刷新 StarRocks 內部相關的物化視圖,使歷史數據的查詢體驗儘可能接近實時鏈路。
- 搜索鏈路(Elasticsearch)。部分工作負載並非嚴格的關係型數據,例如:模糊匹配、前綴/後綴搜索、分詞以及相關性評分。我們利用 Flink 或 Spark,從相同的 Kafka/Lakehouse 事實源中將這類數據索引至 Elasticsearch,隨後通過 StarRocks 的 experimental Elasticsearch Catalog 將其暴露給開發人員。
這一方案的核心價值不在於引入了 ES,而在於開發人員不再需要直接調用 ES 接口。從他們的視角來看,一個搜索密集型的索引僅僅是另一張可以被 SQL 關聯查詢的“表”,且使用的仍是原有的分析連接。這種設計降低了認知負荷,同時也實現了基礎設施接入層的集中化。
(以 Kafka 為中心的“寫入主幹”由 Debezium + Schema Registry 提供強類型的 CDC 數據,並向外分為三條鏈路:實時鏈路(Flink → StarRocks 內部表 + MV → Dashboard/API);歷史鏈路(Flink → Paimon;Spark/Flink → Iceberg;StarRocks 通過外部 Catalog 聯邦查詢並按需刷新 MV);搜索鏈路(Spark/Flink → Elasticsearch;通過 ES Catalog 以 SQL 方式進行關聯查詢)。)
StarRocks 作為統一入口,通過一個 MySQL 端點,實現了熱數據、歷史數據與搜索鏈路的統一:時延最敏感的數據切片落在內部列式表;長期事實與維度數據保留在 Iceberg/Paimon;文本密集型數據寫入 Elasticsearch。StarRocks 通過外部 Catalog 統一接入這三類存儲,因此工程師只需編寫標準的 SQL,無需關注數據的具體存放位置。
StarRocks 的優化器與物化視圖改寫機制會自動規劃最優查詢路徑:優先命中內部表或物化視圖,必要時則下推至 Lakehouse 或 Elasticsearch 執行。我們採用存算分離模式,實現了計算與存儲解耦,在應對業務高峯擴縮容時無需重分佈數據,保障了尾部延遲的穩定與運維的極簡。數據回灌統一落入 Lakehouse,並通過聯邦查詢或物化視圖刷新實現感知。這種架構確保了底層數據演進的同時,上層查詢接口也能保持穩定。
(在生產環境中按數據新鮮度做了分層:Hot(秒級)通過 Kafka → Flink → StarRocks 內部表;Warm(分鐘級)由 StarRocks 直接查詢 Iceberg/Paimon(聯邦查詢,必要時配合 MV 加速);Deep history(深度歷史)保留在 Iceberg/Paimon 中,由 Spark 以版本化快照方式進行回灌與補齊。)
案例:首頁分析查詢性能優化
我們的首頁承載着面向客户的分析功能——包括“優秀員工”(雙月對比)、“熱門服務”以及實時銷售動態。起初這些功能由 Postgres 支撐,在小客户場景下表現尚可,但在大客户側卻遭遇了性能瓶頸:頁面加載動輒 15-20 秒甚至直接超時,還對 OLTP 業務造成了嚴重的連帶傷害
這是典型的失效模式:一次冷啓動查詢擊穿了 buffer cache;首個請求在拖回海量數據頁的過程中超時,後續請求雖能“僥倖”成功,卻已污染了內存空間,進一步拖慢其他無關的事務。
我們決定將這些視圖遷移至 StarRocks,並提出了一個硬性要求:分鐘級的數據時延。用户不能在完成一筆交易後,因為看不到實時反饋而產生困惑。我們最初嘗試使用 Iceberg,功能上沒問題但運行層面不穩定——高頻寫入產生的大量小文件和 Compaction 壓力,使分鐘級時延難以持續保證。於是,熱點鏈路切換至 StarRocks 內部表,並將 Iceberg/Paimon 繼續作為歷史數據的長期記錄。
關鍵點在於,我們並未直接使用物化視圖,而是基於內部表構建了分層 SQL 視圖。這樣開發者可以複用業務語義,而無需重複定義。整體架構如下:
- 由 Flink 寫入的基礎表 rt_sales(Debezium → Kafka → Flink → StarRocks);
- 作為統一語義層的 vw\_sales\_enriched 視圖,用於補全業務關聯並應用狀態口徑;
- 用於定義“最近成交”的 vw\_recent\_sales 視圖(包含時間窗口與可計入的狀態範圍);
- 在其之上構建的高層視圖,例如 vw\_top\_employees_2m、vw\_top\_services,均基於前述層級組合計算得到。
(首頁分層視圖示例:rt_sales(CDC upsert 寫入)→ vw\_sales\_enriched(業務關聯、狀態口徑、分區過濾條件,以及衍生字段/過濾字段,例如 day、is\_eligible、provider\_bucket)→ vw\_recent\_sales → vw\_top\_*。)
由於業務語義都封裝在視圖裏,產品團隊只需要查詢 vw\_top\_ 和 vw\_recent\_;不必記住哪些狀態需要計入、“recent”具體怎麼定義,或或者銷售數據如何關聯補全。與此同時,StarRocks 的優化器會將過濾條件與列裁剪下推至整個視圖棧,在無需維護物化視圖刷新任務的前提下,依然獲得高質量的執行計劃質量。
最終成效:即使在最複雜的過濾與聚合條件下,首頁分析查詢的響應時間也縮短至 200 毫秒左右,並達到了用户預期的分鐘級時效性。Postgres 不再被當作“臨時緩存”來透支,確保了 OLTP 事務的響應速度,而首頁產生的分析性併發壓力則由 StarRocks 承接。
深度歷史數據依然保留在 Lakehouse 中(通過 Spark 回灌至 Iceberg/Paimon),而這套分層視圖可以根據需要進行跨源聯邦查詢,從而覆蓋更長的時間窗口。這意味着我們無需為不同場景開發多套代碼,僅需維護一套可複用的語義定義。
(啓用 StarRocks 查詢鏈路(通過 feature flag)前後的延遲分位對比:左圖為舊的 Postgres 方案,查詢經常出現多秒級峯值;右圖為開啓 StarRocks 後,p95 降至接近 1 秒以內,並且長尾(p99/p99.9)的峯值基本消失。)
實踐中的問題與解決方案
實現無誤的 DDL 遷移
我們構建了一套 ActiveRecord 風格的遷移工具:採用層級命名規範,為每項變更編寫顯式的 up/down SQL,並在 StarRocks 中維護一個聲明式的 Schema 版本號(這是一個原子遞增的單一事實源)。
由於 StarRocks 的許多 DDL 操作是異步執行的,該工具會持續輪詢變更狀態,直到所有後台任務達到最終的 FINISHED 狀態後才會更新版本號;一旦失敗,它將通過配對的 down SQL 進行回滾。最終效果是:實現了一套與 StarRocks 語義對齊、可逆且支持協作安全的 Schema 演進流程。
查詢性能分析
我們統一使用 EXPLAIN ANALYZE 生成的 Profile,並梳理出一套符合常識的核心指標(掃描字節數、命中的分區數量、Join 類型、P50/P95)。這讓所有人對“什麼變慢了”擁有了一致的判斷框架:是分區過多、Join 策略不合適,還是由於過濾條件無法下推。
分區策略:不向業務代碼“泄露”底層細節
我們按時間進行分區,並按業務鍵(例如 provider_id)進行分桶。為了防止開發人員因疏忽導致全表掃描,我們將過濾謂詞封裝在視圖內部。
例如,vw_recent_sales 視圖中直接定義了“Recent”的時間範圍及合規狀態,更高級別的視圖則基於此構建。Planner 依然能將過濾條件透傳至底層引擎,但調用者無需再記憶複雜的分區計算邏輯。
維度關聯:避免大規模 Shuffle
大事實表與小維度表的關聯採用 Broadcast 模式;大事實表與大維度表之間的關聯則優先使用 Colocate 模式(通過對齊分桶鍵與分桶數實現),在無法滿足 Colocate 條件時則退而求其次使用 Bucket-shuffle。
我們對維度表進行版本化管理,並儘可能精簡字段(Narrow Tables)以適配 Broadcast;當某個維度表規模增長到不再適合 Broadcast 時,我們會將其提升至 Colocate Group 中,並調整其分桶策略以匹配主事實表。
數據跳讀與索引取捨
為了降低範圍查詢和點查的成本,我們充分利用了 StarRocks 的 Zone Map(每個 Segment 的最大/最小值過濾)以及基於排序列的 prefix/short-key index。此外,我們僅在能產生實質性收益(Move the needle)的場景下,有選擇性地添加 Bloom Filter 或 Bitmap 索引。
我們的原則是:在添加索引前,必須通過 Profile 證明其確實減少了掃描字節數;同時,定期清理不再使用的舊索引。
Schema 演進
所有 Schema 變更都始於 Avro Schema Registry 的兼容性檢查;數據寫入方(Writers)最後才進行發佈。內部表遵循“僅增量”原則,優先添加新列;視圖層則採用版本化定義(如 vw_sales_enriched_v2),並配合一個名為 vw_sales_enriched 的視圖指針,待數據 Backfill完成後再進行原子切換。Flink Sink 均具備冪等性或通過主鍵(PK)進行數據對齊。此外,CI 環節會攔截任何可能導致下游模型失效的變更。
總結
StarRocks 正逐漸成為我們日常分析中可靠的核心工具:它提供了統一的 SQL 接入層,將實時鏈路、歷史鏈路與搜索鏈路有機統一;在存算分離架構下,性能穩定可靠;同時具備開發者友好的易用性,讓團隊能夠通過平實的標準 SQL 快速交付業務,而非陷入複雜的定製化管道中。
通過這一套架構,我們實現了預期的工程目標:內部表上的準實時讀取、開放格式上的歷史數據聯邦查詢,以及通過 ES Catalog 實現的搜索關聯查詢。更重要的是,在實現這一切的同時,我們依然保持了 Lakehouse 作為唯一事實源的架構地位。