[{"data":1,"prerenderedAt":2874},["ShallowReactive",2],{"home-about":3,"home-projects":181,"home-articles":1472},{"id":4,"title":5,"avatar":6,"body":7,"certifications":38,"description":5,"email":58,"experience":59,"extension":97,"location":98,"meta":99,"name":100,"navigation":64,"path":101,"resume":102,"role":103,"seo":104,"skills":105,"socials":166,"stem":179,"__hash__":180},"about\u002Fabout\u002Findex.md","","https:\u002F\u002Fui-avatars.com\u002Fapi\u002F?name=Tom+Chang&background=2f6f4e&color=fff&size=300&bold=true",{"type":8,"value":9,"toc":33},"minimark",[10,15,24,27,30],[11,12,14],"h2",{"id":13},"哈囉我是-tom","哈囉，我是 Tom 👋",[16,17,18,19,23],"p",{},"具備扎實的後端開發基礎，現專注於 ",[20,21,22],"strong",{},"DevSecOps、SRE 以及平台工程（Platform\nEngineering）"," 領域。我帶領 DevOps 團隊完成多項重點專案並實現顯著效益，運用\n結構化維運管理與自動化平台設計，在精簡人力下持續提升工程效率與團隊產出。精通\n雲原生架構、Kubernetes 生態系及高可用監控系統（LGTM）導入，並具備 AIOps 落地\n經驗。",[16,25,26],{},"除了技術本身，我也很重視工程文化：主導技術標準化流程、定義系統可用性目標\n（SLO\u002FSLI），並打造符合流暢度、安全性與高擴展性的企業內部開發環境。",[11,28,29],{"id":29},"目前狀態",[16,31,32],{},"現居台灣，帶領 DevOps\u002FSRE 團隊，持續在正式環境中深化平台工程與 AIOps 實踐。",{"title":5,"searchDepth":34,"depth":34,"links":35},2,[36,37],{"id":13,"depth":34,"text":14},{"id":29,"depth":34,"text":29},[39,46,51],{"name":40,"issuer":41,"image":42,"url":43,"issuedAt":44,"expiresAt":45},"AWS Certified Solutions Architect – Associate","Amazon Web Services","\u002Fimages\u002Fcertifications\u002Faws-saa.png","https:\u002F\u002Fwww.credly.com\u002Fbadges\u002F7d2d9262-2d2a-400a-8dc7-af2e25d7872b","2024-06-07","2027-06-07",{"name":47,"issuer":41,"image":48,"url":49,"issuedAt":50,"expiresAt":45},"AWS Certified Cloud Practitioner","\u002Fimages\u002Fcertifications\u002Faws-ccp.png","https:\u002F\u002Fwww.credly.com\u002Fbadges\u002F41df9f3f-3e8e-461a-a16b-1fb1c5d4bb02","2024-04-02",{"name":52,"issuer":53,"image":54,"url":55,"issuedAt":56,"expiresAt":57},"Cisco CCNA Routing and Switching","Cisco","\u002Fimages\u002Fcertifications\u002Fccna.png","https:\u002F\u002Fwww.credly.com\u002Fbadges\u002Ff12fa3e0-8c90-47f4-993b-92fe7b50960b","2018-06-15","2021-12-15","tom30913@gmail.com",[60,79,92],{"title":61,"industry":62,"period":63,"current":64,"summary":65,"highlights":66},"DevOps Leader","證券業","2024 年 10 月 – 現在",true,"擔任技術架構師，制定並落實技術中台規範，檢視與優化至少 10 項架構使用情境，提升系統整體一致性及可用性。",[67,68,69,70,71,72,73,74,75,76,77,78],"定義技術中台服務的 SLI 與 SLO，確保服務可用率達 **99.99%**","擔任公司 Terraform Module 設計創始人，完成並落實符合 Terraform Modularize 之設計規範","於證券系統導入 ArgoCD，透過 Helm 管理應用程式，完整實作 GitOps 流程","高度自動化 Docker 環境中的 Redis、Kafka 等基礎服務，並以 Komodo 落實 IaC，提升 **80%** 建置與維運效率","正在導入 AI 化平台工程（AI-driven Platform Engineering），預期可節省 **90%** 維運時間","導入 Centralized Pipeline Libraries 管理腳本，讓開發人員免寫 CI\u002FCD 腳本","將原始碼掃描（SAST）、開源套件掃描整合至 CI\u002FCD 流程並自動產生 SBOM","規劃並導入藍綠部署（Blue\u002FGreen Deployment），成功降低 **90%** 退版機率","推動跨機房集中化監控管理，統一藉由 Grafana 集中管理告警儀表板，監控覆蓋率達 **99%**","主導 Log 系統架構優化，將吞吐量由 8 萬 RPS 大幅提升至 **20 萬 RPS**，且達成零延遲","規劃並執行 K8S 升版遷移專案，達成升級過程服務完全不中斷（Zero Downtime）","帶領 4 人編制的 DevOps 團隊，制定團隊發展藍圖、落實 Retrospective 機制並主導技術培育",{"title":80,"industry":81,"period":82,"summary":83,"highlights":84},"高級軟體工程師","房仲業","2021 年 5 月 – 2024 年 10 月","擔任公司主要流量頻道的後端技術負責人，同時兼任 DevOps\u002FSRE 與雲端架構規劃。",[85,86,87,88,89,90,91],"主導公司主力房仲平台的主要流量頻道翻寫重構，從 .NET Framework \u002F IIS 轉移至 .NET Core on Kubernetes","獨立開發旗下加盟房仲平台專案，為公司外網第一個容器化、也是第一個採用 Elasticsearch 的專案","開發集團旗下多個房仲品牌共用的數據中台","設計並開發公司內部 ChatGPT 平台（NexusGPT）","導入 Terraform，使用 DynamoDB + S3 + IAM 建立 Terraform Backend，並製作 ELB、CloudFront、EC2、S3、EKS 等模組，搭配 Go 撰寫單元測試","使用 AWS Lambda（.NET 8）+ API Gateway + S3 + CloudFront 建立前端靜態檔案服務，以及 ELB + EC2 + Auto Scaling Group 雲端應用程式服務","培育新人並撰寫技術文件，協助新進人員於兩週內快速上手",{"title":93,"industry":94,"period":95,"summary":96},"網路工程師","電信網路業","2019 年 1 月 – 2021 年 5 月","負責設定、檢修網路設備，建立危機處理標準流程，並分析、解決客戶的網路問題。","md","台灣",{},"Tom Chang","\u002Fabout",null,"DevOps \u002F SRE Leader ‧ 平台工程師",{"description":5},[106,120,128,144,153],{"group":107,"items":108},"後端開發",[109,110,111,112,113,114,115,116,117,118,119],"ASP.NET Core","C#","Domain-Driven Design","Test-Driven Design","Extreme Programming","Hexagonal Architecture","API Gateway Design","xUnit","MSTest","WebSocket","GraphQL",{"group":121,"items":122},"資料庫與訊息佇列",[123,124,125,126,127],"Elasticsearch","Redis","MongoDB","MSSQL","RabbitMQ",{"group":129,"items":130},"DevOps 與平台工程",[131,132,133,134,135,136,137,138,139,140,141,142,143],"ArgoCD","GitOps","Helm","Rancher","Istio","KEDA","OpenTelemetry","Grafana Loki","Tempo","Prometheus","Terraform","Canary Deploy","Azure DevOps",{"group":145,"items":146},"雲端與容器",[147,148,149,150,151,152],"AWS","Docker","containerd","Kubernetes","nerdctl","kubectl",{"group":154,"items":155},"IoT、嵌入式與機器人",[156,157,158,159,160,161,162,163,164,165],"Arduino","ESP8266","感測器整合（超音波 HC-SR04）","繼電器控制","DS3231 RTC","Bluetooth \u002F Wi-Fi 通訊","MIT App Inventor","Android","LEGO MINDSTORMS EV3","PD 控制",[167,171,175],{"label":168,"url":169,"icon":170},"GitHub","https:\u002F\u002Fgithub.com\u002Ftom3019","mdi:github",{"label":172,"url":173,"icon":174},"LinkedIn","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002F語桐-張-20a9ba228","mdi:linkedin",{"label":176,"url":177,"icon":178},"Credly","https:\u002F\u002Fwww.credly.com\u002Fusers\u002Fyu-tung-chang","mdi:certificate","about\u002Findex","HCNPzTMtx9NrZ_qVpHxVNymPJfDHJQNWviOEe8C4kIc",[182,955],{"id":183,"title":184,"award":102,"body":185,"cover":934,"description":5,"extension":97,"featured":64,"link":102,"meta":935,"navigation":64,"order":936,"path":937,"pinned":64,"repo":938,"role":939,"seo":940,"stack":941,"stem":951,"summary":952,"year":953,"__hash__":954},"portfolio\u002Fportfolio\u002Fecommerce-microservices-observability.md","電商微服務可觀測性專案",{"type":8,"value":186,"toc":923},[187,190,201,205,212,274,305,309,312,319,340,347,351,354,754,757,764,806,825,828,831,875,878,910,912,919],[11,188,189],{"id":189},"專案概述",[16,191,192,193,196,197,200],{},"一個以訂單（Order）、付款（Payment）、物流（Shipping）三個微服務組成的電商練\n習專案，目的不是做出完整電商功能，而是紮實地練習 ",[20,194,195],{},"DDD 戰略與戰術設計、\nEvent Sourcing"," 與",[20,198,199],{},"可觀測性（Observability）"," 在 .NET 微服務中的落地方\n式。每個服務都各自擁有獨立資料庫，並透過領域事件串接彼此的業務流程。",[11,202,204],{"id":203},"ddd-戰略設計strategic-design","DDD 戰略設計（Strategic Design）",[16,206,207,208,211],{},"三個服務對應三個獨立的 ",[20,209,210],{},"Bounded Context","，各自有自己的資料庫、程式碼庫與通\n用語言（Ubiquitous Language），彼此不共用資料表、也不直接呼叫對方的 API：",[213,214,215,230],"table",{},[216,217,218],"thead",{},[219,220,221,224,227],"tr",{},[222,223,210],"th",{},[222,225,226],{},"核心聚合",[222,228,229],{},"負責的通用語言",[231,232,233,248,261],"tbody",{},[219,234,235,239,245],{},[236,237,238],"td",{},"OrderService",[236,240,241],{},[242,243,244],"code",{},"Order",[236,246,247],{},"建立訂單、確認付款、揀貨、完成訂單、取消訂單",[219,249,250,253,258],{},[236,251,252],{},"PaymentService",[236,254,255],{},[242,256,257],{},"Payment",[236,259,260],{},"請款、付款完成、付款失敗、取消付款",[219,262,263,266,271],{},[236,264,265],{},"ShippingService",[236,267,268],{},[242,269,270],{},"Shipment",[236,272,273],{},"請求出貨、已派送、已送達",[16,275,276,277,280,281,283,284,287,288,290,291,293,294,297,298,301,302,304],{},"Context 之間",[20,278,279],{},"只透過 Kafka 上的領域事件溝通","，不共享資料庫、不同步呼叫對\n方，屬於以「Published Language（事件即契約）」串接的 Context Mapping：\n",[242,282,238],{}," 發布 ",[242,285,286],{},"OrderCreatedEvent"," 後即結束自己的職責，完全不知道\n",[242,289,252],{}," 內部如何處理請款；",[242,292,252],{}," 處理完也只發布\n",[242,295,296],{},"PaymentCompletedEvent"," \u002F ",[242,299,300],{},"PaymentFailedEvent","，同樣不知道 ",[242,303,238],{},"\n收到後會做什麼。這種鬆耦合讓三個服務可以獨立開發、獨立部署、獨立擴展。",[306,307,308],"h3",{"id":308},"事件流程示意圖",[16,310,311],{},"下圖是訂單成立、付款、出貨（以及付款失敗導致取消訂單）的完整事件流：",[16,313,314],{},[315,316],"img",{"alt":317,"src":318},"電商訂單事件流程圖","\u002Fimages\u002Fportfolio\u002Fecommerce-order-event-flow.png",[320,321,322,332],"ul",{},[323,324,325,328,329],"li",{},[20,326,327],{},"成功路徑","：",[242,330,331],{},"User → 建立訂單 → OrderCreated → 發起支付請求 → PaymentRequested → 支付完成 → PaymentCompleted → 確認付款 → OrderPaid → 發起配送請求 → ShippingRequested",[323,333,334,328,337],{},[20,335,336],{},"失敗路徑",[242,338,339],{},"支付失敗 → PaymentFailed → 取消訂單 → OrderCancelled",[16,341,342,343,346],{},"整個流程是典型的",[20,344,345],{},"事件編舞（Choreography）","：沒有一個中控服務下指令，每個\n服務只是「訂閱自己關心的事件、做完事、發布下一個事件」，服務間的協作關係完全\n從事件的訂閱／發布關係中浮現出來。",[11,348,350],{"id":349},"ddd-戰術設計tactical-design","DDD 戰術設計（Tactical Design）",[16,352,353],{},"在單一 Bounded Context 內部，則落實了完整的戰術設計工具箱：",[320,355,356,657,680,707,718,735],{},[323,357,358,328,361,363,364,363,366,368,369,372,373,376,377,380,381,384,385,388,389],{},[20,359,360],{},"Aggregate Root + Event Sourcing",[242,362,244],{},"、",[242,365,257],{},[242,367,270],{}," 都繼承\n共用的 ",[242,370,371],{},"AggregateRoot\u003CTId>","。狀態",[20,374,375],{},"不能被直接賦值修改","，只能透過\n",[242,378,379],{},"Apply(DomainEvent)"," 產生事件、在 ",[242,382,383],{},"When()"," 中依事件重建狀態，並在\n",[242,386,387],{},"EnsureValidState()"," 中檢查不變條件（Invariant），修改狀態與驗證規則被綁在\n一起，不會有「改了忘記驗證」的問題\n",[390,391,395],"pre",{"className":392,"code":393,"language":394,"meta":5,"style":5},"language-csharp shiki shiki-themes github-dark","public void ConfirmPayment()\n{\n    Apply(new OrderPaidEvent(Id, ShipmentAddress));\n}\n\nprotected override void When(DomainEvent domainEvent)\n{\n    switch (domainEvent)\n    {\n        case OrderCreatedEvent e:\n            Status = Status.PendingPayment;\n            break;\n        case OrderPaidEvent:\n            Status = Status.Paid;\n            break;\n        \u002F\u002F ...\n    }\n}\n\nprotected override void EnsureValidState()\n{\n    if (Status == Status.Cancelled && string.IsNullOrEmpty(CancellationReason))\n        throw new OrderDomainException(\"Cancellation reason is required.\");\n}\n","csharp",[242,396,397,417,422,440,446,452,477,482,491,497,512,524,533,542,552,559,566,572,577,582,596,601,631,652],{"__ignoreMap":5},[398,399,402,406,409,413],"span",{"class":400,"line":401},"line",1,[398,403,405],{"class":404},"snl16","public",[398,407,408],{"class":404}," void",[398,410,412],{"class":411},"svObZ"," ConfirmPayment",[398,414,416],{"class":415},"s95oV","()\n",[398,418,419],{"class":400,"line":34},[398,420,421],{"class":415},"{\n",[398,423,425,428,431,434,437],{"class":400,"line":424},3,[398,426,427],{"class":411},"    Apply",[398,429,430],{"class":415},"(",[398,432,433],{"class":404},"new",[398,435,436],{"class":411}," OrderPaidEvent",[398,438,439],{"class":415},"(Id, ShipmentAddress));\n",[398,441,443],{"class":400,"line":442},4,[398,444,445],{"class":415},"}\n",[398,447,449],{"class":400,"line":448},5,[398,450,451],{"emptyLinePlaceholder":64},"\n",[398,453,455,458,461,463,466,468,471,474],{"class":400,"line":454},6,[398,456,457],{"class":404},"protected",[398,459,460],{"class":404}," override",[398,462,408],{"class":404},[398,464,465],{"class":411}," When",[398,467,430],{"class":415},[398,469,470],{"class":411},"DomainEvent",[398,472,473],{"class":411}," domainEvent",[398,475,476],{"class":415},")\n",[398,478,480],{"class":400,"line":479},7,[398,481,421],{"class":415},[398,483,485,488],{"class":400,"line":484},8,[398,486,487],{"class":404},"    switch",[398,489,490],{"class":415}," (domainEvent)\n",[398,492,494],{"class":400,"line":493},9,[398,495,496],{"class":415},"    {\n",[398,498,500,503,506,509],{"class":400,"line":499},10,[398,501,502],{"class":404},"        case",[398,504,505],{"class":411}," OrderCreatedEvent",[398,507,508],{"class":411}," e",[398,510,511],{"class":415},":\n",[398,513,515,518,521],{"class":400,"line":514},11,[398,516,517],{"class":415},"            Status ",[398,519,520],{"class":404},"=",[398,522,523],{"class":415}," Status.PendingPayment;\n",[398,525,527,530],{"class":400,"line":526},12,[398,528,529],{"class":404},"            break",[398,531,532],{"class":415},";\n",[398,534,536,538,540],{"class":400,"line":535},13,[398,537,502],{"class":404},[398,539,436],{"class":411},[398,541,511],{"class":415},[398,543,545,547,549],{"class":400,"line":544},14,[398,546,517],{"class":415},[398,548,520],{"class":404},[398,550,551],{"class":415}," Status.Paid;\n",[398,553,555,557],{"class":400,"line":554},15,[398,556,529],{"class":404},[398,558,532],{"class":415},[398,560,562],{"class":400,"line":561},16,[398,563,565],{"class":564},"sAwPA","        \u002F\u002F ...\n",[398,567,569],{"class":400,"line":568},17,[398,570,571],{"class":415},"    }\n",[398,573,575],{"class":400,"line":574},18,[398,576,445],{"class":415},[398,578,580],{"class":400,"line":579},19,[398,581,451],{"emptyLinePlaceholder":64},[398,583,585,587,589,591,594],{"class":400,"line":584},20,[398,586,457],{"class":404},[398,588,460],{"class":404},[398,590,408],{"class":404},[398,592,593],{"class":411}," EnsureValidState",[398,595,416],{"class":415},[398,597,599],{"class":400,"line":598},21,[398,600,421],{"class":415},[398,602,604,607,610,613,616,619,622,625,628],{"class":400,"line":603},22,[398,605,606],{"class":404},"    if",[398,608,609],{"class":415}," (Status ",[398,611,612],{"class":404},"==",[398,614,615],{"class":415}," Status.Cancelled ",[398,617,618],{"class":404},"&&",[398,620,621],{"class":404}," string",[398,623,624],{"class":415},".",[398,626,627],{"class":411},"IsNullOrEmpty",[398,629,630],{"class":415},"(CancellationReason))\n",[398,632,634,637,640,643,645,649],{"class":400,"line":633},23,[398,635,636],{"class":404},"        throw",[398,638,639],{"class":404}," new",[398,641,642],{"class":411}," OrderDomainException",[398,644,430],{"class":415},[398,646,648],{"class":647},"sU2Wk","\"Cancellation reason is required.\"",[398,650,651],{"class":415},");\n",[398,653,655],{"class":400,"line":654},24,[398,656,445],{"class":415},[323,658,659,328,662,363,665,363,668,671,672,675,676,679],{},[20,660,661],{},"強型別 ID（Value Object）",[242,663,664],{},"OrderId",[242,666,667],{},"PaymentId",[242,669,670],{},"ShipmentId","、\n",[242,673,674],{},"MemberId"," 都是獨立的 Value Object，而不是裸的 ",[242,677,678],{},"Guid","，避免把訂單 ID 誤傳\n成會員 ID 這種編譯期抓不到的錯誤",[323,681,682,328,685,687,688,691,692,671,695,698,699,702,703,706],{},[20,683,684],{},"狀態機驅動",[242,686,244],{}," 從 ",[242,689,690],{},"PendingPayment"," 開始，依序經過 ",[242,693,694],{},"Paid",[242,696,697],{},"PickingInProgress","，最終到達 ",[242,700,701],{},"Completed","，或在任何節點被 ",[242,704,705],{},"Cancelled","；狀\n態轉換完全由事件決定，畫面上看到的狀態永遠是「套用過哪些事件」的結果",[323,708,709,712,713,363,715,717],{},[20,710,711],{},"事件即事實，而非指令","：領域事件一律用過去式命名\n（",[242,714,286],{},[242,716,296],{},"），代表「已經發生的事\n實」，而不是「請你做什麼」的命令，這也是它能安全地被多個服務訂閱、不需要協\n調誰先誰後的原因",[323,719,720,723,724,297,727,730,731,734],{},[20,721,722],{},"Null Object Pattern","：以 ",[242,725,726],{},"INullObject",[242,728,729],{},"Order.Null"," 取代 ",[242,732,733],{},"null"," 判\n斷，减少呼叫端重複的防禦性檢查",[323,736,737,328,740,743,744,671,747,363,750,753],{},[20,738,739],{},"事件溯源基礎設施",[242,741,742],{},"AggregateRoot"," 內建 ",[242,745,746],{},"Version",[242,748,749],{},"GetUncommittedEvents()",[242,751,752],{},"Load(history)","，聚合可以完全由事件歷史重建，而\n非只依賴目前快照",[11,755,756],{"id":756},"系統架構",[16,758,759,760,763],{},"三個服務都採用 ",[20,761,762],{},"Hexagonal Architecture（Ports & Adapters）","，並拆成獨立的\n專案分層：",[320,765,766,772,788,794,800],{},[323,767,768,771],{},[242,769,770],{},"Entities","：聚合根與領域事件，對應各服務的狀態機",[323,773,774,777,778,671,781,363,784,787],{},[242,775,776],{},"UseCase","：應用層服務與 In\u002FOut Port 介面（如 ",[242,779,780],{},"CreateOrderService",[242,782,783],{},"RequestPaymentService",[242,785,786],{},"DispatchOrderService","）",[323,789,790,793],{},[242,791,792],{},"Adapter.Out","：以 EF Core 實作的資料庫存取層與 Migration",[323,795,796,799],{},[242,797,798],{},"WebApi","：Minimal API 端點與事件訂閱者（Event Handler）",[323,801,802,805],{},[242,803,804],{},"MainComponent","：各服務的組裝根（DI 註冊）",[16,807,808,809,812,813,816,817,820,821,824],{},"共用套件則抽成獨立專案：",[242,810,811],{},"E-Commerce.SeedWork","（AggregateRoot、DomainEvent 等\nDDD 基礎建構）、",[242,814,815],{},"E-Commerce.EventBus","（以 Wolverine 包裝 Kafka 的領域事件匯流\n排）、",[242,818,819],{},"E-Commerce.Diagnostics","（Tracing／Metrics／AOP）。並以 ",[20,822,823],{},".NET Aspire","\n作為本機開發時的服務編排（AppHost）。",[11,826,827],{"id":827},"可觀測性亮點",[16,829,830],{},"這個專案真正想練習的重點：",[320,832,833,844,854,861,868],{},[323,834,835,836,839,840,843],{},"以 .NET 內建 ",[242,837,838],{},"ActivitySource"," 在三個服務中發送 Trace，並將 ",[20,841,842],{},"TraceId 寫入\n領域事件本身","，讓一筆訂單從建立、付款到出貨的完整流程，能在分散式追蹤中被\n串成同一條 Trace",[323,845,846,847,850,851],{},"自訂 ",[242,848,849],{},"MethodTracingAttribute","（AOP）快速為方法加上追蹤埋點，不需手動包\n",[242,852,853],{},"using activity",[323,855,856,857,860],{},"透過 ",[242,858,859],{},"IMeterFactory"," 建立自訂 Metrics（計數器、直方圖、浮動計數器），例如\n訂單金額分佈、待處理付款數量",[323,862,863,864,867],{},"以 OpenTelemetry Collector 統一收集 Trace／Metric／Log，轉送至 ",[20,865,866],{},"Tempo、\nPrometheus、Loki","，並用 Grafana 儀表板呈現",[323,869,870,871,874],{},"整合 ",[20,872,873],{},"Grafana MCP 與 Tempo MCP","，讓開發時能直接用 TraceQL／PromQL／LogQL\n在編輯器內查詢追蹤與指標，不必切換到 Grafana 介面除錯",[11,876,877],{"id":877},"使用技術",[320,879,880,886,892,898,904],{},[323,881,882,885],{},[20,883,884],{},"應用層","：.NET 8、ASP.NET Core Minimal API、Wolverine、Kafka",[323,887,888,891],{},[20,889,890],{},"架構","：Domain-Driven Design（戰略＋戰術設計）、Event Sourcing、\nHexagonal Architecture",[323,893,894,897],{},[20,895,896],{},"資料層","：EF Core、Code First、MSSQL",[323,899,900,903],{},[20,901,902],{},"可觀測性","：OpenTelemetry、Grafana、Tempo、Loki、Prometheus",[323,905,906,909],{},[20,907,908],{},"開發體驗","：.NET Aspire、xUnit、Docker Compose",[11,911,29],{"id":29},[16,913,914,915,918],{},"專案仍在持續補充中，目前已完整展示三服務間的事件編舞（Choreography）與可觀測\n性串接；後續規劃導入 ",[20,916,917],{},"SAGA Pattern"," 讓跨服務交易一致性有明確的補償流程可\n循，並在服務前加上 API Gateway。",[920,921,922],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}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);}",{"title":5,"searchDepth":34,"depth":34,"links":924},[925,926,929,930,931,932,933],{"id":189,"depth":34,"text":189},{"id":203,"depth":34,"text":204,"children":927},[928],{"id":308,"depth":424,"text":308},{"id":349,"depth":34,"text":350},{"id":756,"depth":34,"text":756},{"id":827,"depth":34,"text":827},{"id":877,"depth":34,"text":877},{"id":29,"depth":34,"text":29},"https:\u002F\u002Fraw.githubusercontent.com\u002Ftom3019\u002FE-Commerce\u002Fmaster\u002Fimgs\u002Ftracing.png",{},1.5,"\u002Fportfolio\u002Fecommerce-microservices-observability","https:\u002F\u002Fgithub.com\u002Ftom3019\u002FE-Commerce","個人 Side Project ‧ 架構設計與開發",{"title":184,"description":5},[942,943,944,945,114,946,947,823,137,948,139,949,140,950],".NET 8","ASP.NET Core Minimal API","DDD","Event Sourcing","Wolverine","Kafka","Grafana","Loki","EF Core","portfolio\u002Fecommerce-microservices-observability","訂單、付款、物流三個微服務組成的電商練習專案，以 DDD 戰略／戰術設計 + Hexagonal Architecture 實作 Event Sourcing，並完整導入 OpenTelemetry 可觀測性堆疊與 Grafana／Tempo MCP 查詢工具。",2024,"uAOzN5MK_-PBR8yFYoG4EJtJrgar0vzEby5kl5zuZDU",{"id":956,"title":957,"award":102,"body":958,"cover":1454,"description":5,"extension":97,"featured":64,"link":102,"meta":1455,"navigation":64,"order":448,"path":1456,"pinned":1457,"repo":1458,"role":1459,"seo":1460,"stack":1461,"stem":1468,"summary":1469,"year":1470,"__hash__":1471},"portfolio\u002Fportfolio\u002Faws-terraform-iac.md","AWS 基礎設施即程式碼（Terraform 模組）",{"type":8,"value":959,"toc":1445},[960,962,965,968,976,994,997,1000,1128,1151,1155,1158,1207,1222,1226,1240,1409,1416,1419,1436,1439,1442],[11,961,189],{"id":189},[16,963,964],{},"建立公司的 Terraform 標準規範：一套遠端 State 後端，以及一系列可重複使用、附\n完整文件與測試的模組，讓各團隊能以一致的方式建置 AWS 基礎設施，而非各自土法\n煉鋼。",[11,966,967],{"id":967},"專案結構",[390,969,974],{"className":970,"code":972,"language":973},[971],"language-text","infrastracture\u002F\n  global\u002F         # 只需要存在一份的共用資源：State 後端、鎖表、IAM Role\n  development\u002F\n  stage\u002F\n  production\u002F\nmodules\u002F\n  aws\u002F\n    alb\u002F\n    cloudfront\u002F\n    ec2\u002F\n    s3\u002F\n","text",[242,975,972],{"__ignoreMap":5},[16,977,978,981,982,985,986,989,990,993],{},[242,979,980],{},"global"," 存放 Terraform 自身需要的基礎設施——狀態儲存與鎖定機制，",[242,983,984],{},"development","\n／",[242,987,988],{},"stage","／",[242,991,992],{},"production"," 則各自獨立，同一份模組在不同環境用不同的變數值套用，\n彼此的 State 完全隔離。",[11,995,996],{"id":996},"遠端後端與存取控制",[16,998,999],{},"Bootstrap 一次性建立好 Terraform 自己需要的基礎設施後，往後所有模組都指向同\n一組遠端後端：",[390,1001,1005],{"className":1002,"code":1003,"language":1004,"meta":5,"style":5},"language-hcl shiki shiki-themes github-dark","terraform {\n  backend \"s3\" {\n    bucket         = \"tom-terraform-backend\"\n    key            = \"global\u002Fs3\u002Ftom-terraform-backend\u002Fterraform.tfstate\"\n    region         = \"ap-northeast-1\"\n    dynamodb_table = \"tom-terraform-backend-lock\"\n    encrypt        = true\n    assume_role = {\n      role_arn    = \"arn\u002FS_Terraform_Role\"\n      policy_arns = [\"arn\u002FS_Terraform_Role-policy\"]\n    }\n  }\n}\n","hcl",[242,1006,1007,1015,1026,1037,1048,1058,1069,1080,1089,1099,1115,1119,1124],{"__ignoreMap":5},[398,1008,1009,1012],{"class":400,"line":401},[398,1010,1011],{"class":411},"terraform",[398,1013,1014],{"class":415}," {\n",[398,1016,1017,1020,1024],{"class":400,"line":34},[398,1018,1019],{"class":411},"  backend",[398,1021,1023],{"class":1022},"sDLfK"," \"s3\"",[398,1025,1014],{"class":415},[398,1027,1028,1031,1034],{"class":400,"line":424},[398,1029,1030],{"class":415},"    bucket",[398,1032,1033],{"class":404},"         =",[398,1035,1036],{"class":647}," \"tom-terraform-backend\"\n",[398,1038,1039,1042,1045],{"class":400,"line":442},[398,1040,1041],{"class":415},"    key",[398,1043,1044],{"class":404},"            =",[398,1046,1047],{"class":647}," \"global\u002Fs3\u002Ftom-terraform-backend\u002Fterraform.tfstate\"\n",[398,1049,1050,1053,1055],{"class":400,"line":448},[398,1051,1052],{"class":415},"    region",[398,1054,1033],{"class":404},[398,1056,1057],{"class":647}," \"ap-northeast-1\"\n",[398,1059,1060,1063,1066],{"class":400,"line":454},[398,1061,1062],{"class":415},"    dynamodb_table",[398,1064,1065],{"class":404}," =",[398,1067,1068],{"class":647}," \"tom-terraform-backend-lock\"\n",[398,1070,1071,1074,1077],{"class":400,"line":479},[398,1072,1073],{"class":415},"    encrypt",[398,1075,1076],{"class":404},"        =",[398,1078,1079],{"class":1022}," true\n",[398,1081,1082,1085,1087],{"class":400,"line":484},[398,1083,1084],{"class":415},"    assume_role",[398,1086,1065],{"class":404},[398,1088,1014],{"class":415},[398,1090,1091,1094,1096],{"class":400,"line":493},[398,1092,1093],{"class":415},"      role_arn    ",[398,1095,520],{"class":404},[398,1097,1098],{"class":647}," \"arn\u002FS_Terraform_Role\"\n",[398,1100,1101,1104,1106,1109,1112],{"class":400,"line":499},[398,1102,1103],{"class":415},"      policy_arns ",[398,1105,520],{"class":404},[398,1107,1108],{"class":415}," [",[398,1110,1111],{"class":647},"\"arn\u002FS_Terraform_Role-policy\"",[398,1113,1114],{"class":415},"]\n",[398,1116,1117],{"class":400,"line":514},[398,1118,571],{"class":415},[398,1120,1121],{"class":400,"line":526},[398,1122,1123],{"class":415},"  }\n",[398,1125,1126],{"class":400,"line":535},[398,1127,445],{"class":415},[320,1129,1130,1144],{},[323,1131,1132,1135,1136,1139,1140,1143],{},[20,1133,1134],{},"S3"," 儲存 State、",[20,1137,1138],{},"DynamoDB"," 做 State Lock，避免多人同時 ",[242,1141,1142],{},"apply"," 造成競\n爭寫入",[323,1145,1146,1147,1150],{},"所有操作都透過 ",[242,1148,1149],{},"assume_role"," 走一個專屬的 Terraform 執行角色，而不是用長期\n存在的 IAM User AccessKey，降低憑證外洩風險",[11,1152,1154],{"id":1153},"模組文件terraform-registry-風格","模組文件（Terraform Registry 風格）",[16,1156,1157],{},"每個模組（ALB、CloudFront、EC2、S3）都附上 Registry 慣用的四段式文件：",[213,1159,1160,1170],{},[216,1161,1162],{},[219,1163,1164,1167],{},[222,1165,1166],{},"項目",[222,1168,1169],{},"說明",[231,1171,1172,1183,1191,1199],{},[219,1173,1174,1177],{},[236,1175,1176],{},"Usage",[236,1178,1179,1182],{},[242,1180,1181],{},"module"," 區塊的最小可用範例",[219,1184,1185,1188],{},[236,1186,1187],{},"Requirements",[236,1189,1190],{},"Terraform／Provider 版本限制",[219,1192,1193,1196],{},[236,1194,1195],{},"Inputs",[236,1197,1198],{},"每個變數的型別、預設值、是否必填",[219,1200,1201,1204],{},[236,1202,1203],{},"Outputs",[236,1205,1206],{},"模組回傳的屬性（如 ARN、ID）",[16,1208,1209,1210,1213,1214,1217,1218,1221],{},"以 S3 模組為例，Inputs 包含 ",[242,1211,1212],{},"s3_bucket_name","（必填）、\n",[242,1215,1216],{},"s3_bucket_versioning","（預設開啟版本控制）、",[242,1219,1220],{},"prevent_destroy","（預設鎖住刪\n除，避免誤刪正式環境的 bucket），呼叫端不需要讀原始碼就知道怎麼用。",[11,1223,1225],{"id":1224},"用-goterratest驗證模組","用 Go（Terratest）驗證模組",[16,1227,1228,1229,1232,1233,1235,1236,1239],{},"不是只做 ",[242,1230,1231],{},"terraform validate","，而是每個模組都有一支 Go 整合測試，實際\n",[242,1234,1142],{}," 一份範例設定、檢查輸出的屬性符合預期、再 ",[242,1237,1238],{},"destroy"," 乾淨：",[390,1241,1245],{"className":1242,"code":1243,"language":1244,"meta":5,"style":5},"language-go shiki shiki-themes github-dark","func TestS3Example(t *testing.T) {\n    ops := &terraform.Options{TerraformDir: \"..\u002Fexample\"}\n    defer terraform.Destroy(t, ops)\n\n    terraform.InitAndApply(t, ops)\n\n    bucketArn := terraform.Output(t, ops, \"bucket_arn\")\n    bucketTags := terraform.OutputMap(t, ops, \"tags\")\n    assert.NotNil(t, bucketArn)\n    assert.Equal(t, \"expected-bucket-name\", bucketTags[\"Name\"])\n}\n","go",[242,1246,1247,1275,1301,1315,1319,1329,1333,1353,1372,1383,1405],{"__ignoreMap":5},[398,1248,1249,1252,1255,1257,1261,1264,1267,1269,1272],{"class":400,"line":401},[398,1250,1251],{"class":404},"func",[398,1253,1254],{"class":411}," TestS3Example",[398,1256,430],{"class":415},[398,1258,1260],{"class":1259},"s9osk","t",[398,1262,1263],{"class":404}," *",[398,1265,1266],{"class":411},"testing",[398,1268,624],{"class":415},[398,1270,1271],{"class":411},"T",[398,1273,1274],{"class":415},") {\n",[398,1276,1277,1280,1283,1286,1288,1290,1293,1296,1299],{"class":400,"line":34},[398,1278,1279],{"class":415},"    ops ",[398,1281,1282],{"class":404},":=",[398,1284,1285],{"class":404}," &",[398,1287,1011],{"class":411},[398,1289,624],{"class":415},[398,1291,1292],{"class":411},"Options",[398,1294,1295],{"class":415},"{TerraformDir: ",[398,1297,1298],{"class":647},"\"..\u002Fexample\"",[398,1300,445],{"class":415},[398,1302,1303,1306,1309,1312],{"class":400,"line":424},[398,1304,1305],{"class":404},"    defer",[398,1307,1308],{"class":415}," terraform.",[398,1310,1311],{"class":411},"Destroy",[398,1313,1314],{"class":415},"(t, ops)\n",[398,1316,1317],{"class":400,"line":442},[398,1318,451],{"emptyLinePlaceholder":64},[398,1320,1321,1324,1327],{"class":400,"line":448},[398,1322,1323],{"class":415},"    terraform.",[398,1325,1326],{"class":411},"InitAndApply",[398,1328,1314],{"class":415},[398,1330,1331],{"class":400,"line":454},[398,1332,451],{"emptyLinePlaceholder":64},[398,1334,1335,1338,1340,1342,1345,1348,1351],{"class":400,"line":479},[398,1336,1337],{"class":415},"    bucketArn ",[398,1339,1282],{"class":404},[398,1341,1308],{"class":415},[398,1343,1344],{"class":411},"Output",[398,1346,1347],{"class":415},"(t, ops, ",[398,1349,1350],{"class":647},"\"bucket_arn\"",[398,1352,476],{"class":415},[398,1354,1355,1358,1360,1362,1365,1367,1370],{"class":400,"line":484},[398,1356,1357],{"class":415},"    bucketTags ",[398,1359,1282],{"class":404},[398,1361,1308],{"class":415},[398,1363,1364],{"class":411},"OutputMap",[398,1366,1347],{"class":415},[398,1368,1369],{"class":647},"\"tags\"",[398,1371,476],{"class":415},[398,1373,1374,1377,1380],{"class":400,"line":493},[398,1375,1376],{"class":415},"    assert.",[398,1378,1379],{"class":411},"NotNil",[398,1381,1382],{"class":415},"(t, bucketArn)\n",[398,1384,1385,1387,1390,1393,1396,1399,1402],{"class":400,"line":499},[398,1386,1376],{"class":415},[398,1388,1389],{"class":411},"Equal",[398,1391,1392],{"class":415},"(t, ",[398,1394,1395],{"class":647},"\"expected-bucket-name\"",[398,1397,1398],{"class":415},", bucketTags[",[398,1400,1401],{"class":647},"\"Name\"",[398,1403,1404],{"class":415},"])\n",[398,1406,1407],{"class":400,"line":514},[398,1408,445],{"class":415},[16,1410,1411,1412,1415],{},"這讓模組的正確性不是靠人工檢查 ",[242,1413,1414],{},".tf"," 檔案，而是每次修改都能重新驗證「這個模組\n套用後，AWS 上真的長出預期的資源與屬性」。",[11,1417,1418],{"id":1418},"實作內容",[320,1420,1421,1424,1427,1430,1433],{},[323,1422,1423],{},"使用 S3 + DynamoDB + IAM 建立 Terraform 遠端後端，並以 tfvars 管理可編輯人員",[323,1425,1426],{},"製作 ALB、CloudFront、EC2、S3 等可重複使用模組，依環境（development／\nstage／production）切分",[323,1428,1429],{},"用 Go（Terratest）為模組撰寫 Apply → Assert → Destroy 的整合測試",[323,1431,1432],{},"撰寫 Registry 風格的模組使用說明文件與內部 Terraform 教學訓練",[323,1434,1435],{},"盤點並調整公司現有 IAM User，改用 AssumeRole 降低常駐憑證數量",[11,1437,1438],{"id":1438},"成果",[16,1440,1441],{},"讓每個團隊都能以一致、經過測試的方式建置 AWS 基礎設施，取代過去各專案各自為\n政、缺乏文件與遠端後端管理的 Terraform 用法。",[920,1443,1444],{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}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 .snl16, html code.shiki .snl16{--shiki-default:#F97583}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 .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":5,"searchDepth":34,"depth":34,"links":1446},[1447,1448,1449,1450,1451,1452,1453],{"id":189,"depth":34,"text":189},{"id":967,"depth":34,"text":967},{"id":996,"depth":34,"text":996},{"id":1153,"depth":34,"text":1154},{"id":1224,"depth":34,"text":1225},{"id":1418,"depth":34,"text":1418},{"id":1438,"depth":34,"text":1438},"\u002Fimages\u002Fportfolio\u002Faws-web-services-logo.svg",{},"\u002Fportfolio\u002Faws-terraform-iac",false,"https:\u002F\u002Fgithub.com\u002Ftom3019\u002FIaC.Aws","DevOps \u002F SRE ‧ 房仲業",{"title":957,"description":5},[141,147,1462,1463,1138,1134,1464,1465,1466,1467],"Go","Terratest","IAM","EC2","CloudFront","ALB","portfolio\u002Faws-terraform-iac","依環境切分的 Terraform 專案結構，附上 Terraform Registry 風格的模組文件（Requirements／Providers／Inputs／Outputs）與 Go（Terratest）整合測試。",2025,"wMzMBY2PaZh_D-oJxL3LoM2wVsYnXoXfFC4YZd5Ydh0",[1473,2584],{"id":1474,"title":1475,"body":1476,"date":2572,"description":2573,"excerpt":102,"extension":97,"meta":2574,"navigation":64,"path":2575,"seo":2576,"stem":2577,"tags":2578,"__hash__":2583},"articles\u002Farticles\u002Fk8s-hpa.md","Kubernetes HPA 為什麼看 Request 而不是 Limit？從擴展公式反推 CPU Request／Limit 怎麼設",{"type":8,"value":1477,"toc":2553},[1478,1485,1492,1496,1499,1505,1515,1521,1524,1552,1556,1560,1571,1580,1584,1587,1631,1638,1642,1649,1653,1660,1689,1693,1698,1720,1723,1814,1817,1893,2067,2070,2077,2167,2171,2217,2221,2228,2246,2249,2329,2336,2340,2347,2361,2371,2375,2382,2396,2399,2423,2427,2497,2512,2515,2550],[16,1479,1480,1481,1484],{},"設定 HPA（Horizontal Pod Autoscaler）時，最常被問的一個問題是：「為什麼 ",[242,1482,1483],{},"averageUtilization: 75"," 的 75% 是相對 Request 算，而不是相對 Limit？Limit 才是 Pod 真正能用到的上限啊。」",[16,1486,1487,1488,1491],{},"這個問題背後其實藏著 HPA 的設計哲學：",[20,1489,1490],{},"HPA 不是在 Pod 快撐不住時才救火，而是在用量超過「預期常態」的某個比例時，就提早擴展","，替新 Pod 的啟動與就緒爭取緩衝時間。理解這點之後，Request、Limit、HPA Target 三個參數該怎麼設，就會變成一個可以推導的問題，而不是憑感覺填數字。",[11,1493,1495],{"id":1494},"先看-hpa-到底怎麼算","先看 HPA 到底怎麼算",[16,1497,1498],{},"HPA 控制器預設每 15 秒執行一次，從 metrics-server 取得每個 Pod 的 CPU 用量，然後套用這條公式：",[390,1500,1503],{"className":1501,"code":1502,"language":973},[971],"desiredReplicas = ceil( currentReplicas × currentUtilization \u002F targetUtilization )\n\ncurrentUtilization = 所有 Pod 實際 CPU 用量的平均 ÷ 每個 Pod 的 CPU Request\n",[242,1504,1502],{"__ignoreMap":5},[16,1506,1507,1508,1511,1512,328],{},"舉例：目前 2 個 Pod、Request 為 ",[242,1509,1510],{},"800m","、Target 為 75%，兩個 Pod 平均用了 ",[242,1513,1514],{},"1000m",[390,1516,1519],{"className":1517,"code":1518,"language":973},[971],"currentUtilization = 1000m \u002F 800m = 125%\ndesiredReplicas    = ceil( 2 × 125 \u002F 75 ) = ceil(3.33) = 4\n",[242,1520,1518],{"__ignoreMap":5},[16,1522,1523],{},"從公式可以讀出幾個重要的事實：",[320,1525,1526,1532,1542],{},[323,1527,1528,1531],{},[20,1529,1530],{},"分母是 Request，Limit 完全不參與計算。"," 使用率可以超過 100%（上例就是 125%），因為 Pod 被允許用到 Request 以上、Limit 以下的空間。",[323,1533,1534,1537,1538,1541],{},[20,1535,1536],{},"沒有設定 CPU Request 的 Pod，HPA 算不出使用率。"," 這時 HPA 會顯示 ",[242,1539,1540],{},"\u003Cunknown>","，完全不會擴展。如果 Pod 裡有 sidecar（例如 Istio 的 envoy），sidecar 也要設 Request，因為使用率是整個 Pod 所有容器的加總。",[323,1543,1544,1547,1548,1551],{},[20,1545,1546],{},"有 10% 的容忍區間。"," 當 ",[242,1549,1550],{},"currentUtilization \u002F targetUtilization"," 落在 0.9～1.1 之間時，HPA 不會動作，避免在目標值附近來回抖動。",[11,1553,1555],{"id":1554},"為什麼基於-request-而不是-limit","為什麼基於 Request 而不是 Limit？",[306,1557,1559],{"id":1558},"_1-用-limit-算擴展時機一定太晚","1. 用 Limit 算，擴展時機一定太晚",[16,1561,1562,1563,1566,1567,1570],{},"Limit 是硬性天花板。CPU 用量碰到 Limit 時，Linux 的 CFS 排程器會開始 ",[20,1564,1565],{},"Throttling","——容器在每個 100ms 週期內用完配額就被強制暫停，反映到應用程式上就是延遲突然飆高、timeout 開始出現。記憶體碰到 Limit 則更直接，容器會被 ",[20,1568,1569],{},"OOMKilled","。",[16,1572,1573,1574,1579],{},"而新 Pod 從被建立到真正能接流量，要經過排程、拉 image、應用程式啟動、通過 Readiness Probe，常常要 1～3 分鐘（探針怎麼設計可以參考",[1575,1576,1578],"a",{"href":1577},"\u002Farticles\u002Fdotnet-k8s-health-checks","《Kubernetes 探針怎麼設計？》","）。如果等到接近 Limit 才觸發擴展，這段空窗期裡舊 Pod 早就被節流或砍掉了。",[306,1581,1583],{"id":1582},"_2-request-才是預期常態用量","2. Request 才是「預期常態用量」",[16,1585,1586],{},"在容量規劃上，兩個參數的角色是這樣分工的：",[213,1588,1589,1603],{},[216,1590,1591],{},[219,1592,1593,1597,1600],{},[222,1594,1596],{"align":1595},"left","參數",[222,1598,1599],{"align":1595},"角色",[222,1601,1602],{"align":1595},"意義",[231,1604,1605,1618],{},[219,1606,1607,1612,1615],{},[236,1608,1609],{"align":1595},[20,1610,1611],{},"Request",[236,1613,1614],{"align":1595},"保證水位",[236,1616,1617],{"align":1595},"應用程式在正常流量下的基準消耗量，Kubernetes 保證一定分得到",[219,1619,1620,1625,1628],{},[236,1621,1622],{"align":1595},[20,1623,1624],{},"Limit",[236,1626,1627],{"align":1595},"突發容量（Burst capacity）",[236,1629,1630],{"align":1595},"應對短時間尖峰的彈性空間，不保證一定分得到",[16,1632,1633,1634,1637],{},"HPA 的邏輯因此很自然：",[20,1635,1636],{},"用量超過常態基準的某個比例，就該加副本了","；而加副本的這段時間，超出的負載由 Request 到 Limit 之間的彈性空間來吸收。",[306,1639,1641],{"id":1640},"_3-與-scheduler-的資源記帳方式一致","3. 與 Scheduler 的資源記帳方式一致",[16,1643,1644,1645,1648],{},"Kubernetes Scheduler 決定 Pod 要放到哪個 Node 時，看的是 ",[20,1646,1647],{},"Request 的加總","，而不是 Limit，也不是實際用量。HPA 也用 Request 計算，代表「HPA 決定加幾個 Pod」跟「叢集能不能放得下這些 Pod」用的是同一套帳本——HPA 算出要 4 個 Pod，Scheduler 就準確知道需要 4 × 800m 的可分配空間。",[11,1650,1652],{"id":1651},"常見疑慮流量長期大於-request會不會無限擴展","常見疑慮：流量長期大於 Request，會不會無限擴展？",[16,1654,1655,1656,1659],{},"會一直擴展，但這是 HPA ",[20,1657,1658],{},"預期中","的健康行為——流量本來就超過基礎水位了，系統本來就該增加副本數來攤平負載。要防止的不是「擴展」本身，而是「擴展到把叢集資源榨乾」，正確的作法是：",[1661,1662,1663,1673,1679],"ol",{},[323,1664,1665,1672],{},[20,1666,1667,1668,1671],{},"設定 ",[242,1669,1670],{},"maxReplicas"," 上限","，並確認叢集（或 Cluster Autoscaler 的 Node 上限）真的放得下這麼多副本。",[323,1674,1675,1678],{},[20,1676,1677],{},"設定合理的 Target Utilization","，一般落在 60%～80%。越低代表擴得越早、越保守，但閒置成本越高。",[323,1680,1681,1684,1685,1688],{},[20,1682,1683],{},"Request 要貼近實際常態用量。"," 設得極低（例如為了塞更多 Pod 而只給 ",[242,1686,1687],{},"100m","），HPA 就會在常態流量下一直以為使用率爆表，副本數永遠頂在上限。",[11,1690,1692],{"id":1691},"實戰情境常態-05-core峰值-2-core-怎麼設","實戰情境：常態 0.5 core、峰值 2 core 怎麼設",[16,1694,1695],{},[20,1696,1697],{},"情境假設（單一 Pod 的用量）：",[320,1699,1700,1707,1713],{},[323,1701,1702,1703,1706],{},"常態用量：",[242,1704,1705],{},"500m","（0.5 core）",[323,1708,1709,1710,1712],{},"流量翻倍：",[242,1711,1514],{},"（1 core）",[323,1714,1715,1716,1719],{},"極端峰值：",[242,1717,1718],{},"2000m","（2 core，常態的四倍）",[306,1721,1722],{"id":1722},"推薦參數配置",[213,1724,1725,1736],{},[216,1726,1727],{},[219,1728,1729,1731,1734],{},[222,1730,1596],{"align":1595},[222,1732,1733],{"align":1595},"建議設定值",[222,1735,1169],{"align":1595},[231,1737,1738,1752,1766,1785,1800],{},[219,1739,1740,1745,1749],{},[236,1741,1742],{"align":1595},[20,1743,1744],{},"CPU Request",[236,1746,1747],{"align":1595},[242,1748,1510],{},[236,1750,1751],{"align":1595},"排程的保證水位，也是 HPA 計算使用率的分母",[219,1753,1754,1759,1763],{},[236,1755,1756],{"align":1595},[20,1757,1758],{},"CPU Limit",[236,1760,1761],{"align":1595},[242,1762,1718],{},[236,1764,1765],{"align":1595},"硬限制，用來吸收極端峰值，避免新 Pod 啟動空窗期被 Throttling",[219,1767,1768,1773,1778],{},[236,1769,1770],{"align":1595},[20,1771,1772],{},"HPA Target",[236,1774,1775],{"align":1595},[242,1776,1777],{},"75%",[236,1779,1780,1781,1784],{"align":1595},"平均用量達到 ",[242,1782,1783],{},"600m","（800m × 75%）時開始擴展",[219,1786,1787,1792,1797],{},[236,1788,1789],{"align":1595},[20,1790,1791],{},"minReplicas",[236,1793,1794],{"align":1595},[242,1795,1796],{},"2",[236,1798,1799],{"align":1595},"至少兩個副本，單一 Pod 掛掉時服務不中斷",[219,1801,1802,1806,1811],{},[236,1803,1804],{"align":1595},[20,1805,1670],{},[236,1807,1808],{"align":1595},[242,1809,1810],{},"10",[236,1812,1813],{"align":1595},"依叢集容量與下游（DB、外部 API）能承受的併發數決定",[16,1815,1816],{},"對應的 YAML：",[390,1818,1822],{"className":1819,"code":1820,"language":1821,"meta":5,"style":5},"language-yaml shiki shiki-themes github-dark","# deployment.yaml（節錄）\nresources:\n  requests:\n    cpu: 800m\n    memory: 512Mi\n  limits:\n    cpu: 2000m\n    memory: 512Mi   # 記憶體建議 Request = Limit，原因見後文\n","yaml",[242,1823,1824,1829,1837,1844,1855,1865,1872,1881],{"__ignoreMap":5},[398,1825,1826],{"class":400,"line":401},[398,1827,1828],{"class":564},"# deployment.yaml（節錄）\n",[398,1830,1831,1835],{"class":400,"line":34},[398,1832,1834],{"class":1833},"s4JwU","resources",[398,1836,511],{"class":415},[398,1838,1839,1842],{"class":400,"line":424},[398,1840,1841],{"class":1833},"  requests",[398,1843,511],{"class":415},[398,1845,1846,1849,1852],{"class":400,"line":442},[398,1847,1848],{"class":1833},"    cpu",[398,1850,1851],{"class":415},": ",[398,1853,1854],{"class":647},"800m\n",[398,1856,1857,1860,1862],{"class":400,"line":448},[398,1858,1859],{"class":1833},"    memory",[398,1861,1851],{"class":415},[398,1863,1864],{"class":647},"512Mi\n",[398,1866,1867,1870],{"class":400,"line":454},[398,1868,1869],{"class":1833},"  limits",[398,1871,511],{"class":415},[398,1873,1874,1876,1878],{"class":400,"line":479},[398,1875,1848],{"class":1833},[398,1877,1851],{"class":415},[398,1879,1880],{"class":647},"2000m\n",[398,1882,1883,1885,1887,1890],{"class":400,"line":484},[398,1884,1859],{"class":1833},[398,1886,1851],{"class":415},[398,1888,1889],{"class":647},"512Mi",[398,1891,1892],{"class":564},"   # 記憶體建議 Request = Limit，原因見後文\n",[390,1894,1896],{"className":1819,"code":1895,"language":1821,"meta":5,"style":5},"# hpa.yaml\napiVersion: autoscaling\u002Fv2\nkind: HorizontalPodAutoscaler\nmetadata:\n  name: order-api\nspec:\n  scaleTargetRef:\n    apiVersion: apps\u002Fv1\n    kind: Deployment\n    name: order-api\n  minReplicas: 2\n  maxReplicas: 10\n  metrics:\n    - type: Resource\n      resource:\n        name: cpu\n        target:\n          type: Utilization\n          averageUtilization: 75\n",[242,1897,1898,1903,1913,1923,1930,1940,1947,1954,1964,1974,1983,1993,2003,2010,2023,2030,2040,2047,2057],{"__ignoreMap":5},[398,1899,1900],{"class":400,"line":401},[398,1901,1902],{"class":564},"# hpa.yaml\n",[398,1904,1905,1908,1910],{"class":400,"line":34},[398,1906,1907],{"class":1833},"apiVersion",[398,1909,1851],{"class":415},[398,1911,1912],{"class":647},"autoscaling\u002Fv2\n",[398,1914,1915,1918,1920],{"class":400,"line":424},[398,1916,1917],{"class":1833},"kind",[398,1919,1851],{"class":415},[398,1921,1922],{"class":647},"HorizontalPodAutoscaler\n",[398,1924,1925,1928],{"class":400,"line":442},[398,1926,1927],{"class":1833},"metadata",[398,1929,511],{"class":415},[398,1931,1932,1935,1937],{"class":400,"line":448},[398,1933,1934],{"class":1833},"  name",[398,1936,1851],{"class":415},[398,1938,1939],{"class":647},"order-api\n",[398,1941,1942,1945],{"class":400,"line":454},[398,1943,1944],{"class":1833},"spec",[398,1946,511],{"class":415},[398,1948,1949,1952],{"class":400,"line":479},[398,1950,1951],{"class":1833},"  scaleTargetRef",[398,1953,511],{"class":415},[398,1955,1956,1959,1961],{"class":400,"line":484},[398,1957,1958],{"class":1833},"    apiVersion",[398,1960,1851],{"class":415},[398,1962,1963],{"class":647},"apps\u002Fv1\n",[398,1965,1966,1969,1971],{"class":400,"line":493},[398,1967,1968],{"class":1833},"    kind",[398,1970,1851],{"class":415},[398,1972,1973],{"class":647},"Deployment\n",[398,1975,1976,1979,1981],{"class":400,"line":499},[398,1977,1978],{"class":1833},"    name",[398,1980,1851],{"class":415},[398,1982,1939],{"class":647},[398,1984,1985,1988,1990],{"class":400,"line":514},[398,1986,1987],{"class":1833},"  minReplicas",[398,1989,1851],{"class":415},[398,1991,1992],{"class":1022},"2\n",[398,1994,1995,1998,2000],{"class":400,"line":526},[398,1996,1997],{"class":1833},"  maxReplicas",[398,1999,1851],{"class":415},[398,2001,2002],{"class":1022},"10\n",[398,2004,2005,2008],{"class":400,"line":535},[398,2006,2007],{"class":1833},"  metrics",[398,2009,511],{"class":415},[398,2011,2012,2015,2018,2020],{"class":400,"line":544},[398,2013,2014],{"class":415},"    - ",[398,2016,2017],{"class":1833},"type",[398,2019,1851],{"class":415},[398,2021,2022],{"class":647},"Resource\n",[398,2024,2025,2028],{"class":400,"line":554},[398,2026,2027],{"class":1833},"      resource",[398,2029,511],{"class":415},[398,2031,2032,2035,2037],{"class":400,"line":561},[398,2033,2034],{"class":1833},"        name",[398,2036,1851],{"class":415},[398,2038,2039],{"class":647},"cpu\n",[398,2041,2042,2045],{"class":400,"line":568},[398,2043,2044],{"class":1833},"        target",[398,2046,511],{"class":415},[398,2048,2049,2052,2054],{"class":400,"line":574},[398,2050,2051],{"class":1833},"          type",[398,2053,1851],{"class":415},[398,2055,2056],{"class":647},"Utilization\n",[398,2058,2059,2062,2064],{"class":400,"line":579},[398,2060,2061],{"class":1833},"          averageUtilization",[398,2063,1851],{"class":415},[398,2065,2066],{"class":1022},"75\n",[306,2068,2069],{"id":2069},"套進公式驗算一次",[16,2071,2072,2073,2076],{},"假設 ",[242,2074,2075],{},"minReplicas: 2","，下表是不同流量下 HPA 的反應（「單 Pod 用量」指擴展前每個 Pod 承受的量）：",[213,2078,2079,2098],{},[216,2080,2081],{},[219,2082,2083,2086,2089,2092,2095],{},[222,2084,2085],{"align":1595},"流量情境",[222,2087,2088],{"align":1595},"單 Pod 用量",[222,2090,2091],{"align":1595},"使用率",[222,2093,2094],{"align":1595},"HPA 算出的副本數",[222,2096,2097],{"align":1595},"擴展後單 Pod 用量",[231,2099,2100,2119,2142],{},[219,2101,2102,2105,2109,2112,2115],{},[236,2103,2104],{"align":1595},"常態",[236,2106,2107],{"align":1595},[242,2108,1705],{},[236,2110,2111],{"align":1595},"62.5%",[236,2113,2114],{"align":1595},"2（不動作）",[236,2116,2117],{"align":1595},[242,2118,1705],{},[219,2120,2121,2124,2128,2131,2137],{},[236,2122,2123],{"align":1595},"翻倍",[236,2125,2126],{"align":1595},[242,2127,1514],{},[236,2129,2130],{"align":1595},"125%",[236,2132,2133,2134],{"align":1595},"ceil(2 × 125 \u002F 75) = ",[20,2135,2136],{},"4",[236,2138,2139,2141],{"align":1595},[242,2140,1705],{},"（62.5%）",[219,2143,2144,2147,2151,2154,2160],{},[236,2145,2146],{"align":1595},"極端峰值",[236,2148,2149],{"align":1595},[242,2150,1718],{},[236,2152,2153],{"align":1595},"250%",[236,2155,2156,2157],{"align":1595},"ceil(2 × 250 \u002F 75) = ",[20,2158,2159],{},"7",[236,2161,2162,2163,2166],{"align":1595},"約 ",[242,2164,2165],{},"571m","（71%）",[306,2168,2170],{"id":2169},"為什麼這樣設定最合理","為什麼這樣設定最合理？",[1661,2172,2173,2198,2207],{},[323,2174,2175,2178,2179,2181,2182,2185,2186,2188,2189,2191,2192,2194,2195,2197],{},[20,2176,2177],{},"常態用量不會觸發無效擴展。","\n如果 Request 照常態用量設成 ",[242,2180,1705],{},"、Target 80%，觸發點是 ",[242,2183,2184],{},"400m","，常態的 ",[242,2187,1705],{}," 就已經是 125% 使用率，HPA 會在平常就不斷擴展。把 Request 提高到 ",[242,2190,1510],{},"，觸發點落在 ",[242,2193,1783],{},"，常態 ",[242,2196,1705],{}," 時使用率只有 62.5%，系統會安穩維持在最小副本數。",[323,2199,2200,2203,2204,2206],{},[20,2201,2202],{},"流量翻倍時平順擴展。","\n用量跨過 ",[242,2205,1783],{}," 往 1 core 走時 HPA 就開始擴展，這時離 Limit（2 core）還有一倍的空間，舊 Pod 完全不會被節流，使用者感受不到擴展正在發生。",[323,2208,2209,2212,2213,2216],{},[20,2210,2211],{},"極端峰值時，Limit 負責掩護空窗期。","\n四倍流量瞬間湧入時，HPA 偵測 + 新 Pod 啟動通常要 1～3 分鐘。這段時間舊 Pod 靠 ",[242,2214,2215],{},"limit: 2000m"," 直接 Burst 到 2 core 消化請求；等新 Pod 就緒、流量被攤平，每個 Pod 的用量又回到 70% 上下的健康水位。",[11,2218,2220],{"id":2219},"擴縮的節奏behavior-設定","擴縮的節奏：behavior 設定",[16,2222,2223,2224,2227],{},"HPA 預設的擴縮節奏是「",[20,2225,2226],{},"擴得快、縮得慢","」：",[320,2229,2230,2236],{},[323,2231,2232,2235],{},[20,2233,2234],{},"Scale Up","：沒有穩定窗口，每 15 秒最多可以翻倍（或多加 4 個 Pod，取較大者）。",[323,2237,2238,2241,2242,2245],{},[20,2239,2240],{},"Scale Down","：有 ",[20,2243,2244],{},"300 秒","的穩定窗口——HPA 會取過去 5 分鐘內算出的最大建議值，避免流量一掉下去就馬上縮、下一波尖峰又要重新擴。",[16,2247,2248],{},"大多數情境用預設值就夠了。如果服務的流量是「尖峰很短、但一小時來好幾次」的鋸齒型，可以把縮容放得更保守：",[390,2250,2252],{"className":1819,"code":2251,"language":1821,"meta":5,"style":5},"spec:\n  behavior:\n    scaleDown:\n      stabilizationWindowSeconds: 600   # 觀察 10 分鐘才縮\n      policies:\n        - type: Pods\n          value: 1                      # 每分鐘最多縮 1 個\n          periodSeconds: 60\n",[242,2253,2254,2260,2267,2274,2287,2294,2306,2319],{"__ignoreMap":5},[398,2255,2256,2258],{"class":400,"line":401},[398,2257,1944],{"class":1833},[398,2259,511],{"class":415},[398,2261,2262,2265],{"class":400,"line":34},[398,2263,2264],{"class":1833},"  behavior",[398,2266,511],{"class":415},[398,2268,2269,2272],{"class":400,"line":424},[398,2270,2271],{"class":1833},"    scaleDown",[398,2273,511],{"class":415},[398,2275,2276,2279,2281,2284],{"class":400,"line":442},[398,2277,2278],{"class":1833},"      stabilizationWindowSeconds",[398,2280,1851],{"class":415},[398,2282,2283],{"class":1022},"600",[398,2285,2286],{"class":564},"   # 觀察 10 分鐘才縮\n",[398,2288,2289,2292],{"class":400,"line":448},[398,2290,2291],{"class":1833},"      policies",[398,2293,511],{"class":415},[398,2295,2296,2299,2301,2303],{"class":400,"line":454},[398,2297,2298],{"class":415},"        - ",[398,2300,2017],{"class":1833},[398,2302,1851],{"class":415},[398,2304,2305],{"class":647},"Pods\n",[398,2307,2308,2311,2313,2316],{"class":400,"line":479},[398,2309,2310],{"class":1833},"          value",[398,2312,1851],{"class":415},[398,2314,2315],{"class":1022},"1",[398,2317,2318],{"class":564},"                      # 每分鐘最多縮 1 個\n",[398,2320,2321,2324,2326],{"class":400,"line":484},[398,2322,2323],{"class":1833},"          periodSeconds",[398,2325,1851],{"class":415},[398,2327,2328],{"class":1022},"60\n",[16,2330,2331,2332,2335],{},"另外要注意：HPA 對",[20,2333,2334],{},"剛啟動的 Pod"," 有保護機制——Pod 還沒 Ready、或剛啟動時（預設 5 分鐘內）的 CPU 數據會被保守處理，避免 JIT 編譯、快取預熱這種啟動期的 CPU 尖峰讓 HPA 誤以為負載很高、連鎖擴展。這也是 Readiness Probe 要設計正確的另一個理由。",[11,2337,2339],{"id":2338},"記憶體為什麼不建議拿來當-hpa-指標","記憶體為什麼不建議拿來當 HPA 指標？",[16,2341,2342,2343,2346],{},"HPA 也支援 ",[242,2344,2345],{},"memory"," 指標，但對 .NET、Java 這類有 GC 的執行環境通常不適合：",[320,2348,2349,2355],{},[323,2350,2351,2354],{},[20,2352,2353],{},"記憶體用量不會隨流量下降而回落。"," GC 回收後，執行環境常常不會把記憶體還給作業系統，擴上去之後使用率一直維持高檔，HPA 就永遠縮不回來。",[323,2356,2357,2360],{},[20,2358,2359],{},"記憶體不可壓縮。"," CPU 超用只會被節流變慢，記憶體超過 Limit 是直接被 OOMKilled，沒有「Burst 一下撐過去」的空間。",[16,2362,2363,2364,2370],{},"所以一般建議是：",[20,2365,2366,2367],{},"CPU 用 HPA 彈性擴展，記憶體則設 ",[242,2368,2369],{},"Request = Limit","，讓每個 Pod 拿到的記憶體是確定的，排程與 OOM 行為都可預期。如果業務上真正的瓶頸是請求量、佇列長度（例如 Kafka consumer lag），比起記憶體，更值得透過 Prometheus Adapter 或 KEDA 改用自訂指標來擴展。",[11,2372,2374],{"id":2373},"️-架構師視角的提醒overcommit-風險","⚠️ 架構師視角的提醒：Overcommit 風險",[16,2376,2377,2378,2381],{},"上面的設定中，CPU Request（0.8）與 Limit（2.0）差距達 2.5 倍，這代表高度依賴 Kubernetes 的 ",[20,2379,2380],{},"Overcommit（超賣）"," 機制：Scheduler 只依 Request 把 Pod 塞進 Node，Node 上所有 Pod 的 Limit 加總可以遠大於 Node 的實體 CPU。",[320,2383,2384,2390],{},[323,2385,2386,2389],{},[20,2387,2388],{},"優點","：應對偶發峰值非常有效，平常不用為峰值預留整顆 CPU，資源利用率高。",[323,2391,2392,2395],{},[20,2393,2394],{},"風險","：如果同一個 Node 上的多個 Pod「同時」迎來峰值，Node 的實體 CPU 不夠分，大家會依 Request 的比例分配 CPU 時間，結果是每個 Pod 都拿不到它以為能用的 2 core，延遲一起上升。",[16,2397,2398],{},"實務上的因應方式：",[1661,2400,2401,2407,2417],{},[323,2402,2403,2406],{},[20,2404,2405],{},"監控 Node 層級的 CPU 使用率","，不只是 Pod 層級。Node 長期高於 70%～80%，代表 Burst 空間已經被吃掉了。",[323,2408,2409,2412,2413,2416],{},[20,2410,2411],{},"同一個服務的副本要分散到不同 Node。"," 用 ",[242,2414,2415],{},"topologySpreadConstraints"," 或 Pod Anti-Affinity，避免「同一波流量」讓擠在同一台 Node 上的副本同時 Burst。",[323,2418,2419,2422],{},[20,2420,2421],{},"核心服務考慮縮小 Request／Limit 差距","，甚至設成 Guaranteed QoS（Request = Limit），用成本換取可預期性；非核心、可容忍變慢的服務，才放大 Overcommit 的比例。",[11,2424,2426],{"id":2425},"上線後怎麼確認-hpa-有正常運作","上線後怎麼確認 HPA 有正常運作",[390,2428,2432],{"className":2429,"code":2430,"language":2431,"meta":5,"style":5},"language-bash shiki shiki-themes github-dark","# 看目前使用率 \u002F 目標值與副本數，TARGETS 若是 \u003Cunknown> 通常是沒設 Request 或 metrics-server 異常\nkubectl get hpa order-api\n\n# 看 HPA 的事件與每次擴縮的原因\nkubectl describe hpa order-api\n\n# 看每個 Pod 的實際 CPU \u002F 記憶體用量\nkubectl top pod -l app=order-api\n","bash",[242,2433,2434,2439,2452,2456,2461,2472,2476,2481],{"__ignoreMap":5},[398,2435,2436],{"class":400,"line":401},[398,2437,2438],{"class":564},"# 看目前使用率 \u002F 目標值與副本數，TARGETS 若是 \u003Cunknown> 通常是沒設 Request 或 metrics-server 異常\n",[398,2440,2441,2443,2446,2449],{"class":400,"line":34},[398,2442,152],{"class":411},[398,2444,2445],{"class":647}," get",[398,2447,2448],{"class":647}," hpa",[398,2450,2451],{"class":647}," order-api\n",[398,2453,2454],{"class":400,"line":424},[398,2455,451],{"emptyLinePlaceholder":64},[398,2457,2458],{"class":400,"line":442},[398,2459,2460],{"class":564},"# 看 HPA 的事件與每次擴縮的原因\n",[398,2462,2463,2465,2468,2470],{"class":400,"line":448},[398,2464,152],{"class":411},[398,2466,2467],{"class":647}," describe",[398,2469,2448],{"class":647},[398,2471,2451],{"class":647},[398,2473,2474],{"class":400,"line":454},[398,2475,451],{"emptyLinePlaceholder":64},[398,2477,2478],{"class":400,"line":479},[398,2479,2480],{"class":564},"# 看每個 Pod 的實際 CPU \u002F 記憶體用量\n",[398,2482,2483,2485,2488,2491,2494],{"class":400,"line":484},[398,2484,152],{"class":411},[398,2486,2487],{"class":647}," top",[398,2489,2490],{"class":647}," pod",[398,2492,2493],{"class":1022}," -l",[398,2495,2496],{"class":647}," app=order-api\n",[16,2498,2499,2500,2503,2504,2507,2508,2511],{},"如果有接 Prometheus，建議再觀察 ",[242,2501,2502],{},"container_cpu_cfs_throttled_periods_total"," 對 ",[242,2505,2506],{},"container_cpu_cfs_periods_total"," 的比例——",[20,2509,2510],{},"Throttling 比例持續偏高，代表 Limit 太緊，或 HPA 擴得太慢","，即使平均 CPU 使用率看起來很健康也一樣。",[11,2513,2514],{"id":2514},"總結",[320,2516,2517,2524,2530,2536,2544],{},[323,2518,2519,2520,2523],{},"HPA 的使用率是 ",[20,2521,2522],{},"實際用量 ÷ Request","，Limit 不參與計算；沒設 Request，HPA 就不會動。",[323,2525,2526,2529],{},[20,2527,2528],{},"Request 設成「常態用量 ÷ Target」再留一點餘裕","，常態流量才不會觸發無效擴展（本例：500m ÷ 75% ≈ 667m，取 800m）。",[323,2531,2532,2535],{},[20,2533,2534],{},"Limit 的任務是掩護擴展空窗期","，讓舊 Pod 在新 Pod 就緒前能 Burst 撐住峰值。",[323,2537,2538,2543],{},[20,2539,2540,2542],{},[242,2541,1670],{}," 才是防止無限擴展的開關","，而不是把 Request 設高或 Target 設高。",[323,2545,2546,2547,2549],{},"CPU 適合 HPA，記憶體建議 ",[242,2548,2369],{},"；Request 與 Limit 差距越大，越要盯緊 Node 層級的資源與副本分散。",[920,2551,2552],{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}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 .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":5,"searchDepth":34,"depth":34,"links":2554},[2555,2556,2561,2562,2567,2568,2569,2570,2571],{"id":1494,"depth":34,"text":1495},{"id":1554,"depth":34,"text":1555,"children":2557},[2558,2559,2560],{"id":1558,"depth":424,"text":1559},{"id":1582,"depth":424,"text":1583},{"id":1640,"depth":424,"text":1641},{"id":1651,"depth":34,"text":1652},{"id":1691,"depth":34,"text":1692,"children":2563},[2564,2565,2566],{"id":1722,"depth":424,"text":1722},{"id":2069,"depth":424,"text":2069},{"id":2169,"depth":424,"text":2170},{"id":2219,"depth":34,"text":2220},{"id":2338,"depth":34,"text":2339},{"id":2373,"depth":34,"text":2374},{"id":2425,"depth":34,"text":2426},{"id":2514,"depth":34,"text":2514},"2026-10-07","設定 HPA（Horizontal Pod Autoscaler）時，最常被問的一個問題是：「為什麼 averageUtilization: 75 的 75% 是相對 Request 算，而不是相對 Limit？Limit 才是 Pod 真正能用到的上限啊。」",{},"\u002Farticles\u002Fk8s-hpa",{"title":1475,"description":2573},"articles\u002Fk8s-hpa",[150,2579,2580,2581,2582],"HPA","Autoscaling","SRE","容量規劃","hTVrarh1tSnfhO1cXy4RE8RtCtkjVdBT_3XPxqDlYJM",{"id":2585,"title":2586,"body":2587,"date":2863,"description":5,"excerpt":102,"extension":97,"meta":2864,"navigation":64,"path":2865,"seo":2866,"stem":2867,"tags":2868,"__hash__":2873},"articles\u002Farticles\u002Fdevops-calms-framework.md","DevOps CALMS 實踐框架：不是先買工具，而是先看哪個字母在拖後腿",{"type":8,"value":2588,"toc":2854},[2589,2593,2596,2599,2602,2606,2612,2620,2623,2627,2633,2636,2656,2659,2663,2666,2686,2689,2693,2696,2699,2757,2760,2764,2767,2787,2790,2794,2797,2851],[11,2590,2592],{"id":2591},"calms-是拿來自我檢查的框架不是驗收清單","CALMS 是拿來自我檢查的框架，不是驗收清單",[16,2594,2595],{},"CALMS 這個字，是 Culture（文化）、Automation（自動化）、Lean（精實）、Measurement（量測）、Sharing（分享）五個字的縮寫，最早由 Jez Humble 在《The DevOps Handbook》系列的討論中提出，用來描述一個組織要走向 DevOps 需要同時成立的五個條件。",[16,2597,2598],{},"它常被誤用成一份「導入清單」——CI\u002FCD 裝好、Grafana 儀表板做好、就打勾說「我們 DevOps 了」。但這五個字母彼此互為前提：沒有文化上「出事不究責」的安全感，自動化只會把犯錯的速度變快；沒有量測，精實改善無從驗證；沒有分享，前面四項全部卡在少數人身上，離開就斷層。",[16,2600,2601],{},"真正有用的用法，是拿它當一份自我診斷表：團隊現在踩了哪個字母的坑，資源就該優先投在那裡，而不是每個字母都均勻地花錢買工具。",[11,2603,2605],{"id":2604},"culture先問出事之後第一個問題是誰還是為什麼","Culture：先問「出事之後，第一個問題是誰還是為什麼」",[16,2607,2608,2609],{},"文化是五個字裡最抽象、卻決定其他四個能不能落地的一項。實務上最快分辨團隊文化健不健康的問題是：",[20,2610,2611],{},"發生 Incident 之後，會議室裡第一句話是「這是誰改的」還是「發生了什麼、我們怎麼避免下次再發生」。",[320,2613,2614,2617],{},[323,2615,2616],{},"前者是究責文化（Blame Culture），工程師學到的教訓是「少改東西、少扛風險」，長期結果是沒人敢碰核心系統，變更集中在少數「敢做」的人身上",[323,2618,2619],{},"後者是無究責事後檢討（Blameless Postmortem）文化，把焦點放在系統性原因——為什麼這個錯誤沒有被測試攔下來、為什麼監控沒有更早示警、為什麼一個人的失誤可以直接影響到生產環境",[16,2621,2622],{},"該問的問題：團隊寫的 Postmortem 裡，行動項目（Action Item）是「加強某某人審查」還是「補上某個自動化檢查、修正某個流程缺口」？前者是文化沒到位的訊號。",[11,2624,2626],{"id":2625},"automation自動化的順序通常應該是部署-測試-環境","Automation：自動化的順序，通常應該是「部署 → 測試 → 環境」",[16,2628,2629,2630],{},"自動化常被簡化成「把 CI\u002FCD 建起來」，但 CI\u002FCD 只是自動化裡最顯眼的一塊。更完整的問法是：",[20,2631,2632],{},"團隊裡有哪些步驟，還在依賴某個人手動執行、手動記得要做、只有他知道怎麼做？",[16,2634,2635],{},"這些步驟不分好壞，都是自動化的候選名單，常見的排序原則是先做「頻率高、風險低、規則明確」的事：",[320,2637,2638,2644,2650],{},[323,2639,2640,2643],{},[20,2641,2642],{},"部署流程","：手動 SSH 上機、手動跑腳本，是最常見也最該優先自動化的部分——風險最高、又最常做",[323,2645,2646,2649],{},[20,2647,2648],{},"測試","：自動化測試套件（Unit \u002F Integration \u002F E2E）要能在 CI 裡穩定跑完，而不是「本地測過就上」",[323,2651,2652,2655],{},[20,2653,2654],{},"環境建置","：用 IaC（Terraform、CloudFormation）取代手動在主控台點設定，讓環境本身也能重現、能被版本控制",[16,2657,2658],{},"常見誤區：自動化的目標不是「消滅人的判斷」，而是消滅重複、機械化、容易手滑的步驟，把人的判斷力留給真正需要判斷的事情（例如：這次變更要不要上、風險評估）。",[11,2660,2662],{"id":2661},"lean把還在做的浪費找出來而不是先加流程","Lean：把「還在做的浪費」找出來，而不是先加流程",[16,2664,2665],{},"Lean（精實）源自製造業的精實生產，核心精神是持續辨識並移除「浪費」（Waste）。在軟體交付的情境下，常見的浪費包括：",[320,2667,2668,2674,2680],{},[323,2669,2670,2673],{},[20,2671,2672],{},"批次過大","：累積一個月的變更才一次上線，出問題時不知道是哪個改動造成的，回滾成本也高",[323,2675,2676,2679],{},[20,2677,2678],{},"等待","：一個 PR 卡審核卡三天、一個環境申請要走簽核流程一週",[323,2681,2682,2685],{},[20,2683,2684],{},"交接損耗","：需求從 PM 傳到工程師、再傳到維運，每次交接都遺失一部分上下文，導致重工",[16,2687,2688],{},"該問的問題：從一行程式碼被寫出來，到它真正在生產環境跑起來、產生價值，中間有多少步驟是在等待，而不是在做事？精實的第一步永遠是先把整條交付流程畫出來（Value Stream Mapping），再決定要優化哪一段，而不是先加一堆新流程、新工具。",[11,2690,2692],{"id":2691},"measurement先能回答這次改動有沒有變好才能談持續改善","Measurement：先能回答「這次改動有沒有變好」，才能談持續改善",[16,2694,2695],{},"沒有量測，前面三項都無法驗證是不是真的有效——文化變好了嗎？自動化真的省下時間了嗎？精實改善真的縮短前置時間了嗎？",[16,2697,2698],{},"DevOps 圈子最常用來量測交付績效的是 DORA（DevOps Research and Assessment）四大指標：",[213,2700,2701,2711],{},[216,2702,2703],{},[219,2704,2705,2708],{},[222,2706,2707],{},"指標",[222,2709,2710],{},"衡量什麼",[231,2712,2713,2724,2735,2746],{},[219,2714,2715,2721],{},[236,2716,2717,2720],{},[20,2718,2719],{},"部署頻率","（Deployment Frequency）",[236,2722,2723],{},"多久上一次線",[219,2725,2726,2732],{},[236,2727,2728,2731],{},[20,2729,2730],{},"變更前置時間","（Lead Time for Changes）",[236,2733,2734],{},"從 commit 到上線要多久",[219,2736,2737,2743],{},[236,2738,2739,2742],{},[20,2740,2741],{},"變更失敗率","（Change Failure Rate）",[236,2744,2745],{},"上線之後導致問題的比例",[219,2747,2748,2754],{},[236,2749,2750,2753],{},[20,2751,2752],{},"服務恢復時間","（Time to Restore Service）",[236,2755,2756],{},"出問題後多久能恢復",[16,2758,2759],{},"這四個指標刻意同時看穩定性與速度——只看部署頻率會鼓勵「衝快但常常壞」，只看變更失敗率又會鼓勵「怕出事乾脆少上線」，四個一起看才不會顧此失彼。實務上不需要一次到位地精算，先能穩定拿到這四個數字的粗略趨勢，就已經比完全沒量測進步很多。",[11,2761,2763],{"id":2762},"sharing知識卡在一個人身上就是團隊最大的單點故障","Sharing：知識卡在一個人身上，就是團隊最大的單點故障",[16,2765,2766],{},"分享指的是把知識、工具、經驗跨團隊、跨角色地流通，而不是留在少數人腦中或某個沒人維護的 Wiki 裡。常見的實踐方式：",[320,2768,2769,2775,2781],{},[323,2770,2771,2774],{},[20,2772,2773],{},"內部技術分享","：把踩過的坑、解決問題的過程整理成文件或分享會，而不是解決完就結束",[323,2776,2777,2780],{},[20,2778,2779],{},"共用工具與範本","：Runbook、部署腳本、監控儀表板範本統一維護，而不是每個團隊各自重造輪子",[323,2782,2783,2786],{},[20,2784,2785],{},"輪值與結對","：讓值班（On-Call）、部署這類「只有某人會」的工作，透過輪值或結對逐漸擴散給更多人",[16,2788,2789],{},"該問的問題：如果團隊裡「懂最多」的那個人請假一週，會不會有東西因此卡住？如果答案是會，代表分享這一項還沒做到位，而這通常也是知識最集中、風險最高的地方。",[11,2791,2793],{"id":2792},"用-calms-做自我檢查而不是導入清單","用 CALMS 做自我檢查，而不是導入清單",[16,2795,2796],{},"實務上判斷團隊卡在哪個字母，可以用下面這張表快速定位：",[213,2798,2799,2809],{},[216,2800,2801],{},[219,2802,2803,2806],{},[222,2804,2805],{},"症狀",[222,2807,2808],{},"通常對應的維度",[231,2810,2811,2819,2827,2835,2843],{},[219,2812,2813,2816],{},[236,2814,2815],{},"出事就開始找戰犯，沒人敢碰核心系統",[236,2817,2818],{},"Culture",[219,2820,2821,2824],{},[236,2822,2823],{},"部署還要靠一個人手動跑腳本",[236,2825,2826],{},"Automation",[219,2828,2829,2832],{},[236,2830,2831],{},"PR 卡審核好幾天、環境申請要走簽核",[236,2833,2834],{},"Lean",[219,2836,2837,2840],{},[236,2838,2839],{},"說不出這次改動到底有沒有變好",[236,2841,2842],{},"Measurement",[219,2844,2845,2848],{},[236,2846,2847],{},"某人離職／請假，某個流程就沒人會",[236,2849,2850],{},"Sharing",[16,2852,2853],{},"CALMS 的價值不在於背下這五個字，而在於每次要導入新工具或新流程之前，先問一句：這解決的是文化、自動化、精實、量測還是分享的問題？如果答案不清楚，很可能只是在給一個沒被真正診斷出來的問題，貼上一塊新的 OK 繃。",{"title":5,"searchDepth":34,"depth":34,"links":2855},[2856,2857,2858,2859,2860,2861,2862],{"id":2591,"depth":34,"text":2592},{"id":2604,"depth":34,"text":2605},{"id":2625,"depth":34,"text":2626},{"id":2661,"depth":34,"text":2662},{"id":2691,"depth":34,"text":2692},{"id":2762,"depth":34,"text":2763},{"id":2792,"depth":34,"text":2793},"2026-09-21",{},"\u002Farticles\u002Fdevops-calms-framework",{"title":2586,"description":5},"articles\u002Fdevops-calms-framework",[2869,2870,2871,2872],"DevOps","CALMS","團隊文化","工程實踐","m0dIxAvcZ9TG7HoLq1hXbmXZgGWlpu8TgAjRg5C9Ugo",1792959149538]