後端技術負責人 ‧ 房仲業 · 2023
房仲平台核心系統重構
將大型房仲平台的主要流量頻道,從 .NET Framework/IIS 重構為 Kubernetes 上的 CQRS + Event Sourcing 架構:DMZ 以 API Gateway 作為唯一入口,內網拆出買屋、行情、社區三大資料中台,讀取全部走 Elasticsearch(含 GEO 地圖搜尋),MSSQL 異動透過排程與 RabbitMQ 同步進索引,並以 OpenTelemetry、Grafana、NeuVector 與 Argo CD 建立可觀測性與 GitOps 維運。

專案概述
主導公司主力房仲平台的主要流量頻道重構,包含買屋、成交行情、社區與快售等 頻道,將系統從 .NET Framework/IIS 遷移至 Kubernetes 上的 CQRS + Event Sourcing 架構。
重構前,每個頻道都是一個獨立的 IIS 站台,各自直接查詢 MSSQL,再把查詢結果整包 放進記憶體快取來撐住流量——每個站台光快取就吃掉約 10G 記憶體,資料更新要等 快取過期,部署時快取要重新暖機,一次部署就要 30 分鐘。搜尋類的查詢(多條件篩 選、地圖範圍)對關聯式資料庫本來就不友善,複雜一點的條件要 20 秒才回得來。
這次重構的方向是把「讀」和「寫」拆開:
- 寫入端維持在 MSSQL,作為資料的唯一真實來源(Single Source of Truth)
- 讀取端全部改由 Elasticsearch 提供,並依業務領域拆成買屋、行情、社區三 大資料中台,前端與集團內其他系統都透過中台取資料,不再直接碰資料庫
- 中間用排程 + RabbitMQ 把 MSSQL 的異動同步進 Elasticsearch
系統架構
整體分成兩個網路區域、兩座 Kubernetes 叢集:
- DMZ:只放 API Gateway,是整個平台唯一對外開放的入口。外部請求一律先 進 Gateway,再依路由規則轉進內網,內網的服務本身不對外暴露
- 內網:前端網站與三大中台都跑在內網的 Kubernetes 上;Elasticsearch、 RabbitMQ、MSSQL 等資料服務也都只在內網可達
把對外入口收斂到一個點的好處是,所有安全控管、流量監控、路由調整都只需要在 Gateway 這一層處理;內網的中台可以專心做資料查詢,不用各自處理對外的防護。
三大資料中台
三個中台都是 ASP.NET Core Web API,背後都只查詢 Elasticsearch:
| 中台 | 提供的資料 | 主要查詢 |
|---|---|---|
| 買屋中台 | 在售物件 | 關鍵字、多條件篩選、自動補全、地圖搜尋、鄰近物件推薦 |
| 行情中台 | 實價登錄 / 成交行情 | 區域與社區行情、地圖搜尋 |
| 社區中台 | 社區(建案)資訊 | 社區基本資料、社區內物件與行情彙整 |
以「業務領域」而不是「頁面」來切中台,讓同一份資料只有一個出口:前端的買屋頁、 App、集團內的其他品牌網站要查在售物件,都是打同一個買屋中台,不會再出現各站台 各寫一套查詢、結果卻對不起來的情況。
地圖搜尋:Elasticsearch GEO

買屋與行情頻道都有地圖找房:使用者拖曳、縮放地圖時,畫面上要即時顯示這個 範圍內的物件與成交價格。這類「範圍內有哪些點」的查詢,正好是 Elasticsearch GEO 的強項。
每一筆物件 / 成交資料在建立索引時,就把經緯度存成 geo_point:
PUT /buy-house
{
"mappings": {
"properties": {
"location": { "type": "geo_point" },
"price": { "type": "scaled_float", "scaling_factor": 10 },
"city": { "type": "keyword" },
"district": { "type": "keyword" },
"updateTime": { "type": "date" }
}
}
}
前端在地圖移動後,把目前可視範圍的左上角與右下角座標送給中台,中台組成
geo_bounding_box 查詢,再疊上使用者選的篩選條件(總價、房數、屋齡…):
GET /buy-house/_search
{
"query": {
"bool": {
"filter": [
{
"geo_bounding_box": {
"location": {
"top_left": { "lat": 25.0480, "lon": 121.5500 },
"bottom_right": { "lat": 25.0300, "lon": 121.5800 }
}
}
},
{ "range": { "price": { "lte": 3000 } } }
]
}
}
}
物件詳細頁的「附近物件推薦」則改用 geo_distance,以目前物件為圓心找出一定
半徑內的其他物件,再依距離排序。
幾個實作上的考量:
- 條件都放在
filter:地圖範圍與篩選條件都是「符合 / 不符合」,不需要算相關 性分數,放在filter裡 Elasticsearch 可以快取結果,拖曳地圖時的重複查詢會快 很多 - 只回傳畫面需要的欄位:地圖上的標籤只需要座標、價格與 ID,用
_source過 濾掉其他欄位,點進去時才查詳細資料 - 限制單次回傳數量:縮放到很大的範圍時物件數量會暴增,中台會限制單次筆數, 避免一次把幾萬筆點丟給前端畫
資料同步:從 MSSQL 到 Elasticsearch
讀取全部改走 Elasticsearch 之後,最關鍵的問題就變成:MSSQL 的資料改了,索引 要怎麼跟上? 同步分成兩條路線:
① 全量:BCH 批次
原始資料由 BCH 批次從 MSSQL 整批讀出、轉成 Elasticsearch 的文件格式後寫入, 用在第一次建立索引,或是 mapping 調整後需要整份重建的時候。
② 增量:異動欄位 + 排程 + RabbitMQ
日常的資料更新不可能每次都整批重跑,所以和 DBA 約定:資料在更新時,一併更新 一個異動欄位。我寫的同步排程(Hangfire)定期比對這個欄位,找出上次同步之後 有變動的資料,把每一筆異動包成事件送進 RabbitMQ,再由同步消費者重組成 Elasticsearch 文件、更新對應的索引。
// 示意:排程只負責「找出變動、發出事件」,不直接寫 ES
public async Task DetectChangesAsync()
{
var checkpoint = await _checkpointStore.GetAsync("buy-house");
var changed = await _repository.GetChangedSinceAsync(checkpoint);
foreach (var item in changed)
{
await _publisher.PublishAsync(new HouseChangedEvent(item.Id, item.ChangeMark));
}
if (changed.Count > 0)
{
await _checkpointStore.SaveAsync("buy-house", changed.Max(x => x.ChangeMark));
}
}
中間多放一層 RabbitMQ,而不是排程撈到資料就直接寫 Elasticsearch,原因是:
- 偵測與寫入解耦:排程只負責「找出哪些資料變了」,寫索引的工作交給消費者。 某一段時間異動量暴增時,事件會先在佇列裡排隊,消費者照自己的速度處理,不會把 排程或 Elasticsearch 壓垮
- 失敗可重試:單筆文件寫入失敗時,事件可以重新投遞或進入死信佇列,不會因為一 筆壞資料讓整批同步中斷
- 一份異動、多個訂閱者:同一筆物件異動,買屋索引要更新、社區的物件彙整也可能 要更新,各自訂閱同一個事件即可,不需要在排程裡寫死所有下游
安全與可觀測性
NeuVector:DMZ 流量監控
DMZ 的 Kubernetes 上部署了 NeuVector,監控容器之間與進出叢集的網路流量。 API Gateway 是唯一對外的服務,也是最常被掃描、攻擊的地方;NeuVector 會依平常的 流量模式學習出正常的連線行為,出現異常的連線或流量時發出告警,讓問題在進到內網 之前就被看見。
OpenTelemetry + Grafana:全面可觀測性
DMZ 與內網的服務都接上 OpenTelemetry,Traces、Metrics、Logs 統一送到 OpenTelemetry Collector,由 Collector 集中處理後轉送到後端儲存,最後在 Grafana 上用儀表板呈現:
- 一個請求從 API Gateway 進來、經過哪個中台、查了哪個索引、花了多少時間,都能從 同一條 Trace 串起來
- 各中台的請求量、延遲、錯誤率,以及同步佇列的堆積狀況,都在 Grafana 儀表板上 一眼看得到
- 應用程式只需要對接 Collector,之後要更換或新增後端儲存,只要改 Collector 的設 定,不必改程式
部署:Argo CD + IaC
兩座 Kubernetes 叢集都交給 Argo CD 管理,基礎設施與 Kubernetes 的部署設定 全部以程式碼描述、放在 Git 上:
- 要部署或調整設定,就是發一個 PR;合併後 Argo CD 自動把叢集同步成 Git 上描述的 狀態
- 叢集的實際狀態若被手動改動、和 Git 不一致,Argo CD 會標示出差異,避免「線上 跟設定檔對不起來」的情況
- 每一次變更都有 Git 紀錄可以追溯,出問題時可以直接回退到上一個版本
遷移策略:新舊系統並行
重構不是一次切換。API Gateway 除了是對外入口,也是遷移期間的路由開關:新的 中台與頁面完成一個,就在 Gateway 上把對應的路由切到新系統,還沒完成的部分繼續導 回舊的 IIS 站台。整個遷移過程新舊系統並行運作,對使用者完全沒有中斷,某個 新功能有問題時也能立刻把路由切回舊系統。
成果
- 移除每個 IIS 站台 10G 的記憶體快取,記憶體資源減少 超過 80%(10G → 800Mb)
- 數據中台上線後,查詢回應速度提升 10 倍以上(20 秒 → 1.5 秒)
- 容器化並移除記憶體快取後,後端部署效率提升 5 倍以上(30 分鐘 → 5 分鐘)
- 前後端各別容器化後,前端部署效率提升 3 倍以上(15 分鐘 → 5 分鐘)
- 買屋、行情、社區資料由三大中台統一提供,集團內其他系統可直接共用
使用技術
- 後端:C#、ASP.NET Core Web API、CQRS、Event Sourcing、Hangfire
- 資料:MSSQL、Elasticsearch(
geo_point、geo_bounding_box、geo_distance)、BCH 批次 - 訊息:RabbitMQ
- 平台:Kubernetes(DMZ / 內網兩座叢集)、API Gateway
- 安全:NeuVector
- 可觀測性:OpenTelemetry、OpenTelemetry Collector、Grafana
- 部署:Argo CD(GitOps)、IaC