[{"data":1,"prerenderedAt":1074},["ShallowReactive",2],{"portfolio-\u002Fportfolio\u002Frealty-platform-rebuild":3},{"id":4,"title":5,"award":6,"body":7,"cover":180,"description":202,"extension":1053,"featured":1054,"link":6,"meta":1055,"navigation":721,"order":209,"path":1056,"pinned":1054,"repo":6,"role":1057,"seo":1058,"stack":1059,"stem":1070,"summary":1071,"year":1072,"__hash__":1073},"portfolio\u002Fportfolio\u002Frealty-platform-rebuild.md","房仲平台核心系統重構",null,{"type":8,"value":9,"toc":1041},"minimark",[10,14,23,30,33,60,63,70,73,91,94,97,100,168,171,175,181,188,196,360,371,543,554,557,587,591,597,604,609,616,621,632,837,840,860,863,868,875,880,895,906,910,920,931,935,946,949,982,985,1037],[11,12,13],"h2",{"id":13},"專案概述",[15,16,17,18,22],"p",{},"主導公司主力房仲平台的主要流量頻道重構，包含",[19,20,21],"strong",{},"買屋、成交行情、社區與快售","等\n頻道，將系統從 .NET Framework\u002FIIS 遷移至 Kubernetes 上的 CQRS + Event Sourcing\n架構。",[15,24,25,26,29],{},"重構前，每個頻道都是一個獨立的 IIS 站台，各自直接查詢 MSSQL，再把查詢結果整包\n放進",[19,27,28],{},"記憶體快取","來撐住流量——每個站台光快取就吃掉約 10G 記憶體，資料更新要等\n快取過期，部署時快取要重新暖機，一次部署就要 30 分鐘。搜尋類的查詢（多條件篩\n選、地圖範圍）對關聯式資料庫本來就不友善，複雜一點的條件要 20 秒才回得來。",[15,31,32],{},"這次重構的方向是把「讀」和「寫」拆開：",[34,35,36,43,53],"ul",{},[37,38,39,42],"li",{},[19,40,41],{},"寫入端","維持在 MSSQL，作為資料的唯一真實來源（Single Source of Truth）",[37,44,45,48,49,52],{},[19,46,47],{},"讀取端","全部改由 Elasticsearch 提供，並依業務領域拆成",[19,50,51],{},"買屋、行情、社區三\n大資料中台","，前端與集團內其他系統都透過中台取資料，不再直接碰資料庫",[37,54,55,56,59],{},"中間用",[19,57,58],{},"排程 + RabbitMQ"," 把 MSSQL 的異動同步進 Elasticsearch",[11,61,62],{"id":62},"系統架構",[15,64,65],{},[66,67],"img",{"alt":68,"src":69},"房仲平台架構圖：使用者經由 DMZ Kubernetes 上的 API Gateway 進入內網，內網 Kubernetes 上有前端網站與買屋、行情、社區三大中台，中台都查詢 Elasticsearch；MSSQL 原始資料透過 BCH 批次全量轉入、同步排程與 RabbitMQ 增量同步到 Elasticsearch；遙測資料送到 OpenTelemetry Collector 再由 Grafana 呈現，Argo CD 依 Git 上的設定同步 DMZ 與內網叢集","\u002Fimages\u002Fportfolio\u002Frealty-platform-architecture.svg",[15,71,72],{},"整體分成兩個網路區域、兩座 Kubernetes 叢集：",[34,74,75,85],{},[37,76,77,80,81,84],{},[19,78,79],{},"DMZ","：只放 ",[19,82,83],{},"API Gateway","，是整個平台唯一對外開放的入口。外部請求一律先\n進 Gateway，再依路由規則轉進內網，內網的服務本身不對外暴露",[37,86,87,90],{},[19,88,89],{},"內網","：前端網站與三大中台都跑在內網的 Kubernetes 上；Elasticsearch、\nRabbitMQ、MSSQL 等資料服務也都只在內網可達",[15,92,93],{},"把對外入口收斂到一個點的好處是，所有安全控管、流量監控、路由調整都只需要在\nGateway 這一層處理；內網的中台可以專心做資料查詢，不用各自處理對外的防護。",[11,95,96],{"id":96},"三大資料中台",[15,98,99],{},"三個中台都是 ASP.NET Core Web API，背後都只查詢 Elasticsearch：",[101,102,103,119],"table",{},[104,105,106],"thead",{},[107,108,109,113,116],"tr",{},[110,111,112],"th",{},"中台",[110,114,115],{},"提供的資料",[110,117,118],{},"主要查詢",[120,121,122,140,155],"tbody",{},[107,123,124,130,133],{},[125,126,127],"td",{},[19,128,129],{},"買屋中台",[125,131,132],{},"在售物件",[125,134,135,136,139],{},"關鍵字、多條件篩選、自動補全、",[19,137,138],{},"地圖搜尋","、鄰近物件推薦",[107,141,142,147,150],{},[125,143,144],{},[19,145,146],{},"行情中台",[125,148,149],{},"實價登錄 \u002F 成交行情",[125,151,152,153],{},"區域與社區行情、",[19,154,138],{},[107,156,157,162,165],{},[125,158,159],{},[19,160,161],{},"社區中台",[125,163,164],{},"社區（建案）資訊",[125,166,167],{},"社區基本資料、社區內物件與行情彙整",[15,169,170],{},"以「業務領域」而不是「頁面」來切中台，讓同一份資料只有一個出口：前端的買屋頁、\nApp、集團內的其他品牌網站要查在售物件，都是打同一個買屋中台，不會再出現各站台\n各寫一套查詢、結果卻對不起來的情況。",[11,172,174],{"id":173},"地圖搜尋elasticsearch-geo","地圖搜尋：Elasticsearch GEO",[15,176,177],{},[66,178],{"alt":179,"src":180},"地圖搜尋畫面：在信義區地圖上依可視範圍顯示每個物件與成交案件的價格標籤","\u002Fimages\u002Fportfolio\u002Frealty-map-search.png",[15,182,183,184,187],{},"買屋與行情頻道都有",[19,185,186],{},"地圖找房","：使用者拖曳、縮放地圖時，畫面上要即時顯示這個\n範圍內的物件與成交價格。這類「範圍內有哪些點」的查詢，正好是 Elasticsearch\nGEO 的強項。",[15,189,190,191,195],{},"每一筆物件 \u002F 成交資料在建立索引時，就把經緯度存成 ",[192,193,194],"code",{},"geo_point","：",[197,198,203],"pre",{"className":199,"code":200,"language":201,"meta":202,"style":202},"language-json shiki shiki-themes github-dark","PUT \u002Fbuy-house\n{\n  \"mappings\": {\n    \"properties\": {\n      \"location\":   { \"type\": \"geo_point\" },\n      \"price\":      { \"type\": \"scaled_float\", \"scaling_factor\": 10 },\n      \"city\":       { \"type\": \"keyword\" },\n      \"district\":   { \"type\": \"keyword\" },\n      \"updateTime\": { \"type\": \"date\" }\n    }\n  }\n}\n","json","",[192,204,205,214,220,230,238,260,289,307,323,342,348,354],{"__ignoreMap":202},[206,207,210],"span",{"class":208,"line":209},"line",1,[206,211,213],{"class":212},"s95oV","PUT \u002Fbuy-house\n",[206,215,217],{"class":208,"line":216},2,[206,218,219],{"class":212},"{\n",[206,221,223,227],{"class":208,"line":222},3,[206,224,226],{"class":225},"sDLfK","  \"mappings\"",[206,228,229],{"class":212},": {\n",[206,231,233,236],{"class":208,"line":232},4,[206,234,235],{"class":225},"    \"properties\"",[206,237,229],{"class":212},[206,239,241,244,247,250,253,257],{"class":208,"line":240},5,[206,242,243],{"class":225},"      \"location\"",[206,245,246],{"class":212},":   { ",[206,248,249],{"class":225},"\"type\"",[206,251,252],{"class":212},": ",[206,254,256],{"class":255},"sU2Wk","\"geo_point\"",[206,258,259],{"class":212}," },\n",[206,261,263,266,269,271,273,276,279,282,284,287],{"class":208,"line":262},6,[206,264,265],{"class":225},"      \"price\"",[206,267,268],{"class":212},":      { ",[206,270,249],{"class":225},[206,272,252],{"class":212},[206,274,275],{"class":255},"\"scaled_float\"",[206,277,278],{"class":212},", ",[206,280,281],{"class":225},"\"scaling_factor\"",[206,283,252],{"class":212},[206,285,286],{"class":225},"10",[206,288,259],{"class":212},[206,290,292,295,298,300,302,305],{"class":208,"line":291},7,[206,293,294],{"class":225},"      \"city\"",[206,296,297],{"class":212},":       { ",[206,299,249],{"class":225},[206,301,252],{"class":212},[206,303,304],{"class":255},"\"keyword\"",[206,306,259],{"class":212},[206,308,310,313,315,317,319,321],{"class":208,"line":309},8,[206,311,312],{"class":225},"      \"district\"",[206,314,246],{"class":212},[206,316,249],{"class":225},[206,318,252],{"class":212},[206,320,304],{"class":255},[206,322,259],{"class":212},[206,324,326,329,332,334,336,339],{"class":208,"line":325},9,[206,327,328],{"class":225},"      \"updateTime\"",[206,330,331],{"class":212},": { ",[206,333,249],{"class":225},[206,335,252],{"class":212},[206,337,338],{"class":255},"\"date\"",[206,340,341],{"class":212}," }\n",[206,343,345],{"class":208,"line":344},10,[206,346,347],{"class":212},"    }\n",[206,349,351],{"class":208,"line":350},11,[206,352,353],{"class":212},"  }\n",[206,355,357],{"class":208,"line":356},12,[206,358,359],{"class":212},"}\n",[15,361,362,363,366,367,370],{},"前端在地圖移動後，把目前",[19,364,365],{},"可視範圍的左上角與右下角座標","送給中台，中台組成\n",[192,368,369],{},"geo_bounding_box"," 查詢，再疊上使用者選的篩選條件（總價、房數、屋齡…）：",[197,372,374],{"className":199,"code":373,"language":201,"meta":202,"style":202},"GET \u002Fbuy-house\u002F_search\n{\n  \"query\": {\n    \"bool\": {\n      \"filter\": [\n        {\n          \"geo_bounding_box\": {\n            \"location\": {\n              \"top_left\":     { \"lat\": 25.0480, \"lon\": 121.5500 },\n              \"bottom_right\": { \"lat\": 25.0300, \"lon\": 121.5800 }\n            }\n          }\n        },\n        { \"range\": { \"price\": { \"lte\": 3000 } } }\n      ]\n    }\n  }\n}\n",[192,375,376,381,385,392,399,407,412,419,426,454,479,484,489,495,522,528,533,538],{"__ignoreMap":202},[206,377,378],{"class":208,"line":209},[206,379,380],{"class":212},"GET \u002Fbuy-house\u002F_search\n",[206,382,383],{"class":208,"line":216},[206,384,219],{"class":212},[206,386,387,390],{"class":208,"line":222},[206,388,389],{"class":225},"  \"query\"",[206,391,229],{"class":212},[206,393,394,397],{"class":208,"line":232},[206,395,396],{"class":225},"    \"bool\"",[206,398,229],{"class":212},[206,400,401,404],{"class":208,"line":240},[206,402,403],{"class":225},"      \"filter\"",[206,405,406],{"class":212},": [\n",[206,408,409],{"class":208,"line":262},[206,410,411],{"class":212},"        {\n",[206,413,414,417],{"class":208,"line":291},[206,415,416],{"class":225},"          \"geo_bounding_box\"",[206,418,229],{"class":212},[206,420,421,424],{"class":208,"line":309},[206,422,423],{"class":225},"            \"location\"",[206,425,229],{"class":212},[206,427,428,431,434,437,439,442,444,447,449,452],{"class":208,"line":325},[206,429,430],{"class":225},"              \"top_left\"",[206,432,433],{"class":212},":     { ",[206,435,436],{"class":225},"\"lat\"",[206,438,252],{"class":212},[206,440,441],{"class":225},"25.0480",[206,443,278],{"class":212},[206,445,446],{"class":225},"\"lon\"",[206,448,252],{"class":212},[206,450,451],{"class":225},"121.5500",[206,453,259],{"class":212},[206,455,456,459,461,463,465,468,470,472,474,477],{"class":208,"line":344},[206,457,458],{"class":225},"              \"bottom_right\"",[206,460,331],{"class":212},[206,462,436],{"class":225},[206,464,252],{"class":212},[206,466,467],{"class":225},"25.0300",[206,469,278],{"class":212},[206,471,446],{"class":225},[206,473,252],{"class":212},[206,475,476],{"class":225},"121.5800",[206,478,341],{"class":212},[206,480,481],{"class":208,"line":350},[206,482,483],{"class":212},"            }\n",[206,485,486],{"class":208,"line":356},[206,487,488],{"class":212},"          }\n",[206,490,492],{"class":208,"line":491},13,[206,493,494],{"class":212},"        },\n",[206,496,498,501,504,506,509,511,514,516,519],{"class":208,"line":497},14,[206,499,500],{"class":212},"        { ",[206,502,503],{"class":225},"\"range\"",[206,505,331],{"class":212},[206,507,508],{"class":225},"\"price\"",[206,510,331],{"class":212},[206,512,513],{"class":225},"\"lte\"",[206,515,252],{"class":212},[206,517,518],{"class":225},"3000",[206,520,521],{"class":212}," } } }\n",[206,523,525],{"class":208,"line":524},15,[206,526,527],{"class":212},"      ]\n",[206,529,531],{"class":208,"line":530},16,[206,532,347],{"class":212},[206,534,536],{"class":208,"line":535},17,[206,537,353],{"class":212},[206,539,541],{"class":208,"line":540},18,[206,542,359],{"class":212},[15,544,545,546,549,550,553],{},"物件詳細頁的「",[19,547,548],{},"附近物件推薦","」則改用 ",[192,551,552],{},"geo_distance","，以目前物件為圓心找出一定\n半徑內的其他物件，再依距離排序。",[15,555,556],{},"幾個實作上的考量：",[34,558,559,571,581],{},[37,560,561,567,568,570],{},[19,562,563,564],{},"條件都放在 ",[192,565,566],{},"filter","：地圖範圍與篩選條件都是「符合 \u002F 不符合」，不需要算相關\n性分數，放在 ",[192,569,566],{}," 裡 Elasticsearch 可以快取結果，拖曳地圖時的重複查詢會快\n很多",[37,572,573,576,577,580],{},[19,574,575],{},"只回傳畫面需要的欄位","：地圖上的標籤只需要座標、價格與 ID，用 ",[192,578,579],{},"_source"," 過\n濾掉其他欄位，點進去時才查詳細資料",[37,582,583,586],{},[19,584,585],{},"限制單次回傳數量","：縮放到很大的範圍時物件數量會暴增，中台會限制單次筆數，\n避免一次把幾萬筆點丟給前端畫",[11,588,590],{"id":589},"資料同步從-mssql-到-elasticsearch","資料同步：從 MSSQL 到 Elasticsearch",[15,592,593],{},[66,594],{"alt":595,"src":596},"資料同步流程圖：全量時由 BCH 批次把 MSSQL 整批轉入 Elasticsearch；增量時 DBA 更新資料會一併寫入異動欄位，同步排程定期比對異動欄位撈出變動資料，送進 RabbitMQ，再由同步消費者重組文件並更新索引","\u002Fimages\u002Fportfolio\u002Frealty-platform-data-sync.svg",[15,598,599,600,603],{},"讀取全部改走 Elasticsearch 之後，最關鍵的問題就變成：",[19,601,602],{},"MSSQL 的資料改了，索引\n要怎麼跟上？"," 同步分成兩條路線：",[15,605,606],{},[19,607,608],{},"① 全量：BCH 批次",[15,610,611,612,615],{},"原始資料由 ",[19,613,614],{},"BCH"," 批次從 MSSQL 整批讀出、轉成 Elasticsearch 的文件格式後寫入，\n用在第一次建立索引，或是 mapping 調整後需要整份重建的時候。",[15,617,618],{},[19,619,620],{},"② 增量：異動欄位 + 排程 + RabbitMQ",[15,622,623,624,627,628,631],{},"日常的資料更新不可能每次都整批重跑，所以和 DBA 約定：",[19,625,626],{},"資料在更新時，一併更新\n一個異動欄位","。我寫的同步排程（Hangfire）定期比對這個欄位，找出上次同步之後\n有變動的資料，把每一筆異動包成事件送進 ",[19,629,630],{},"RabbitMQ","，再由同步消費者重組成\nElasticsearch 文件、更新對應的索引。",[197,633,637],{"className":634,"code":635,"language":636,"meta":202,"style":202},"language-csharp shiki shiki-themes github-dark","\u002F\u002F 示意：排程只負責「找出變動、發出事件」，不直接寫 ES\npublic async Task DetectChangesAsync()\n{\n    var checkpoint = await _checkpointStore.GetAsync(\"buy-house\");\n    var changed = await _repository.GetChangedSinceAsync(checkpoint);\n\n    foreach (var item in changed)\n    {\n        await _publisher.PublishAsync(new HouseChangedEvent(item.Id, item.ChangeMark));\n    }\n\n    if (changed.Count > 0)\n    {\n        await _checkpointStore.SaveAsync(\"buy-house\", changed.Max(x => x.ChangeMark));\n    }\n}\n","csharp",[192,638,639,645,664,668,697,717,723,743,748,770,774,778,795,799,829,833],{"__ignoreMap":202},[206,640,641],{"class":208,"line":209},[206,642,644],{"class":643},"sAwPA","\u002F\u002F 示意：排程只負責「找出變動、發出事件」，不直接寫 ES\n",[206,646,647,651,654,658,661],{"class":208,"line":216},[206,648,650],{"class":649},"snl16","public",[206,652,653],{"class":649}," async",[206,655,657],{"class":656},"svObZ"," Task",[206,659,660],{"class":656}," DetectChangesAsync",[206,662,663],{"class":212},"()\n",[206,665,666],{"class":208,"line":222},[206,667,219],{"class":212},[206,669,670,673,676,679,682,685,688,691,694],{"class":208,"line":232},[206,671,672],{"class":649},"    var",[206,674,675],{"class":656}," checkpoint",[206,677,678],{"class":649}," =",[206,680,681],{"class":649}," await",[206,683,684],{"class":212}," _checkpointStore.",[206,686,687],{"class":656},"GetAsync",[206,689,690],{"class":212},"(",[206,692,693],{"class":255},"\"buy-house\"",[206,695,696],{"class":212},");\n",[206,698,699,701,704,706,708,711,714],{"class":208,"line":240},[206,700,672],{"class":649},[206,702,703],{"class":656}," changed",[206,705,678],{"class":649},[206,707,681],{"class":649},[206,709,710],{"class":212}," _repository.",[206,712,713],{"class":656},"GetChangedSinceAsync",[206,715,716],{"class":212},"(checkpoint);\n",[206,718,719],{"class":208,"line":262},[206,720,722],{"emptyLinePlaceholder":721},true,"\n",[206,724,725,728,731,734,737,740],{"class":208,"line":291},[206,726,727],{"class":649},"    foreach",[206,729,730],{"class":212}," (",[206,732,733],{"class":649},"var",[206,735,736],{"class":656}," item",[206,738,739],{"class":649}," in",[206,741,742],{"class":212}," changed)\n",[206,744,745],{"class":208,"line":309},[206,746,747],{"class":212},"    {\n",[206,749,750,753,756,759,761,764,767],{"class":208,"line":325},[206,751,752],{"class":649},"        await",[206,754,755],{"class":212}," _publisher.",[206,757,758],{"class":656},"PublishAsync",[206,760,690],{"class":212},[206,762,763],{"class":649},"new",[206,765,766],{"class":656}," HouseChangedEvent",[206,768,769],{"class":212},"(item.Id, item.ChangeMark));\n",[206,771,772],{"class":208,"line":344},[206,773,347],{"class":212},[206,775,776],{"class":208,"line":350},[206,777,722],{"emptyLinePlaceholder":721},[206,779,780,783,786,789,792],{"class":208,"line":356},[206,781,782],{"class":649},"    if",[206,784,785],{"class":212}," (changed.Count ",[206,787,788],{"class":649},">",[206,790,791],{"class":225}," 0",[206,793,794],{"class":212},")\n",[206,796,797],{"class":208,"line":491},[206,798,747],{"class":212},[206,800,801,803,805,808,810,812,815,818,820,823,826],{"class":208,"line":497},[206,802,752],{"class":649},[206,804,684],{"class":212},[206,806,807],{"class":656},"SaveAsync",[206,809,690],{"class":212},[206,811,693],{"class":255},[206,813,814],{"class":212},", changed.",[206,816,817],{"class":656},"Max",[206,819,690],{"class":212},[206,821,822],{"class":656},"x",[206,824,825],{"class":649}," =>",[206,827,828],{"class":212}," x.ChangeMark));\n",[206,830,831],{"class":208,"line":524},[206,832,347],{"class":212},[206,834,835],{"class":208,"line":530},[206,836,359],{"class":212},[15,838,839],{},"中間多放一層 RabbitMQ，而不是排程撈到資料就直接寫 Elasticsearch，原因是：",[34,841,842,848,854],{},[37,843,844,847],{},[19,845,846],{},"偵測與寫入解耦","：排程只負責「找出哪些資料變了」，寫索引的工作交給消費者。\n某一段時間異動量暴增時，事件會先在佇列裡排隊，消費者照自己的速度處理，不會把\n排程或 Elasticsearch 壓垮",[37,849,850,853],{},[19,851,852],{},"失敗可重試","：單筆文件寫入失敗時，事件可以重新投遞或進入死信佇列，不會因為一\n筆壞資料讓整批同步中斷",[37,855,856,859],{},[19,857,858],{},"一份異動、多個訂閱者","：同一筆物件異動，買屋索引要更新、社區的物件彙整也可能\n要更新，各自訂閱同一個事件即可，不需要在排程裡寫死所有下游",[11,861,862],{"id":862},"安全與可觀測性",[15,864,865],{},[19,866,867],{},"NeuVector：DMZ 流量監控",[15,869,870,871,874],{},"DMZ 的 Kubernetes 上部署了 ",[19,872,873],{},"NeuVector","，監控容器之間與進出叢集的網路流量。\nAPI Gateway 是唯一對外的服務，也是最常被掃描、攻擊的地方；NeuVector 會依平常的\n流量模式學習出正常的連線行為，出現異常的連線或流量時發出告警，讓問題在進到內網\n之前就被看見。",[15,876,877],{},[19,878,879],{},"OpenTelemetry + Grafana：全面可觀測性",[15,881,882,883,886,887,890,891,894],{},"DMZ 與內網的服務都接上 ",[19,884,885],{},"OpenTelemetry","，Traces、Metrics、Logs 統一送到\n",[19,888,889],{},"OpenTelemetry Collector","，由 Collector 集中處理後轉送到後端儲存，最後在\n",[19,892,893],{},"Grafana"," 上用儀表板呈現：",[34,896,897,900,903],{},[37,898,899],{},"一個請求從 API Gateway 進來、經過哪個中台、查了哪個索引、花了多少時間，都能從\n同一條 Trace 串起來",[37,901,902],{},"各中台的請求量、延遲、錯誤率，以及同步佇列的堆積狀況，都在 Grafana 儀表板上\n一眼看得到",[37,904,905],{},"應用程式只需要對接 Collector，之後要更換或新增後端儲存，只要改 Collector 的設\n定，不必改程式",[11,907,909],{"id":908},"部署argo-cd-iac","部署：Argo CD + IaC",[15,911,912,913,916,917,195],{},"兩座 Kubernetes 叢集都交給 ",[19,914,915],{},"Argo CD"," 管理，基礎設施與 Kubernetes 的部署設定\n",[19,918,919],{},"全部以程式碼描述、放在 Git 上",[34,921,922,925,928],{},[37,923,924],{},"要部署或調整設定，就是發一個 PR；合併後 Argo CD 自動把叢集同步成 Git 上描述的\n狀態",[37,926,927],{},"叢集的實際狀態若被手動改動、和 Git 不一致，Argo CD 會標示出差異，避免「線上\n跟設定檔對不起來」的情況",[37,929,930],{},"每一次變更都有 Git 紀錄可以追溯，出問題時可以直接回退到上一個版本",[11,932,934],{"id":933},"遷移策略新舊系統並行","遷移策略：新舊系統並行",[15,936,937,938,941,942,945],{},"重構不是一次切換。API Gateway 除了是對外入口，也是遷移期間的",[19,939,940],{},"路由開關","：新的\n中台與頁面完成一個，就在 Gateway 上把對應的路由切到新系統，還沒完成的部分繼續導\n回舊的 IIS 站台。整個遷移過程新舊系統並行運作，",[19,943,944],{},"對使用者完全沒有中斷","，某個\n新功能有問題時也能立刻把路由切回舊系統。",[11,947,948],{"id":948},"成果",[34,950,951,958,965,972,979],{},[37,952,953,954,957],{},"移除每個 IIS 站台 10G 的記憶體快取，記憶體資源減少 ",[19,955,956],{},"超過 80%","（10G → 800Mb）",[37,959,960,961,964],{},"數據中台上線後，查詢回應速度提升 ",[19,962,963],{},"10 倍以上","（20 秒 → 1.5 秒）",[37,966,967,968,971],{},"容器化並移除記憶體快取後，後端部署效率提升 ",[19,969,970],{},"5 倍以上","（30 分鐘 → 5 分鐘）",[37,973,974,975,978],{},"前後端各別容器化後，前端部署效率提升 ",[19,976,977],{},"3 倍以上","（15 分鐘 → 5 分鐘）",[37,980,981],{},"買屋、行情、社區資料由三大中台統一提供，集團內其他系統可直接共用",[11,983,984],{"id":984},"使用技術",[34,986,987,993,1007,1013,1019,1025,1031],{},[37,988,989,992],{},[19,990,991],{},"後端","：C#、ASP.NET Core Web API、CQRS、Event Sourcing、Hangfire",[37,994,995,998,999,1001,1002,1001,1004,1006],{},[19,996,997],{},"資料","：MSSQL、Elasticsearch（",[192,1000,194],{},"、",[192,1003,369],{},[192,1005,552],{},"）、BCH 批次",[37,1008,1009,1012],{},[19,1010,1011],{},"訊息","：RabbitMQ",[37,1014,1015,1018],{},[19,1016,1017],{},"平台","：Kubernetes（DMZ \u002F 內網兩座叢集）、API Gateway",[37,1020,1021,1024],{},[19,1022,1023],{},"安全","：NeuVector",[37,1026,1027,1030],{},[19,1028,1029],{},"可觀測性","：OpenTelemetry、OpenTelemetry Collector、Grafana",[37,1032,1033,1036],{},[19,1034,1035],{},"部署","：Argo CD（GitOps）、IaC",[1038,1039,1040],"style",{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":202,"searchDepth":216,"depth":216,"links":1042},[1043,1044,1045,1046,1047,1048,1049,1050,1051,1052],{"id":13,"depth":216,"text":13},{"id":62,"depth":216,"text":62},{"id":96,"depth":216,"text":96},{"id":173,"depth":216,"text":174},{"id":589,"depth":216,"text":590},{"id":862,"depth":216,"text":862},{"id":908,"depth":216,"text":909},{"id":933,"depth":216,"text":934},{"id":948,"depth":216,"text":948},{"id":984,"depth":216,"text":984},"md",false,{},"\u002Fportfolio\u002Frealty-platform-rebuild","後端技術負責人 ‧ 房仲業",{"title":5,"description":202},[1060,1061,1062,1063,1064,630,1065,1066,1067,1068,83,873,885,893,915,1069],"C#","ASP.NET Core","CQRS","Event Sourcing","Hangfire","Elasticsearch","Elasticsearch GEO","MSSQL","Kubernetes","IaC","portfolio\u002Frealty-platform-rebuild","將大型房仲平台的主要流量頻道，從 .NET Framework\u002FIIS 重構為 Kubernetes 上的 CQRS + Event Sourcing 架構：DMZ 以 API Gateway 作為唯一入口，內網拆出買屋、行情、社區三大資料中台，讀取全部走 Elasticsearch（含 GEO 地圖搜尋），MSSQL 異動透過排程與 RabbitMQ 同步進索引，並以 OpenTelemetry、Grafana、NeuVector 與 Argo CD 建立可觀測性與 GitOps 維運。",2023,"VzhHXppQmjQWMgiSMYEkFhRdwzj-lgkHTOOR1_saF5c",1792959149967]