"Api-Design"
- 持久連線推送:WebSocket、SSE、long-polling 的承諾差異
server 推 client 的持久連線機制對消費者承諾什麼:重連誰負責、訊息會不會漏、單向還是雙向;選型看消費者形狀
- 11.1 API 作為服務邊界的責任
介面變更該由誰付成本、哪些介面性質算對外承諾、承諾違約有哪些模式 — 動手設計 endpoint 前的責任框架
- 11.C1 Fielding 論文第 5 章:REST 是約束推導的架構風格
REST 由六個約束從 null style 推導而來、uniform interface 以效率換一般性;所有 REST 論爭的定義基準
- GraphQL Schema 演進:versionless 的紀律代價
只加不改、deprecation 標注、nullable 預設怎麼共同取代版本號 — 以及每個紀律各自的隱藏帳單
- gRPC proto 演進紀律:編碼層相容與 CI gate
要改 proto 又得保證 wire 相容、並想把相容檢查落成 merge 前 CI gate、選檢查等級時的判準
- Hypermedia 與 HATEOAS 的適用邊界
hypermedia 落在哪個消費者形狀:uniform client 前提、格式標準化為何失敗、反方的收益假設拆解、適用與不適用的場景線
- tRPC 型別共享:型別即契約的前提與代價
tRPC 靠 TypeScript 型別推導同步契約、零 codegen;適用同倉 TS-only、公開第三方 API 不適用
- 採現成格式標準還是自建規範
採現成 response 格式標準買到什麼、綁定什麼、以及怎麼在採用前預測一個標準會不會活下去
- SDK 公開 API 設計
init / event / error / metric / flush / close 六個方法構成 SDK 的完整生命週期 — 跨平台共用相同 API 介面
- webhook 對外承諾:投遞保證不是預設、consumer 負責一半
webhook 是盡力而為的事件推送不是可靠佇列:投遞保證逐 vendor 讀、可靠性責任分一半給 consumer
- 11.2 風格選型總覽
REST 式 HTTP+JSON、GraphQL、gRPC、tRPC、JSON-RPC、event 之間選哪個 — 用消費者形狀、演進成本、操作可及性三軸判讀
- 11.C2 Fielding:REST API 必須是 hypertext-driven
REST 定義擁有者公開否定業界主流用法的引爆點文獻、六條規則劃出 hypertext-driven 的判別線
- GraphQL 執行成本與攻擊面
resolver 執行模型讓請求成本不再是常數 — N+1 的基礎設施化、成本計點限流、introspection 偵察、persisted queries 的收斂路線
- gRPC streaming 與部署邊界:trailers、proxy 與 debug 可及性
選 gRPC 前要先確認的部署約束:瀏覽器過不了 trailers 需翻譯 proxy、on-call 徒手 debug 的可及性代價
- JSON-RPC 的適用條件:最小夠用的訊息層
本地雙向低頻、需 notification 語意、生態要求零 codegen 可自省 —— 這組條件下 JSON-RPC 比重型 RPC 更貼
- Richardson 成熟度的實用讀法
RMM 四級當定位與溝通工具的用法、每一級的工程意義、以及把它當合規認證或升級路線圖的誤用邊界
- 描述格式的選型:OpenAPI 與 AsyncAPI
描述 API 形狀的格式標準怎麼選:看既有採用動能、看涵蓋的介面種類、以及 REST 加 event 混合時的治理配置
- 11.3 資源建模與操作語意
endpoint 該建模成資源還是動作、HTTP method 與 status 承諾了什麼、available actions 由誰計算 — 建模決策的判準
- 11.C3 Richardson 成熟度模型:分級階梯與它的自我聲明
RMM 四級是理解 REST 元素的思考工具、一手來源自己警告它不是 REST 分級定義;業界停在 Level 2 的參照系
- gRPC 內部 RPC 的選型位置:框架層集中的組織前提
gRPC 值得選的判準是要不要一個框架層集中點、不是序列化效能;用規模兩端判讀何時集中價值蓋過 debug 代價
- 公開 API 的 GraphQL 進退
GitHub 雙軌、Shopify all-in、與撤退案例 — 同一技術不同結局的情境變數、GraphQL 的適用邊界
- 11.4 錯誤模型設計
錯誤該分幾類、格式怎麼定才有演化空間、機器判讀跟人類訊息怎麼分工 — 錯誤作為契約一級公民的設計判準
- 11.C4 Carson Gross:REST 如何變成 REST 的反義詞
hypermedia 復興派的語意漂移史重建:JSON 取代 XML、業界停在 Level 2、SPA 脫鉤到 GraphQL 放棄名義
- 11.5 版本策略與 deprecation
版本方案怎麼選(URI/header/date-based)、支援窗口怎麼承諾、舊版怎麼安全退場 — 承諾分期與回收的操作設計
- 11.C5 htmx HATEOAS essay:透支帳戶的兩種表徵對照
同一個 domain 狀態的 HTML 與 JSON 表徵耦合差異、HATEOAS 有無的操作型判別法
- 11.6 向後相容的變更紀律
哪些變更算 breaking、相容性檢查放人工還是 CI、檢查粒度怎麼選 — 讓介面變更可審可擋的日常紀律
- 11.C6 HAL spec:JSON hypermedia 標準化的過期 draft
在 JSON 上補 hypermedia 最接近成功的一次:有 spec 有生態、標準化止步於過期 IETF draft
- 11.7 集合介面設計
分頁方案的承諾差異、批次操作的部分失敗語意、超過請求逾時的長時操作怎麼回 — 集合與長時操作的介面判準
- 11.C7 Siren spec:表達力更完整、採用曲線停滯
帶 first-class actions 的 hypermedia 格式、表達力勝 HAL 而採用更少;client 生態決定格式命運的證據
- 11.8 API 層冪等設計
idempotency key 誰生成、存多久、replay 回什麼、衝突怎麼回 — 對外冪等契約的條款設計與無標準現況
- 11.C8 Ben Morris:不做 hypermedia 的 pragmatic REST(反例對照)
反例對照:逐條拆 HATEOAS 的收益假設在 machine-to-machine 場景不成立的 pragmatic 派立場文
- 11.9 對外流量語意
rate limit 對消費者承諾什麼、429 與 Retry-After 怎麼設計、配額 header 該不該信 — 限流作為契約的語意設計
- 11.C9 twobithistory:被挪用的 REST 論文(史觀對照)
第三方歷史考據:論文談的是 HTTP/1.1 設計而非 API 建構、業界棄 SOAP 時把 pragmatic 用法掛上 REST 名字;二手來源
- 11.10 API 規範治理
設計規範怎麼讓幾十個團隊持續遵守 — 提案制、Guild 制、分軌制的治理模式比較、linting 進 CI、規範失敗的成因
- 11.C10 Stripe:日期滾動版本與 version change module
把相容性從路由層搬進轉換層、breaking change 成本由服務端一次吸收;date-based versioning 的原型案例
- 11.11 Status 與錯誤的雙向契約
status 與錯誤是兩端的合作契約:provider 該讓 consumer 知道什麼、consumer 收到錯誤怎麼判讀與回報、以及單邊設計怎麼把成本外部化給對方
- 11.C11 Stripe 現行方案:具名 major release 與相容變更清單
同一家公司版本策略隨規模演進的第二個時間切片、附「什麼算 backwards-compatible」的明文清單
- 11.12 API 消費者用量觀測
版本退場、格式遷移、分頁換機制、冪等收緊要落地,前提是查得出誰在用什麼
- 11.C12 GitHub:REST API calendar versioning 與 24 個月支援承諾
date-based versioning 成為大平台收斂方向的證據點、最低支援窗口把 deprecation 成本變成明文契約
- 11.13 既有 API 的改造路徑
契約已經上線、消費者改不動、觀測還沒建時,選型判準重跑得出不同答案該怎麼落地——收緊已暴露性質的手段與動工順序
- 11.C13 GitHub:密碼認證廢止的 brownout 執行
deprecation 執行機制案例:公告觸及不到的長尾 client、用排程 brownout 的短暫真實故障叫醒
- 11.14 契約條款的送達
條款寫進文件只是宣告——讓消費者不讀也不會踩的送達手段各有射程與強制力,以及哪些條款只能靠人讀
- 11.C14 Fielding:對 API 版本化的建議是「別做」
no-versioning 流派的理論錨點:hypermedia 演化取代版本號、與運營現實路線分歧的根源
- 11.C15 RFC 8594 Sunset header:退場宣告的機器可讀層
用 HTTP header 宣告資源退場時點的標準化嘗試、Informational 地位與實務採用有限
- 11.C16 Slack:四族 API 分階段收斂到 Conversations API
分階段 deprecation 執行:先掐斷新增量、再處理存量、最後硬停、過渡期用 in-band warning
- 11.C17 Facebook Graph API v1.0 退場:靜默語意切換(反例)
反例:到期後把 v1.0 請求靜默當 v2.0 處理而非回明確錯誤、長尾 app 默默壞掉
- 11.C18 GitHub:採用 GraphQL 的可量化動機
REST 佔資料庫層 60% 請求、over/under-fetching 並存的重構動機;什麼規模的痛才值得換風格的錨點
- 11.C19 GitHub:GraphQL point system 成本計點限流
GraphQL 打破 per-request 限流假設、平台被迫發明查詢成本模型、加 node 上限雙層防線
- 11.C20 GitHub:REST 與 GraphQL 雙軌並行的十年穩態
2016 採用者的長期終點是共存而非取代、功能覆蓋不對等被官方明文承認
- Status 裝不下的東西:部分成功、延遲失敗、gateway 歧義
單一 status 表達不了部分成功、延遲失敗與 gateway 歧義時怎麼辦:把狀態下放 body 讓中介層變盲、或收窄語意保持 status 恆為真
- 11.C21 Shopify:宣告 GraphQL 為唯一 API、REST 轉 legacy
跟共存路線相反的策略極端:用新功能只上 GraphQL 製造遷移壓力、配套降成本加倍配額
- 接收方的重試決策:從單一請求到 retry 風暴
收到錯誤之後重不重試:從單請求的 status 加冪等合判、集體的去同步責任、到 retry 預算與斷路閘門
- 11.C22 Matt Bessey:六年 GraphQL 老手的撤退清單(反例)
反例:授權下推到 field、成本不可預測、解析層攻擊面的執行期代價清單、附撤退判準
- 錯誤傳播與信任邊界:中間服務的雙重身分
錯誤跨服務傳遞時誰該轉譯、收到的錯誤能信多少、對外暴露多少細節 — 服務鏈上每一跳同時是 consumer 與 provider 的責任判準
- 11.C23 Echobind:從 GraphQL 撤到 tRPC 的量化帳(反例)
反例:五層重複宣告與三層 codegen 拖垮 DX 的量化紀錄、同時自列 tRPC 的適用前提
- 錯誤回報的回饋迴路:request-id、trace 與呈現回報分工
consumer 收到錯誤之後怎麼跟 provider 溝通:error 要帶什麼定位鉤子、同一個錯誤怎麼分別投影給使用者與回報、持續錯誤什麼時候該升級
- 11.C24 DataLoader 譜系:N+1 的官方解法變成基礎設施
resolver-per-field 讓 N+1 從偶發變預設、官方生態把 batching 做成基礎設施而非優化技巧
- 11.C25 HackerOne:introspection 列舉出未授權的 CreateAdminUser
introspection 作為攻擊面偵察工具的實證:schema 自我揭露讓隱藏 mutation 免 fuzzing 直接可見
- 11.C26 GraphQL 官方:versionless API 與 nullable-by-default
no-versioning 的成本轉嫁鏈:只加不改、deprecation、nullable 預設三個紀律換掉版本號
- 11.C27 WunderGraph:GraphQL 不該直接暴露在公網
介於全開與撤退之間的第三條路:GraphQL 當 server-side 查詢語言、對外只開 persisted operations;vendor 立場需標明
- 11.C28 protobuf 官方規範:field number 紀律
編號不可改、刪除必 reserve、重用導致資料損毀;契約相容性是編碼格式的性質、不是 review 慣例
- 11.C29 Buf breaking detection:四級規則對應消費者依賴
把 proto 相容紀律從人的自律升級成 CI gate、檢查粒度是產品決策不是工具預設
- 11.C30 Buf Connect 發布文:對 grpc-go 的系統性批評
gRPC 部署邊界(trailers、瀏覽器、proxy)最完整的一手批評;發布方是競品、立場需標明
- 錯誤格式之爭:status 的真實性決定誰讀得到錯誤、容器決定誰讀得到細節
選錯誤格式時各派的分歧與代價:錯誤內容跟 transport status 的關係、它讓哪些角色讀得到錯誤、以及演化條款與命名空間由誰提供
- 11.C31 Dropbox Courier:百萬 RPS 規模的 gRPC 遷移
gRPC 當框架層集中可靠性的載體、遷移成本與 TLS 握手踩雷的規模判讀訊號
- 版本策略流派之爭:識別碼是入口、預設行為才是成本的分水嶺
選版本方案時各派的前提與代價:版本識別什麼、變更成本由誰吸收、借用某派結論而不帶其前提會壞在哪
- 11.C32 gRPC: The Bad Parts:cURL 測試不過的 API(反例)
反例:獨立實踐者的 gRPC 批評清單、debug 可及性判準、含生態已修補的平衡敘述
- 分頁之爭:offset 與 keyset 選機制、cursor 決定表示權
選分頁方案時把定位機制跟對外表示分開判斷:cursor 的不透明性給服務端什麼自由、未宣告的 cursor 性質會被誰依賴
- 11.C33 tRPC 設計哲學:無 schema 無 codegen 的型別共享
把 API 契約從 IDL 檔搬進型別系統的極端點、官方自述的前提與代價(TS-only、同倉共置)
- Idempotency key 標準化之爭:標準統一得了揭露的形狀、統一不了業務綁定的值
整合或自建冪等機制時各家條款的實質差異:replay 回首次快照還是最新狀態、保存期是否明文、同 key 並發怎麼處理
- 11.C34 JSON-RPC 重生:LSP 與 MCP 都選它當訊息層
死在 web API、活在編輯器與 agent 協議:最小夠用訊息層的選型條件組合
- 11.C35 RFC 9457:problem+json 標準化錯誤格式
type 用 URI 外部化錯誤命名空間、client 必須忽略未知欄位的演化條款、IANA registry 補 7807 碎片化
- 11.C36 Stripe 錯誤物件:type / code / param 三層分離
路由層、分支層、UI 層做成正交欄位;冪等衝突列 first-class 錯誤型別;標準前自成一格的對照組
- 11.C37 Slack:offset 到 opaque cursor 的分頁遷移
深頁掃描與 page window 漂移兩個失效模式、opaque cursor 把分頁狀態表示權留在 server 端
- 11.C38 Stripe 冪等設計哲學:retry 是 client-server 協作
三種失敗點的 replay 行為分析、client 端 backoff + jitter 責任;冪等只做 server 半邊會放大故障
- 11.C39 Stripe 冪等鍵契約條款:24h 保存、500 也重放
可承諾的冪等契約細節:重放的是該次請求的結局而非成功結果、同 key 不同參數即錯
- 11.C40 IETF Idempotency-Key draft:標準化停在 expired
de facto 先於 de jure 的具體例:業界事實標準先行、IETF 標準化跟進後停滯;引用必標 expired 狀態
- 11.C41 PayPal-Request-Id:同語意、不同契約的冪等實作
跟 Stripe 的三點對照:header 命名、replay 語意(最新狀態 vs 首次結局)、契約精確度
- 11.C42 IETF RateLimit headers:政策與狀態拆兩個 header
限流 header 的承諾邊界:informational only、不是 SLA;active draft、引用標版本
- 11.C43 GitHub 雙層限流:primary / secondary 與 x-ratelimit 契約
單一維度配額擋不住真實濫用、前標準時代 x- header 與 IETF 命名並存的遷移期現實
- 11.C44 Google AIP-151:長時操作實體化成 Operation resource
202 + 輪詢模式的系統化版本:回應型別先宣告、client 統一寫一套 polling、operation 生命週期明訂
- 11.C45 Twilio 2013 計費事故:無冪等防線的重複扣款(反例)
反例:內部 retry 迴圈缺冪等閘門等效於無限重放、fail-safe 是金流 side effect 的斷路器
- 11.C46 Google AIP:規範即提案系統的治理模式
編號提案制、狀態機、簽核門檻把 IETF RFC 流程內化到單一組織;中心化治理、社群貢獻是輸入
- 11.C47 Zalando API-first:Guidelines、Guild、Zally、Portal 四件套
規範存活靠配套組織機制:文件、人的治理、自動化、可發現性缺一環就退化成書架文件
- 11.C48 Microsoft REST Guidelines:單一 repo 內的分軌治理
規範沿組織邊界分化的實況:Core / Azure / Graph 三軌並存、跟「像同一團隊設計」理想的張力
- 11.C49 Spectral 與 Zally:guidelines 變成可執行檢查
治理成本從人工 review 前移到 CI;通用 linter 加組織 ruleset 比單一組織專用 linter 更能存活
- Hyrum's Law
使用者夠多時、介面的一切可觀察行為都會被依賴 — 不管你承諾了什麼;契約設計要主動給機器可讀欄位、否則人類可讀欄位會被迫變成契約
- 11.C50 JSON:API:以停止 bikeshedding 為賣點的格式標準
價值主張放在組織成本而非技術能力:採現成標準 vs 自建規範加治理是規範治理的核心選擇
- 11.C51 OData:ISO 認證救不了生態萎縮(反例)
反例:正式標準化程度最高、主流化程度不成比例;marquee adopter 離場比標準機構背書更能預測標準命運
- 11.C52 OpenAPI Initiative:從 Swagger 捐贈到開放治理
單一 vendor spec 轉軌中立基金會的成功樣本:治理轉移是把既有動能中立化、不是用背書創造動能
- 11.C53 AsyncAPI:刻意相容 OpenAPI 的補位策略
站在既有標準肩上補 event-driven 缺口:以相容性換採用曲線、描述格式的邊界即治理邊界
- 11.C54 White House API Standards:規範制定後棄置(反例)
反例:文件品質不差、但沒有 Guild / linter / review 等執行機制與持續 ownership、34 commits 後封存
- 11.C55 WHATWG SSE spec:內建自動重連與 Last-Event-ID 補送
SSE 把重連與斷點續傳的協商鉤子寫進協議:自動重連、Last-Event-ID 補送、retry 欄位;補送實際保證仍看 server replay
- 11.C56 RFC 6455:WebSocket 是雙向 transport、不內建投遞保證
WebSocket 給雙向管線、不給保證:協議層對投遞保證、ack、重連、斷點續傳全部沉默、留給應用層
- 11.C57 Slack Socket Mode:WebSocket 上自建 ack、retry 與多連線熱備
協議不給保證、vendor 在應用層自建整套可靠性:envelope ack、未 ack 就 retry、多連線熱備、斷線預警
- 11.C58 RFC 6202:long-polling 的機制代價與 fallback 定位
long-polling hold 住請求到有事件才回:header 開銷、三段網路延遲、每 client 佔一條連線是機制決定的、不是實作品質
- 11.C59 Socket.IO:先 long-polling 再 upgrade WebSocket 的 transport negotiation
WebSocket 不保證能建立(proxy/防火牆會擋)、所以先用 long-polling 建連再 upgrade:fallback 的價值是相容性下限、不是效能
- 11.C60 Stripe webhooks:at-least-once 加簽章、明文要求 consumer 冪等與不依賴順序
webhook 對外承諾的教科書樣本:三天重試、重複投遞、no-ordering、簽章驗證的責任在同一頁明文轉移給 consumer
- 11.C61 GitHub webhooks:不自動重試的反向承諾
at-least-once 不是所有 vendor 都給:GitHub 明文只試一次、失敗靠 consumer 自建排程補投;逼你讀 vendor 明文而非假設
- 11.C62 Slack Events API:3 秒 ack 上限加固定三次重試
「慢等於失敗」寫死成 3 秒硬上限、逼 consumer 走立即 2xx 加背景處理;retry header 讓 consumer 辨識重投
- 11.C63 Shopify webhooks:ordering 不保證、指定 header 去重、投遞不保證
跨 vendor 佐證 ordering-not-guaranteed 加冪等 header 是通則;甚至連投遞本身都不保證、需 reconciliation 兜底
- 11.C64 RFC 4918 207 Multi-Status:status line 降格為「請讀 body」
一個 status code 裝不下批次結果:WebDAV 把每資源狀態下放到 body、解析責任隨之轉給 consumer
- 11.C65 Google AIP 部分成功立場:同步必原子、非同步才准部分成功且要顯式 opt-in
AIP-193 明文「不該支援 partial errors」、批次三部曲給出原子性階梯與 LRO 出口:部分成功要 client 顯式同意
- 11.C66 RFC 9110 202 Accepted:接受不等於承諾、HTTP 沒有回傳非同步結果的機制
202 是規範明文的 intentionally noncommittal:一旦回了 202、協定層不再有管道通知最終失敗、通知責任移轉到應用層
- 11.C67 RFC 9110 502/504:gateway 只回報自己的觀察、不回報上游的執行狀態
502/504 定義只說 gateway 沒收到有效或及時回應、沒有欄位區分「請求沒送到」與「執行了但回應丟了」— retry 安全性相反的兩種情況拿到同一個 code
- 11.C68 Exponential Backoff And Jitter:無 jitter 的退避是明確輸家
N 個 client 同時競爭時總工作量隨 N² 成長;三種 jitter 公式實測、Full Jitter 總工作量最少 — consumer 之間的隱性同步要主動打散
- 11.C69 Google SRE Book:retry 放大與跨層疊乘、per-request 上限與 retry budget
retry 放大讓有效工作遞減;三層各 retry 3 次在底層變 64 次;建議 per-request 上限 + server-wide retry budget、provider 要用不同 code 分開可重試與不可重試
- 11.C70 AWS DynamoDB 2015 事故:內部元件的 retry 自保把錯誤率推到 55%(反例)
反例:metadata 服務過載後 storage server 逾時自我下線再重試、風暴成形後系統不自癒、要人工暫停請求才能喘息;事後修正同時動兩端
- 11.C71 Slack 2021-01-04 事故:復原期 retry 加 circuit breaking 是藥方
retry 的反向平衡:網路恢復後正是 retry 與 circuit breaking 讓系統爬回服務狀態 — retry 是否有害取決於 provider 處於過載中還是恢復中
- 11.C72 AWS retry 指南:retry storm 的官方定義與分層限制
Well-Architected 逐字定義 retry storm、建議低層服務 retry 上限 0-1 次、把 retry 委派給上層;Builders' Library 的 token bucket 路線
- 11.C73 gRPC 兩層錯誤模型:status code 是保證層、richer detail 是選配層
標準模型全語言保證、richer error model 走 trailing metadata — proxy 與 logger 看不到、中間節點對錯誤細節是盲的
- 11.C74 gRPC status code 產生者歧義:收到的 code 不一定來自 server 應用層
17 個 code 裡只有 7 個保證來自 user code;UNAVAILABLE / INTERNAL 可能是 library 或 channel 自產 — consumer 對錯誤來源的信任判讀有一手依據
- 11.C75 AIP-193 錯誤內容規範:三層受眾與「不假設使用者懂內部實作」
機器可讀的 (reason, domain) 契約、developer-facing message、LocalizedMessage 三層分工;message 穩定性規則反向揭露 Hyrum's Law
- 11.C76 W3C Trace Context:traceparent 的傳播義務與 security boundary 重開機制
跨 vendor trace 關聯的標準鉤子:每一跳 MUST 傳播、security boundary 可 restart trace、無效 id MUST ignore — 信任邊界寫進規範
- 11.C77 OWASP error handling:錯誤訊息是攻擊者的偵察面
非預期錯誤回 generic response、細節只留 server side log — provider 少暴露的安全端論證、跟 AIP-193 的機器可讀路線形成張力
- 取個原始值有四種寫法 — VO 的 toString 洩漏與 accessor 不一致
value object 家族的取值 accessor 各自為政(displayValue、toString、裸欄位)時,殺傷力會在測試層爆開:自定義 toString 讓裸字串斷言全數過期、四種取法讓修復者自己都寫錯。止血是 helper 函數庫集中取值知識、根治是家族統一 accessor 命名。
- 函式文件分層設計:型別、介面、實作各自該寫什麼
函式文件分層設計:把資訊放在能表達它的最低層次(名稱 / 型別簽章 / 介面 doc / 實作 doc / 範例與測試)、上層留給「下層表達不了的剩餘」。整理各層該寫什麼、容易誤入的內容、配合反模式列表跟寫 doc 前 checklist 收斂出短而精準的文件。
- 型別取代 doc 的收益曲線:強型別語言的 doc 該有多短
型別系統強化等於 doc 表達力轉移——很多 doc 內容應該下移到型別。整理 null safety / enum / wrapper / Result / typestate 各能消除哪類 doc、型別表達不了的剩餘部分(業務動機、性能、副作用、時序)以及收益曲線的邊際遞減點。