[{"data":1,"prerenderedAt":825},["ShallowReactive",2],{"search-api":3},[4,11,34,44,53,59,73,85,93,103,111,123,136,148,156,167,180,193,205,217,229,240,252,263,275,287,296,307,348,358,369,379,391,400,410,420,431,446,461,478,491,499,508,519,534,546,555,565,579,586,597,609,622,629,641,663,690,700,710,718,726,733,746,754,761,768,775,783,796,810],{"id":5,"path":6,"dir":7,"title":8,"description":7,"keywords":9,"body":10},"content:0.index.md","/","","Home",[],"    以下是我們過去探討領域歷程（粗體為書籍或長期主題研討、非粗體為一次性分享會）：    軟體設計與程式 :   Design Pattern , Clean Code,   Refactor , Dependency Injection  後端開發 :   Web API設計與開發 ,   資料密集型應用系統設計 , Akka分散式系統, Redis 快取  程式架構 :   DDD領域驅動開發 ,   Clean Architecture  資料庫管理 :   SQL基礎與進階使用  工具 :   ReactiveX  版本控制 : Git,Git進階用法,Github使用技巧  DevOps與自動化 : 自動環境部屬CICD,持續交付2.0  其他技術與軟技能分享 : Agile Scrum分享,ESP8266應用,OKR設定個人年度目標,加拿大打工分享,Notion介紹",{"id":12,"path":13,"dir":14,"title":15,"description":16,"keywords":17,"body":33},"content:1.api:1.components.md","/api/components","api","Components","Discover every component you can use in your content.",[18,19,20,21,22,23,24,25,26,27,28,29,30,31,32],"\u003CAlert />","\u003CBadge />","\u003CBlockHero />","\u003CButtonLink />","\u003CCallout />","\u003CCard />","\u003CCardGrid />","\u003CCodeGroup />","\u003CCodeBlock />","\u003CCopyButton />","\u003CIcon />","\u003CList />","\u003CSandbox />","\u003CTerminal />","\u003CVideoPlayer />","  Components  Discover every component you can use in your content.   \u003CAlert />     Check out an   info  alert with   code  and a   link .   Check out a   success  alert with   code  and a   link .   Check out a   warning  alert with   code  and a   link .   Check out a   danger  alert with   code  and a   link .     ::alert{type=\"info\"}\n   Check out an   **info**   alert with   `code`   and a [  link  ](  /  ).\n   ::\n   \n   ::alert{type=\"success\"}\n   Check out a   **success**   alert with   `code`   and a [  link  ](  /  ).\n   ::\n   \n   ::alert{type=\"warning\"}\n   Check out a   **warning**   alert with   `code`   and a [  link  ](  /  ).\n   ::\n   \n   ::alert{type=\"danger\"}\n   Check out a   **danger**   alert with   `code`   and a [  link  ](  /  ).\n   ::\n     \u003CBadge />   \u003CBadge />  support same types as   \u003CAlert /> .     v1.2  Deprecated   Not found!     :badge[  v1.2  ]\n   \n   :badge[  Deprecated  ]{type=\"warning\"}\n   \n   ::badge{type=\"danger\"}\n   Not found!\n   ::\n     \u003CBlockHero />      Document-driven framework   Docus reconciles content creators and developers by offering to both the best tools to create and scale content-based websites.     ::block-hero\n   ---\n   cta:\n     -   Get started\n     -   /get-started\n   secondary:\n     -   Open on GitHub →\n     -   https://github.com/nuxtlabs/docus\n   snippet: npx nuxi@latest init docus-app -t nuxtlabs/docus-starter\n   ---\n   #title\n   Document-driven framework\n   \n   #description\n   Docus reconciles content creators and developers by offering to both the best tools to create and scale content-based websites.\n   ::\n     \u003CButtonLink />    Play on StackBlitz     :button-link[Play on StackBlitz]{icon=\"IconStackBlitz\" href=\"https://stackblitz.com/github/nuxtlabs/docus-starter\" blank}\n     \u003CCallout />   \u003CCallout />  support same types as   \u003CAlert /> .      This is a callout! Click me to open.   This is the content of the callout.    This is a callout! Click me to open.   This is the content of the callout.     ::callout\n   #summary\n   This is a callout! Click me to open.\n   \n   #content\n   This is the content of the callout.\n   ::\n    \n   ::callout{type=\"warning\"}\n   #summary\n   This is a callout! Click me to open.\n   \n   #content\n   This is the content of the callout.\n   ::\n     \u003CCard />      Nuxt Architecture.   Based on   Nuxt 3  and   Nuxt Content .   \nUse Nuxt to build a static site, or a serverless app.      ::card{icon=\"logos:nuxt-icon\"}\n    #title\n    Nuxt Architecture.\n    #description\n    Based on   **Nuxt 3**   and   **Nuxt Content**  . :br\n    Use Nuxt to build a static site, or a serverless app.\n    ::\n     \u003CCardGrid />      What's included?      Nuxt Architecture.   Harness the full power of Nuxt and the Nuxt ecosystem.    Vue Components.   Use built-in components (or your own!) inside your content.    Write Markdown.   Enjoy the ease and simplicity of Markdown and discover MDC syntax.     ::card-grid\n   #title\n   What's included\n   \n   #root\n   :ellipsis\n   \n   #default\n     ::card\n     #title\n     Nuxt Architecture.\n     #description\n     Harness the full power of Nuxt and the Nuxt ecosystem.\n     ::\n     ::card\n     #title\n     Vue Components.\n     #description\n     Use built-in components (or your own!) inside your content.\n     ::\n     ::card\n     #title\n     Write Markdown.\n     #description\n     Enjoy the ease and simplicity of Markdown and discover MDC syntax.\n     ::\n   ::\n     \u003CCodeGroup />  This component uses   slots  to create a tab panel of your code examples or preview.        yarn   add   docus\n     npm   install   docus\n     ::code-group\n     ```bash [Yarn]\n     yarn   add   docus\n     ```\n     ```bash [NPM]\n     npm   install   docus\n     ```\n   ::\n     \u003CCodeBlock />  To be used inside a   \u003CCodeGroup />  component to display a preview of some rendered code.     Hello World!     /* Added as a child of   `\u003CCodeGroup />`   */\n   \n   ::code-block{label=\"Preview\" preview}\n     ::badge\n     Hello World!\n     ::\n   ::\n     \u003CCopyButton />        :copy-button{content=\"hey!\"}\n     \u003CIcon />  Icon component gives you access to all   100,000+  icons from   icones.js.org .          :icon{name=\"logos:nuxt-icon\"}\n   :icon{name=\"logos:vue\"}\n   :icon{name=\"logos:nuxt-icon\"}\n     \u003CList />       Important  Always    Amazing  Congrats    Do you know?  You can also do this    Be careful  Use with precautions    Drinking too much  Driving drunk     ::list{type=\"primary\"}\n   -   **Important**\n   -   Always\n   ::\n   \n   ::list{type=\"success\"}\n   -   Amazing\n   -   Congrats\n   ::\n   \n   ::list{type=\"info\"}\n   -   Do you know?\n   -   You can also do this\n   ::\n   \n   ::list{type=\"warning\"}\n   -   Be careful\n   -   Use with precautions\n   ::\n   \n   ::list{type=\"danger\"}\n   -   Drinking too much\n   -   Driving drunk\n   ::\n    \u003CSandbox />  Embed CodeSandbox/StackBlitz easily in your documentation with great performances.  Using the   IntersectionObserver  to load when visible in the viewport.        :sandbox{src=\"https://codesandbox.io/embed/nuxt-content-l164h?hidenavigation=1&theme=dark\"}\n     \u003CTerminal />        :terminal{content=\"nuxi build\"}\n     \u003CVideoPlayer />         ::div\n     :video-player{src=\"https://www.youtube.com/watch?v=o9e12WbKrd8\"}\n   ::\n   html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"id":35,"path":36,"dir":14,"title":37,"description":38,"keywords":39,"body":43},"content:1.api:2.composables.md","/api/composables","Composables","Discover the Docus composables to use in your custom Vue components and pages.",[40,41,42],"useDocus()","useMenu()","useScrollspy()","  Composables  Discover the Docus composables to use in your custom Vue components and pages.   useDocus()    useDocus  ()  gives access to docus runtime config, all default values and your custom config from   app.config.ts    config  refers to the merged config of the current page.   main ,   header ,   aside ,   footer  and   titleTemplate  can be set from   _dir.yml  and any   page.md  file.  The configs in   app.config  file will be used as defaults.     \u003C  script   setup  >\n   const   {   config   }   =   useDocus  ()\n   \u003C/  script  >\n   \n   \u003C  template  >\n     \u003C  div  >\n       \u003C  h1  >{{ config.title }}\u003C/  h1  >\n       \u003C  p  >{{ config.description }}\u003C/  p  >\n     \u003C/  div  >\n   \u003C/  template  >\n    tree  refers to the current navigation tree that is displayed in the   aside  component.     \u003C  script   setup  >\n   const   {   tree   }   =   useDocus  ()\n   \u003C/  script  >\n   \n   \u003C  template  >\n     \u003C  DocsAsideTree   :links  =  \"tree\"   />\n   \u003C/  template  >\n    useMenu()   useMenu()  gives access to   $menu  plugin controlling mobile navigation globally.     const   {\n     // Is menu visible\n     visible  ,\n     // Close menu function\n     close  ,\n     // Open menu function\n     open  ,\n     // Toggle menu function\n     toggle\n   }   =   useMenu  ()\n    useScrollspy()   useScrollspy()  is used in   docs  layout to make the ToC display the currently visible headings.     const   {\n     // Headings on the page\n     visibleHeadings  ,\n     // Active headings (for the current page)\n     activeHeadings  ,\n     // Update headings (an array of DOM nodes)\n     updateHeadings\n   }   =   useScrollspy  ()\n   html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"id":45,"path":46,"dir":14,"title":47,"description":48,"keywords":49,"body":52},"content:1.api:3.layouts.md","/api/layouts","Layouts","Docus provides multiple built-in layouts for displaying your Markdown pages.",[50,51],"default","page","  Layouts  Docus provides multiple built-in layouts for displaying your Markdown pages.   default  The default layout for every page created in the project. This layout renders multiple section alongside the content:   Aside navigation menu (togglable with   aside: false/true )  Page bottom section (togglable with   bottom: false/true )  Table of content (togglable with   toc: false/true )     ---\n   aside  :   true\n   bottom  :   true\n   toc  :   false\n   ---\n   \n   Your awesome content\n  Current page is live sample of default layout.   page   page  layout is content focused layout. This layout does not render aside menu of table of contents.  This layout accept some configuration from content front-matter.    fluid : By setting   fluid: true  in content front-matter the content will be rendered in full width.   constrainedClass : Using this option you can modify layout container look. Like constraining layout width of changing the background.   padded : Setting   padded: true  in front-matter will add horizontal padding in the layout.     ---\n   title  :   Home\n   layout  :   page\n   fluid  :   true\n   ---\n  Check   Home page  as live sample of page layout  html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"id":54,"path":55,"dir":7,"title":56,"description":7,"keywords":57,"body":58},"content:1.member.md","/member","Member",[],"     Our Memeber          changemyminds   喜歡寫程式、熱愛軟體工程, 也喜歡健身以及美食        Spyua   個性外向，喜愛沖咖啡、平常時間喜歡閱讀外，也喜愛寫作。最開心的事是能與家人同樂。        jimpop-tw   平常喜歡..        frank30941   喜愛瑜珈        HankLi0130   平常喜歡喝咖啡        Simon-Din   平常喜歡..       Simon Wu   平常喜歡..",{"id":60,"path":61,"dir":62,"title":63,"description":7,"keywords":64,"body":72},"content:2.workshop:gitlab-cicd.md","/workshop/gitlab-cicd","workshop","使用Gitlab做CI/CD",[65,66,67,68,69,70,71],"前言","簡易開發流程","Runner Server設定","CI撰寫","CD撰寫","參考專案","Docker版本(Speak，後續待寫)","  前言  此章節不屬於持續交付2.0書中內容，額外挪出的一個教學章節。  CI/CD目前在大部分會做頻繁整合與部屬的團隊都已在使用，因此在開發中在目前的趨勢中有一定的必要性。CI為持續整合，他的用意本質在於當既有程式碼或產品有任何異動時，可以在短時間內完成整合，並確保用作如預期。而CD為持續交付，注意英文中的D為Delivery而不是Deployment。用意在讓任何一個異動可以保有品質的盡快交給客戶，並能為此帶來對應的價值，此章節會手把手教學如何使用Gitlab來達到CI/CD自動化。並透過CI/CD將.Net Core專案部屬到部屬機上。  \nCI/CD精神在於做到快速整合並保有高品質且能交付客戶使用，重點在於精神大於自動化工具，因此每個專案與產品狀況團隊也會具有不同的計畫與交付流程。\n  簡易開發流程  不使用CI/CD  一般簡易開發整合流程為   Step1. 開發  Step2. 建置  Step3. 測試  Step4. 部屬  如下圖所示，在沒有自動化整合平台下，基本上所有流程都須透過手動去完成。  \n     使用CI/CD  如下圖所示，我們只需要寫好Script File，交付到Gitlab，並在Gitlab Server上做好設定，Server就會根據Script腳本所寫的指令(命令)，交付Runner Server作執行。在使用此自動化整合架構下，我們可透過Gitlab與Runner Server完成上述的步驟開發、建置、測試與整合，甚至是程式碼分析。  一般CI會做建置測試與程式碼分析，而CD則是做部屬、整合測試與交付。在不同的應用場合下，設置會根據不同環境應用建立專屬CI與CD Runner服務。但此章節不會講到太複雜的情境，會以簡易的開發、建置、測試與部屬四個步驟帶過。  \n     \n一般可做自動化整合的Git Server，都會需要一個代理伺服器(Runner Server)來幫忙執行Script。因此我們會需要一台機器做專門的Runner Server來服務Git Server。\n  Runner Server設定  Runner Server須根據開發應用選擇不同的作業環境，例如若你的專案已.Net Framework為主。你想做自動化整合，你的Runner機台則就須選用Windows OS。若日可做跨平台的.Net Core，你則可以選擇Linux OS環境去架設Runner Server。  因Runner Server會根據Script指令做建置與測試甚至是部屬，所以需安裝可執行相對應的CLI指令及SDK。如上述提到的.Net Core，若Runner Server要編譯.Net Core專案，則就需安裝相對應的SDK。若CD需透過SSH連至部屬機操作，則Runner Server則需具備SSH連線能力。簡單的說，開發環境可做的事情，大部分在Runner上也要都具有相同功能。  Gitlab Runner分類  Gitlab Runner設置有三種模式，如下   Share Runner : 所有Group或專案可使用，不需自行架設Runner，免費版本兩千分鐘使用限制。  Specific(Project) Runner : 特定需求使用，需自行架設Runner。\n   Group Runner : 同Group的Project可使用，需自行架設Runner。    安裝Runner設定  上述提到Runner設置有三種，此文章我們會以Group Runner設置為主，請先至以下路徑  YourGroup → Setting → CI/CD → Runner (點選Expand)  展開後，請點選New group runner view的Take me there就會進入到Runners 設定頁面如下圖，    你可以點選右上角的Register a group runner，並根據你要設置的平台，在Show runner installation and registration instructions 選項有詳細設置方式，如下圖    Windows設置[  官網說明 ]   Step1 :   下載Windows Gitlab Runner檔案  Step2 : 安裝與啟動     # 切換到下載目錄\n   cd D:\\SystemService\\gitlab-runner\n   \n   # 安裝服務\n   gitlab-runner-windows-amd64.exe install\n   \n   # 進行服務啟動\n   gitlab-runner-windows-amd64.exe start\n  安裝和啟動服務成功後，可以透過工具管理員查看狀態   Step3 : 註冊  至YourGroup → Setting → CI/CD → Runner (點選Expand) → Take me there → Register group runner →Show runner installation and registration instructions  點選Windows Tab 此時就會跑出建議指令流程，    請輸入Register指令     ./gitlab-runner.exe register --url https://gitlab.com/ --registration-token your token\n   Step4 : 設置Register參數   a. Gitlab URL : 若是Group Runner，可以輸入Group Gitlab的URL(Ex:  https://gitlab.com/groupxxxx )  b. Registration Token : Your Token (Group Token or Specific Project Token)  c. Description : 關於此Runner的描述  d. Tags : 寫CI.yml時，工作會根據Tag名稱指向符合此名稱的Runner，所以這參數設定很重要，一般可以根據你的環境去命名例如Windows-xxxx或者是Linux-xxxx  e. Maintenance note : 維護說明，可填寫維護時須注意事項  f. Executor : 執行器種類，若要走較一般的CI就是選shell，執行器就可根據不同作業系統做對應的CLI執行器設定(例如Windows設定PowerShell，Linux設定bash。  參考 )  Step5 : 完成  此時我們在點選  YourGroup → Setting → CI/CD → Runner (點選Expand) → Take me there  你就可以看到註冊好的Runner顯示在設定頁面上了    Linux設置[  官網說明 ]   Step 1 : 安裝與啟動  至Gitlab說明文件直接照步驟操刀  至YourGroup → Setting → CI/CD → Runner (點選Expand) → Take me there → Register group runner →Show runner installation and registration instructions     Step 2 : 註冊 (可直接參照上述 Windows設置註冊)  Shell設置  上述設置好Ruuner後，接著需調整Runner設定檔的執行器，請開啟Runner資料夾下的toml檔，並照下圖設置  \n     詳細對應Shell設定可至此查詢[  連結請點我 ]  安裝編譯環境設定  因為Runner為代替本機電腦做建置、測試與部屬。所以需在上面安裝相對應環境，專案上我們會使用dotnet core為範例。所以電腦需安裝對應需有的SDK，若使用Docker則需安裝Docker環境。    Net Core SDK安裝參考   Dokcer安裝參考  好用請給讚，謝謝  CI撰寫  建置與測試  Hello CI/CD  請到此下載Sample Code   https://gitlab.com/test8214/emptyproject  下載下來後將此專案上到你的Group Project，若你的Runner已設好，我們可以直接從Gitlab頁面點選 Set up CI/C，如下圖  \n     接著點選Configure pipeline，此時Gitlab會幫你生成yml Sample Code，此Sample Code已幫你寫好的基本build, test與deploy Stages。請將最上層註解刪除，並加上default區段，runner tag     # default表示所有job都會參考以及使用\n   default:\n     # 使用Gitlab Runner有相關的標籤\n     tags:\n       - Windows(根據你的Runner Tag去填寫)\n  此時若Runner設置無誤，就可以看到Gitlab開始跑CI/CD，如下圖  \n     根據此腳本，我們可以得知CI.yml的基本語法由stages及對應的job name中的script。若要新增Job則只需在stages新增，例如我們在test站點後新增build-release，請修改stage區塊如下     stages  :            # List of stages for jobs, and their order of execution.\n     -   build\n     -   test\n     -   build-release   # 新增build-release\n     -   deploy\n  並將以下Script放置lint-test-job下     lint-test-job  :     # This job also runs in the test stage.\n     stage  :   test      # It can run at the same time as unit-test-job (in parallel).\n     script  :\n       -   echo \"Linting code... This will take about 10 seconds.\"\n       -   sleep 10\n       -   echo \"No lint issues found.\"\n   \n   #新增build-release job\n   build-release-job  :\n     stage  :   build-release\n     script  :\n       -   echo \"Build release app package...\"\n       -   echo \"Build complete.\"\n  接著做commit，我們即可看到Pipeline由三個站點變成四個站點  \n     build 與 test  build-job  一般在CI/CD Sample，很常會看到這個站點，用意在測試建置專案是否能編譯過。在dotnet core專案，我們在CLI模式下可以用dotnet build去建置專案，此時我們可以嘗試將build-job站點script加入dotnet build指令(建議先註解調build job以外的job Script站別)，如下     build-job  :         # This job runs in the build stage, which runs first.\n     stage  :   build\n     script  :\n       -   echo \"Compiling the code...\"\n       -   dotnet build SampleWebAPI\\src\\SampleWebAPI -c debug\n  更新yml file後，檢查一下Pipeline能否編輯的過。編譯過可看到訊息如下  \n     unit-test-job  接著我們嘗試加入Test Job，請刪除lint-test-job只保留unit-test-job並將dotnet test加入script，如下     unit-test-job:   # This job runs in the test stage.\n     stage: test    # It only starts when the job in the build stage completes successfully.\n     script:\n       - echo \"Running unit tests... This will take about 60 seconds.\"\n       - dotnet test SampleWebAPI\\test\\SampleWebAPI.Test\n  更新yml file後，檢查一下Pipeline能否編輯的過。編譯過可看到訊息如下  \n     使用變數  上述編譯與測試有使用到dotnet build與test，基本上我們可以將我們的src與test路徑寫成變數，這樣在使用上可以重複利用減少重複的程式碼     variables  :\n     AppFolderPath  :   SampleWebAPI\\\\src\\\\SampleWebAPI\n     TestFolderPath  :   SampleWebAPI\\\\test\\\\SampleWebAPI.Test\n  接著將原本的Build與Test Jib Script換掉     build-job  :         # This job runs in the build stage, which runs first.\n     stage  :   build\n     script  :\n       -   echo \"Compiling the code...\"\n       -   dotnet build ${AppFolderPath} -c debug\n   \n   unit-test-job  :     # This job runs in the test stage.\n     stage  :   test      # It only starts when the job in the build stage completes successfully.\n     script  :\n       -   echo \"Running unit tests... This will take about 60 seconds.\"\n       -   dotnet test ${TestFolderPath}\n  \n因為我們使用dot net core，如果我們有寫測試Code，我們可以直接省略測試建置這個站點(build-job)。只需要做Test即可(unit-test-job)。下dot net test時，因為要跑測試，他會順便建置App專案。\n  CD撰寫  接著我們要做簡易的Release建置與部屬，因為Windows權限設置較麻煩，這部分Demo我們使用Linux Runner去實現(Windows系統若要快速啟用Linux Runner，可以使用WSL2)。請啟用Linux Runner並在Runner上設置好.net core需要編譯的SDK安裝，另外則是SSH安裝。  Runner設置好後，我們將上述提到的default區塊使用到的tags設定，由Windows改成Linux(在此可根據你的Linux設置)。接著將上述寫好的腳本將斜線改成反斜線，參考Script如下     # default表示所有job都會參考以及使用\n   default  :\n     # 使用Gitlab Runner有相關的標籤\n     tags  :\n       -   Linux\n   \n   variables  :\n     AppFolderPath  :   SampleWebAPI//src//SampleWebAPI\n     TestFolderPath  :   SampleWebAPI//test//SampleWebAPI.Test\n   \n   stages  :            # List of stages for jobs, and their order of execution\n     -   test\n   \n   unit-test-job  :     # This job runs in the test stage.\n     stage  :   test      # It only starts when the job in the build stage completes successfully.\n     script  :\n       -   echo \"Running unit tests... This will take about 60 seconds.\"\n       -   dotnet test ${TestFolderPath}\n  若已確定你的Pipeline無誤，接著就可以往下走測試Release建置與部屬  建置與部屬  在建置部屬之前，請先準備好你的部屬Server，目前此章節用的部屬Server為Ubuntu 20.04 Linux，並已安裝好   .Net Core 3.1 SDK  SSH  Docker  pm2  unzip  SSH設定  為了簡易Demo，連線部分我們直接使用ssh與部屬Server溝通，為了免登入密碼，故須設置ssh私鑰與公鑰。請至你的Linux Runner資料夾底下會有一個.ssh file。若你是照Gitlab上的步驟設置，沒意外你的Runner路徑會在home底下     # 切換sudo\n   sudo -i\n   \n   # 切換到ssh目錄下\n   cd /home/gitlab-runner/.ssh\n   \n   #產生金鑰匙(方便測試可全按Enter)\n   ssh-keygen\n   \n   #更改私鑰權限為600\n   chmod 600 id_rsa\n   \n   # 接著將你的公鑰傳至你的部屬機上\n   scp id_dsa.pub user@abc.xxx.xxx.xxx:/root/.ssh/id_dsa.pub\n  接著連過去部屬機     # 連線部屬機\n   ssh root@abc.xxx.xxx.xxx\n   \n   # 輸入密碼\n   \n   # 至SSH File底下將Key複製一分到authorized_keys檔案底下\n   cat id_rsa.pub > authorized_keys\n   \n   # 確定authorized_keys權限為600\n   chmod 600 authorized_keys\n   \n   # 登出\n   exit\n   \n   # 再次登入確定是否免密碼\n   ssh root@abc.xxx.xxx.xxx\n  比較笨的方式，你也可以手動複製將你自己Runner下的私鑰手動複製到部屬機的authorized_keys檔案  Project SSH Key變數  確定好Runner與部屬機之間已可免密碼連線後，我們需要在將Runner的私鑰複製一份至專案的CI/CD設定下  請至你Group  YourPorject Repo/Settings/CICD 下  此時你會看到Variables，請新增一SSH_PRIVATE_KEY變數如下    \n須注意!因為是Group Runner，所以一開始我只將SSH PK設置在Group Setting的CICD設置變數下。結果Project上tags觸發Pipeline在部屬時找不到SSH PK。所以Project Repo若要上tags，須將SSH PK設置在Project Repo Setting的CICD變數設定下。\n  build-release-job與deploy  部屬機等環境設置好後，接下來就開始寫Release與deploy。先簡單帶artifacts版本，然後連至部屬主機將artifacts載下來後，解壓縮直接透過pm2將web api服務啟起來。  build-release-job  為了減少測試時間。我們可以先嘗試使用dotnet publish指令，自己先試看看有無問題，無問題通常可以直接將指令複製到script上。  建置release stage，參考script如下     default:\n     tags:\n       - Linux\n   \n   variables:\n     AppFolderPath: SampleWebAPI//src//SampleWebAPI\n     TestFolderPath: SampleWebAPI//test//SampleWebAPI.Test\n   \n   stages:          \n     - test\n     # 建置release stage\n     - release\n   \n   unit-test-job:  \n     stage: test\n     script:\n       - echo \"Running unit tests... This will take about 60 seconds.\"\n       - dotnet test ${TestFolderPath}\n   \n   # 建置release stage\n   build-release-job:\n     stage: release\n     script:\n       - echo \"Build release...\"\n       - dotnet publish ${AppFolderPath} -c release\n  建置artifacts  接著我們希望在release job完成後同步產生artifacts。參考script如下     build-release-job:\n     stage: release\n     script:\n       - echo \"Build release...\"\n       - dotnet publish ${AppFolderPath} -c release\n     artifacts:\n       # on_success always on_failure\n       when: always \n       # 30 mins, 1 weeks...\n       expire_in: 30 mins\n       paths:\n         - ./SampleWebAPI/src/SampleWebAPI/bin/release/netcoreapp3.1\n  我們將artifacts寫至build-release-job下，這邊會有幾個設定   when : upload articacts時機 (  參考點我 )  expire : articats 存活時間 (  參考點我 )  paths: 要包裝檔案的路徑  此時我們在跑一次Pipeline，就可以看到build-release-job下有個可下載的介面(如下圖)，代表你的artifactst產生成功。    artifacts url測試(  參考點我 )  接著我們可以透過url下載artifacts，Sample如下   https://gitlab.com/test8214/testproject/-/jobs/artifacts/main/download?job=build-release-job  可修改的部分   test8214 : 你的Group Url  testproject : Project Repository Name  main:表示主線  release-job:表示你的stage job  \n artifacts Project URL請根據你實際的 Project URL設定調整，誤直接照抄\n  所以此url的意思為要從testproject下main主線最後一個成功Pipeline的build-release-job下載artifacts。  另外，比較好的方法是用CI_JOB_TOKEN去打Job Artifacts API拿取Artifacts，參考如\n連結  https://docs.gitlab.com/ee/api/job_artifacts.html  因為url方式是拿取最後一個Pipeline的build-release-job產物，代表他會拿取前一個成功的Pipline產物不是當下運行Pipeline的build-release-job產物。  因為這樣，所以這個Demo就不會將build-release-job上only tags。讓deploy可以正確拿取目前運行Pipeline編譯好的程式碼如下。    \n較好的Pepeline設計是將release設置在tag觸發時，也就是做CD時機點。後續再找時間修正artifacts下載方式\n  deploy  在build-release-job做完後，接著我們要做deploy stage，一開始我們須建立ssh連線，腳本指令如下     deploy-job:\n     stage: deploy\n     script:\n       - eval $(ssh-agent -s)\n       - echo \"$SSH_PRIVATE_KEY\" | tr -d '\\r' | ssh-add -     \n       - mkdir -p ~/.ssh     \n       - chmod 700 ~/.ssh \n       -> \n         ssh -o StrictHostKeyChecking=no -v user@125.229.14.65 -p 40122\n  這部分會稍微難了解一點，整體來說，主要目的在於建立一個具有PK權限的連線，讓Gitlab Runner可以在不同已允許權限的部屬主機連線(也許不只一台)。  細節部分，ssh-agent有點像是管理ssh key的工具。  第一行eval通常是印出來的意思，有點類似 echo，不過他可以直接把指令顯示出來，所以此處為顯示ssh-agent的狀態。  第二行是把 gitlab的環境變數讀取出來，之後 tr -d  '\\r' 主要是把換行給取代掉，windows內建是CRLF 相當於 /r/n，而這邊的 /r 就是Carriage-Return，所謂的迴車字元。  第三行創建 -/.ssh 是在目前使用者目錄底下創建 .ssh，用來記錄等一下你的ssh連線資訊，通常ssh連線後，會記錄在一個叫做 known_host的檔案。  寫好連線deploy-job後，我們可以嘗試讓他跑看看能否連線部屬主機成功，     default:\n     tags:\n       - Linux\n   \n   variables:\n     AppFolderPath: SampleWebAPI//src//SampleWebAPI\n     TestFolderPath: SampleWebAPI//test//SampleWebAPI.Test\n   \n   stages:          \n     - test\n     - release\n     # 部屬\n     - deploy\n   \n   unit-test-job:  \n     stage: test\n     script:\n       - echo \"Running unit tests... This will take about 60 seconds.\"\n       - dotnet test ${TestFolderPath}\n   \n   \n   build-release-job:\n     stage: release\n     script:\n       - echo \"Build release...\"\n       - dotnet publish ${AppFolderPath} -c release\n     artifacts:\n       # on_success always on_failure\n       when: always \n       # 30 mins, 1 weeks...\n       expire_in: 30 mins\n       paths:\n         - ./SampleWebAPI/src/SampleWebAPI/bin/release/netcoreapp3.1\n   # 部屬      \n   deploy-job:\n     stage: deploy\n     script:\n       - eval $(ssh-agent -s)\n       - echo \"$SSH_PRIVATE_KEY\" | tr -d '\\r' | ssh-add -     \n       - mkdir -p ~/.ssh     \n       - chmod 700 ~/.ssh \n       -> \n         ssh -o StrictHostKeyChecking=no -v user@125.229.14.65 -p 40122\n  若連線成功，deploy stage則會運行通過(打勾勾)，接著就可以開始寫連至部屬Server的指令了。這邊可以看到有一個->的符號，代表此符號區段下的指令是在SSH目標主機上執行。  接著連過去後，我們加入要執行的Script指令     # 部屬      \n   deploy-job:\n     stage: deploy\n     script:\n       - eval $(ssh-agent -s)\n       - echo \"$SSH_PRIVATE_KEY\" | tr -d '\\r' | ssh-add -     \n       - mkdir -p ~/.ssh     \n       - chmod 700 ~/.ssh \n       - >\n         ssh -o StrictHostKeyChecking=no -v user@125.229.14.65 -p 40122\n         \"mkdir -p ~/sampleapi &&\n          wget -O ~/sampleapi/release-build.zip https://gitlab.com/test8214/testproject/-/jobs/artifacts/main/download?job=build-release-job &&\n          sudo unzip -o ~/sampleapi/release-build.zip -d ~/sampleapi &&\n          pm2 start 'dotnet ~/sampleapi/SampleWebAPI/src/SampleWebAPI/bin/release/netcoreapp3.1/SampleWebAPI.dll --urls http://0.0.0.0:5000' --name 'code-sense-api-service' &&\n          pm2 delete code-sense-api-service &&\n          pm2 start 'dotnet ~/sampleapi/SampleWebAPI/src/SampleWebAPI/bin/release/netcoreapp3.1/SampleWebAPI.dll --urls http://0.0.0.0:5000' --name 'code-sense-api-service'\n         \"\n  指令內容大致是連至目標主機後，   在user下建置sampleapi資料夾  下載artifacts  解壓鎖  使用pm2將Service Run起來  \n artifacts Project URL請根據你實際的 Project URL設定調整，誤直接照抄\n    deploy完後此時就可以透過API，得到資料   http://x.x.x.x:你的對外Port/weatherforecast  \ndeploy可以看到pm2 start兩次，因為在第一次加入時沒有對應service name可以刪除會出錯。使用> /dev/null 測試沒用。只能先用此法暫解。章節目的還是在於如何建立一起一條簡易的CI/CD Pipeline為注。正式複雜的場合可以參考Docker版本\n  only tags  接著我們要在deploy上tags觸發事件(上標籤)，只需要在原本的script下，多一個tags屬性     # 部屬      \n   deploy-job:\n     stage: deploy\n     script:\n       - eval $(ssh-agent -s)\n       - echo \"$SSH_PRIVATE_KEY\" | tr -d '\\r' | ssh-add -     \n       - mkdir -p ~/.ssh     \n       - chmod 700 ~/.ssh \n       - >\n         ssh -o StrictHostKeyChecking=no -v user@125.229.14.65 -p 40122\n         \"mkdir -p ~/sampleapi &&\n          wget -O ~/sampleapi/release-build.zip https://gitlab.com/test8214/testproject/-/jobs/artifacts/main/download?job=build-release-job &&\n          sudo unzip -o ~/sampleapi/release-build.zip -d ~/sampleapi &&\n          pm2 start 'dotnet ~/sampleapi/SampleWebAPI/src/SampleWebAPI/bin/release/netcoreapp3.1/SampleWebAPI.dll --urls http://0.0.0.0:5000' --name 'code-sense-api-service' &&\n          pm2 delete code-sense-api-service &&\n          pm2 start 'dotnet ~/sampleapi/SampleWebAPI/src/SampleWebAPI/bin/release/netcoreapp3.1/SampleWebAPI.dll --urls http://0.0.0.0:5000' --name 'code-sense-api-service'\n         \"\n     # 上tags觸發事件\n     only:\n       - tags\n  上完tags後，deploy事件則只會在你上tag時觸發。接著就可以在tag頁面看到此次的release tag是否有通過Pipeline Job。    \n一般CD做release build與deploy都會上only tags，此範例因為artifacts還不確定是不是已正確方式拿取，所以只在deploy上only tags。\n  參考專案  Repository:  https://gitlab.com/test8214/testproject  Docker版本(Speak，後續待寫)  Repository:  https://gitlab.com/test8214/demoproject  html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"id":74,"path":75,"dir":62,"title":76,"description":7,"keywords":77,"body":84},"content:2.workshop:grpc.md","/workshop/grpc","gRPC快速入門",[78,79,80,81,82,83],"HTTP2入門介紹","Protobuf 3介紹","gRPC Code generator","gRCP 呼叫的四種類型","gRPC工具","參考","  HTTP2入門介紹  HTTP/2 概述  HTTP/2 是 HTTP 協議的第二個版本，它基於 Google 的 SPDY 協議，旨在提高網頁的加載速度和性能。相比於 HTTP/1.x，HTTP/2 改進了以下方面：   多路徑傳輸 (Multiplexing)：HTTP/2 允許同一個 TCP 連接上進行多個請求和響應，從而減少了 TCP 連接數量和建立連接的延遲。\n首部壓縮 (Header Compression)：HTTP/2 使用 HPACK 算法對首部進行壓縮，從而減少了首部大小，降低了帶寬消耗。\n服務端推送 (Server Push)：HTTP/2 允許服務器在響應一個請求時主動推送相關的資源到客戶端緩存中，從而提高頁面加載速度。\n  HTTP/2 原理  HTTP/2 使用二進制格式傳輸資料，將資料分為 HEADERS 和 DATA 兩部分。多個請求和響應可以在同一個 TCP 連接上進行，每個請求和響應都有自己的 ID。HTTP/2 也支持優先級 (Priority) 機制，可以對請求進行排序和分配優先級。  HTTP/2 和 HTTP/1.x 的不同之處     HTTP/2  HTTP/1.x    基於二進制格式  基於文本格式   允許多路徑傳輸  只能串行傳輸   使用首部壓縮  不支持首部壓縮   支持服務端推送  不支持服務端推送   支持優先級  不支持優先級  HTTP/2 的優缺點  優點   加載速度更快：多路徑傳輸和首部壓縮等機制可以減少延遲和帶寬消耗，從而提高加載速度。  效率更高：多路徑傳輸和優先級等機制可以更好地利用網絡資源，從而提高效率。  安全性更高：HTTP/2 強制要求使用加密，從而提高了安全性。  缺點   需要加密：HTTP/2 強制要求使用加密，這增加了網站的成本和複雜度。  需要支援 HTTP/2 的服務器和客戶端：不是所有的服務器和客戶端都支援 HTTP/2，需要更新到支援 HTTP/2 的版本。   Protobuf 3介紹  Protocol Buffers (protobuf)是Google開發的一種輕量級、高效能的資料序列化格式，用於跨平台資料交換和儲存。Protocol Buffers可以在不同的程式語言之間進行資料交換，並且比XML和JSON等常見的資料序列化格式更小、更快、更易於使用和維護。  常見的欄位以及修飾符號  singular  gRPC內部的預設的欄位類型，沒有撰寫其他修飾符號就表示預設使用  singular 為欄位的最小單位。  optional  參數值為可選的，可以把它看作為  null ，通常會生成程式碼後，會有  HasXXX 方法或屬性來判斷此數值是否存在。我們藉由optional關鍵字來簡化傳輸大小。  repeated  宣告一個空的  list  (非  null )，通常生成程式碼後，會根據對應的語言進行產生，例如: Go為  slice ，C#為  list 等等。   protobuf     message   Person   {\n     string   name   =   1  ;\n     repeated   string   phone_numbers   =   2  ;\n   }\n   java     // 創建一個人的物件\n   Person   person   =   Person.  newBuilder  ()\n     .  setName  (  \"John\"  )\n     .  addPhoneNumbers  (  \"1234567890\"  )\n     .  addPhoneNumbers  (  \"0987654321\"  )\n     .  build  ();\n   \n   // 獲取人的電話號碼列表\n   List  \u003C  String  > phoneNumbers   =   person.  getPhoneNumbersList  ();\n  map  宣告一個空的  map  (非  null )，通常生成程式碼後，會根據對應的語言進行產生，例如: Java為  HashMap ，C#為  Dictionary 等等。   protobuf     message   Student   {\n     string   name   =   1  ;\n     map  \u003C  string  ,   double  > grades   =   2  ;\n   }\n   java     // 創建一個學生的物件\n   Student   student   =   Student.  newBuilder  ()\n     .  setName  (  \"John\"  )\n     .  putGrades  (  \"Math\"  ,   90.0  )\n     .  putGrades  (  \"Science\"  ,   80.0  )\n     .  build  ();\n   \n   // 獲取學生的成績\n   Map  \u003C  String  ,   Double  > grades   =   student.  getGradesMap  ();\n  enum  與我們一般使用的列舉差異不大，但宣告的enum編號必須是從  0 開始。   protobuf     enum   UserRole   {\n     ADMIN   =   0  ;\n     EDITOR   =   1  ;\n     VIEWER   =   2  ;\n   }\n   \n   message   User   {\n     string   name   =   1  ;\n     repeated   UserRole   roles   =   2  ;\n   }\n   java     // 創建一個 User 物件\n   User   user   =   User.  newBuilder  ()\n       .  setName  (  \"John\"  )\n       .  addRoles  (UserRole.EDITOR)\n       .  addRoles  (UserRole.VIEWER)\n       .  build  ();\n   \n   // 獲取 User 物件的屬性值\n   String   name   =   user.  getName  ();\n   List  \u003C  UserRole  > roles   =   user.  getRolesList  ();\n  oneof  跟  enum 非常相似，但主要用於互斥的屬性，且不能搭配  repeated 關鍵字。   protobuf     message   Stock   {\n       // Stock-specific data\n   }\n   \n   message   Currency   {\n       // Currency-specific data\n   }\n   \n   message   ChangeNotification   {\n     int32   id   =   1  ;\n     oneof   instrument {\n       Stock   stock   =   2  ;\n       Currency   currency   =   3  ;\n     }\n     oneof   actionType {\n       int32   increase   =   4  ;\n       int32   decrease   =   5  ;\n     }\n   }\n   java     // 創建ChangeNotification物件，並設置id、股票和增量等資訊\n   ChangeNotification   notification   =   ChangeNotification.  newBuilder  ()\n           .  setId  (  1  )\n           .  setStock  (Stock.  newBuilder  ().  build  ())\n           .  setIncrease  (  100  )\n           .  build  ();\n   \n   // 創建Currency物件，並設置id、貨幣和減量等資訊\n   ChangeNotification   currencyNotification   =   ChangeNotification.  newBuilder  ()\n           .  setId  (  2  )\n           .  setCurrency  (Currency.  newBuilder  ().  build  ())\n           .  setDecrease  (  50  )\n           .  build  ();\n  Well known types     套件名稱  用途    google.protobuf.Empty  類似於void沒有回傳值，但在ProtoBuf中仍需要撰寫   google.protobuf.Timestamp  類似於DateTime或DateTimeOffset   google.protobuf.Duration  類似於TimeSpan   google.protobuf.Any  類似於object，需要透過  Is 和  Unpack\u003C> 來進行判斷物件型別和轉型  💡 更多形別參考👉  參考  gRPC Code generator  buf  Building a better way to work with Protocol Buffers  install buf     brew   install   bufbuild/buf/buf\n  install go tool     go   install   github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest\n   \n   go   install   github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest\n   \n   go   install   google.golang.org/protobuf/cmd/protoc-gen-go@latest\n   \n   go   install   google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest\n   \n   go   install   github.com/envoyproxy/protoc-gen-validate@latest\n  Getting start     git   clone   https://github.com/bufbuild/buf-tour\n   cd   buf-tour/start/getting-started-with-buf-cli\n  buf使用buf.yaml文件配置，使用此命令創建您自己的文件：     cd   proto\n   buf   mod   init\n  運行此命令後，您會在當前目錄中看到一個buf.yaml包含以下內容的文件：\nbuf.yaml     version  :   v1\n   breaking  :\n     use  :\n       -   FILE\n   lint  :\n     use  :\n       -   DEFAULT\n  buf默認情況下假定當前目錄中有一個buf.yaml，或者使用默認值代替文件buf.yaml 。buf.yaml我們建議始終在文件層次結構的根目錄下放置一個文件.proto，因為這是.proto 解析導入路徑的方式。\n在我們繼續之前，讓我們驗證一切都設置正確，然後我們可以構建我們的模塊。如果沒有錯誤，我們就知道我們已經正確設置了一個 buf 模塊：     //   ~/.../buf-tour/start/getting-started-with-buf-cli/proto\n   buf   build   \n   echo   $?\n  生成代碼  使用以下命令移回目錄getting-started-with-buf-cli     cd   ..\n   touch   buf.gen.yaml\n  更新您的內容buf.gen.yaml以包含 Go 和 Connect-Go 插件：     version  :   v1\n   managed  :\n     enabled  :   true\n     go_package_prefix  :\n       default  :   github.com/bufbuild/buf-tour/gen\n   plugins  :\n     -   plugin  :   buf.build/protocolbuffers/go\n       out  :   gen\n       opt  :   paths=source_relative\n     -   plugin  :   buf.build/bufbuild/connect-go\n       out  :   gen\n       opt  :   paths=source_relative\n     buf   generate   proto\n     getting-started-with-buf-cli\n   ├──   buf.gen.yaml\n   ├──   gen\n   │     ├──   google\n   │     │     └──   type\n   │     │         └──   datetime.pb.go\n   │     └──   pet\n   │         └──   v1\n   │             ├──   pet.pb.go\n   │             └──   petv1connect\n   │                 └──   pet.connect.go\n   └──   proto\n       ├──   buf.yaml\n       ├──   google\n       │     └──   type\n       │         └──   datetime.proto\n       └──   pet\n           └──   v1\n               └──   pet.proto\n  protoc  gRCP 呼叫的四種類型  \n     Unary  類似傳統類型的RESTful API，client發送request而Server回應Response。  Server Streaming  Client發送一次request，而server可以回傳多次資料。  Client Streaming  Client發送多次資料，直到告知server資料傳完後，server在給予Response。  Bi Dirctional Streaming  兩邊都用串流的方式傳送資料。  gRPC工具  grpcurl  grpcui  postman  參考   Google Protocol Buffers  html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"id":86,"path":87,"dir":62,"title":88,"description":7,"keywords":89,"body":92},"content:2.workshop:liquibase.md","/workshop/liquibase","Liquibase快速入門",[65,90,91,88,83],"常用的資料庫版本更新方式，有下列兩種","Database Version Control Tools","  前言  此章節主要是補充CI/CD 2.0的11.7章節的內容，關於版本控制  常用的資料庫版本更新方式，有下列兩種    State-based tools   通過比較兩個資料庫中的結構模型而產生的腳本(script)，使用此腳本進行資料庫升級。   Migration-based tools  創建遷移用的腳本，替資料庫從一個版本遷移到下一個版本。  State-based tools (基於狀態的版本控制)  \n     在  state based 模式底下，我們僅需要維護資料庫的目標狀態，每個  表(Table) 、  Stored Procedure 、  View 、  Trigger 將保存為單獨的SQL文件，這些SQL文件就是資料庫真實的樣貌。而升級資料庫所需的腳本會由工具自動生成，從而大大減輕維護成本。   可以想像成MySQL中的mysqldump所建立的DDL表結構語法。  在  IaC(Infrastructure-as-Code) 領域中，Kubernetes、HashiCorp Terraform等流行軟體皆採用這種方式。    💡 目前最流行的做法是推薦此方式，但此方法會有缺點，由於Script是系統產生的，因此還是得自行注意細節。\n  Migration-based tools (基於遷移的版本控制)  \n     基於遷移的方法是將所有的遷移腳本儲存在Repository中。每一個腳本都包含了一系列的DDL語句，例如: CREATE/ALTER/DROP TABLE。最終的資料庫中的資料表結構是由這些腳本按照順序的執行來完成的。  相比  state based 模式，該模式增加了成本的  維護 和  複雜性 ，但它能讓我們更直接地控制  遷移過程 ，從而能夠處理如資料庫遷移這樣上下文相關的場景。  Database Version Control Tools  本圖片的來源資料，是參考  網址  \n     Liquibase快速入門  安裝CLI  Liquibase版本，我們將採用4.15.0版本進行演示，記得必須要安裝Java 8以上(官方建議Java 11)。  Win10   前往  官網載點 或  Github Release 依照對應所需的版本進行安裝，我們使用zip檔進行安裝 \n   \n     \n   下載後進行解壓縮放到指定的路徑底下，將liquibase放置在  D:\\tool\\liquibase-4.15.0 路徑 \n   \n     \n   設定系統環境變數，添加  D:\\tool\\liquibase-4.15.0 路徑  測試指令，重新開啟Terminal，並輸入指令liquibase -v \n   \n     \n   Docker     # 下載image liquibase 4.15.0\n   docker   pull   liquibase/liquibase:4.15.0\n   \n   # 查看liquibase版本，測試是否安裝成工\n   docker   run   --rm   liquibase/liquibase:4.15.0   -v\n  初始化liquibase     # 初始化專案\n   liquibase   init   project\n   \n   # 查看初始化後的結果\n   ls\n   ## example-changelog.sql  liquibase.properties\n  安裝Package Manager  Liquibase提供預設Driver在  internal\\lib 底下，但某些適合的資料庫Driver還是沒有提供，因此我們可以使用  Liquibase Package Manager 來幫助我們快速的搜尋套件來完成。   更多的Liquibase Driver，參考  網址  安裝   前往  Github Release 進行下載，點選對應的OS進行下載，這邊採用Win10進行舉例。  下載完成後解壓縮，設定環境變數 → 系統變數 → Path → 編輯環境變數 → 加上  D:\\tool\\lpm-0.1.7-windows  → 重啟終端機(Terminal)  常用指令     # 查看目前可以使用的lib\n   lpm   update\n   \n   # 查看套件版本\n   lpm   -v\n   \n   # 搜尋套件mongodb名稱\n   lpm   search   mongodb\n   \n   # 將mongodb加入到套件中\n   lpm   add   -g   mongodb\n   lpm   add   -g   liquibase-mongodb\n   \n   # 查看目前加入的套件\n   lpm   list\n  💡 指令  lpm add 的參數  -g 說明     使用global會將套件安裝到lib資料夾中，此資料夾為liquibase預設的lib資料夾  沒使用global則會安裝到liquibase_libs底下，必須再額外執行  JAVA_OPTS 來設定Java環境  執行方式  一般liquibase的執行有兩種方式，分別為   完全的CLI指令，不需要透過  liquibase.properties 設定  使用CLI指令搭配  liquibase.properties 設定檔  常用命令     \n   # 初始化liquibase\n   liquibase   init   project\n   \n   # 產生changelog並把log等級debug寫入到error.log\n   liquibase   --log-level=debug   --logFile=error.log   generateChangeLog\n   \n   # liquibase更新資料庫\n   liquibase   update\n   \n   # liquibase更新資料庫，並且指定某個shopping_cart\n   liquibase   --labels=  \"feature/shopping_cart\"   update\n   \n   # liquibase更新資料庫，指定dev環境上版本\n   liquibase   --log-level=debug   --contexts=  \"dev\"   update\n   \n   # 替目前最後一筆DATABASECHANGELOG，進行tag上版本\n   liquibase   tag   version_1.3\n   \n   # 將所有指定的資料庫中的所有資料表丟棄 (包含非liquibase創建)\n   liquibase   drop-all\n   \n   # 往回退回一個changeset版本\n   liquibase   rollbackCount   1\n   \n   # 退回到version_1.3的tag版本\n   liquibase   rollback   version_1.3\n   \n   # 查看changeset的列表\n   liquibase   status   --verbose\n   \n   # 驗證dbchangelog.xml的格式是否正確\n   liquibase   validate\n  從現有資料庫產生ChangeLog     # 從現有資料庫產生ChangeLog\n   liquibase   --driver=com.microsoft.sqlserver.jdbc.SQLServerDriver   \\\n     --classpath=D:/tool/liquibase-4.15.0/internal/lib/mssql-jdbc.jar   \\\n     --url=jdbc:sqlserver://127.0.0.1:1433  ;database  =  Test_Liquibase_Demo  ;trustServerCertificate  =  true  ;   \\\n     --changeLogFile  =  dbchangelog-test.xml   \\\n     --username=test   \\\n     --password=12345678   \\\n     generateChangeLog\n   \n   # 查看產生的結果\n   ls\n   ##dbchangelog-test.xml\n  Migration-based  傳統的Migration方式，藉由Script版本來進行控制。  前置作業  請先在SQL Serer中建立  Test_Liquibase_Demo 資料庫。  新增資料   修改  liquibase.properties     classpath=D:/tool/liquibase-4.15.0/internal/lib/mssql-jdbc.jar\n   driver=com.microsoft.sqlserver.jdbc.SQLServerDriver\n   url=jdbc:sqlserver://127.0.0.1:1433;database=Test_Liquibase_Demo;trustServerCertificate=true;\n   username=test\n   password=12345678\n   changeLogFile=dbchangelog.xml\n   liquibase.hub.mode=off\n  初始化ChangeLog(產生dbchangelog.xml)     # 初始化，將錯誤訊息，塞入到error.log中\n   liquibase   --log-level=debug   --logFile=error.log   generateChangeLog\n  新增  DbChangeLog_V1.0.sql     --liquibase formatted sql\n   --changeset changemyminds:00001 labels:first-time\n   --comment Create Users table\n   CREATE TABLE Users (\n       -- 使用者編號\n       ID INTEGER NOT NULL IDENTITY NOT FOR REPLICATION,\n       -- 使用者姓名\n       Username NVARCHAR (50) NOT NULL,\n       -- 使用者密碼\n       Password NVARCHAR (50) NOT NULL,\n       -- 使用者狀態\n       Status INTEGER NOT NULL,\n   );\n   --rollback DELETE FROM Users;\n   \n   --changeset changemyminds:00002 labels:first-time\n   --comment Insert default users\n   INSERT INTO\n       Users (Username, Password, Status)\n   VALUES\n       ('Darren', '11111111', 1),\n       ('spyua', '11111111', 1),\n       ('Jimpo', '11111111', 1),\n       ('frank', '11111111', 1);\n   --rollback DELETE FROM Users WHERE Username='Darren' OR Username='spyua OR Username='Jimpo OR Username='frank'\n  新增  DbChangeLog_V1.1.sql     --liquibase formatted sql\n   --changeset changemyminds:00003 labels:feature/shopping_cart\n   --comment Create table SHOES\n   CREATE TABLE SHOES(\n    [ID] [bigint] IDENTITY(1, 1) NOT NULL PRIMARY KEY,\n    Brand VARCHAR(50),\n    Size bigint, \n   );\n   --rollback DROP TABLE SHOES\n   \n   --changeset changemyminds:00004 labels:feature/shopping_cart\n   --comment Insert shoes record\n   INSERT INTO SHOES(Brand, Size) VALUES('Addias', 100);\n   INSERT INTO SHOES(Brand, Size) VALUES('NewBlance', 400);\n   --rollback DELETE FROM SHOES WHERE Brand='Addias' OR Brand='NewBlance'\n  修改  dbchangelog.xml     \u003C?  xml   version  =  \"1.1\"   encoding  =  \"UTF-8\"   standalone  =  \"no\"  ?>\n   \u003C  databaseChangeLog   xmlns  =  \"http://www.liquibase.org/xml/ns/dbchangelog\"   xmlns:ext  =  \"http://www.liquibase.org/xml/ns/dbchangelog-ext\"       xmlns:pro  =  \"http://www.liquibase.org/xml/ns/pro\"   xmlns:xsi  =  \"http://www.w3.org/2001/XMLSchema-instance\"   xsi:schemaLocation  =  \"http://www.    liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/pro     http://www.liquibase.org/xml/ns/pro/liquibase-pro-latest.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/    dbchangelog/dbchangelog-latest.xsd\"  >\n       \u003C  include   file  =  \"DbChangeLog_V1.0.sql\"   />\n       \u003C  include   file  =  \"DbChangeLog_V1.1.sql\"   />\n       \u003C!-- 新增Tag版本 -->\n       \u003C  changeSet   id  =  \"tag_1.2\"   author  =  \"changemyminds\"  >\n           \u003C  tagDatabase   tag  =  \"version_1.2\"   />\n       \u003C/  changeSet  >\n   \n   \u003C/  databaseChangeLog  >\n  常見操作     # 執行first-time標籤，進行migration\n   liquibase   --log-level=debug   --labels=  \"first-time\"   update\n   \n   # 執行全部的migration\n   liquibase   --log-level=debug   update\n   \n   # 執行rollback\n   liquibase   rollbackCount   1\n   \n   # 將所有指定的資料庫中的所有資料表丟棄 (包含非liquibase創建)\n   liquibase   drop-all\n  State-based  某些情況下我們想要比較Database的版本，例如: 測試機的  Schema 和正式機的  Schema 中的差異，然後自動產生正式機可以使用SQL Script來讓我進行操作。  我們需要利用  diff 、  diffChangeLog 、  updateSQL 、  futureRollbackSQL 來完成。   使用  State-based 方式，由於產生的腳本(script)是由  Liquibase 進行產生的，因此可能會有差異，建議還是要檢查，若沒有安全感的話，建議還是使用  Migration-based 比較可以自己掌控。  前置作業  請先在SQL Serer中建立  Test_Liquibase_Demo_Prod 資料庫。  執行操作   新增  liquibase.properties     # 連線的驅動driver\n   classpath=D:/tool/liquibase-4.15.0/internal/lib/mssql-jdbc.jar\n   driver=com.microsoft.sqlserver.jdbc.SQLServerDriver\n   \n   # 產生的檔案\n   changeLogFile=dbchangelog.xml\n   \n   # 關閉hub\n   liquibase.hub.mode=off\n   \n   # 正式機資料庫\n   url=jdbc:sqlserver://127.0.0.1:1433;databaseName=Test_Liquibase_Demo_Prod;trustServerCertificate=true;\n   username=test\n   password=12345678\n     \n   # 參考的測試機資料庫\n   referenceUrl=jdbc:sqlserver://127.0.0.1:1433;databaseName=Test_Liquibase_Demo;trustServerCertificate=true;\n   referenceUsername=test\n   referencePassword=12345678\n  執行指令     # 對資料庫進行兩者差異的比較，並將結果寫入到檔案中\n   liquibase   diff   --outputFile=diff_between_DEV_PROD.log\n   \n   # 對資料庫進行兩者差異的比較，並且產生changelog\n   liquibase   diffChangeLog\n   \n   # 創建差異的SQL\n   liquibase   updateSQL   >   update.sql\n   \n   # 也可以產生rollback的sql\n   liquibase   futureRollbackSQL   >   rollback.sql\n  更新正式機資料庫     liquibase   update\n  參考    liquibase   version-control-tools  html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"id":94,"path":95,"dir":62,"title":96,"description":7,"keywords":97,"body":102},"content:2.workshop:redis.md","/workshop/redis","Redis快速入門",[65,98,99,100,101,83],"安裝","常用的資料結構","其他常用功能","常見的三大問題","  前言  此章節不屬於  資料密集型應用系統設計 書中內容，額外挪出的一個教學章節。  這邊主要是補充快取(Cache) Redis 資料庫的使用方式以及操作。  安裝  版本號選擇   奇數： 2.7、2.9、3.1，為  不穩定版本  偶數： 2.6、2.8、3.0，為  穩定版本  Linux 安裝  原始碼安裝     # 下載檔案\n   curl   -LO   \"https://download.redis.io/releases/redis-6.2.7.tar.gz\"\n   \n   # 解壓縮\n   tar   -xvf   redis-6.2.7.tar.gz\n   \n   # 移除安裝檔\n   rm   redis-6.2.7.tar.gz\n   \n   # 前往解壓縮目錄\n   cd   redis-6.2.7\n   \n   # 編譯專案 (類似編譯了class)\n   make\n   \n   # 將binary檔安裝到某個目錄底下\n   make   install   PREFIX=/root/redis-6.2.7/redis\n   \n   # 查看make的安裝目錄\n   $   ls   /root/redis-6.2.7/redis/bin/\n   redis-benchmark    redis-check-aof    redis-check-rdb    redis-cli    redis-sentinel    redis-server\n  💡 如果執行 make 指令報錯誤，則需要執行下列指令，進行安裝   Ubuntun     # 請先確認gcc是否安裝\n   gcc   -v\n   \n   # 更新apt套件\n   sudo   apt-get   update\n   \n   # 安裝apt套件\n   sudo   apt   -y   install   gcc   automake   autoconf   libtool   make\n  CentOS     \n   # 請先確認gcc是否安裝\n   gcc   -v\n   \n   # 更新yum套件\n   sudo   yum   update\n   \n   # 安裝yum套件\n   sudo   yum   -y   install   gcc   automake   autoconf   libtool   make\n  安裝目錄底下的檔案介紹     可執行的檔案  功能用途    redis-server  啟動 redis 伺服器   redis-cli  redis 客戶端(client)操作   redis-benchmark  性能測試工具，可以在本機電腦運行，看看本機效能如何 (服務啟動起來後執行)   redis-check-aof  AOF 持久化文件檢測和修復工具   redis-check-rdb  RDB 持久化文件檢測和修復工具   redis-sentinel  啟動哨兵模式 (Redis Cluster 使用)  其他安裝   apt 安裝     # lsb: Linux Standard Base縮寫，用來顯示LSB和特定版本的相關訊息\n   # 某些最小環境的情況下需要安裝，例如在Docker Container內部\n   sudo   apt   install   lsb-release\n   \n   # 將repository加入到apt index中，後續進行更新和安裝\n   curl   -fsSL   https://packages.redis.io/gpg   |   sudo   gpg   --dearmor   -o   /usr/share/keyrings/redis-archive-keyring.gpg\n   echo   \"deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(  lsb_release   -cs  ) main\"   |   sudo   tee   /etc/apt/sources.list.d/redis.list\n   \n   # 更新apt-get\n   sudo   apt-get   update\n   \n   # 安裝redis\n   sudo   apt-get   install   redis\n   snap 安裝     # 查詢redis名稱\n   snap   find   redis\n   \n   # 查看redis的資訊\n   snap   info   redis\n  Windows 實體安裝  Redis 主要支援在 Linux 環境底下，因此在 Windows 環境底下支援性很差。\n目前有兩個非官方的使用載點可以參考。    網友版本 - 5.0.14.1 ，目前還有在維護   微軟版本 - 3.0.504 ，目前已經不在維護。  Docker 安裝   Redis Server     docker   run   --name   redis   -d   -p   6379:6379   redis:6.2.4-alpine\n  Redis CLI     # 這邊的IP 192.168.23.192，替換為主機的IP\n   docker   run   -it   --rm   redis   redis-cli   -h   192.168.23.192   -p   6379\n  更多 Docker 使用方式參考  Redis Official  常用操作工具    RedisInsight  redis-cli  官方推薦  RedisInsight 一款 Web base 的工具，個人習慣使用  redis-cli 和  RedisInsight 一起使用。  由於大家一起使用 RedisInsight，為了方便查看 client 端是誰，這邊我們設定 Redis Client 的使用者名稱     # 設定client的名稱，將darren替換成你自己的名子\n   CLIENT   SETNAME   darren\n   \n   # 查看目前有哪些連線的client\n   CLIENT   LIST\n  常用的資料結構  Strings    String 是  Redis 最基本的類型，可以理解成與  Memcached 一模一樣的類型，一個  Key 對應一個  Value   String 類型是二進制(Binary)安全的。意味著  Redis 的  String 可以包含資料。比如 jpg 圖片或者序列化的物件   String 類型是  Redis 最基本的資料類型，一個 Redis 中 Value 最多可以是 512M  原子性\n   所謂原子性操作，是指不會被其他執行緒的呼叫機制打斷的操作，這種操作一但開始，就一直運行到結束，中間不會有任何的 context switch (切換到另外一個執行緒)\n   在  單執行緒 中，能夠在單條指令中完成的操作都可以認為是”原子操作”，因為中斷只能發生於指令之間  在  多執行緒 中，不能被其它執行緒打斷的操作，就叫”原子操作”  常用指令  set / get / append / strlen / setnx     # 增加key: k1, value: v1\n   >   set k1 v1\n   \n   # 更新key: k1, value: v11\n   >   set k1 v11\n   \n   # 取得value數值\n   >   get k1\n   \"v11\"\n   \n   # 從尾部後相加，回傳目前value整體的長度\n   >   append k1 __\n   (  integer  )   5\n   \n   # 取得append後的長度\n   >   get k1\n   \"v11__\"\n   \n   # 取得k1中的value長度\n   >   strlen k1\n   (  integer  )   5\n   \n   # 只有key不存在時，才會設置key的值\n   >   setnx k1 k0000\n   (  integer  )   0\n  incr / decr / incrby / decrby     # 增加key: k1, value: v1\n   >   set k1 v1\n   \n   # incr只支援integer\n   >   incr k1\n   (  error  )   ERR   value   is   not   an   integer   or   out   of   range\n   \n   # 替不存在的k2進行\"數字字串\"值加1\n   >   incr k2\n   (  integer  )   1\n   \n   # 替存在的k2進行\"數字字串\"加1\n   >   incr k2\n   (  integer  )   2\n   \n   # 替存在的k2進行\"數字字串\"減1\n   >   decr k2\n   (  integer  )   1\n   \n   # 替存在的k2進行\"數字字串\"減1\n   >   decr k2\n   (  integer  )   0\n   \n   # 替存在的k2進行\"數字字串\"減1\n   >   decr k2\n   (  integer  )   -1\n   \n   # 取得k2數值，記得他是\"數字字串\"\n   >   get k2\n   \"-1\"\n   \n   # 一次加上100的數值\n   >   incrby k2 100\n   (  integer  )   99\n   \n   # 一次減上100的數值\n   >   decrby k2 100\n   (  integer  )   -1\n  setex / getset     set   k1   v1\n   \n   # 設定過期時間\n   expire   k1   1000\n   \n   # 更新數值\n   set   k1   v2\n   \n   # 查詢數值\n   >   ttl k1\n   (  integer  )   -1\n   \n   # 將k1設定為100，且數值為v2 (重新設定則時間重新計算)，下面兩種方式皆可以\n   >   setex k1 100 v2\n   >   set k1 v2 EX 100\n   \n   # 傳入新增的v3參數，把舊的v2傳回 (與Java中的HashMap的Put功能相同)\n   >   getset k1 v3\n   \"v2\"\n  使用場景   快取(Cache)資料  計數器。\n   用來統計文章瀏覽次數  某些商品的點擊數量等等  分散式共享 Session  設定過期(Expire)來完成時效性的資料\n   手機驗證碼  使用者登入碼  分散式鎖  Hash   Redis hash 是一個鍵值集合  Redis hash 是一個 string 類型的  field 和  value 的映射表，hash 特別適合用於  儲存物件  類似 Java 中的  Map\u003CString, Object> ，  C# 中的  Dictionary\u003Cstring, object>  常用指令  hset / hget / hmset     # 設定假資料\n   >   hset h1 familyName Chang\n   >   hset h1 givenName EnShuo\n   >   hset h1 age 28\n   \n   # 取得hash中的數值\n   >   hget h1 familyName\n   \"Chang\"\n   >   hget h1 givenName\n   \"EnShuo\"\n   >   hget h1 test\n   (  nil  )\n   \n   # 使用批次來進行設定\n   >   hmset h2 familyName Chang givenName EnShuo age 28\n   OK\n  hexists / hkeys / hvals / hincrby / hsetnx     # 設定假資料\n   >   hset h1 familyName Chang givenName EnShuo age 28\n   \n   # 查看h1中是否存在field\n   >   hexists h1 age\n   (  integer  )   1\n   >   hexists h1 sex\n   (  integer  )   0\n   \n   # 取得所有的h1的field\n   >   hkeys h1\n   1  )   \"familyName\"\n   2  )   \"givenName\"\n   3  )   \"age\"\n   \n   # 取得所有的h1中的value\n   >   hvals h1\n   1  )   \"Chang\"\n   2  )   \"EnShuo\"\n   3  )   \"28\"\n   \n   # 替age加上3\n   >   hincrby h1 age 3\n   (  integer  )   31\n   \n   # 當age存在時，操作失敗\n   >   hsetnx h1 age 100\n   (  integer  )   0\n   \n   # 當sex不存在時，則新增\n   >   hsetnx h1 sex man\n   (  integer  )   1\n  使用場景   儲存使用者資訊，例如使用者名稱、密碼、電子郵件地址、手機號碼等。  儲存商品資訊，例如商品名稱、價格、庫存數量、圖片等。  儲存文章資訊，例如文章標題、作者、發布時間、內容等。  儲存設定資訊，例如資料庫連線字串、快取時間、超時時間等。  Lists   單一鍵值(Key)多個數值(Value)  是一個字串列表，按照插入順序排序。你可以添加一個元素到列表的  頭部(左邊)  或 尾部(右邊)  它的底層其實是  雙鏈接串列 ，對兩端的操作性能很高，通過索引下的操作中間的節點效能比較差  常用指令  lpush / rpush / lpop / rpop / rpoplpush     # 從key值l1左邊推送四筆資料，因此順序為v4 v3 v2 v1\n   >   lpush l1 v1 v2 v3 v4\n   (  integer  )   4\n   \n   # 從key值l1右邊推送三筆資料，因此順序為v4 v3 v2 v1 v5 v6 v7\n   >   rpush l1 v5 v6 v7\n   (  integer  )   7\n   \n   # 從左方取得數值\n   >   lpop l1\n   \"v4\"\n   \n   # 從右方取得數值\n   >   rpop l1\n   \"v7\"\n   \n   # 從左方key值l2推送v2、v3，因此順序為v3、v2\n   >   lpush l2 v2 v3\n   \n   # 從左方key值l3推送v1，因此順序為v1\n   >   lpush l3 v1\n   \n   # 從l2(key)右邊取值，從l3(key)左邊推入\n   # l2: v3\n   # l3: v2、v1\n   >   rpoplpush l2 l3\n   \n   # 從l3(key)取值，則取到v1\n   >   rpop l3\n   \"v1\"\n  lrange / lindex / llen     # 從key值l1左邊推送四筆資料，因此順序為v4 v3 v2 v1\n   >   lpush l1 v1 v2 v3 v4\n   (  integer  )   4\n   \n   # 取得0(索引)的第0個位置\n   >   lrange l1 0 0\n   1  )   \"v4\"\n   \n   # 取得1(索引)的第一位\n   >   lrange l1 1 2\n   1  )   \"v3\"\n   2  )   \"v2\"\n   \n   # 用反轉(負號)的方式獲取全部，從左到右一次全部取出\n   >   lrange l1 0 -1\n   1  )   \"v4\"\n   2  )   \"v3\"\n   3  )   \"v2\"\n   4  )   \"v1\"\n   \n   # 測試取值\n   >   lindex l1 0\n   \"v4\"\n   >   lindex l1 -1\n   \"v1\"\n   >   lindex l1 -10\n   (  nil  )\n   \n   # 取得長度\n   >   llen l1\n   (  integer  )   4\n  使用場景   實現佇列(Queue)：可以將新加入的元素插入到清單的末尾，將清單的第一個元素刪除即可實現佇列的先進先出 (FIFO) 的特性。  實現堆疊(Stack)：可以將新加入的元素插入到清單的末尾，將清單的最後一個元素刪除即可實現棧的後進先出 (LIFO) 的特性。  儲存日誌資訊：例如系統日誌、應用程式日誌等。  實現訊息佇列(Message Queue)：可以將訊息插入到清單的末尾，然後通過消費者(Consumer)不斷從清單的末尾獲取訊息來實現訊息的非同步處理。  Sets   Redis Set 對外提供的功能與  list 類似也是一個列表的功能，它特殊之處在於可以排除重複的數值，當如果要儲存的一個資料列表，不希望出現重複的資料時，set 是一個很好的選擇，且 set 提供 API 能判斷某個資料是否存在於集合中，而 list 沒有這種 API。  Redis 的 Set 是 string 類型的無序集合。它底層是一個 Hash table，因此  新增 、  刪除 、  查詢 的時間複雜度皆為  O(1)  常用指令  sadd / smembers / sismember     # 會自動將重複的數值進行過濾\n   >   sadd s1 v1 v1 v1 v2 v3 v4 v5 v5\n   (  integer  )   5\n   \n   # 顯示set的s1(key)的所有數值\n   >   smembers s1\n   1  )   \"v2\"\n   2  )   \"v1\"\n   3  )   \"v4\"\n   4  )   \"v3\"\n   5  )   \"v5\"\n   \n   # 判s1(key)中的v1是否存在\n   >   sismember s1 v1\n   (  integer  )   1\n   \n   # 判s1(key)中的v9是否存在\n   >   sismember s1 v9\n   (  integer  )   0\n  scard / srem / spop / srandmember     # 將數值進行加入\n   >   sadd s1 v1 v2 v3 v4 v5\n   (  integer  )   5\n   \n   # 取得s1(key)的數量\n   >   scard s1\n   (  integer  )   5\n   \n   # 刪除s1(key)中的v4\n   >   srem s1 v4\n   (  integer  )   1\n   \n   # 隨機從s1(key)中取出，並進行刪除\n   >   spop s1 2\n   1  )   \"v3\"\n   2  )   \"v2\"\n   \n   # 隨機從s1(key)中取出，並不刪除\n   >   srandmember s1 2\n   1  )   \"v5\"\n   2  )   \"v1\"\n  sinter / sunion / sdiff     # 新增兩個set，做為測試\n   >   sadd s1 v1 v2 v3 v4\n   >   sadd s2 v3 v4 v5 v6\n   \n   # 取交集\n   >   sinter s1 s2\n   1  )   \"v3\"\n   2  )   \"v4\"\n   \n   # 取聯集\n   >   sunion s1 s2\n   1  )   \"v4\"\n   2  )   \"v6\"\n   3  )   \"v3\"\n   4  )   \"v2\"\n   5  )   \"v1\"\n   6  )   \"v5\"\n   \n   # 取s1的差集\n   >   sdiff s1 s2\n   1  )   \"v1\"\n   2  )   \"v2\"\n   \n   # 取s2的差集\n   >   sdiff s2 s1\n   1  )   \"v5\"\n   2  )   \"v6\"\n  使用情境   實現抽獎功能，例如: 在一個抽獎活動中，可以使用 sets 將所有的參與者的訊息加入到集合中  統計網站拜訪過的 IP。將拜訪過的使用者 IP 進行加入到集合中，由於 set 可以防止重複訊息，因此最終可以顯示唯一。  追蹤聊天室的成員，並通過集合運算來管理成員的列表。  Sorted sets   Reids 有序集合 zset 與普通的 set 非常相似，也是一個沒有重複元素的字串集合。不同之處在於 zset 的  \u003Cvalue> 都關聯一個評分(score)，這個評分(score)被用來按照從低分到最高分的方式排序。集合的  \u003Cvalue> 是唯一的，但評分(score)可以是重複的。  因為元素是有序的，所以可以根據評分(score)或者次序(postion)來獲取一個範圍的元素。存取 zset 的中間元素也是非常快的，因此你能夠 zset 做為一個沒有重複  \u003Cvalue> 的智能列表。  常用指令  zadd / zrange / zrangebyscore / zrevrangebyscore     # 增加參數\n   >   zadd z1 100 chang 200 chen 150 huang\n   (  integer  )   3\n   \n   # 查看原本資料\n   >   zrange z1 0 -1\n   1  )   \"chang\"\n   2  )   \"huang\"\n   3  )   \"chen\"\n   \n   # 替huang的score增加51為150+51 = 201\n   >   zincrby z1 51 huang\n   \"201\"\n   \n   # 查看修改後的資料\n   >   zrange z1 0 -1 withscores\n   1  )   \"chang\"\n   2  )   \"100\"\n   3  )   \"chen\"\n   4  )   \"150\"\n   5  )   \"huang\"\n   6  )   \"201\"\n   \n   # 統計z1中的100~200分數之間有幾個\n   >   zcount z1 100 200\n   (  integer  )   2\n   \n   # 取得該\u003Ckey>集合中的排名，由0開始計算\n   >   zrank z1 chen\n   (  integer  )   1\n   \n   # 取不到數值時，則回傳(nil)\n   >   zrank z1 error\n   (  nil  )\n   \n   # 刪除該z1中的chen\n   >   zrem z1 chen\n   (  integer  )   1\n  使用情境   實現一個文章的閱讀排行榜  根據時間排序的新聞列表  直播聊天室中的送禮排行榜  其他常用功能  Transactions  介紹   Reids 事務是一個單獨的隔離操作，事務中的所有命令都會被序列化、按順序地執行。事務在執行的過程中，不會被其他客戶端發送來的命令請求給中斷。  Redis 事務的主要作用就是串聯多個命令防止別的命令插隊  Multi / Exec / discard   從輸入  Multi 指令開始，輸入的命令都會依次進入命令柱列中，但不會執行，直到輸入  Exec 後，Redis 會將之前的命令柱列中的命令依序執行  如果不想要這次的命令柱列，我們可以透過  discard 來進行放棄。  使用一個  Client A 和  Client B 來進行模擬操作     # Client A 開啟事務\n   >   Multi\n   OK\n   \n   # Client A執行操作\n   (  TX  )  >   set   k1   v1\n   QUEUED\n   \n   # Client A執行操作\n   (  TX  )  >   get   k1\n   QUEUED\n   \n   # Client B執行操作\n   >   set k1 v100\n   OK\n   \n   # Client A執行操作，可以發現將一連串的動作執行操作，不受Client B客戶端的操作\n   (  TX  )  >   Exec\n   1  ) OK\n   2  )   \"v1\"\n  將事務進行丟棄     # 開啟事務\n   >   Multi\n   OK\n   \n   # 設定k7數值，不小心打錯\n   (  TX  )  >   set   k7\n   (  error  )   ERR   wrong   number   of   arguments   for   'set'   command\n   \n   # 將本次事務進行丟棄\n   localhost:6379(TX  )  >   discard\n   OK\n  使用 watch / unwatch 指令 (樂觀鎖)    watch   在執行  multi 之前，先執行  watch \u003Ckey1> \u003Ckey2> ，可以監視一個或多個  \u003Ckey> ，如果事務執行之前，這個(或這些)key 的數值，被其他指令或他人改動過，那麼事務將被打斷。   unwatch   取消  WATCH 命令對所有 key 的監控  如果在執行  WATCH 命令之後，  EXEC 或  DISCARD 命令先被執行的話，就不需要在執行  UNWATCH 了  使用 Redis 中  Watch 的功能，這種方式就是  check-and-set 機制     # 初始化k1的數值為1 (Client A)\n   >   set k1 1\n   OK\n   \n   # 啟用監看key k1功能 (Client A)\n   >   WATCH k1\n   OK\n   \n   # 開啟事務交易 (Client A)\n   >   MULTI\n   OK\n   \n   # 此時有人修改k1內的數值為4 (Client B)\n   >   set k1 4\n   OK\n   \n   # 替k1的value進行+1 (Client A)\n   (  TX  )  >   incr   k1\n   QUEUED\n   \n   # 替k1的value進行+1 (Client A)\n   (  TX  )  >   incr   k1\n   QUEUED\n   \n   # 執行事務結果，可以發現執行是失敗的，沒有返回兩個結果 (Client A)\n   (  TX  )  >   exec\n   (  nil  )\n  三大特性   單獨的隔離操作\n   事務中所有命令都會序列化、按順序地執行。事務在執行的過程中，不會被其他客戶端發送來的命令請求打斷  沒有隔離級別的概念\n   柱列中的命令沒有提交之前都不會實際的被執行，因為事務提交前任何指令都不會被實際執行  不保證原子性\n   Redis 同一個事務中，如果有一條命令執行失敗了，其後面的命令仍然會被執行且沒有回滾(Rollback)  Pub/sub  使用上的限制   Pub/Sub 的內容，是不會用 Key 的方式去保存的，所以自然資料無法持久化，所以資料有可能會遺失。  當發送者(Pub)有發送消息時且訂閱者(Sub)連線中斷，則訂閱者將無法接收到此筆訊息  Redis Server 服務停止或當機，則資料會遺失  Redis 設定參數     # 32mb：緩衝區(buffer)一旦超過 32MB，Redis 直接將訂閱者(Sub)會強制踢    下線\n   # 8mb + 60：緩衝區超過 8MB，並且持續 60 秒，Redis 也會把訂閱(Sub)會    被踢下線。\n   client-output-buffer-limit   pubsub   32mb   8mb   60\n  使用上的限制  使用上的考量   由於 Pub/Sub 的內容，是不會進行保存的，所以重要的資料不建議使用此方式來傳遞  發佈者(Pub)發布消息後，並不會管訂閱者(Sub)是否有接收到消息，也沒有 ACK 機制，所以無法確定訂閱者(Sub)是否有接收到訊息，因此在可靠訊息的場合中不建議使用  訂閱者(Sub)會占用一個 Redis Server 的連線，所以要注意是否有佔用過多的連線  當發佈者(Pub)發送”大量”訊息，若訂閱者(Sub)來不及消化，資料會阻塞在通道(channel)中，阻塞時間越長，資料丟失的風險就越高，當訊息量過大時，會造成緩衝區(buffer)溢出，就會導致資流失  常用指令     # 查看全部的channel\n   >   PUBSUB CHANNELS\n   >   PUBSUB CHANNELS   *\n   \n   # 查看channel使用wild card\n   >   PUBSUB CHANNELS sss  *\n   \n   # 訂閱listen:something的頻道\n   SUBSCRIBE   listen:something\n   \n   # 發送資料給listen:something的頻道\n   PUBLISH   listen:something   hello\n   \n   # 解除訂閱listen:something的頻道\n   UNSUBSCRIBE   listen:something\n   \n   # 使用MONITOR來Debug模式查看目前送出狀態\n   MONITOR\n  使用情境   SignalR / WebSocket 在 K8s 底下多個 Pod 的廣播應用，  Redis backplane  在網頁應用程序中實現的即時聊天室。例如: 當有一個用戶在聊天室中發布了一條消息時，所有訂閱該消息的用戶都會收到通知。  常見的三大問題  Cache Penetration (快取穿透)  在高併發的情況下，查詢一個不存在的數值時會發生。例如: id 為   -1  或  特別大不存在的資料時 ，會造成 Cache 內的資料是找不到的，既然從 Cache 中找不到數值，因此大量的 Request 皆會落到資料庫上，造成資料庫負擔。  💡 請求未命中的 Cache，直接存取資料庫，這就是快取穿透。  ✅ 解決方式   在 Interface 層  增加驗證 ，比如使用者權限驗證、參數驗證，不合法的參數就直接 Return，比如: id 做基本的驗證，id ≤ 0 直接  return 。   快取NULL數值 ，但是 Cache NULL 的時間不能太長，否則 NULL 資料長時間得不到更新，也不能太短，否則達不到防止 Cache Penetration(快取穿透)  布隆過濾器(Bloom Filter) 類似 Hash table 的演算法，將所有可能的查詢生成一個 bitmap，在進行資料庫查詢之前會使用這個 bitmap 進行過濾，如果不在其中則直接過濾，從而減輕資料庫層面的壓力。  Cache Breakdown / Hotspot Invalid (快取擊穿)  在 Cache 中的一個 Key(比如一個促銷商品)，在某個時間點過期的時候，恰好這個時間點對這個 Key 有大量的高併發請求，這些請求發現 Cache 過期了，因此都從資料庫加載資料回 Cache，如果這個時間點有高併發請求則可能會瞬間造成資料庫壓垮。  💡 熱點 Key，Cache 過期，直接攻擊資料庫  ✅ 解決方式   設置熱資料(Hot data)永遠  不過期  使用加互斥鎖，對 Cache 查詢時加鎖，如果 Key 不存在就加鎖，然後查 DB 進入到 Cache 中，然後解鎖，其他執行緒發現有鎖就必須等待，然後解鎖後返回資料或到資料庫查詢  Cache Avalanche (快取雪崩)  大量的 Cache Key 在同一時間失效，導致大量的請求都落在資料庫上，如活動系統裡面同時進行著非常多的活動，但在某個時間點所有的 Cache 皆過期。   設置熱資料(Hot data)永遠  不過期  Cache 資料的過期時間設置為隨機，防止同一時間大量資料過期現象發生。  參考    關於 Redis Persistence - 資料持久化   Redis Pub/Sub   一分鐘學習 Redis 常見的三大問題  html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"id":104,"path":105,"dir":62,"title":106,"description":7,"keywords":107,"body":110},"content:2.workshop:redis-ring.md","/workshop/redis-ring","Redis Ring",[108,106,109],"Hash Function","Redis Ring 的缺點","  Hash Function  以下是簡單的 Hash Function 的公式：   XXX mod N = Y  (N: number of nodes, Y: node index)\n  這個公式可以用來計算 key 要存放在哪個 node 上。\n但是這個公式有個問題，就是當 N 增加時，會造成大量的資料重新分配，使資料不平均分配，所以 Redis Ring 使用   CRC16  作為 Hash Function，對於 CRC16 所產生的值範圍是 0 ~ 65535。  Redis Ring  每個 node 都是一個 Redis server，而 slot 是一個 0 ~ 16383 的數字，每個 node 負責一個 slot。  例如：node1 負責 slot 0 ~ 511，node2 負責 slot 512 ~ 1023，以此類推，直到 slot 分配到 16383 為止。  每個 key 都會被 Hash Function 轉換成一個 slot，並存放在對應的 node 上\n其分配方式如下：   slot = CRC16(key) mod 16384\n     # 依次序set key value 操作Redis Server\n   set book1 abc\n   set book2 def\n   set book3 ghi\n   set book4 jkl\n   set book5 mno\n   set book6 pqr\n   set book7 stu\n   \n   # Redis叢集資料Slot分流機制\n                             client\n                               |  (依上面次序進行set操作)\n                               V\n                     CRC16(key) mod 16384\n   --------------------------------------------------------------\n          Redis               Redis               Redis\n        (Master)            (Master)            (Master)\n       |        |           |       |           |       |\n       V        V           V       V           V       V\n     Redis    Redis       Redis    Redis       Redis    Redis\n   (Replica) (Replica)  (Replica) (Replica)  (Replica) (Replica)\n   --------------------------------------------------------------\n        book1=abc            book2=def             book3=ghi\n        book4=jkl            book5=mno             book6=pqr\n        book7=stu\n   Day27 Redis架構實戰-Redis叢集Slot分流機制  Redis Ring 的缺點   因為 Redis Ring 是一個同步的集群，所以當一個 node 過載時，會造成整個 Redis Ring 都會變慢。  且Redis Server集群採用Gossip協議實施無中心式叢集，如果超過1000個主節點時因為Gossip協議需要頻繁的訊息傳輸所以效率會變差，主要是因為網路頻寬的問題，故不建議部署超過1000個主節點  對於 Redis Ring 的資料分配，當資料量增加時，會造成資料分配不均勻，所以 Redis Ring 會造成資料分散在不同的 node 上，而不是在同一個 node 上。  所以根據上述問題 lua script 不能對不同的 node 執行，所以在 lua script 中，不能使用 Redis Ring 的特性。(不過可以使用key{hash}的方式來分配到同一個 node 上,但有傾斜的潛在問題，或是使用pipeline的方式來執行)  html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"id":112,"path":113,"dir":62,"title":114,"description":115,"keywords":116,"body":122},"content:2.workshop:tdd_part1.md","/workshop/tdd_part1","TDD技術分享 - Part1 灌輸","TDD為軟體開發一種手法，核心思維在於釐清功能情境後，從測試程式碼開始開發。與一般開發習慣有很大的不同。",[117,118,119,120,121],"1. 以範例初步體會TDD測試驅動開發","2. 使用ATDD設計好的故事設定","3. BDD神輔助","4. TDD Combo Hit","5. 天人合一","  TDD為軟體開發一種手法，核心思維在於釐清功能情境後，從測試程式碼開始開發。與一般開發習慣有很大的不同。  一開始會用例子情境直接去帶入理解TDD與一般開發不同之處，例子如下   情境 : 作為銀行客戶，我想要透過一個安全的登入系統登入我的帳戶，以便查看我的餘額和進行交易。  針對這個情境，需驗證登入合法性，條件如下   驗證正確的帳號和密碼可以成功登入。  驗證錯誤的帳號或密碼無法登入。  驗證三次登入失敗後帳戶將被鎖定。   註: 此例在此文章會被不停地拿來使用  1. 以範例初步體會TDD測試驅動開發  a. 一般情境開發  一般開發大都會從最基礎的資料基礎建設開始做開發，流程如下     DBContext : 撰寫DB Entity，建置完後做Migration產出DB Table  Repository : 設計Repository Layer，實作資料CRUD，隔離DBContext  Service : 實際撰寫登入密碼驗證邏輯與服務方法(通常會參考資料層實作)  API Interface : Controller層，實際撰寫In Out Bound部分  Test Code : 最後針對Service邏輯部分撰寫測試程式碼  此為根據需求從資料層建置到API介面層的一個開發流程，期間雖然會思考使用者情境邏輯，但通常到Test Code階段情境Case才會想得比較齊全。  但對於不寫測試程式碼的工程師就會懶於寫測試程式碼…此時程式會因需求變動或是除錯修改程式碼發生改A壞B或是對的東西改成錯的，在沒測試驗證邏輯保護下跑就會大概機率會出錯。  b. TDD簡易開發流程示意  TDD實際開發流程如下，會首要考慮情境去寫測試程式碼    關鍵在於的在Test Code到Service這段，這段實際細節流程如下    細部流程會是一個測試驗證與撰寫程式碼的重複流程，我們以驗證正確的帳號和密碼可以成功登入這個測試情境為例。一開始先產生測試程式碼(注:測試程式碼非Product Code)如下，此時直接跑測試一般會出現警告或是失敗，IDE測試結果會產生紅燈，此紅燈為測試的起手情境。接著我們要想辦法讓他變成綠燈，開始實際寫測式流程     [  TestMethod  ]\n   public   void   LoginWithCorrectUsernameAndPassword_ShouldBeSuccess  ()\n   {\n       \n   }\n  在撰寫測試程式，過程中我們一開始並不關心是否有真正的Concrete Class，會直接寫測試邏輯，例如下述這段驗證帳密正確性。寫這段測試時，實際沒有LoginService實體Class，因為我們著重於測試情境邏輯與流程，簡單來說我們一開始關心的是我們該如何寫完整性較高的測試”流程”，注重的是邏輯與流程，這會與一般先寫好再建置Concreate Class的思維非常不同。     [  TestMethod  ]\n   public   void   LoginWithCorrectUsernameAndPassword_ShouldBeSuccess  ()\n   {\n       LoginService   loginService   =   new   LoginService  ();\n       bool   result   =   loginService.  Validate  (  \"Spyua\"  ,   \"password123\"  );\n       Assert.  IsFalse  (result);\n   }\n  在沒有Concreate Class狀況下，IDE會檢測出沒有LoginService這個Concreate而標註紅線，此時通常會透過IDE熱鍵快速產生LoginService Class與Validate方法如下。在我們完成Concreate Class，可以刻意先不寫邏輯，讓他回傳false讓測試通過，當我們完成這段在執行測試時此時IDE就會Show綠燈。     public   class   LoginService\n   {\n       public   bool   Validate  (  string   username  ,   string   password  )\n       {\n           return   false  ;   // 刻意返回false讓測試通過\n       }\n   }\n  接著我們要開始做重構，重構在另一個小節我會描述他的精神，在這情境重構在於完善Validate邏輯部分，因此我們快速補一下示意邏輯，再做一次測試驗證會是綠燈。     public   class   LoginService\n   {\n       public   bool   Validate  (  string   username  ,   string   password  )\n       {\n           if  (username   ==   \"Spyua\"   &&   password   ==   \"password123\"  )\n               return   true  ;\n           return   false  ;\n       }\n   }\n  過了一遍這個流程，大致要帶出涵義在於TDD為測試流程，由測試不過(紅燈)到測試過(轉綠燈)，期間會不停地做測試，寫Product Code，測試，重構再次測試，直到你將Product Code完成。最後，會再做一次Review看是否要做重構(重新整理程式碼)。  此時你應該會小疑問，Service參考實作Repository怎麼沒有? 這後面會搭配如何使用測試Mock去寫這段，並做詳細說明。  2. 使用ATDD設計好的故事設定  上章節討論完後，對於TDD實作流程應該會有一定的認知。但你有發現到一件事情嗎? TDD是其實很貼近於開發角色，實際他是偏於底層的思維，對於較接近User的上層情境邏輯會有一段小落差外，另外就是實際開發過程，我們還是需要開發者能透徹了解實際的情境外，且能與User與PM達到共識，理想上整體的測試情境與條例都可讓三方理解。  而這整個過程就是要探討如何從User Requirement定義出User Story與測試條例過程，我們或稱做ATDD，英文縮寫為 Acceptance Test-Driven Development。  他會是TDD整個流程中最重要的一環 。歸根究柢，還是要透徹討論去理解使用這需求。就像寫小說一樣，有清楚的故事設定背景，才會有好的故事內容。   系統的存在目的，是為了滿足使用者需求，而不是給開發人員寫爽的。 (91)  ATDD探討會有幾個環節，分別為   從User Requirement定義出 User Story  根據User Story設計出Use case diagram並搭配Use case\ndescription  根據1,2 列出實際功能清單  定義出acceptance testing  而這四個環節，會不斷的與實際撰寫測試做循環驗證與修正  a. User Story Lightning Talk (參考91)  我們如果要將User Requirement用最簡單的方式讓大家都可以瞭解這個需求是什麼，通常會使用User Story描述技巧，它是敏捷開發中用來描述功能需求的一個簡單又實用的方法。透過User Story，開發者可以更容易理解使用者的需求和目標，並專注於創造有價值的解決方案。通常，User Story的制定格式會遵循以下結構：  中文版   作為（某個角色），我想要（某個功能或動作），以便（達到某個目的或效益）。  英文版   As a \"role\", I want \"goal/desire\" so that \"benefit\"     某個角色 ：使用該功能的人，可以是特定的使用者、系統管理員、客戶等。   某個功能或動作 ：這部分描述想要開發的具體功能或系統應該執行的動作。   達到某個目的或效益 ：這部分解釋了為什麼這個功能是有價值的，也就是說，它可以達到什麼特定的效益或目的。  以我們上述的登入系統情境，我們會這樣定義     作為 一個銀行客戶，\n   我希望 能夠透過一個安全的登入系統登入我的帳戶，\n   以便 查看我的餘額和進行交易，\n   並且我期望：\n   \n   如果 我輸入的帳號和密碼是正確的，那麼 我應該可以成功登入。\n   如果 我輸入的帳號或密碼是錯誤的，那麼 我不應該能夠登入。\n   如果 我連續三次登入失敗，那麼 我的帳戶應該被鎖定，以保護我的安全。\n  \n    小練習  情境描述  作為銀行客戶，我想要從我的一個帳戶轉帳到另一個帳戶，以便管理我的財務。  Acceptance Test Case:   驗證足夠餘額的轉帳是成功的。  驗證餘額不足的轉帳應該失敗。  驗證成功的轉帳應該在兩個帳戶間正確移動金額。  User Story     作為 一個銀行客戶，\n   我希望 能夠從我的一個帳戶轉帳到另一個帳戶，\n   以便 管理我的財務，\n   同時確保：\n   \n   如果 我的帳戶有足夠的餘額，那麼 轉帳應該成功。\n   如果 我的帳戶餘額不足，那麼 轉帳應該失敗。\n   如果 轉帳成功，那麼 兩個帳戶間的金額應該正確移動。\n  I. User Story 討論  上述是直接定義出User Story的情境結果，一般我們會使用敏捷開發討論User Story，在敏捷開發中，User Story的定義和討論是一個持續的過程，涉及多個階段和參與者(PO、PG…)。  \n    細部實際討論做法簡易描述    撰寫User Story ：在便條紙或數字化的看板上寫下User Story的基本描述，包括角色、功能和效益。   定義Acceptance Criteria ：測試人員和開發人員共同參與的一個重要環節。Acceptance Criteria（或Acceptance Test Case）明確界定了User Story完成的條件。這些條件可以在同一張便條紙的下方寫出，也可以單獨記錄貼在同一個區塊。   User Story討論會議 ：開發團隊、產品擁有者、測試人員和其他利益相關者所有人將共同討論User Story的細節，包括Acceptance Criteria。這個過程有助於確保每個人都理解需求並同意測試標準。   估算和排程 ：團隊可能還會為User Story進行大小估算和排程。使用像是點數費式數列這樣的估算方法，並將User Story分配到特定的開發迭代或冲刺中。   持續溝通和澄清 ：隨著開發的進展，可能還需要進一步的討論和澄清。涉及更多的會議或非正式的交流。  細部討論細節歸類大原則上Follow 3C原則，也就是Card、Conversation與Confirmation  \n    3C原則    Card（卡片） ：\n-   目的 ：卡片作為一個描述溝通媒介，用於紀錄User Story的核心概念。\n-   內容 ：通常包括角色、功能和效益。例如，“作為銀行客戶，我希望能夠透過一個安全的登入系統登入我的帳戶，以便查看我的餘額和進行交易。”\n-   特點 ：卡片應該簡短明了，就像Lighting Talk，短時間提供足夠的信息來引發更深入的討論。   Conversation（對話） ：\n-   目的 ：透過團隊之間的對話來深入理解User Story的細節，並解決任何歧義和不清楚的地方。\n-   內容 ：包括討論Acceptance Criteria、技術細節、依賴關係等。\n-   特點 ：對話可以在正式的計畫會議中進行，也可以是隨時發生的非正式討論。   Confirmation（確認） ：\n-   目的 ：通過明確的Acceptance Criteria確定User Story的完成條件，確保所有利益相關者對需求和期望有共同理解。\n-   內容 ：明確定義何時User Story被認為是“完成”的，例如，正確的帳號和密碼可以成功登入等。\n-   特點 ：這部分通常涉及測試人員、開發人員和產品擁有者的密切合作，以確保所有人對接受標準有共同理解。  這三個元素共同構成了完整的User Story，支援敏捷開發過程中的協作和溝通。卡片是引發對話的起點，對話是深入理解需求的過程，確認則是確保所有人對完成標準達成共識的過程。  II. User Case Digram  在討論過程中，我們會使用Digram來輔助討論，可以明確看出系統邊界、角色關係與案例，一般會Follow 5W準則(Who、Where、What、When與Why)。如我們此章節提到的登入系統情境例子，它的User Case Digram可能會長的如下    圖中的pre與post為pre-condition（前置條件）和post-condition（後置條件）的角色。    Pre-condition（前置條件） ：   描述了必須先滿足的條件，用例才能夠被執行。  例如，客戶必須輸入正確的帳號和密碼。   Post-condition（後置條件） ：   描述了用例執行成功後系統的狀態。  例如，客戶可以成功登入，並能夠訪問其帳戶以查看餘額和進行交易  III. Acceptance testing  Acceptance testing為接收測試描述，它一樣是在討論過程中會去定義出的驗收標準描述。用來說明某一個user story，系統在特定情況下應該完成什麼樣功能，以及針對某一些輸入，應該具備怎樣的輸出結果。從從使用者的角度來檢視，系統是否能正常運作（是否符合使用者的期望)。通常以自然語言撰寫。  描述內容盡量把握幾個準則 (91)   使用者為owner(PO)來主導撰寫 : 使用者為最後的驗收人員，所以使用者最有資格、也最應該來撰寫驗收測試。讓使用者來主導，而由有經驗的測試人員與開發人員輔助撰寫測試案  重點在What，而不是How : 驗收測試案例的重點在於該有什麼樣的功能，用什麼樣的方式，能代表滿足這個user story。如何做到細節就不用過於描述。  簡潔、準確、避免無異議的描述 : 例如我們提到的登入例子，以ATDD接受測試描述就會如下   驗證正確的帳號和密碼可以成功登入。  驗證錯誤的帳號或密碼無法登入。  驗證三次登入失敗後帳戶將被鎖定。  寫過測試經驗的開發者應該會意識到，這測試描述其實還需要轉譯成測試3A撰寫描述，這就是我們下章節會提到BDD章節，回提到關於DSL(Domain Specific Lanaguage)語言描述，DSL就會較貼近實際的測試撰寫情境。  3. BDD神輔助  這章節用意在於要帶出使用DSL語法描述測試條例並使用Net Specflow Tool協助我們產生測試程式碼，來輔助我們TDD整個開發過程。BDD英文全名為Behavior-driven development，由使用者行為情境討論來做開發起始點，過程TDD這章節不加以詳述，我們要探討的是DSL(Domain Specific Lanaguage)語言描述。  a. Gherkin 3A  DSL(Domain Specific Lanaguage)語言描述為了解決特定領域的問題而設計的，例如我們常看到的SQL與HTML則就是針對資料庫領域與網頁領域而設計的語法。  相對於BDD開發，也有針對此領域而設計的語言叫   Gherkin ，使用 \"Given-When-Then\" 的結構去描述軟件功能的期望行為。例如我們提到的安全登入例子，以Gherkin描述就會如下     Feature: 安全登入\n   \n   Scenario: 正確的帳號和密碼\n     Given 一個有效的帳號和密碼\n     When 用戶嘗試登入\n     Then 登入應成功\n   \n   Scenario: 錯誤的帳號或密碼\n     Given 一個無效的帳號或密碼\n     When 用戶嘗試登入\n     Then 登入應失敗\n   \n   Scenario: 三次登入失敗\n     Given 用戶已經嘗試登入兩次並失敗\n     When 用戶再次嘗試登入並失敗\n     Then 帳戶應被鎖定\n  相對於我們上述提到的Acceptance testing 描述，會更接近於Test的3A架構，3A即是Arrange、Act與Assert。在寫測試段，一個測試會有這三個流程階段。如果我們轉成3A描述則會如下   Scenario: 正確的帳號和密碼    Arrange（安排） ：設置測試的初始條件。   Given 一個有效的帳號和密碼 。   Act（執行） ：執行要測試的行為。   When 用戶嘗試登入 。   Assert（斷言） ：驗證系統的實際行為是否符合預期。   Then 登入應成功 。   Scenario: 錯誤的帳號或密碼    Arrange（安排） ：   Given 一個無效的帳號或密碼 。   Act（執行） ：   When 用戶嘗試登入 。   Assert（斷言） ：   Then 登入應失敗 。   Scenario: 三次登入失敗    Arrange（安排） ：   Given 用戶已經嘗試登入兩次並失敗 。   Act（執行） ：   When 用戶再次嘗試登入並失敗 。   Assert（斷言） ：   Then 帳戶應被鎖定 。  實際的測試程式碼會如下(非Specflow產生)     using   NUnit  .  Framework  ;\n   \n   [  TestFixture  ]\n   public   class   LoginTests\n   {\n       private   LoginService   _loginService  ;\n   \n       [  SetUp  ]\n       public   void   Setup  ()\n       {\n           _loginService   =   new   LoginService  ();   // Arrange: 初始化測試環境\n       }\n   \n       [  Test  ]\n       public   void   LoginWithValidCredentials_ShouldSucceed  ()\n       {\n           // Arrange: 設置測試的初始條件\n           _loginService.  SetCredentials  (  \"validUsername\"  ,   \"validPassword\"  );\n   \n           // Act: 執行要測試的行為\n           bool   result   =   _loginService.  AttemptLogin  ();\n   \n           // Assert: 驗證系統的實際行為是否符合預期\n           Assert.  IsTrue  (result);\n       }\n   \n       [  Test  ]\n       public   void   LoginWithInvalidCredentials_ShouldFail  ()\n       {\n           // Arrange\n           _loginService.  SetCredentials  (  \"invalidUsername\"  ,   \"invalidPassword\"  );\n   \n           // Act\n           bool   result   =   _loginService.  AttemptLogin  ();\n   \n           // Assert\n           Assert.  IsFalse  (result);\n       }\n   \n       [  Test  ]\n       public   void   LoginWithThreeFailedAttempts_ShouldLockAccount  ()\n       {\n           // Arrange\n           _loginService.  SetCredentials  (  \"invalidUsername\"  ,   \"invalidPassword\"  );\n   \n           // Act\n           _loginService.  AttemptLogin  ();\n           _loginService.  AttemptLogin  ();\n           _loginService.  AttemptLogin  ();\n   \n           // Assert\n           Assert.  IsTrue  (_loginService.  IsAccountLocked  ());\n       }\n   }\n  b. Specflow Tool (  官方文件 )  I. 安裝  Step1 : 至VS IDE延伸模組安裝 SpecFlow    Step2 :  新增Nunit專案  Step3 : Nunit專案安裝 SpecFlow.NUnit    II. 撰寫Feature檔案並撰寫內容  先在Nunit專案新增feature file，如果延伸套件有裝，應該可以在新增選項看到Specflow項目    新增完後，接著我們根據3.a提到的Gherkin 描述範例填到feature file內     Feature: 安全登入\n   \n   Scenario: 正確的帳號和密碼\n       Given 一個有效的帳號和密碼\n       When 用戶嘗試登入\n       Then 登入應成功\n   \n   Scenario: 錯誤的帳號或密碼\n       Given 一個無效的帳號或密碼\n       When 用戶嘗試登入\n       Then 登入應失敗\n   \n   Scenario: 二次登入失敗\n       Given 用戶已經嘗試登入兩次並失敗\n       When 用戶再次嘗試登入並失敗\n       Then 帳戶應被鎖定\n  III. 產生測試Code  撰寫完feature檔案後，在feature內文還沒產生對應的測試Code，他會是一個紫色HL的狀態，如下圖    此時我們針對檔案內文點選右鍵(任何一處都可)，點選Define Step    他會產生一個選擇表如下，基本上都是全選    你就會看到測試程式碼被產生，而且是整個對應你的feature file (Testing Item Board點下去會指到feature file不是Test  cs file)    IV. (重要)修改feature  這小節稍微紀錄一下，如果我們要改需求，勢必會動到feature file，此時會需要同步更新所產生的Testing Code。理想上基本上更動是要整個連動，但實際上這工具目前版本使用起來沒這麼直覺。  這邊給個上述case如下     Scenario: 二次登入失敗\n       Given 用戶已經嘗試登入兩次並失敗\n       When 用戶再次嘗試登入並失敗\n       Then 帳戶應被鎖定\n  原本是2次登入失敗，如果我們想改描述變成3次登入，  此時不能直接改文字描述部分 ，你必須對Given按右鍵，點選Rename Steps，跳出下面視窗修改，才能同步改道所產生的Testing Code，  但僅止於Attribute描述部分，Testing Function Name要自己重新命名    另外如果點擊後沒反應，只需要對解決方案按右鍵並做重新編輯即可   Specflow讓PO、User與PG專注在feature file的異動，且三方都看得懂實際的測試項目，相對於一般3A的測試Code撰寫可讀性會在更接近自然語言  4. TDD Combo Hit  討論完Specflow部分，我們試著完成整個測試，請先建置你的Product Project，結構如下    a. Run TDD (  Sample Code )  繼上述由Specflow產生的Scenario，針對正確的帳號和密碼情境參考完善測試Code如下 (PS : 測試專案需安裝FluentAssertions) ，     public   class   安全登入StepDefinitions\n   {\n       private   LoginService   _loginService  ;   // 假設有一個登入服務\n       private   string   _username  ;\n       private   string   _password  ;\n       private   bool   _loginResult  ;\n   \n       public   安全登入StepDefinitions  ()\n       {\n           _loginService   =   new   LoginService  ();   // 初始化登入服務\n           _username   =   string  .Empty;\n           _password   =   string  .Empty;\n           _loginResult   =   false  ;\n       }\n   \n       [  Given  (  @\"一個有效的帳號和密碼\"  )]\n       public   void   Given一個有效的帳號和密碼  ()\n       {\n           _username   =   \"validUser\"  ;\n           _password   =   \"validPassword\"  ;\n       }\n   \n       [  When  (  @\"用戶嘗試登入\"  )]\n       public   void   When用戶嘗試登入  ()\n       {\n           _loginResult   =   _loginService.  Login  (_username, _password);\n       }\n   \n       [  Then  (  @\"登入應成功\"  )]\n       public   void   Then登入應成功  ()\n       {\n           _loginResult.  Should  ().  BeTrue  ();\n       }\n   }\n  在一開始我們並沒有LoginService這個物件，因此IDE會反紅，此時直接使用IDE的Quick Action，點選產生新的類型    專案點選Production WebApi Project，並選擇建立檔案，指定到Service資料夾下    接著對Login紅色HL部分，一樣使用IDE的Quick Action快速產生Method    此時執行測是會是紅燈，錯誤會顯示The method or operation is not implemented，我們下手去改這段Login Code如下，造著邏輯寫，Login流程在於針對username撈取用戶資訊後，做Password比對，過程中透過Quick Action去產生IUserRepoitory 介面以及User Entity物件。  \n    Code     public   class   LoginService\n   {\n       private   IUserRepoitory   _userRepoitory  ;\n   \n       public   LoginService  (  IUserRepoitory   userRepoitory  )\n       {\n           _userRepoitory   =   userRepoitory;\n       }\n   \n       public   bool   Login  (  string   username  ,   string   password  )\n       {\n           User   user   =   _userRepoitory.  GetUser  (username);\n           \n           if  (user   ==   null   ||   !  user.Password.  Equals  (password) )\n           {\n               return   false  ;\n           }\n   \n           return   true  ;\n       }\n   }\n   \n   public   class   User\n   {\n       public   string   Password   {   get  ;    set  ; }\n       public   string   UserName   {   get  ;    set  ; }\n   }\n  Production寫好後，接著我們要下手修改程式碼，要針對IUserRepoitory 做Stub替身物件，此時我們需要安裝NSubstitute，Stub替身物件設置如下     private   IUserRepoitory   _userRepoitory  ;   // 假設有一個使用者資料庫\n   private   LoginService   _loginService  ; \n   private   string   _username  ;\n   private   string   _password  ;\n   private   bool   _loginResult  ;\n   \n   public   安全登入StepDefinitions  ()\n   {\n       _userRepoitory   =   Substitute.  For  \u003C  IUserRepoitory  >();   //替身設置\n       _loginService   =   new   LoginService  (_userRepoitory); \n       _username   =   string  .Empty;\n       _password   =   string  .Empty;\n       _loginResult   =   false  ;\n   }\n  Stub替身物件的存在在於模擬原物件的回傳值，在我們這個Case，我們需要透過替身物件模擬資料庫的回傳，以正確的帳號和密碼這個情境，我們假設回傳Password為我們Given所設置的Password如下     [  When  (  @\"用戶嘗試登入\"  )]\n   public   void   When用戶嘗試登入  ()\n   {\n           // 替身物件假設回傳正確的User訊息\n       _userRepoitory.  GetUser  (_username).  Returns  (  new   User   { UserName   =   _username, Password   =   _password });\n       _loginResult   =   _loginService.  Login  (_username, _password);\n   }\n  然後我們執行測試就會進入綠燈狀態了~~~  在綠燈狀態後，我們再回到原本的LoginService程式碼，看一下有沒有哪部分需要做Refactor的，如果以可讀性來說，其實我們可以把if的判斷內容使用 Extract Method提煉出一個Invalid Method，一樣我們快速使用IDE的Qucik Action快速做重構，重構後如下，此時會看到Login Method Invalid那段的可讀性相對於之前就高了許多     public   bool   Login  (  string   username  ,   string   password  )\n   {\n       User   user   =   _userRepoitory.  GetUser  (username);\n   \n       if   (  InvalidPassword  (password, user))\n       {\n           return   false  ;\n       }\n   \n       return   true  ;\n   }\n   \n   private   static   bool   InvalidPassword  (  string   password  ,   User   user  )\n   {\n       return   user   ==   null   ||   !  user.Password.  Equals  (password);\n   }\n  b. 探討測試與重構  TDD過程中，對於如何寫測試與如何重構需要有一定的實作認知，這邊稍微概略提一下過程中較常使用到的技巧  I. 關於單元測試  TDD中的Test指的就是單元測試(Unit Test)，單元測試重點在於寫Product Code邏輯段的驗證程式碼。如我們提到登入密碼驗證的例子，密碼驗證就是一個判斷，只要程式碼有關係到一些邏輯判斷與運算判斷。基本上在開發都需要寫單元測試去保護我們的Product Code。就算你不是以TDD流程去做開發，直接從Product Code開發，一樣要補測試程式碼，這樣才是叫完善的開發考慮。通常會有幾個注意要點   如何寫測試  最基礎就是如何使用測試框架，Net大多使用Nunit，Java為JUnit。該如何使用此測試框架去寫測試為TDD最基本要求，如何寫這部分可以參考  Nunit 與  JUnit 的教學參考。關鍵概念有   如何產生測試專案  如何撰寫測試程式碼  有哪些常用測試工具套件輔助  如何偵測測試覆蓋率    測試程式碼是否容易閱讀，越貼近自然語言越好  例如我們有一個測試情境為一個購物車購物後結算金額需正確，我們先來看一段測試程式如下，這段程式碼基本上只有工程師看得懂，離簡易的自然語言還有一段距離     [  TestMethod  ]\n   public   void   test_total_should_be_the_sum_of_all_subtotals_plus_shpping_fee  ()\n   {\n       #  region   Given\n       //the cart has 5 Erasers\n       ShoppingCart   cart   =   new   ShoppingCart  ();\n       cart.  AddShoppingItem  (  new   BaseShoppingModel  ()\n       {\n           Name   =   \"Erasers\"  ,\n           UnitPrice   =   10  ,\n           Qty   =   5  ,\n           MaxPurchaseQty   =   10  ,\n       });\n   \n       #  endregion\n   \n       #  region   When\n       //the customer adds 10 pencils to the cart\n       cart.  AddShoppingItem  (  new   BaseShoppingModel  () \n       { \n           Name  =  \"Pencil\"  ,\n           UnitPrice   =   20  ,\n           Qty   =   10  ,\n           MaxPurchaseQty   =   10  ,\n       });\n       #  endregion\n   \n       #  region   Then            \n       int   shoppingFee   =   cart.  GetTotalShoppingFee  ();\n       Assert.  AreEqual  (shoppingFee,   310  );\n       #  endregion\n   \n   }\n  我們看一下另一段測試程式碼如下，這段程式碼驗證情境與上述一樣，當相對起來他就非常淺顯易懂，很明確看出Given就是在做構物情境，When為將物品放置購物車，Then則是做結算判斷。  所以不只構物Method命名須看出測試Case外，整個測試內容流程盡量能接近自然語言一目了然最好     [  SetUp  ]\n   public   void   Setup  ()\n   {\n       Cart   =   new   Cart  ();\n       Erasier   =   new   CardItem  (  name  :   \"Erasiers\"  ,   unitPrice  :   10  ,   maxPurchaseQty  :   10  );\n       Pencial   =   new   CardItem  (  name  :   \"Pencial\"  ,   unitPrice  :   20  ,   maxPurchaseQty  :   10  );\n       BluePen   =   new   CardItem  (  name  :   \"BluePen\"  ,   unitPrice  :   30  ,   maxPurchaseQty  :   10  );\n       Ruler   =   new   CardItem  (  name  :   \"Ruler\"  ,   unitPrice  :   30  ,   maxPurchaseQty  :   10  );\n       Notebook   =   new   CardItem  (  name  :   \"Notebook\"  ,   unitPrice  :   50  ,   maxPurchaseQty  :   5  );\n       PencilSharpener   =   new   CardItem  (  name  :   \"PencilSharpener\"  ,   unitPrice  :   50  ,   maxPurchaseQty  :   5  );\n   }\n   \n   [  Test  ]\n   public   void   Test_total_should_be_the_sum_of_all_subtotals_plus_shipping_fee  ()\n   {\n       // Given \n       Erasier.  Order  (  5  );\n       Pencial.  Order  (  5  );\n   \n       // When\n       Add_To_Cart  (Erasier, Pencial);\n   \n       // Then\n       Cart.TotalPrice.  Should  ().  Be  (  150  );\n   }\n   根據情境善用測試假物件  在我們寫單元測試過程中，測試的Method如果有參考到一些其他物件，他並不一定會與要測試的邏輯區段有關係，又或是需要模擬實際資料基礎建設層撈取資料的動作，但又不直接與DB連線，此時我們就會需要製作假物件注入到我們要測試的物件。  這些假物件類別分為Dummy 、Stub、Spy、Mock、Fake，如要了解可參考Teddy文章(  點我 )。  II. 關於重構  在TDD過程，我們由紅轉綠燈後，接著就會進入重構階段。重構目的在於    提高可讀性：  讓程式碼更容易被人理解。   消除重複：  移除重複的代碼，使得未來的修改更容易。   提高可維護性：  使程式更容易修改和擴展。   改善設計：  使程式結構更合理，更符合設計原則。  而重構發生情境通常會在除錯、壞味道產生與測試時會去做這件事情，而TDD就是要寫測試，所以基本上一定會遇到重構這個環節。在TDD我們會常用的重構技巧有    提取方法（Extract Method）：  如果一個方法太長或太複雜，你可以將部分代碼提取到新的方法中，使主方法更容易理解。   合併重複的代碼（Remove Duplicated Code）：  找到並消除重複的代碼，通常可以提取到共用的方法或類中。   重新命名（Rename）：  通過更改變量、方法或類的名稱使之更具描述性，以提高代碼的可讀性。   移動特性（Move Feature）：  重新安排方法或屬性的位置，使其與相關的功能更接近，例如將方法從一個類移動到另一個更相關的類中。  利如我們Run TDD小節就有使用Extract Method將if區段的程式碼提起出一個Invaild的方法。基本上重構也是一門很深的學問，有興趣可以去看這重構小藍書(連結點我)。但重構的上手很吃實際使用經驗與情境演練，純看理論會很難消化。另外針對TDD重構有一個重構九式思考脈絡，可參考此篇(點我)。此九式會帶出整個重構重要的環節脈絡，包含如何做好隔離相依這件事情。  III. 重構急速開發  TDD要進入心流狀態，我們需要配合熱鍵的使用。心流的意思是進入全專注思考開發，過程全程透過鍵盤來產生程式碼，熱鍵達成要求包括   如何快速找到要修改的程式碼區段 (導航搜索)  如何快速生成物件檔案到指定路徑  如何快速重構 (程式碼重構)  這些都需要配合工具與熱鍵設置的搭配，例如JetBrains的Resharper，或是Rider與IntelJ IDE，都有許多導航搜索與重構的快速設置，能提高開發人員的生產力，讓開發人員在開發過程中不會因為需思考要點選畫面哪個工具區塊而產生思考斷點。這也會是TDD開發要點環節之一(但優先權不會是最前面)。Part2 則會為這一塊做一個詳細的設置解說與情境演練。  c. 關於工具  這個章節稍微做個工具整理   C#  Nunit : 測試框架  NSubstitute : 替身物件工具  FluentAssertions : 編寫更具可讀性和更富表達力的單元測試斷言  Specflow :    Gherkin 編寫與測試程式碼產生工具  Resharper : 開發助手工具，提供開發人員更快速的編輯功能  Java  JUnit : 測試框架  Mockito : 替身物件工具  AssertJ : 編寫更具可讀性和更富表達力的單元測試斷言  Cucumber  :    Gherkin 編寫與測試程式碼產生工具  IntelJ : Java開發IDE，本身也提供開發人員更快速的編輯功能  5. 天人合一  a. 技能樹  基本上TDD為一個開發流程，而過程中的思維與技術包含面很廣，對於一開始軟體新手很難入心。  硬技術層面包含   如何撰寫測試及如何撰寫好的測試  好的軟功基礎，OO與  SOLID 基本，需有Clean Code基礎且有一定的Pattern應用經驗，更進階到架構層面的設計  重構，這會建置在好的軟功基礎，且對DI概念與介面隔離相依都要有一定的概念  熱鍵Gen Code & Refactor使用，效益度這會建置在前三點基礎上，此點才可發揮出最大效果  軟技術層面須對V-Model有一定的認知，此為軟體開發流程模型，強調過程中的測試活動。此章節沒提，因為包含範圍也很大。之中包含需求分析、系統設計、架構設計和單元設計。這些階段是按順序進行的，每個階段的輸出成為下一個階段的輸入。然後對應到單元測試、集成測試、系統測試和驗收測試。每個環節都有其一定的專業度，需有一定的了解，才會讓整個討論設計過程進入狀況。  TDD雖然是一個流程形式的開發，有點像是一套實際可以打出有傷害力的技能，但這技能的強度可以打出多高的傷害，跟角色本身素質及被動技(軟硬實力)能有一定的相依性，再搭配裝備()(工具使用)的加成打出更高的傷害。所以他牽扯的環節會蠻多，使用模仿不難，要融會貫通整個開悟理解需要有一定的基礎。  b. TDD、BDD與DDD  除了TDD，還有其他開發技巧叫BDD與DDD，BDD我們章節有提到，大致可以感受到Acceptance testing與**Gherkin的感受度差異。基本上不太可能用純TDD開發，因為TDD只是開發中實作的一個環節，他比較偏向底層開發思維，會較靠近開發者。所以我們需要理解較高層的使用情境，這樣才可以讓底層較貼近於實際的使用者情境。此時我們就可以以BDD行為驅動開發的探討去輔助。但如果更複雜的情境且有複雜的Context互動，此時通常會以服務架構角度去開發系統，Domain物件要設計得好，才能在複雜的Context互動有一定的解偶，在這狀況我們就會以DDD角度去開發，配合領域專家讓Domain Model能定義更明確。**大致理解之間的脈絡相依性後，會有種融會貫通的感覺，針對情境去做切換設計使用會更得心應手。  順便一提DDD與整潔架構有很高的相依性，要設計得好對於圓形的整潔架構要有一定的認知。  html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"id":124,"path":125,"dir":126,"title":127,"description":7,"keywords":128,"body":135},"content:3.web-api:chapter2.md","/web-api/chapter2","web-api","第2章 EndPoint設計與請求形式",[129,130,131,132,133,134],"2.1 設計通過API公開的功能","2.2 API EndPoint的設計思想","2.3 HTTP方法和EndPoint","2.4 API端點的設計","2.5 搜尋與查詢參數的設計","關於登錄OAuth2.0 (2.6 ~ 2.10)","  2.1 設計通過API公開的功能  SNS在線服務功能   使用者註冊  登入  獲取自己的資訊  更新自己的資訊  取得使用者資訊  搜尋使用者  添加好友  刪除好友  取得好友列表  搜尋好友  發送消息  取得好友的訊息  編輯訊息  刪除訊息  好友動態列表  特定用的動態列表  發表動態訊息  編輯動態訊息  刪除動態訊息  2.2 API EndPoint的設計思想  EndPoint的基本設計  怎麼設計一個優秀的URI，有一個重要的原則   容易記憶，URI包含的功能一目了然  💡  容易記憶的原則如下   短小便於輸入的URI  人可以讀懂的URI  沒有大小寫混用的URI  修改方便的URI  不會曝露伺服器架構的URI  規則統一的URI   短小便於輸入的URI  💀 反例     http://api.example.com/service/api/search\n  💡 正例     http://api.example.com/search\n  ✅ 結論   將URI使用短小、簡單的方式進行表達，更易於理解和記憶。  人可以讀懂的URI  💀 反例     http://api.example.com/sv/u\n  💡 正例     http://api.example.com/products/12345\n  📦 補充  API的設計查詢時，該使用  search 還是  find 詞彙?  通常使用  search 來表示，  search 表示在某個地方尋找，而  find 則是尋找某個特定物品。  ✅ 結論   盡量少用縮寫，適當的使用完整的英文單字來表示。  沒有大小寫混用的URI  💀 反例     http://api.example.com/Users/12345\n   http://example.com/API/getUserName\n  💡 正例     http://api.example.com/users/12345\n  📦 補充  如果遇到兩種大小寫的URI進行混用時，應該如何進行處理?     http://example.com/USERS/12345\n   http://example.com/users/12345\n  在普通的Web網站下，如果採用了不論大小寫都會返回相同的結果會出現一種問題，會導致Google等搜尋引    擎會認為有多個頁面返回了相同的結果而導致網站排名進行下降。  參考下列服務，當遇到大寫字母的URI時，會自動返回404     在線服務  處理混有大寫字母的URL    Foursqare  出錯404   Github  出錯404   Tumblr  出錯404  ✅ 結論   盡量不要使用大小寫字母混用會造成API難以理解，一般標準的做法是，統一使用小寫的URI。  修改方便的URI  修改方便在英語文語意為  Hackable 。修改方便的URI指的是能將某個URI非常容易修改為另外一個URI。    通常應用在獲取某種商品。  💀 反例  按照資料庫的資料表進行結構區分，例如: 1 ~ 300000儲存到alpha資表表內。     # ID的範圍 1 ~ 300000\n   http://api.example.com/v1/items/alpha/:id\n   \n   # ID的範圍 400001 ~ 500000\n   http://api.example.com/v1/items/beta/:id\n   \n   # ID的範圍 500001 ~ 700000\n   http://api.example.com/v1/items/gamma/:id\n   \n   # ID的範圍 700001 ~\n   http://api.example.com/v1/items/delta/:id\n  💡 正例     http://api.example.com/v1/items/123456\n  ✅ 結論   盡量讓URI的延展性佳(這邊指的是/items/{id})，可以藉由輸入不同的編號，來修改URI，而不是必須    要去猜測。  不會曝露伺服器架構的URI  💀 反例     http://api.example.com/cgi-bin/get_user.php?user  =100\n  💡 正例     http://api.example.com/user/100\n  ✅ 結論   不要將無意義的資訊暴露出來。例如:    cgi-bin ，可以猜測你可能是使用CGI的方式運行。   get_user.php ，可以猜測你可能是使用php進行撰寫。  規則統一的URI  💀 反例     # 獲取好友資訊\n   http://api.example.com/friends?id  =100\n   \n   # 發送好友資訊\n   http://api.example.com/friend/100/message\n  💡 正例     # 獲取好友資訊\n   http://api.example.com/friends/100\n   \n   # 發送好友資訊\n   http://api.example.com/friends/100/message\n  ✅ 結論   統一URI的設計，讓使用者易於理解。  2.3 HTTP方法和EndPoint     方法名稱  說明    GET  獲取資源   POST  新增資源   PUT  更新已有資源   DELETE  刪除資源   PATCH  更新部分資源   HEAD  獲取資源的Metadata資訊   Metedata為描述資料的資料，舉例：描述HTML5這份文件的資料。   \nMetedata不會呈現在畫面上，只會給瀏覽器和搜尋引擎查看。    https://ithelp.ithome.com.tw/articles/10237545     2.4 API端點的設計     目的  EndPoint  方法  其他相同    使用者註冊   http://api.example.com/v1/users       http://api.example.com/v1/auth/sign       http://api.example.com/v1/auth/register  POST  其他相同   登入   http://api.example.com/v1/auth/login  POST    獲取自己的資訊   http://api.example.com/v1/users/me       http://api.example.com/v1/auth/me  GET    更新自己的資訊   http://api.example.com/v1/users/me       http://api.example.com/v1/auth/me  PUT    取得使用者資訊   http://api.example.com/v1/users/:id       http://api.example.com/v1/users/{id}  GET  搜尋使用者   取得使用者列表   http://api.example.com/v1/users  GET    取得好友列表   http://api.example.com/v1/users/:id/friends  GET    添加好友   http://api.example.com/v1/users/:id/friends  POST    刪除好友   http://api.example.com/v1/users/:id/friends/:id  DELETE    搜尋好友   http://api.example.com/v1/users/:id/friends/:id  GET    發送消息   http://api.example.com/v1/friends/:id/message  POST    取得好友的訊息   http://api.example.com/v1/friends/:id  GET    編輯訊息   http://api.example.com/v1/friends/:id  PUT    刪除訊息   http://api.example.com/v1/friends/:id  DELETE    好友動態列表   http://api.example.com/v1/users/:id/friends/updates  GET    取得特定使用者的動態訊息   http://api.example.com/v1/users/:id/updates  GET    發表動態訊息   http://api.example.com/v1/updates  POST    編輯動態訊息   http://api.example.com/v1/updates/:id  PUT    刪除動態訊息   http://api.example.com/v1/updates/:id  DELETE     :id 為佔位符號  2.4.1 訪問資源的EndPoint設計的注意事項   使用名詞的複數形式    URI 表示資源的集合   HTTP 方法表示一般動詞  注意所用的單字  \n例如:   search 和  find 兩者中，該選用哪種比較好?   API一般設計採用  search  可以從👇網站查看各種API範例\n  ProgrammableWeb  不使用空格以及需要編碼的字串  \n當URI裡存在無法直接使用的字串時，則需要使用到百分號編碼（英語：Percent-encoding），又稱：URL編碼（  URL encoding ）。\n例如：   %E3%81%82 。  4️. 使用連接符來連接多個單字，基本上連接字串的方式總共有三種寫法。   S  pinal-Case寫法  一般稱為脊柱形命名法。   http://api.example.com/v1/users/12345/profile-image\n  Snake Case  寫法  一般稱為蛇型命名法。   http://api.example.com/v1/users/12345/profile_image\n  Camel Case寫法  一般稱為駝峰命名法。   http://api.example.com/v1/users/12345/profileImage\n     \n  💡 這三種寫法中，網路上最推薦Spinal-Case的方法，其中一個原因是因為Google推薦使用。  另外最好在URI中使用多個單字，例如不要使用  popular_users ，而是使用  users/popular 用路徑那樣子來劃分。  2.5 搜尋與查詢參數的設計  2.5.1 獲取資料量和獲取位置的查詢參數  當資料量很龐大的時候，例如 👇 使用者資料列表API，如果今天是FB等級的使用者，那可能有好幾億，這樣一次把所有數值吐回來是不可能達成的，因此可以採用分頁(Pagination)來處理。   http://api.example.com/v1/users\n   分頁的使用，一般可以透過SQL中  limit 和  offset 數值來產生。   各大服務查詢的方式\n   資料量 使用  limit 、  count 、  per_page  資料位置 使用  page 、  offset 、  cursor     一般來說   per_page  和  page 會一起出現，而  limit 和  offset 會成對出現。   分頁的舉例\n1頁可以容納50條紀錄，當要取第三頁(從101開始)的資料時，該怎麼撰寫呢?   per_page=50&page=3\n\nlimit=50&offset=100\n   💡 一般  page 從1開始(1-based)計數，而  offset 則從0開始(0-based)計數。  2.5.2 使用相對位置存在的問題  當使用  offset 或  limit 來取得指定的資料位置時，其實都是要從頭開始數第幾條，每次都要從第1條資料開始計數，因此效能較差。   從頭開始計數\n   當資料更新的頻率比較高的時候，會導致當前獲取資料出現一定的偏差。\n   2.5.3 使用絕對位置來取得資料  可以透過指\"定某個ID之前\"或\"某個日期之前\"等條件，來解決相對應位置取得資料的問題。  例如：Twiiter的API中的  max_id 、YouTube中的  publishedBefore 。  2.5.4 使用參數來過濾  以下例子使用Linkedin的People Search API舉例。   http://api.linkedin.com/v1/people-search?first-name=Clair\n\nhttp://api.linkedin.com/v1/people-search?last-name=Clair\n\nhttp://api.linkedin.com/v1/people-search?school-name=Shermer&20High%20School\n  2.5.5 查詢參數和路徑的使用區別  到底該把參數附加在查詢參數裡面? 還是放在路徑裡呢? 可以依據下列兩點來解釋   是否可以表示唯一資源所需的資訊   http://api.example.com/v1/users/10\n   是否可以省略  分頁的額外補充   請求\n    perPage  每個頁面的大小(每個頁面的項目)   page  目前頁面的編號   http://api.example.com/v1/users?page=3&perPage=50\n   回應\n    currentPage  目前頁面的編號   pageSize  每個頁面的大小(每個頁面的項目)   totalPages  頁面總數量   totalItems   項目的總數量   items  目前頁面上的項目  關於登錄OAuth2.0 (2.6 ~ 2.10)  OAuth在API設計上是蠻重要的一環，你一定很常遇到在使用某個網頁時，他會想要你Google或是Facebook的資料，此時頁面會導向一個小視窗要你輸入Google或Facebook的帳號密碼，輸入完畢後接著就會導回原本的網頁。接著網頁就可以獲取你Google與FB的相關資訊了。這種B網頁要向你索取第三方網頁的認證工作，就是OAuth會替你處理掉。  2.6.   陽春認證  上述描述的認證工作，最簡易的認證機制如下圖，使用者在客戶端透過基本的帳號與密碼向後端伺服器驗證身份。伺服器會經過一連串的驗證流程，驗證成功後透過如 Session / Cookie 機制，在客戶端保存用戶登入的狀態資訊。    此陽春認證大致會有幾個問題   第三方程式必須儲存 Resource Owner 的帳號密碼，通常是明文儲存。  Server 必須支援密碼認證，即使密碼有天生的資訊安全上的弱點。  第三方程式會得到幾乎完整的權限，可以存取 Protected Resources ，而 Resource Owner 沒辦法限制第三方程式可以拿取 Resource 的時效，以及可以存取的範圍 (subset)。  任何第三方程式被破解 (compromized)，就會導致使用該密碼的所有資料被破解。  2.7 OAuth的基本認證機制  那麼OAuth在第三方認證這流程上，中間到底發生什麼事情，我們可以看下述圖意流程     Step1：User點下允許訪位Facebook個人資訊認證  Step2：第三方網頁請求認可  Step3：User輸入帳命並告知Facebook可以把Token轉交給第三方網頁  Step4：將Token轉交給第三方網頁  Step5：第三方網頁帶取Token向Facebook調用索取資訊API  Step6：Facebook Response請求給第三方網頁  上述OAuth常見的基本流程，使用OAuth方便的是，User無須再次對請求輸入帳號密碼，認證過程中會通過Facebook提供的頁面(一般常見為帳號密碼輸入或是認證許可按鈕如下圖)。    如果OAuth訪問成功就可從Facebook獲取access token，通過此token，第三方網頁就可訪問Facebook用戶允許的相關訊息(public profile mail 介紹...)。  2.8 OAuth 2.0 的角色定義   Resource Owner：可以授權別人去存取 Protected Resource 。如果這個角色是人類的話，則就是指使用者 (end-user)。  Resource Server：存放 Protected Resource 的伺服器，可以根據 Access Token 來接受 Protected Resource 的請求。  Client：代表 Resource Owner 去存取 Protected Resource 的應用程式。 “Client” 一詞並不指任何特定的實作方式（可以在 Server 上面跑、在一般電腦上跑、或是在其他的設備）。  Authorization Server：在認證過 Resource Owner 並且 Resource Owner 許可之後，核發 Access Token 的伺服器。  2.9 OAuth的認證流程形式(Grant Type)  上述簡單描述OAuth基本流程，但實際的認證流程會有四種形式，推薦此篇  認識 OAuth 2.0：一次了解各角色、各類型流程的差異 。對於四種形式我覺得此篇作者整理得很淺白易懂。這邊我也會直接擷取他對於四種形式的描述來解說。     Type  常見應用    Authorization Code  有透過Server處理   Implicit  Clinet端處理   Resource Owner Password Credentials    Client Credentials  M2M(Machine to Machine)  2.10   Authorization Code  流程示意  常見類型，通常應用在SSR伺服器渲染的設計上，大部分的邏輯處理程式碼以極機密都會保存在Server。    詳細流程  (1) Authorization Request  【Client】GET -> 【Authorization Endpoint】  第一步是 Client 產生一個 URL 連到 Authorization Endpoint ，要 Resource Owner 打開（點擊）這個 URL ，從而產生「向 Authorization Endpoint 發送 GET request」的操作。  把參數包在 URI 的 query components 裡面。     參數名  填什麼/意義    response_type  code   client_id  自己的 Client ID   state  內部狀態   redirect_uri  申請結果下來之後要轉址去哪裡   scope  申請的存取範圍   GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz\n    &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1\nHost: server.example.com\n   (4) Authorization Response  【Authorization Endpoint】 302 Response ->  GET 【Client: Redirection Endpoint】  Resource Owner 若同意授權，這個「同意授權」的 request 會往 Authorization Endpoint 發送，接著會收到 302 的轉址 response ，裡面帶有「前往 Client 的 Redirection Endpoint 的 URL」的轉址 (Location header)，從而產生「向 Redirection URI 發送 GET Request」的操作。     參數名  填什麼/意義    code  Authorization Code   state  原內部狀態   HTTP/1.1 302 Found\nLocation: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA\n          &state=xyz\n   state 如果 (1) 的時候有附上，則 Resopnse 裡面必須有，完全一致的原值。如果原本就沒有，就不需要回傳。  Authorization Code：   必須是短時效的，建議最長 10 分鐘。  Client 只能使用一次，如果重複使用，Authorization Server 必須拒絕，並且建議撤銷之前透過這個 Grant 核發過的 Tokens  要綁定 Code ↔ Client ID ↔ Redirection URI 的關係  長度由 Authorization Server 定義，應寫在文件中， Client 不可以瞎猜。   (5) Access Token Request  【Client】POST -> 【Token Endpoint】     參數名  填什麼/意義    grant_type  Authorization Code   code  在 (4) 拿到的 Authorization Code   redirect_uri  如果 (A) 有提供，則必須提供一模一樣的。   client_id  自己的 Client ID  Authorization Server 的處理程序   要求 Client 認證自己（如果是 Confidential Client 或有拿到 Client Credentials）  如果 Client 有出示認證資料，就認證它  確定 Authorization Code 是發給 Client 的\n   Confidential: 用 Client 的認證過程來證明  Public: 用 Client ID 來證明  驗證 Authorization Code 正確  如果 (1) 有給 Redirection URI 的話，確定這次給的 Redirection URI 與 (1) 時的一模一樣。   POST /token HTTP/1.1\nHost: server.example.com\nAuthorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW\nContent-Type: application/x-www-form-urlencoded\n\ngrant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA\n&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb\n  (5) Access Token Response  【Client】 \u003C- 【Token Endpoint】  若 Access Token Request 合法且有經過授權，則核發 Access Token，同時可以核發 Refresh Token （非必備）。如果 Client 認證失敗，或 Request 不合法，則依照   RFC 6749 Section 5.2  的規定回覆錯誤。   HTTP/1.1 200 OK\nContent-Type: application/json;charset=UTF-8\nCache-Control: no-store\nPragma: no-cache\n\n{\n  \"access_token\":\"2YotnFZFEjr1zCsicMWpAA\",\n  \"token_type\":\"example\",\n  \"expires_in\":3600,\n  \"refresh_token\":\"tGzv3JOkF0XG5Qx2TlKWIA\",\n  \"example_parameter\":\"example_value\"\n}\n   Implicit  流程示意  通常應用在SPA設計，整個應用程式都在前端運行，依需求向後端 API 取得資料。由於整個應用程式都在前端運行，所以會缺少「後端伺服器透過 Authorization Code Grant 交換 Access Token 」的步驟。取而代之的是請 Authorization Server 直接核發 Access Token。  這邊要注意最終應用程式就能拿著 Access Token 向 Resource Server 取得資料。特別留意：不像 Authorization Code Flow，這邊是由前端獲得與管理 Access Token，並帶著 Access Token 發出請求前往取得資源，因此在安全性上「相對脆弱」。    詳細流程  (1) Authorization Request  【Client】GET ->【Authorization Endpoint】  第一步是 Client 產生一個 URL 連到 Authorization Endpoint ，要 Resource Owner 打開（點擊）這個 URL ，從而產生「向 Authorization Endpoint 發送 GET request」的操作。  把參數包在 URI 的 query component 裡面。     參數名  填什麼/意義    response_type  code   client_id  自己的 Client ID   state  內部狀態   redirect_uri  申請結果下來之後要轉址去哪裡   scope  申請的存取範圍  Authorization Server 的處理程序  因為 Implicit Grant Flow 是直接在 Authorization Endpoint 發 Access Token ，所以資料驗證和授權都在這一步處理。所以這個 Request 進來的時候， Authorization Server 要做這些事：  驗證所有必須給的參數都有給且合法  Redirection URI 與預先在 Authorization Server 設定的相符。\n如果沒問題，就詢問 Resource Owner 是否授權，即 (B) 步驟  (4) Authorization Response  【Client】 \u003C- 302【Authorization Endpoint】  Resource Owner 若同意授權，這個「同意授權」的 request 會往 Authorization Endpoint 發送，接著會收到 302 的轉址 response ，裡面帶有「前往 Client 的 Redirection Endpoint 的 URL」的轉址 (Location header)，從而產生「向 Redirection URI 發送 GET Request」的操作。  參數要用 URL Encoding 編起來，放在 Fragment Component 裡面。  若 Access Token Request 合法且有經過授權，則核發 Access Token。如果 Client 認證失敗，或 Request 不合法，則依照 Section 5.2 的規定回覆錯誤。  特別注意 Implicit Grant Type 禁止 核發 Refresh Token。  某些 User-Agent 不支援 Fragment Redirection ，這種情況可以使用間接轉址，即是轉到一個頁面，放一個 “Continue” 的按鈕，按下去連到真正的 Redirection URI 。     參數名  填什麼/意義    access_token  即 Access Token   expires_in  建議有\t幾秒過期，如 3600 表示 10 分鐘。若要省略，最好在文件裡註明效期。   scope  Access Token 的授權範圍 (scopes)。   state  原內部狀態。  其中 scope 如果和 (1) 申請的不同則要附上，如果一樣的話就不必附上。  其中 state 如果 (1) 的時候有附上，則 Resopnse 裡面必須有，完全一致的原值。如果原本就沒有，就不需要回傳。  Access Token 的長度由 Authorization Server 定義，應寫在文件中， Client 不可以瞎猜。  Client 遇到不認識的參數必須忽略。   HTTP/1.1 302 Found\nLocation: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA\n          &state=xyz&token_type=example&expires_in=3600\n   Resource Owner Password Credentials  流程示意  由使用者提供帳號與密碼等資訊給應用程式，由應用程式直接向 Authorization Server 交換 Access Token，因此「必須是使用者高度信賴的應用程式」才適合使用，且唯有前兩種皆不可行時，才會考慮使用當前類型的流程。  體驗上和以往的帳號密碼登入雷同。    流程細節  (2,3) Authorization Request & Response  在這個流程裡面， Authorization Grant 就是 Resource Owner 的帳號密碼，所以在 Step (A) 裡面直接向 Resource Onwer 索取，沒有經過網路來取得 Authorization。  Spec 不規定 Client 要怎麼拿到帳號密碼，但是 Client 取得 Access Token 之後，必須把 Resource Owner 的帳號密碼給銷毀掉。  (4) Access Token Request  【Client】POST -> 【Token Endpoint】     參數名  填什麼/意義    grant_type  password   username  Resource Owner 的帳號   password  Resource Owner 的密碼   scope  申請的存取範圍  Authorization Server 的處理程序  這個 Request 進來的時候， Authorization Server 要做這些事：  要求 Client 認證自己（如果是 Confidential Client 或有拿到 Client Credentials）  如果 Client 有出示認證資料，就認證它   Client Credentials  流程示意  通常是由應用程式向 Authourization Server 請求取得 Access Token 以獲取「自己」的相關資源，而非使用者的資源。  這個流程已經跳脫使用者，因此，使用者身份驗證的流程將不再需要。取而代之的，是應用程式必須向 Authorization Server 提供驗證所需的自身資訊。    流程細節  (1) Access Token Request  【Client】POST -> 【Token Endpoint】     參數名  填什麼/意義    grant_type  password   scope  申請的存取範圍   POST /token HTTP/1.1\nHost: server.example.com\nAuthorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW\nContent-Type: application/x-www-form-urlencoded\n\ngrant_type=client_credentials\n  (2) Access Token Response  【Client】POST \u003C- 【Token Endpoint】  若 Access Token Request 合法且有經過授權，則核發 Access Token，但是最好不要核發 Refresh Token。如果 Client 認證失敗，或 Request 不合法，則依照的  RFC6749規定(Section5.2) 回覆錯誤。   HTTP/1.1 200 OK\nContent-Type: application/json;charset=UTF-8\nCache-Control: no-store\nPragma: no-cache\n\n{\n  \"access_token\":\"2YotnFZFEjr1zCsicMWpAA\",\n  \"token_type\":\"example\",\n  \"expires_in\":3600,\n  \"example_parameter\":\"example_value\"\n}\n  簡易整理    d.OAuth端點範例  上述探討完四種認證流程形式，那實際使用OAuth時，端點的形式該如何設計?下述為常見的幾個有名的網站    書中筆者比較建議的方式是像  /oauth2/token 此種方式去設計，因為明確指出使用的是OAuth2.0，並且與RFC 6749給出的範例雷同。  我們稍微看一下實際medium在索取facebook認證資訊時的內容如下圖，基本上也是照這邏輯下去設計     OAuth2.0   OAuth 2.0 筆記  html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"id":137,"path":138,"dir":126,"title":139,"description":7,"keywords":140,"body":147},"content:3.web-api:chapter5.md","/web-api/chapter5","第5章 開發方便更改設計的API",[141,142,143,144,145,146],"5.1 方便更改設計的重要性","5.2 透過版本訊息來管理API","5.3 版本變更的方針","5.4 終止提供API","5.5 編排層(中間層)","5.6小結","  5.1 方便更改設計的重要性   API必須保持公開提供串接的狀態，但是我們會需要添加新功能、廢棄某些功能  如果只是修改API內部邏輯，對外的數據格式不變，則不需要更新API設計規範  5.1.1 公開發佈的API   LSUD(Large set of unknown developers)\n   ex.FB這類的公開API  影響不特定開發者，範圍未知  API改版的影響程度大  無法保證用戶端可以搭配API改版而重新串接，此時強制變更API會導致終端用戶、串接應用，覺得這個服務不可靠  5.1.2 面向移動應用的API   SSKD(Small set of known develop)\n   只有影響特定開發者，範圍可控  API的改版影響程度小  注意不更新應用程式的刁民  5.1.3 Web服務中使用的API   注意快取的影響  5.2 透過版本訊息來管理API   更新API有會不等程度的影響，透過某些機制讓不更新用戶可以繼續使用舊的服務，更新後的用戶使用新的服務    實現的方式   不同的URI\n   舊的：  http://api.example.com/user/123  新的：  http://newapi.example.com/user/123  議題：new的命名不好，如果之後又要改版，newnewapi?  5.2.1 在URI中嵌入版本編號    http://api.example.com/v2/user/123  最常見的方式，舊版本的api短時間能可繼續使用    5.2.2 如何添加版本編號   編號規則：主版本編號、次版本編號、補丁版本編號。ex.1.0.1  如果只是修正BUG，則增加補丁版本編號ex.1.0.2  如果功能新、刪，且向下兼容，則增加次版本編號ex.1.1.1  如果本次修改無法向下兼容，則增加主版本編號ex.2.0.1  以  http://api.example.com/v1/user/123來說，只使用一個版本編號，代表為主版本編號，因此當主版本編號增加時，API才進行版本升級  同時維護多版本的API成本相當高，也容易讓用戶混淆該用哪一個，因此小更動會盡量不去升級api版本，而是盡可能地向下相容  只有發生可以放棄下向相容的重大更新時才去升級api版本，因此大部分的api的網址版本編號只有主版本編號ex./v1  5.2.3 在查詢字串加入版本訊息    http://api.example.com/user/123?v=1.5  差異：使用參數的方式代表是可以省略，如果沒有填，伺服器會直接使用默認的版本(通常是最新版本)  該選擇加在網址路徑還是參數？\n   建議加在網址路徑，因為參數的方式如果省略的話，用戶其實不知道呼叫到的是哪個版本  5.2.4 透過媒體類型來指定版本訊息   在header加入Accept:application/api.example.v2+json  好處：URL可以作為純粹的資源使用  5.2.5 該採用什麼方法   最常用的是在URL的路徑中嵌入版本訊息  5.3 版本變更的方針   盡可能向下相容  ex.原本使用gender(Int)1=男生2=女生，如果想要改為gender(String)男生、女生的方式紀錄，則建議多一個參數genderStr(String)，並把gender標示為廢棄  什麼時候該升級版本\n   權限、驗證機制調整，ex.v1沒有身份驗證機制，v2有身份驗證機制，則可以廢棄v1升級到v2  因為這類的調整會動到的地方太多，難達成向下相容，因此該升級版本  5.4 終止提供API   公告讓用戶知道該升級，舊API什麼時候結束服務，用戶可以規劃系統升級的時程  Blackout Test：暫時一段時間不提供服務，逼迫用戶提早升級  5.4.2 正式停止舊API   回傳http status 410，同時給予錯誤訊息  應用服務必須針對410的錯誤提示用戶該升級系統  5.4.3 在使用條款中寫明支援期限   在X個月後，不再支援舊版api，但不是直接關閉服務  新的API出來的時候，同時宣布至少支援X月  5.5 編排層(中間層)   ex.fb graph api，源頭端的API可以負責叫小範圍，就像是單一個積木，減少開發的複雜性  終端透過中間層去組裝多個積木來達成頁面的需求  終端可以受到API改版的影響較小，源頭端可以減少API變更的阻礙    5.6小結   最大限度減少API版本更新的頻率，注意向下兼容  在URI中嵌入API版本的主版本編號  停止提供API服務時不能立即中止，至少需要繼續公開六個月",{"id":149,"path":150,"dir":126,"title":151,"description":7,"keywords":152,"body":155},"content:3.web-api:chapter6.md","/web-api/chapter6","第6章 開發牢固的WebAPI",[153,154],"6.5 同安全相關的 HTTP Header","6.6 應對大規模訪問的對策","  6.5 同安全相關的 HTTP Header     當使用者通過瀏覽器發送request到伺服器上，伺服器會回應response給瀏覽器，此時就會帶上一些header，其中有些header可以保證網站安全。    6.5.1 X-Content-Type-Options   連結    IE系列：   IE8 ━ ：無效(自帶Content Sniffing)  IE8 ✚：可用    避免Client執行不正常的 Content-Type 類型檔案。  該標頭  告訴瀏覽器不要猜測所提供內容的MIME （多用途Internet郵件擴展名）類型，而是信任\" Content-Type\"標頭。如果沒有設定：  X-Content-Type-Options 標頭，則某些較舊的瀏覽器可能會錯誤地將文件檢測為 Script 或 CSS Style，從而可能導致XSS攻擊。     推薦設定：   X-Content-Type-Options：nosniff \n\n阻止瀏覽器探知檔案的 mime type ( disable Content sniffing )，一般情況下瀏覽器會去探知資源的 Content-Type ，以判別資源類型。\n\n例如：image/png、text/css，而有些資源的 Content-Type 有可能是錯誤或缺少的，此時就會進行 Content sniffing猜測解析內容，將 X-Content-Type-Options 設成 nosniff，可阻止這種行為。\n\n沒有設成 nosniff 的風險為攻擊者可能使用錯誤的 header 隱藏攻擊的 script ，例如 \u003Cscript src=”https://example.com/attacker.txt\" >\u003C/script>，attacker.txt 實際是 js 檔，表面的 header 是 text/plain ，實際上瀏覽器會解析 scrip t的content type ，並且執行 script。\n       什麼是 content sniffing：  一般來說瀏覽器會透過 Content-Type 來判斷請求到的資源是什麼類型，像透過   \u003Cscript src=\"script.js\">  拿到的 Content-Type 一般都是   text/javascript ，因此瀏覽器看到之後就會拿來執行。但有些網站（尤其是十幾二十年前的舊網站）在開發時並沒有把 Content-Type 設好，導致某些 JS 檔的 Content-Type 是   text/plain ，也就是純文字檔。為了讓這些網站可以順利運作，  瀏覽器除了參考 Content-Type 之外，也會做 content sniffing 從檔案內容分析是什麼類型 ，如果分析出是 JS 那就會拿去執行，這樣舊網站才不會壞掉。    sniffing 這個動作看似貼心，卻也是一個弱點：  譬如說有些網站允許使用者上傳檔案，那**  攻擊者就可以惡意上傳一些有 JS 特徵的 JPG 檔（這些圖片會被瀏覽器判斷成腳本）**。 接著想辦法讓這張圖片被載入到前端來，導致裡面的惡意腳本被執行，  造成 XSS 攻擊。       6.5.2 X-XSS-Protection   連結    IE系列：   IE8 ━ ：無效  IE8 ✚：可用(可停用)\n   瀏覽器檢測到 XSS Script 攻擊，即停止執行頁面載入。  可搭配設置 Content-Security-Policy 來禁用 Corss Javascript。  現代多數瀏覽器不太需要這項保護，但對於不支援 CSP 的舊版瀏覽器的用戶提供保護。   X-XSS-Protection: 0\n    禁止XSS过滤。\n\nX-XSS-Protection: 1 \n    啟用XSS過濾（通常瀏覽器是預設的）。如果檢測到跨站腳本攻擊，瀏覽器將清除頁面（刪除不安全的部分）。\n\nX-XSS-Protection: 1; mode=block\n    啟用XSS過濾。如果檢測到攻擊，瀏覽器將不會清除頁面，而是阻止頁面載入。\n\nX-XSS-Protection: 1; report=\u003Creporting-uri> \n    啟用XSS過濾。如果檢測到跨站腳本攻擊，瀏覽器將清除頁面並使用CSP report-uri指令的功能發送違規報告。\n     推薦設定：   X-XSS-Protection: 1; mode=block\n啟用XSS過濾。如果檢測到攻擊，瀏覽器將不會清除頁面，而是阻止頁面載入。\n     6.5.3 X-Frame-Options   連結    IE系列：   IE8 ━ ：無  IE8 ✚：這個 HTTP response header 在 2009 年時首先由 IE8 實作，接著其他瀏覽器才跟上，在 2013 年時才變成了完整的   RFC7034 。   允許或禁止網頁載入   \u003Cframe>  與   \u003Ciframe> 。   副作用是其他正常的網站，也無法在 frame 中顯示被禁用 frame 的網頁。    X-Frame-Options  最前面的   X  說明了它比較像是一個過渡時期的東西，在未來新的瀏覽器當中，它的功能會被 CSP（Content Security Policy）給取代，並且把上面提到的問題解決。   X-Frame-Options: DENY\n    拒絕任何網頁把這個網頁嵌入，包含 \u003Ciframe>, \u003Cframe>, \u003Cobject>, \u003Capplet>, \u003Cembed> 這些 tag 都不行。\n\nX-Frame-Options: SAMEORIGIN\n    只有 same origin 的網頁可以。\n\nX-Frame-Options: ALLOW-FROM https://example.com/\n    只允許特定的 origin 嵌入，除此之外其他的都不行（只能放一個值不能放列表，所以如果要多個 origin，要像 CORS header 那樣在 server 動態調整輸出）。\n\nWebAPI：可以減輕在通過Frame讀取資料導致的某些漏洞而被惡意使用。(應該也不會在Fram內讀取資料，有加有保庇。)\n     推薦設定：   X-Frame-Options: SAMEORIGIN\n    只有 same origin 的網頁可以。\n   💡    補充：Clickjacking 點擊劫持攻擊  點擊劫持（Clickjacking）技術又稱為    界面偽裝攻擊（UI redress attack） \n是一種將惡意程式隱藏在看似正常的網頁中，並誘使使用者在不知情情況下點擊的手段。    攻擊原理   使用者被誘使點擊種下惡意程式的連結時，  該連結上其實覆蓋了一個隱藏的   \u003Ciframe> ，  點擊該連結時，實際上是點選了隱藏的   \u003Ciframe> ，  如果   \u003Ciframe>  內容是個 facebook 的「讚」按鈕，  用戶點到連結時，實際上是操作的是 facebook 的介面。如 Twitter、Facebook 和 Paypal 等網站上，都曾經發生過此種攻擊。    會偽裝連結使受害者點擊惡意連結   CSS z-index 屬性 ：在 CSS 當中 z-index 的數字越大，表示越上層，受害者插入   \u003Ciframe>  或其他 HTML 的時候，可以調整標籤的「透明度」、「z-index」，可以設定為透明且最上層，使受害者可以點擊。       不識廬山真面目：Clickjacking 點擊劫持攻擊 (cymetrics.io)   Day17【Web】網路攻擊：點擊劫持 Clickjacking - iT 邦幫忙::一起幫忙解決難題，拯救 IT 人的一天 (ithome.com.tw)     6.5.4 Content-Security-Policy   連結     內容安全策略 （  CSP ） 是一個額外的安全層，用於檢測並削弱某些特定類型的攻擊，包括跨站腳本 （  XSS  ） 和數據注入攻擊等。 \n無論是數據盜取、網站內容污染還是散發惡意軟體，這些攻擊都是主要的手段。  \n📌 CSP 建議在 server 端加 ，雖然前端可以利用 `meta http-equiv`\n給瀏覽器一些額外資訊， 但所有文件還是 prefer 在 Server 那邊加 HTTP header\n     推薦設定：   Content-Security-Policy: default-src larry.com\n  限制瀏覽器只能從 larry.com 這個網域載入圖片、CSS、字體等等各種資源\n\nWebAPI：Content-Secuity-Policy: default-src 'none'\n  告知瀏覽器不要讀取其他資源**\n    \n💡 補充：\n   簡單弄懂同源政策 (Same Origin Policy) 與跨網域 (CORS)   Content Security Policy (CSP) — 幫你網站列白名單吧   Day11-記得要戴安全帽（一）     6.5.5 Strict-Transport-Security   連結     全名是 HTTP Strict Transport Security。  強制瀏覽器只能使用安全的 HTTPS 協定跟網站進行連線，而不能使用 HTTP。  譬如說很多網站其實用 HTTP 跟 HTTPS 都連得上，但考量到安全性，當然是希望使用者都走 HTTPS。這時只要在 header 裡加上   Strict-Transport-Security: max-age=31536000; includeSubDomains ，那在往後的 31536000 秒內（其實就是一年啦XD），只要使用者的瀏覽器看到這個網域或他的子網域，就會全部改成用 HTTPS 進行連線，真的是很方便呢。    \n📌 只有在 HTTPS 有效\n     6.5.6 Public-Key-Pins   連結    已棄用     不再建議使用此功能。雖然某些瀏覽器可能仍然支援它，但它可能已經從相關的Web標準中刪除，可能正在被刪除，或者可能只是為了相容目的而保留。避免使用它，並盡可能更新現有代碼;請參閱此頁面底部的[相容性表](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Public-Key-Pins#browser_compatibility)，以指導您的決定。請注意，此功能可能隨時停止工作。   用於將特定加密公  鑰 與某個 Web 伺服器相關聯，以降低使用偽造證書進行   MITM  攻擊的風險。但是，它已從現代瀏覽器中刪除，不再支援。     推薦設定：  \n📌 Certificate Transparency\n  \n📌 Expect-CT\n     6.5.7 Set-Cookie   連結      從後端設置 ：使用後端語言設置 cookie，通過 response 的 Set-Cookie header，瀏覽器會根據 Set-Cookie 儲存 cookie    從前端 javascript 設置 ：如：document.cookie = “name=John;”。   cookie原理   連結  \n📌 Cookie（也叫HTTP cookie，web cookie）是保存在瀏覽器中的一小塊數據（不超過4K）。 HTTP協定是無狀態的，因此cookie最初被設計來幫助網站保存一些狀態資訊，或者使用者的一些操作歷史。 比如基於cookie實現的前端購物車（現在通常會用local storage來替代cookie），或者結合session來實現使用者登錄狀態，亦或者是保留使用者在網站上填寫的表單資訊方便下次輸入。\n    同源策略  cookie常常被用於存儲使用者的私有資訊，所以為了保證隱私安全，防止cookie資訊被盜取，瀏覽器施行了同源政策。 只有當各個網路訪問滿足同源策略時，才允許共用cookie資訊。 所謂的「同源」指的是：   協定相同  功能變數名稱相同  埠相同   http:// www.test.com :80/xxxxxx\n------- ------------ ---\n    |         |       |\n   協議      域名     端口**\n  只有當這三者相同時，瀏覽器才認為是符合同源策略的。 而基於cookie的網路攻擊則是通過繞過各種辦法繞過同源策略來實施攻擊，比如XSS、CSRF攻擊。   cookie帶來的安全問題    網路竊聽（中間人攻擊）      DNS緩存中毒      斷續器 - cookie盜用      斷續器 - 代理請求     Cookie flag：\n    Expires：設定一個日期，到日期時就會自動失效。\n    Max-Age：設定秒數，秒數過後自動失效，比Expires優先度高。\n    Domain：設定作用網域，設定之後會包含子網域，若無設定則默認當前網域，不包含子網域。\n    Path：設定作用路徑，設定/admin將匹配/admin/users,/admin/roles等路徑。\n    Secure：只能使用https傳到伺服器。\n    HttpOnly：只能經由伺服器存取cookie，不能經由document.cookie。\n    SameSite：Lax: default 值，在不同網域時不會發送，但在其他網域導向原本網域時會發送。\n    Strict: 只能在同網域下傳送。\n    None: 可以跨域發送，但必須有Secure flag。\n     推薦設定：   Cookie flag：\n    **Expires：設定一個日期，到日期時就會自動失效。\n    Max-Age：設定秒數，秒數過後自動失效，比Expires優先度高。\n    Domain：設定作用網域，設定之後會包含子網域，若無設定則默認當前網域，不包含子網域。\n    Path：設定作用路徑，設定/admin將匹配/admin/users,/admin/roles等路徑。\n    Secure：只能使用https傳到伺服器。\n    HttpOnly：只能經由伺服器存取cookie，不能經由document.cookie。\n    SameSite：Lax: default 值，在不同網域時不會發送，但在其他網域導向原本網域時會發送。**\n    Strict: 只能在同網域下傳送。\n    None: 可以跨域發送，但必須有Secure flag。\n  \n💡補充：\n   資安議題 — Cookie 安全. 前言 | by LSZ | 程式愛好者 | Medium   Cookie 的安全隱患 - iT 邦幫忙::一起幫忙解決難題，拯救 IT 人的一天 (ithome.com.tw)         資安議題 — Http Security Header. 當使用者通過瀏覽器發送request到伺服器上，伺服器會回應response給瀏… | by LSZ | 程式愛好者 | Medium   前端單兵基本教練 - X-Frame-Options、CSP frame-ancestors 網站內嵌限制實測-黑暗執行緒 (darkthread.net)     6.6 應對大規模訪問的對策      不僅是Web API服務，任何在網路上公開的服務都會時不時地遇到來自外部的大規模訪問。當伺服器遇到大規模訪問時，為了處理這些訪問會耗盡資源，進而無法提供服務。這時不僅是這些大規模訪問，任何人都無法和伺服器端建立連線。  \n⚠️ **DDOS：**\n   ddos 攻擊定義，防護策略與三手法| Cloudflare | Cloudflare     Famous DDoS Attacks | Cloudflare     【Security 的老生常談】又是你嗎？DDoS ?! - (discover-thefutureofwork-tw.cloud)       6.6.1 限制使用者的訪問 *      用什麼樣的機制來識別使用者   如何確定限速的數值   以什麼單位來設置限速的數值   在什麼時候重置限速的數值       Twitter\n       對搜索推文的操作  (search  /  tweet)：每   15   分鐘   180   次\n     對直接取得消息的操作  (direct_message)：每   15   分鐘   15   次\n   \n   Zendesk\n       基本   1   分鐘內   200   次\n     不同端點，有些只允許   10   分鐘   15   次   (更新 ticket) , 或更少\n     6.6.2 限速的單位      上限值要根據所設想的API使用情境進行調整   要了解你所提供的API會在怎樣的情況被用戶使用，並以此為依據來決定如何設置訪問限制速。  \n❌ 對資料庫頻繁更新的查詢類 API ： 用戶需要頻繁地訪問API，取得最新資料。如果設定 1 小時只允許訪問 10 次這樣嚴格的限制。那麼即使使用API，也無法給應用或服務帶來更高的附加價值，API用戶數也難以增加。\n  \n📌 Etsy 服務引入\"progressive rate limit\"(累進限速)的限速方式，用戶訪問上限 24 小時 1 萬次\n  計算：\n24 小時 拆成 12 個單元，每個單元 2 小時，以過去 12 個單元累計訪問次數做為訪問上限。\n即使用戶超過上限，只需要再等待 2 小時，第一個單元的訪問次數就會重置，用戶就可以再訪問。     6.6.3 應對超出上限值的情況     可以返回HTTP協議中備好的“429 Too Many Request”狀態碼。   當用戶超出訪問上限值時，服務端該如何返回響應訊息呢？這種情況下可以返回HTTP協議中備好的“429 Too Many Request”狀態碼。429狀態碼在2012年4月釋出的RFC 6585中定義，當特定使用者在一定時間內發起的請求次數過多時，伺服器端可以返回該狀態碼錶示出錯。RFC 文件中對該狀態碼描述如下：         6.6.4 向用戶告知訪問限速的信息     用戶會一直訪問 API   用戶如果知道限速信息，就可能會針對性的編寫出自動調整訪問量的客戶端程序。           https://api.github.com/rate_limit               Web API 的設計與開發 | 七月十五九月初七 (luisedware.github.io)   html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"id":157,"path":158,"dir":159,"title":160,"description":7,"keywords":161,"body":166},"content:4.cicd-2.0:5.chapter5.md","/cicd-2.0/chapter5","cicd-2.0","05 持續交付的軟體系統架構",[65,162,163,164,165],"5.1 ”大系統小做”原則","5.2 常見架構模式","5.3 架構改造實施模式","5.4 小結","  前言  在 2000 年，著名的電商網站 AWS 仍然是使用傳統的巨石應用開發，而不是今天大家看到的微服務架構。巨石應用每次部署的時，都需要將整個網站作為一整體並進行統一部署。在大型促銷活動時，網站的穩定性遇到了嚴峻挑戰，儘管團隊之前做了預估擴充，但在活動流量期間往往超出了團隊的預估。生產事件造成的問題，頻頻發生，經常修復一處問題卻引發另外一處問題。  公司管理層內部開始查看原因，最主要的原因是因為系統耦合度太高且複雜。但由於時間比較緊迫，工程師忙於開發自己手上的功能，沒有時間進行溝通。  後來將巨石架構改為服務導向架構   (Service-Oriented Architecture, SOA)，並提出以下要求   所有團隊都要以服務介面的方式，提供資料和各種功能  團隊之間必須通過介面來通訊  不允許其他形式的互相操作、不允許直接存取其他團隊的資料庫、不允許共享記憶體、不允許任何形式的後門。唯一許可的通訊方式，就是通過網路來呼叫服務。  具體的實現技術不做規定，HTTP、Corba、Pub/Sub 方式，自定義協定即可。  所有的服務介面，必須從一開始就是公開的設計介面。簡而言之，在設計介面的時候，就預設這個介面可以讓外部人員使用，沒有討價還價的餘地。  如果不遵循上面的規定，就會被解僱。   AWS 在 2011 年，其生產環境的部署頻率已經非常高。工作日的部署頻率高達平均每 11.6 秒一次，一小時內最高部署次數 1079 次。  5.1 ”大系統小做”原則  5.1.1 持續交付架構要求  為了提升交付速度，獲得持續交付能力，系統架構在設計時，應考量下列因素。   為測試而設計 (design for test)。每次撰寫好程式碼之後，都需要花大量的時間和精力來進行測試，這樣驗證花太多時間，就沒有辦法快速發佈。  為部署而設計 (design for deployment)。開發完成新功能後，當部署發佈時，需要花費很長時間準備，甚至要停機才能部署，就沒有辦法快速發佈。  為監控而設計 (design for monitor)。當功能上線後，無法即時監控，得等到出問題時，使用者回饋後才知道問題，這樣持續交付的優勢就大幅降低了。  為擴展而設計 (design for scale)。有兩點，一是支持團隊成員的規模擴展; 二是支持系統自身的擴展  為失敗而設計(design for failure)。快速部署一定會遇到問題的。一但部署或發佈失敗，該如何優雅且快速地處理。  5.1.2 系統拆分原則  大系統小做的方法由來已久，在 1971 年時，David Parnas 發表了一篇論文\"On the Criteria To Be Used in Decomposing Systems into Modules”(將系統分解為模組的標準)。大系統應該由很多組件(component)或服務(service)組成。通常會以 jar/war/dll/gem 等形式出現，其顆粒度要比一個類(class)大，但要比整個系統小很多。  參考  連結   組件通常在編譯建置或部署時被集合在一起，而服務由多個組件組成，且能夠獨立運行，並在運行時與整個系統進行通訊，成為整個系統的一部份。  目前軟體的發展趨勢，以持續交付的要求對系統進行拆分，有以下原則   做為系統的一部分，每個組件或服務有他自己的業務職責，可以被獨立修改，甚至可以被另外一種方案所替代。  “高內聚，低耦合”，使整個系統比較易於維護，每個組件或服務地功能盡量單一化，不要太複雜。  整個系統容易建置和測試。將系統拆分後，這些組件仍然需要組合在一起，為使用者提供服務。  使團隊之間的溝通合作更加順暢。  由於將系統進行拆分，因此產生了新問題。例如: 多個服務組成的系統來說，一個請求可能經過多次不同服務之間的相互呼叫才完成。這樣呼叫鏈過長，當有上百個上千個服務時，沒有服務發現機制是很難想像的，當你呼叫其他人的服務，並要查找問題時，就會發現 Debug 難度非常高。  因此系統拆分後，我們需要建立對應的方法   建置  測試  部署  監控機制  5.2 常見架構模式  這邊討論三種架構模式   微核架構：向使用者分發的 Client 端軟體  微服務架構：用於企業自身可控的後台 Server 端軟體  巨石應用：常見於創業公司的產品項目。  5.2.1 微核架構  微核架構(microkernel architecture)，又稱為套件架構(plugin architecture)，只軟體核心框架相對較小，而主要業務功能和邏輯都通過套件實現。   核心(core)架構：通常只包含系統運行的基礎功能，例如: 基礎的通訊模組、基本渲染功能和介面整體框架。  套件：套件之間是互相獨立的，套件之間的通訊只能通過核心框架進行，避免出現互相依賴的問題。  \n    \n      插件：外掛程式或擴充套件，通常跟 UI 有關係，例如: Chrome 常用的 Chrome Extension 功能。  這邊舉幾個例子   前端相關   將核心架構看作為 Chrome 瀏覽器，而套件看作為 Chrome Extension 工具。  將核心架構看作為 VSCode，而套件看作為 VSCode Plug-in。  後端相關   API Getway，API Management kong，開箱即用的 Gateway  驅動相關   印表機驅動程式  USB 驅動程式  ✅ 優點   良好的功能延伸性(extensibility)：需要什麼功能，開發一個擴充套件即可。  易發佈：套件可以獨立地加載和卸載，使他比較容易發佈。  易測試：功能之間是隔離的，互相不干擾，因此可以獨立進行測試。  可訂製性高：適應不同開發的需求。  可以逐漸式地開發：逐步增加功能。  ❌ 缺點   擴展性(scalability)差，core 通常是一個獨立單元，不容易做成分布式，但為 Client 端軟體來說，這不是一個嚴重的問題。  開發難度相對較高，因為設計 Plug-in 與 Core 的通訊，以及內部的 Plug-in 登記機制。  高度依賴框架，當框架的介面升級時，有可能影響所有 Plug-in，導致大量的改造工作。  5.2.2 微服務架構  微服務架構(Microservice Architecture)是服務導向架構(service-oriented architecture)，縮寫 SOA 升級。  每一個服務就是一個獨立的部署單元(separately deployed unit)。這些單元都是分佈式的互相解偶，通過通訊協定(REST、SOAP)來通訊。  \n    \n     微服務架構分種三種模式   RESTful API 模式：服務通過 API 提供，雲服務(Cloud Service)就屬於這一類型。  RESTful 應用模式：服務通過傳統網路協議或應用協議提供，背後通常是一個多功能的應用程式，常見於企業內部  集中消息模式：採用訊息代理(Message Broker)，可以實現消息柱列、負載均衡、統一日誌和異常處理。建議將 Message Broker 做成群集(Cluser)，來增加穩定度和可靠性。  ✅ 優點   擴展性好，各個服務之間低耦合  易部署，每個服務都是可部署單元  易開發，每個組件都可以進行單獨開發，單獨部署，不間斷地升級  易於單獨測試，如果修改指涉及單一服務，只需要測試該服務即可  ❌ 缺點   由於強調互相獨立和低耦合，服務可能被拆分很細。導致系統依賴大量的服務，變的凌亂和笨重，網路通訊消耗也會變很大  一次外部請求(Request)會涉及到多個內部服務之間的通訊，使得問題的除錯與診斷比較困難，需要更強大的工具支持。  為原子性 (Atomicity)操作帶來困難，例如：需要事務交易操作的場景。   ACID 為資料庫操作寫入或更新的過程  \nA：原子性 (Atomicity)  \nC：一致性 (Consistency)  \nI：事務隔離 (Isolation)  \nD：持久性 (Durability)  會遇到常見的事務交易問題 👉  參考  跨服務的組合業務場景比較難測試，需要同時部署和啟動多個微服務  公共類別庫的升級和管理比較困難。在使用一些公共的工具性質的類別庫時，需要在建置每個微服務時，都將其打包到部署包中。  正是這些缺點，因此使用微服務架構模式下，需要確保每一個服務能夠獨立部署之外，還要確保服務升級之後不會影響到下游服務。  5.2.3 巨石應用  巨石應用(monolithic application)，也稱為巨石架構，只單一結構組成的軟體應用，也有另一個稱呼為單體式應用。其用戶的介面和資料存取程式碼都綁定在同一個語言平台的同一個應用程式上。   巨石架構通常是一個完整的包，例如: 一個 Jar、Exe 或一個完整的目錄結構，只要有這個包，則所有功能皆有了。  \n     \n  ✅ 優點   利於開發和除錯：當前所有工具和 IDE 都很好地支援巨石應用程式開發。系統架構簡單，Debug 方便。  部署操作本身比較簡單：例如，只需要運行部署時內的 WAR 或 Exe 檔案等等。  很容易擴展：只要在負載均衡器(nginx、IIS)後面放置多個應用的副本，就可以擴展應用程式。  ❌ 缺點   對整體應用程式不太熟悉的人來說，容易產生混淆的程式碼，汙染整個應用，給老舊程式碼的學習和理解帶來困難。  很難與新技術共存。  只能將整個應用作為一個整體進行擴展。  持續部署非常困難，更新一個組件，必須重新部署整個應用程式。  微服務場景相關  (a) 圖有兩個名為 A 服務，分別為新和舊版本，這樣的方式會讓主控服務，無法認識到底該使用哪一個服務進行運作。  (b) 圖有兩個實體的測試環境，A 測試環境用來測試新的 A 服務，B 測試環境用來測試舊的 B 服務。這樣的測試方式，當如果多人都需要測試服務時，會需要大量的測試環境才能夠進行測試。   下方是沒有考慮除錯環境的微服務框架\n     下方是通過路由機制建立共享的微服務測試環境\n   5.3 架構改造實施模式  對部署頻率較低的老舊系統來說，很少會仔細考慮到易測試、易部署和易擴展這三個因素。因此我們總會遇到架構改造的需求。通常改造有三種模式：   拆遷者模式  絞殺者模式  修繕者模式  5.3.1 拆遷者模式  拆遷者模式就是根據當前的業務需求，對軟體架構重新設計，並組織單獨的團隊，重新發佈一個全新的版本，一次性的替代原有的老舊系統。    ✅ 優點   與舊版本完全沒有瓜葛，沒有歷史包袱  ❌ 缺點   業務需求遺漏。在軟體的歷史版本中，有很多不為人熟知的功能還在使用。  市場環境變化。由於新版本架構花費時間較長，無法馬上修改完成，因此當市場發生變化時，就會錯失市場機會。  人力資源消耗大。必須分出人力，一邊維護舊版本的功能或緊急需求，一邊要安排充分人力進行改造。  閉門造車。新版本上線後，無法滿足業務需求。  下面舉實際案例  ✅ 成功案例  HP 惠普印表機的韌體架構改造就是一個成功的案例。在 2008 年的時候，HP 惠普團隊已經筋疲力盡，整個團隊只有 5%在開發新特性，其餘人數皆在進行維護。經過三年的努力，HP 惠普團隊將整個韌體架構改為  微核架構模式 ，在每台印表機上都有一個最小的韌體初始化版本。當印表機連上網後，就會根據韌體的版本，從網路上下載最新的功能模組，並自動安裝。  ❌ 失敗案例  網景通訊公司，主要是在做瀏覽器以 Netscape Navigator 而聞名。由於其老舊的軟體架構，讓使用者體驗越來越差，已經很難對抗主流的瀏覽器發展，於是公司高層決定使用  拆遷者模式 對軟體進行改造。在改造期間，微軟憑藉著 IE 瀏覽器和 Windows 的成功，直接躍升為瀏覽器市場第一名，而網景通訊公司後來則一蹶不振。   網景通訊公司：網景通訊，舊稱網景通訊公司，通常簡稱為網景，是一家已倒閉的美國電腦服務公司，以其開發的同名網頁瀏覽器而聞名。當還是一家獨立公司時，其總部設在加利福尼亞州的山景城。  5.3.2 絞殺者模式  絞殺者模式是只保留原來的老舊系統不變，當需要開發新的功能時，重新開發一個服務，來實現新的功能。通過不斷的建置新的服務，逐步讓老舊的系統失效，最終取代它。     ✅ 優點   不會遺漏原有需求  可以穩定地提供價值，頻繁地交付版本，可以更好的監控其改造發展  避免  閉門造車 的問題  ❌ 缺點   架構改造的時間會大幅地變長  產生一定的跌代成本  5.3.3 修繕者模式  修繕者模式是指將老舊系統的部分功能與其餘部分隔離，以新的架構進行單獨改善。在改善的同時，需要保證與其他部分能協同工作。    ✅ 優點   系統外部無感知  不會遺漏原有需求  可以隨時停下改造工作，響應高優先權的業務需求  避免  閉門造車 的問題  ❌ 缺點   架構改造的時間會大幅地變長  會有更多額外的架構改造成本  巨石應用轉變為微服務的改造\n   5.3.4 資料庫的拆分方法  關聯式資料庫很可能是巨石應用中最大的耦合點。對於有狀態的服務改造，我們需要非常小心地處理資料庫資料。做資料庫拆分時，應該遵循以下步驟：   詳細瞭解資料庫結構，包括外來鍵、共享的可變資料以及事務性的邊界等等。如(a)圖  先拆分資料庫，可以參考 12.3.2 節中介紹的資料庫遷移。如 (b)圖  當寫入兩邊的資料庫都無誤時，找到程式架構中的細縫，如 (c)圖  將拆分的程式模組和資料庫組合在一起，形成微服務。如 (d)圖    💡 應該圍繞業務目標進行架構改造  \n對巨石應用進行拆分時，可以先拆分顆粒度相對較大的服務。當拆分完成後，如果達到拆分的目標，那麼就可以停下來，不應該為了架構而架構，為了技術而技術。當拆成微服務架構時，你必須考慮要建立相應的基礎設施，例如：服務管理、服務監控、自動化測試與自動化部署。  5.4 小結  本章節主要討論”  持續交付 2.0 能力 ”對軟體系統架構的要求，在軟體開發時就考慮了下列項目 👇，並且 �� 論系統架構的拆分原則   可測試性  易部署性  易監控性  易擴展性  可能的失敗處理  常見的三種軟體架構模式在不同場景的分析和比較：   微核架構模式，通常是應於 Client 端軟體  微服務架構，通常適用於大型後台 Server 端系統  巨石應用架構，通常適用於創業公司或中小型專案  討論了三種系統架構改造的方式   拆遷者模式，一次性的重寫所有程式碼。  絞殺者模式，就是不改變或少改變原有老舊系統，通過增加的新服務來不斷替代老舊的功能。  修繕者模式，就是通過跌代的方式，將原本老舊系統逐步進行改造，同時開發新的功能。  也介紹了解決絞殺者模式和修繕者模式中，可能會遇到資料庫中的資料表和資料的拆分以及拆遷問題。   為了持續交付，並且降低架構改造的風險，團隊需要根據情況，來採用絞殺者模式或修繕者模式對遺留系統的架構進行改造。",{"id":168,"path":169,"dir":159,"title":170,"description":7,"keywords":171,"body":179},"content:4.cicd-2.0:7.chapter7.md","/cicd-2.0/chapter7","07 部署流水線原則與工具設計",[65,172,173,174,175,176,177,178],"7.1 簡單的部署流水線","7.2 Pipeline 的設計與使用","7.3 Pipeline 平台的組成","7.4 基礎支撐服務雲端化","7.5 企業成品庫的管理","7.6 多種多樣的部署流水線","7.7 為開發者建置自助式工具","  前言  Deployment Pipeline 為 CI 的核心，能完整呈現軟體交付的整個過程。從程式碼完成後的提交、建置、部署與測試到正式的發布，除了可以清楚知道整個歷程外，也可即時得知提交進度。  那我們該如何根據團隊與不同產品去設計整個 Pipeline?此章的重點就在談這件事情。  \n部署Pipeline受到軟體架構、Git Flow及團隊與產品不同而有所不同設計概念。\n  7.1 簡單的部署流水線  此節以實際例子來簡易 OverView 在 Pipeline 設計上會有那些環節與實際情境。書中例子以  Curise 為範例，他就像今日我們很常聽到 Jenkis，是一個以 Java Base 開發的持續整合工具。其程式碼高達 5 萬行，而自動化單元測試及整合測試 Case 就多達 2350 個，端對端測試為 140 個，在架構也算蠻龐大的軟體系統。  但他在 2010 年就停止維護了，有興趣了解可以到他的  官方網站 。Curise 在 2010 年停止維護後更名為 GoCD，並走 Open Source 開發。並將 Source Code 放置  Github 。  7.1.1 GoCD 簡單的產品研發流程  GoCD 算典型的持續整合代理伺服器架構，其架構如下，GoCD Server 提供使用者 UI 及 Pipeline 腳本控制及指派工作，讓 Agent 去執行 Pipeline 過程中需要執行的 Command(此處簡單帶過)。另外一提，他使用的版控工具為 Mercurial 不是一般主流的 Git。    維護此產品的團隊人數約為 12 人，產品的交付其中與迭帶週期為一周。在這麼快速的迭帶週期，團隊也使用 CICD，在每個迭帶結束後，用新版本替換掉目前團隊在使用的舊版本，並在每兩個迭帶後將試用版本部署到公司內部的公用伺服器，若公司內部試用版本使用到一個品質檢測標準，一周後再將版本交給企業試用。其週期如下圖所示，   白正方形:單周 Blood 版本(團隊)  灰正方形:雙周 Alpha 版本(公司)  白圈:雙周 Beta 版本(外部企業)  大圓圈:全球發布  \n     \n  7.1.2 初始 Pipeline 設計  GoCD 的 Pipeline 設計是 Base on 六步提交法理論，六步如下   第一步: Clone 成功版本至本地端  第二步: 修改程式碼  第三步: 本地端 Build && Test  第四步: Pull Merge 其他人程式碼再跑一次 Build && Test  第五步: 提交  第六步: 進 Pipeline  \n     \n  感覺六部提交法是針對新的小碼農制定的口訣，避免小碼農 Clone 錯誤的程式碼或是沒有做好 Build Test 就提交程式碼，會 Focus 在個人建置部分。  我們來看 GoCD 的 Pipeline 設計如下圖，整個提交整合與部署分為八個 Stage 站別，有手出現的 Icon 代表人為去介入  \n     \n  基本上每個站別都只有一類任務類型，   1.提交建置:Build 與 單元集成測試  2.次級建置:End To End 測試(Windows && Linux)  3.將打包好的檔案部署到 UAT 環境  4.測試人員驗證完後 Tag 驗收通過，並對 Pipeline 點擊繼續  5.做自動化性能測試(此處沒說明做什麼性能測試)  6.將 Alpha 版本部署到公司內部伺服器給團隊試用  7.試用 OK 後發布 Beta 版本給外部企業試用  8.外部企業試用試用完成後正式發布  \nUAT，(User Acceptance Test),使用者接受度測試 即驗收測試，主要是用來作為客戶體驗的環境。\n  基本上這 Pipeline 設計大部分專案情境可能都會經過這些步驟，只是也許後面部署與測試順序不一定會完全相同。例如有些企業會覺得 UAT 測試完後其實就可以上 Production，但 GoCD 為了嚴謹在部署測試上又多了好幾個站別，確保 Product 能完全運行順利。  另外值得一提的，在這個設計中，GoCD 在單元集成測試的時間非常長，有五個集成測試，基本上每個集成測試要花 15min，而第二站別的次級建置端對端測試也多達 140 個測試項目，需要最長時間為 30min....  7.1.3 Pipeline 狀態解析  管線運型實際狀況如下，可看到在建置編號 12 版本，在 UAT 部署就停了下來。也許這建置版本並沒有新功能，所以可以在這階段就停止。而建置編號 13 在次級建置跳過，有可能因為 12 還沒完成。至於此處是手動按停止還是自動停止就不太清楚。  不過此節要表達的是，再多 Job 管線運行上，會根據不同狀況管線過的 Stage 狀況也會不太一樣。  \n     \n  7.2 Pipeline 的設計與使用  在介紹完 GoCD Pipeline 的實際使用狀況，大體上應該會曉得基本 Pipeline 會有什麼工作需要運行。那我們該如何透過設計去優雅的使用 Pipeline 呢?  7.2.1 Pipeline 的設計原則  書中提到有 5 個觀念去設計 Pipeline    一次建置，多次使用 。Pipeline 上的任務要產生部署使用的檔案，盡量在前面的站別就一次建置完成，並可直接讓後面的站別做使用。盡量不要在後面在別再做重複性建置。另外如果後續的站屬如果要使用此編制檔案，也必須此部署檔案來源是與上流站別是同一份。   與業務邏輯松耦合 。簡單來說在就是不要為了方便，將一些部署所需使用的腳本或資料放在 Pipeline Server 上，盡量與 Pipeline Server 不要有編譯耦合的設計。相反，若有相依檔案或腳本，我們必須存放在存取庫中，照樣就可以輕鬆對這些腳本設計做修正。簡單來說，對於 Pipeline，他就像是調度、執行與紀錄者，他只需要知道整個調度流程，不需要知道如何建置與部署軟件需要自己提供哪些東西。   並行化原則 。如果有五個自動化測試任務，我們也可以設計並行 Pipeline 同步跑這五個測試任務，並即時提供結果訊息，從而修正問題。若使用併行，整體等待回應的時間就能大幅收短。就像前面提到 GoCD 有五個測試集成，若使用並行化原則就能大幅縮短測試時間。   快速回應結果 。如果資源較貧乏(例如 Runner 規格特爛)，在 Stage 的設計，我們可以將依些運行較快的自動化驗證優先做執行，較慢與消耗資源較多的放在後面執行。感覺這邊得意思 花較長時間的測試，就放在越接近正式上線前做，來縮短前面開發測試時間。   重要的回應結果優先 。呼應第四點，雖然為了達到快速回應結果將一些較快的測試放在前面做，但依然要以重要優先權高的為主。  7.2.2 團隊紀律  1.立即暫停原則  Pipeline 一出問題，團隊需要有人立即去處理，而不是放任他不管。再問題修復前，禁止任何人提交新的程式碼。  2.安全審核原則  所有代碼與軟件包都需要有偷過板控及審核完畢才可使用。  7.3 Pipeline 平台的組成  這章節主要述說 Pipeline 的主要組成區塊  7.3.1 工具鏈整體架構  書中提出的圖有點難去了解，在這我拿一張以 Github 結合 Jenkis 的架構圖去輔佐解說(Jenkis 那張圖先不管架構 Solution 是否為最好)。    Pipeline 整體架構主要分成、唯一信受源(程式碼與打包物存取庫)、調度及展示(調度器)以及基礎支撐服務(測試、部署等實際執行環境..)  1.唯一信受源  在 Pipeline 過程中，團隊角色若對任何訊息產生質疑時，要做追溯都應該要能追回到存取庫裡的產出物(要部署檔案)，而在存取庫中的打包檔案都可以找到對應程式碼及他相依的類別庫檔案，或是能找到下載的 URL Source。  2.調度及展示(Pipeline)  能接受不同的服務基礎平台，且具有調度不同任務，完成整個交付流程的功能。並此能展示整個過程的歷史訊息。  3.基礎支撐服務  一間較大的公司具有相對應的建置、測試及部署的服務。規模較不大的通常前兩個通常就直接在一個 Runner Execute 直接做掉。那如果是前者，在 Pipeline 設計上就要考慮如何去與這些基礎支撐服務去連通與協作，讓整體 Pipeline 過程中達到最大的效益性。  7.3.2 平台應具有的基本能力  Pipeline 事團隊多角色的統籌協作系統，因此關注的是軟體在 Pipeline 的流動效率，包含部署與上線，過程能精準展現個環節的狀態與訊息，並能在不增加團隊負擔情況下自動收集各環節產生的數據。例如，衡量某一功能的開發週期。  此章節提到重點就下述兩點，   1.追溯能力  2.重新建置能力  針對 1 不多說，簡單來說就是對於事件能查詢他所有的歷程。對於 2，只要存取庫程式碼依舊存在，就算遇到版本出問題，依舊能再次修改重新做自動部署。又或是自動化失敗因為對應服務環境出問題，在對應環境 Recovery 後，能再次重新運行測試。  7.3.3 工具鏈建設策略  上述 7.3.1 圖中提到的平台架構，可看出他是由不同的工具與子系統組成。因此我們可以根據公司的習慣與策略去客製設計。例如 GoCD 團隊因為在自動化測試量較龐大，因此就自行開發一個自動化測試分組插建，由此插件自動將所有測試分配到不同任務哩，並將這些任務分配到多個測試環境中執行。但對更大型的公司，其環境會更加複雜，其各產品組件之間的關聯關係也會更加龐大複雜。為了發揮持續交付的威力，上述提到的各類支撐服務雲端化也成為必要選項。例如 AWS、Facebook 與 Google 都具有自己的 DevOps 平台工具鏈，甚至將其中一部分工具開放給 Open Source。  那此服務系統間個詳細的關係是什麼? 7.4 章節會述說這件事情。  7.4 基礎支撐服務雲端化  大多大公司服務端程序部署頻率都非常快，幾乎都又自己的雲化支撐服務，如下表    7.4.1 基礎支撐服務協過過程  此章節為稍微大致講解整個管線與基礎建置服務互動的過程，大分類上分三個步驟來看，    第 0 步:環境準備(yml 設定)  這部白話一點就是整體部署有哪些設定及有哪些基礎服務建設，針對 Stage 任務對應相關設定指派相對應基礎建設服務準備。  第 1 步:提交建置(建置，測試)  將 Source Code 交至相對應服務做健置與測試，建置管理服務會將代碼從程式存取庫中提出，然後在建置環境建置打包後，放入成品庫。  接著部署管理服務根據 Pipeline 定義將編譯好的成品，放到測試環境測試。如果測試需要一些比較特別的設定，則同時從部署配置讀取相關配置，成功後就開始執行 Pipeline 的測試任務。  第 2 步:次級建置(部署，測試)  當測試完成後，部署管理服務會再從成品庫中拿去成品，並從部署配置讀取 UAT 相關部署訊息，將兩者結合，部署到 UAT 環境進行端對端測試。  第 3 步:部署生產環境  當次級建置 UAT 測試完成後，部署管理服務會再次從成品庫取出成品，並讀取配置訊息，部署到正式環境。  7.4.2 建置管理服務  構成分三個區塊   任務管理  調度  執行器    每個區塊服務都有柱列緩衝，任務管理將任務交給調度器後，調度器會根據一定的調度算法選擇建置任務將其發送相對應的執行器編譯。例如 c#代碼若為 Net Framework 則指派到 Windows 環境下編譯，C++則指派到 Linux 下環境編譯。而集群管理器則是管理這些執行器的建立與狀態管理(繁忙、空閒、失去連線..)。  執行器為建置任務的代理，集群中可以有多個執行器，每個執行器會根據收到訊息對應的存取庫 URI 檢出代碼並根據要求編制建置任務。建置完成後會將指定產物(部署需用檔案)放到成品庫中。並向 Pipeline 回報執行結果。  另外可以看到圖中左邊 Request 輸入點有個人頭，代表這架構也允許工程師在本地編寫期間就可以直接使用此服務。  7.4.3 自動化管理服務  構成分三(四)個區塊   任務管理  調度  執行器  測試健康管理    針對任務管理、調度與集群管理猶如 7.4.2 所說，大致上是一樣的意思，只是編制任務變成測試任務。值得一提的是測試健康管理。當測試在不同節點或資源條件下，因為失敗重複執行太多次(書中寫 1000 次)。  此時測試健康管理器就會將不穩定的 Case 拉到不穩定池並通知團隊做處理。這邊健康管理器英文對應一時查不太到....所以如何實作也不太清楚。  7.4.4 軟件部署管理服務  雖然在 Pipeline 過程中的測試都沒問題，但也有可能到了實際正式環境會出錯。原因在於測試環境與正式環境還是會有一定的差異性。  書中舉過內很多大型企業的生產環境用的 J2EE 應用服務器都是商業軟件，但因為過於笨重，所以用語法檢查就不嚴格的 Tomcat。此時如果部署到企業的正式生產環境，就會產生有些頁面因為 Html 標籤不匹配而發生錯誤。  好，上述講一講不是下圖重點..下圖要表達的是，因為為了協調運維部門與產品部門的合作。之間的接口建議有個正式產品庫及上線單。此時運維部門就可以根據上線單，從正式產品庫拉取相對應的產品至正式(生產)環境步數。    7.4.5 基礎環境管理服務  為建置、測試、部署管理提供環境準備與監控服務。能接受這三種環境的請求為期準備相對應的環境。隨著 Docker 技術成熟，配置基礎環境慢慢以 Docker Image 形式，需要時直接啟動，並提供服務。    7.5 企業成品庫的管理  企業成品庫是部署流水線工具鏈中企業的受信源之一，也是企業信息管理中的一個重要節   \n點。只有通過安全驗證的軟體包才會被納入成品庫，並且安全驗證部門也應定期對存儲的   \n對存儲的內容作安全掃描及清理。  7.5.1 成品庫的分類     臨時軟體包庫(A)   \n用於存儲團隊開發並通過流水部屬線生成程式碼的所有軟體包。  正式軟體包庫(B)   \n用於存儲通過流水線部署且經過安全驗證，被確認能夠發布到生產環境或使用者的軟體包。  外部軟體包庫(C)   \n指該軟體包的程式碼並非由團隊管理或維護，但在開發中所使用到的其他軟體包。這些軟   \n體包通過互聯網或是第三方取得，亦將其存儲在成品庫中。   \n外部軟體包一般存儲的形式可能包含 3 種：   \n(1) 以二進制的方式保存。   \n(2) 以程式碼副本的方式保存。   \n(3) 以外部連結地址的方式保存。    臨時鏡像庫(D)、正式鏡像庫(E)、外部鏡像庫(F)   \n基本上同軟體包庫，只是以鏡像的方式作存儲。  7.5.2 成品庫的管理原則  成品庫中，每個成品都應該有標示，並且連同其來源、組成的部件以及用途等，一起保   \n存為該成品的信息。所有成品都要能夠追溯至源頭， 包括臨時成品庫中的成品。  7.6 多種多樣的部署流水線  7.6.1 多組件的部署流水線  若一個軟體產品由多個組件建置而成，每個組件都有獨自的程式碼倉庫，並且每個組件   \n由一個單獨的團隊負責開發與維護，整個產品的部署流水線的設計通常與下圖相似。    每個組件的部署流水線成功以後，都能觸發下游的產品集成部署流水線。而這個集成部   \n署流水線的打包階段，會自動從軟體包庫中獲取每個組件最近成功的軟體包，然後對其   \n進行產品打包，再觸發集成部署流水線的後續階段。  7.6.2 個人部署流水線  每名工程師創建了自己專屬的部署流水線，用於個人在未推送程式碼到團隊倉庫之前的   \n使用。個人的部署流水線並不會部署到團隊共同擁有的環境中，而是僅覆蓋個人開發環   \n節。    工程師通過部署流水線的模板功能，複製一份團隊部署的副本。並僅保留兩個階段(提   \n交建置、次級建置)的內容。令工程師能夠監聽自己程式碼倉庫的變化，並且自動化去   \n觸發。當開發人員提交程式碼到個人倉庫時，都會自動觸發個人專屬的部署流水線。  這樣做的好處有 3 個：   個人部署流水線與團隊的部署流水線共享建置及測試環境。  保證每個工程師都能利用到相同的測試資源，加快個人檢驗的速度。  個人部署流水線的測試用例與團隊的部署流水線的驗證集合相同，因為是相同的建置資源，若是發生建置失敗，則可以容易的定位到問題點。  7.6.3 部署流水線的不斷演進  截止到 2018 年 4 月，GoCD 的社區版本每月會發布一次正式版，而其團隊的複雜的部署流   \n水線設計也已演變如下圖所示：    構置 Linux 包這個部署流水線中，包含兩個階段。第一個階段是 build-no_server。多   \n個任務並行執行，構置組成 Server 所需的多個 Jar 包，也並行執行 Java 測試用例和   \nJavaScript 單元測試用例。這體現了部署流水線盡量並行化原則。第二個階段是   \nbuild-server，使用經第一個階段己初步驗證通過的多個 Jar 包組裝成 Sever 包。  Linux 驗收測試這個部署流水線中，也包含兩個階段。第一個階段是運行高優先級的功   \n能測試，第二個階段是對插件部分的自動化功能測試，這體現了部署流水線的快速反饋   \n優先原則。  而在後續的各類測試(如驗收測試、回歸測試或者功能測試)中，被測試的二進制包均來   \n自前面各部署流水線的產出物，而且確保其使用同一程式碼版本。  7.7 為開發者建置自助式工具  優秀的互聯網公司採用了一種工具平台的設計理念，即為開發工程師設計他們認為好用   \n的工具。這種方式要求創建強大的工具平台，能夠很好地支持開發工程師做產品服務。  例如 Facebook，開發人員可以通過他們內部平台看到自己的程式碼已經發佈到哪個階   \n段，有多少用戶在使用。開發工程師在不需要任何人幫助的情況下，就能夠了解他的程   \n式碼已經發佈到哪個階段了。    例如電商公司 Etsy，開發工程師可以查看到自上次生產部署以後，每次的程式碼變更   \n數量，並且非常方便地查找程式碼差異。  ",{"id":181,"path":182,"dir":159,"title":183,"description":7,"keywords":184,"body":192},"content:4.cicd-2.0:8.chapter8.md","/cicd-2.0/chapter8","08 利於集成的分支策略",[65,185,186,187,188,189,190,191],"8.1 版本控制系統的使用目的","8.2 常見分支開發模式","分支策略","8.3 分支模式的演化","8.4 分支策略","8.5 小結","Additional Reference:","  前言  我們已經討論過如何將需求拆分成多個可交付、可驗收的用戶故事，以及如何將它們安排到我們交付迭代的過程中。接下來，本章將介紹研發團隊通過原始碼倉庫，高效組織團隊多人并發協作的方法，即代碼分支策略。分支策略的選擇對持續交付的成本與效果有很大的影晌。  8.1 版本控制系統的使用目的  版本控制系統(Version Control System）主要用於存儲及追蹤目前（文件夾）和文件的修訂歷史（這里的修訂操作包括 3 類：新增、修改和刪除），從而讓你能夠回溯那些被納入其管理範圍之內的任意對象的任意一次修訂。其最本質的作用是回答\"4 個 w\"，即在什麼時間(When)、修改了什麼內容（What),是誰修改的（Who)以及為什麼要修改（Why),。其中最後一個\"W\"是通過用戶提交代碼變更時書寫提交注釋(Comments）的方式提供的。現在，版本控制系統已經成為團隊合作共同交付軟體過程中所用到的重要協作管理機制，是軟體公司的基礎設施。其目標是支持軟體配置管理活動，追綜和紀錄多個版本的并發和維護活動。根據版本控制系統的運作方式，目前市面上的主流版本管理系統被劃分為集中式版本控制系統和分佈式版本控制系統兩種類型。  8.1.1 集中式版本控制系統  集中式版本控制系統的出現，解決了多人如何進行協同修改代碼的問題。這類版本控制系統，都有一個單一的集中管理的版本控制管理服務器，保存所有文件的歷史修訂版本紀錄。團隊成員之同的代碼交換必須通過客戶端連接到這台服務器，獲取自己需要的文件。每令人如果想獲得其他人最新提交的修訂紀錄，就必須從集中式版本控制系統中獲得。此時，客戶端并沒有整個集中式倉庫保存的所有內容，而是根據用戶的指定命令，一次僅能獲取倉庫中的某一次代碼文件快照。集中式版本控制系統示意圖如圖 8-1 所示。當工程師修改了部分代碼，但尚未完成全部工作時，如果希望將這個中間成果保存成臨時版本，做個備份時，則他通常只有兩種選擇：一是複製一份到另一個本地目錄中；二是直接提交到中央倉庫。而直接提交未經過質量檢驗的半成品到中央倉庫，可能會影響原有的功能，妨礙團隊其他人工作。這種類型版本控制系統的典型代表是 Subversion,簡稱為 SVN。    集中式版本控制系統有兩點劣勢。首先，在網路環境不佳的情況下，同步大量文件時會經常失敗。2007 年底，GoCD 團隊使用 Subversion 作為版本管理工具。軟體產品研發團隊主要在北京工作，銷售人員在美國，售後支持人員在印度斑加羅爾，源代碼倉庫所用的 Subversion 服各器部署在美國芝加哥。有一次，開發人員從北京到斑加羅爾出差，找了一台新計算機，想在當地公司辦公室修改并提交代碼，但手上沒有源代碼。於是，打算將源代碼庫從版本控制庫檢出到這台新計算機上。然而源代碼庫稍大，印度辦公室的網路不穩定，前後花了數個小時，也沒能將代碼從中央服務器拉取到這台計算機上。  其次，集中式版本服務器具有單點故障風險。假如 Subversion 服務器檔機一小時，那麼在這一小時內，誰都無法提交更新，也無法從服務器獲取文件。最壞的情況是，如果服務器的硬碟發生故障，并且沒有做過備份或者備份不及時，則會有丟失大量數據的風險。那次事件以後，GoCD 團隊將源代碼倉庫從 Subversion 遷移到了 Mercurial，它是一款分佈式版本控制系統，簡你為 Hg, Facebook 也在使用它。  8.1.2 分佈式版本控制系統  分布式版本控制系統與集中式版本控制系統的區別在於多個服務器共存，每個人的節點都是一個代碼倉庫，所有的節點都是平等的。在團隊協作過程中，通常會指定某個節點作為團隊的中央服務器，如圖 8-2 所示。    分布式控制系統的特點是：提交（commit)操作都是在本地進行而無須經過服務器，因此提交速度也更快。只有當需要向其他人或遠端服務器做文件提交或同步時，才通過網路將其推送到遠端倉庫或從遠端倉庫拉取。因此，即使在沒有網路環境的情況下，你也可以非常愉快地頻繁提交更新。當有了網路環境的時候，再推送到遠端的團隊代碼倉庫。目前主流的分布式版本控制系統是 Git。  前面提到 GoCD 團隊工程師在印度遇到的情況，如果使用分布式版本管理倉庫，即使網路不穩定，也可以比較方便地完成代碼拉取操作，如圖 8-3 所示。  (1) Bob 通過斑加羅爾辦公網路克隆（clolle）一份 Sara 的代碼倉庫（sara 也在斑加多\n羅爾辦公室）。\n(2) Bob 從芝加哥的中央倉庫中拉取（pull）與本地倉庫有差異的代碼。\n(3) Bob 修改代碼文件，井提交（commit）到本地倉庫，產生一個新的文件版本。\n(4) Bob 將這個新的版本推送（push）至中央倉庫。\n(5) Sara 即可從中央倉庫拉取（pull）所有的差異代碼。    8.1.3 版本控制系統中的基本概念  版本控制系統要解決的核心問題是多人協作過程中的文件版本管理問題。目前所有的版本控制系統中都有幾個相似的概念，用於協調多人協作。在具體討論多人協作模式之前，因爲有多種版本控制系統，所以有必要對這些概念進行統一定義，以方便後續的討論。   代碼倉庫（codebase）是指一個包含一組文件所有歷史修改信息、的還輯單位，通常用於保存有關一個軟體產品或某一組件的所有文件信息紀錄。  分支（branch）是指對選定的代碼基線創建一個副本。人們可以對這個副本中的文件進行操作，而這些操作與原有代碼基線的文件操作是互不影響的。  主幹（trunk/master）是一令具有特殊意義的分支（branch)，通常在創建代碼倉庫時即由版本控制系統默認創建，每個代碼倉庫有且僅有一個這樣的分支。其特殊意文在於其於軟體的開發活動和發布方式緊密關聯，例如，在 SVN 中以\" trunk \"命名的分支和 Git 中以\" master \"命名的分支都是主幹分支，我們將在分支模式進一步討論它們的特殊意義。  版本號（revision）對應在某個分支（ branch ）上的一次提交操作，是系統產生的一個編號。通過這個編號，你可以獲取該次提交操作時的所有文件鏡像。在 SVN 中，它叫作 revision，是一令連續變化的正整數。而在 Git 中，它是一令 40 位的散列值，類似於\"734713bco47d87b··…65ae793478c50d3\"這樣一段字母與數字的組合。為了方便使用， Git 可以使用該散列值的前幾個字符來視別某次提交，只要你提供的那部分 SHA-1 不短於 4 個字符，并且沒有歧文即可。  標籤（tag）是某個分支上某個具休版本號的一個別名，以方便記憶與查找。你可以通過版本控制工具自身提供的命令來創建這個別名。  頭（head）是指某個分支上的最新一次提交對應的版本號。  合併（merge）是指將一令分支上的所有內容與某個目標分支上的所有內容進行合并，并在該目標分支上創建一個新版本號。  仲突（conflict）是指在合併操作時，兩個分支上的同一個文件在相同位置上出現不一致的內容。通常需要人工介人，確認如何修改後，方可合併目標分支。  依據上面的定義，通過下面的字串記憶方式可以唯一確定某個代碼鏡像：\n{代碼倉庫名｝:｛分支名｝:｛版本號｝或者｛代碼含庫名｝:｛分支名｝:｛標籤｝  8.2 常見分支開發模式  目前基於版本控制系統的開發模型，根據新功能開發以及版本發布所用的分支進行分析，主要有 3 種，它們分別是：\n(1）主幹開發，主幹發布（Trunk-based Development&Release);\n(2）主幹開發，分支發布（Trunk-based Development&Branch-based Release);\n(3）分支開發，主幹發布（Branch-based Development&Trunk-based Release)。\n下面我們分別介紹一下它們各自的特點。  8.2.1 主幹開發，主幹友布  顧名思義，\"主幹開發，主幹發布\"是指工程師向主幹上提交代碼（或者每個分支的生命周期很短，如數小時，或少於 1 天），并用主幹代碼進行軟體交付（如圖 8-4 所示）。也就是說，所有新功能的開發，代碼均提交到主幹（trunk）上；但需要發布新功能時，直接將主幹上的代碼部署到生產環境上。    根據交付頻率不同，可以分為低頻交付和高頻交付兩類。低頻交付模型常見於一些周期比較長的大型軟體開發項目，也是一種最古老的軟體開發模式，當時的 IT 行業是以數年或數月為一個交付周期。\n在低頻工作模式下，其主幹代碼總是長時間處於不可用狀態，只有在項目內所有功能的代碼開發完成後，才開始進行軟體聯調和集成測試工作。\n在開發期間，版本控制系統的作用僅僅是確保代碼不丟失，是純粹的代碼備份倉庫。高頻交付子類型是指代碼庫中的代碼發布頻率較高，通常每天都會發布一次，甚至多次。\n高頻交付子類型常見於具有比較完備的交付基礎設施（自動化配置構建、自動化測試、自動化運維、自動化監控與報警等）的互聯網產品團隊，通常也有快速缺陷修復能力，尤其適用於後台服各端產品形態（如 Web 網站或 SaaS 軟體的後台服各）。\n這種模式的優點在於分支方式簡單，因此分支管理工作量較少（如代碼合并成本）,但也存在弱點。例如，針對低頻交付模式，其項目後期的缺陷修復階段，并不是團隊所有人都需要做缺陷修復，會有一定的資源浪費。針對這種情況，很多團隊會釆用後續介紹的\"主幹開發，分支發布\"模式，下面會詳細介紹。\n針對高頻交付模式，由於多人向主幹上頻繁提交代碼，其代碼變動非常快。假如某個開發人員拉出一個私有開發分支，并在該開發分支上進行開發，開發完成後再合并回主幹。此時，他只有兩種工作方式。一是每天從主幹上更新代碼到他自己的分支上。此時該開發人員很可能每天需要一兩個小時將主幹上的代碼與自己分支上的代碼進行合并。二是不做每日更新，而是一段時間後（例如在分支上開發完成功能）之後，再向主幹合并。此時，很可能由於主幹上的代碼變化太大，導致自己這個分支上的代碼已經無法再合并回去了。  無法完成的\"合并任務\":  2011 年，百姓網（一今生活分類網站）的研友團隊只有 12 名工程師。他們使用高頻交付模式，每天早上 7 點做一次生產環境發布。為了對某個重要模塊迸行重大重構，其技木負責人曾經創建了一個專有分支。然而，一周以後，他不得不宣布放棄該分支的所有代碼，因為其他工程師在主幹上己經做了太多的改動，專有分支已經無法合并回主幹。  \"未開發完成的功能代碼不能帶入將要發布的版本裡\"曾被認為是一和最佳軟體質量管理實踐。然而，在這種高頻交付模式下，很難再遵守這一實踐。相反，底該允許提交未完成功能的代碼，前提是不影響用戶的正常使用和發布。為了使未開發完成的功能不影響發布質量，可以使用一些特殊技術管理手段（如開發技術或抽象分支方法等）來處理這類問題，自然，這些手段也會序生一定的管理開銷。詳細方法參見第 12 章。與此同時，高頻交付模式也要求質量保征活動能夠做到既快速又全面。  8.2.2 主幹開發，分支發布    這種開發模式是指(圖 8-5)：   開發人員將寫好的代碼提交到主幹；  當新版本的功能全部開發完成或者已經接近版本發佈時間點的時候，從主幹上拉出一個新的分支；  在這個新的分支上進行集成測試，並修復缺陷，進行版本質量打磨。當質量達標後，再對外發布該版本。  其特點如下：   主幹代碼提交活動頻繁，對保障主幹代碼質量有較大的挑戰；  分支只修復缺陷，不增加新功能；  新版本發布後，如果發現嚴重缺陷，而且必須立即修復的話，只要在該版本所屬的分支上修復後，再次發布補丁版本，然後將分支上的修改合併回主幹即可。也可以在主幹上修復缺陷，然後將針對該缺陷的修復代碼挑出來（ cherry-pick ）合井到該缺陷所在的分支上。Facebook 的移動端產品開發流程就使用後面這種方式。  通常，發布分支的生命週期不應該持續時·間過長，一段時間後應埠終止該分支上的任何操作活動，例如，圖 8-6 中 Vl.01 發佈點之後， Vl.0 分支應該結束。\n在\"主幹開發、分支發布\"模式下，從拉出發布分支開始，到分支代碼達到可交付狀態的時間週期可以作為評估主幹代碼質量的指示器，我們稱之為\"質量打磨週期（ Branch Stabilization Time ）\"。打磨週期越短，說明主幹代碼質量越好。當質量打磨週期極短時，就可以轉換到高頻的\"主幹開發，主乾髮布\"模式。當然，做到這一點並不容易，需要結合本書其他部分所描述的原則、方法與實踐，方能游刃有餘。  該模式的優勢在於：   與將要發布的新功能無關的人員可以持續工作在開發主幹上，不受版本發布的影響；  新發布的版本出現缺陷後，可以直接在其自己的版本發布分支上進行修復，簡單便捷。即使當前開發主幹上的代碼已經發生了較大的變化，該分支也不會受到影響。  其不足在於：   主幹上的代碼通常只能針對下一個新發布版本的功能開發。只要新發布版本的任何功能在主幹上還沒有開發完成，就不能創建版本發布分支，否則很有可能影響下一個發布的開發計劃，開源項目在發佈時間點以及特性功能方面的壓力小一些，因此常常採用這種分支方式；  使用這種開發模式，對發布分支的數量不加約束，並且分支週期較長，很容易出現\"分支地獄\"傾向，這種傾向常見於\"系列化產品簇＋個性化定制\"的項目，例如某硬件設備的軟件產品研發的分支模式，如圖 8-6 所示。    該硬件設備最初只有一種類型，其類別定義為 A ，型號是 x ，對應軟件的發布版本為 Ax1.0 發布以後，客戶提出了同類別不同型號的緊急需求，公司為了能夠快速響應客戶需求，從 Ax l.0 的產品分支上又拉出一個產品分支，名為 Ay 分支，其發布版本為 Ay2.0 。然後又在 Ay 的基礎上開發了一個增強版 Az，對應的分支及時間點如圖 8-6 所示。隨後在 Axl.0  上發現了一個嚴重缺陷，需要增發 A1.01 補丁版本。該缺陷在 Ay 分支和 Az 分支上也同時存在。因此就要將修復缺陷的代碼移植到主幹及 Ay 和 Az 兩個分支。\n該公司以這種管理模式支持了更多類別和型號的產品。如圖 8-6 中，公司開發了硬件產品 B，而其軟件版本是從主幹分支上拉出，並先後發布了 B1.0 和 B1.01。客戶需要在 B 類型上也具有 Ay2 和 Az3 上的部分新功能特性，於是，公司決定從 Ay2.01 和 Az3.0 的分支上移植該新功能的代碼到 B 分支上。  隨著硬件類 別和型 號的不斷衍生，研發團 隊效率越來越差。如圖 8-6 中的虛線處所示，團隊終將疲於在分支間移植代碼和測試。這與《大規模敏捷開發實踐：HP LaserJet 產品線敏捷轉型的成功經驗》一書中描述的 HP 激光打印機固件團隊在 2008 年的狀態相似。該團隊僅有 5% 的資源用於新功能的開發，而各分支間移植代碼會佔用團隊 25%的時間，如圖 8-7 所示。  8.2.3 分支開發，主幹發布     這種模式是指(圖 8-8 ) ：   團隊從主幹上拉出分支，並在分支上開發軟件新功能或修復缺陷；  當某個分支（或多個分支）上的功能開發完成後要對外發布版本時，才合入主幹；  通常在主幹上進行缺陷修復，質量達標後，再將主幹上的代碼打包發布。  這種模式的優勢在於 ：   在分支合井之前，每個分支之間的開發活動互相不受影響；  團隊可以自由選擇發布 i 哪個分支上的特性；  如果新版本出現缺陷，可以直接在主幹上進行修復或者使用 hotfix 分支修復，簡單便捷，無須考慮其他分支。  它的優勢也會導致不良後果，即為了分支之間盡量少受影響，開發人員通常會減少向\n主幹合併代碼的頻率，從而推遲了發現各分支中代碼衝突的時間，不利於及時進行代碼重\n構，如圖 8-9 所示。  該主幹上的代碼中原有一個方法簽名為 handleY(int b）。 Alice 和 Bob 各自領取一個新功能的開發任務，並創建了對應的一個分支 A 和 B ，而且，在新功能開發完成之前，兩人都沒有向主幹合併代碼。為了完成自己的新功能， Alice 對 handleY 方怯進行了修改，將其簽名變更為 handleY(int  ， boolean c）。同時， Bob 在自己的分支上也修改了 handleY(int b) 的內部實現。主幹上發生了兩次 hotfix ，兩人都將主幹的修改合入了自己的分支上。在這之後 ， Bob 又從 handleY(int b） 中抽取了一個方告， 簽名為 findX(int   a）。此時 Alice 開發完成了自己的新功能，將代碼（從 al 到 a4 ） 合入主幹。當 Bob 打算提交 代碼到主幹時，他需要將 Alice 的 4 次代碼變更與自己的 5 次變更合併在一起。由於 Alice 修改較大，這個合併很可能成了非常大的包袱。Bob 發現，Alice 不但修改了很多文件，而且對方戰 handleY（）進行了較大的重構。然而，這些還只是文本上的衝突，比較容易發現和修正。風險更大的則是語義上的衝突，即程序運行時的邏輯衝突。這類情況的發生會令團隊成員進行代碼重構的意願大大下降，從而令代碼的可維護性越來越糟糕。    不頻繁的集成導致的巨型代碼合併  如果分支過多，那麼衍生出來的問題是：當某個分支的生命週期（即從主幹拉出分支那一時刻至將其再次合入主幹這段時間週期）過長，代碼合併及驗收成本會快速增加。成本增加的數量與其生命週期中合入主幹的分支數量成正比。  若想成功使用這種模式，其關鍵點在於：   讓主幹盡可能一直保持在可發布狀態；  每個分支的生命週期應該盡可能短：  主幹代碼儘早與分支同步；  一切以主幹代碼為準，盡可能不要在各特性分支之間合併代碼。  另外，根據分支的存在周期和目的，\"分支開發，主乾髮布\"模式還可以進一步分為兩種子類型，它們分別是特性分支模式和團隊分支模式。   特性分支模式在開發過程中，允許多個開發分支同時存在，且每個分支對應一個功能特性的開發工作。當該特性開發完成後，立即合入主幹，其他尚未合入主幹的特性分支需要從主幹拉取主幹代碼，與自己分支上的代碼進行合併後，才能再合回主幹。這種模式為特性分支模式，如圖 8-1O 所示。    該模式的目的是：  讓團隊更容易在\"特性\"這個層次上並行工作，同時保持主幹的穩定可發布狀態。其優勢在於每次發布的內容調整起來比較容易。假如某個新功能或者缺陷在版本發佈時間點之前無桂完成，則不必合入主幹中，也不會影響其他功能的發佈時間點。  但這種模式也有不足：如果特性分支過多，會帶來比較多的合併成本。例如，每當某個特性分支開發完成打算合入主乾時，都需要與主幹的代碼合井，並進行質量驗證。一旦主幹代碼的質量驗證通過，其他分支此時都應該從主幹上拉取最近的通過質量驗證的新代碼。否則，如果在特性開發完成後再與主幹合井，那麼這種一次性合併會帶來較大的工作量和質量驗證工作。如圖 8-10 所示，特性 2 分支需要合併特性 1、3 和 4 的代碼。  假如有多個特性同時開發完成，怎麼辦？下面是兩種極端的做法。   所有已完成的特性分支一同向主幹合井，然後再共同設怯讓主幹代碼達到可交付狀態。這種方式通常會被特性團隊排斥。因為共同合併後，多方代碼交織在一起，出現的缺陷可能很難快速定位和快速修復。  所有已完成的特性分支排成隊列，以順序方式合入主幹。每個特性分支向主幹合人代碼後，必須使主幹上的代碼達到可交付狀態後，下一個特性分支才可以合入。這種方式通常是特性分支的常見做氈，也是特性分支的優勢所在。但所帶來的問題是，多個特性分支按排隊順序進行合井，會導致排在隊尾的特性分支等待較長的時間 。  如果想讓特性分支方式更好地工作，需要做好下面的管理。   每個特性分支的生命週期都應該很短，分支上的開發和測試工作盡量在 3 天內完成。這要求盡可能將\"特性\"拆分成小需求。關於需求拆分的方怯，參見第 6 章。  開發人員每天從主幹上拉取最新的可交付代碼，與自己的分支合井。  不要從其他特性分支上拉取代碼。    2.團隊分支模式  團隊分支可以看作是特性分支的一種特殊情況。也就是說，一組人一起在同一個分支上進行開發工作，而且該分支上通常包括一組相近及相關的特性集合的開發。由於是一組特性集合的開發，因此其分支存續時間比特性分支的存續時間長。  這種分支模式通常出現於規模較大的團隊（ 40 人以上）共同開發同一款產品，團隊被分成多個組，每組開發不同的系統組件。只有當一系列功能特性開發完成後，才對外發布新的軟件版本，很容易成為典型的瀑布開發流程，如圖 8-12 所示。    團隊分支模式在通信公司的產品研發或大型客戶端軟件產品研發中比較常見，例如第 14 章的案例中，團隊研發管理模式改進之前，就使用這種開發模式。成功應用這種模式的關鍵在於：   每個團隊儘早向主幹合入高質量的代碼，即使不馬上發布：  向主幹合入代碼後，盡快使其達到可交付狀態：  其他團隊儘早從主幹拉取可交付狀態的代碼，與自己分支上的代碼合井。  分支策略  8.3 分支模式的演化  8.3.1 三駕馬車分支模式  Chrome 瀏覽器於 2010 年使用此分支模式    開發 Branch 擁有足夠多的新功能 (或者準備 Release 時), 將該 Branch 使用   Cherry Pick  撿到 Pre-Release 分支上  Pre-Release   BugFix  Document  Deployment  Feature  Pre-Release 達到 Alpha 等級後 → 發布 Alpha 版本 (給予極少部分使用者先行體驗) → Beta 版本 (給予嘗鮮用戶進行體驗) → 收集存在的質量與 Bug 問題與修正 → Beta 版本穩定 → 合併至 Release 分支 → RC 版本 → RC 版本穩定 → 正式版本  8.3.2 GitFlow    Master  (Main) 分支是正式版本的發布分支   Release  分支用於品質打磨的預發布分支，如果 Release 的品質達標，將 Release 合併至 Main 分支與 Development 分支   Development  分支為 Feature 新功能整合的分支   Feature  分支是為了新的功能，開發人員從 Development 分支上 Checkout 出來的分支。當 Feature 開發完後，合入 Development 分支。    Gitflow 為特性分支模式與三駕馬車分支模式的組合   優點: 分支定義明確  缺點: 分支較多，具有特性分支的不足  8.3.3 GitHubFlow   名稱來自 GitHub 團隊的工作實現  對於開發者的開發紀律比較嚴格，對於品質保證的要求也較高。  Flow   從 Master (Main) 建立一個新分支, 以特性 (Feature) 或 缺陷 (Bug) 的編號 (Issue Number) 命名該分支。  在這個新分支上撰寫並提交程式碼。  功能開發完成後，並自行測試通過，建立 Pull Request (簡稱 PR)  其他開發人員對這個 PR 進行審查 (Code Review)，確保程式碼品質沒問題後，合併回 Master (Main)    如果 Feature 分支存在時間很短，則此模式可被認為是高頻率的   主線開發，主線發布  的模式。   8.4 分支策略  企業可以依照以下類型與條件，來確定適合團隊的分支方式   開發或維護的軟體產品類型  發布的頻率  團隊成員能力  基礎架構的等級\n   自動化測試  程式運行環境的管理  團隊紀律性  8.4.1 版本發布模式  版本發布的基本模式有三種:   專案制發布模式 ( Project Release Mode )  發布火車模式 ( Release Train Mode )  城際快線模式 ( Intercity Express Mode)    專案制發布模式  在軟體研發規畫中，先確認好某一版本需要的功能特性數量，只有當該次版本所需的功能全部開發完成並達到相對應的品質標準後，才能發布該版本。  發布版本的時間間隔並沒有強制的規定，而是根據新版本要求的功能集合開發完成並達到發布標準後，對所需的時候進行評估後決定。  此模式為最古老的發布模式，針對一個特定版本，確定了版本的功能數量與品質標準後，再估計版本交付的週期，等於先固定了功能數量與品質要求，因此團隊可能交付的時間點也就相對固定。  Pros:   可以確切知道每個版本包括哪些具體功能，有利於商業套裝軟體的銷售模式 (賣版本和授權，收取維護費用，當有心功能版本發行後，再向客戶收取新版本的升級費用。  符合人們的安全生產習慣，也就是不能把未完成的功能帶到即將發布的版本中。  Cons:   專案交付周期較長，參與人員眾多  如果開發週期因為某些原因導致需求變更 (如增加需求、修改原本需求實作方式或更換需求) 時，需要重新評估專案的交付時間，會連動影響那些原本能夠如期交付的需求。  需要等待所有需求全部實現完成後才能一起發布    發布火車模式  常見於大型套裝軟體。大型傳統軟體產業通常有許多產品線，各產品線之間存在非常複雜的相互依賴關係。為了能夠讓各個產品線協同發布。這些企業會為每條產品線都制定好每個版本的發布週期，也就是每個版本都像火車一樣，事先計畫好什麼時候發車。  \n💡 車是按照時間發的，能趕上火車的就一起走，趕不上車的，就等下一班車。\n    為了能夠準時發布，要求所有參與到該版本的開發團隊必須對齊該版本的每個開發階段。這種嚴格的時間一致性要求是因為如果該產品線的時間變更會引起其他產品線的時間變更，而這些更改也可能會影響到共享的集成測試環境的分配。  大多數的情況下，由於計畫和各產品集成與依賴關係，因此發布火車通常以一季為單位，但不會超過 10 個月。( *Note: 本書作者以大型企業來做舉例，但 Release Train Mode 不一定依照此週期為規範。)  當發布火車時間表時，發布管理團隊通常與負責個產品開發的團隊進行提前溝通，討論要發布那些內容，有時甚至需要幾個月的時間，將其結論發布在企業版本表中。    圖 8-17 為 LiberOffice 的發布火車時間表。提前制定出時間，目的就是讓各種業務與技術部門有足夠多的時間進行計畫，以便評估出各種依賴與影響。  制定發布計畫是一個 ˊ 非常正式與結構化的過程，需要有各種格式化數據以確保參加的團隊能夠對正式發布的可行性做出判斷。數據包含發布的詳細資訊( 相對標籤、名稱、部屬日期、風險級別、發布類型-企業,計畫或投資組合)、整個生命週期的各個階段及預定日期(如圖 8-18 )、每個階段要完成的活動與任務、里程碑時間、品質要求、以及管理發布火車的主要負責人。    Pros:   對於企業來說，可以通過並行多台車的方式，將突發需求排入一台發布火車  用戶可以提前體驗最新版產品提供的新特性，而不必影響原本生產線上的舊版本。體驗之後在決定要不要應用於自己的生產環境中  即便已經決定將這個新版本用於自己的生產環境中，也可以等到新版本成熟穩定之後再這麼做  Cons:  如果參與團隊的人數越多，溝同協調的成本會越高   3 城際快線模式  城際快線模式是指在發布模式三要素中，固定   時間  與   品質  兩個維度，且時間周期較短(一周，一天，甚至更少)，針對那些在 0 發布時間點已達到對應品質標準的特性進行一次發布。  跟火車發布的區別在於兩點:   發布周期較短，通常是兩周以內  負責功能開發的團隊可以自己選擇搭乘哪台城際快線，而不必在很久之前就先確定把時間確定下來。  這種模式常見於提供網際網路服務或 SaaS 服務的軟體公司。好處在於減少了團隊之間溝通協調成本。因為每個人都知道每次發布的具體時間點，所有工作任務都可以按照這個時間點提前進行協調。而且即使功能沒有及時趕上最近一次的版本發布，團隊也能知道這個功能是否可以在下一次發布的時間進行發布。Facebook 的 Web 網站於 2013 年部屬推送頻率以達到每天發布兩次，每周一次大版本。如圖 8-19 所示。    每個周日從主幹上拉一個發布分支，自動化測試驗證通過後，在公司內部人員開放(在公司內訪問，重定向到 latest.facebook.com)。運行過程中如果出現問題，可以在主幹上修復，然後分撿到 (CherryPick) 發布分支上。發布分支上代碼每天兩次更新到 latest.facebook.com，供公司員工內部開發使用。如果版本穩定，就對外發布，同樣是每天兩次。  自 2017 年開始 Facebook 的發布策略已經從一天兩次的\"主幹開發，分支發布\" 改變為平均每天發布 9~10 次的 \"主幹開發，主幹發布\"模式。  城際快線模式的優點有兩個:   每個人都非常清楚各個時間點  更加聚焦生產品質  缺點:   發布頻率較高，因此未完成功能的代碼也會一同發布出去  對於程式碼要求品質較高，需要強大的品質基礎設施保證。  使用城際快線模式，間隔多長時間發出一趟合適? 在不影響用戶體驗，不增加成本且合規的前提下，讓發布週期盡可能縮短到令你感到有些緊張的節奏，例如:每個月發布一次版本，現在可以把兩周當作一個目標。  8.4.2 分支策略與發布周期的關係   分支策略與版本發布周期有一定的相關性    分支策略與版本發布周期之间有一定的相關性，如圖 8-20 所示   軟體開發週期極長的“專案制”團隊和軟體發布頻率的極高 “城際快線模式”團隊會使用   主幹開發，主幹發布  的分支策略  次之的團隊會使用   主幹開發，分支發布  的策略  最後的區間使用   分支開發，主幹發布  的分支策略  這之間不是絕對的，其中會有很大的重疊部分，通常會受到團隊成員人數，產品架構與品質保障基礎設施等影響  8.5 小結  每個分支策略都有其優點與挑戰。它對於發布頻率以及每次發布的效率也有較大的影響。  目前的發展趨勢為: 軟體的發布的頻率越來越高，發布週期越來越短，矽谷頂級的網路公司大多採用 \"主幹開發\" 或者高頻的   GitHub Flow  分支模式。  一個企業到底選擇哪種分支策略，需要依據團隊的具體情況來決定。如果相對應的基礎設施不足 ( 如軟體架構、人員能力、和工具平台成熟度)，盲目地提高發布頻率，縮短發布週期會造成不必要的損失。   持續交付 2.0  提倡持續極盛的分支策略，選擇分支模式的原則有以下幾項:   分支越少越好，最好只有一條主幹  分支生存週期越短越好，最好在 3 天以內  在業務允許的條件，發布週期越短越好。  企業管理者應該遵循   持續交付 2.0  的思想、理念與原則。制定合理的改善目標，促進公司 IT 交付能力不斷提升，才能夠跟上時代的發展。  Additional Reference:   Git 常用的分支管理模型   DevOps 技术：主干开发   什么是 Release Train?",{"id":194,"path":195,"dir":159,"title":196,"description":7,"keywords":197,"body":204},"content:4.cicd-2.0:9.chapter9.md","/cicd-2.0/chapter9","09 持續整合",[198,199,200,201,202,203],"9.1 起源與定義","9.2 六步提交法","9.3 速度與品質權衡","9.4 在團隊中實施持續整合實作","9.5 常見執行問題","9.6 小結","  9.1 起源與定義   Chrysler Comprehensive Compensation System(C3)，是一個用戶數達 87000 名的綜合人事與工資系統  1994 年開始開發，預計 1999 年上線，1996 年 Kent Beck 被邀請來挽救這個項目，並採用新的開發方式(極限編程方法)\n   遇到的問題：要把系統的不同部分整合起來，並且讓系統運作，常常需要 1~2 週的時間  極限編程：提高整合頻率，每次合併的東西較少，減少整合需要的時間，整合過程中的問題較好排解(Debug)  方式：開發人員撰寫 shell 腳本，這個腳本定期去訪問儲存庫，只要發現有新的程式碼被提交，就將程式碼自動拉取到建構主機進行編譯  但是最後上線的版本僅能支撐 10000 人使用，2000 年系統被捨棄  9.1.1 原始定義   每日構建(daily build)、每晚構建(nightly build)\n   每天定時自動執行一次軟體構建工作，將版本控制系統最新版本的程式碼在建構環境(沒有安裝集成開發環境的乾淨機器)下進行編譯、鏈結、打包的過程  有助於確保開發人員明確了解前一天編寫的程式在整合的過程中是否發生問題，幫助開發團隊確定新的程式碼變更是否破壞原有功能  「持續集成是一種軟體開發實踐，團隊成員頻繁地將他們的工作成果集成再一起(通常每人每天至少提交一次，這樣每天就會有多次集成)；每次提交後，自動觸發一次包含自動化驗證的構建任務，以便能儘早發現集成問題」 —— Martin Fowler(2006)  9.1.2 一次集成過程   開發人員提交程式碼到儲存庫  建構主機定期輪詢程式碼儲存庫  有新的程式碼，下載到建置環境  根據建置腳本，執行建置  建置完成後上傳結果    9.2 六步提交法   開發人員 check out 最新建置成功的程式碼  修改程式碼實作需求  第一次個人建置(Build)：建置自己開發的東西(確保自己開發的東西沒問題)  第二次個人建置：從主幹中下載最新建置成功的程式碼(如果這段期間有其他人更新)，並且進行建置(自己開發的東西＋這段期間其他人的東西)\n   確保要上去主線的程式碼是沒問題的  提交程式碼  第三次建置：如果程式碼通過自動化測試、靜態程式碼檢測，則將該程式碼並回主線，如果失敗則進行修復    9.2.1 四個關鍵點   六步提交法中的三次驗證有什麼作用   三次驗證的腳本必須一樣  第一次驗證：確認開發者撰寫的內容是否正確，內容為自己  第二次驗證：確認與最新版合併後系統是否正常，內容為自己＋他人  第三次驗證：在乾淨受控的環境中執行與第二次驗證一樣的腳本，確保開發人員提交完整且無程式品質問題，內容為儲存庫最新版本\n   如果第二次驗證有過，第三次驗證沒過，代表 1.自己這次程式碼提交不完整 2.自己的建置環境跟團隊有差 3.團隊成員有提交新代碼，但是自己沒發現  個人驗證(第一次驗證、第二次驗證)一定要做兩次嗎？   第二次驗證目標是驗證自己改的程式碼跟其他人提交的程式碼合併再一起，也符合預期  如果第一次有過，第二次沒過，代表其他人提交的內容影響到這次的開發內容  信心爆棚的工程師可以跳過個人第一次驗證，直接做第二次驗證即可  如何確保在提交前執行個人建置   在代碼合併到主線時，強制進行第二次個人驗證  每次建置應該包含哪些品質檢驗內容   單元測試、程式碼靜態掃描(白箱)、程式碼規範檢查(code style)  建置驗證測試(build verification test)   建置結束產生的二進制內容是否包含正確的內容 ex.配置文件的完整性  這個建置結果是否能夠正常安裝並且啟動運行  運行後最基本的功能是否可以使用 ex.登入  針對接手舊案(大量遺留代碼的儲存庫)，使用程式碼靜態掃描可能會出現一堆錯誤，該怎麼處理？以下提供兩種解法   減少規範，關注重點：提取最重要的程式碼規範，早期只關注嚴重類型的問題，以後在逐步增加程式碼規範  執行「童子軍營地」原則：遺留代碼多，且系統已經上線一段時間，且最近不會動到那邊的程式碼，則暫時不去修(當作沒看到)，有動到那部份的程式碼，則考慮進行修復，做到每次提交時，沒有新增問題，最好問題可以逐步減少   💡 童子軍營地原則：離開營地前，確保營地和你使用之前一樣乾淨，能在乾淨一點就更好了  9.2.2 同步異步模式   主要的差異是在程式碼提交到主線時(六步提交法的第五步)，後續開發人員的行為差異  同步：開發人員需要等到建置完成後，確保通過才進行下一個開發任務  異步：提交後就可以開始下一個開發任務  Kent Beck 不建議採用異步，因為可能存在浪費，ex.當開發人員已經在做下一個任務時，被通知上一個驗證沒過，需要調整，開發人員需要回想當初做了什麼  9.2.3 自查表   可以從以下六個面向來檢查是否自己已經符合持續整合的最佳狀態  主線開發，頻繁提交：開發分之生命週期不超過三天  每次提交都是一個完整的任務  讓提交的建置都可以在 10 分鐘內完成：尤其使用同步模式(參考 9.2.2)  提交建置失敗後應禁止團隊成員提交新的程式碼，也不允許其他人 check out 該程式碼\n   當團隊成員提交代碼引起建置失敗，說明系統整體品質可能存在問題，因此整個團隊不應該繼續提交新的程式碼，而是集中火力解決這個問題  立即在十分鐘內修復已失敗的提交建置，否則該主線分支退回上一個提交點\n   因為問題被修復之前，這個分支上的內容無法上到 release，導致其他人就算提交內容，這個分支的內容還是不能用  為了讓其他人的內容可以持續提交，出問題的原開發有足夠的時間進行思考與修復，捨棄這個有問題的提交  自動化建置驗證通過後，對軟體品質有較大的信心\n   反例情境：團隊並不覺得使用持續整合有什麼用處，因為他們有很多自動化測試案例，但是隨著系統功能增加，新增的測試案例卻很少，而且現有的測試案例測試失敗後，如果該問題太難修復，就刪除這個測試案例(解決提出問題的人，就是解決問題 😀)  9.3 速度與品質權衡  9.3.1 分級建置   隨著系統功能擴展，自動化測試的數量會越來越多，超果我們可以忍受的建置時間(10 分鐘)  可以將自動化測試拆成兩個部分，將運行速度較快，重點驗證項目放入提交建置裡面，運行較慢、不常驗證失敗的測試案例，放在次級建置驗證的內容  次級構建失敗，應該立即發出通知，並且立即修復，在修復之前不得提交新程式碼  9.3.2 多人同時提交的建置   如果目前次級建置(每次可能需要執行 30 分鐘以上)進行中，新提交的程式碼就不執行次級建置，只做主要構建  直到次級建置完成後，新提交的程式碼(合併到儲存庫)，才會觸發下一次的次級建置，中間的次級建置全部被省略\n   1.節省建置資源 2.同時進行如果前者壞了，後者執行的內容全沒意義    9.3.3 雲端建置的威力   編譯過程主要分為預編譯、編譯、鏈結  可以透過雲端叢集的概念同步執行，減少整體建置時間的為「編譯」，各機器完成編譯後，再交由一台主機做整合的鏈結    9.4 在團隊中實施持續整合實作  9.4.1 實作五步法   建置腳本化，搭建持續整合框架\n   選擇一款持續整合工作，目前比較夯的是 Jenkins  在該持續整合工具上建立一個建置任務，可以從你的儲存庫拉取程式碼  寫一個腳本文件，可以自動完成系統的編譯、建置、打包  修改持續整合工作上的任務(第二步)，讓他可以調用第三步的腳本  向儲存庫提交一次程式碼，驗證持續整合工作可以發現新代碼並拉取正確的程式碼版本，運行指定的腳本  添加已有的自動化驗證集合\n   增加自動化測試案例  加入程式碼規範掃描\n   SonarQube、Android Lite、CCCC、cppcheck、Clang、Pclint  選擇利於持續整合的分支策略\n   如果分支過多，則不利於團隊持續整合的效果  建立六步提交法  持續優化\n   初期整個機制運作正常，但隨著系統功能增加，整個過程出現了問題  ex.測試案例本身的程式碼品質不良，導致隨機的測試失敗  優化編譯打包的時間  調整程式碼分支策略  自動化測試案例的分級(ex.哪些應該放在次級構建)  優化程式碼規範掃描(ex.調整規則)  生成數據報告，方便團隊了解目前的程式碼品質狀態  工程師改變習慣，並提升技能\n   要求工程師主動提早整合，非推遲整合(不要一大包才提 PR，一個 PR 不要包含多個任務)  學習持續整合的工具如何使用    9.4.2 分支策略與部署流水線   主線開發，主線發布：開發團隊只要架設一個持續整合服務，關注主線的程式碼變更即可  主線開發，分支發布：當新增分支時，需要加增這個分支的部署流水線  分支開發，主線發布：(只有一個儲存庫，但是有多個系統)每個分支都需要有部署流水線，當有人併入主線觸發主線部署流水線，當分支不需要時，同時可以砍掉其部署流水線  多建置集成：系統來自於多個儲存庫，個別建置完觸發產品部署流水線        9.5 常見執行問題   團隊原有工作習慣，例如\n   開發人員在自己的開發任務完成之前不希望與別人的程式碼進行整合  測試人員希望在整批的開發完成後再進行集中測試  對於程式碼靜態掃描問題視而不見，1.團隊對於掃描規範沒有大家都認同 2.問題太多沒時間修  自動化測試案例不夠，導致採用人工測試，必須開發完才有辦法整批測試，無法在每次提交時就進行測試  技術研發管理缺乏導致，例如\n   開發、測試和維運環境沒有分離或分離不徹底，多人共用測試環境  各類測試環境的準備工作很複雜  9.6 小結   主線開發，高頻提交代碼  每次提交都是完整有意義的任務  提交建置階段在十分鐘內完成  提交建置失敗後，立即修復，其他人不得在修復前提交代碼  應該在十分鐘內修復失敗，否則放棄這次的提交  自動化建置成功後，團隊對於系統品質比較有信心",{"id":206,"path":207,"dir":159,"title":208,"description":7,"keywords":209,"body":216},"content:4.cicd-2.0:10.chapter10.md","/cicd-2.0/chapter10","10 自動化測試策略與方法",[65,210,211,212,213,214,215],"10.1 自動化測試的自身定位","10.2 突破傳統自動化測試的困境","10.3 自動化測試的實施策略","10.4 用戶驗收自動化測試要點","10.5 其他質量檢查方法","10.6 小結","  前言  想發揮持續整合的真正作用，一個至關重要的部分就是自動化測試策略。  本章主要討論軟體進入生產環境之前的   「自動化測試管理」  ，包含：   自動化測試的定位：\n   包含哪些測試，怎麼分類？  哪些可以被自動化？  本章節主要探討哪些測試？  傳統自動化測試的困境：\n   過去常見的一種自動化測試策略模式，它哪些問題？  作為良好自動化測試的一個反例  如何實踐良好的自動化測試\n   自動化測試的分層  測試案例的管理、從何開始做自動化測試…  10.1 自動化測試的自身定位  測試領域的4類活動   「問題認知」：對業務問題本身的理解與認識 (了解測試的目標是什麼)  「分析」：分析、設計測試流程或實作測試程式碼 (以最低的測試成本，達到驗證的目標)  「執行」：執行測試，得到測試結果數據  「決策」：根據測試結果進行下一步行動判斷   其中的「執行」存在大量重複性勞動，自動化測試能夠降低此成本  回歸測試：重複執行以前的全部或部分相同的測試工作\n   確保過去開發的功能服務都有正常運作  產品生命週期越長，回歸測試案例庫就越大，回歸測試的工作負擔越重  Brian Marick 測試4象限  \n有很多不同的測試，如何分類？哪些測試可以被自動化？\n     四個面向\n   面向業務專家：能與業務專家無障礙溝通 (ex: PM、使用者)  面向技術人員：易與技術人員達成共識 (ex: 開發工程師)  支持編程：目標是為了幫助研發團隊檢查功能需求是否開發完成 (ex: 功能性和UI是否一致)  評判項目：目標是為了找出產品是否有缺限 (ex: 產品是否滿足客戶的實際需求)  四個象限\n   第一象限：(手動) 用戶演示demo、可用性測試、探索性測試  第二象限：  (自動化)  功能驗收測試 => 以用戶的角度  第三象限：  (自動化)  系統集成測試、組件測試、單元測試 => 系統角度，技術實現的驗證  第四象限：(自動化/手動) 非功能驗收測試(性能測試、容量測試、可靠性測試…)  本章主要討論第二、第三象限的自動化測試  10.1.1 自動化測試的優勢  \n相較於手動測試，自動化測試有哪些優勢  \n(機械自動化 v.s. 人的手動執行)\n  「減少失誤率，提高準確性」:   機械不受情緒、經驗的影響，每次執行都重覆相同的動作並紀錄詳細的結果  「節省時間和執行成本」   在長生命週期、發佈頻率高的產品中，時間成本的節省相當明顯  「提升測試覆蓋度」:   自動化測試可以增加測試的深度和範圍，以提高軟件質量  ex: 內存使用、內部程序狀態…  「做手動無法完成的測試」:   手動測試難做到如摸擬成千上萬個虛擬用戶同時間與軟體進行互動 (mock)  「為開發人員提高質量反饋速度」:   讓開發人員可以方便的執行，快速發現問題  「提高團隊士氣」:   讓團隊可以將時間花在更具挑戰性和更有價值的活動中  ex: 探索性測試  10.1.2 自動化測試所需的投入  \n事物都具有兩面性，自動化測試帶來收益的同時，也會產生成本\n     成本  說明    工具投入成本  測試工具、測試框架的研究與培訓   測試案例維護成本  功能調整的同時，需維護相對應的測試案例   專業技能人員的成本  測試編程、設計規劃的經驗、培訓   設備資源的投入  自動化測試無法完全替代手動測試  保留手動測試所需的測試環境，也要為自動化測試的執行準備相應的測試環境  手動測試與自動化測試的不同  \n自動化測試有很多優勢，但手動測試依舊不可完全被取代\n  \n為什麼？兩者的本質上有哪些不同？\n   自動化測試   無主觀意識，它只做腳本要求它做的事  比較沒有無主動觀察、主動認知和分析的能力，沒有創造力  擅長回答：系統是否照我們「預先設計」的方式正確運行？  ex: 用戶功能驗收測試  手動測試   有人為主觀意識的介入  具主動觀察、學習、分析、創造力  擅長回答：我們是否正在開發一個正確的，滿足用戶期望的軟體系統？  ex: 探索性測試  自動化測試與手動測試的成本收益對比示意圖    10.2 突破傳統自動化測試的困境   自動化測試常作為提測前的檢驗標準 (提交給手動測試之前，先運行自動化測試案例)  傳統自動化測試的創建流程     測試分析者 => 分析測試案例，並文檔化(撰寫測試流程的文檔)  測試執行者 => 照測試流程文件執行手動測試，發現BUG向開發人員報告  開發人員   => 修復bug  測試執行者 => 再次執行測試，直至驗證通過  測試分析者 => 從測試案例文檔中選出一些重要且變動可能性較小的測試案例  自動化測試開發者 => 對挑選出來的測試案例編寫自動化腳本，並歸入自動化回歸測試庫  10.2.1 傳統自動化測試的特點  1. 測試案例執行成本過高   處理流程較長、要準備的測試案例較多，需花費較多時間、精力  多為黑盒自動化測試案例，摸擬真實用戶的介面操作來驅動的系統集成測試  2. 自動化測試執行頻率低   通常是在軟件開發完後(提測階段)、或系統回歸測試(重覆以前全部或部份的測試工作)用  3. 質量反饋滯後   大部份的測試案例是回歸測試案例，無法對當前正在開發的新功能進行測試  為了能安全執行，經常會通過 sleep 指令讓流程暫停，再繼續下一流程，因此很耗時  4. 測試環境準備成本高   完善的測試數據集  整套的運行環境  測試環境的搭建手動操作較多，甚至需要多人的參與  5. 測試結果可性度低   受機械硬體配置、網路狀況、案例處理時間長度等影響，可能會產生隨機失敗  若界面需求的改動沒有即時通知測試團隊對測試進行修改，也會造成測試失敗  這些失敗都會造成團隊可能會更傾向於忽略這些自動化測試案例的存在  6. 人員依賴性強   編寫自動化測試案例很大程度依賴少數測試開發專職人員  適合傳統自動化測試的團隊   適合用于發佈版本週期較長，使用傳統瀑布開發方法的團隊  現代軟件版本迭代速度越來越快，這種自動化測試案例的投資回報率越來越低  名詞解釋：   黑箱測試(ex: 功能驗收測試)\n   把測試軟件當成黑箱，不在乎內部的實作，只在乎輸入輸出結果是否正確  根據用戶說明書、需求文檔，去進行操作、輸入測試  優點：站在用戶的立場進行測試  缺點：無法對程序內部特定的內容進行測試  白箱測試(ex: 單元測試)\n   把軟體當成透明的盒子，在乎程式內部的運行邏輯，對其結構進行測試  優點：對程式內部的問題進行覆蓋(盡量作到能夠覆蓋到每一行原碼中)  缺點：無法檢驗程式外部的特徵，也無法站在用戶的角度進行測試  10.2.2 自動化測試的分層   本章聚焦的測試類型為\n   「用戶驗收測試」(用戶角度) 第二象限  「系統集成測試」(系統角度) 第三象限  傳統自動化測試方式產生的測試案例類型通常偏上層測試\n   形成測試捲筒冰淇淋 (頭重腳輕) => 不利於持續整合\n   被測範圍較大：執行時間成本、準備成本都很高  測試捲筒冰淇淋    「快、捷、時、信」  \n良好的自動化測試實踐，應該要「快、捷、時、信」\n     項目  說明    快速  測試案例的執行速度要快  (持續整合的要求是，最好在十分鐘以內，不要超過15分鐘)   便捷  每名工程師都可方便執行測試，不會影響到他人   及時  一但功能發生變化，就能馬上告知本次代碼變更對軟體質量的影響   (若沒有一直開發新測試案例對新功能即時驗證，會導至反饋速度的降低)   可信  不存在隨機失敗的現象。  持續整合實踐要求一但自動化測試案例失敗，必須立即修復  隨機失敗會大大增加工程師的無效投入，並降低工程師對持續整合的信心  \n透過傳統自動化測試的方式，能達到快、捷、時、信嗎？(困難)  \n快 => 降低時間成本  \n信 => 提高穩定性\n   開發上層測試案例成本較高，穩定性差\n   被測對象範圍較大，測資準備工作量大，執行時間久  有外部依賴，不穩定性也高  降低成本：\n   減少上層測試案例數量  增加下層測試案例數量  形成穩定的正三角形    10.2.3 不同類型的測試金字塔  \n軟體架構發展的趨勢朝「服務化」和「微服務化」發展   \n掀起大規模分散式應用服務浪潮\n   服務化：軟體服務由「單體應用架構」拆分成「服務導向的架構(SOA)」\n   服務之間透過RPC等方式進行溝通(RPC：Remote Procedure Call, 遠端過程呼叫)  微服務化：服務導向架構 => 微服務架構/微核架構\n   後台服務化模組被拆分成很多的微服務 (微服務架構)  客戶端軟體向元件化或微核架構發展   (微核架構)    1. 微核架構的測試金字塔     端到端自動化測試\n    「摸擬介面操作」  來驅動的自動化測試  API自動化測試\n   UI層之下，   「透過API介面」  來驅動下層業務邏輯的自動化測試  元件或外掛間服務的介面自動化測試\n    「兩個或兩個以上元件(外掛)間」  的功能正確性  元件測試\n   對   「單個元件或框架」  本身進行品質驗證  自動化單元測試\n   最細粒度的自動化測試  2. 微服務架構的測試金字塔     單元測試：   測試目標：驗證   「業務邏輯單元」  的正確性，不在乎上層程式要實作什麼功能  什麼是單元？\n   軟體中最小可測業務邏輯單元 (一個函式、一個Class)  對外部依賴(如檔案系統、網路等)比較少  測試運行時不需系統處於執行狀態  測試運行速度快  業務元件或服務測試   測試目標：驗證   「單個元件或服務」  的行為是否符合設計預期  什麼是元件？\n   由多個最小業務邏輯單元組成  可能與本系統「內部」的其它元件互動，也可能負責與「外部」整合點進行互動  ex: GoCD 中的類別 MaterialService，就是負責與Git或Subversion打交道的一個元件  測試通常不需要系統處於執行狀態，但可能涉及外部依賴(檔案系統、網路、資料庫等)  執行進度可能比單元測試稍慢  契約測試，又稱消費者驅動的契約測試(consumer driven contracts test)   測試目標：  「兩個服務之間」  的契約\n   測試消費者介面與服務者介面之間的正確性  驗證服務者提供的資料是否為消費者所需要的  契約：指軟體系統中各個服務間互動的資料標準格式    業務工作流測試   測試目標：驗證   「多個被測服務」  之間是否可以正常工作，達成某一業務請求  啟動執行兩個以上的微服務，進行業務流程上的測試  端到端測試   對   「整個軟體服務」  的流程進行測試\n   模擬用戶在可視化介面上執行各種操作  若軟體服務對外提供非可視介面的服務(如API呼叫)，這類測試也歸屬端到端測試  從用戶的角度驗證整個功能的準確性和可用性  不關注某一細小功能的實作  10.3 自動化測試的實施策略   考慮自動化測試案例的成本，為了更好的投資報酬率，需要更聰明地啟動自動化測試實踐  10.3.1 增加自動化測試案例的著手點  1. 針對代碼熱區補充自動化測試案例   程式碼熱區\n    程式碼變動頻率相對較高  的檔案或函數   經常出問題  的功能元件  若非程式碼熱區，說明執行穩定，編寫自動化測試的投報率較低  2. 跟隨新功能開發的進度   給目前的功能開發提供即時的品質回饋  若只是在補充原有功能的自動化測試案例，無法即時發揮保護網的作用  多數自動化測試案例由開發工程師自行負責編寫\n   若同一功能由一人開發，一人編寫測試，就會產生溝通成本\n   (傳統自動化測試的缺點)  3. 從測試金字塔的中間層向上下兩端擴展   上層開發成本高，下層單元測試覆蓋率小，中間投報率最好  ex: 採用微服務架構的服務端軟體 => 從契約層開始著手  ex: 手機端APP(微核架構) => 從元件級或API級的測試開始入手  4. 自動化測試案例的質量比數量重要   在能達到「驗證品質目的」的前提下，自動化測試的案例越少越好 (降低成本)   「在實作成本最低的測試層級上，進行相應業務邏輯的測試」   不要在不同層級的測試(如單元測試層和元件測試層)中，針對相同的邏輯編寫測試案例  數量夠就好，絕不寫不必要的測試程式碼  10.3.2 提高自動化測試的執行次數   測試執行次數越多 => 成本越低、獲益越高  如何提高執行次數？  1. 共享自動化測試案例 (測試可以被反覆利用，甚至自動化)   反例：\n   測試只保存在自己的電腦中  測試只能在自己的環境中執行  測試案例無法針對不同環境(如測試環境、預生產環境)進行適配  正例：\n   將自動化測試案例放到團隊的程式碼倉庫中  整合到持續部署流水線中自動執行  2. 開發人員是自動化測試的第一用戶   每個開發人員，隨時都能非常方便地執行自動化測試案例\n   將自動化測試作為開發人員日常開發中的一張品質保護網  而非測試人員用來驗收開發人員工作成果的工具  10.3.3 良好自動化測試的特徵  1. 案例之間必須相互獨立   案例間若存在順序依賴\n   就只能線性執行  執行時間拉長，回饋效率降低  搜尋失敗原因難度提高  2. 測試案例的運行結果必須穩定   不穩定的測試案例只會提供錯誤的品質訊號，浪費團隊的時間  3. 測試案例的運行速度必須快   將一個測試案例分解成多個獨立的測試案例，每個案例僅測原測試案例的一部份\n   => 平行執行  「等待」改為「輪詢」，以很小的時間間隔不斷查詢是否到達下一步執行的狀態\n   => polling 取代 sleep  4. 測試環境應該統一 (最大化測試案例共享的獲益)   反例：測試只能在某測試環境上執行，甚至只能在某開發人員的開發機器上執行  想辦法讓大家的測試環境統一或是讓測試能適配不同的環境  10.3.4 共享自動化測試的維護職責   自動化測試也是軟體程式碼，應花費時間心力使其易於維護，避免程式碼腐爛  自動化測試應盡可能與生產程式碼同步變化\n   沒有同步變化，執行就會失敗  當執行自動化測試失敗後，就應該馬上進行修改  (可能是功能出錯，也可能是測試需要修改)  「破窗效應」：\n   犯罪學理論：\n   環境中的不良現象如果被放任存在，會誘使人們仿效，甚至變本加厲  一個廢棄的房子出現了一個破窗沒人理，久了就會越多，甚至出現塗鴉  越來越多測試失敗卻沒有受到關注，久了就對自動化測試的結果視而不見  10.3.5 程式碼測試覆蓋率   有人認為它是非常有用的指標\n   應該要求達到一定比例的測試覆蓋度  有人認為它是有用的工具，但它\n   只能告訴你「哪些程式碼缺乏測試案例覆蓋」  卻無法證明「被覆蓋的程式碼就一定是真正經過良好驗證的程式碼」  提供一種錯誤的安全感  Google公司並沒有規定測試覆蓋率的統一標準，只有一個建議性標準：\n   單元測試覆蓋率達到85%  Facebook於2004年剛上線時也沒寫自動化測試\n   業務發展快速、工程師數量大幅增加  交付品質變差、開發人員常常處於救火狀態  2008年始，Facebook正式引入自動化測試實踐  Facebook 也無統一規定要寫多少自動化測試  Facebook 公司中，自動化測試案例的大多數由開發工程師自己負責      10.4 用戶驗收自動化測試要點   用戶驗收自動化測試處於自動化測試金字塔的最高層，成本最高  開發更多用戶驗收測試前，必須精心準備，以便以較低的成本持續維護比較健康的測試案例集  10.4.1 先搭建分層框架   先選擇適合專案的測試框架，做一定的調整  產生腳本程式後，進行測試腳本程式的分層重構  1. 測試案例的描述層   test case 描述  2. 測試案例的實現層   實作 test case  3. 測試案例的接口層(介面層)   把 test case 中 可以重用的東西提取出來  10.4.2 測試案例應保持低位(案例數應保持少量，低成本)   測試案例數量不應太多  focus在驗證軟體服務的核心工作流程  實作細節可交由下層的自動化測試案例來覆蓋  ex: 驗證系統登入行為時\n   驗證目標主要在驗證整個登錄流程是否得到正確執行  而不是驗證輸入資訊是否非法  後者可透過更低層次的自動化測試案例來覆蓋  10.4.3 為自動化測試案例預留API   少用模擬圖形介面操作的程式碼\n   介面操作反應慢  可能會有不容易定位，或執行不穩定的情況  盡量呼叫位於介面下層的API來驅動業務流程的執行  10.4.5 測試數據的準備   通過一些規則，編寫程序自動生成數據(當規則複雜時，較難編寫程序)  通過錄制手工測試時產生的數據  將生產環境的非敏感數據克隆一份，或截取一個片段  進行生產環境數據的自動化錄制，並保存備份  10.5 其他質量檢查方法  10.5.1 差異批注測試方法   一種半自動測試方法\n   當做預定義的數據集輸入系統后，收集運行后的輸出結果，對其中需要驗證的數據進行提取，並將提取結果放入文本檔中，通過對比前後兩次測試的結果，用人工批注的方式進行半自動測試。 需要特別注意動態信息(日期時間)的處理，常見的工具包括TextTest和ApprovalTests等。    10.5.2 代碼規範檢查與代碼動靜態檢測    代碼風格規範檢查：  是指通過工具 ，依據團隊定義的一些代碼編寫規範，針對源代碼進行檢查，如發現破壞規範的代碼，就加以指正。   工具常用的有 Checkstyle、PMD、SonarQube 等。  增強代碼的可讀性和易維護性。  Google 工程師在做代碼評審時，對代碼可讀性要求就非常嚴格。   代碼動靜態檢測：  通過一些工具對產品原始程式碼進行自動化掃描，發現代碼中存在的問題或潛在風險。    靜態掃描：  寫好源代碼後，無須經過編譯器編譯，而直接使用一些掃描工具對進行掃描，找出代碼中存在的一些語義缺陷、安全漏洞的解決方案。   常用的工具包括：lint，Coverity，ColcWork等。   動態分析：  通過在真實或虛擬處理機器上執行目標程序進行分析，比如，在可能的漏洞處插入專門編製的故障發生函數，迫使目標軟體產生異常，然後通過監控程式來檢查是否發生了邊界溢出或者其他異常現象。   常用工具包括：Valgrind，Purify等。  \n  當代碼庫的規模較大時，這種質量掃描工作可能會花費較長時間。\n    應該在提交構建之前，提供增量掃描的方式。  將完整代碼庫的掃描放到後期執行。  單獨執行。  正如我們可能將提交構建與次級構建分開一樣，這也是為了在反饋時間與反饋質量之間取得平衡。\n  10.5.3 AI在測試領域的應用   在代碼分析、缺陷定位等方面Al工具很多，其他UI、安全性測試也有一些： Appdiff、DiffBlue、BugDojo、微軟AI安全風險檢測工具、Facebook Sapienz等。雖然這些智能測試工具還在探索中，但已經有一些喜人的成果出現。   例如 2018 月， Facebook的工程網站 code.facebook.com 上，Ke Mao 和 Mark Harman 撰寫了一篇文章，名為《Sapien z: Intelligent automated so ware testing at scale》，講述了 Sapienz 對 Facebook 自身安卓應用進行智能自動化軟件測試的結果，稱：「除能夠加速測試過程以外， Sapienz測試結果的假陽性率極低…… Sapienz的結果報告中，需要修復的比例占75%。」    将人工智能（AI）应用于软件测试中    將AI應用於軟件測試中   查找和修復錯誤，利用AI進行更快速的測試  利用AI提高DevOps工作效率  將AI與機器學習相結合    使用AI進行軟件測試   從單元測試和靜態分析開始  向金字塔上移至API和UI級別    华为大咖分享：AI在软件测试领域应用探索    10.6 小結   自動化測試案例運行次數越多，平均成本越低，收益越大  自動化測試案例間應該盡可能獨立，互不影響  在質量有保障的前提下，自動化測試的案例越少越好  遺留代碼的自動化測試編寫應從代碼熱區開始  自動化測試案例從測試金字塔的中間層開始補充，投入產出比最高",{"id":218,"path":219,"dir":159,"title":220,"description":7,"keywords":221,"body":228},"content:4.cicd-2.0:11.chapter11.md","/cicd-2.0/chapter11","11 軟件配置管理",[222,223,224,225,226,227],"11.4 環境基礎設施管理","11.5 軟體配置項的管理","11.6 不可變基礎設施與應用","11.7 資料的版本管理","11.8 需求與原始碼的版本關聯","11.9 小結","  11.4 環境基礎設施管理  11.4.1 環境準備的4種狀態   蠻荒法\n   以 \"人腦 + 手工” 代表  剛起步的專案，整體不複雜，用戶與客戶數量不多\n   開發人員自己就可以搞定所有的軟體部屬相關問題  所有環境準備相關的知識都在開發人員腦中，團隊核心與英雄  規範法\n   以 \"文檔\" + \"私有腳本\" 代表的 規範化狀態  隨著軟體服務的成功，用戶跟客戶數量變多，更多需求滿天飛過來，伺服器的數量也因此增長不少，環境維護工作變得多了起來\n   要求正式上線的部屬文件，通常會總結出一個環境部屬的準備說明書，由維運的人來執行\n   例如 文檔可能有11個步驟，但每個步驟中又有許多子步驟。  很多團隊是找不到這樣的環境說明文件，即使找到，可能也是無法使用過時的文件  要有規範化的上線部屬流程，此階段需要有個SOP文件，詳細紀錄每次的操作  利用私有化腳本，提升效率。每次的升級腳本都會重複之前一些動作   現在仍然有非常多的企業處於圖中此種狀態\n    流程通過 \"人\" 來維護，經常有遺漏  文檔通過 \"EMAIL\"追蹤，查找不方便  審查工作量大，由於手工工作量大，常有人繞過過程  自動化腳本不規範統一，而且經常出錯，導致部屬過程中斷  以辦公自動化為代表的 \"標準化狀態\"\n   隨著軟體服務的越來越成功，公司也不斷的壯大，維運人員越來越多，生產部屬事件也多了起來  無紙化的型態出現，原先使用office寫文件，轉為線上系統填寫\n   流程平台統一化，軟體開發與維運皆在同一平台上  部分內容標準統一  可以部份重用，減少工作量  審核工作變得容易，所有紀錄皆保留平台上  但仍然有些許問題\n   系統操作變得複雜，有些仍需人工餐與  兩次上線部屬差異比對仍然困難，雖然開發人員可以複製上一份資料，但每個皆為獨立副本，很難進行比對查找  以受控式自動化腳本為代表的 \"自動化狀態\"\n   此階段的自動化維運腳本有兩種型態\n   操作過程式為主\n   符合原有的思考習慣，將原本的手工操作步驟轉為腳本語言即可  靈活，想做甚麼操作，幾乎手工操作都可以辦到，不受任何約束  狀態聲明式為主\n   可以明確知道，無論何種情況或誰來執行這個腳本，皆可以得到相同結果  如果將此腳本放置 git倉庫，可通過diff功能，比對差異  學習成本高，此DSL(Domain=Specific Language，領域專屬語言)用來描述環境部屬的專有操作與狀態  腳本數量較多時，文件管理也存在相同儲存結構問題  此時可使用一些工作來做為輔助\n    工具運行模式可分兩種\n   拉模式\n   目標伺服器安裝 agent，保持與伺服器的連線與接受指令  推模式\n   不須安裝客戶端 agent，只需由伺服器遠端連線進行操作即可  11.4.2 領域專屬語言的應用     以上說明的這些工具都定義了各自工具領域，此種語言可以描述出環境部屬狀態的文件。  以 Puppet管理 apache2 服務為例來說明\n   Ansible 語法範例\n   11.4.3 環境基礎設施即代碼   現在，已將環境基礎設施一系列準備工作以腳本描述出來，也能通過自動化方式來執行。\n   好處\n1. 無論哪個環境出問題，皆能快速構建出全新的環境\n1. 只要有權限，任何人皆可完成任務\n1. 任何對環境修改都可以被記錄與審核\n1. 對不同環境來說，將腳本對比即可知道差異，不需再登入至伺服器  為了更進一步的優化，版本管理應該要有\n1. 操作系統名稱的版本號、補丁號與系統配置訊息\n1. 依賴軟體包的所有版本號與設置的內容\n1. 需與應用程式連線的版本號與設定檔內容  11.5 軟體配置項的管理  11.5.1 二進制與配置項的分離   一個應用程式通常程式碼與可執行檔是分開的，一旦編譯完成，此編譯執行檔就不會更改  編譯建置過程中，可能會有依賴其他套件包，通常套件包與編譯完成的執行檔是為分開的，但這兩項為必須，因而可視為不可分割的一環\n   二進制執行檔部屬時需與套件包的各版本完全相同  11.5.2 配置信息的版本管理   編譯執行檔時根據內容不同會設置各種設定檔\n   環境配置項(environment configuration)\n   網域、ip、api port、web port ...等  應用配置項(application configuration)\n   初始資料(帳密、通用資料...等)  VM資源設置  DB連線資料  Log紀錄相關設定  商務配置項(bussiness configuration)\n   預設值設定檔  某些商業邏輯計算設定\n    使用資料夾來區分不同開發環境的設定檔\n   11.5.3 配置項的儲存組織方式   儲存配置方法很多，最簡單的為使用純文字來記錄  可針對不同環境寫不同設置  編譯時將根據不同環境自動載入配置檔\n   11.5.4 配置飄移與治理   隨著生產環境發展，某些設定檔會偏移原先預期的設定，通常是由臨時修改測試所引起的  一段時間必須做校正回歸，否則將常出現不穩定狀態，甚至當機  好的配置可以解決飄移問題，也可避免人為操作上的遺漏  11.6 不可變基礎設施與應用  11.6.1 實現不可變基礎設施   物理主機鏡象技術 與 虛擬機鏡象技術\n   這兩種技術都可以提供環境準備效率  將上線的服務一模一樣分為兩份，同時部屬至不同地方  虛擬機除了可鏡象分離腳本外也可動態分配系統資源  Docker容器技術\n   容器化特性可使部屬軟體部分大幅降低難度與成本  11.6.2 雲原生應用   雲端服務PAAS，可讓部屬人員使用 git push 即可完成部屬動作，只需等待建置完成後的結果  PAAS先驅 Heroku 提出了 雲原生應用12要素\n   一套基本代碼多環境部屬  顯示聲明依賴關係  在環境中儲存配置  把後端服務當作附加資源  嚴格分離建置、發佈與運行  應用程式本身應該是一個或多個無狀態進程，進程之間沒有資料共享  通過端口綁定提供服務  通過進程模型進行擴展  快速啟動與優雅終止  盡可能讓開發環境、預生產環境與生產環境等價  日誌作為事件流  將管理/管理任務作為一次性進程運行  11.6.3 優勢與挑戰   優勢\n   簡化維運工作  部屬流程自行產生文件  持續部屬不停機，故障更少  減少錯誤與威脅  多類環境基礎設施的一致性  杜絕了\"配置飄移\"  被測試的即是被使用的  相對代價的挑戰\n   為不可變基礎設施建立一套自動化，初期成本較高  生產環境突發狀況，修復時間可能稍長  對大規模軟體服務來說，大檔案鏡象發佈至多台伺服器會消耗大量的網路資源，時間也會消耗不少  有狀態儲存軟體服務不容易被直接替換  11.7 資料的版本管理  每個軟體都需要處理資料，對資料進行版本管理是一件比較困難的事情，但是我們可以通過對其中一部分內容進行版本管理，來提高產品之間的合作效率。例如: 加快測試環境的建立，提高自動化測試使用案例的執行可靠性。  11.7.1 資料庫結構變更  當你使用關聯式資料庫系統，當軟體部署頻率變高，同時參與軟體開發的人員變多時，就應該對資料庫進行版本管理。除審計(Audit)管理和問題訂位外，對資料庫的版本管理在開發和自動化測試中也是非常有效地。  下圖為通過Flyway或Liquibase這類工具進行資料庫的升級或降級操作。搭配CI/CD工具的在執行Pipeline時，進行資料庫的管理。\n   當我們把資料庫結構的變動腳本放到原始碼倉庫後，在執行自動化測試時，可以很方便地得到一個乾淨的初始化資料庫版本。當發布給客戶的歷史版本出現Bug時，為了訂位問題，我們可以很快的對資料庫進行清空然後重建到特定的資料庫版本。  11.7.2 Binary文件  對於二進制文件的版本管理，就不能使用原始碼版本控制系統了。此時可以通過類似FTP、遠端檔案系統進行管理。   💡 這邊推薦MINIO系統服務來管理，他支援二進制的檔案版本管理，非常方便，參考  MINIO Versioning  當某公司內有一個  大檔案儲存系統 時，你可以將大尺寸檔案上傳到系統中時，它會返回一個URI，將URI放到一個文字檔案內容中，然後將文件檔內容納入到Git版本控制。這樣就將資料與產品原始碼版本進行統一的版本管理了。此時URL就是一個引用，而  大檔案儲存系統 就相當於一個儲存資料並進行版本管理的共享倉庫，這種方式對於測試資料管理是非常方便的。  另外一種狀況，當應用程式的啟動或運行需要仰賴一組資料檔案時，例如:   資料庫連線字串 ，我們可以先將資料庫連線字串進行演算法加密，當作一份二進制文件，然後將解密的演算法腳本加入到Git資料庫版本控管中，在程式執行時，將二進制文件進行載入後，在執行解密的演算法後，再進行資料庫的連線。  資料庫版本補充  請參考  Appendix 01 Database Version Control  11.8 需求與原始碼的版本關聯  由11-1圖中，程式碼、軟體包、環境都進行管理了，但我們想要對程式碼與需求項目進行關聯時，應該如何處理呢?  我們可以將需求內容進行文件化，並與對應的軟體包版本進行關聯，如果需求顆粒度要更細的管理，也可以將管理平台中的每個需求項目ID(或缺陷管理系統的缺陷ID)與程式碼進行關聯。例如: 再每次向程式碼倉庫提交程式碼時，將需求項目的ID，做為提交註解的一部分，並將這種關聯訊息進行展開出來。下圖是Mingle工具中的關聯參考   Mingle的SaaS從2019/7/31號開始已經停止服務。參考  官網 。你可以使用其他替代工具，例如: jira、Asana 、TFS等等。    11.9 小結  良好的軟體管理是打造持續交付Pipeline、加速驗證壞環節的基礎。  本章節主要有三個核心管理。   對一切進行版本管理  共享唯一來源  標準化與自動化  可以透過下面5個問題來驗證檢查你是否對一切都做了版本管理。下列5個問題是否都有放入到版本控制系統中呢?   產品原始碼和測試程式碼  軟體應用的配置訊息  各類環境的系統配置  自動化的建置和部署腳本  軟體包是否進行版本管理  另外可以利用下列兩個問題來檢查軟體管理是否做得足夠好。   可以透過原始碼倉庫的專案，一鍵式地建構出完整軟體包嗎?  在沒有他人的幫助下，任何團隊成員都可以一鍵式自動化搭建出一套應用軟體系統，用於體驗產品新功能嗎?",{"id":230,"path":231,"dir":159,"title":232,"description":7,"keywords":233,"body":239},"content:4.cicd-2.0:12.chapter12.md","/cicd-2.0/chapter12","12 低風險發佈",[65,234,235,236,237,238],"12.1 高頻發佈是一種趨勢","12.2 降低發佈風險的方法","12.3 高頻發佈支撐技術","12.4 影響發佈頻率的因素","12.5 小結","  前言     前幾章主要討論快速驗證環中「建置階段」的工作  本章主要討論如何「高頻」「低風險」地進行軟體部署和發佈，儘早讓軟體在生產環境中執行\n   「高頻」的好處與可能帶來的成本與風險  「降低風險」的各種方式：藍綠部署、開關技術…  12.1 高頻發佈是一種趨勢   2007年以前，「敏捷軟體開發」的認同度不高，一些傳統IT企業表示：「我們不需要那麼快速地交咐軟體」  2009，Flickr 的 John Allspaw 和 Paul Hammond 在 Velocity 2009 年的大會上分享了題目為《Flickr 每天部署10次以上：開發與維運的高效率合作》  2011年5月，亞馬遜公司的月度統計顯示，平均每11就觸發一次軟體部署操作，當月最高部署頻率達到每小時1079次之多。平均1萬台伺服器會同時收到部署請求，而最高一次是3萬台伺服器同時執行一個部署操作  2017年，Facebook每天對其網站推送多次部署。    高頻發佈的好處   有更多機會與真實用戶互動，快速決定或調整自己產品前進的方向  每次變更規模較小，降低部署風險  單次部署成本降低  出現問題易定位、易修復、能快速修正  仍使用低頻發佈模式，強行執行高頻發佈的話，會帶來較高的迭代成本   設某團隊以手動模式每個月發佈一次，現改為每週發佈一次  假設「仍舊採用原有手動模式部署」，那每個月的工作量就是原來的四倍  12.2 降低發佈風險的方法   高頻帶來好處的同時，也存在各種風險(ex: 工作量提高、錯誤處理…)  藍綠部署   💡 準備兩套完全一致的執行環境，交互作為生產環境與預生產環境，目的為減少釋出過程中服務停止的時間   準備兩套完全一致的執行環境\n   一套環境 For 正式生產環境  另一套 For 新版本的預生產環境  對新版本環境進行驗收測試，直至確認無問題  將存取流量引流到新版本所在的環境，作為正式生產環境，同時保持舊版本所在環境不變  直至確定新版本沒有問題後，再將舊版本所執行的環境作為下一個新版本的預生產環境，部署未來的新版本     很多藍綠部署方案會使用相同的資料庫服務(因為資料庫複製的成本較高)\n   同一個資料庫儲存格式需對新舊兩個軟體版本做相容性處理  當切換發生在用戶的一次業務操作過程中且涉及事務處理時，如何處理資料的一致性問題\n   切換發生時尚未返回結果的舊有請求，舊版本環境允許其存取完成  之後不再接收新的請求即可    滾動部署     從服務群集中選擇一個或多個服務單元，停止服務後執行版本更新  相較於藍綠部署，不需要準備兩套一模一樣的服務執行環境，伺服器成本相當於少了一半  當新版本出現問題時，這種方式無法像藍綠部署那樣透過前面的流量負載平衡器切回舊環境\n   要對其中已部署新版本的伺服器進行還原  或快速修正後發部第三個版本V3，此時服器群集中可能會同時有V1~V3三個版本存在  金絲雀區佈   💡 讓一小部份用戶先行使用新版本，以便提前發現軟體存在的問題  17世紀，礦工帶一隻金絲雀下井。若井下存在有害氣體，在人體還沒察覺到前，金絲雀就會因無法抵抗瓦斯氣體而死亡  灰度發佈   💡 將發佈分成不同的階段，每個階段的用戶數量逐級增加     2012 Facebook 發佈一次首頁大改版\n   用戶數達總用戶的1%，網站的瀏覽量、頁面打開率等都有所下降  用戶超過10%後，各項關鍵業務仍舊表現不佳  於是Facebook 最終放棄這次首頁大改版，恢復了原有的版本  兩種實作方式\n   開關隔離方式：設置開關針對不同步圍的用戶開放新功能  滾動部署：將軟體的新版本部署到生產環境中的一部分節點上  暗部署   為演算法設一個開關\n   當開關打開時，會有流量進入此演算法，但用戶並不知道他用的是舊演算法還是新演算法  若此演算法的性能不夠好，可以馬上關閉這個開關  流量克隆\n   對每個請求都克隆一份，發送給新演算法  但新演算法並不向用戶回饋結果，而是開發人員自己收集資料  12.3 高頻發佈支撐技術  第8章提到，當一個軟體團隊的發佈頻率高於一週一次時，採「主線開發，主線發佈」更為經濟  但當某個功能無法在兩次發佈之間完成開發，怎麼辦？  拆分功能   分解為更小的在一個開發週期內能夠完成的功能集  先後再前   先實作伺服端功能，再實作用戶介面    功能開關技術   透過開關來隱藏未開發完成的功能   開關提供兩種用途\n   隔離：將未完成功能的程式隔離在執行路徑之外  快速止血：一但生產環境出了問題，直接找到對應功能的開關將其關閉  需考慮系統中所有開關的數量和組合測試的問題\n   開關越多，維護成本越高  開關存在時間越長，維護成本越高  應盡可能少用開關技術\n   if else 語句會帶來程式的複雜性，造成程式碼設計混亂  模組職責不清時，更容易出錯  但如果在「分支」和「開關」間選擇，盡可能選擇開關技術\n   建立分支會帶來後期的分支合入及多次測試成本  應對開關設置項目進行統一管理  定期檢查和清理不必要的開關項目  常見工具\n   C/C++: gflag，Google公司貢獻的開源工具  Java社群可用：Togglz or Flip  Grails: grails-feature-toggle  .Net: FeatureToggle  資料移轉技術   因為資料庫更新可能需要較長的時間，停機更新的方式可能不合適，所以處理資料庫的版更時可以選擇：   小幅改動的話：\n   只增不刪：欄位盡可能只增不刪  大幅資料結構的改變，則資料移轉SOP如下\n   為資料庫結構增加一個新版本  修改應用程式，同時向兩個版本的結構中寫入資料  編寫腳本程式，以後台服務的方式將原來的歷史資料，回填到新版本的結構中  修改應用程式，從新舊兩個版本中讀取資料，並進行比較，確保一致  當確認無誤後，修改應用程式，只向新版本結構寫入資料，可以將原來的舊版本資料保留一段時間，以防止未預期的問題出現  抽象分支方法  大型架構變動會需要較長的時間，傳統做法如下圖，以真實分支，大規摹覆寫的方式進行架構調整    這種方式在調整後第一次發佈時出現問題的概率較大，需要一定的品質打磨週期   💡 「抽象分支方法」是在不建立真實分支的情況下，透過設計手段，將大的重構專案分解成很多個小的程式碼變更步驟，逐步完成大的程式碼架構調整。     好處：\n   重構的同時也能交付業務功能需求  可以逐步驗證架構調整的方向和正確性  如果遇到緊急的情況，很容易暫停，且不浪費之前的工作量  能強化團隊的合作性  可以使軟體架構更模組化，變得更容易維護  成本\n   整個修改的時間週期可能會拉長  整體工作量比一次性完成的情況要大  案例\n   iBatis 和 Hibernate 是兩種物件關係映射框架 (Object Relational Mapping，ORM)  GoCD團隊曾使用抽象分支方法成功將IBatis 替換成Hibernate，並有兩個對外發佈的版本同時包含了這兩個ORM框架  在使用抽象分支方法前，也曾嘗試使用從主線上建立分支進行框架替換，但失敗了\n   團隊大多數人在主維上開發功能，分支上做框架替換，每天將主線程式碼同步到分支上  原預計三週可以完成的任務，六週也沒有能夠完成    如果使用建立分支的方式，通常必須停止大部分的新功能開發，否則很難成功  升級替代還原   我們總會遇到部署或發佈後出現一些問題，需要馬上修復  此時若有使用開關技術，便重新設置一下開關即可  但若沒有使用開關技術，怎麼辦呢？ ⇒ 升級替代還原  Facebook的處理方法：   盡可能以程式碼升級方式替代binary還原\n   典型的還原操作：將與待修復的問題相關的某次提交，以及與之相關的任何提交一同從程式碼倉庫中直接剔除，然後再次提交，等待下一次發佈即可  Facebook之所以可以以升級方式替代還原，得益於：\n   其工程師的程式碼提交遵循「小步、獨立、頻繁」的原則  發佈頻率高  Facebook工程師平均每天提交程式碼0.75次，平均每人每天提交約100行程式碼的修改  12.4 影響發佈頻率的因素   高頻發佈並非適合所有類型的軟體\n   需隨硬體發佈的嵌入式軟體開發來說，其對外發佈的成本非常高  一但軟體出現問題而導致退貨率上升，其損失可能相當高  決定軟體的發佈頻率時，需綜合考慮以下影響因素\n   增量發佈帶來的獲益和可行性  每次發佈或部署的操作執行成本有多高  出現問題的概率與由這些問題帶來的成本有多少  維護同一軟體的眾多不同版本帶來的成本  高頻發佈模式對工程師的技能要求  支撐這種高頻發佈所需要的基礎工具設施與流程完善性  組織對這種高頻發佈的態度與文化取向  其中 5、6、7對前面四項的結果會產生直接影響  可能會因這三項原因使高頻發佈的成本高居不下，獲益相對較少  此時企業領導就需做出更多努力，在後面三項上投入更多的精力  推遲發佈動機的漸進壞強環     大家習慣推遲風險  兩次發佈之間的間隔越長  累積的程式碼變更越多  所需品質驗證時間就越長，部署成本、發佈風險就越高  12.5 小結  本章討論了   如何在快速部署發佈的情況下，透過多種技術手段降底風險   資料庫遷移技術  各種部署策略\n   藍綠部署  金絲雀(灰度)發佈  抽象分支部署  暗部署  應對高頻部署的其它技巧\n   拆分功能、先後再前  開關技術  資料移轉技術  抽象分支方法  升級替代還原  在某些業務場景下，我們的確無法直接高頻地對外發佈軟體  但我們若能使用本章介紹的方法持續向預生產環境進行發佈與部署，就可以儘早獲得軟體的相關品質回饋，進而減少正式發佈後的風險。  如果我們能將每次發佈的平均成本降低到足夠低，那麼將會直接改變團隊的產品研發流程",{"id":241,"path":242,"dir":159,"title":243,"description":7,"keywords":244,"body":251},"content:4.cicd-2.0:13.chapter13.md","/cicd-2.0/chapter13","13 監測與決策",[65,245,246,247,248,249,250],"13.1 生產監測範圍","13.2 數據監控體系","13.3 問題處理體系","13.4 生產環境測試","13.5 向東，還是向西","13.6 小結","  前言   在有效率的開發高品質的軟體，並以低風險方式佈署和發布後，我們要開始觀測交付到用戶手中的軟體或應用程式的使用情形。  了解用戶的使用偏好和軟體執行的狀況，並以蒐集到的量化數據做為回饋，確保我們可以持續開發並改進軟體，為使用者繼續服務。  從用戶需求開始進行軟體開發，到低風險佈署/軟體發布，接著監控營運狀況並蒐集數據，我們完成了軟體開發流程的封閉環－讓我們驗證開發的功能是否有達到預期的目標。    13.1 生產監測範圍   為了了解用戶與軟體之間的互動，企業會根據相關的數據做為研發與改善的指標。  一個軟體對用戶的服務橫跨我們管理的後台伺服器和用戶裝置上，因此這兩個裝置上的服務和運行狀態都需要監測。  13.1.1 後臺服務的監測   基礎監測：針對系統基礎設施，包括網路與伺服器節點的監測。內容包含網路連線狀態、流量、CPU負載、內部和外部儲存空間等使用狀況。  應用監測：針對應用程式的運行健康，如流程是否存在、是否有正常提供服務、是否有缺陷、是否有正常的與伺服器連線、是否出現time out、是否友直回例外或警告、是否能應付突增的服務請求量等。  業務監測：針對業務指標，如用戶造訪量、功能使用需求等。  13.1.2 用戶裝置上的監測   監測用戶裝置上的軟體運作狀態和設備運行狀態需要取得使用者授權，並將蒐集的數據定期發送到後台伺服器上來分析。在離線狀態下，程式得先在本地端暫存數據，待連線後再一併上傳。  監測的項目與在後台的一樣：   基礎監測：針對用戶設備的硬體環境，以及與伺服器的連線狀態。  應用監測：同後台監測的內容。  服務監測：本地端的服務功能使用情形。  最後，也要根據市場評價來修正軟體服務的缺失，提供更優質的體驗。  13.2 數據監控體系  對監測數據的獲取過程、處理流程全面管理，包括數據來源、數據格式、採集週期、數據處理方法等。  13.2.1 收集與處理    每個步驟的職責如下：   採集上傳：在本地端蒐集事先定義的事件數據並上傳。  數據整理：過濾整理上傳數據。  實時分析：分析當前數據。  離線分析：對大量數據進行規則提取或模型化。  結果輸出：呈現實時和離線分析結果，供決策參考。  問題決策：根據輸出結果，透過人為或自動方式判定情況，並儲存判定結果，以利日後參考。  數據儲存：保存離線狀態的原始和分析後數據。  自動修復與執行系統之間的管道。修復指令需要被傳給執行系統，並由它將指令分配到對應的節點，進行相關操作。  13.2.2 數據標準化  要得到監測的數據，我們得先規劃軟體內如何觸動「事件」，並持續追蹤。持續交付價值環中強調的「探索」和「驗證」依靠用戶回饋來執行。為了提升驗證的即時性，在開發一個功能前，除了訂定功能開發目標外，還需要：   定義功能指標，以及如何處理功能指標－如何以指標量化該功能，且找出使否有與其他功能指標有關連；以及  數據事件的定義－要在程式碼內合適的位置安插監聽事件，定義輸入與輸出的格式，並確認數據是否與其他事件有關係。  若在開發實沒有定義數據指標的體系，當遇到產品開發瓶頸時，就無法察覺用戶行為。指標包括：   偏好關注基礎事件和應用事件的數據分析，而忽略了業務方面的數據。  在沒有重視業務數據監測的情況下，當打算蒐集數據時，得額外花費時間取得資料。  因此，在高度不確定的功能環境開發下，業務數據蒐集的重要性遠高於功能實現的重要性。  為了利於統計分析，一開始就必須先定義數據日誌格式與蒐集規範。訂定標準可以使數據蒐集和處理更方便，減少無用的數據或錯誤的分類，進而提升分析處理的效率和準確性。  數據紀錄的紀錄內容包含基礎訊息和擴展訊息。前者描述基本的應用情況，即 Who（哪一個用戶或服務）、When（何時發生）、Where（哪裡發生）、和 What（做了什麼）。擴展訊息是為了讓數據有更好的擴展性，以應對不同監測統計需求。  13.2.3 監測數據系統和能力衡量  監測用數據的可靠度可以用三個面向來衡量：   正確性：即收集到的數據與事實的一致性；  全面性：即收集到的數據是否足以用來下達決策；  及時性：即從數據產生到下達決策所需處理時間夠短。  團隊運作初期較常遇到數據結果與實際情況不符的情況，需要花比較多的時間。可以以下列兩種方式來驗確證數據品質：   可在運作初期尋找專家經驗來建立數據系統。  可使用來自系統外部（不同團隊）或內部（不同面相）的數據相互印證。  及時性是「  敏捷 」的重點。在更新某些功能後，都希望能儘早知道它對業務相關數據的影響。除了以上三個基本行量為度以外，監測系統還應具備  抽象能力 ，即根據實際數據量的需要，工程師可以即時配置每種數據採樣的密度。  13.3 問題處理體系  判別問題的方式有兩種：人工判別或是機器自動判別。面對大量的數據，不可能全依靠人工處理。通常先由電腦根據各種規則判斷，盡可能找出疑似的問題。無法自動處理時，就發布警告給指定的工程師。  13.3.1 海量的警告與智能化管理  面對大量的警告訊息，會希望減少警告，但另一方面又擔心出了是沒有警告，只好再加入更多的警報。事實上，大部分的警告訊息會被忽略，主要是因為：   自己不是該警告的負責人，  這只是一個預警，不用馬上處理。  警告當然有它們的正確性和真實性，但同時也要提高這些訊息的另外兩個面向：即時性和可操作性。針對後者，處理者應該可以對警告做出有用的操作，否則該訊息應被當成垃圾訊息屏蔽，避免降低工作效率；一但真正的警告被海量的垃圾訊息淹沒，就很容易釀成災難。這個問題可以靠四個方面來減緩：   讓監控點離問題發生地更近。  透過動態閥值設定合理的警告。  定期回顧警告配置，並清理多餘的警告。  用AI動態解除警告。  這些方法可以將警告數量控制在一定的範圍內，但無法完全消除。常見的警告可能已有對應的方法；真正要花時間的是那些以往沒有出現過的異常警告，因為它們很可能是「生產問題」。  13.3.2 問題處理是一個學習過程    問題處理是一個複雜的團隊活動。可提高效率的做法：   儘可能將人工步驟自動化  建立工單系統  回顧處理流程  13.4 生產環境測試   生產環境是獨一無二，我們永遠無法1保證發現生產環境中可能出現的所有問題。  隨著軟體快速發佈訴求的提升，非生產環境的測試場景越來越顯得不充分。  如何在不影響生產的前提下，在生產環境中進行測試？  13.4.1 測試活動扁平化趨勢   傳統的混布式軟體開發方法中，測試執行和決策活動通常集中在軟體研發周期的中部。  隨著現代軟體交付頻率的不斷加快，這種情況出現了變化。很多團隊的測試活動開始向左右兩側移動，如圖13-7所示。\n   測試左移(測試前移)，是指測試人員更早且積極地參與到軟體專案前期各階段活動中，在開發功能之前就定義相關的測試案例。  測試執行任務也在向左移動，表現為 : 在越來越多的軟體團隊中，測試角色開始擁抱「增量測試」，即在軟體整合測試之前，就開始針對單個已開發完成的功能集進行品質驗證，提前發現品質風險。儘管這種「增量測試」無法發現全部品質問題，但可以減少整合測試階段的時間壓力，如圖13-8所示\n   測試左右移動   「測試右移」是指將一部份品質驗證工作放在軟體發布之後，也就是讓子彈飛一回兒。  主要因為網際網路軟體產品的測試，與原先企業內部應用的軟體測試有顯著不同，即無法窮舉性。  企業內部軟體的使用者有限，環境相對可控, 但網際網路產品的情況有所不同，每個人的電腦或手機上都安裝著不同的軟體，硬體與作業系統也有很多種組合，因此無法以窮舉方式進行所有相關場景測試。因此大家也越來越依賴於生產環境上的品質驗證  測試右移現象多見於軟體產品中的示範性功能(software for show)，即軟體功能更多地傾向於內容展示，例如搜尋軟體、拍照軟體、商品展示。即便這類功能出現一些問題，但只要即時發現即時修復，就不會對用戶造成本質性損失或嚴重影嚮。  對事務性軟體(software for transaction)以及問題修復成本較高的軟體(firmware)來說，一但生產環境出現問題，會帶來比較大的損失。因此軟體團隊不會冒險將功能驗證的活動右移，而是有強列的動機將測試活動盡可能左移，同時加強右測的監測能力。  13.4.2 生產環境中的測試   QA部份應將生產環境中的測試列入日常工作中，稱為「生產巡檢」。  對生產環境中的後台服務進行定期功能驗證，以確保該後台服務備舊對外正常提供服務，且處理的結果是正確的。  通常的做法是：建立一個覆蓋應用程式主要功能的日常健康檢查清單，對生產環境進行例行測試和檢查軟體服務的品質。這種測試方法與監測不同，它們是由軟體團隊自行安排的品質驗證工作，並且定期執行，因為這是一些例行驗證，所以應該被自動化執行．。這類測試中最典型的就是介面測試。  很多團隊開始將一些自動化介面測試的案例放在生產環境鞏，周期性執行，以代替手動檢查。  這類生產環境上的品質保障工作應遵循以下原則\n   建立自用的測試資料，確保不污染真實用戶的資料  使用的測試資料盡可能真實  不要隨便修改真實用戶的資料  建立測試專用的用戶存取憑證，登錄生產環境  13.4.3 混沌工程   混沌工程(chaos engineering)是指在生產環境中注入「問題」，進而發現生產環境系統性弱點，並進行系統性改進的方法或手段。  其目標是不斷提升生產環境面對任可變更的可靠性。這與疫苗注射類似，向系統注入一些小劑量的「病毒」使身體康立對它的抵抗力，進而使身體獲得免疫性。  此部份最有名的例子為Netfilx的  Simon Army \n![13-8-1-Simian Army.png](images/cicd-2.0/13/13-8-1-Simian Army.png)  Netflix在AWS上，開發了一系列生產環境測試工具，稱為Simon Army，用於模擬AWS如區(zone)或大區(region)出現問題，以及模擬人為製造呼叫延遲，用來模擬服務降級，看依賴這些服務的模組是否能正確做出反應。  這種問題注入(Failing Injection)式的主動檢測，可使軟體工程師在架構設計上就需考慮一些常見的失敗問題。  13.5 向東，還是向西     快速驗證環中，我們將精練後的試驗方案變為可執行的軟體，部署到生產環境，並且收集了執行結果和用戶回饋。現在是要決定下一步「向東，還是向西」以完成第一個業務閉迴圈  透過分析總結，可幫助我們從收集到的數據中來判定前進目標  若收集的結果不合預期，只要與團隊共同分析，看是針對現行方案進行微調，還是從備案中選擇新的試驗方案，繼續驅動這個快速驗證環  13.6 小結  生產環境的監測範圍包含三個層次 :   基礎監測  應用監測  業務監測  每個層次的特點，監測資料的採集方式有所不同，但其處理流程基本一致，包括:\n資料收集、上報、整理、分析、展現與決策這幾個環節  而對監測系統能力的衡量有3個維度 :   資料的準確性  全面性  即時性   告警處理是研發人員和維運人員的常規工作，但告警過多反而會造成工作中的困擾，降底工作產出。因此我們應該不斷對告警點的設置及臨異值計算方式進行最佳化，進而盡可能提升有效告警率。一但告警成立，就需要啟動問題處理流程。這個流程的最後兩個環節 「根因分析」和「根源解決」是學習型組織的重要特徵  隨著發佈頻率的提高，測試場景的複雜性提高，越來越多團隊開始找尋方法在生產環境上進行軟體測試，這被稱為測試活動右移。這種只適用於軟體出錯後的成本和影嚮相對較少。而那些較交易性軟體或回收成本較高的軟體來說，測試左移的趨勢也較明顯。  測試右移主要有兩種類型 :   將測試案例在生產環境上自動執行  混沌工程(Chaos engineering)   Netfilx開發了一系列破壞性測試工具(Simian Army)可以促使工程師在軟體設計與開發之時，就提前考慮各種失敗的可能性，這被稱為「為失敗而設計」進而提高生產環境的軟體服務穩定性，為用戶提供更好的服務體驗。  當收集到真實的資料回饋，就可印證在價值探索環中所提出的假設或目標，並透過主動關聯分析，最終確定是繼續進行更多的試驗，還是重新再選擇一條新的路",{"id":253,"path":254,"dir":159,"title":255,"description":256,"keywords":257,"body":262},"content:4.cicd-2.0:16.chapter16.md","/cicd-2.0/chapter16","16 研發推動的DevOps","過分強調並專注於docker、Jenkins、測試框架、Mock框架或持續集成服務器等工具，有悖於敏捷宣言 -- 和流程與工具相比，個體與交互更為重要。",[258,259,260,261],"16.1 改進的關鍵點","16.2 第一階段：敏捷101","16.3 第二階段：DevOps轉型","16.4 小結","  過分強調並專注於docker、Jenkins、測試框架、Mock框架或持續集成服務器等工具，有悖於敏捷宣言 -- 和流程與工具相比，個體與交互更為重要。  案例背景：   B公司部門組成：\n   B公司某業務線下的一個後台服務業務團隊，這個團隊負責網頁搜索產品的後台服務。該服務是一個相對獨立的子系統，接受其他服務的請求，也將自己服務的處理結果提供給其他服務使用。   由一個程序模組組成，  總體代碼量約為10萬行，  全部由C++語言編寫，  每個模組都是一個單獨的程序服務（可以認為是微服務），  運行在近300台服務器上。從前端的資料流獲取開始，再到資料流的解析、分類入庫。    工作模式   分支開發、集中聯調階段，測試人員參與較少  程式碼管理：分支開發、主幹發布    16.1 改進的關鍵點  16.1.1 改進方法論   目標驅動  從簡單問題開始  持續改善  16.1.2 定義改進目標   工作過程中的痛點   部門負責人的期望  團隊管理者交付的壓力  項目負責人的煩惱  制訂合理的階段性改進目標   短期目標\n   項目近預期時間交付  創建新的軟體開發協作方式  建立必要的基礎設施，以支持之後的持續交付  中期目標\n   縮短發布週期，可以快速上線  不降低生產環境的品質  降低測試人力總投入  改進目標對應兩個實施階段   第一階段，敏捷101  第二階段，DevOps轉型  16.2 第一階段：敏捷101   簡介   也稱為Water-Scrum-fall模式，指瀑布開發框架模式下，對各階段內部進行迭代時間盒的劃分  迭代週期通常為1~4週。開發階段內的每個迭代週期內，都會發生需求分析、程式開發、測試活動。並且每個開發迭代結束時做演示驗收  進入開發階段後，仍舊有需求收集、分析和計劃的階段，並且在結束後也會再安排1~2個作為系統測試迭代  最後安排系統試營運階段，然後再正式上線  特點：   瀑布開發模式中的幾大階段沒有變化，只對各階段內部活動進行適當調整  此模式常應用在對於持續交付理解不深、研發基礎設施不完備，但希望進行改善的團隊    16.2.1 做個靠譜的計劃   需求拆分，開發負責人根據下列3個要求作拆分，並交給測試人員進行評審和補充後，再次相互討論並達成意見一致\n   每個需求實現少於三天  拆分要遵循  INVEST原則   獨立的（Independent）  可協商的（Negotiable）  有價值的（Valuable）  可估算的（Estimatable）  較小的（Small)  可測試的（Testable）  拆分過程中的權衡  相對估算，使用排序法對需求估算，排序法有2主張(1  2)與4前提(3 6)\n   將較多的需求放一起，結合上下文做比較 (降低因人員能力差異帶來的偏查)  在需求的數量較多時，項目整體規模評估會相對準確  每個需求至少有兩個人較了解，且是可以完成的  不需要評估測試活動的工作量 (開發環節才是系統的瓶頸，若測試環節就會成為瓶頸甚至可認為開發資源已投入過多)  所有需求都拆解，並且規模不會相差太多  需求的個數相對較多  初始計劃\n   理想情況下，每週工作時間是多少  理想情況下，每週能完成多少需求  實際每週最有可能開發完成多少需求  如何處理整個項目開發過程中的線上需求變更\n   例如是在發布主幹上拉一條項目開發分支，所有人以這個分支進行開發。對於臨時的緊急需求，在線上發布版本分支上單獨拉分支，快速修復線上問題或開發緊急需求，再快速合併上線\n   系統測試的時間在哪裡？  其他類型的測試怎麼辦？  計劃時還需要考慮依賴因素\n   例如考慮業務或技術上，是否與其他團隊有依賴的關係？  項目計劃在整體上要加一個緩衝時間  16.2.2 開發階段啟航   迭代週期的選擇     水位當作開發的時間範圍，岩石當作日常問題  若以傳統模式開發，此時問題難以暴露出來  若以迭代模式開發，因為交付時間變短，相當於水位變低，問題就能提早凸顯出來。但是水位的高低的選擇，則需要團隊視情況而決定  團隊協作流程   每個迭代的工作約定  對於單個需求的開發流程約定   批量開發，批量提測：指開發人員等到全部或大部分功能開發完成後，再一起聯合驗證  單例開發，即時提測：指每當開發人員開發完一個需求之後，就即刻交給測試人員進行驗證    16.2.3 對過程品質的約束   如何能自覺遵守CI定律？  編譯時間過長   開發人員寫完程式碼，提交前在自己機器上運行命令腳本，將SVN當前版本耗與patch一起提交給服務端，空閒時就會自動編譯。若出錯就會得到反饋，若沒問題開發人員則可提交程式碼    開發人員無法運行自動化測試   迭代開發中，自動化測試的第一個用戶是開發人員，來檢查新增修改的程式碼是否正常  自動化測試的策略。根據自動化測試金字塔理論及團隊狀況做出測試約定   單元測試  模組自動化測試  服務接口自動化測試  子系統級自動化測試  大環自動化測試    自動化測試所需運行環境不足怎麼辦  如何確定一個需求可以提測   測試人員在開發人員的機器上體驗過該需求  所有自動化測試都通過。  如何作性能與壓力測試   對開發人員的工作要求\n   在迭代開發的工作模式下，所有需求是依據PARID五因素綜合優先級排入迭代計劃當中，這會要求開發人員盡量能夠多了解和修改不同的模組功能  團隊的自我主動改進\n   每個迭代舉行回顧會議，對工作過程中成員的協作方式、工作活動的約定、團隊面臨的問題及可能的解決方案進行討論  不能修改最初估算的大小\n   新增需求：以前未評估過，所以需要快速討論、估算，以便加入開發計劃當中  已估需求：單個需求估算本就不精確，原先的估算是基於整體的總量評估，因此不太會因為個別需求重新評估而偏差太多  16.2.4 階段性改進點   業務目標合理  項目計劃透明  流程\"自定義，自遵守\"，團隊確保高品質交付  定期主動回顧，而非事件驅動的回顧  通過細粒度需求組織開發流程  持續集成六步提交法  適當是用自動化測試，提高品質反饋效率    16.3 第二階段：DevOps轉型   透過敏捷101的方式，對於時程預估、開發團隊的合作、測試流程都有一定的成長，團隊開始朝向小批量生產的方式開發，並且開始有比較頻繁的成果發佈(ex.兩週一次)  從過去的分支開發、合併主線內容到分支、集中測試，改為主線開發、分支發布的城際快線開發模式\n   在wiki上可以知道每個發布的時間點，以及對應的發布列表  客戶可以隨時提交需求  需求列表中，不但有客戶要求，還包含自我優化需求排程  16.3.1 與維運人員的衝突   因為頻繁的發布，引發維運人員的憂慮\n   過去測試時間那麼長，上線的系統還是會出現各種問題，現在測試的時間縮短後(測試左移，導致最後上線前測試時間縮短)，那上線一定會更容易出問題  我會被累死，部署到多台主機，以前三個月加班一次，現在變成兩週就要加班一次  透過讓維運人員參與團隊日常的運作，增加透明度\n   開發負責人為維運人員詳細講解目前使用的研發流程模式，讓他了解目前對於程式碼品質的保障方式  邀請維運人員參與開發團隊活動，參加日常工作的討論ex.參與站立會議、參與團隊衝刺週期後的回顧會議  將維運人員加入開發團隊的工作溝通群中，以便隨時了解運作狀況，這維運人員不再視為開發團隊是一個看不到內部的「黑盒」  針對維運需求進行架構調整，讓維運人員的工作更有效率(過去對於維運的需求都以有空再來處理而忽悠過去，現在變成一個合作夥伴，一起讓讓部署過程可以更自動化)  16.3.2 高頻部署發布中的具體障礙   由於歷史共業，各模組的部署方式不一致，導致不利於統一維運  部分模組在部署時，維運人員需要手動創建某個目錄，備份程式運行時產生的臨時數據  同樣的模組在不同機器上的部署位置都有差別  同樣的模組在不同機器上對外的接口策略也不同  程式碼是用絕對路徑而非相對路徑  部署操作文件是由開發在部署前編寫(代表每次可能都不一樣，導致無法透過自動化反覆執行)，維運人員根據文件說明操作  開發環境的部署方式跟正式環境的部署方式不同  測試代碼存在QA的儲存庫，而不是跟著專案跑，導致只有QA才有辦法執行測試  開發環境、測試環境共用，導致髒資料議題  自動化測試覆蓋率不足，導致測試人員仍然需要執行很多手工回歸測試(每次上版都要手動測試全系統)  16.3.3 整體解決方案的設計   自動化測試策略的調整：\n   把測試從QA擴展到全開發，對於開發人員進行單元測試框架的培訓  減少不同層次間的重複測試，認知「通過低層次的測試來驗證很難在高層次驗證的測試」，找到最適合測試的階層，不是所有的測試都是交給QA執行整合測試     自動化測試的便捷性\n   透過配置文件的方式讓不同的開發可以共用一個測試環境(ex.只要換不同的配置文件，系統就可以上到某一台測試主機，要換到另一台主機，只要更換配置文件不需要修改程式碼就可以遷移過去)  測試代碼同源\n   把測試代碼放到專案裡面，而不是QA的儲存庫，這樣任何人都可以去執行測試  配置管理優化\n   代碼庫結構\n   程式資料夾有統一的結構，模組相關的就在該模組資料夾下(ex.該模組的測試，是放在該模組的test資料夾內，而不是整個系統的測試全部放一起)，該模組的config就放在該模組下  ex.單元測試就放在「模組a/test/unittest/測試案例1」  會有output的資料夾，提供維運人員部署使用  產出物的標準化與版本管理\n   持續整合產出的成品，透過建構號(build_id)作為成品的唯一識別  取代手工自行編譯  軟件包管理\n   只有經過一系列的自動化測試，並經過測試人員手工驗證後，才在臨時構建成品庫中標記為「符合品質標準」，當要獲取某個版本部署時，只能從臨時構建成品庫中選擇那些符合品質標準的版本放入產品發布庫    16.3.4 DevOps階段的團隊改變   完整的跨功能團隊，維運積極參與團隊的日常工作迭代會議  所有內容都做版本控制，包括原始碼、測試程式、各類環境的配置、相關的打包及安裝腳本，以及一些數據  所有環境標準化管理，可以一鍵式準備好測試環境  建立完整的部署流水線，可以一鍵式發佈到多種部署環境    16.4 小結   所有改進都是一種一連串的連續流程，從明確目標、診斷問題、解決方案、持續運營優化  過程中抱持著持續學習的心態  ",{"id":264,"path":265,"dir":266,"title":267,"description":7,"keywords":268,"body":274},"content:5.ddia:1.chapter1.md","/ddia/chapter1","ddia","01 可靠性、可伸縮性和可維護性",[269,270,271,272,273],"第一章：可靠性、可伸縮性和可維護性","關於資料系統的思考","可靠性","可伸縮性","可維護性","  第一章：可靠性、可伸縮性和可維護性  現在多數系統都是  資料密集型（data-intensive）  的，而非 計算密集型（compute-intensive）  資料密集型應用通常由標準組件構建而成，標準組件提供了很多通用的功能；例如，許多應用程式都需要：   儲存資料，以便自己或其他應用程式之後能再次找到 （  資料庫，即 databases ）  記住開銷昂貴操作的結果，加快讀取速度（  快取，即 caches ）  允許使用者按關鍵字搜尋資料，或以各種方式對資料進行過濾（  搜尋索引，即 search indexes ）  向其他程序傳送訊息，進行非同步處理（  流處理，即 stream processing ）  定期處理累積的大批次資料（  批處理，即 batch processing ）  關於資料系統的思考  近些年來，出現了許多新的資料儲存工具與資料處理工具。它們針對不同應用場景進行最佳化，因此不再適合生硬地歸入傳統類別【1】。類別之間的界限變得越來越模糊，例如：資料儲存可以被當成訊息佇列用（Redis），訊息佇列則帶有類似資料庫的持久保證（Apache Kafka）。  其次，越來越多的應用程式有著各種嚴格而廣泛的要求，單個工具不足以滿足所有的資料處理和儲存需求。取而代之的是，總體工作被拆分成一系列能被單個工具高效完成的任務，並透過應用程式碼將它們縫合起來。  例如，如果將快取（應用管理的快取層，Memcached 或同類產品）和全文搜尋（全文搜尋伺服器，例如 Elasticsearch 或 Solr）功能從主資料庫剝離出來，那麼使快取 / 索引與主資料庫保持同步通常是應用程式碼的責任。  圖 1-1 給出了這種架構可能的樣子（細節將在後面的章節中詳細介紹）。    可靠性  人們對可靠軟體的典型期望包括：   應用程式表現出使用者所期望的功能。  允許使用者犯錯，允許使用者以出乎意料的方式使用軟體。  在預期的負載和資料量下，效能滿足要求。  系統能防止未經授權的訪問和濫用。  如果所有這些在一起意味著 “正確工作”，那麼可以把可靠性粗略理解為 “即使出現問題，也能繼續正確工作”。   造成錯誤的原因叫做   故障（fault）   **故障（fault）**不同於   失效（failure） -  故障為部分狀態偏離標準，失效則為整個系統掛掉，因此最好設計容錯機制，以防因故障而失效。  我們可以透過故意引發故障來確保容錯機制不斷執行並接受考驗，從而提高故障自然發生時系統能正確處理的信心。範例：Netflix 公司的   Chaos Monkey 【4】  能預料並應對故障的系統特性可稱為 **容錯（fault-tolerant）**或   韌性（resilient）  硬體故障**（hardware faults）**    為了減少系統的故障率，第一反應通常都是增加單個硬體的冗餘度 ，例如：磁碟可以組建 RAID，伺服器可能有雙路電源和熱插拔 CPU，資料中心可能有電池和柴油發電機作為後備電源，某個元件掛掉時冗餘元件可以立刻接管。這種方法雖然不能完全防止由硬體問題導致的系統失效，但它簡單易懂，通常也足以讓機器不間斷執行很多年。  **但是隨著資料量和應用計算需求的增加，越來越多的應用開始大量使用機器，這會相應地增加硬體故障率。**此外，在類似亞馬遜 AWS的一些雲服務平臺上，虛擬機器例項不可用卻沒有任何警告也是很常見的，因為雲平臺的設計就是優先考慮 靈活性（flexibility） 和 彈性（elasticity），而不是單機可靠性。   如果在硬體冗餘的基礎上進一步引入軟體容錯機制，那麼系統在容忍整個（單臺）機器故障的道路上就更進一步了。這樣的系統也有運維上的便利 ，例如：如果需要重啟機器（例如應用作業系統安全補丁），單伺服器系統就需要計劃停機。而允許機器失效的系統則可以一次修復一個節點，無需整個系統停機。  軟體錯誤  是內部的  系統性錯誤（systematic error） ，例子包括：   接受特定的錯誤輸入，便導致所有應用伺服器例項崩潰的 BUG。例如 2012 年 6 月 30 日的閏秒，由於 Linux 核心中的一個錯誤，許多應用同時掛掉了。  失控程序會用盡一些共享資源，包括 CPU 時間、記憶體、磁碟空間或網路頻寬。  系統依賴的服務變慢，沒有響應，或者開始返回錯誤的響應。  級聯故障，一個元件中的小故障觸發另一個元件中的故障，進而觸發更多的故障。  雖然軟體中的系統性故障沒有速效藥，但我們還是有很多小辦法，例如：   仔細考慮系統中的假設和互動  徹底的測試  程序隔離  允許程序崩潰並重啟  測量、監控並分析生產環境中的系統行為  如果系統能夠提供一些保證（例如在一個訊息佇列中，進入與發出的訊息數量相等），那麼系統就可以在執行時不斷自檢，並在出現 **差異（discrepancy）**時警報  人為錯誤  人為錯誤是導致服務中斷的首要原因，但還是有些方法可以增加系統可靠度：   以最小犯錯機會的方式設計系統，讓做對事情更容易，讓做錯事情更困難。  將人們最容易犯錯的地方與可能導致失效的地方   解耦（decouple） 。特別是提供一個功能齊全的非生產環境  沙箱（sandbox） ，使人們可以在不影響真實使用者的情況下，使用真實資料安全地探索和實驗。  在各個層次進行徹底的測試，從單元測試、全系統整合測試到手動測試。自動化測試特別適合用來覆蓋正常情況中少見的  邊緣場景（corner case） 。  允許從人為錯誤中簡單快速地恢復，以最大限度地減少失效情況帶來的影響。 例如，快速回滾配置變更，分批發布新程式碼（以便任何意外錯誤只影響一小部分使用者），並提供資料重算工具（以備舊的計算出錯）。  配置詳細和明確的監控，比如效能指標和錯誤率。 在其他工程學科中這指的是 遙測（telemetry）（一旦火箭離開了地面，遙測技術對於跟蹤發生的事情和理解失敗是至關重要的）。監控可以向我們發出預警訊號，並允許我們檢查是否有任何地方違反了假設和約束。當出現問題時，指標資料對於問題診斷是非常寶貴的。  良好的管理實踐與充分的培訓 —— 一個複雜而重要的方面，但超出了本書的範圍。  可伸縮性  **可伸縮性（Scalability）**是用來描述系統應對負載增長能力的術語。  服務**降級（degradation）**的一個常見原因是負載增加  描述負載  負載可以用一些稱為 **負載引數（load parameters）**的數字來描述。引數的最佳選擇取決於系統架構，它可能是每秒向 Web 伺服器發出的請求、資料庫中的讀寫比率、聊天室中同時活躍的使用者數量、快取命中率或其他東西。除此之外，也許平均情況對你很重要，也許你的瓶頸是少數極端場景。  描述效能  當負載增加時會發生什麼，可以從兩個角度來看   增加負載引數並保持系統資源（CPU、記憶體、網路頻寬等）不變時，系統性能將受到什麼影響？  增加負載引數並希望保持效能不變時，需要增加多少系統資源？  對於 Hadoop 這樣的批處理系統，通常關心的是   吞吐量（throughput） ，即每秒可以處理的記錄數量，或者在特定規模資料集上執行作業的總時間。  對於線上系統，通常更重要的是服務的   響應時間（response time） ，即客戶端傳送請求到接收響應之間的時間。  就算是相同需求，響應時間也會略有不同，因此需要將響應時間視為一個可以測量的數值   分佈（distribution） ，而不是單個數值。  如果想知道**典型（typical）**響應時間，使用中位數會比平均值更好。為了弄清楚異常值有多糟糕，可以看更高的百分位點，例如95%、99%、99.9%。  響應時間的高百分位點（也稱為   尾部延遲 ，即   tail latencies ）非常重要，因為它們直接影響使用者的服務體驗。  這是因為請求響應最慢的客戶往往也是資料最多的客戶，也可以說是最有價值的客戶 —— 因為他們掏錢了  另一方面，最佳化第 99.99 百分位點（一萬個請求中最慢的一個）被認為太昂貴了，不能為亞馬遜的目標帶來足夠好處。減小高百分位點處的響應時間相當困難，因為它很容易受到隨機事件的影響，這超出了控制範圍，而且效益也很小。  百分位點通常用於   服務級別目標（SLO, service level objectives）  和   服務級別協議（SLA, service level agreements） ，即定義服務預期效能和可用性的合約。 SLA 可能會宣告，如果服務響應時間的中位數小於 200 毫秒，且 99.9 百分位點低於 1 秒，則認為服務工作正常（如果響應時間更長，就認為服務不達標）。這些指標為客戶設定了期望值，並允許客戶在 SLA 未達標的情況下要求退款。   排隊延遲（queueing delay）  通常佔了高百分位點處響應時間的很大一部分。由於伺服器只能並行處理少量的事務（如受其 CPU 核數的限制），所以只要有少量緩慢的請求就能阻礙後續請求的處理，這種效應有時被稱為   頭部阻塞（head-of-line blocking）  。  應對負載的方法    縱向伸縮 （scaling up，也稱為垂直伸縮，即 vertical scaling，轉向更強大的機器）   橫向伸縮 （scaling out，也稱為水平伸縮，即 horizontal scaling，將負載分佈到多臺小機器上）  跨多臺機器分配負載也稱為 “  無共享（shared-nothing） ” 架構。可以在單臺機器上執行的系統通常更簡單，但高階機器可能非常貴，所以非常密集的負載通常無法避免地需要橫向伸縮。現實世界中的優秀架構需要將這兩種方法務實地結合，因為使用幾臺足夠強大的機器可能比使用大量的小型虛擬機器更簡單也更便宜。   有些系統是   彈性（elastic）  的，這意味著可以在 檢測到負載增加時自動增加計算資源  而其他系統則是手動伸縮（人工分析容量並決定向系統新增更多的機器）  如果負載   極難預測（highly unpredictable） ，則彈性系統可能很有用，但手動伸縮系統更簡單，並且意外操作可能會更少（請參閱 “  分割槽再平衡 ”）。  跨多臺機器部署   無狀態服務（stateless services）  非常簡單，但  將帶狀態的資料系統從單節點變為分散式配置則可能引入許多額外複雜度 。出於這個原因，常識告訴我們應該將資料庫放在單個節點上（縱向伸縮），直到伸縮成本或可用性需求迫使其改為分散式。  一個良好適配應用的可伸縮架構，是圍繞著   假設（assumption）  建立的：哪些操作是常見的？哪些操作是罕見的？這就是所謂負載引數。如果假設最終是錯誤的，那麼為伸縮所做的工程投入就白費了，最糟糕的是適得其反。在早期創業公司或非正式產品中，通常支援產品快速迭代的能力，要比可伸縮至未來的假想負載要重要的多。  可維護性  避免自己的系統變成  遺留（legacy）  系統的三個設計原則：  一、可操作性（Operability）  便於運維團隊保持系統平穩執行。   一個優秀運維團隊的典型職責如下（或者更多）：   監控系統的執行狀況，並在服務狀態不佳時快速恢復服務。  跟蹤問題的原因，例如系統故障或效能下降。  及時更新軟體和平臺，比如安全補丁。  瞭解系統間的相互作用，以便在異常變更造成損失前進行規避。  預測未來的問題，並在問題出現之前加以解決（例如，容量規劃）。  建立部署、配置、管理方面的良好實踐，編寫相應工具。  執行複雜的維護任務，例如將應用程式從一個平臺遷移到另一個平臺。  當配置變更時，維持系統的安全性。  定義工作流程，使運維操作可預測，並保持生產環境穩定。  鐵打的營盤流水的兵，維持組織對系統的瞭解。   資料系統可以透過各種方式使日常任務更輕鬆：   透過良好的監控，提供對系統內部狀態和執行時行為的   可見性（visibility） 。  為自動化提供良好支援，將系統與標準化工具相整合。  避免依賴單臺機器（在整個系統繼續不間斷執行的情況下允許機器停機維護）。  提供良好的文件和易於理解的操作模型（“如果做 X，會發生 Y”）。  提供良好的預設行為，但需要時也允許管理員自由覆蓋預設值。  有條件時進行自我修復，但需要時也允許管理員手動控制系統狀態。  行為可預測，最大限度減少意外。  二、簡單性（Simplicity）  從系統中消除儘可能多的   複雜度（complexity） ，使新工程師也能輕鬆理解系統（注意這和使用者介面的簡單性不一樣）。  一個陷入複雜泥潭的軟體專案有時被描述為   爛泥潭（a big ball of mud）  **複雜度（complexity）**有各種可能的症狀，例如：狀態空間激增、模組間緊密耦合、糾結的依賴關係、不一致的命名和術語、解決效能問題的 Hack、需要繞開的特例等等。  簡化系統並不一定意味著減少功能；它也可以意味著消除   額外的（accidental）  的複雜度。 Moseley 和 Marks把   額外複雜度  定義為：由具體實現中湧現，而非（從使用者視角看，系統所解決的）問題本身固有的複雜度。消除  額外複雜度  的最好工具之一是   抽象（abstraction）  三、可演化性（evolvability）  使工程師在未來能輕鬆地對系統進行更改，當需求變化時為新應用場景做適配。也稱為   可擴充套件性（extensibility） 、**可修改性（modifiability）**或   可塑性（plasticity） 。   敏捷（agile）  工作模式為適應變化提供了一個框架。敏捷社群還開發了對在頻繁變化的環境中開發軟體很有幫助的技術工具和模式，如 **測試驅動開發（TDD, test-driven development）**和   重構（refactoring） 。  修改資料系統並使其適應不斷變化需求的容易程度，是與   簡單性  和   抽象性  密切相關的：簡單易懂的系統通常比複雜系統更容易修改。但由於這是一個非常重要的概念，我們將用一個不同的詞來指代資料系統層面的敏捷性：   可演化性（evolvability）",{"id":276,"path":277,"dir":266,"title":278,"description":279,"keywords":280,"body":286},"content:5.ddia:3.chapter3.md","/ddia/chapter3","03 數據儲存與檢索","ddia/ch3.md at master · Vonng/ddia",[281,282,283,284,285],"引言","驅動資料庫的資料結構","B樹","事務處理還是分析？","本章小結","   ddia/ch3.md at master · Vonng/ddia  引言  數據庫最基本兩件事：   儲存數據  檢索數據  本章會討論   資料庫如何儲存資料  如何找到資料  儲存引擎依據負載進最佳化，分成兩種   事務型負載最佳化  Online Transaction Processing (OLTP) 儲存引擎，用於高併發的交易性應用，需要支援併發的讀取或寫入操作。  分析性負載最佳化  Online Analytical Processing (OLAP) 儲存引擎，用於針對大數據集進行分析查詢，支援複雜的聚集計算和多維分析。  兩大類資料庫引擎   page-oriented  頁面導向資料庫被用於儲存表格，其中每個表格被分為好幾個磁碟頁面。磁碟頁面是資料庫系統與磁碟交互的最小單位，通常大小為 4KB 或 8KB。  log-structured  日誌導向資料庫最初是由 Google 發明，用於處理 Web 搜索的索引數據。這種資料庫引擎將所有修改操作寫入一個追蹤寫入（write-ahead log, WAL）中，然後將實際數據讀取到記憶體中，並在記憶體中進行修改。當修改完成時，這些修改操作會被追加到 WAL 中，然後再以某種方式寫入磁碟。這種方式允許寫入操作非常快速，並且可以維護良好的寫入吞吐量，從而使其成為許多大型分佈式數據庫的理想選擇。  驅動資料庫的資料結構  最簡單的資料庫     #!/bin/bash\n   db_set   () {\n     echo   \"  $1  ,  $2  \"   >>   database\n   }\n   \n   db_get   () {\n     grep   \"^  $1  ,\"   database   |   sed   -e   \"s/^  $1  ,//\"   |   tail   -n   1\n   }\n  這個簡單的程式實現鍵值儲存和讀取，每次set 時會在檔案尾巴追加記錄，讀取時會從最後一行往上找直到找到key     $ db_set   123456   '{\"name\":\"London\",\"attractions\":[\"Big Ben\",\"London Eye\"]}'\n   \n   $ db_set   42   '{\"name\":\"San Francisco\",\"attractions\":[\"Golden Gate Bridge\"]}'\n   \n   $ db_get   42\n   {  \"name\"  :  \"San Francisco\"  ,  \"attractions\"  :[  \"Golden Gate Bridge\"  ]}\n  db_set 有非常好的效能，因為在檔尾追加寫入是非常高效的，許多資料庫的內部使用了log structed，也是用  僅追加(append-only)  db_get 的效能則很糟, O(n)，為了改善這個問題，我們需要一個數據結構 Index  Index 是一個「額外」的資料，能改變查詢的效能，但是在寫入時會增加開銷，所以寫入效能很難超過單純的追加寫入檔案  權衡選什麼索引就是DBA的專業  Hash Index  為了簡化，書中假設要儲存的資料是 key-value Data  假如資料要存進檔案，那 Index 最簡單的作法就是 存下每筆資料在檔案的 Offset    查詢資料時，只要從index取得 offset，再去檔案尋找(seek)即可。  感覺很簡單，實際上 Bitcask 就是這麼做的。  這種結構特別適合在很多寫操作的場景，像是貓咪影片的點擊次數  如果檔案太大的話，就開新的檔案，然後有時間再把舊的檔案壓縮    健值更新日誌 (統計貓咪影片的播放次數)  檔案很多的話，可以合併     同時執行壓縮和分段合併  為每個檔案儲存自己的 hash index  其它實現細節：   檔案格式  書中提到，在儲存數據時使用二進制格式會比使用文本格式更快，因為文本格式需要進行解析和序列化，而二進制格式可以直接使用。這是許多資料庫的優化技巧之一。  刪除記錄  當資料被刪除時，為了避免 hash index 中的資料不一致，可以先標記它們為被刪除的狀態，稱為墓碑（tombstone）。當 hash index 查找到被標記為墓碑的資料時，就知道這個資料已經被刪除，並且不會再被回傳。  崩潰恢復  如果資料庫重新啟動，因為記憶體中的雜湊對映表丟失，所以必須重新構建。這可以通過在啟動時重新讀取每個數據檔案並重建對映表來實現。Bitcask 會把 index cache 起來  部分寫入記錄  資料庫隨時可能崩潰，包括在將記錄追加到日誌的過程中。 Bitcask 檔案包含校驗和，允許檢測和忽略日誌中的這些損壞部分。  併發控制  由於寫操作是以嚴格的順序追加到日誌中的，所以常見的實現是隻有一個寫入執行緒。也因為資料檔案段是僅追加的或者說是不可變的，所以它們可以被多個執行緒同時讀取。  僅追加日誌似乎很浪費：為什麼不直接在檔案裡更新，原因如下   順序寫入比隨機讀取更快，因為它允許操作系統以更高效的方式編組寫入操作。  併發和崩潰恢復容易。  合併舊段資料可以防止碎片化問題  hash index 也有其侷限性：   memory 放不下 index 時，hash index 在 disk 上表現很糟  範圍查詢效率不高，無法輕鬆掃描 kitty00000 ~ kitty99999  SSTables and LSM tree  在之前的 hash index 結構，寫入檔案的 key 值順序，是照寫入時間先後的  SSTable(Sorted String Table) 則是要求寫入的順序照 key 的順序  用SSTable 的好處是   合併檔案可以更高效，同時讀取多個檔案合併  可以不用存下所有的 key offset 對應，因為 key 是排序的，只要先找到大概的位置，再掃描檔案即可  index 的空間節省，IO也節省     具有記憶體索引的 SSTable   構建和維護SSTables  如何讓資料能夠預先排好序呢？畢竟我們接收到的寫入請求是沒有順序的。  雖然在硬碟上維護有序結構也是可能的，但在記憶體內則容易多  方便排序的資料結構   紅黑樹  AVL 樹  我們可以讓我們的儲存引擎以如下方式工作：   有新寫入時，將其新增到記憶體中的平衡樹資料結構。  當   記憶體表  大於某個閾值（通常為幾兆位元組）時，將其作為 SSTable 檔案寫入硬碟。當該 SSTable 被寫入硬碟時，新的寫入可以在一個新的記憶體表例項上繼續進行。  收到讀取請求時，首先嘗試在記憶體表中找到對應的鍵，如果沒有就在最近的硬碟段中尋找，如果還沒有就在下一個較舊的段中繼續尋找，以此類推。  時不時地，在後臺執行一個合併和壓縮過程，以合併段檔案並將已覆蓋或已刪除的值丟棄掉。  只剩一個問題，資料庫崩潰，則最近的寫入（在記憶體表中，但尚未寫入硬碟）將丟失。  解法：硬碟上儲存一個單獨的日誌檔，在寫入記憶體錢先寫入日誌檔，用於崩潰後回復記憶體   用SSTables製作LSM樹  LSM-Tree（Log Structured-Merge Tree）是一種非常流行的樹型資料結構，用於實現許多現代資料庫系統。LSM-Tree 將數據分為多個層，每層使用不同的資料結構（如 SSTable）來進行儲存和檢索。當資料寫入時，它會先被寫入具有較快寫入速度的記憶體中，然後當記憶體中的資料達到一定閾值時，會將其寫入下一層。這樣可以避免過多的硬碟寫入，同時也可以提高查詢效率。      LSM Read 實作方式  在 LSM-Tree 中，當我們需要查詢資料時，會首先在記憶體中查找，如果找不到，就繼續在下一層的 SSTable 中查找，直到找到該筆資料或者所有的 SSTable 都被查詢過為止。此外，LSM-Tree 通常還會使用 Bloom Filters 來進一步加速查詢。  Bloom Filters 是一種資料結構，常用於判斷某個元素是否存在於一個集合中。它是由 Burton Bloom 於 1970 年提出的。  相較於傳統的B-Tree，LSM-Tree的優點在於：   寫入效率高，因為數據先被寫入記憶體中，而不是直接寫入磁碟中，當記憶體中的數據達到一定閾值時，再進行一次性的寫入操作。  可以進行批量寫入和刪除操作，因為數據被分為多個層，可以使用批量寫入和刪除操作來進行優化。  可以進行崩潰恢復，因為數據被分為多個層，可以在數據崩潰時進行恢復操作。  LSM-Tree 的缺點包括：   查詢效率不穩定，因為數據被分為多個層，所以查詢效率可能會隨著層數的增加而下降，尤其是在 SSTable 中有很多墓碑（tombstone）時。  LSM-Tree 的儲存格式比較複雜，需要將數據分為多個層，每層使用不同的資料結構進行儲存，這導致它比較難以實現。  在寫入時，需要先將數據寫入記憶體中，然後再進行一次性的寫入操作。這樣會導致寫入操作的延遲性較高。  總體而言，LSM-Tree 是一種非常有效的數據儲存和檢索方式，但它也有一些缺點，需要在實際使用中加以注意。  效能最佳化  當查詢不存在的鍵時，需要檢查記憶體表和從最近到最早的所有段檔案，那樣很慢。  解決方法：   使用布隆過濾器(Bloom Filters)來最佳化查詢不存在的鍵時的訪問  考慮使用 size-tiered 和 leveled compaction 來確定 SSTables 被壓縮和合並的順序和時間  LSM 樹的基本思想是將一系列在後臺合併的 SSTables 儲存起來，即使資料集比可用記憶體大得多，它仍能繼續正常工作。資料按排序順序儲存，可高效地執行範圍查詢，並且因為硬碟寫入是連續的，所以 LSM 樹可以支援非常高的寫入吞吐量。  Bloom Filter（布隆過濾器）是用來檢驗一個元素是否存在於特定集合中的概念。它在空間複雜度方面比 hash table 更優秀，但使用它時需要注意偽陽性(false positive)的問題  false positive：沒有就是沒有，有代表有可能有  B樹  日誌結構索引已經很常見，但它們不是最常用的索引。B 樹是一種最常見的索引結構，從 1970 年被引入以來，僅不到 10 年就變得“無處不在”，被廣泛使用在關係資料庫和非關係資料庫中。  像 SSTables 一樣，B 樹保持按鍵排序的鍵值對，這允許高效的鍵值查詢和範圍查詢。  B 樹將資料庫分解成固定大小的塊或分頁，通常大小為 4KB，並且一次只能讀取或寫入一個頁面。每個頁面都可以使用地址或位置來標識，這允許一個頁面引用另一個頁面，類似於指標，只是是在硬碟而不是在記憶體中。我們可以使用這些頁面引用來構建一個頁面樹，如圖 3-6 所示。     使用 B 樹索引查詢一個鍵   B樹由根頁面、子頁面和葉子頁面組成  子頁面負責連續鍵範圍，根頁面上的鍵表示相鄰子頁面管理的鍵的邊界  查找鍵值時，從根頁面開始，按範圍查找子頁面，直到葉子頁面  分支因子是每個頁面中對子頁面引用的數量，範例上是6個，通常是幾百     更新 B 樹中的鍵值，需搜尋包含該鍵的葉子頁面，更改值，再將該頁面寫回硬碟（引用仍有效）。  新增鍵需找到可包含它的頁面，如空間不足，則分成兩半滿頁面，並更新父頁面。  演算法確保樹平衡，n 個鍵的 B 樹深度為 O(log n)。多數資料庫可用三到四層樹，分支因子為 500 的 4KB 頁面的四層樹，可儲存 256TB 資料。  讓B樹更可靠   B 樹的基本寫入操作，  如何覆寫硬碟上的頁面以及如何處理拆分頁面等情況。  討論了預寫式日誌（WAL）檔案以及併發控制的問題。  日誌結構化的方法相對於 B 樹的優勢。  B 樹的基本寫入操作是用新資料覆寫硬碟上的頁面，而不會改變頁面的位置。這表示當頁面被覆寫時，對該頁面的所有引用不會改變。這與日誌結構索引（如 LSM 樹）形成鮮明對比，後者只是追加到檔案，從不修改檔案中已有的內容。  覆寫硬碟上的頁面就像對實際硬體進行操作。在磁性硬碟驅動器上，這意味著將磁頭移動到正確的位置，等待旋轉盤上的正確位置出現，然後用新的資料覆寫適當的扇區。固態硬碟上，由於 SSD 必須一次擦除和重寫相當大的儲存晶片塊，因此會發生更複雜的事情。  有時候，需要覆寫多個不同的頁面。例如，因為插入導致頁面過滿而拆分頁面，則需要寫入新拆分的兩個頁面，並覆寫其父頁面以更新對兩個子頁面的引用。這是一個危險的操作，因為如果資料庫在系列操作進行到一半時崩潰，那麼最終將導致一個損壞的索引。  B 樹實現中通常會有預寫式日誌（WAL，即 write-ahead log，也稱為重做日誌）檔案，以處理異常崩潰的情況。每次對 B 樹的修改都必須先寫入該檔案，然後才能應用到樹本身的頁面。當資料庫在崩潰後恢復時，這個日誌將被用來使 B 樹恢復到一致的狀態。  當多個執行緒要同時訪問 B 樹時，還需要仔細的併發控制，否則執行緒可能會看到樹處於不一致的狀態。通常是透過使用鎖存器（latches，輕量級鎖）保護樹的資料結構來完成。日誌結構化的方法在這方面更簡單，因為它們在後臺進行所有的合併，不干擾新接收到的查詢，並且能夠時不時地將段檔案切換為新的（該切換是原子操作）。  B樹的最佳化  B樹存在已久，多年來已開發出許多優化設計，包括：   使用寫時複製方案，例如LMDB，而非覆寫頁面並維護WAL以支援崩潰恢復。經修改的頁面被寫入不同位置，並在樹中建立父頁面的新版本以指向新位置。這種方法對於併發控制也很有用。  縮短鍵的大小，而非儲存整個鍵，以節省頁面空間。在樹內部的頁面上，鍵只需提供足夠的資訊來充當鍵範圍之間的邊界。在頁面中包含更多的鍵允許樹具有更高的分支因子，因此也就允許更少的層級。  不要求相鄰鍵範圍的頁面也放在硬碟上相鄰的區域，但這樣隨機存取的速度很慢。因此，許多B樹的實現在佈局樹時會盡量使葉子頁面按順序出現在硬碟上，但隨著樹的增長，要維持這個順序是很困難的。  新增指標到樹中，例如每個葉子頁面引用其左右兄弟頁面，使得不用跳回父頁面即可按順序掃描鍵。  B樹的變體如分形樹借用了一些日誌結構的思想來減少硬碟查詢。  比較B樹和LSM樹   通常，LSM樹的寫入速度較快，B樹的讀取速度較快。  LSM樹具有較高的寫入吞吐量，較低的寫放大和碎片，對固態硬碟較為友好。  LSM樹可能在壓縮過程中干擾讀寫操作，且在高寫入吞吐量下壓縮性能受限  B樹為許多工作負載提供了始終如一的良好效能，並適用於事務隔離等應用。  B樹和LSM樹的比較中，B樹實現通常較為成熟，但LSM樹在效能特徵方面也很有吸引力。經驗表明，LSM樹的寫入速度通常更快，而B樹的讀取速度更快。LSM樹上的讀取相對較慢，因為需要檢查多個資料結構和SSTables的不同壓縮層級。  然而，基準測試結果通常取決於工作負載細節，所以需要針對特定工作負載進行測試以進行有效比較。在評估儲存引擎效能時，需要考慮幾個因素。  LSM樹具有更高寫入吞吐量、較低的寫入放大和碎片化，並且可以將資料壓縮得更好，因此在硬碟上產生的檔案較小。不過，壓縮過程可能干擾到正在進行的讀寫操作，導致性能波動和較高的響應時間。在高寫入吞吐量下，壓縮性能可能受限，導致硬碟上未合併段的數量不斷增加，從而影響讀取速度。  B樹的優勢在於每個鍵在索引中只存在一個位置，並且通常具有較快的讀取速度。此外，B樹在需要強大事務語義的資料庫中非常適用，因為可以直接在B樹索引中的鍵範圍上應用鎖。然而，B樹可能具有更高的寫入放大和碎片化，導致磁碟空間使用增加和寫入吞吐量降低。  B樹在資料庫架構中具有深厚基礎，而LSM樹在新型資料庫中越來越受歡迎。要確定哪種儲存引擎最適合特定使用場景，需要根據實際工作負載進行測試。  其他索引結構  名詞解釋   主鍵索引：在關聯式資料庫、文件資料庫或圖形資料庫中，主鍵唯一識別一行、一個文檔或一個頂點。其他紀錄可以通過主鍵（或ID）引用該行/文檔/頂點，索引用於解析這些引用。  次級索引：允許在資料庫表格上建立額外的索引，以提高查詢性能。次要索引可以從鍵值索引構建，主要區別在於鍵不是唯一的。這可以通過使索引中的每個值成為匹配行標識符的列表或通過將行標識符附加到鍵上使其唯一來解決。  heap file：存儲行數據的無特定順序的文件。堆文件避免了多個次要索引存在時的數據重複問題：每個索引僅引用堆文件中的位置，實際數據存儲在一個地方。  Clustered 索引：將索引行直接存儲在索引中的一種方法，可以提高查詢性能。例如，MySQL的InnoDB存儲引擎中，表的主鍵始終是聚簇索引，次要索引引用主鍵（而不是堆文件位置）。  包含列的索引：Clustered and non clustered index 的過渡，只保存部份需要用到的資料  多列索引：同時查詢多個表列或文檔字段的索引。這對於需要根據多個條件查找數據的情況非常有用。  串接索引：將多個字段組合成一個鍵的多列索引。串接索引的鍵是按照指定的順序將字段附加在一起的。它可以用來查找具有特定條件的所有數據，但如果只查詢其中一個字段，它可能無法提供幫助。  多維索引：用於查詢多個列的一般方法，特別適用於地理空間數據。  R樹：專門用於地理空間數據的空間索引。  全文搜索：用於模糊查詢的技術，支持對詞彙、語法變化和近義詞等進行搜索。  目前，我們只討論了鍵值索引，它們就像關係模型中的   主鍵索引 。  次級索引（secondary indexes）也很常見。在關係資料庫中，你可以使用   CREATE INDEX  命令在同一個表上建立多個次級索引。  儲存Index 中的值  索引的鍵是查詢欄位，而值可以是行本身或是儲存在別處行的參照。後者儲存在 heap file 中  heap file 資料沒有特定順序，這種方法常見，因為可以避免在有多個次級 index 時複製資料。  Clustered Index 將整個資料列的所有資料存入索引中，查詢可直接在索引樹上完成，速度較快，但只能建立在唯一的鍵上。相反地，非聚集式 index 將索引資料存入索引樹中，而將資料列放置在另一個文件中，因此查詢需要先在索引樹上完成，再到文件中查詢資料列。  cover index 介於 clustered index and non clustered index 之間，包含查詢所需的所有欄位，可不必查詢表格本身即可回答查詢，因此可大幅提高查詢性能。  多列 index  又叫複合 index，對查詢有多種條件時，能加速，要注意 key 的順序是重要的，和下 where 欄位的順序一致的話，查詢是最快的。  多維 index  用於加速多維資料的查詢，例如帶有時間和地點兩個維度的天氣資料。它通常使用 R 樹資料結構來實現，具有高效的範圍查詢和空間查詢功能。在設計多維 index 時，必須仔細選擇索引的維度，以便在查詢時獲得最佳效能。  B 樹或 LSM 樹索引無法高效處理這種查詢：它可以返回一個經度範圍內的所有餐廳（但緯度可能是任意值），或者返回在同一緯度範圍內的所有餐廳（但經度可能是任意值），但不能同時滿足兩個條件。  全文搜尋和模糊索引  到目前為止所討論的所有索引都假定你有確切的資料，並允許你查詢鍵的確切值或具有排序順序的鍵的值範圍。他們不允許你做的是搜尋  類似 的鍵，如拼寫錯誤的單詞。這種模糊的查詢需要不同的技術，如全文搜尋引擎等。全文搜尋引擎允許用戶搜索相似的鍵，而不僅僅是確切的鍵值。  模糊索引（fuzzy indexes）類似於全文搜尋，但允許在查詢過程中包含近似匹配，以處理拼寫錯誤、拼音相似性等問題。為實現模糊索引，可以使用像Levenshtein距離、Damerau-Levenshtein距離和Jaro-Winkler距離等編輯距離度量，或使用如三元組或四元組的n-gram方法來度量相似性。   在記憶體中儲存一切  記憶體資料庫在效能、資料模型和擴展性方面具有優勢：   效能：記憶體資料庫通過直接從記憶體讀取和寫入資料，可以提供更高的讀寫速度，從而提高整體效能。這比傳統的基於硬碟的資料庫更快，因為後者需要在硬碟上查找和操作資料，這會消耗更多的時間。  資料模型：記憶體資料庫可以較容易地支援多種資料結構（如優先順序佇列和集合），從而提供更好的應用支援。這使得開發人員可以更靈活地設計和實現資料模型，以滿足不同的應用需求。  擴展性：記憶體資料庫可以通過反快取方法在記憶體不足的情況下將最近最少使用的資料從記憶體轉移到硬碟，並在需要時重新將其載入到記憶體中。這使得記憶體資料庫能夠支援比可用記憶體更大的資料集，同時保持高效能。  隨著 RAM 成本的降低，將資料集完全儲存在記憶體中變得更加可行。此外，非易失性儲存器（NVM）技術的發展有望為未來的資料存儲帶來重大變革，因為它們結合了記憶體和硬碟的特點，並可以在不同的應用場景中提供更好的性能。  總之，記憶體資料庫在資料庫領域具有顯著優勢，特別是在處理較小資料集時。隨著 RAM 成本降低和非易失性儲存器技術的發展，記憶體資料庫將在資料庫領域發揮越來越重要的作用。   事務處理還是分析？  早期資料庫寫入對應商業交易。後來資料庫應用於非金錢相關領域，仍使用\"事務\"一詞。事務處理允許客戶端低延遲讀寫，稱為線上事務處理（OLTP）。  資料庫越來越多地用於資料分析，這些分析查詢被稱為線上分析處理（OLAP）。OLTP主要是查詢少量記錄，終端使用者操作。OLAP則主要在大批次記錄上聚合，供內部資料分析師使用。  起初，事務處理和分析查詢使用相同資料庫，但企業趨勢是在單獨的資料倉庫上執行分析。   表 3-1 比較事務處理和分析系統的特點     屬性  事務處理系統 OLTP  分析系統 OLAP    主要讀取模式  查詢少量記錄，按鍵讀取  在大批次記錄上聚合   主要寫入模式  隨機訪問，寫入要求低延時  批次匯入（ETL）或者事件流   主要使用者  終端使用者，透過 Web 應用  內部資料分析師，用於決策支援   處理的資料  資料的最新狀態（當前時間點）  隨時間推移的歷史事件   資料集尺寸  GB ~ TB  TB ~ PB  資料倉庫  資料倉庫是一個獨立的資料庫，用於儲存企業內所有OLTP（線上事務處理）系統中的只讀資料副本。企業可能擁有多個交易處理系統，如終端客戶網站、實體商店收銀系統、倉庫庫存追踪等。這些OLTP系統對業務運作至關重要，要求高可用和低延遲，所以一般不允許業務分析人員直接在這些資料庫上執行分析查詢。  資料倉庫允許分析人員查詢資料，而不影響OLTP操作。資料倉庫從OLTP資料庫中提取資料，轉換為適合分析的格式，然後載入資料倉庫。這個過程稱為抽取-轉換-載入（ETL）。     圖 3-8 ETL 至資料倉庫的簡化提綱  資料倉庫可針對分析類的訪問模式進行最佳化。雖然資料倉庫和關係型OLTP資料庫都有SQL查詢介面，但它們針對非常不同的查詢模式進行最佳化。許多資料庫供應商專注於支援事務處理負載或分析工作負載，而不是兩者兼顧。  一些資料庫（如Microsoft SQL Server和SAP HANA）在同一產品中支援事務處理和資料倉庫。還有一些資料倉庫供應商，如Teradata、Vertica等，它們的產品通常使用昂貴的商業許可證銷售。開源SQL-on-Hadoop專案正與商業資料倉庫系統競爭，例如Apache Hive、Spark SQL、Cloudera Impala等。   星型和雪花型：分析的模式  星型和雪花型模式是分析型資料庫中常見的資料模型。在星型模式中，資料庫中心是一個事實表，它記錄了特定事件（例如客戶購買的產品）。事實表的每一行都包含一個事件，並通過外部鍵引用其他表（稱為維度表）。這些維度表包含了事件發生的物件、內容、地點、時間、方式和原因等資訊。星型模式之所以被稱為星型，是因為當我們將表之間的關係視覺化時，事實表位於中心，被維度表包圍，連線就像星星的光芒。     圖 3-9 用於資料倉庫的星型模式的示例  雪花模式是星型模式的一個變體，其中維度被進一步分解成子維度。這意味著維度表可以再引用其他表，將資料進一步細分。雖然雪花模式比星型模式更規範化，但分析師通常更喜歡使用星型模式，因為它更簡單。   列式儲存  列式存儲是一種在資料庫中儲存資料的方式，它將表中的每一列資料存儲在一起，而不是將同一行的所有值相鄰存儲。這與常見的行式存儲方式不同，行式存儲方式會將表中同一行的所有值相鄰存儲。  在資料倉庫和大規模分析查詢中，列式存儲具有明顯優勢。當查詢只涉及表中少數幾列時，列式存儲可以有效地減少IO操作，提高查詢性能。這是因為查詢引擎只需要讀取和解析查詢中涉及的列，而不是整行數據。這在大型事實表（可能包含數百列）的資料倉庫查詢中特別有用。     圖 3-10 按列儲存關係型資料，而不是行   列壓縮  透過壓縮資料，降低對硬碟吞吐量的需求。列式儲存通常適合壓縮，因為在列式儲存中，每一列的值序列通常具有重複性，這有利於壓縮。根據列中的資料，可以使用不同的壓縮技術。  點陣圖編碼：在資料倉庫中特別有效的壓縮技術。點陣圖編碼將具有 n 個不同值的列轉換為 n 個獨立的點陣圖，每個不同值對應一個位圖，每行對應一個位元位。如果該行具有該值，則該位為 1，否則為 0。     圖 3-11 壓縮的點陣圖索引儲存佈局  對於稀疏的點陣圖（其中包含許多零），可以使用遊程編碼（一種無損資料壓縮技術）進一步壓縮。這可以使列的編碼非常緊湊。  記憶體頻寬和向量化處理  資料倉庫查詢中，兩個主要性能瓶頸為   硬碟到記憶體頻寬  記憶體到CPU的頻寬  解決方法：   列式儲存佈局可以降低從硬碟載入的資料量，從而提高效率。  查詢引擎可以將壓縮後的列資料放入CPU的L1快取，然後在緊密的迴圈中遍歷。這樣的迴圈相對於包含大量函式呼叫和條件判斷的程式碼執行速度更快。  列壓縮允許更多的行同時放入有限的L1快取。  向量化處理是一種可以直接在壓縮列資料塊上操作的技術。例如，按位 \"與\" 和 \"或\" 運算子可以被設計成在壓縮列資料塊上執行。   列式儲存中的排序順序  列式儲存中資料排序順序的概念和其對查詢效率的影響。   在列式儲存中，資料的行順序不是非常重要。可以按插入順序儲存，但也可以根據某種排序順序排列資料以作為索引機制。  對每一列分別進行排序是沒有意義的，因為這樣就無法重建出完整的行。所以需要對整行資料進行統一操作，即使實際儲存方式是按列的。  資料庫管理員可以根據常用查詢的需求來選擇用於排序的列。例如，如果查詢通常涉及日期範圍，則可以使用日期作為第一個排序鍵，這樣查詢引擎只需掃描特定日期範圍的行，而不需要掃描所有行。  對資料進行排序還有助於壓縮列。如果主要排序列的值重複程度較高，則排序後可以用簡單的運行長度編碼來進行壓縮。  C-Store 和 Vertica 等資料庫系統中的一個擴充套件是，既然不同查詢受益於不同的排序順序，為什麼不以多種不同方式儲存相同資料？這樣可以在處理查詢時，選擇最適合查詢模式的排序版本。  總之，列式儲存中的排序順序對於查詢效率和資料壓縮具有重要影響。不同的排序順序可以優化查詢性能並提高資料壓縮率。   寫入列式儲存  在資料倉庫中，列式儲存、壓縮和排序對於大型只讀查詢有幫助。但是，寫入列式儲存比較困難。由於行由列中的位置標識，插入新行可能需要重寫所有列檔案。  一個解決方案是使用LSM樹。寫操作先進入記憶體中的儲存，被加入已排序結構，然後批次寫入硬碟上的列檔案。查詢操作需要檢查硬碟上的列資料和記憶體中的最近寫入，將兩者合併。查詢最佳化器會隱藏這些細節，使得修改後的資料能立即反映在查詢中。   聚合：資料立方體和物化檢視  為了提高資料倉庫的查詢性能，可以利用資料立方體和物化檢視進行數據聚合。這兩種技術具有不同的特點和用途：   物化檢視：\n   將查詢結果的實際副本儲存起來，加速常用聚合函式，如 COUNT、SUM、AVG 等。  與虛擬檢視不同，虛擬檢視只是查詢的捷徑，不儲存任何實際數據。  需要在底層數據變化時進行更新，這會增加寫入成本，在 OLTP 資料庫中不太常用。  資料立方體：\n   物化檢視的一個特例，按照不同維度分組的聚合網格，也被稱為 OLAP 立方。  允許根據不同維度對數據進行彙總，例如根據日期和產品對銷售額進行彙總。  當事實具有多個維度時，可以擴展到多維超立方體，例如日期、產品、商店、促銷和客戶等五個維度。  優點：讓某些查詢變得非常快，因為這些查詢已經被有效地預先計算了。  缺點：缺乏查詢原始數據的靈活性，因為價格等某些範圍條件不是立方體的維度。  資料倉庫通常會保留儘可能多的原始數據，並將聚合數據（如資料立方體）僅作為某些查詢的效能提升手段。     本章小結  在本章中，我們詳細討論了資料庫如何處理資料的儲存和檢索。資料儲存在資料庫後，查詢時資料庫會進行檢索。我們了解了針對事務處理（OLTP）和線上分析（OLAP）兩種不同使用場景的儲存引擎，以及他們之間的區別。   OLTP系統：\na. 面向終端使用者，處理大量請求。\nb. 每個查詢通常只訪問少量記錄，使用索引進行查詢。\nc. 硬碟查詢時間是瓶頸。  OLAP系統：\na. 面向業務分析人員，查詢量較少。\nb. 每個查詢開銷高，需要短時間內掃描大量記錄。\nc. 硬碟頻寬是瓶頸，列式儲存有助於提高效能。  兩派主流的儲存引擎：\na. 日誌結構學派：例如Bitcask、SSTables、LSM樹、LevelDB、Cassandra、HBase、Lucene等。這些技術通過將隨機訪問寫入轉換為硬碟上的順序寫入，提高寫入吞吐量。\nb. 就地更新學派：例如B樹，用於關係資料庫和非關係型資料庫。將硬碟視為一組可覆寫的固定大小的頁面。  複雜的索引結構與記憶體內資料庫：介紹了一些更複雜的索引結構，以及針對所有資料都放在記憶體裡而最佳化的資料庫。  資料倉庫的高階架構：\na. 當查詢需要在大量行中順序掃描時，索引的重要性降低。\nb. 非常緊湊地編碼資料變得非常重要，以減少查詢需要從硬碟讀取的資料量。\nc. 列式儲存有助於實現這一目標，提高查詢效能。  作為一名應用程式開發人員，了解儲存引擎內部的知識將有助於您更好地了解哪種工具最適合特定應用程式。在調整資料庫最佳化引數時，這種理解讓您能夠預測增減某個值所產生的效果。  了解資料庫儲存引擎的內部運作對於選擇最適合您需求的資料庫系統至關重要。以下是一些建議幫助您選擇資料庫：   評估您的工作負載類型：確定您的應用程式是否更偏向OLTP或OLAP，以選擇針對特定需求最佳化的資料庫。  考慮資料庫的可擴展性：根據您的應用程式的未來需求和成長，確保資料庫可以滿足這些需求。  評估資料庫的性能：根據您的應用程式需求，選擇性能最佳的資料庫，特別是在讀取和寫入操作方面。  瞭解資料庫的安全性和完整性：選擇具有內建安全性和完整性機制的資料庫，以確保您的數據安全無虞。  考慮資料庫的支持和社區：選擇具有良好支持和活躍開發者社區的資料庫，這將有助於您解決問題和獲取技術支援。  儘管本章無法使您成為特定儲存引擎的調參專家，但至少能讓您獲得足夠的概念和詞彙，以便您能夠閱讀和理解所選資料庫的文檔。掌握資料庫儲存引擎的基本知識對於您作為應用程式開發人員來說是非常有價值的。  html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"id":288,"path":289,"dir":266,"title":290,"description":7,"keywords":291,"body":295},"content:5.ddia:4.chapter4.md","/ddia/chapter4","04 編碼與演化",[292,293,294],"編碼資料的格式bo","資料流的型別","小結","  \n  tr, th, td {\n    border: none!important;\n  }\n  .nolinebreak {\n    white-space: nowrap;\n  }\n  .movepadding {\n    padding-left: 2%!important;\n  }\n  當資料的  格式 (format)或  模式 (schema)發生變化時，通常需要對應用程式程式碼進行相應的更改(例如，為記錄新增新欄位，然後修改程式開始讀寫該欄位)。但在大型應用程式中，程式碼變更通常不會立即完成：  \n     \n       服務端 (server-side)： \n      可能需要執行   滾動升級 (rolling upgrade)，一次將新版本部署到少數幾個節點，檢查新版本是否執行正常，然後逐漸部完所有的節點。 \n   \n    \n       客戶端 (client-side)： \n      升不升級就要看使用者的心情了。使用者可能相當長一段時間裡都不會去升級軟體。 \n    這也表示，新舊版本的程式碼或是新舊資料格式可能會在系統中同時共處。系統想要繼續順利執行，就需要保持  雙向相容性 ：  \n     \n       向後相容 (backward compatibility)： \n      新的程式碼可以讀取由舊的程式碼寫入的資料。較容易處理，因為作者知道舊程式碼所使用的資料格式(一般最簡單的方法，就是保留舊程式碼即可讀取舊資料)。 \n   \n    \n       向前相容 (forward compatibility)： \n      舊的程式碼可以讀取由新的程式碼寫入的資料。較不易處理，因為舊版的程式需要忽略新版資料格式中新增的部分。 \n      編碼資料的格式bo  程式通常(至少)使用兩種形式的資料：   資料以物件、結構體、列表、陣列、散列表、樹等方式儲存到  記憶體 中。這些資料結構針對 CPU 的高效訪問和操作進行了最佳化(通常使用指標)。  資料寫入檔案，或透過網路傳送，則必須將其轉換為某種自包含的位元組序列(例如，JSON 文件)。這個位元組序列表示會與通常在記憶體中使用的資料結構不同。  要在兩種表示之間進行某種型別的翻譯。從記憶體中表示到位元組序列的轉換稱為  編碼 (Encoding)(也稱為  序列化 (serialization)或  編組 (marshalling))，反過來稱為  解碼 (Decoding)2(  解析 (Parsing)，  反序列化 (deserialization)，  反編組  (unmarshalling))    語言特定的格式  許多程式語言都內建了將記憶體物件編碼為位元組序列的支援。   Java 有 java.io.Serializable。  Ruby 有 Marshal。  Python 有 pickle。  第三方類別庫像是 Kryo for Java ....等。  編碼庫很方便，可以用很少的程式碼實現記憶體物件的儲存與恢復。但也有一些深層次的問題：   與特定的程式語言深度繫結，其他語言很難讀取這種資料，且很難將系統與其他組織的系統(可能用的是不同的語言)進行整合。  為了恢復相同物件型別的資料，解碼過程需要 例項化任意類。這會有安全性問題，若攻擊者可以讓應用程式解碼任意的位元組序列，他們就能例項化任意的類。  容易出現向前向後相容性帶來的麻煩問題。  效率(編碼或解碼所花費的 CPU 時間，以及編碼結構的大小)。例如，Java 的內建序列化由於其糟糕的效能和臃腫的編碼而臭名昭著。    JSON、XML和CSV  JSON，XML 和 CSV 屬於文字格式，因此具有人類可讀性。但也存在一些微妙的問題：   數字(numbers)編碼有很多模糊之處。在 XML 和 CSV 中，無法區分數字和純數字組成的字串(除了引用外部模式)。JSON 雖然區分字串與數字，但並不區分整數和浮點數，並且不能指定精度。  處理大數字時是個問題。例如大於        2  53  2^{53}      2          53 的整數無法使用 IEEE 754 雙精度浮點數精確表示因此在使用浮點數(例如 JavaScript)的語言進行分析時，這些數字會變得不準確。  JSON 和 XML 不支援二進位制資料(即不帶 字元編碼(character encoding)的位元組序列)。  XML 和 JSON 都有可選的模式支援。這些模式語言相當強大，相對學習和實現起來都比較複雜。  CSV 沒有任何模式，因此每行和每列的含義完全由應用程式自行定義。如果應用程式變更添加了新的行或列，那麼這種變更必須透過手工處理。    二進位制編碼  小資料集合的編碼造成的效能影響可以忽略不計；但一旦達到 TB 級別，資料格式的選型就會產生巨大的影響。二進位制串是很有用的功能，人們透過使用 Base64 將二進位制資料編碼為文字來繞過此限制。其特有的模式標識著這個值應當被解釋為 Base64 編碼的二進位制資料。   {\n    \"userName\": \"Martin\",\n    \"favoriteNumber\": 1337,\n    \"interests\": [\"daydreaming\", \"hacking\"]\n}\n     MessagePack  下圖為 MessagePack 編碼後的格式定義：\n    第 1 個位元組為 83(1000 0011)，1000 表示資料型別為 fixmap，0011 表示有 3 個項目。  第 2 ~ 10 個位元組表示欄位 \"userName\"。\n   第 2 個位元組為 a8(1010 1000)，101 表示資料型別為 fixstr，01000 表示長度為 8。  第 3 ~ 10 個位元組表示 userName 的 ASCII 編碼。  第 11 ~ 17 個位元組表示值 \"Martin\"。  第 18 ~ 32 個位元組表示欄位 \"favoriteNumber\"。  第 33 ~ 35 個位元組表示值 1337。\n   第 33 個位元組為 cd(1100 1101)，表示資料型別為 uint16。  第 34 ~ 35 表示 1337(05 39) 的十六進制編碼。  第 36 ~ 45 個位元組表示欄位 \"interests\"。  第 46 ~ 66 個位元組表示為一個陣列集合的資料。\n   第 46 個位元組為 92(1001 0010)，1001 表示資料型別為 fixarray，0010 表示有 2 個項目。  第 47 ~ 58 個位元組表示值 \"daydreaming\"。  第 59 ~ 66 個位元組表示值 \"hacking\"。  詳細教學可參考  MessagePack官網 。     Thrift  下圖為 Thrift BinaryProtocol 編碼後的格式定義：\n    第 1 ~ 13 個位元組表示值 \"Martin\"。\n   第 1 個位元組為 0b 表示為資料型別為 string。  第 2 ~ 3 個位元組為 00 01 表示為第 1 個項目。  第 4 ~ 7 個位元組為 00 00 00 06 表示為長度為 6。  第 8 ~ 13 個位元組表示 Martin 的 ASCII 編碼。  第 14 ~ 24 個位元組表示值 1337。\n   第 14 個位元組為 0a 表示為資料型別為 int64。  第 15 ~ 16 個位元組為 00 02 表示為第 2 個項目。  第 17 ~ 24 個位元組表示 1337 的十六進制編碼。  第 25 ~ 59 個位元組表示 1 個陣列集合。\n   第 25 個位元組為 0f 表示為資料型別為 list。  第 26 ~ 27 個位元組為 00 03 表示為第 3 個項目。  第 28 個位元組為 0b 表示為集合內的資料型別為 string。  第 29 ~ 32 個位元組為 00 00 00 02 表示集合含有 2 個項目。  第 33 ~ 36 個位元組為 00 00 00 0b 表示為長度為 11。  第 37 ~ 47 個位元組為 daydreaming 的 ASCII 編碼。  第 48 ~ 51 個位元組為 00 00 00 07 表示為長度為 7。  第 52 ~ 58 個位元組為 hacking 的 ASCII 編碼。  第 59 個位元組為此集合的結束標記。  詳細教學可參考  Thrift Binary protocol encoding 。  下圖為 Thrift CompactProtocol 編碼後的格式定義：\n    第 1 ~ 8 個位元組表示值 \"Martin\"。\n   第 1 個位元組為 18(0001 1000)。0001 表示 1 個項目，1000 表示為資料型別為 string。  第 2 個位元組為 06 表示為長度為 6。  第 3 ~ 8 個位元組表示 Martin 的 ASCII 編碼。  第 9 ~ 11 個位元組表示值 1337。\n   第 9 個位元組為 16(0001 0110)。0001 表示 1 個項目，0110 表示為資料型別為 int64。  第 10 ~ 11 個位元組表示 1337 的十六進制編碼(先作 ZigZag，再作 varint)。  第 12 ~ 34 個位元組表示值一個陣列集合。\n   第 12 個位元組為 19(0001 1001)。0001 表示 1 個項目，1001 表示為資料型別為 list。  第 13 個位元組為 28(0010 1000)。0010 表示 2 個項目，1000 表示為資料型別為 string。  第 14 ~ 25 個位元組表示 daydreaming 的長度 及 ASCII 編碼。  第 26 ~ 33 個位元組表示 hacking 的長度 及 ASCII 編碼。  第 34 個位元組為此集合的結束標記。  詳細教學可參考  Thrift Compact protocol encoding 。     Protocol Buffers  下圖為 Protocol Buffers 編碼後的格式定義：\n      Avro  下圖為 Thrift CompactProtocol 編碼後的格式定義：\n      Writer模式與Reader模式  \n     \n       Writer 模式 ： \n      應用程式想要編碼一些資料(將其寫入檔案或資料庫，透過網路傳送等)時，它使用它知道的任何版本的模式編碼資料。 \n   \n    \n       Reader 模式 ： \n      應用程式想要解碼一些資料(從一個檔案或資料庫讀取資料，從網路接收資料等)時，它希望資料在某個模式中。 \n    Avro 的關鍵思想是 Writer 模式和 Reader 模式不必是相同的 - 他們只需要相容。當資料解碼(讀取)時，Avro 庫透過並排檢視 Writer 模式和 Reader 模式並將資料從 Writer 模式轉換到 Reader 模式來解決差異。  Writer 模式和 Reader 模式的欄位順序不同，這是沒有問題的，因為模式解析透過欄位名匹配欄位。如果讀取資料的程式碼遇到出現在 Writer 模式中但不在 Reader 模式中的欄位，則忽略它。如果讀取資料的程式碼需要某個欄位，但是 Writer 模式不包含該名稱的欄位，則使用在 Reader 模式中宣告的預設值填充。       Writer模式到底是什麼  對於一段特定的編碼資料，Reader 如何知道其 Writer 模式？這取決於 Avro 使用的上下文。例如：   有很多記錄的大檔案。  支援獨立寫入的記錄的資料庫。  透過網路連線傳送記錄。    模式的優點   可以比各種 “二進位制 JSON” 變體更緊湊，因為它們可以省略編碼資料中的欄位名稱。  模式是一種有價值的文件形式，因為模式是解碼所必需的，所以可以確定它是最新的。  維護一個模式的資料庫允許你在部署任何內容之前檢查模式更改的向前和向後相容性。  對於靜態型別程式語言的使用者來說，從模式生成程式碼的能力是有用的，因為它可以在編譯時進行型別檢查。    資料流的型別  想要將某些資料傳送到不共享記憶體的另一個程序，就需要將它編碼為一個位元組序列。而資料可以透過多種方式從一個流程流向另一個流程，下列是一些常見的方式：   透過資料庫(詳細參閱 資料庫中的資料流)。  透過服務呼叫(詳細參閱 服務中的資料流：REST與RPC)。  透過非同步訊息傳遞(詳細參閱 訊息傳遞中的資料流)。    資料庫中的資料流  在資料庫中，寫入資料庫的過程對資料進行編碼，從資料庫讀取的過程對資料進行解碼。假若今天有一個程序訪問資料庫，在這種情況下，當程序更新為後續版本，就必須考慮將資料庫中的內容儲存為能向未來的自身程序傳送訊息。因此向後相容性顯然是必要的，否則你未來的程序將無法解碼你以前程序的資料。    當較舊版本的應用程式更新以前由較新版本的應用程式編寫的資料時，如果不小心，資料可能會丟失。    在不同的時間寫入不同的值  資料庫通常允許任何時候更新任何值。這意味著在一個單一的資料庫中，可能有一些值是五毫秒前寫的，而一些值是五年前寫的。  將資料重寫(遷移)到一個新的模式當然是可能的，但是在一個大資料集上執行是一個昂貴的事情，所以大多數資料庫如果可能的話就避免它。大多數關係資料庫都允許簡單的模式更改，例如新增一個預設值為空的新列，而不重寫現有資料。讀取舊行時，對於磁碟上的編碼資料缺少的任何列，資料庫將填充空值。  因此，模式演變允許整個資料庫看起來好像是用單個模式編碼的，即使底層儲存可能包含用各種歷史版本的模式編碼的記錄。    歸檔儲存  也許你不時為資料庫建立一個快照，例如備份或載入到資料倉庫。在這種情況下，即使源資料庫中的原始編碼包含來自不同時代的模式版本的混合，資料轉儲通常也將使用最新模式進行編碼。既然你不管怎樣都要複製資料，那麼你可以對這個資料複製進行一致的編碼。  由於資料轉儲是一次寫入的，而且以後是不可變的，所以 Avro 物件容器檔案等格式非常適合。這也是一個很好的機會，可以將資料編碼為面向分析的列式格式，例如列壓縮(Parquet)。    服務中的資料流：REST與RPC  透過網路進行通訊的方式最常見的安排兩個角色：  \n     \n       伺服器 ： \n      透過網路公開 API，也就是所謂的服務。 \n   \n    \n       客戶端 ： \n      可以連線到伺服器以向該 API 發出請求。 \n    Web 以這種方式工作：客戶(Web 瀏覽器)向 Web 伺服器發出請求，透過 GET 請求下載 HTML、CSS、JavaScript、影象等，並透過 POST 請求提交資料到伺服器。  API 則包含一組標準的協議和資料格式(HTTP、URL、SSL/TLS、HTML 等)。由於網路瀏覽器、網路伺服器和網站作者大多同意這些標準，因此可以使用任何網路瀏覽器訪問任何網站。  也由於 Web 瀏覽器不是唯一的客戶端型別，所以伺服器的響應通常不是用於顯示給人的 HTML，而是便於客戶端應用程式進一步處理的編碼資料(例如 JSON)。雖然 HTTP 可能被用作傳輸協議，但頂層實現的 API 是特定於應用程式的，客戶端和伺服器需要就該 API 的細節達成一致。  伺服器本身可以是另一個服務的客戶端(例如，典型的 Web 應用伺服器充當資料庫的客戶端)。這種方法通常用於將大型應用程式按照功能區域分解為較小的服務，這樣當一個服務需要來自另一個服務的某些功能或資料時，就會向另一個服務發出請求。這種構建應用程式的方式傳統上被稱為  面向服務的體系結構 (service-oriented architecture，SOA)，最近被改進和更名為  微服務架構 。  這類服務類似於資料庫，它們通常允許客戶端提交和查詢資料。並公開了一個特定於應用程式的 API，只允許由服務的業務邏輯(應用程式程式碼)預定的輸入和輸出。這種限制提供了一定程度的封裝，限制了對客戶可以做什麼和不可以做什麼。面向服務 / 微服務架構的設計目標是使服務獨立部署來讓應用程式更易於更改和維護。每個服務應該由一個開發團隊擁有，而不必與其他團隊協調，因此伺服器和客戶端使用的資料編碼必須在不同版本的服務 API 之間相容。    Web服務  服務使用 HTTP 作為底層通訊協議時，可稱之為 Web 服務。它不僅在 Web 上使用，而且在幾個不同的環境中使用：   執行在使用者裝置上的客戶端應用程式，透過 HTTP 向服務發出請求。  一個服務向同一組織擁有的另一個服務提出請求，這些服務通常位於同一資料中心內，作為面向服務 / 微服務架構的一部分。(支援這種用例的軟體有時被稱為  中介軟體 (middleware))  一個服務透過網際網路向不同組織所擁有的服務提出請求。這用於不同組織後端系統之間的資料交換。此類別包括由線上服務(如信用卡處理系統)提供的公共 API，或用於共享訪問使用者資料的 OAuth。  兩種流行的 Web 服務方法：  \n     \n       REST ： \n      是一個基於 HTTP 原則的設計。強調簡單的資料格式，使用 URL 來標識資源，並使用 HTTP 功能進行快取控制，身份驗證和內容型別協商。 \n   \n    \n       SOAP ： \n      用於製作網路 API 請求，使用稱為 Web 服務描述語言(WSDL，詳細教學可參考  WSDL教學 )的基於 XML 的協議。WSDL 支援程式碼生成，客戶端可以使用本地類和方法呼叫(編碼為 XML 訊息並由框架再次解碼)訪問遠端服務。雖然它最常用於 HTTP，但其目的是獨立於 HTTP，並避免使用大多數 HTTP 功能。 \n    WSDL文件範例如下：   \u003Cdefinitions name = \"HelloService\"\n   targetNamespace = \"http://www.examples.com/wsdl/HelloService.wsdl\"\n   xmlns = \"http://schemas.xmlsoap.org/wsdl/\"\n   xmlns:soap = \"http://schemas.xmlsoap.org/wsdl/soap/\"\n   xmlns:tns = \"http://www.examples.com/wsdl/HelloService.wsdl\"\n   xmlns:xsd = \"http://www.w3.org/2001/XMLSchema\">\n\n   \u003Cmessage name = \"SayHelloRequest\">\n      \u003Cpart name = \"firstName\" type = \"xsd:string\"/>\n   \u003C/message>\n\n   \u003Cmessage name = \"SayHelloResponse\">\n      \u003Cpart name = \"greeting\" type = \"xsd:string\"/>\n   \u003C/message>\n\n   \u003CportType name = \"Hello_PortType\">\n      \u003Coperation name = \"sayHello\">\n         \u003Cinput message = \"tns:SayHelloRequest\"/>\n         \u003Coutput message = \"tns:SayHelloResponse\"/>\n      \u003C/operation>\n   \u003C/portType>\n\n   \u003Cbinding name = \"Hello_Binding\" type = \"tns:Hello_PortType\">\n      \u003Csoap:binding style = \"rpc\"\n         transport = \"http://schemas.xmlsoap.org/soap/http\"/>\n      \u003Coperation name = \"sayHello\">\n         \u003Csoap:operation soapAction = \"sayHello\"/>\n         \u003Cinput>\n            \u003Csoap:body\n               encodingStyle = \"http://schemas.xmlsoap.org/soap/encoding/\"\n               namespace = \"urn:examples:helloservice\"\n               use = \"encoded\"/>\n         \u003C/input>\n\n         \u003Coutput>\n            \u003Csoap:body\n               encodingStyle = \"http://schemas.xmlsoap.org/soap/encoding/\"\n               namespace = \"urn:examples:helloservice\"\n               use = \"encoded\"/>\n         \u003C/output>\n      \u003C/operation>\n   \u003C/binding>\n\n   \u003Cservice name = \"Hello_Service\">\n      \u003Cdocumentation>WSDL File for HelloService\u003C/documentation>\n      \u003Cport binding = \"tns:Hello_Binding\" name = \"Hello_Port\">\n         \u003Csoap:address\n            location = \"http://www.examples.com/SayHello/\" />\n      \u003C/port>\n   \u003C/service>\n\u003C/definitions>\n    遠端過程呼叫(RPC)的問題  遠端過程呼叫(RPC)的思想，RPC 模型試圖向遠端網路服務發出請求，看起來與在同一程序中呼叫函式或方法相同。但根本上是有缺陷的。因為網路請求與本地函式呼叫還是有不同之處：     本地函式呼叫  網路請求    可預測的，成功或失敗取決於受你控制的引數  不可預測的，請求或響應可能由於網路問題會丟失   返回/不返回結果、丟擲異常  返回/不返回結果、丟擲異常、或者因為超時造成返回時已沒有結果   不會發生響應丟失的情況  能會發生請求實際上已經完成，只是響應丟失的情況   執行時間大致相同  執行時間不穩定(會有網路擁塞或者遠端服務超載的問題)   可以高效地將引用(指標)傳遞給本地記憶體中的物件  所有引數都需要被編碼成可以透過網路傳送的一系列位元組    RPC的當前方向  上述的編碼在基礎上構建了各種 RPC 框架：   Thrift 和 Avro 帶有 RPC 支援  gRPC 是使用 Protocol Buffers 的 RPC 實現  Finagle 也使用 Thrift  Rest.li 使用 JSON over HTTP。  新一代的 RPC 框架更加明確的是，遠端請求與本地函式呼叫不同。   Finagle 和 Rest.li 使用 futures(promises)來封裝可能失敗的非同步操作。  gRPC 支援流，其中一個呼叫不僅包括一個請求和一個響應，還可以是隨時間的一系列請求和響應。    訊息傳遞中的資料流  RPC 和資料庫之間的非同步訊息傳遞系統。   與 RPC 類似，因為客戶端的請求(通常稱為訊息)以低延遲傳送到另一個程序。  與資料庫類似，不是透過直接的網路連線傳送訊息，而是透過稱為訊息代理(也稱為訊息佇列或面向訊息的中介軟體)的中介來臨時儲存訊息。  與直接 RPC 相比，使用訊息代理有幾個優點：   如果收件人不可用或過載，可以充當緩衝區，從而提高系統的可靠性。  可以自動將訊息重新發送到先前已經崩潰的程序，從而防止訊息丟失。  避免發件人需要知道收件人的 IP 地址和埠號(這在虛擬機器經常出入的雲部署中特別有用)。  它允許將一條訊息傳送給多個收件人。  將發件人與收件人邏輯分離(發件人只是釋出郵件，不關心使用者)。  與 RPC 相比，差異在於訊息傳遞通訊通常是單向的：傳送者通常不期望收到其訊息的回覆。一個程序可能傳送一個響應，但這通常是在一個單獨的通道上完成的。這種通訊模式是非同步的：傳送者不會等待訊息被傳遞，而只是傳送它，然後忘記它。    訊息代理  訊息代理的使用方式如下：一個程序將訊息傳送到指定的佇列或主題，代理確保將訊息傳遞給那個佇列或主題的一個或多個消費者或訂閱者。在同一主題上可以有許多生產者和許多消費者。  訊息代理通常不會執行任何特定的資料模型 —— 訊息只是包含一些元資料的位元組序列，因此你可以使用任何編碼格式。如果編碼是向後和向前相容的，你可以靈活地對釋出者和消費者的編碼進行獨立的修改，並以任意順序進行部署。    分散式的Actor框架  Actor 模型是單個程序中併發的程式設計模型。邏輯被封裝在 actor 中，而不是直接處理執行緒。actor 通常代表一個客戶或實體，它可能有一些本地狀態(不與其他任何角色共享)，它透過傳送和接收非同步訊息與其他角色通訊。不保證訊息傳送：在某些錯誤情況下，訊息將丟失。由於每個角色一次只能處理一條訊息，因此不需要擔心執行緒，每個角色可以由框架獨立排程。分散式的 Actor 框架實質上是將訊息代理和 actor 程式設計模型整合到一個框架中。  三個流行的分散式 actor 框架處理訊息編碼如下：   Akka 使用 Java 的內建序列化，不提供前向或後向相容性。 但是，你可以用類似 Prototol Buffers 的東西替代它，從而獲得滾動升級的能力。  Orleans 預設使用不支援滾動升級部署的自定義資料編碼格式；要部署新版本的應用程式，你需要設定一個新的叢集，將流量從舊叢集遷移到新叢集，然後關閉舊叢集。像 Akka 一樣，可以使用自定義序列化外掛。  Erlang OTP 中，對記錄模式進行更改是非常困難的(儘管系統具有許多為高可用性設計的功能)。 滾動升級是可能的，但需要仔細計劃。 一個新的實驗性的 maps 資料型別(2014 年在 Erlang R17 中引入的類似於 JSON 的結構)可能使得這個資料型別在未來更容易。    小結",{"id":297,"path":298,"dir":266,"title":299,"description":7,"keywords":300,"body":306},"content:5.ddia:5.chapter5.md","/ddia/chapter5","05 複製",[281,301,302,303,304,305,294],"領導者與追隨者","同步複製與非同步複製","複製落後的問題","多領導複製","無領導複製","  \n  tr, th, td {\n    border: none!important;\n  }\n  .nolinebreak {\n    white-space: nowrap;\n  }\n  .movepadding {\n    padding-left: 2%!important;\n  }\n  引言   複製資料的目的：   使資料與使用者在地理上接近（減少延遲）  提高系統可用性（即使部分故障）  提高讀取吞吐量（伸縮讀請求機器數量）  複製資料系統的挑戰：   假設資料集非常小  假設每臺機器可以儲存整個資料集的副本  處理資料的變更  變更複製演算法：   單領導者（single leader）  多領導者（multi leader）  無領導者（leaderless）  複製權衡：   同步複製 vs 非同步複製  處理失敗的副本  複製算法歷史：   70 年代研究  分散式資料庫變為主流  複製延遲問題：   最終一致性（eventual consistency）  讀己之寫（read-your-writes）  單調讀（monotonic read）  領導者與追隨者   副本（replica）：儲存資料庫複製的節點。   確保所有資料落在所有副本上的問題：\n向資料庫的寫入操作需要傳播到所有副本。  基於領導者的複製（leader-based replication）：   領導者（leader）：負責接收寫入操作。  追隨者（followers）：負責拉取日誌並更新本地資料庫副本。  複製過程：   客戶端將寫入請求傳送給領導者。  領導者將新資料寫入本地儲存。  領導者將資料變更傳送給所有追隨者（複製日誌/變更流）。  追隨者按照相同處理順序更新本地資料庫副本。  讀取操作：客戶端可以向領導者或任一追隨者進行查詢。  基於領導者的複製應用範例：   關係資料庫：PostgreSQL（9.0 版本開始）、MySQL、Oracle Data Guard、SQL Server 的 AlwaysOn 可用性組。  非關係資料庫：MongoDB、RethinkDB、Espresso。  分散式訊息代理：Kafka、RabbitMQ 高可用佇列。  網路檔案系統：DRBD 等塊複製裝置。  \n      圖 5-1 基於領導者的（主/從）複製  同步複製與非同步複製   複製細節：複製可以是同步（synchronously）或非同步（asynchronously）。   同步複製：主庫在報告寫入成功前，需要等待從庫確認已收到寫入操作。  非同步複製：主庫傳送訊息，但不等待從庫響應。  同步複製的優缺點：   優點：從庫保證有與主庫一致的最新資料副本。  缺點：同步從庫無響應時，主庫無法處理寫入操作。  半同步複製（semi-synchronous）：   一個從庫是同步的，其他的從庫則是非同步的。  同步從庫不可用或緩慢時，將一個非同步從庫改為同步執行。  完全非同步複製：   主庫失效且不可恢復時，尚未複製給從庫的寫入會丟失。  優點：主庫可以繼續處理寫入，即使所有從庫都落後。  鏈式複製（chain replication）：   同步複製的一種變體，旨在不丟資料並提供良好效能和可用性。  已在一些系統中實現，例如 Microsoft Azure Storage。  \n      圖 5-2 基於領導者的複製：一個同步從庫和一個非同步從庫  複製的一致性與共識（consensus）：   第九章將詳細探討共識理論。  本章主要討論實踐中的資料庫常用的簡單複製形式。  設定新從節點：   獲取主節點一致性快照。  將快照複製到新的從節點。  從節點連線到主節點並拉取快照後的所有資料變更。  從節點趕上主庫後，可以繼續及時處理主節點資料變化。  處理節點宕機：   以基於領導者的複製實現高可用。  從節點失效：追趕恢復   重新連線到主節點。  請求並處理斷開期間的所有資料變更。  主節點失效：故障切換   確認主庫失效。  選擇新的主節點。  重新配置系統以啟用新的主節點。  故障切換過程中可能出現的問題：   非同步複製導致資料損失。  與其他外部儲存協調時的問題。  腦裂問題。  超時配置的困難。  儘管有些軟體支援自動故障切換，但許多運維團隊仍更願意手動執行故障切換。   分散式系統中的基本問題包括節點故障、不可靠的網路、副本一致性、永續性、可用性和延遲。這些問題將在第八章和第九章中深入討論。  複製日誌的實現可以分為以下幾種：   基於語句的複製：主節點記錄每個寫入請求並將語句日誌傳送給從庫。但這種方式存在諸多問題，如非確定性函式、自增列、副作用等。  傳輸預寫式日誌（WAL）：主庫將日誌通過網路傳送給從庫。此方法使複製與儲存引擎緊密耦合，升級資料庫軟體可能需要停機。  邏輯日誌複製（基於行）：對複製和儲存引擎使用不同的日誌格式，使主庫和從庫能夠執行不同版本的資料庫軟體或儲存引擎。邏輯日誌格式更容易解析，有利於資料變更捕獲（change data capture）。  基於觸發器的複製：通過資料庫中的觸發器和儲存過程，將資料更改記錄到單獨的表中，再使用外部程式讀取該表並將資料變更複製到另一個系統。儘管開銷較大且容易出錯，但基於觸發器的複製具有很高的靈活性。  以上四種複製方式各有優缺點，具體應用需根據實際情況選擇合適的複製方法。  複製落後的問題  能夠容忍節點故障只是人們採用複製的原因之一。  包括容錯、可伸縮性和降低延遲。\n基於領導者的複製方法可用於讀多寫少的場景，通過增加從節點來分散讀請求以達到讀取伸縮。  然而，這種方法主要適用於非同步複製，因為完全同步的配置容易受到節點故障和網路中斷的影響，導致整個系統無法寫入。最後可能會遇到最終一致性問題。  當應用程式讀取從節點時，可能會看到過時的資訊，導致不一致。\n這種不一致是暫時的，但在某些情況下，如系統負載過高或網路問題，延遲可能達到幾分鐘。  這不僅是理論問題，還會在應用設計中產生實際影響。\n本節將探討複製延遲時可能出現的問題以及解決方案。  讀己之寫  用戶提交資料後可能會立即查看。  然而，如果使用異步複製，新資料可能尚未同步到副本中，導致用戶看不到剛剛提交的資料，造成用戶不滿。  \n      圖 5-3 使用者寫入後從舊副本中讀取資料。需要寫後即讀 (read-after-write) 的一致性來防止這種異常  「寫後讀一致性」的概念，它確保用戶在提交更新後重新加載頁面時能看到自己的更新。這並不保證其他用戶立即看到這些更新，但可確保用戶的輸入已被妥善儲存。  在基於領導者的複製系統中實現寫後讀一致性的方法：   對可能被使用者修改的內容，總是從主節點讀取。  跟蹤上次更新時間，決定是否從主節點讀取。監控從節點的複製延遲，避免讀取過於滯後的從節點。  客戶端記住最近一次寫入的時間戳，確保讀取時所有變更已傳播到從節點。  考慮數據中心分佈，確保需要主節點的請求路由到正確的資料中心。  使用多個裝置的使用者需提供跨裝置的寫後讀一致性的挑戰。以下是兩個重點：   跨裝置元資料的中心化儲存：由於不同裝置上的程式無法知道其他裝置的操作，因此需要對元資料進行中心化儲存，以記住使用者的更新時間戳。  跨資料中心的請求路由：當副本分布在不同的資料中心時，確保來自不同裝置的請求路由到同一資料中心變得困難。解決方法是將所有使用者裝置的請求路由到同一個資料中心。  單調讀取  這段描述了在使用非同步從庫讀取資料時，可能出現的時光倒流現象。\n當使用者從不同從節點進行多次讀取時，可能會看到不一致的結果，  例如先看到一條評論，然後該評論消失。這種現象可能會讓使用者感到困惑。  \n      圖 5-4 使用者首先從新副本讀取，然後從舊副本讀取。時間看上去回退了。為了防止這種異常，我們需要單調的讀取。  介於強一致性和最終一致性之間的保證。  單調讀確保使用者在連續讀取過程中不會看到時間回退，即使讀取到的是舊資料。\n實現單調讀的方法之一是讓每個使用者始終從同一副本讀取資料。  簡單來說，單調讀保證了使用者讀取資料的連續性，避免了時間回退。  一致性前綴讀取  複製延遲異常如何違反因果律。  當第三個人聽到 Cake 夫人和 Poons 先生的對話時，由於從庫延遲的差異，他們聽到的對話順序顛倒了。\n這使得觀察者感覺 Cake 夫人在問題被提出之前就已經回答了，顯得具有超能力。  這樣的現象可能會讓人感到困惑。  \n      圖 5-5 如果某些分割槽的複製速度慢於其他分割槽，那麼觀察者可能會在看到問題之前先看到答案。  系統會為每個寫入操作分配一個全局唯一的時間戳。\n這些時間戳保證了操作的全局順序。  當用戶發起讀取請求時，系統會返回小於或等於該請求時間戳的最新數據。\n這樣，即使某些節點的數據尚未同步，用戶也能獲得一個一致的數據快照。  此外，這種策略還能保證讀取操作的因果一致性，即用戶能夠觀察到他們之前的寫入操作所引起的變化。  總之，一致性前綴讀取是一種在分布式系統中確保數據讀取一致性的有效策略。\n通過使用全局唯一的時間戳對寫入操作進行排序，並在讀取時返回小於或等於請求時間戳的最新數據，這個策略確保了用戶在讀取數據時能夠獲得一個一致的數據快照，同時保證了讀取操作的因果一致性。  複製落後的解決方案  確保資料一致性和正確性非常重要。\n當複製延遲變大時，我們需要關注應用程式的行為，確保使用者體驗不受影響。  應用程式可以在資料庫層面提供更強的保證，而事務就是一個很好的解決方案。\n儘管在分散式資料庫中，一些系統放棄了事務，但事務仍然是一個有力的工具。  後續章節(第7章、第9章)將更詳細地討論事務和其他替代方案。  多領導複製  目前為止，我們專注在單一主節點的複製架構，但還有其他選擇。單一主節點的問題在於所有寫入都必須通過它，而多領導者配置允許多個節點接受寫入。在這種情況下，每個主節點也同時是其他主節點的從節點。  多領導複製的適用場景  在單個數據中心內部使用多個主節點的配置沒有太大意義，因為其導致的複雜性已經超過了能帶來的好處。但在一些情況下，這種配置也是合理的。  多資料中心操作  討論了一種多主節點配置，用於在多個資料中心分散數據庫副本。這種配置可提高容錯能力和地理親近性。每個資料中心都有一個主庫，並在資料中心之間互相複製更改。這樣可以在本地資料中心內使用常規的主從複製。  \n      圖 5-6 跨多個數據中心的多主複製  我們來比較一下在運維多個數據中心時，單領導和多領導的適應情況   效能   單領導：寫入需穿越網際網路，增加寫入時間。  多領導：寫入在本地資料中心進行，降低網路延遲影響。  容忍資料中心停機   單領導：需故障切換，從庫成為主庫。  多領導：各資料中心獨立運作，故障資料中心恢復後自動複製。  容忍網路問題   單領導：對資料中心間連線問題敏感，寫入為同步操作。  多領導：採非同步複製，對網路問題有更高的承受能力。  一些資料庫支援多領導配置，並且可以透過外部工具實現。\n儘管多領導複製具有優點，但也存在寫衝突問題。  此外，由於多領導複製在很多資料庫中是改裝功能，可能會有一些配置上的缺陷，並可能與其他資料庫功能產生意外反應。\n因此，多領導複製被認為是一個高風險領域，應該盡量避免使用。  客戶端的離線操作  另一種適用多領導複製的場景，即需要在斷網狀態下仍能使用的應用程式\n例如日曆應用程式。\n在這種情況下，每個裝置都有一個本地資料庫，並存在非同步的多主複製過程來同步日曆副本。\n這種設定類似於資料中心之間的多領導複製，但網路連線極不可靠。  雖然有一些工具可以幫助實現多領導複製，但實際應用中仍然有很多難點需要克服。  其中一個工具是CouchDB。  協作編輯  這種應用允許多人同時編輯同一份文件，例如 Etherpad 和 Google Docs。  為了避免編輯衝突，應用程式必須先取得文件的鎖定，然後才能進行編輯。\n但為了加速協作，可能會將更改的單位設定得非常小，並避免鎖定。  這種方法允許多個使用者同時進行編輯，但需要解決衝突。\n這種協作模式相當於主從複製模型下在主節點上執行事務操作。  處理寫入衝突  容易遇到寫衝突的問題。\n例如，兩個使用者同時編輯同一個維基頁面，且對同一個標題進行修改，這就可能發生衝突。  這時需要解決衝突，以保證數據的一致性。\n多領導者的資料庫同步上 通常被認為有風險，應儘量避免．  \n      圖 5-7 兩個主節點同時更新同一記錄引起的寫入衝突  同步與非同步衝突檢測  在單領導資料庫中，兩個寫入操作會發生阻塞，必須等待第一個操作完成才能進行第二個。  而在多領導配置中，兩個操作都能成功進行，但是衝突的檢測需要在稍後的時間進行，可能導致解決衝突的過程較為困難。  如果想要同步衝突檢測，可以等待所有副本接受寫入後再告訴使用者寫入成功，但是這樣會失去多領導複製的優勢。\n如果需要同步衝突檢測，建議使用單領導複製。  避免衝突  最簡單策略，即確保所有寫入特定記錄的操作都透過同一個主庫進行。\n這樣可以避免多個主庫同時寫入同一個記錄而導致衝突。  然而，如果需要更改指定的主庫，例如因為某個資料中心出現故障，就需要處理不同主庫同時寫入的可能性。\n這需要在應用程式中實現一些特殊處理。  漸趨一致的狀態  如何處理多個複本之間的數據一致性問題。\n當單個主資料庫進行寫操作時，最後一次寫操作將決定該欄位的最終值。  但在多主配置中，由於沒有明確的寫入順序，最終值很難確定。\n因此，必須以一種收斂的方式解決衝突，以確保所有複本的最終值都是相同的。  有多種方法可以實現這個目標，其中包括：   使用唯一 ID 來選擇勝利者並丟棄其他寫入，這種方法稱為最後寫入勝利。然而，這種方法容易造成資料丟失。  為每個副本分配一個唯一的 ID，以 ID 編號較高的寫入為優先。同樣，這種方法也可能導致資料丟失。  將寫入的值合併在一起，例如按字母順序排序後合併。  使用顯式資料結構記錄衝突，然後編寫解決衝突的程式碼來保留所有資訊。  自訂解決衝突的邏輯   寫入時執行\n指當資料庫系統在複製更改日誌中發現衝突時，會立即呼叫衝突處理程式碼。  讀取時執行\n將所有的不同版本都儲存在資料庫中。\n當應用程式下一次讀取這些資料時，會將這些不同版本的資料都返回給應用程式。\n應用程式可以讓使用者解決衝突或自動解決衝突，然後將結果寫回資料庫。\n這是一種常見的解決衝突的方法，例如 CouchDB 就是這樣做的。   自動衝突解決\n衝突解決規則可能很容易變得越來越複雜，自定義程式碼可能也很容易出錯。\n亞馬遜是一個經常被引用的例子，由於衝突解決處理程式而產生了令人意外的效果：一段時間以來，購物車上的衝突解決邏輯將保留新增到購物車的物品，但不包括從購物車中移除的物品。\n因此，顧客有時會看到物品重新出現在他們的購物車中，即使他們之前已經被移走【37】。  已經有一些有趣的研究來自動解決由於資料修改引起的衝突。有幾項研究值得一提：   無衝突複製資料型別（Conflict-free replicated datatypes，CRDT）【32,38】是可以由多個使用者同時編輯的集合、對映、有序列表、計數器等一系列資料結構，它們以合理的方式自動解決衝突。一些 CRDT 已經在 Riak 2.0 中實現【39,40】。  可合併的持久資料結構（Mergeable persistent data structures）【41】顯式跟蹤歷史記錄，類似於 Git 版本控制系統，並使用三向合併功能（而 CRDT 使用雙向合併）。  操作轉換（operational transformation）  42  是 Etherpad 【30】和 Google Docs 【31】等協同編輯應用背後的衝突解決演算法。它是專為有序列表的並發編輯而設計的，例如構成文字文件的字元列表。\n這些演算法在資料庫中的實現還很年輕，但很可能將來它們會被整合到更多的複製資料系統中。自動衝突解決方案可以使應用程式處理多主資料同步更為簡單。  怎樣才算衝突？  有些衝突是明顯的，例如同時修改同一欄位的寫操作，但有些衝突可能不太容易察覺，  例如在會議室預訂系統中避免同一房間的重疊預訂，雖然可以在預訂時再次檢查，但在多領導不同主庫時還是可能發生衝突．  接下來的章節將進一步討論如何解決這些衝突，\n包括在第七章中介紹更多衝突案例，以及在第十二章中討論檢測和解決衝突的可伸縮方法。  Multi-Leader 複製的拓樸  即資料如何從一個節點傳播到另一個節點。\n當有兩個主庫時，唯一合理的拓撲是將所有寫入都發送到對方。  但如果有兩個以上的主庫時，就有多種不同的拓撲結構可選擇。  \n      圖 5-8 三種可以在多主複製中使用的拓撲示例。  常見的三種拓撲結構   環狀拓墣  星狀拓墣  All to all 拓墣  其中，全部到全部的拓撲結構容錯性較好，因為它可以避免單點故障，但可能存在訊息超前的問題。\n而環形和星形拓撲結構的容錯性較差，一旦某個節點出現故障，可能會中斷複製訊息流，需要手動重新配置。  此外，在環形和星形拓撲中，為了避免無限複製迴圈，每個節點都被賦予一個唯一的識別符號。  \n      圖 5-9 使用多主複製時，寫入可能會以錯誤的順序到達某些副本。  多主複製系統中的一個因果關係問題，  例如當不同的節點以不同順序處理寫入時可能會發生衝突。\n為了解決這個問題，可以使用**版本向量(version vectors)**技術進行排序。  但是，有些多主複製系統的衝突檢測技術可能不夠好，因此在使用這些系統時應該仔細閱讀文件並進行測試，以確保系統能提供所需的保證。  無領導複製  本章節討論了不同的複製方法，如單主複製和多主複製，它們的共同點是都有一個主庫來處理寫請求。  然而，無主複製方法不同，因為它不依賴主庫。  無主複製系統如 Dynamo、Riak、Cassandra 和 Voldemort 允許客戶端直接向副本寫入資料，或通過協調者節點進行寫入。\n無主複製設計對資料庫的使用方式具有重要影響。  當節點失效時寫入資料庫  假設有三個副本的資料庫。當其中一個副本不可用時，基於領導者的配置可能需要進行故障切換。  然而，在無主配置中，故障轉移不存在。\n客戶端會將寫入傳送到所有副本，而無需擔心其中一個副本錯過了寫入。  只要有兩個副本確認寫入，就認為寫入成功。  \n     圖 5-10 法定寫入，法定讀取，並在節點中斷後讀修復。  作者提到了節點重新連線後的數據同步問題。  為了解決可能讀取到過時數據的問題，客戶端會向多個節點發送讀請求。\n然後，通過版本號確定各節點返回的數據哪個是最新的，以避免使用到舊的數據資料。  利用讀取來修復和反熵  在密集型應用的資料複製過程中，有兩種常用的方法來確保所有資料在各個副本中一致：   利用讀取來修復：客戶端在讀取多個節點時，檢測並修正舊的數據資料(傳送新值給該節點)，適用於經常被讀取的值。  反熵過程：一個後台程序在副本間不斷檢查並複製缺失的資料，不依賴特定的順序，可能有一定延遲。  然而，並非所有系統都實現了這兩種機制。如果沒有反熵過程，很少被讀取的值可能會丟失，降低了永續性。  讀取和寫入的法定票數演算(Quorums)  我們討論了資料密集型應用中的資料副本。  以三個副本的情況為例，只要至少有兩個副本完成寫入，該寫入就算成功。為了確保讀取到的資料是最新的，我們需要從至少兩個副本讀取。\n通常，n 代表副本數，w 代表寫入成功需要的節點數，r 代表讀取需要查詢的節點數。  只要 r 和 w 的和大於 n，就能確保讀取到最新的資料。\n這種讀寫方法被稱為法定人數的讀寫。  在 Dynamo 風格的資料庫中，n、w 和 r 的值都是可配置的，可以根據需求進行調整。   叢集中可能有多於 n 個的節點（叢集的機器數可能多於副本數目）。但是任何給定的值只能儲存在 n 個節點上。這允許對資料集進行分割槽，從而可以支援比單個節點的儲存能力更大的資料集。我們將在 第六章 繼續討論分割槽。   如果 w \u003C n ，當節點不可用時，我們仍然可以處理寫入。  如果 r \u003C n ，當節點不可用時，我們仍然可以處理讀取。  對於 n = 3，w = 2，r = 2，我們可以容忍一個不可用的節點。  對於 n = 5，w = 3，r = 3，我們可以容忍兩個不可用的節點。 這個案例如 圖 5-11 所示。  通常，讀取和寫入操作始終並行傳送到所有 n 個副本。引數 w 和 r 決定我們等待多少個節點，即在我們認為讀或寫成功之前，有多少個節點需要報告成功。  \n     圖 5-11 如果 w + r > n ，讀取 r 個副本，至少有一個副本必然包含了最近的成功寫入。  在進行資料寫入或讀取時，如果可用節點少於所需的 w 或 r 值，將會出現錯誤。\n節點失效可能有多種原因，如節點關閉、操作錯誤或網路問題等。  我們只需關注節點是否成功響應，而無需區分錯誤類型。  Quorum 一致性的限制  當有 n 個副本，並滿足 w + r > n 的條件時，通常每次讀取都能獲得最新寫入的值。\n為確保 w + r > n，並在節點故障時保持可靠性，r 和 w 通常被選為大於 n/2 的數字。\n設計分散式算法時，可以靈活選擇其他讀寫節點數量。  當 w 和 r 設定較小，以使 w + r \u003C= n 時，讀取和寫入操作僅需要少量成功響應。\n這樣的設定可以提高可用性和降低延遲，但可能導致讀取到較舊的資料。\n只有當可達副本數量低於 w 或 r 時，資料庫才無法進行讀寫。  雖然法定人數 (w + r > n) 似乎可以保證讀取返回最新的寫入值，但實際上仍可能出現邊緣情況，導致讀取到舊的 數據資料。\n這些邊緣情況包括寫入和讀取落在不同節點上、寫入衝突、寫入和讀取同時進行、寫入部分成功或節點故障等。  因此，在實踐中，Dynamo 風格的資料庫通常對可容忍最終一致性的用例進行最佳化。\n雖然可以通過調整 w 和 r 的值來減少讀取到陳舊值的機率，但將其視為絕對保證是不明智的。  為了獲得更強有力的保證，可能需要使用  交易(Translation)  或 共識方法(Consensus Algorithm) 。  監控舊值  在監控資料庫健康狀況時，了解複製延遲和陳舊讀取對於確保數據準確性非常重要。  對於基於領導者的複製，可以通過比較主從庫的位置來測量延遲解決舊數據資料的判斷。  但是無主複製系統的監控更為困難，無主寫入順序是不固定的，讓監控更加困難．\n如果沒有反熵，落後程度就會沒有上限．  目前對於無主複製的監測有一定的研究，但尚未普及。  將陳舊測量值納入標準度量集是有益的，\n因為最終一致性是個模糊的保證，從維運角度來看，最終\"量化\"仍然是件很重要的事情。  寬鬆的Quorums和提示移交  使用合理配置的法定人數可以讓資料庫在不需要故障切換的情況下容忍節點故障。\n這對於需要高可用性、低延遲並能容忍偶爾讀取到舊資料的應用非常有吸引力。  然而，法定人數在網路中斷時可能無法如預期那樣具有容錯性。  如果客戶端與資料庫節點失去連接，則剩餘的可用節點可能不足以達到法定人數，導致客戶端無法正常使用資料庫。\n如果在網路中斷期間客戶端能連線到部分資料庫節點，但無法達到法定節點數量，設計人員需考慮：   對無法滿足法定 w 或 r 個節點數的請求是否返回錯誤？  或是接受寫入，寫到部分可達節點，而非常見制定的 n 個節點？「寬鬆的Quorums」  簡而言之，如何在網路中斷時保證資料庫正常運行是設計人員需要考慮的問題。\n寬鬆的法定人數（sloppy quorum）在設計資料密集型應用的過程中的作用。  當網路出現問題時，寬鬆的法定人數可以提高寫入可用性，允許寫入操作在任何可用的節點上完成。\n但這種方法不能確保讀取到最新的數據，因為最新的數據可能已經暫時寫入了不在主節點集合中的節點。  提示移交（hinted handoff）機制可在網路問題解決後將這些暫時寫入的數據傳送到主節點。  寬鬆的Quorums 模式 在不同的 Dynamo 實現中是可選的，並且其預設設置因實現而異。  多資料中心營運  無領導複製在多資料中心操作中的適用性，特別是在容忍衝突、網路中斷和延遲尖峰方面。Cassandra 和 Voldemort 運用無主模型來實現多資料中心支援，  將副本分布在各個資料中心，並允許配置每個資料中心的副本數量。  客戶端通常僅等待本地資料中心內的法定節點確認，以減少跨資料中心鏈路延遲和中斷的影響。  Riak 則將通訊限制在單一資料中心範圍內，並在後台非同步執行跨資料中心的複製。  檢測並發寫入  Dynamo 風格的資料庫可能會遇到衝突問題，因為多個客戶端可以同時寫入相同的鍵。  由於網路延遲和部分節點故障，事件可能以不同順序到達不同節點，導致衝突。  例如，圖 5-12 顯示了兩個客戶機 A 和 B 同時寫入三節點資料儲存中的鍵 X：   節點 1 接收來自 A 的寫入，但由於暫時中斷，未接收到來自 B 的寫入。  節點 2 首先接收來自 A 的寫入，然後接收來自 B 的寫入。  節點 3 首先接收來自 B 的寫入，然後從 A 寫入。  \n     圖 5-12 並發寫入 Dynamo 風格的資料儲存：沒有明確定義的順序。  如何達成一致性。為了達成一致性，副本應該趨於相同的值。  雖然理想情況下，資料庫應該自動處理複製衝突，但實際上可能需要應用程式開發人員具備一定的資料庫內部知識來避免資料丟失。  本章還將更深入地探討解決衝突的方法。  後寫者贏(丟棄並發的寫入)  為了實現最終收斂，可以使用最後寫入勝利LWW(Last Write Win)方法，即選擇具有最大時間戳的寫入作為最新的，並丟棄具有較早時間戳的寫入。  但是，LWW會以永續性為代價，可能導致部分寫入丟失。\n在某些情況下，例如快取，這可能是可以接受的，但如果資料丟失不可接受，則LWW可能不是最佳選擇。  要安全地在資料庫中使用LWW，應確保每個鍵只寫入一次，並將其視為不可變，以避免對同一個鍵進行並發更新。  happens-before 和並發性  判斷兩個操作是否並發的關鍵在於一個操作是否在另一個操作之前發生。\n當兩個操作互不知道對方時，它們是並發的。  在這種情況下，需要解決潛在的衝突。總結來說，操作的並發性可以分為三種情況：   A 在 B 之前  B 在 A 之前  A 和 B 並發   並發性、時間和相對性\n如果兩個操作 “同時” 發生，似乎應該稱為並發 —— 但事實上，它們在字面時間上重疊與否並不重要。由於分散式系統中的時鐘問題，現實中是很難判斷兩個事件是否是 同時 發生的，這個問題我們將在 第八章 中詳細討論。  為了定義並發性，確切的時間並不重要：如果兩個操作都意識不到對方的存在，就稱這兩個操作 並發，而不管它們實際發生的物理時間。人們有時把這個原理和物理學中的狹義相對論聯絡起來【54】，該理論引入了資訊不能> 比光速更快的思想。因此，如果兩個事件發生的時間差小於光透過它們之間的距離所需要的時間，那麼這兩個事件不可能相互影響。  在計算機系統中，即使光速原則上允許一個操作影響另一個操作，但兩個操作也可能是 並發的。例如，如果網路緩慢或中斷，兩個操作間可能會出現一段時間間隔，但仍然是並發的，因為網路問題阻止一個操作意識到另一個操作的存在。  擷取 happens-before 先後關係  用於確定兩個操作是否為並發的，或者一個在另一個之前。\n該例子中，兩個客戶端同時將商品加入購物車。  這個過程涉及五次寫入操作，每次客戶端嘗試將新商品添加到購物車時，伺服器都會分配新的版本號。\n在這個過程中，不同客戶端的操作可能互相影響，最後伺服器將保留並發值。  簡單來說，該演算法是為了確定操作之間的並發性，並根據操作的順序和版本號來合併購物車內容。  \n     圖 5-13 在同時編輯購物車時捕獲兩個客戶端之間的因果關係。  \n     圖 5-14 圖 5-13 中的因果依賴關係圖。  該演算法的工作原理如下   伺服器為每個鍵維護一個版本號，寫入時遞增版本號，並將新版本號與值一起儲存。  客戶端讀取鍵時，伺服器返回所有未覆蓋值及最新版本號；客戶端需先讀取再寫入。  客戶端寫入鍵時，附帶讀取的版本號，並將之前讀取的所有值合併在一起。  伺服器收到寫入時，覆蓋相同或更低版本號的值，並以更高版本號儲存所有值。  寫入時包含讀取的版本號有助於確定寫入基於的狀態。\n無版本號的寫入與其他寫入並發，不會覆蓋內容，而只會作為隨後讀取的一個值返回。  合併並發寫入的值  當多個客戶端同時進行資料寫入時，如何解決衝突並合併資料。  以購物車為例，作者提出一種將並發值做並集的合併方法。\n然而，若要允許用戶移除購物車中的項目，這種方法可能無法產生正確結果。  為解決這個問題，系統應留下刪除標記（墓碑），以表示某個項目已被移除。\n另外，為簡化合併過程，有一些資料結構（如CRDT）可自動執行合併操作。  版本向量  在圖 5-13 的例子中，僅使用了一個副本。  當有多個副本而無主庫時，我們需要修改算法。\n我們將使用版本向量來捕捉多個副本之間的依賴關係，每個副本都有自己的版本號。  當副本處理寫入時，版本號會增加，並追踪其他副本的版本號。\n這有助於確定覆蓋哪些值以及保留哪些值。  版本向量允許區分覆蓋寫入和並發寫入，並確保從一個副本讀取並寫回到另一個副本是安全的。\n然而，應用程式可能需要合併並發值。\n只要能正確合併，就不會丟失資料。   版本向量和向量時鐘\n版本向量有時也被稱為向量時鐘，即使它們不完全相同。其中的差別很微妙 —— 細節請參閱參考資料【57,60,61】。簡而言之，在比較副本的狀態時，版本向量才是正確的資料結構。  小結  在本章中，我們考察了複製的問題。複製可以用於幾個目的：   高可用性  即使在一臺機器（或多臺機器，或整個資料中心）停機的情況下也能保持系統正常執行  斷開連線的操作  允許應用程式在網路中斷時繼續工作  延遲  將資料放置在地理上距離使用者較近的地方，以便使用者能夠更快地與其互動  可伸縮性  透過在副本上讀，能夠處理比單機更大的讀取量  儘管是一個簡單的目標 - 在幾臺機器上保留相同資料的副本，但複製卻是一個非常棘手的問題。它需要仔細考慮並 發和所有可能出錯的事情，並處理這些故障的後果。至少，我們需要處理不可用的節點和網路中斷（這還不包括更隱蔽的故障，例如由於軟體錯誤導致的靜默資料損壞）。  我們討論了複製的三種主要方法：   單主複製  客戶端將所有寫入操作傳送到單個節點（主庫），該節點將資料更改事件流傳送到其他副本（從庫）。讀取可以在任何副本上執行，但從庫的讀取結果可能是陳舊的。  多主複製  客戶端將每個寫入傳送到幾個主庫節點之一，其中任何一個主庫都可以接受寫入。主庫將資料更改事件流傳送給彼此以及任何從庫節點。  無主複製  客戶端將每個寫入傳送到幾個節點，並從多個節點並行讀取，以檢測和糾正具有陳舊資料的節點。  每種方法都有優點和缺點。單主複製是非常流行的，因為它很容易理解，不需要擔心衝突解決。在出現故障節點、網路中斷和延遲峰值的情況下，多主複製和無主複製可以更加健壯，其代價是難以推理並且僅提供非常弱的一致性保證。  複製可以是同步的，也可以是非同步的，這在發生故障時對系統行為有深遠的影響。儘管在系統執行平穩時非同步複製速度很快，但是要弄清楚在複製延遲增加和伺服器故障時會發生什麼，這一點很重要。如果主庫失敗後你將一個非同步更新的從庫提升為新的主庫，那麼最近提交的資料可能會丟失。  我們研究了一些可能由複製延遲引起的奇怪效應，我們也討論了一些有助於決定應用程式在複製延遲時的行為的一致性模型：   寫後讀一致性  使用者應該總是能看到自己提交的資料。  單調讀  使用者在看到某個時間點的資料後，他們不應該再看到該資料在更早時間點的情況。  一致字首讀  使用者應該看到資料處於一種具有因果意義的狀態：例如，按正確的順序看到一個問題和對應的回答。  最後，我們討論了多主複製和無主複製方法所固有的並發問題：因為他們允許多個寫入並發發生，這可能會導致衝突。我們研究了一個數據庫可以使用的演算法來確定一個操作是否發生在另一個操作之前，或者它們是否並發發生。我們還談到了透過合併並發更新來解決衝突的方法。  在下一章中，我們將繼續考察資料分佈在多臺機器間的另一種不同於 複製 的形式：將大資料集分割成 分割槽。",{"id":308,"path":309,"dir":266,"title":310,"description":7,"keywords":311,"body":347},"content:5.ddia:9.chapter9.md","/ddia/chapter9","09 一致性與共識",[312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346],"一致性與共識","目錄","9.1 一致性保證","9.2 線性一致性","是什麼東西可以讓系統線性一致**？**","依賴線性一致性","實現線性一致的系統","線性一致性的代價","9.3 順序保證","順序與因果關係(因果一致性 causally consistent)","序列號順序","全序廣播","9.4 分散式事務與共識","共識的不可能性","原子提交與兩階段提交","兩階段提交簡介","系統承諾","協調者失效","三階段提交","實踐中的分散式事務","恰好一次的訊息處理","XA事務","懷疑持有鎖","從協調者故障中恢復","分散式事務的限制","容錯共識","共識演算法和全序廣播","單主複製與共識","紀元編號和法定人數","共識的局限性","成員與協調服務","將工作分配給節點","服務發現","成員資格服務","9.5 本章小結","  設計資料密集型應用-ch9  一致性與共識  目錄   9.1    一致性保證  9.2    線性一致性  9.3    順序保證  9.4     分散式事務與共識  9.5     本章小結  9.1 一致性保證  大多數複製的資料庫至少提供了最終一致性，這意味著如果你停止向資料庫寫入資料並等待一段不確定的時間，那麼最終所有的讀取請求都會返回相同的值。換句話說，不一致性是暫時的，最終會自行解決（假設網路中的任何故障最終都會被修復。  對於應用開發人員而言，最終一致性是很困難的，因為它與普通單執行緒程式中變數的行為有很大區別。如果將一個值賦給一個變數，然後很快地再次讀取，不可能讀到舊的值，或者讀取失敗。資料庫表面上看起來像一個你可以讀寫的變數，但實際上它有更複雜的語義。  我們在使用弱一致性的資料庫時必須了解弱一致性的局限性,因為錯誤常常很難找到也很難測試，因為應用可能在大多數情況下執行良好。當系統出現故障（例如網路中斷）或高併發時，最終一致性的邊緣情況才會顯現出來。      強一致性  弱一致性    解釋  強一至性表示，一旦一個操作（如寫入）完成，該操作的效果會立即反映在接下來所有的讀取操作上，無論這些讀取操作是從哪個節點發出的。  在弱一至性模型下，寫入操作的效果可能不會立即反映在隨後的讀取操作中，特別是來自其他節點的讀取操作。   例子  假設你在銀行轉帳，當錢從一個帳戶轉到另一個帳戶後，不管是你自己還是其他任何人，只要查詢餘額，都會立即看到最新的數字。  假設你更新了你的社交媒體狀態。你的朋友可能會在幾秒鐘後或幾分鐘後才看到這個更新，取決於各種因素（如網絡延遲、緩存等）。   模型  線性一致性(linearizability)最強  最終一致性(Eventual Consistency)   容錯性  少  高   所需資源  高  低   實踐  易  難  9.2 線性一致性  介紹：  在最終一致的資料庫，如果你在同一時刻問兩個不同副本相同的問題，可能會得到兩個不同的答案。這很讓人困惑。如果資料庫可以提供只有一個副本的假象（即，只有一個數據副本），那麼事情就簡單太多了。每個客戶端都會有相同的資料檢視，且不必擔心複製滯後了。  這種抽象的想法也稱為  原子一致性（atomic consistency） 【7】，  強一致性（strong consistency） ，  立即一致性（immediate consistency）  或   外部一致性（external consistency ）     圖 9-1 這個系統是非線性一致的，導致了球迷的困惑  描述：  計分員成功寫入德國獲勝到Leader  Leader開始寫入Foll1 Foll2  Alice用手機看到德國贏了然後Bob因為Leader寫入的時間差以為比賽還在進行  是什麼東西可以讓系統線性一致**？**  介紹：  真正的想法就是如何讓操作多個資料庫像在單一操作一個資料庫一樣     圖 9-2 如果讀取請求與寫入請求併發，則可能會返回舊值或新值  x稱為暫存器,可以是鍵或是資料庫的行或是資料庫的一個文件  描述：  A與B會在C寫入中產生不確定性  test     package   main\n   \n   import   (\n       \"  log  \"\n       \"  math/rand  \"\n       \"  sync  \"\n       \"  time  \"\n   )\n   \n   type   DB   struct   {\n       Value   int\n   }\n   type   Client   struct   {\n       Name   string\n       wg     *  sync  .  WaitGroup\n   }\n   \n   func   (  c   *  Client  )   Read  (  db   *  DB  ) {\n       defer   c.wg.  Done  ()\n       for   i   :=   0  ; i   \u003C   3  ; i  ++   {\n           time.  Sleep  (time.Millisecond   *   time.  Duration  (rand.  Intn  (  10  )))\n           log.  Printf  (  \"  %s   x=  %d   %v   \\n  \"  , c.Name, db.Value, time.  Now  ().  UnixMilli  ())\n       }\n   }\n   \n   func   (  c   *  Client  )   Write  (  db   *  DB  ,   i   int  ) {\n       defer   c.wg.  Done  ()\n       time.  Sleep  (time.Millisecond   *   time.  Duration  (rand.  Intn  (  10  )))\n       db.Value   =   i\n   }\n   \n   func   main  () {\n       rand.  Seed  (time.  Now  ().  UnixNano  ())\n       wg   :=   &  sync  .  WaitGroup  {}\n   \n       wg.  Add  (  3  )\n       db   :=   &  DB  {Value:   0  }\n       clientA   :=   &  Client  {Name:   \"ClientA\"  , wg: wg}\n       clientB   :=   &  Client  {Name:   \"ClientB\"  , wg: wg}\n       clientC   :=   &  Client  {Name:   \"ClientC\"  , wg: wg}\n   \n       go   clientA.  Read  (db)\n       go   clientB.  Read  (db)\n       go   clientC.  Write  (db,   1  )\n   \n       wg.  Wait  ()\n   }\n   /*\n   2023/09/01 17:34:13 ClientA x=0 1693560853277 \n   2023/09/01 17:34:13 ClientA x=0 1693560853278 \n   2023/09/01 17:34:13 ClientB x=0 1693560853278 \n   2023/09/01 17:34:13 ClientA x=0 1693560853283 \n   2023/09/01 17:34:13 ClientB x=1 1693560853288 \n   2023/09/01 17:34:13 ClientB x=1 1693560853298\n   */\n   為了使系統線性一致需要新增另外一個約束“只要有任何一個Client讀取x都必須返回最新值     圖 9-3 任何一個讀取返回新值後，所有後續讀取（在相同或其他客戶端上）也必須返回新值。  描述：  這張圖是與上一個圖的比較,這張圖是最後想要達到的目標(線性一致)也就是我不管C到底在哪個時間點真正寫入,只要A已經讀到1的話,B永遠不能讀到其他值一定要是1     圖 9-4 視覺化讀取和寫入看起來已經生效的時間點。 B 的最後讀取不是線性一致性的  描述：  這邊想要描述如果write與cas混用會發生什麼問題  D最後出錯了原因是他一開始沒有用cas先比較資料庫原本有的內容是什麼等到他想用cas的時候發現cas(2,0,3) 2≠0 所以error,  C想用cas更新資料庫,而C最後一次的記憶體裡面x=2所以cas(2,2,4) 2=2所以x更新成4  第七章CAS(Compare and Swap)的解釋   *補全  第7章”事務”有提到  CAS 與ACID  cas(實例數值(內存數值),期望數值,更新數值)  = True or False     sequenceDiagram\n     participant T1\n     participant Memory\n     participant T2\n     Note over Memory: x=77\n   \n       T2->>Memory: 讀取數值 (Read x)\n     Memory->>T2: 返回 77\n     T1->>Memory: 讀取數值 (Read x)\n     Memory->>T1: 返回 77\n       \n   \n     Note over T1,T2: 開始競爭搶做cas更新\n       \n       Note over T1,Memory: T1先搶到開始做cas\n     T1->>Memory: CAS(77, 77, 78) \n     Note over Memory: 比較 77 == 77\n     Memory->>T1: CAS成功，更新為78\n   \n       Note over Memory: x=78\n     Note over Memory,T2: T2比T1晚一步做cas\n     T2->>Memory: CAS(78, 77, 7788)\n     Note over Memory: 比較 78 != 77\n     Memory->>T2: CAS失敗\n   \n     Note over Memory: 最終數值為 x=78\n  *重點：  CAS只在乎寫，而初始值的讀取可以是刻意先去讀或者是曾經讀過  沒有先讀的cas不能比較  test     type   CAS   struct   {\n       value   int\n   }\n   \n   func   (  c   *  CAS  )   CompareAndSwap  (  expected   int  ,   newValue   int  )   bool   {\n       if   c.value   ==   expected {\n           c.value   =   newValue\n           return   true\n       }\n       return   false\n   }\n  第7章說CAS是輕型事務所以用redis了解了一下事務  最後對於cas的理解就像是redis的watch會去監視某個key當某個key被併發寫入時就會發出錯誤停止事務     go   func  ()  {   //A\n       WATCH   x\n       MULTI\n       SET   x   \"newValueFromA\"\n       EXEC\n   }  ()\n   go   func  ()  {   //B\n       WATCH   x\n       MULTI\n       SET   x   \"newValueFromB\"\n       EXEC\n   }  ()\n   /*\n   2023/09/04   00:06:19   Goroutine   A   aborted:   redis:   transaction   failed\n   2023/09/04   00:06:19   Goroutine   B   succeeded\n   2023/09/04   00:06:19   Final   value   of   x:   newValueFromB\n   *  /\n  test     package   main\n   \n   import   (\n       \"  context  \"\n       \"  log  \"\n       \"  sync  \"\n   \n       \"  github.com/go-redis/redis/v8  \"\n   )\n   \n   func   main  () {\n       var   ctx   =   context.  Background  ()\n       rdb   :=   redis.  NewClient  (  &  redis  .  Options  {\n           Addr:       \"localhost:6379\"  ,\n           Password:   \"redis\"  ,\n       })\n   \n       var   wg   sync  .  WaitGroup\n       wg.  Add  (  2  )\n   \n       go   func  () { \n           defer   wg.  Done  ()\n   \n           err   :=   rdb.  Watch  (ctx,   func  (  tx   *  redis  .  Tx  )   error   {\n   \n               _, err   :=   tx.  TxPipelined  (ctx,   func  (  pipe   redis  .  Pipeliner  )   error   {\n                   pipe.  Set  (ctx,   \"x\"  ,   \"newValueFromA\"  ,   0  )\n                   return   nil\n               })\n   \n               return   err\n           },   \"x\"  )\n   \n           if   err   !=   nil   {\n               log.  Printf  (  \"Goroutine A aborted:   %v  \"  , err)\n           }   else   {\n               log.  Printf  (  \"Goroutine A succeeded\"  )\n           }\n       }()\n   \n       go   func  () { \n           defer   wg.  Done  ()\n   \n           err   :=   rdb.  Watch  (ctx,   func  (  tx   *  redis  .  Tx  )   error   {\n               _, err   :=   tx.  TxPipelined  (ctx,   func  (  pipe   redis  .  Pipeliner  )   error   {\n                   pipe.  Set  (ctx,   \"x\"  ,   \"newValueFromB\"  ,   0  )\n                   return   nil\n               })\n   \n               return   err\n           },   \"x\"  )\n   \n           if   err   !=   nil   {\n               log.  Printf  (  \"Goroutine B aborted:   %v  \"  , err)\n           }   else   {\n               log.  Printf  (  \"Goroutine B succeeded\"  )\n           }\n       }()\n   \n       wg.  Wait  ()\n   \n       value, err   :=   rdb.  Get  (ctx,   \"x\"  ).  Result  ()\n       if   err   !=   nil   {\n           log.  Fatalf  (  \"Could not get x:   %v  \"  , err)\n       }\n   \n       log.  Printf  (  \"Final value of x:   %v  \"  , value)\n   }\n   /*\n   2023/09/04 00:06:19 Goroutine A aborted: redis: transaction failed\n   2023/09/04 00:06:19 Goroutine B succeeded\n   2023/09/04 00:06:19 Final value of x: newValueFromB\n   */\n  依賴線性一致性  介紹：  線性一致性在什麼情況下有用？觀看體育比賽的最後得分可能是一個輕率的例子：滯後了幾秒鐘的結果不太可能在這種情況下造成任何真正的傷害。然而對於少數領域，線性一致性是系統正確工作的一個重要條件。    鎖定和領導選舉  在單主複製架構中，只有一個領導者（也叫做主節點）負責執行所有寫操作，以預防腦裂現象，即避免多個節點同時認為自己是領導者。為了協調哪個節點可以成為領導者，系統會採用分散式鎖。這種鎖必須要達到線性一致性，也就是所有節點必須共識到誰擁有這把鎖。協調服務像 Apache ZooKeeper 和 etcd 常被用來實現這樣的分散式鎖和領導者選舉。儘管它們提供了線性一致性的寫操作，讀取數據可能仍是過時的，因為這些讀取操作可能由任何一個副本完成。    約束和唯一性保證  在維持關聯式資料庫中的  PRIMARY KEY 和  UNIQUE 約束時，通常會使用線性一致性來確保所有節點達到共識。例如，在一個用戶註冊系統中，可能會選擇使用電郵作為唯一識別。如果沒有線性一致性，這可能導致資料不一致或用戶重複註冊。  對於更寬鬆的約束，如  FOREIGN KEY ，線性一致性不一定是必須的。以電商平台為例，  Orders  表與   Products  表是緊密相關的。新商品一旦加入   Products  表，便可能會立即在搜索結果中出現，但相對應的訂單處理（涉及   Orders  表）可能會因為庫存或價格等因素稍微延後。在這樣的情境下，最終一致性會是一個更靈活的選擇。    跨通道的時序依賴  圖9-1中有一個細節：如果Alice沒有喊德國贏了Bob就不會靠腰資料是舊的，是因為這個系統多了一個額外通道(通話)才導致發現線性不一致。  計算機系統也會出現類似的情況。例如，假設有一個網站，使用者可以上傳照片，一個後臺程序會調整照片大小，降低解析度以加快下載速度（縮圖）。該系統的架構和資料流如  圖 9-5 所示  假設有一個網站使用者上傳照片，後台會縮小照片降低解析度增加下載速度(縮圖)。     圖 9-5 Web 伺服器和影象縮放器透過檔案儲存和訊息佇列進行通訊，開啟競爭條件的可能性。  描述：  1.使用者上傳照片(1.jpg)到Web server  2.Web server將照片(UUID.jpg)放到File storage(/temp)儲存  3.將一個訊息push到MQ(UUID.jpg,/temp)  4.MQ通知Image resizer要縮小的檔案位置(/temp/UUID.jpg)  5.Image resizer讀取File storage的照片(/temp/UUID.jpg)  6.Image resizer處理完縮小照片後上傳到File storage(/result/UUID.jpg)給使用者下載  如果MQ比File stroage複製的更快，可能會導致步驟5出現舊的影像或沒有影像，如果某個節點處理是舊的影像則就產生了永久性的不一致。  出現這個問題是因為Web server和Image resizer之間存在不同的通道：File storage與MQ，這兩個通道之間有可能產生競爭條件。這種狀況類似Alice與Bob語音通訊與網頁更新也存在了競爭條件。  線性一致性並不是避免這種競爭條件的唯一方法，只是這種方式最容易理解，如果可以透過控制MQ則可以使用第五章(  Read-Your-Writes Consistency )類似的方法，但是會有而外的複雜代價。  第五章  Read-Your-Writes Consistency ~~ *補全~~  當使用者寫入一則評論，有可能重新整理頁面發現剛剛寫入的資料不見了(可能從其他節點讀取但是那個節點根本還沒有被Leader更新)，因此使用者困惑了可能會再次寫入剛剛的評論，所以要在寫入Leader的時候直接讀取使用者評論，這樣就可以避免使用者讀取到尚未最終一致性的資料。     圖 5-3 使用者寫入後從舊副本中讀取資料。需要寫後讀 (read-after-write) 的一致性來防止這種異常  描述：  1.User寫評論到Leader然後  2.使用者去Follower2讀取剛剛的comments並沒有剛剛寫入55555的評論  3.Leader過了一段時間才完成最終一致性這時候User才能取到真剛剛寫入的55555  總結：  可以利用寫入Leader寫入回傳的ok來處理讀自己寫的假資料至少讓他不要去Follower2去查看發現資料不對  實現線性一致的系統  介紹：  由於線性一致性本質上意味著 “表現得好像只有一個數據副本，而且所有的操作都是原子的”，所以最簡單的答案就是，真的只用一個數據副本。但是這種方法無法容錯：如果持有該副本的節點失效，資料將會丟失，或者至少無法訪問，直到節點重新啟動。  使系統容錯最常用的方法是使用複製。我們再來回顧    第五章  中的複製方法，並比較它們是否可以滿足線性一致性：   單主複製（可能線性一致）  在具有單主複製功能的系統中（請參閱 “   領導者與追隨者 ”），主庫具有用於寫入的資料的主副本，而追隨者在其他節點上保留資料的備份副本。如果從主庫或同步更新的從庫讀取資料，它們   可能（potential）  是線性一致性的   ^iv 。然而，實際上並不是每個單主資料庫都是線性一致性的，無論是因為設計的原因（例如，因為使用了快照隔離）還是因為在併發處理上存在錯誤【10】。  從主庫讀取依賴一個假設，你確切地知道領導者是誰。正如在 “   真相由多數所定義 ” 中所討論的那樣，一個節點很可能會認為它是領導者，而事實上並非如此 —— 如果具有錯覺的領導者繼續為請求提供服務，可能違反線性一致性【20】。使用非同步複製，故障切換時甚至可能會丟失已提交的寫入（請參閱 “   處理節點宕機 ”），這同時違反了永續性和線性一致性。  共識演算法（線性一致）  一些在本章後面討論的共識演算法，與單主複製類似。然而，共識協議包含防止腦裂和陳舊副本的措施。正是由於這些細節，共識演算法可以安全地實現線性一致性儲存。例如，Zookeeper 【21】和 etcd 【22】就是這樣工作的。  多主複製（非線性一致）  具有多主程式複製的系統通常不是線性一致的，因為它們同時在多個節點上處理寫入，並將其非同步複製到其他節點。因此，它們可能會產生需要被解決的寫入衝突（請參閱 “   處理寫入衝突 ”）。這種衝突是因為缺少單一資料副本所導致的。  無主複製（也許不是線性一致的）  對於無主複製的系統（Dynamo 風格；請參閱 “   無主複製 ”），有時候人們會聲稱透過要求法定人數讀寫(w+r>n)可以獲得 “強一致性”。這取決於法定人數的具體配置，以及強一致性如何定義（通常不完全正確）。  基於日曆時鐘（例如，在 Cassandra 中；請參閱 “   依賴同步時鐘 ”）的 “最後寫入勝利” 衝突解決方法幾乎可以確定是非線性一致的，由於時鐘偏差，不能保證時鐘的時間戳與實際事件順序一致。寬鬆的法定人數（請參閱 “   寬鬆的法定人數與提示移交 ”）也破壞了線性一致的可能性。即使使用嚴格的法定人數，非線性一致的行為也是可能的，如下節所示。  第五章無主複製   補全  使用者每次都讀寫多個複製件  讀寫法定人數：  公式 w+r>n 讀加寫要大於總結點  因爲n=3 w=2 r=2 所以2+2>3成立     圖 5-10 法定寫入，法定讀取，並在節點中斷後讀修復。  敘述：  1.UserA寫入N1 N2 ‘me-new.jpg’(因為N3死掉了)  2.UserB讀取N1 N2 N3 結果發現N3的版本是6其他是7  3.UserB就將N3 ’me-new.jpg’的version設定成7  結論：  無主複製就是要將監督機制直接寫在使用者身上(應用層)而不是讓資料庫自己去發現    線性一致性和法定人數  介紹：  直覺上無主複製模型中嚴格的法定人數應該是線性一致的。但是當可變的網路延遲就能產生競爭條件。     圖 9-6 非線性一致的執行，儘管使用了嚴格的法定人數  敘述：  w = 3 r = 2 n = 3   x=0  Writer 寫入x = 1 到 N1 N2 N3(最快寫入)  ReaderA 讀取N1(X) N2(x=0) N3(x=1)  ReaderB 讀取N1(x=0) N2(x=0) N3(X)  結論：  就算法定人數正確也無法保證線性一致，所以最安全的做法就是當作無主複製系統不能提供線性一致  線性一致性的代價  介紹：  一些複製方法可以提供線性一致性，另一些複製方法則不能，因此深入地探討線性一致性的優缺點是很有趣的。  我們已經在    第五章  中討論了不同複製方法的一些用例。例如對多資料中心的複製而言，多主複製通常是理想的選擇（請參閱 “   運維多個數據中心 ”）。   圖 9-7  說明了這種部署的一個例子。     圖 9-7 網路中斷迫使線性一致性和可用性之間做出選擇。  敘述：  多主資料庫：兩個datacenter還是可以單獨使用等到網路恢復後只需要排隊並交換  單主複製：從庫資料中心無法寫入也不能執行任何線性一致的讀取，但是還是可以不使用線性一致的讀取但是讀的資料可能是舊的。  結論：  整體來說不管多主單主都無法避免斷線帶來的線性不一致    CAP定理  介紹：  這個問題不僅僅是單主複製和多主複製的後果：任何線性一致的資料庫都有這個問題，不管它是如何實現的。這個問題也不僅僅侷限於多資料中心部署，而可能發生在任何不可靠的網路上，即使在同一個資料中心內也是如此。問題面臨的權衡如下：   如果應用需要線性一致性，且某些副本因為網路問題與其他副本斷開連線，那麼這些副本掉線時不能處理請求。請求必須等到網路問題解決，或直接返回錯誤。（無論哪種方式，服務都   不可用 ）。  如果應用不需要線性一致性，那麼某個副本即使與其他副本斷開連線，也可以獨立處理請求（例如多主複製）。在這種情況下，應用可以在網路問題解決前保持可用，但其行為不是線性一致的。  這兩種選擇有時分別稱為 CP（在網路分割槽下一致但不可用）和 AP（在網路分割槽下可用但不一致）。 但是，這種分類方案存在一些缺陷【9】，所以最好不要這樣用。  補充：  一致性（Consistency）:  所有節點在同一時刻可以看到相同的數據。  可用性（Availability）:  每次請求都會在有限的時間內收到一個明確的成功或失敗回應。  分區容錯性（Partition tolerance）:  即使網絡分區（Partition）發生，系統依然能正常運作。  CAP定理認為必須要在一致性與可用性作出權衡   可以有一個 CP（一致性和分區容錯性） 的系統，比如 ZooKeeper。  可以有一個 CA（一致性和可用性） 的系統，但這基本上只能在單一節點的情況下實現，一旦涉及到分布式，網絡分區是無法避免的。  可以有一個 AP（可用性和分區容錯性） 的系統，比如 Cassandra 或 Couchbase。    線性一致性和網路延遲  1.連cpu上的記憶體都不一定是線性一致性的：  如果一個 CPU 核上執行的執行緒寫入某個記憶體地址，而另一個 CPU 核上執行的執行緒不久之後讀取相同的地址，並沒有保證一定能讀到第一個執行緒寫入的值（除非使用了 記憶體屏障（memory barrier） 或 圍欄（fence）【44】）。  而且CPU核心與核心之間的通訊基本都是正常的狀況下還有可能發生線性不一致性。  2.以CPU來說是為了提高效能而選擇犧牲線性一致性而不是為了容錯  3.可能找不到高效並且線性一致性的系統  9.3 順序保證  介紹：  這小節主要要探討圖9-4順序   在    第五章  中我們看到，領導者在單主複製中的主要目的就是，在複製日誌中確定   寫入順序（order of write） —— 也就是從庫應用這些寫入的順序。如果不存在一個領導者，則併發操作可能導致衝突（請參閱 “   處理寫入衝突 ”）。  在    第七章  中討論的   可序列化 ，是關於事務表現的像按   某種先後順序（some sequential order）  執行的保證。它可以字面意義上地以   序列順序（serial order）  執行事務來實現，或者允許並行執行，但同時防止序列化衝突來實現（透過鎖或中止事務）。  在    第八章  討論過的在分散式系統中使用時間戳和時鐘（請參閱 “   依賴同步時鐘 ”）是另一種將順序引入無序世界的嘗試，例如，確定兩個寫入操作哪一個更晚發生。    順序與因果關係 (因果一致性 causally consistent)   5-5  一致字首讀：有因果關係     圖 5-5 如果某些分割槽的複製速度慢於其他分割槽，那麼觀察者可能會在看到問題之前先看到答案。  通常，問題和答案之間存在明確的因果關係，因為要回答一個問題，首先要有問題的存在。  因果關係例子：  在一個線上問答社區，通常需要先有人提出問題，然後其他人才能看到並回答這個問題。如果由於網絡延遲，有人先看到了回答而後看到問題，這違反了正常的因果順序。  5-9  複製和網路延遲：有因果關係     圖 5-9 使用多主複製時，寫入可能會以錯誤的順序到達某些副本。  在這個情境下，明確的因果關係是，一個記錄必須先被創建，然後才能被更新。  因果關係例子：  如果在家中的多台電腦上同步文件，通常必須先在一台電腦上創建文件，然後它才能被同步到其他電腦上。如果由於網絡延遲，文件的更新在某台電腦上先於文件的創建被接收到，這違反了正常的因果順序。   檢測併發寫入：有可能有因果關係     圖 5-12 併發寫入 Dynamo 風格的資料儲存：沒有明確定義的順序。  在這個情境下，可能存在因果關係，例如，如果操作A影響了操作B的結果，那麼就存在因果關係。但如果A和B是獨立並行的，則不存在因果關係。  因果關係例子：  在超市結賬時，如果兩個人同時到達結賬台，需要確定哪個人應該先結賬以保持秩序。這種順序決定了誰先誰後，存在因果關係。   事務快照隔離(  快照隔離和可重複讀 )：有因果關係     圖 7-6 讀取偏差：Alice 觀察資料庫處於不一致的狀態  不可重複讀，除非知道因果關係要不然重複讀取會產生不一致  因果關係例子：  查看社交媒體的時候看到了一個回覆，理所當然的，你會期望能夠看到被回覆的原始帖子，因為回覆（效果）依賴於原始帖子（原因）。   寫偏差(  寫入偏差與幻讀 ):可能有因果關係     圖 7-8 寫入偏差導致應用程式錯誤的示例  Alice依賴Bob的on_call狀態，Bob也依賴Alice的on_call狀態  寫偏差的情境下可能存在因果關係，例如，某個事務的結果可能依賴於另一個事務的狀態。  因果關係例子：  在一個忙碌的餐廳，如果兩個服務員同時去搶一個空出來的桌子給等待的客人，但由於沒有及時通信，可能會導致兩組客人被安排到同一個桌子，這是因果關係管理的失敗。   因果依賴和時序(Alice & Bob 看球賽)：有因果關係     圖 9-1 這個系統是非線性一致的，導致了球迷的困惑  在這個情境下，存在明確的因果關係，比如進球（原因）和球員慶祝（效果）。  因果關係例子：  在觀看足球比賽時，球員的慶祝是依賴於先前的進球事件。進球是原因，球員的慶祝是效果，二者之間存在明確的因果關係。  結論：    因果一致性 ：重視操作之間的因果關係。如果操作A在因果上先於操作B，那麼系統應保證所有節點上A的效果都會在B的效果之前被看到。這使得系統能夠保留操作的自然順序，但不保證全局的操作順序。   線性一致性 ：要求系統中的所有操作都能夠按照某一全局順序（通常是時間順序）來呈現，使得所有操作看起來就像是在一個單一的、原子的、全局順序的時間點上發生的。這提供了一個非常強的一致性保證，但可能會降低系統的性能和可用性。    因果順序不是全序的   全序(total order)：  允許任意兩個元素進行比較，所以如果有兩個元素，你總是可以說出哪個更大，哪個更小。例如，自然數集是全序的：給定兩個自然數，比如說 5 和 13，那麼你可以知道，13 大於 5。  偏序(partially ordered)：考慮三個集合：A={1,2}，B={2,3}，和C={1,2,3}。在這裡，A和B是無法比較的，因為它們之間沒有子集關係。但是，A和C可以比較，因為A是C的子集，記作A⊂C。  全序和偏序之間的差異反映在不同的資料庫一致性模型中：   線性一致性  線性一致的系統中，操作是全序的：如果系統表現的就好像只有一個數據副本，並且所有操作都是原子性的，這意味著對任何兩個操作，我們總是能判定哪個操作先發生。這個全序在    圖 9-4  中以時間線表示。  因果性  我們說過，如果兩個操作都沒有在彼此   之前發生 ，那麼這兩個操作是併發的（請參閱    “此前發生” 的關係和併發 ）。換句話說，如果兩個事件是因果相關的（一個發生在另一個事件之前），則它們之間是有序的，但如果它們是併發的，則它們之間的順序是無法比較的。這意味著因果關係定義了一個偏序，而不是一個全序：一些操作相互之間是有順序的，但有些則是無法比較的。併發是偏序的     graph TD;\n           全序\n       A[任務 A];\n       B[任務 B];\n       C[任務 C];\n       D[任務 D];\n       E[任務 E];\n       全序 --> A;\n       A --> B;\n       B --> C;\n       C --> D;\n       D --> E;\n   \n           偏序\n           A1[任務 A];\n       B1[任務 B];\n       C1[任務 C];\n       D1[任務 D];\n       E1[任務 E];\n       F1[任務 F];\n       \n           偏序-->A1;\n       A1 --> B1;\n       B1 --> D1;\n       A1 --> C1;\n       C1 --> E1;\n       D1 --> F1;\n       E1 --> F1;\n  如果你熟悉像 Git 這樣的分散式版本控制系統，那麼其版本歷史與因果關係圖極其相似。通常，一個   提交（Commit）  發生在另一個提交之後，在一條直線上。但是有時你會遇到分支（當多個人同時在一個專案上工作時），  合併（Merge）  會在這些併發建立的提交相融合時建立。因為知道develop發生過什麼事情再去看5-22e4a46才知道是全序的     gitGraph:\n   options\n   {\n       \"nodeSpacing\": 150,\n       \"nodeRadius\": 10\n   }\n   end\n   commit\n   commit\n   branch develop\n   checkout develop\n   commit\n   commit\n   checkout main\n   merge develop\n   commit\n   commit\n    線性一致性強於因果一致性  線性一致性隱含了因果關係，也就是說，任何線性一致的系統都能正確地保持因果性，即使在有多個通訊通道的情況下也能自動保證，無需進行特殊的操作如傳遞時間戳。這種隱含的關係使得線性一致性成為了一個非常強大而又直觀的一致性模型。  許多分散式系統看起來需要線性一致性但實際上只要保證網路不延遲其實使用因果一致性就算是最強的一致性模型。  捕獲因果關係  為了保持因果性，系統必須能夠識別操作間的“發生在先前”（happened before）關係。這形成了一種偏序（partial ordering）關係，意味著在某些情況下，可以明確地說一個操作發生在另一個操作之前，而在其他情況下（如併發操作），則無法做出此判定。  為了明確因果依賴，需要有方式描述系統中各節點的“知識”。這種“知識”是指節點在進行某些操作時，對系統中其他事件的了解。例如，如果一個節點在發出寫入Y的請求時已經看到了X的值，那麼就可能存在一個因果關係，表示X是Y發生的原因。這種情況類似於在欺詐指控的刑事調查中常見的問題，例如在做出某個決定Y時，CEO是否知道了X的情況。在這種情況下，X和Y之間的因果關係是基於節點（或在刑事調查中的CEO）在執行操作時所具有的知識。這種分析方式可以幫助確定事件之間的因果依賴，並且對於維持系統的因果一致性至關重要。  為了確定因果順序，資料庫需要知道應用讀取了哪個版本的資料。這就是為什麼在    圖 5-13  中，來自先前操作的版本號在寫入時被傳回到資料庫的原因。在 SSI 的衝突檢測中會出現類似的想法，如 “   可序列化快照隔離 ” 中所述：當事務要提交時，資料庫將檢查它所讀取的資料版本是否仍然是最新的。為此，資料庫跟蹤哪些資料被哪些事務所讀取。  結論：  我們可以從資料庫的版本“知識點”來捕獲因果關係，由於這個知識是找出因果的關鍵所以可能會製造出衝突讓多節點系統保持一致性    序列號順序  雖然因果是一個重要的理論概念，但是要跟蹤所有的因果關係是不切實際的，還好有一個更好的方法，我們可以使用   序列號（sequence number）  或   時間戳（timestamp）  來排序事件，時間戳不一定要來自於日曆時鐘，可以用使用邏輯時鐘，因為日曆時鐘會有“不可靠時鐘”問題，典型的邏輯時鐘實現是使用每次操作自增計數器。  使用自增計數器可以提供比較值越大的後發生，我們可以使用因果一致的全序來產生序列號，這樣就可以產生一個比因果性更嚴格的順序。  一個因果關係不一致的全序很容易建立但是是垃圾，例如我們隨機產生了一個UUID但是這個序列根本沒辦法為我們提供操作的先後順序，或者是否觀察的出來操作是否是併發的。  在單主複製的資料庫中，複製日誌定義因果一致的寫操作，主庫可以為每個動作採用自增計數，而從每個操作分配一個遞增的序列號。如果一個從庫按照複製日誌的順序來寫入，這樣一來從庫的狀態會始終是因果一致的。就算從庫落後於領導者也能保持因果一致，因為有複製日誌的序列讓從庫知道先後順序該如何寫入。     sequenceDiagram\n       participant 主庫\n       participant 複製日誌\n       participant 從庫\n   \n       主庫->>複製日誌: 操作1 (序列號: 1)\n       複製日誌-->>從庫: 操作1 (序列號: 1)\n       主庫->>複製日誌: 操作2 (序列號: 2)\n       複製日誌-->>從庫: 操作2 (序列號: 2)\n       主庫->>複製日誌: 操作3 (序列號: 3)\n       複製日誌-->>從庫: 操作3 (序列號: 3)\n    非因果序列號生成器  如果主庫不存在（可能因為使用了多主資料庫或無主資料庫，或者因為使用了分割槽的資料庫），如何為操作生成序列號就沒有那麼明顯了。在實踐中有各種各樣的方法：   每個節點都可以生成自己獨立的一組序列號。例如有兩個節點，一個節點只能生成奇數，而另一個節點只能生成偶數。通常，可以在序列號的二進位制表示中預留一些位，用於唯一的節點識別符號，這樣可以確保兩個不同的節點永遠不會生成相同的序列號。     sequenceDiagram\n       participant Node1 as 節點1 (奇數序列號)\n       participant Node2 as 節點2 (偶數序列號)\n       Note over Node1,Node2: 每個節點可以生成自己獨立的一組序列號\n       Node1->>Node2: 操作1 (序列號: 1)\n       Node2->>Node1: 操作2 (序列號: 2)\n       Node1->>Node2: 操作3 (序列號: 3)\n       Node2->>Node1: 操作4 (序列號: 4)\n       Note over Node1,Node2: 兩個不同的節點永遠不會生成相同的序列號\n  可以將日曆時鐘（物理時鐘）的時間戳附加到每個操作上【55】。這種時間戳並不連續，但是如果它具有足夠高的解析度，那也許足以提供一個操作的全序關係。這一事實應用於* 最後寫入勝利 * 的衝突解決方法中（請參閱 “   有序事件的時間戳 ”）。\n”最後只有操作三可以成功寫入”     sequenceDiagram\n       participant Node1 as 節點1\n       participant Node2 as 節點2\n       Note over Node1,Node2: 操作依賴物理時鐘\n       Node1->>Node2: 操作1 (時間戳: 08:00:00)\n       Node2->>Node1: 操作2 (時間戳: 08:01:00)\n       Node1->>Node2: 操作3 (時間戳: 08:02:00)\n       Note over Node1, Node2: 最後只有操作三可以成功寫入\n  可以預先分配序列號區塊。例如，節點 A 可能要求從序列號 1 到 1,000 區塊的所有權，而節點 B 可能要求序列號 1,001 到 2,000 區塊的所有權。然後每個節點可以獨立分配所屬區塊中的序列號，並在序列號告急時請求分配一個新的區塊。     sequenceDiagram\n       participant NodeA as 節點A\n       participant NodeB as 節點B\n       Note over NodeA,NodeB: 預先分配序列號區塊\n       NodeA->>NodeB: 操作1 (序列號: 500)\n       NodeB->>NodeA: 操作2 (序列號: 1500)\n       NodeA->>NodeB: 操作3 (序列號: 800)\n       NodeB->>NodeA: 操作4 (序列號: 1600)\n  這三個選項都比單一主庫的自增計數器表現要好，並且更具可伸縮性。它們為每個操作生成一個唯一的，近似自增的序列號。然而它們都有同一個問題：生成的序列號與因果不一致。   每個節點每秒可以處理不同數量的操作。因此，如果一個節點產生偶數序列號而另一個產生奇數序列號，則偶數計數器可能落後於奇數計數器，反之亦然。如果你有一個奇數編號的操作和一個偶數編號的操作，你無法準確地說出哪一個操作在因果上先發生。     sequenceDiagram\n       participant Node1 as 節點1 (奇數序列號)\n       participant Node2 as 節點2 (偶數序列號)\n       \n       Note over Node1,Node2: 每個節點每秒可以處理不同數量的操作\n       \n       Node1->>Node2: 操作1 (序列號: 1, 時間: 08:00:00)\n       Note over Node1: 節點1 快速處理操作\n       Node1->>Node2: 操作3 (序列號: 3, 時間: 08:00:01)\n       \n       Note over Node2: 節點2 較慢地處理操作\n       Node2->>Node1: 操作2 (序列號: 2, 時間: 08:00:05)\n       Node2->>Node1: 操作4 (序列號: 4, 時間: 08:00:10)\n       \n       Note over Node1,Node2: 無法確定操作2和操作3哪個在因果上先發生\n  來自物理時鐘的時間戳會受到時鐘偏移的影響，這可能會使其與因果不一致。例如    圖 8-3  展示了一個例子，其中因果上晚發生的操作，卻被分配了一個更早的時間戳。可以使物理時鐘時間戳與因果關係保持一致：在 “   全域性快照的同步時鐘 ” 中，我們討論了 Google 的 Spanner，它可以估計預期的時鐘偏差，並在提交寫入之前等待不確定性間隔。這種方法確保了實際上靠後的事務會有更大的時間戳。但是大多數時鐘不能提供這種所需的不確定性度量。     sequenceDiagram\n       participant Node1 as 節點1\n       participant Node2 as 節點2\n       Note over Node1,Node2: 時鐘偏移影響\n       Node1->>Node2: 操作1 (時間戳: 10:00:01)\n       Node2->>Node1: 操作2 (時間戳: 10:00:00, 因果上晚於操作1)\n       Note over Node1,Node2: 因果關係與時間戳不一致\n  在分配區塊的情況下，某個操作可能會被賦予一個範圍在 1,001 到 2,000 內的序列號，然而一個因果上更晚的操作可能被賦予一個範圍在 1 到 1,000 之間的數字。這裡序列號與因果關係也是不一致的。     sequenceDiagram\n       participant NodeA as 節點A\n       participant NodeB as 節點B\n       Note over NodeA,NodeB: 序列號與因果不一致\n       NodeA->>NodeB: 操作1 (序列號: 1500)\n       NodeB->>NodeA: 操作2 (序列號: 500, 因果上晚於操作1)\n       Note over NodeA,NodeB: 因果關係與序列號不一致\n    蘭伯特時間戳  儘管剛才描述的三個序列號生成器與因果不一致，但實際上有一個簡單的方法來產生與因果關係一致的序列號。它被稱為蘭伯特時間戳，萊斯利・蘭伯特（Leslie Lamport）於 1978 年提出【56】     圖 9-8 Lamport 時間戳提供了與因果關係一致的全序。  目標：  Client(Count節點計數器,Node節點名稱) → 使用者攜帶個節點最大值並且在操作兩個節點時更新節點最大值  解說：  每次被操作Node1 與 Node2都會c都會+1，ClientA先存取Node1所以Node1 c=1 中間一段時間都是ClientB存取Node2不管存取幾次因為Node2的C持續的增加，只要ClinetA某次存取到Node2就可以因為讀取到Node2的c而去更新自己的最大值  EX:ClientA(1,1)→ClientA(5,2)→因為曾經讀取過Node2的最大計數值所以ClientA可以更新Node1的c=6，最後產生ClientA(6,1)\n因為使用這樣的計數方式可以看得出彼此的因果關係     ClinetA  ClientB    (1,1)  (1,2)   (5,2)  (2,2)   (6,1)  (3,2)    (4,2)    (6,2)  就算不看圖以資料形式看待也大概猜的的出來兩個Node之間有版本差異    光有時間戳排序還不夠  雖然蘭伯特時間戳定義了一個與因果一致的全序，但它還不足以解決分散式系統中的許多常見問題。  例如：  同時間有兩個使用者要創建同一個名稱的帳號Alice     sequenceDiagram\n       participant User1 as 使用者1\n       participant User2 as 使用者2\n       participant NodeA as 節點A\n       participant NodeB as 節點B\n   \n       Note over User1,User2: 嘗試創建相同的使用者名稱\n   \n       User1->>NodeA: 請求創建帳戶 (使用者名稱: Alice)\n       User2->>NodeB: 請求創建帳戶 (使用者名稱: Alice)\n   \n       Note over NodeA,NodeB: 生成蘭伯特時間戳\n   \n       NodeA->>NodeB: 通知創建請求 (使用者名稱: Alice, 蘭伯特時間戳: L1)\n       Note right of NodeB: 網路延遲\n   \n       NodeB->>NodeA: 通知創建請求 (使用者名稱: Alice, 蘭伯特時間戳: L2)\n       Note right of NodeA: 網路延遲\n   \n       Note over NodeA,NodeB: 由於網路問題，節點無法準確比較蘭伯特時間戳，無法確定哪個請求先來。\n   \n       alt 網路恢復\n           NodeA->>NodeB: 重新通知創建請求 (使用者名稱: Alice, 蘭伯特時間戳: L1)\n           NodeB->>NodeA: 重新通知創建請求 (使用者名稱: Alice, 蘭伯特時間戳: L2)\n           Note over NodeA,NodeB: 現在可以比較蘭伯特時間戳，但已經太遲了。\n       else 網路未恢復\n           Note over NodeA,NodeB: 系統被拖至停機，無法解決請求。\n       end\n    請求發送:  使用者1和使用者2分別向節點A和節點B發送請求，試圖創建使用相同名稱的帳戶。   生成蘭伯特時間戳:  節點A和節點B在接收到請求時生成蘭伯特時間戳。假設節點A生成的時間戳為L1，節點B生成的時間戳為L2。   嘗試通知:  節點A嘗試通知節點B它的請求和時間戳L1，而節點B嘗試通知節點A它的請求和時間戳L2。   網路延遲:  由於網路延遲或其他網路問題，這些通知可能會延遲到達，或者可能根本無法到達(節點A與節點B之間的網路延遲)。這使得節點之間無法立即比較蘭伯特時間戳，也就無法確定哪個請求先到來。   網路恢復（如果恢復的話）:  如果網路最終恢復，節點A和節點B可以重新通知對方它們的請求和蘭伯特時間戳。現在，它們可以比較蘭伯特時間戳來確定哪個請求先到來。但是，這可能已經太遲了，因為使用者可能已經不再等待，或者系統可能已經因其他原因而進入不一致的狀態。   網路未恢復:  如果網路未能恢復，節點A和節點B無法確定哪個請求先到來，這可能導致系統被拖至停機，無法解決這些請求。  結論：  即使使用了蘭伯特時間戳（Lamport Timestamps）來確定全序關係，仍然會因為操作的不確定性導致並不完全全序。只有在所有的操作都被收集後，操作的全序才會出現。如何確定全序關係已經塵埃落定，這將在    全序廣播  一節中詳細說明。    全序廣播  介紹：    單CPU核心的操作順序 ：當程式在單個CPU核心上運行時，定義操作的全序相對簡單，因為操作的順序就是它們被CPU執行的順序。   分散式系統的操作順序 ：在分散式系統中，各節點可能在不同的時間執行操作，使得確定全域性操作順序變得困難。   時間戳或序列號排序的局限性 ：雖然可以通過時間戳或序列號來排序操作，但是這種方法不如單主複製有效，尤其是在需要確保唯一性時。   單主複製 ：單主複製通過選擇一個節點作為主庫來確定操作的全序，並在主庫的單個CPU核心上對所有操作進行排序。   吞吐量和故障恢復的挑戰 ：\n   如果系統的吞吐量需求超過了單個主庫的處理能力，則需要考慮如何擴展系統。  如果主庫失效，則需要考慮如何進行故障切換，以保證系統的持續運行。   全序廣播和原子廣播 ：這兩種技術旨在解決分散式系統中的全序問題，確保所有節點都以相同的順序看到所有操作，即使在面對節點失效或網路問題的時候也能保持一致性。  全序廣播必須滿足兩個條件：  1.可靠交付  沒有訊息丟失：如果訊息被傳遞到一個節點，它將被傳遞到所有節點。  2.全序交付  訊息以相同的順序傳遞給每個節點。  正確的全序廣播演算法必須始終保證可靠性和有序性，即使節點或網路出現故障。當然在網路中斷的時候，訊息是傳不出去的，但是演算法可以不斷重試，以便在網路最終修復時，訊息能及時透過並送達（當然它們必須仍然按照正確的順序傳遞）。    使用全序廣播    共識服務與全序廣播 ：\n   類似 ZooKeeper 和 etcd 的共識服務實際上實現了全序廣播，顯示了全序廣播與共識之間的密切聯繫。   資料庫複製 ：\n   全序廣播是資料庫複製所需的，每個訊息代表一次資料庫的寫入，所有副本按相同的順序處理相同的寫入，保證副本間的一致性，這被稱為狀態機複製。   可序列化的事務 ：\n   使用全序廣播可以實現可序列化的事務，每個訊息代表一個確定性事務，並且每個節點以相同的順序處理這些訊息，保證資料庫的分割槽和副本的一致性。   訊息順序的固化 ：\n   在全序廣播中，訊息順序在訊息送達時被固化，不允許追溯地將訊息插入順序中的較早位置，使全序廣播比時間戳排序更強。   日誌的建立 ：\n   全序廣播也是建立日誌的一種方式，傳遞訊息就像追加寫入日誌，所有節點必須以相同的順序傳遞相同的訊息，以確保所有節點都可以讀取日誌並看到相同的訊息序列。   防護令牌的鎖服務 ：\n   在實現提供防護令牌的鎖服務中，全序廣播也非常有用。每個獲取鎖的請求都作為一條訊息追加到日誌末尾，並按日誌中的順序依次編號，這些序列號可以作為防護令牌使用。    使用全序廣播實現線性一致的儲存    基於全序廣播建立線性一致儲存 ：\n   全序廣播保證訊息以固定的順序可靠地傳送，而線性一致性則保證讀取能看見最新的寫入值。基於全序廣播，可以構建線性一致的儲存系統。   利用 CAS (比較並設定) 原子操作 ：\n   對每個可能的使用者名稱，創建一個帶有 CAS 原子操作的線性一致暫存器，以確保使用者名稱的唯一性。   僅追加日誌的全序廣播 ：\n   以僅追加日誌的形式實現全序廣播，並通過在日誌中追加和讀取訊息來實現線性一致的 CAS 操作。   處理併發寫入 ：\n   由於日誌項以相同的順序送達所有節點，可以選擇衝突寫入中的第一個作為勝利者，並中止後來者，以此確保所有節點對某個寫入是提交還是中止達成一致。   線性一致的讀取 ：\n   為了使讀取也線性一致，可以選擇：\n   在日誌中追加訊息並等待該訊息被讀回來執行實際的讀取操作；  查詢最新日誌訊息的位置，等待所有訊息傳達後再執行讀取；  或從同步更新的副本中進行讀取以確保結果是最新的。  結論：  全序廣播就是確保所有電腦（節點）收到的訊息順序都是一樣的，好比是把訊息按照1、2、3、4的順序發給每台電腦，讓每台電腦都按照這個順序來處理事情。這樣做的好處是，當多人同時要求做某件事（比如同時要求建立同一個使用者名稱的帳戶）時，全序廣播可以確定一個固定的順序來處理這些要求，避免亂七八糟的情況發生。也能確保即便網路有延遲，訊息還是能按照正確的順序被送達和處理。全序廣播也能支援一些特定的操作，比如比較然後設定（CAS），確保這些操作能正確無誤地完成。最後，全序廣播通過讓所有電腦按照相同的順序來處理事情，使得每台電腦的狀態都能保持一致。所以，全序廣播是實現線性一致儲存（讓所有電腦的資料保持一致）的一個重要方法。    使用線性一致性儲存實現全序廣播    利用線性一致性儲存實現全序廣播的方法 ：\n   利用線性一致的暫存器儲存一個整數，並提供原子的自增並返回操作或原子CAS操作。  對於每個要透過全序廣播發送的訊息，首先執行自增並返回操作，獲得一個序列號，然後將此序列號附加到訊息中。  之後將訊息傳送到所有節點，收件人按照序列號的順序依序傳遞訊息。   與蘭伯特時間戳的區別 ：\n   自增線性一致性暫存器獲得的是一個沒有間隙的序列，與蘭伯特時間戳的隨機序列不同，這是全序廣播和時間戳排序間的關鍵區別。   實現困難 ：\n   在單節點情況下實現線性一致性暫存器相對簡單，但在分散式環境中，特別是當節點出錯或網路連接中斷時，實現變得困難。  要實現原子性的自增並返回操作，需要深入思考線性一致性的序列號生成器，並可能需要一個共識演算法。   與共識問題的關聯 ：\n   線性一致的CAS或自增並返回暫存器與全序廣播都與共識問題等價，意味著解決了其中一個問題，就可以轉化為解決其他問題的方案。  結論：  利用線性一致性儲存來做全序廣播就是，先有一個特別的儲存空間，它能夠自動增加一個數字，每當有新訊息要發送時，就先讓這個數字加一，然後把這個數字貼到訊息上，接著把訊息發給所有的電腦節點。這樣，每個電腦節點收到訊息時，就按照貼在訊息上的數字順序來處理它們。如果發現有數字跳號了，就先等一下，直到收到缺的那個訊息。如果在這過程中，有電腦節點出了問題或是網路有問題，也有辦法找回並繼續增加那個數字，保證不會亂掉順序。  兩者比較：  使用全序廣播實現線性一致的儲存，就像是有一個郵差負責把信依照特定的順序送到大家手上，每個人都按照收信的順序來讀信，這樣就保證了大家都有相同的理解。這個郵差就像是全序廣播，他保證信件按照一定的順序送到每個人手上，而每個人的儲存空間就像是他們手中的信件，按照順序來讀和理解。  使用線性一致性儲存實現全序廣播，就像是大家都有一個共用的信箱，每個人都可以在裡面放信，但信箱有一個特殊的鎖，只允許按照一定的順序打開來取信。這個信箱就是線性一致性儲存，它保證大家按照一定的順序來放入和取出信件，而全序廣播就是透過這個信箱來保證信件的順序。  簡單來說，前者是靠郵差（全序廣播）來保證信件順序，讓儲存保持一致；後者是靠信箱（線性一致性儲存）來保證信件順序，進而實現全序廣播。  蘭伯特時間戳VS全序廣播     sequenceDiagram\n       participant User1 as 使用者1\n       participant User2 as 使用者2\n       participant NodeA as 節點A\n       participant NodeB as 節點B\n   \n       Note over User1,User2: 嘗試創建相同的使用者名稱\n   \n       User1->>NodeA: 請求創建帳戶 (使用者名稱: Alice)\n       User2->>NodeB: 請求創建帳戶 (使用者名稱: Alice)\n   \n       Note over NodeA,NodeB: 生成蘭伯特時間戳\n   \n       NodeA->>NodeB: 通知創建請求 (使用者名稱: Alice, 蘭伯特時間戳: L1)\n       Note right of NodeB: 網路延遲\n   \n       NodeB->>NodeA: 通知創建請求 (使用者名稱: Alice, 蘭伯特時間戳: L2)\n       Note right of NodeA: 網路延遲\n   \n       Note over NodeA,NodeB: 由於網路問題，節點無法準確比較蘭伯特時間戳，無法確定哪個請求先來。\n   \n       alt 網路恢復\n           NodeA->>NodeB: 重新通知創建請求 (使用者名稱: Alice, 蘭伯特時間戳: L1)\n           NodeB->>NodeA: 重新通知創建請求 (使用者名稱: Alice, 蘭伯特時間戳: L2)\n           Note over NodeA,NodeB: 現在可以比較蘭伯特時間戳，但已經太遲了。\n       else 網路未恢復\n           Note over NodeA,NodeB: 系統被拖至停機，無法解決請求。\n       end\n     sequenceDiagram\n       participant User1 as 使用者1\n       participant User2 as 使用者2\n       participant NodeA as 節點A\n       participant NodeB as 節點B\n       participant TotalOrderBroadcast as 全序廣播服務\n   \n       Note over User1,User2: 嘗試創建相同的使用者名稱\n   \n       User1->>NodeA: 請求創建帳戶 (使用者名稱: Alice)\n       User2->>NodeB: 請求創建帳戶 (使用者名稱: Alice)\n   \n       NodeA->>TotalOrderBroadcast: 發送創建請求 (使用者名稱: Alice)\n       NodeB->>TotalOrderBroadcast: 發送創建請求 (使用者名稱: Alice)\n   \n       Note over TotalOrderBroadcast: 全序廣播服務確保所有節點看到相同順序的消息\n   \n       TotalOrderBroadcast->>NodeA: 傳遞創建請求 (使用者名稱: Alice, 順序: 1)\n       TotalOrderBroadcast->>NodeB: 傳遞創建請求 (使用者名稱: Alice, 順序: 1)\n   \n       TotalOrderBroadcast->>NodeA: 傳遞創建請求 (使用者名稱: Alice, 順序: 2)\n       TotalOrderBroadcast->>NodeB: 傳遞創建請求 (使用者名稱: Alice, 順序: 2)\n   \n       Note over NodeA,NodeB: 節點根據全序廣播服務的順序處理請求\n   \n       alt 順序1的請求成功，順序2的請求失敗\n           NodeA->>User1: 回應請求成功\n           NodeB->>User2: 回應請求失敗\n       else 順序2的請求成功，順序1的請求失敗\n           NodeA->>User1: 回應請求失敗\n           NodeB->>User2: 回應請求成功\n       end\n  9.4 分散式事務與共識  介紹：  共識：目標是讓幾個節點達成一致  領導選舉：  單主複製的資料庫中所有節點需要與某個領導者達成一致，如果一些節點由於網路而故障可能會對領導權歸屬引起爭議。所以共識對於避免錯誤的故障切換至關重要，錯誤的切換會導致兩個節點都認為自己是領導者(腦裂現象)。  原子提交：  在多節點或多分割槽的資料庫中，為保持事務的原子性（全部提交或全部回滾），所有節點需對事務結果達成共識，此問題稱為原子提交。原子提交與共識稍有區別但可相互簡化，非阻塞的原子提交比共識更為困難。  共識的不可能性  介紹：  FLP結果指出，在有節點可能崩潰風險的情況下，無法總是達成共識。這個結果是基於非同步系統模型。然而，若演算法能使用超時或其他方法來識別可能崩潰的節點，或者利用隨機數，共識成為可能。儘管FLP展示了理論上的限制，但現實中的分散式系統通常能達成共識。在探討原子提交問題時，兩階段提交（2PC）演算法常被用來解決，它是一種共識演算法，但不是最佳的。通過學習2PC，可以進一步探索更好的一致性演算法，如ZooKeeper的Zab和etcd的Raft。  原子提交與兩階段提交  介紹：  原子性：  事務的方式通常都是用撤銷或丟棄的方式來解決衝突，原子性可以防止失敗的事物攪亂資料庫，避免了資料庫陷入半成品結果與半更新。  這對多物件事務和維護次級索引的資料庫非常重要，每個次級索引都是與主資料分離的資料結構，因此如果修改了一些資料，則還需要在次級索引中進行相應的更改。  原子性可以確保次級索引與主資料庫保持一致。  從單節點到分散式原子提交  介紹：  在單個數據庫節點執行的事務，原子性通常由儲存引擎實現。  當客戶端請求資料庫節點提交事務時，資料庫將使事務的寫入持久化（通常在預寫式日誌中，請參閱 “   讓 B 樹更可靠 ”），然後將提交記錄追加到磁碟中的日誌裡。如果資料庫在這個過程中間崩潰，當節點重啟時，事務會從日誌中恢復：如果提交記錄在崩潰之前成功地寫入磁碟，則認為事務被提交；否則來自該事務的任何寫入都被回滾。     sequenceDiagram\n       participant Client as 客戶端\n       participant Database as 資料庫節點\n       participant WAL as 預寫式日誌\n       participant Disk as 磁碟\n   \n       Client->>Database: 提交事務請求\n       Database->>WAL: 寫入事務資料\n       WAL-->>Database: 確認寫入\n       Database->>Disk: 將提交記錄追加到日誌\n       Disk-->>Database: 確認寫入\n       Database-->>Client: 提交成功\n   \n       alt 崩潰然後重啟\n           Database->>Disk: 檢查日誌\n           alt 提交記錄已寫入\n               Disk-->>Database: 事務已提交\n           else 提交記錄未寫入\n               Disk-->>Database: 回滾事務\n           end\n       end\n  單節點：  事務的提交主要取決於資料持久化落盤的   順序 ：首先是資料，然後是提交記錄。事務提交或終止的關鍵決定時刻是磁碟完成寫入提交記錄的時刻：在此之前，仍有可能中止（由於崩潰），但在此之後，事務已經提交（即使資料庫崩潰）。因此，是單一的裝置（連線到單個磁碟的控制器，且掛載在單臺機器上）使得提交具有原子性。  多節點：  僅向所有節點發送提交請求並獨立提交每個節點的事務是不夠的。這樣很容易發生違反原子性的情況：提交在某些節點上成功，而在其他節點上失敗：   某些節點可能會檢測到違反約束或衝突，因此需要中止，而其他節點則可以成功進行提交。假設某個節點已經有某個唯一鍵所以被迫停止某些節點因為沒有違反則成功  某些提交請求可能在網路中丟失，最終由於超時而中止，而其他提交請求則通過。順序會有問題  在提交記錄完全寫入之前，某些節點可能會崩潰，並在恢復時回滾，而其他節點則成功提交。  如果某些節點提交了事務，但其他節點卻放棄了這些事務，那麼這些節點就會彼此不一致。而且一旦在某個節點上提交了一個事務，如果事後發現它在其它節點上被中止了，它是無法撤回的。出於這個原因，一旦確定事務中的所有其他節點也將提交，節點就必須進行提交。     sequenceDiagram\n       participant 節點1\n       participant 節點2\n       participant 節點3\n   \n       節點1->>節點2: 嘗試提交事務\n       節點1->>節點3: 嘗試提交事務\n       Note right of 節點3: 事務已提交\n       節點2->>節點1: 放棄事務\n       Note right of 節點1: 事務已放棄\n       Note left of 節點2: 節點間存在不一致狀態\n  事務提交必須是不可撤銷的 ，事務提交之後，不能改變主意，並追溯性地中止事務。這個規則的原因是，一旦資料被提交，其結果就對其他事務可見，因此其他客戶端可能會開始依賴這些資料。這個原則構成了   讀已提交  隔離等級的基礎，在 “   讀已提交 ” 一節中討論了這個問題。如果一個事務在提交後被允許中止，所有那些讀取了   已提交卻又被追溯宣告不存在資料  的事務也必須回滾。     sequenceDiagram\n       participant 節點\n       participant 客戶端1\n       participant 客戶端2\n   \n       客戶端1->>節點: 提交事務\n       Note right of 節點: 事務已提交\n       節點->>客戶端2: 通知已提交事務\n       客戶端2->>節點: 讀取已提交數據\n       Note right of 節點: 事務被撤銷（不允許）\n       節點->>客戶端1: 通知事務被撤銷（錯誤）\n       節點->>客戶端2: 通知事務被撤銷（錯誤）\n  雖然還是有辦法在事務事後執行一個補償機制來取消，但從資料庫的角度，跨事務正確性的保證都必須要是應用自己處理。  兩階段提交簡介  介紹：   兩階段提交（two-phase commit）  是一種用於實現跨多個節點的原子事務提交的演算法，即確保所有節點提交或所有節點中止。它是分散式資料庫中的經典演算法。2PC 在某些資料庫內部使用，也以   XA 事務  的形式對應用可用（例如 Java Transaction API 支援）或以 SOAP Web 服務的   WS-AtomicTransaction  形式提供給應用。    說明了 2PC 的基本流程。2PC 中的提交 / 中止過程分為兩個階段（因此而得名），而不是單節點事務中的單個提交請求。  使用者經過協調者進行通訊，而協調者會進行兩個步驟  1.先跟節點溝通說要寫入資料  2.問各節點準備好了?  3.認後提交  協調者(事務管理器):  1.通常會在多節點事務出現  2.通常以庫的方式實現(Java EE)，也可以像是程式或是服務的方式Narayana、JOTM、BTM 或 MSDTC。  參與者:  1.資料庫節點在於2PC通常會稱為餐與者  比喻:  西方傳統結婚儀式  1.牧師先問雙方是否要結婚(雙方是否都在)  2.雙方回復”我願意”  3.牧師宣布正式成為夫妻  系統承諾  2PC之所以可以保證原子性解說:   當應用想要啟動一個分散式事務時，它向協調者請求一個事務 ID。此事務 ID 是全域性唯一的。  應用在每個參與者上啟動單節點事務，並在單節點事務上捎帶上這個全域性事務 ID。所有的讀寫都是在這些單節點事務中各自完成的。如果在這個階段出現任何問題（例如，節點崩潰或請求超時），則協調者或任何參與者都可以中止。  當應用準備提交時，協調者向所有參與者傳送一個   準備  請求，並打上全域性事務 ID 的標記。如果任意一個請求失敗或超時，則協調者向所有參與者傳送針對該事務 ID 的中止請求。  參與者收到準備請求時，需要確保在任意情況下都的確可以提交事務。這包括將所有事務資料寫入磁碟（出現崩潰、電源故障或硬碟空間不足都不能是稍後拒絕提交的理由）以及檢查是否存在任何衝突或違反約束。透過向協調者回答 “是”，節點承諾，只要請求，這個事務一定可以不出差錯地提交。換句話說，參與者放棄了中止事務的權利，但沒有實際提交。  當協調者收到所有準備請求的答覆時，會就提交或中止事務作出明確的決定（只有在所有參與者投贊成票的情況下才會提交）。協調者必須把這個決定寫到磁碟上的事務日誌中，如果它隨後就崩潰，恢復後也能知道自己所做的決定。這被稱為   提交點（commit point） 。  一旦協調者的決定落盤，提交或中止請求會發送給所有參與者。如果這個請求失敗或超時，協調者必須永遠保持重試，直到成功為止。沒有回頭路：如果已經做出決定，不管需要多少次重試它都必須被執行。如果參與者在此期間崩潰，事務將在其恢復後提交 —— 由於參與者投了贊成，因此恢復後它不能拒絕提交。     sequenceDiagram\n       participant App as 應用\n       participant Coordinator as 協調者\n       participant Node1 as 參與者1\n       participant Node2 as 參與者2\n       App->>Coordinator: 請求事務ID\n       Coordinator->>App: 返回事務ID\n       App->>Node1: 啟動單節點事務(事務ID)\n       App->>Node2: 啟動單節點事務(事務ID)\n       Note over Node1,Node2: 執行事務中...\n       App->>Coordinator: 準備提交\n       Coordinator->>Node1: 準備請求(事務ID)\n       Coordinator->>Node2: 準備請求(事務ID)\n       Node1->>Coordinator: 是\n       Node2->>Coordinator: 是\n       Note over Coordinator: 所有參與者都已準備好\n       Coordinator->>Coordinator: 決定提交並記錄到事務日誌\n       Coordinator->>Node1: 提交請求(事務ID)\n       Coordinator->>Node2: 提交請求(事務ID)\n       Node1->>Coordinator: 提交成功\n       Node2->>Coordinator: 提交成功\n  結論:  2PC可以保證原子性的原因是”協調者追蹤決定並使其不可撤銷”  比喻:  回到西方結婚儀式  1.牧師再問雙方是否要結婚(只要其中一放放棄牧師就不可能會繼續事務)  2.其中有一個人昏倒(沒有聽到\"正式宣布成為夫妻”)因為說出我願意了所以還是有效  3.當昏倒的那個人醒來，可以透過查詢牧師的預約日誌查詢是否結婚成功  協調者失效  如果任何一個準備請求超時，協調者就會終止事務。如果提交或終止請求失敗。協調者將會無條件重試。  如果再准便前請求失敗，參與者可以安全終止事務，必須等待協調者回復提交終止或是成功，一旦協調者出現網路或是崩潰問題，餐與者就只能傻傻等待。所以參與者對於這種事務狀態稱為存疑或不確定。     圖 9-10 參與者投贊成票後，協調者崩潰。資料庫 1 不知道是否提交或中止  這張圖說明了協調者在提交部分出現問題導致參與者1只能呆呆的等待，少了協調者的訊息參與者不會知道事務已經提交或是放棄。在這種狀況底下只能等待協調者，所以這就是為什麼參與者需要在提交或終止之前先將”我願意”寫到日誌的原因因為最後的提交還是有可能失敗，任何沒有寫在事務日誌的提交紀錄都會被終止，所以2PC的提交點就是協調者的常規單節點原子原則。     sequenceDiagram\n       participant Coordinator as 協調者\n       participant Node1 as 參與者1\n       participant Node2 as 參與者2\n       participant Log1 as 參與者1日誌\n       participant LogC as 協調者日誌\n       \n       Coordinator->>Node1: 準備請求(事務ID)\n       Coordinator->>Node2: 準備請求(事務ID)\n       Node1->>Log1: 寫入\"我願意\"(事務ID)\n       Node2->>Coordinator: 是(事務ID)\n       Coordinator->>LogC: 寫入決定:提交(事務ID)\n       Note over Coordinator: 協調者出現問題，暫時無法通訊\n       Note over Node1: 等待協調者的提交/中止請求...\n       Note over Coordinator: 協調者恢復\n       Coordinator->>LogC: 讀取事務日誌(事務ID)\n       Coordinator->>Node1: 提交請求(事務ID)\n       Coordinator->>Node2: 提交請求(事務ID)\n       Node1->>Coordinator: 提交成功(事務ID)\n       Node2->>Coordinator: 提交成功(事務ID)\n  三階段提交  1.兩階段提交（2PC）被視為阻塞的原子提交協議，因為在協調者故障時會造成參與者的等待。  2.三階段提交（3PC）作為2PC的替代方案，但其有效性依賴於有限的網路延遲和節點響應時間，這在實際系統中不常見。  3.非阻塞原子提交需要完美的故障檢測器以可靠地判斷節點的狀態，但在無限延遲的網路中，超時不是一個可靠的故障檢測機制。  4.由於這些挑戰，2PC仍被使用，儘管存在協調者故障的可能性。  實踐中的分散式事務  介紹:  分散式事務的名聲譭譽參半，尤其是那些透過兩階段提交實現的。一方面，它被視作提供了一個難以實現的重要的安全性保證；另一方面，它們因為導致運維問題，造成效能下降，做出超過能力範圍的承諾而飽受批評。許多雲服務由於其導致的運維問題，而選擇不實現分散式事務。  報告顯示MySQL的分散事務比單節點事務慢了10倍以上，2PC不只需要使用較高的效能大部分時間都是在處理崩潰恢復所需的強制寫入(fsync)與網路往返。  fsync:    fsync  是一個在多種操作系統中使用的系統呼叫，其主要目的是確保指定的文件描述符（file descriptor）所關聯的文件的所有元數據和元信息已經從操作系統的緩衝區寫入到底層存儲系統。當一個程序寫入文件時，操作系統可能會將數據暫時保留在緩衝區中，以便稍後再將它寫入磁碟，以提高效率。然而，在某些情況下，程序可能需要確保數據已經被實際寫入磁碟，以防止在系統崩潰或其他故障情況下失去數據。  分散式事務:  1.資料庫內部的分散式事務  相同的資料庫例如 MySQL Cluster的NDB與VoltDB，所有參與事務的節點都是使用相同的資料庫軟體。  2.異構分散式事務  由兩種或是兩種以上不同技術組成，甚至是非資料庫系統(訊息代理)。跨系統的分散式事務必須要確保原子提交。  恰好一次的訊息處理  異構的分散式事務只要能提供提交原子性來視線安全提交並且保證只被處理一次，假設郵件伺服器不支援2PC，如果訊息處理失敗並重試則可能導致重複發送郵件問題，但是如果訊息都可以在事務終止回滾，那這樣處理流程就可以安全地重試而且就好像什麼錯誤都沒有發生過一樣。  XA事務  1.X/Open XA（擴充套件架構，eXtended Architecture）**是一個於1991年推出的標準，旨在通過兩階段提交實現異構技術間的整合，廣泛應用於許多傳統關聯式資料庫和訊息代理中。  2.  XA非網路協議，而是一個C API ，用於連線至事務協調者。它也有在其他語言例如Java中的實現，如Java事務API (JTA)。  3.  事務協調者需實現XA API ，通常是作為一個庫載入到發起事務的應用程序中，並使用本地磁碟上的日誌記錄事務的決定（提交/中止）。  4.  如果應用程序崩潰，協調者也會失效 。必須重啟伺服器並讓協調程序庫讀取日誌以恢復每個事務的提交/中止結果，然後協調者才能要求參與者提交或中止。  5.  通訊必須透過客戶端庫 ，因為資料庫伺服器不能直接聯絡協調者。這意味著所有的事務協調都是通過應用程序進行的。  結論:  XA事務就是一種以API的形式讓許多想要整合不同系統上事務用的通用溝通介面，並保證數據的一致性與完整性。我的認知是XA事務就是多系統的協調者  假設:    初始化事務   您的應用程序首先會向事務協調者發起一個新的事務。   準備階段   應用程序將執行必要的操作，例如：\n   在MySQL中插入一條新的記錄。  在Redis中設置一個新的key-value對。  在RabbitMQ中發布一條新的訊息。  每個系統的操作都會通過XA API告知事務協調者它們已經準備好提交或回滾。   提交或回滾   如果所有的操作都成功準備好，事務協調者會告知所有的系統提交事務。  如果任何一個操作報告它不能提交，或者在準備階段出現錯誤，事務協調者會告知所有的系統回滾事務。   結束事務   一旦所有的系統都報告事務已經被提交或回滾，事務協調者會結束這個事務。     sequenceDiagram\n       participant App as Application\n       participant TM as Transaction Manager\n       participant MySQL\n       participant Redis\n       participant RabbitMQ\n       \n       App->>TM: Start Transaction\n       TM->>MySQL: Prepare\n       TM->>Redis: Prepare\n       TM->>RabbitMQ: Prepare\n       MySQL->>TM: Ready\n       Redis->>TM: Ready\n       RabbitMQ->>TM: Ready\n       TM->>MySQL: Commit\n       TM->>Redis: Commit\n       TM->>RabbitMQ: Commit\n       MySQL->>TM: Acknowledge Commit\n       Redis->>TM: Acknowledge Commit\n       RabbitMQ->>TM: Acknowledge Commit\n       TM->>App: Transaction Completed\n  這是我想像中XA事務當作協調者可能會做的事情  懷疑持有鎖  為什麼我們這麼關心存疑事務？系統的其他部分就不能繼續正常工作，無視那些終將被清理的存疑事務嗎？  1.如果其他部分如果繼續運作會導致髒寫，或是資料不一致性。  2.如果協調者崩潰，事務必須在整個存疑期間持有這個鎖，這可能導致應用大面積不可用，直到存疑事務得到解決。  3.存疑事務是問題的核心，因為它們持有資料庫的鎖，並阻止其他事務訪問相同的資料行。  結論：  回到9-9這裡存疑事務並沒有說明的很清楚但是經過查證過後應該是9-9的灰色部分locks held by transaction，在這個狀況底下所有需要使用事務的節點都會被這個鎖卡死。     sequenceDiagram\n       participant 協調者\n       participant 資料庫\n       participant 事務A\n       participant 事務B\n   \n       事務A->>資料庫: 開始事務, 獲取行鎖\n       Note right of 資料庫: 行鎖被持有\n       資料庫-->>協調者: 通知事務A的行鎖\n       協調者->>資料庫: 保持行鎖直至提交或中止\n       事務B->>資料庫: 開始事務, 嘗試獲取相同行的鎖\n       Note right of 資料庫: 事務B被阻塞\n       資料庫-->>協調者: 通知事務B的阻塞\n       協調者-->>事務A: 請求提交或中止事務\n       事務A->>資料庫: 提交事務\n       資料庫-->>協調者: 通知事務A的提交\n       協調者->>資料庫: 釋放事務A的行鎖\n       Note right of 資料庫: 行鎖被釋放\n       資料庫-->>事務B: 獲取行鎖, 繼續事務\n  從協調者故障中恢復  理論上協調者崩潰後並重新啟動，應該可以從日誌中恢復，並解決所有的存疑事務。然而實際的系統很有可能會產生孤立的事務，孤立事務會出現在協調者無法確認事務結果的狀況發生(例如事務日誌由於軟體錯誤丟失或損壞)。當然這種狀況事務基本上無法自己解決，所以這些鎖就會卡在資料庫中阻塞其他事務。  真正的原因是因為2PC的實踐中即時重新啟動也會保留存疑事務鎖(為了保持原子性保證)，唯一的方式就是讓管理者手動決定提交還是回滾，這時候管理者就要一一去查看存疑事務的參與者(昏倒的那個請他再說一次我願意，或是有點像是git的同一份文件多人修改需要人為解決衝突)。  許多XA的實踐都有一個叫做啟發式決策，允許參與者單方面決定提交或是放棄，無需協調者作出最終決定。但是壞處就是可能破壞了原子性，有趣的是啟發式代表的是可能破壞原子性的委婉說法，啟發式決策只是為了逃出災難性的情況而準備(  其實發錢或是發張電影票或許可以解決 )。  分散式事務的限制  XA事務解決了多個參與者相互一致的實現和重要問題，但也引入了嚴重的維運問題，事務協調者本身就是一種資料庫，因此需要像其他重要資料庫一樣小心操作。  1.如果協調者沒有HA，可能會導致存疑事務鎖死，令人驚訝的是好像很多協調者都沒有HA或者只有基本的複製支援\n2.引入協調者後，間接地引入了系統狀態。通常的HTTP服務是無狀態的，但一旦加入了協調者，協調者的日誌就必須持久化以保存系統的狀態。因此，在協調者資料庫崩潰後，必須能夠恢復存疑事務。  3.由於XA協議要兼容多種不同的資料系統，它必須採用所有系統共通的基本機能。例如，XA無法檢測跨不同系統的死鎖，因為這需要一個標準的協議來交換各個事務正在等待的鎖的資訊，而XA並未提供這樣的協議。  4.相對於XA，針對資料庫內部的分散式事務，限制不是那麼嚴格。例如，可以實現分散式版本的SSI。然而，仍然存在一些問題：使用兩階段提交（2PC）成功提交一個事務需要得到所有參與者的回應。所以，如果系統的任何一部分出現故障，事務就會失敗。因此，分散式事務有放大失效（amplifying failures）的風險，這與我們構建容錯系統的目標是相違背的。  容錯共識  介紹：  共識＝多個節點就某個事件達成一致  例如多個人同時嘗試定飛機最後一個位置，共識演算法可以用來確定這些互不相容的操作，最終決定哪一個是贏家  一個或多個節點可以提議某些值，而共識演算法決定採用某個值。  例如：  在訂飛機票的時候，當同時多個客戶嘗試訂購最後一個座位時，處理request的每個節點可以提議將要服務的客戶ID，而決定指明哪個客戶訂到座位  共識演算法必須滿足以下性質：  統一共識，相當於不可靠故障檢測器的非同步系統中的常規共識，學術文獻通常指的是process而不是節點。在這裡使用節點。  屬性解釋：   一致同意（Uniform agreement）  沒有兩個節點的決定不同。  完整性（Integrity）  沒有節點決定兩次。  有效性（Validity）  如果一個節點決定了值   v  ，則   v  由某個節點所提議。  終止（Termination）  由所有未崩潰的節點來最終決定值。    共識核心屬性 ：   在共識演算法的領域中，\"一致同意\"（Agreement）、\"完整性\"（Integrity）和\"有效性\"（Validity）是三個基本的屬性，它們共同定義了一個良好共識演算法的基本要求。   一致同意（Agreement） ：\n   這個屬性要求所有非失效的節點必須達成共識，即它們都必須同意同一個值。所有人必須達成相同的決定。   完整性（Integrity） ：\n   完整性要求一個值一旦被選定，就不應該被改變。換句話說，每個節點只能接受一個值，並且一旦值被決定，就不能更改。   有效性（Validity） ：\n   有效性要求被選定的值必須是由某個節點提議的值，而不應是一個隨意創造的值。這個屬性主要是為了避免某些“偽”解決方案。一個演算法可能始終選擇**  null **作為共識值，無論節點們提議了什麼值。這個演算法滿足了一致同意和完整性（因為它始終選擇同一個值並且不會更改它），但它不滿足有效性，因為它選擇的值並不是由節點提議的。  這三個屬性合起來確保了共識演算法的基本要求：所有非失效的節點能夠達成共識，選定一個由節點提議的值，並且一旦這個值被選定，就不能再被更改。這是實現分布式系統一致性的基本框架，並確保了系統的穩定性和可靠性。  如果不關心容錯，那麼滿足前三個屬性很容易：你可以將一個節點硬編碼為 “獨裁者”，並讓該節點做出所有的決定。但如果該節點失效，那麼系統就無法再做出任何決定。事實上，這就是我們在兩階段提交的情況中所看到的：如果協調者失效，那麼存疑的參與者就無法決定提交還是中止。   獨裁者節點與協調者的影響 ：   硬編碼一個節點作為“獨裁者”可以輕易達成共識，但若該節點失效，系統不能做出決定。  兩階段提交的情況下，若協調者失效，存疑的參與者不能決定提交還是中止。   終止屬性與容錯 ：   終止屬性要求共識演算法不能無限期地等待，即使有節點失效，也必須達成決定。  終止屬性取決於，不超過一半節點不可用。  即使大多數節點失效或網路存在問題，共識的實現仍需保證安全屬性（一致同意，完整性和有效性）。   拜占庭錯誤((n+1)/3)與共識 ：   大多數共識演算法假設不存在拜占庭錯誤，即節點都會正確遵循協議。  穩健地達成共識是可能的，只要少於三分之一的節點存在拜占庭錯誤。  共識演算法和全序廣播  最著名的容錯共識演算法是檢視戳複製(VSR, Viewstamped Replication)。但是本書不介紹!!!暸解共通的高階思想就足夠了，除非自己要實現一個共識系統。  大多數的共識演算法實際上並不直接使用剛剛提到的這些模型(提議與決定單個值，並滿足一致同意、完整性、有效性和終止屬性)。取而代之的反而是值的順序，同時也促使了全序廣播演算法。  全序廣播要求將訊息按照相同的順序，恰好傳送一次，準確地送到所有節點。  所以全序廣播相當於重複進行了多輪共識(每次共識決定與一次訊息傳遞相對應)：   由於   一致同意  屬性，所有節點決定以相同的順序傳遞相同的訊息。  由於   完整性  屬性，訊息不會重複。  由於   有效性  屬性，訊息不會被損壞，也不能憑空編造。  由於   終止  屬性，訊息不會丟失。  視戳複製，Raft 和 Zab 直接實現了全序廣播，因為這樣做比重複   一次一值（one value a time）  的共識更高效。在 Paxos 的情況下，這種最佳化被稱為 Multi-Paxos。  結論：  全序廣播棒棒，支持了共識演算法所需要基本要求一致同意……等等屬性  單主複製與共識  為什麼在第五章沒有擔心過共識問題？因為單主複製將所有操作都交給主庫並以相同的順序丟給從庫，使從庫的副本保持在最新狀態。  如果主庫是由維運人員手動操作那就是一種獨裁型別的共識演算法：只有一個節點被允許接受寫入，如果該節點發生故障，則整個系統無法寫入，直到維運人員處理。這樣的系統在實踐中表現良好，但是無法滿足終止屬性，因為需要人為干預才能取得進展。  某些資料庫會自動執行領導者選舉，但是會導致腦裂，因為領導者之間需要共識演算法。  結論：  要有共識演算法要先有領導者，而選出領導者TMD又要有共識演算法所以，先有雞還是先有蛋的問題出現了！  紀元編號和法定人數    領導者與紀元編號的關係 :\n   共識協議通常內部有領導者的概念，但不能保證領導者的獨一無二。通過紀元編號（在不同協議中可能有不同名稱）來確保在每個時期，領導者是唯一的。   領導者選舉 :\n   當現任領導者被認為失效時，節點間會開始新的領導者選舉，並賦予新的紀元編號，以保證紀元編號的全序且單調遞增。   領導者衝突的解決 :\n   若不同時期的領導者之間出現衝突，則以紀元編號較高的領導者為准。   法定人數 :\n   一個領導者在做出任何決定前，必須先從法定人數的節點中獲得贊成。通常需要兩輪投票：一輪選出領導者，一輪對領導者的提議進行投票。   投票過程與2PC的區別 :\n   這個投票過程與兩階段提交（2PC）相似，但區別在於領導者的產生方式及投票要求。共識演算法只需要多數節點的贊成，並且有明確的恢復過程來保證系統的安全屬性。  結論：  在領導者選拔裡弄了一個更嚴謹的紀元編號與法定人數並讓編號最大的人當選領導者，投票過程就是更嚴謹的2PC     sequenceDiagram\n       participant Node1\n       participant Node2\n       participant Node3\n       participant Node4\n   \n       Note over Node1,Node4: 初始狀態 (領導者: Node1)\n   \n       alt 領導者失效\n           Node1->>Node1: 領導者失效\n       end\n   \n       Note over Node1,Node4: 紀元編號 1 開始\n   \n       Node2->>+Node3: 領導者失效，開始領導者選舉 (紀元編號: 1)\n       Node3->>+Node2: 投票給 Node2 (紀元編號: 1)\n       Node4->>+Node2: 投票給 Node2 (紀元編號: 1)\n   \n       Note over Node2,Node4: Node2 以多數票當選為領導者\n   \n       Node2->>Node3: 提議值 (紀元編號: 1)\n       Node2->>Node4: 提議值 (紀元編號: 1)\n       Node3->>Node2: 贊成提議 (紀元編號: 1)\n       Node4->>Node2: 贊成提議 (紀元編號: 1)\n   \n       Note over Node2,Node4: 提議值被以多數票接受\n   \n       alt 領導者失效\n           Node2->>Node2: 領導者失效\n       end\n   \n       Note over Node2,Node4: 紀元編號 2 開始\n   \n       alt Node1恢復\n           Node1->>Node1: Node1恢復\n       end\n   \n       Node1->>+Node3: 領導者失效，開始領導者選舉 (紀元編號: 2)\n       Node3->>+Node1: 投票給 Node1 (紀元編號: 2)\n       Node4->>+Node1: 投票給 Node1 (紀元編號: 2)\n   \n       Note over Node1,Node4: Node1 以多數票當選為領導者\n   \n       Node1->>Node3: 提議值 (紀元編號: 2)\n       Node1->>Node4: 提議值 (紀元編號: 2)\n       Node3->>Node1: 贊成提議 (紀元編號: 2)\n       Node4->>Node1: 贊成提議 (紀元編號: 2)\n   \n       Note over Node1,Node4: 提議值被以多數票接受\n   alt Node2恢復\n           Node2->>Node2: Node2恢復\n       end\n       alt 領導者衝突的解決\n           Node2->>Node1: 嘗試成為領導者 (紀元編號: 2)\n           Node1->>Node2: 以更高的紀元編號解決衝突 (紀元編號: 3)\n       end\n   \n       Note over Node1,Node4: Node1 保持為領導者\n    初始狀態 :\n   系統初始時，Node1 是領導者。   領導者失效 (Node1) :\n   Node1 失效，系統進入紀元編號1。   領導者選舉 (紀元編號 1) :\n   Node2、Node3 和 Node4 開始領導者選舉。Node3 和 Node4 投票給 Node2，Node2 以多數票當選為新的領導者。   提議和投票 (紀元編號 1) :\n   Node2 提出一個提議值，並得到 Node3 和 Node4 的贊成，提議值被多數票接受。   領導者失效 (Node2) :\n   Node2 失效，系統進入紀元編號2。   Node1恢復 :\n   Node1 恢復並參與接下來的領導者選舉。   領導者選舉 (紀元編號 2) :\n   Node1、Node3 和 Node4 開始領導者選舉。Node3 和 Node4 投票給 Node1，Node1 以多數票當選為新的領導者。   提議和投票 (紀元編號 2) :\n   Node1 提出一個提議值，並得到 Node3 和 Node4 的贊成，提議值被多數票接受。   Node2恢復 :\n   Node2 恢復並嘗試重新成為領導者。   領導者衝突的解決 :\n   Node2 嘗試在紀元編號2成為領導者，但 Node1 以更高的紀元編號（3）回應，以解決領導者衝突，並保持為領導者。  共識的局限性  共識演算法為分散式系統提供了基本的安全屬性，如一致同意、完整性和有效性，並在多數節點正常運作時保持容錯和進展。但它有其侷限性：    同步複製問題 ：共識演算法依賴同步複製來確保一致性，與常見的非同步複製相比，可能會在效能上有所損失。   節點數量要求 ：至少需要三個節點以容忍單節點故障，五個節點以容忍兩個節點故障。如果網路故障導致節點間連線中斷，只有多數節點所在的網路能繼續運作。   固定節點集合 ：多數共識演算法假設參與投票的節點是固定的，雖然有動態成員擴充套件，但理解和實施較為困難。   超時檢測的依賴 ：共識系統依賴超時來檢測失效節點，但在網路延遲高度變化的環境中可能會導致錯誤的失效檢測和領導者選舉，影響效能。   對網路問題的敏感性 ：例如，Raft 在特定的網路連線不可靠時可能會出現領導者頻繁切換的問題，影響系統進展。這類問題在其他一致性演算法中也存在，而設計能夠健壯應對不可靠網路的演算法仍是開放的研究領域。  Raft：   Raft 是一種用於替代  Paxos 的  共識 演算法。相比於  Paxos ，Raft的目標是提供更清晰的邏輯分工使得演算法本身能被更好地理解，同時它安全性更高，並能提供一些額外的特性。  [1]  [2] :1Raft能為在  電腦叢集 之間部署  有限狀態機 提供一種通用方法，並確保叢集內的任意節點在某種狀態轉換上保持一致。Raft演算法的開源實現眾多，在  Go 、  C++ 、  Java 以及   Scala 中都有完整的代碼實現。Raft這一名字來源於\"Reliable, Replicated, Redundant, And Fault-Tolerant\"（「可靠、可複製、可冗餘、可容錯」）的首字母縮寫。  [3]  叢集內的節點都對選舉出的領袖採取信任，因此Raft不是一種  拜占庭容錯 演算法。  [2]  拜占庭將軍問題：  一組  拜占庭 將軍分別各率領一支軍隊共同圍困一座城市。為了簡化問題，將各支軍隊的行動策略限定為進攻或撤離兩種。因為部分軍隊進攻部分軍隊撤離可能會造成災難性後果，因此各位將軍必須通過投票來達成一致策略，即所有軍隊一起進攻或所有軍隊一起撤離。因為各位將軍分處城市不同方向，他們只能通過信使互相聯絡。在投票過程中每位將軍都將自己投票給進攻還是撤退的資訊通過信使分別通知其他所有將軍，這樣一來每位將軍根據自己的投票和其他所有將軍送來的資訊就可以知道共同的投票結果而決定行動策略。  成員與協調服務  介紹：  ZooKeeper 和 etcd 被設計為容納少量完全可以放在記憶體中的資料（雖然它們仍然會寫入磁碟以保證永續性），所以你不會想著把所有應用資料放到這裡。這些少量資料會透過容錯的全序廣播演算法複製到所有節點上。正如前面所討論的那樣，資料庫複製需要的就是全序廣播：如果每條訊息代表對資料庫的寫入，則以相同的順序應用相同的寫入操作可以使副本之間保持一致。   線性一致性的原子操作  使用原子 CAS 操作可以實現鎖：如果多個節點同時嘗試執行相同的操作，只有一個節點會成功。共識協議保證了操作的原子性和線性一致性，即使節點發生故障或網路在任意時刻中斷。分散式鎖通常以   租約（lease）  的形式實現，租約有一個到期時間，以便在客戶端失效的情況下最終能被釋放（請參閱 “   程序暫停 ”）。  操作的全序排序  如 “   領導者和鎖 ” 中所述，當某個資源受到鎖或租約的保護時，你需要一個防護令牌來防止客戶端在程序暫停的情況下彼此衝突。防護令牌是每次鎖被獲取時單調增加的數字。ZooKeeper 透過全序化所有操作來提供這個功能，它為每個操作提供一個單調遞增的事務 ID（  zxid ）和版本號（  cversion ）【15】。  失效檢測  客戶端在 ZooKeeper 伺服器上維護一個長期會話，客戶端和伺服器週期性地交換心跳包來檢查節點是否還活著。即使連線暫時中斷，或者 ZooKeeper 節點失效，會話仍保持在活躍狀態。但如果心跳停止的持續時間超出會話超時，ZooKeeper 會宣告該會話已死亡。當會話超時時（ZooKeeper 稱這些節點為   臨時節點 ，即 ephemeral nodes），會話持有的任何鎖都可以配置為自動釋放。  變更通知  客戶端不僅可以讀取其他客戶端建立的鎖和值，還可以監聽它們的變更。因此，客戶端可以知道另一個客戶端何時加入叢集（基於新客戶端寫入 ZooKeeper 的值），或發生故障（因其會話超時，而其臨時節點消失）。透過訂閱通知，客戶端不用再透過頻繁輪詢的方式來找出變更。  在這些功能中，只有線性一致的原子操作才真的需要共識。但正是這些功能的組合，使得像 ZooKeeper 這樣的系統在分散式協調中非常有用。  結論：  在使用分散式資料庫時其實要考慮哪些資訊是必須線性一致性的，如果需要線性一致性的資料再放入像是ZooKeeper這種分散式資料庫保證線性一致性的原子操作。或是說如果是最終一致性的內容就不需要放到這種分散式資料庫的架構讓node之間彼此慢慢溝通就好了。  將工作分配給節點  ZooKeeper/Chubby 模型執行良好的一個例子是，如果你有幾個程序例項或服務，需要選擇其中一個例項作為主庫或首選服務。如果領導者失敗，其他節點之一應該接管。這對單主資料庫當然非常實用，但對作業排程程式和類似的有狀態系統也很好用。    領導者選舉與失敗恢復 ：\n   ZooKeeper或Chubby模型可以用於選擇主節點或首選服務，並在主節點失效時由其他節點接管，這在單主資料庫、作業排程系統和其他有狀態系統中非常實用。   資源分割槽的分配與再平衡 ：\n   通過使用ZooKeeper，可以決定將哪個分割槽分配給哪個節點，並在新節點加入或節點失效時重新平衡負載。   原子操作、臨時節點與通知 ：\n   ZooKeeper提供了原子操作、臨時節點和通知功能，有助於實現分割槽的再平衡和失效節點的工作接管，並能讓系統自動從故障中恢復。   高層工具的支援 ：\n   例如Apache Curator，它是基於ZooKeeper客戶端API而提供更高層的工具，降低了從頭實現共識演算法的困難和風險。   協調節點的外包 ：\n   ZooKeeper允許在固定數量的節點上執行多數票選舉，而非在數千個節點上，並“外包”一些協調節點的工作，如共識、操作排序和故障檢測，以支援大量的客戶端。   資料型別的緩慢變化 ：\n   ZooKeeper適合管理變化緩慢的資料，而不是每秒會改變數千或數百萬次的執行時狀態資料。   其他工具的配合 ：\n   如Apache BookKeeper，可以用於複製應用狀態，與ZooKeeper配合使用，提供更完整的分散式系統解決方案。  結論：  各種工具特性與應用方式  服務發現    服務發現 :\n   ZooKeeper、etcd 和 Consul 常用於服務發現，即確定連接到特定服務所需的 IP 地址。在雲資料中心，由於虛擬機器的動態性，服務的 IP 地址通常不是事先知道的。服務可配置為在啟動時註冊其網路端點，以便其他服務能夠找到它們。   共識與服務發現 :\n   服務發現不一定需要共識，而是更重視可用性和對網路中斷的魯棒性。傳統上，DNS 用於查詢服務的 IP 地址，並通過多層快取來提高效能和可用性，它不保證線性一致性。   領導者選舉 :\n   與服務發現不同，領導者選舉需要共識。如果共識系統已經識別出領導者，這些資訊可以用於幫助其他服務找出領導者。   只讀快取副本 :\n   一些共識系統支援只讀快取副本，這些副本能非同步接收共識演算法的所有決策日誌，但不參與投票，從而能夠處理不需要線性一致性的讀取請求，降低了需求共識的情境，同時保持了系統的效能與可用性。  成員資格服務    成員資格服務的核心功能 :\n   它主要用於確定哪些節點目前是活動的，並且是叢集的活動成員。由於無限的網路延遲，無法可靠地檢測到另一個節點是否失效，但通過共識，節點可以達成一致意見，決定哪些節點應被認為是存在或不存在。   共識在故障檢測中的角色 :\n   透過共識來進行故障檢測可以協助節點就哪些節點是存在或不存在達成一致。雖然可能會有節點被錯誤地宣告死亡，但對系統來說，知道哪些節點是當前的成員是非常有用的。   領導者選舉與成員資格 :\n   在選擇領導者時，例如可能簡單地選擇當前成員中編號最小的成員。但如果不同的節點對現有成員有不同的意見，這種方法將不起作用。成員資格服務確保節點對叢集的現有成員有一致的理解，從而促進了有效的領導者選舉。  9.5 本章小結  達成共識意味著以這樣一種方式決定某件事：所有節點一致同意所做決定，且這一決定不可撤銷。透過深入挖掘，結果我們發現很廣泛的一系列問題實際上都可以歸結為共識問題，並且彼此等價（從這個意義上來講，如果你有其中之一的解決方案，就可以輕易將它轉換為其他問題的解決方案）。這些等價的問題包括：   線性一致性的 CAS 暫存器  暫存器需要基於當前值是否等於操作給出的引數，原子地   決定  是否設定新值。  原子事務提交  資料庫必須   決定  是否提交或中止分散式事務。  全序廣播  訊息系統必須   決定  傳遞訊息的順序。  鎖和租約  當幾個客戶端爭搶鎖或租約時，由鎖來   決定  哪個客戶端成功獲得鎖。  成員 / 協調服務  給定某種故障檢測器（例如超時），系統必須   決定  哪些節點活著，哪些節點因為會話超時需要被宣告死亡。  唯一性約束  當多個事務同時嘗試使用相同的鍵建立衝突記錄時，約束必須   決定  哪一個被允許，哪些因為違反約束而失敗。  如果領導者失效，或者如果網路中斷導致領導者不可達，這樣的系統就無法取得任何進展。應對這種情況可以有三種方法：   等待領導者恢復，接受系統將在這段時間阻塞的事實。許多 XA/JTA 事務協調者選擇這個選項。這種方法並不能完全達成共識，因為它不能滿足   終止  屬性的要求：如果領導者續命失敗，系統可能會永久阻塞。  人工故障切換，讓人類選擇一個新的領導者節點，並重新配置系統使之生效，許多關係型資料庫都採用這種方方式。這是一種來自 “天意” 的共識 —— 由計算機系統之外的運維人員做出決定。故障切換的速度受到人類行動速度的限制，通常要比計算機慢（得多）。  使用演算法自動選擇一個新的領導者。這種方法需要一種共識演算法，使用成熟的演算法來正確處理惡劣的網路條件是明智之舉【107】。  html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"id":349,"path":350,"dir":351,"title":352,"description":7,"keywords":353,"body":357},"content:6.clean-arch:1.chapter1.md","/clean-arch/chapter1","clean-arch","01 什麼是設計與結構",[65,354,355,356],"目標","案例研究","總結","  前言  這些年來，在設計和架構方面一直存在不少的疑惑。到底什麼是設計(design)? 什麼是架構(architecture)? 兩者有何區別?  本書的目的之一就是要消除這些困惑，並一勞永逸地定義架構和設計。   架構： 更高層次的概念，它涉及到系統的整體結構和組件之間的關係。架構關注於系統的組織方式，確保系統能夠滿足非功能性需求，如可擴展性、可維護性和安全性。  設計： 較低層次的概念，它涉及軟體開發的具體細節，例如: 類別的定義、方法的實現、變數的使用等。  舉個例子，以一個設計的新家來舉例，這個家有一個架構嗎? 當然有的。那麼架構是什麼呢? 就是家的形狀(Shape)、外觀、挑高、和空間以及房間的佈局。但當我們去看設計師製作的  設計圖 時，你會看到大量的低層次細節，例如: 插座、燈的開關和燈被放置在哪裡。  因此，這就是軟體設計。低層次的細節和高層次的結構都是同一個整體的一部分。他們形成了一個連續的結構，定義了系統的形狀(Shape)。你不能只有一個而沒有另外一個; 實際上，這兩者沒有很明確的分界線。只有從最高層到最低層的一系列決策。  目標  這些決策的目標是什麼? 好的軟體設計的目標是什麼?   💡查看答案   軟體架構的目標是最小化建置和維護「需求系統」所需要的人力資源。  設計品質的度量標準。在系統的整個生命週期中，花費的精力都是保持少的，那麼這個設計就是好的，如果每次花費的精力都會隨著每個新版本的增長而增長的話，那麼這個設計就是不好的。  案例研究  下面是採用真實的案例研究來進行展示, 主要是透過幾個觀點來進行比較   工程人員的增加  工程人員的生產力  程式碼花費的成本  \n    \n    \n     爛攤子的記號  你看到上面的案例研究就是所謂的爛攤子記號。當對程式碼的整潔程度和設計架構沒有規劃好的情況下，會導致你的 Release 越大時，花費的成本也跟著提高。  \n     \n  從開發人員的觀點來看，投入了大量的時間，但他們產出非常的低，是因為他們需要努力來管理這些爛攤子，而導致沒有任何的產能。  高層管理的觀點  \n     \n   第一個版本的每月花費是數十萬美元。  第二個版本的每月花費比第一個版本多了數十萬美元。  第八個版本的每月花費是 2000 萬美元，這個金額持續上升  我們比較圖 1.5 和圖 1.2 中每個版本的程式碼行數。你會發現最初只花每個月十幾萬美元換得了許多功能，但最後的 2000 萬美元幾乎沒有得到任何的東西! 如果你是公司的主管看到這兩張圖表，就可以知道避免災難並立即採取行動是必要的。  高層管理人員必須思考下列的事情，嘗試找出問題點   該怎麼採取行動?  哪裡出了問題?  什麼問題導致生產力如此驚人地下降?  什麼地方出了錯？  在兩千六百年前，伊索告訴我們「龜兔賽跑」的故事。這個故事的寓意已經被陳述很多次了:   踏實和穩定持續是贏得比賽的關鍵  這場比賽不是比迅速，也不是比誰較為強壯  越快的，反而速度慢  這個故事本身說明了過度自信的愚蠢。兔子對於自己的速度如此自信，又不認真對待比賽，所以烏龜穿越終點時還在睡午覺。  現代開發者也處於類似的競賽狀態，表現出類似的過度自信。大多數的現代開發者工作得很努力，但他們的大腦一部分確實在睡覺的，這邊是指好的、整潔的、精心設計的程式碼  很重要 的部分仍然正在睡覺。  這些開發人員常陷入一個熟悉的謊言：「我們晚點整理程式碼，我們先把產品上市先」，當然，你知我也知，這些骯髒的程式碼在之後也永遠不可能被重構，因為市場壓力永遠不會減少，你沒有額外的時間去進行整理的。而且進入市場就意味著你後面有一大堆的競爭對手，你必須盡可能快點跑到最前面。  所以開發人員絕不可能回頭清理或重構這些程式碼，因為他們必須持續不斷地完成下一個功能。而這時爛攤子就此產生了，生產力繼續朝著0的方向趨近。  就像兔子對於牠的速度過於自信那樣，開發者對於自己的生產力也過於自信。但程式碼的髒亂使得他們工作效率不彰，這種情況從未停止也不會減緩。如果按照這樣的方式，在幾個月內生產力就會降低到0。  開發人員陷入一個更大的謊言是，撰寫髒亂的程式碼可以使他們在短期內走得更遠更快，只有長期下來才會變慢。接受這種謊言的開發人員表現出兔子過分自信的能力，認為他在未來將有機會可以處理這些髒亂的程式碼，這樣犯了一個是低級的錯誤。事實上，  製造髒亂的開發速度總是比保持整潔的開發速度還要慢 。  下圖中，Jason Gorman進行了一項測試實驗，他採用了測試驅動開發(TDD，test-driven development)開發了三天，另外三天他並沒有遵守這個原則來開發，這邊比較了兩者之間的成果。  \n     \n  我們可以發現圖1.6的學習曲線。後一天的工作比之前得更快完成。值得注意的是採用TDD的工作日比非TDD日的工作快了近10%，即便是最慢的TDD日也比最快的非TDD日還要快。  有些人可能會這為這樣的分析是一個了不起的結果，但對那些沒有被兔子過度自信迷惑的人來說，這樣的結果是可以預料到的，因為他們知道這是軟體開發的基本道理:   💡 想要走得快，唯一的方式就是要走得好。  這就是高層人員困境的解答。扭轉生產力下降和成本增加的唯一方法是，讓開發人員停止過度自信的兔子那樣思考，並替他們那骯髒的程式碼進行負責。  而開發人員可能會認為，從頭開始再次重新設計整個系統就好，但這只不過是兔子再次發言。因為就算重新開發了，但程式碼並沒有品質，到頭來還是會重蹈覆轍的。   💡 他們的過度自信將驅使進行重新設計，就像原本的專案那樣髒亂。  總結  要避免這種情況發生，最好的選擇就是讓開發部門認識和避免自己的過度自信，並認真對待軟體架構的品質。  要認真對待軟體架構，你需要知道什麼是好的軟體架構。為了建置一個能「滿足付出精力最小化及生產力最大化」的系統設計和架構。  這就是本書的內容: 他描述了什麼是好的整潔架構和設計(clean architecture and designs)，這樣開發人員可以建置出對於生命週期長期有利的系統。",{"id":359,"path":360,"dir":351,"title":361,"description":7,"keywords":362,"body":368},"content:6.clean-arch:2.chapter2.md","/clean-arch/chapter2","02 兩種價值觀的故事",[65,363,364,365,366,367],"行為","架構","更高的價值","Eisenhower 矩陣","為架構而戰","  前言  每一個軟體系統都會為利益相關者提供兩種不同的價值:   行為(behavior)\n   關於系統「做什麼」  行為是指軟體系統執行的動作或功能。這包括系統如何響應使用者輸入、處理資料、執行計算、與其他系統溝通以及產生輸出等。  結構(structure)\n   關於系統「如何組織和構建」  結構是指軟體系統的內部組織和設計。這包括系統的架構、模組的劃分、類別和函數的設計、資料儲存和管理方式等。結構確保了系統的可擴展性、可維護性、可測試性和其他非功能性需求。  軟體開發人員有責任確保這些價值都保持在高價值，但通常只會把重點放在其中一個而排除另外一個。更糟的是他們往往只會把重點放在這兩個中價值較小的一個，這導致軟體系統最終變得沒有價值。  行為  軟體的第一個價值就是「行為」。程式設計師是被雇用來設計機器的行為，透過機器的人行為能夠獲得利益或節省資金。透過幫助利益相關制定功能規格(functional specification)或需求文件(requirements document)來做到這一點。  當機器違反這些需求時，程式設計師會透過除錯器(debugger)來解決問題。  許多程式設計師認為這就是他們全部的工作。他們相信他們的工作就是讓機器實作出來需求並修復任何錯誤。但可悲的是，他們是錯誤的。  架構  軟體的第二個價值跟software這個字是有相關的，soft表示軟的、ware表示產品(prodcut)，當我們提到軟體(software)時，代表它可以輕易地修改機器行為的一種方式。而如果機器的行為是難以改變的，我們則稱呼為硬體(hardware)。  為了達到軟體的本來目的，軟體就必須是  軟的 ，也就是說它可以很容易改變。當需求方改變需求時，這個改變應該能簡單地實現。進行這種改變難度應該只與改變的範疇(scope)成正比，而不是跟具體形狀(shape)成正比。  需求變更的範疇(scope)和具體形狀(shape)，是決定軟體變更實施成本高低的關鍵。這也是為什麼第二年的研發成本比第一年高很多，第三年又比第二年更高。  問題的根源當然是系統的架構設計。如果系統的架構設計偏向某種特定的形狀，那麼新的變更就會越來越難以實施。所以好的系統架構應該盡可能做到與   具體形狀(shape)  無關。  更高的價值  功能還是架構更有價值呢? 軟體系統能夠工作比較重要，還是軟體系統更容易變更比較重要?  如果你去詢問業務經理，通常會得到的答案是  軟體系統能夠工作比較重要 。而開發人員也會經常跟著這種態度和思考下去做。這是錯誤的態度。我們可以用簡單的邏輯以極端的情況來進行驗證。   如果您給我一個完美的程式，但不可能修改，那麼當需求改變時，它就不能工作了，而我也將無法使其工作。所以這個程式變得毫無用處。  如果您給我的程式不能工作但容易修改，那麼我可以使其工作，並隨著需求的變化而繼續工作。因此，該程式將持續是有用的。  有些系統實際上是不可能改變的，因為改變的成本超過了改變的利益。許多系統在某些功能特性或設置的確如此。  當你詢問業務經理，他們是否想要做點改變，他們會說當然是要的，但你會發現到他們只注意到目前的功能，而不會注意到往後的系統擴充或靈活度，畢竟對業務經理來說他們只關注在產品的「行為」上。而今天當業務經理要求你對一個簡易的功能進行變更時，而你把成本估得非常高時，相信業務經理是憤怒的。  Eisenhower 矩陣  由 Dwight D. Eisenhower 總統提到的重要性與緊急性矩陣(圖 2.1)   💡 我有兩類問題，緊急和重要。緊急的不重要，重要的不緊急。  \n     \n  上述這句話有深的道理。我們用軟體的方式來舉例:   軟體的第一個價值「行為」是  迫切 的，但並不總是特別重要。  軟體的第二個價值「架構」是  重要 的，但從不特別迫切。  當然有些事情既迫切又重要，有些事情不迫切也不重要。最終，我們可以把這四個點排出它的優先順序:    迫切 且  重要  不  迫切 但  重要   迫切 但不  重要  不  迫切 也不  重要  請注意，軟體的系統架構(重要的事情)位於此列表的前兩位，而系統行為(迫切的事情)只佔據了第一和第三的位置。  業務經理和開發人員經常犯的錯誤就是把位置 3 的項目提升到位置 1。換句話說，他們沒有把那些迫切但不重要的功能從那些真正迫切且重要的功能中分離出來。這個失誤會導致忽略了系統架構的重要性，而去重視系統不重要的功能。  開發人員的困境是業務經理沒有能力去評估架構的重要性。而這就是  軟體開發人員該去做的 。因此軟體開發團隊有責任主張架構的重要性高於功能的迫切性。  為架構而戰  如果你是一名  軟體架構師 更應該注重在系統的結構而不是其特性和功能。架構師建立了一個架構，使這些特性和功能非常容易開發且易於修改，這才是架構師的職責。  最後需要記住的是，如果架構是最後才出現的，那麼開發系統將變得更昂貴，因為要去作全面性的修改，將變得幾乎不可能完成的。",{"id":370,"path":371,"dir":351,"title":372,"description":7,"keywords":373,"body":378},"content:6.clean-arch:3.chapter3.md","/clean-arch/chapter3","03 範式概述",[65,374,375,376,377,356],"結構化程式設計","物件導向程式設計","函數式程式設計","響應式程式設計","  前言  任何軟體架構的實作都離不開具體的程式碼，所以我們對軟體架構的討論應該從第一行被寫下的程式碼開始。   1938 年，Alan Turing 為現代電腦程式設計打下了基礎。他也是第一個提出「程式即數據」的人。  1945 年，Turing 已經在真實電腦上編寫我們現在也能看懂的電腦程式了。這些程式中用到了循環、分支、賦值、子呼叫、堆疊等如今我們都很熟悉的結構。而他用的程式語言就是簡單的二進制數序列。  20 世紀 40 年代末期出現了彙編器（assembler），它能自動將一段程式轉換為相應的二進制數序列，大幅解放了程式設計師。  1951 年，Grace Hopper 發明了 A0，這是世界上第一個編譯器（compiler）。事實上，編譯器這個名字就是他定義和推廣使用的。  1953 年，那一年 FORTRAN 面世了。接下來就是層出不窮的新程式語言了 —— COBOL、PL/1、SNOBOL、C、Pascal、C++、Java 等等，不勝枚舉。  除此之外，電腦程式設計領域還經歷了另一個更巨大、更重要的變革，那就果程式設計範式（paradigm）的變化。程式設計範式指的是程式的編寫模式，與具體的程式語言關係相對較小。這些範式會告訴你應該在什麼時候採用什麼樣的程式碼結構。直到今天，我們也總共只有三個程式設計範式，而且未來幾乎不可能再出現新的，接下來我們就來看看為什麼。  結構化程式設計  結構化程式設計（Structured programming，簡稱SP）是一種程式設計的範式，旨在透過使用  子程序 、  區塊結構 、  迴圈 和  決策控制結構 （如 if-else 語句和 switch 語句）來增强程式碼的清晰度、品質和開發效率。它鼓勵程序員避免使用 goto 語句，這樣可以使程式碼更易於理解和維護。結構化程式設計強調的是將大問題分解成小問題，並將這些小問題分解成更小的問題，直到這些問題可以用簡單的語句來解決。   使用 goto 及結構化的程式碼比較  我們現在有一個任務是從 1 數到 10，但當數到 5 時，我們要跳過 5 直接數到 6，繼續數到 10。  使用 goto 的程式碼：     using   System  ;\n   \n   class   GotoComplexExample   {\n       static   void   Main  () {\n           int   num   =   1  ;\n           \n           start  :\n           if   (num   ==   5  ) {\n               goto   skipFive  ;\n           }\n           \n           Console.  WriteLine  (num);\n           num  ++  ;\n           \n           if   (num   \u003C=   10  ) {\n               goto   start  ;\n           }\n           goto   end  ;\n           \n           skipFive  :\n           num  ++  ;\n           goto   start  ;\n           \n           end  :\n           Console.  WriteLine  (  \"Done\"  );\n       }\n   }\n  結構化的程式碼：     using   System  ;\n   \n   class   StructuredComplexExample   {\n       static   void   Main  () {\n           for   (  int   num   =   1  ; num   \u003C=   10  ; num  ++  ) {\n               if   (num   ==   5  ) {\n                   continue  ;   // 跳過5，直接繼續下一輪循環\n               }\n               Console.  WriteLine  (num);\n           }\n           Console.  WriteLine  (  \"Done\"  );\n       }\n   }\n   以下是結構化程式設計的主要概念和重點：   基本控制結構：    順序結構 ：程式碼按順序執行。 \n    \n    程式碼演示     Console.  WriteLine  (  \"第一步\"  );\n   Console.  WriteLine  (  \"第二步\"  );\n   Console.  WriteLine  (  \"第三步\"  );\n \n      選擇結構 ：根據條件選擇不同的執行路徑，如if-else語句。 \n    \n    程式碼演示     var   number   =   10  ;\n   if   (number   >   0  )\n       Console.  WriteLine  (  \"數字是正數。\"  );\n   else   if   (number   \u003C   0  )\n       Console.  WriteLine  (  \"數字是負數。\"  );\n   else\n       Console.  WriteLine  (  \"數字是零。\"  );\n \n      迴圈結構 ：重複執行程式碼塊直到滿足某個條件，包括for迴圈、while迴圈等。 \n    \n    程式碼演示     for   (  int   i   =   0  ; i   \u003C   5  ; i  ++  )\n       Console.  WriteLine  (  $\"迴圈次數：{  i  }\"  );\n \n     模塊化和函數：   將大的問題分解成小的、更易於管理的子問題。  每個子問題由一個模塊或函數來解決，使程式碼更易於理解和重用。 \n   \n   程式碼演示     // 定義一個函數來計算兩數之和\n   static   int   Add  (  int   num1  ,   int   num2  ) {\n       return   num1   +   num2;\n   }\n   \n   // 主程式調用Add函數\n   static   void   Main  (  string  []   args  ) {\n       int   result   =   Add  (  5  ,   3  );\n       Console.  WriteLine  (  $\"5 + 3 = {  result  }\"  );\n   }\n \n    局部變量和作用域：   推廣在最接近使用位置聲明變量，以提高程式碼的可讀性和減少錯誤。  使用局部變量來限制變量的作用範圍，增強程式的模塊性。 \n   \n   程式碼演示     static   void   ShowScopeExample  () {\n       // 局部變量i的作用域開始\n       int   i   =   100  ;\n       Console.  WriteLine  (  $\"在ShowScopeExample內部，i = {  i  }\"  );\n       // 局部變量i的作用域結束\n   }\n   \n   static   void   Main  (  string  []   args  ) {\n       ShowScopeExample  ();\n       // 以下將無法訪問i，因為它的作用域僅限於ShowScopeExample函數內\n       // Console.WriteLine(i); // 這會導致編譯錯誤\n   }\n \n    結構化設計的原則：    自頂向下設計 ：從最廣泛的問題開始，逐步細化到具體的功能和細節。   逐步細化 ：將複雜的問題逐步分解成更簡單、更易於管理的部分。   模塊獨立性 ：提高模塊之間的獨立性，降低它們之間的耦合。  使用結構化程式設計的優點：   提高程式碼的清晰度和可維護性。  促進程式碼重用。  減少錯誤和開發時間。  使程式設計更加系統化和規範化。  結構化程式設計是對早期無結構、難以維護的程式設計方法的一種改進，它強調清晰的結構和簡單的控制流。隨著軟件工程的發展，雖然出現了許多新的程式設計範式（如物件導向程式設計），結構化程式設計仍然是教授初學者基本程式設計技巧和實踐的重要方法。  物件導向程式設計  物件導向程式設計（Object-Oriented Programming，簡稱OOP）是一種程式設計範式，它使用「  物件 」（objects）這個概念來模擬現實世界中的事物和事物間的互動，從而實現軟件的  封裝 、  繼承 和  多型性 等特性。以下是物件導向程式設計的主要概念和重點：    類別（Class） ：\n   定義了一組具有相同屬性（Attributes）和行為（Methods）的物件的藍圖或模板。  類別是創建物件的基礎，物件是類別的實例。   物件（Object） ：\n   是類別的實例，具有類別定義的屬性和行為。  物件是程式運行時操作的基本單位。   屬性（Attributes） ：\n   代表物件的狀態或特徵，例如：一輛車的顏色、型號等。  在類別中定義，每個物件可以有自己的屬性值。   方法（Methods） ：\n   描述物件能夠執行的操作，例如：一輛車可以加速、停車等。  方法在類別中定義，物件通過方法與外界互動。   封裝（Encapsulation） ：\n   指將物件的細節（屬性和方法）隱藏起來，只暴露有限的介面給外部使用。  通過封裝，可以減少外部對物件內部的依賴，提高程式的模塊性和可維護性。   繼承（Inheritance） ：\n   允許新的類別繼承現有類別的屬性和方法。  繼承支持代碼重用，並能建立一個層次化的類別結構。   多型性（Polymorphism） ：\n   允許不同類別的物件對同一訊息做出回應。  通過多型性，相同的接口可以用於不同的物件，增強程式的彈性和可擴展性。   抽象（Abstraction） ：\n   是將複雜的實際問題抽象化，只關注對當前目標有用的資訊。  通過抽象，可以隱藏背後的複雜實現，只提供必要的操作。   程式碼演示    定義抽象基類 - Vehicle ：在這個步驟中，Vehicle將被定義為一個抽象類別，它包含了一個抽象方法DisplayInfo。     public   abstract   class   Vehicle\n   {\n       // 屬性\n       public   string   Brand   {   get  ;   set  ; }\n       public   int   Year   {   get  ;   set  ; }\n   \n       // 建構子\n       public   Vehicle  (  string   brand  ,   int   year  )\n       {\n           Brand   =   brand;\n           Year   =   year;\n       }\n   \n       // 抽象方法\n       public   abstract   void   DisplayInfo  ();\n   }\n   定義子類別 - Car 和 Truck ：這裡的Car和Truck類別將實現（override）基類的DisplayInfo抽象方法。     public   class   Car   :   Vehicle\n   {\n       public   int   Seats   {   get  ;   set  ; }\n   \n       public   Car  (  string   brand  ,   int   year  ,   int   seats  ) :   base  (brand, year)\n       {\n           Seats   =   seats;\n       }\n   \n       // 實現抽象方法\n       public   override   void   DisplayInfo  ()\n       {\n           Console.  WriteLine  (  $\"Car: {  Brand  }, Year: {  Year  }, Seats: {  Seats  }\"  );\n       }\n   }\n   \n   public   class   Truck   :   Vehicle\n   {\n       public   int   LoadCapacity   {   get  ;   set  ; }\n   \n       public   Truck  (  string   brand  ,   int   year  ,   int   loadCapacity  ) :   base  (brand, year)\n       {\n           LoadCapacity   =   loadCapacity;\n       }\n   \n       // 實現抽象方法\n       public   override   void   DisplayInfo  ()\n       {\n           Console.  WriteLine  (  $\"Truck: {  Brand  }, Year: {  Year  }, Load Capacity: {  LoadCapacity  }kg\"  );\n       }\n   }\n   展示封裝、繼承、多型性和抽象的應用 ：     class   Program\n   {\n       static   void   Main  (  string  []   args  )\n       {\n           // 創建物件\n           Vehicle   myCar   =   new   Car  (  \"Toyota\"  ,   2020  ,   5  );\n           Vehicle   myTruck   =   new   Truck  (  \"Ford\"  ,   2018  ,   1200  );\n   \n           // 通過多型性處理不同的物件\n           List  \u003C  Vehicle  >   vehicles   =   new   List  \u003C  Vehicle  > { myCar, myTruck };\n   \n           foreach   (  Vehicle   vehicle   in   vehicles)\n           {\n               vehicle.  DisplayInfo  ();\n           }\n       }\n   }\n  在這個範例中，Vehicle是一個抽象基類，它定義了一個抽象方法DisplayInfo。Car和Truck是具體的子類別，它們實現了Vehicle中的抽象方法。這樣的設計不僅展示了繼承和多型性（因為Car和Truck都是Vehicle的類型），也利用了抽象來強制要求任何Vehicle子類別都必須提供自己的DisplayInfo方法實現。此外，屬性的使用（如Brand和Year等）以及方法的實現，展示了封裝的概念，因為它們將資料和功能封裝在各自的類別內部。透過這些概念的應用，程式碼變得更加模組化、易於維護和擴展。   物件導向程式設計的這些概念和原則共同作用，使得程式設計更加靈活、模塊化，並且易於維護和擴展。透過對這些概念的理解和應用，開發者可以更有效地處理軟件開發中的複雜問題。  函數式程式設計  函數式程式設計（Functional Programming，簡稱FP）是一種建立在數學函數概念上的程式設計範式。相對於指令式程式設計，它強調應用函數和避免改變狀態及可變數據。以下是函數式程式設計的一些核心概念整理：    不可變性（Immutability） ：是函數式程式設計中的一個重要概念，指的是一旦數據結構被創建後，其內部的數據就不能被改變。這意味著如果你需要修改某個數據結構，你應該創建一個新的數據結構來代替原有結構的某部分，而不是直接修改原有結構中的數據。這種方式有助於提高程式的可預測性，減少錯誤，並且在多線程環境中避免了  競態條件 （Race Conditions）。 \n   \n   優點    簡化了複雜度 ：不需要擔心數據在系統的某個部分被意外改變，因為數據是不可變的。   提高了程式的安全性 ：由於數據不會改變，因此在多線程環境下沒有競態條件問題，這使得程式更加安全。   便於進行時間旅行調試 （Time Travel Debugging）：由於每次數據變更都會產生新的數據結構，因此可以很容易地追蹤數據的變化歷史。 \n  \n   \n   程式碼演示   使用不可變集合\n     using   System  .  Collections  .  Immutable  ;\n   \n   var   list   =   ImmutableList\u003C  int  >.Empty;\n   list   =   list.  Add  (  1  );\n   list   =   list.  Add  (  2  );\n   // list 現在包含 [1, 2]，過程中沒有修改任何集合，而是創建了新的集合。\n  設計不可變類\n     public   class   ImmutablePerson\n   {\n       public   string   Name   {   get  ; }\n       public   int   Age   {   get  ; }\n   \n       public   ImmutablePerson  (  string   name  ,   int   age  )\n       {\n           Name   =   name;\n           Age   =   age;\n       }\n   }\n   \n   var   person   =   new   ImmutablePerson  (  \"Alice\"  ,   30  );\n   // person 的 Name 和 Age 一旦設定，就無法更改。\n \n在這個例子中，ImmutablePerson 的實例一旦創建，其 Name 和 Age 就不可更改，從而保證了實例的不可變性。這種方式有助於創建更加安全、簡潔、可維護的代碼。 \n      純函數 （Pure Functions） ：是函數式程式設計中的一個核心概念，它指的是這樣一種函數：對於相同的輸入，永遠會返回相同的輸出，且在過程中不會有任何副作用（Side Effects），例如不會修改任何外部狀態、不會有 I/O 操作等。 \n   \n   優點    可預測性（Predictability） ：由於純函數對相同的輸入總是產生相同的輸出，這使得它們非常可預測。這種可預測性使得程式的調試和理解變得更加容易。   易於測試（Ease of Testing） ：純函數不依賴於外部狀態，也不產生副作用，因此它們更加容易進行單元測試。測試者無需擔心設置或清理外部環境，只需關注輸入和預期的輸出即可。   無副作用（No Side Effects） ：純函數的執行不會對系統的其他部分產生不可預見的影響。這降低了程式在擴展或修改時產生錯誤的風險。   可移植性／自包含性（Portability / Self-contained） ：純函數不依賴於程序的外部狀態，因此它們更容易在程式的不同部分間移植，或者用於其他程式中。   可利用性（Reusability） ：由於純函數的自包含性和無副作用特性，它們具有很高的可重用性，可以作為程式中的構建塊，被多處使用而不必擔心意外影響。   並行/並行處理（Concurrency/Parallelism） ：純函數不會改變任何外部狀態，這意味著多個純函數可以安全地並行或並行執行，這對於提高性能在多核心處理器的利用上尤為重要。   參照透明性（Referential Transparency） ：由於純函數的結果僅依賴於其輸入，所以它們的呼叫可以被其輸出值替換而不會影響程式的行為，這種性質稱為參照透明性。這進一步提高了程式的可理解性和簡化了優化。   更容易進行形式化驗證（Easier Formal Verification） ：純函數的性質使它們更容易適應形式化的驗證方法，這對於需要高度可靠性的應用場景（如金融和航空系統）非常重要。 \n  \n   \n   程式碼演示     using   System  ;\n   \n   class   Program\n   {\n       // 定義一個純函數\n       static   int   Add  (  int   a  ,   int   b  )\n       {\n           return   a   +   b;\n       }\n   \n       static   void   Main  (  string  []   args  )\n       {\n           int   result   =   Add  (  5  ,   3  );\n           Console.  WriteLine  (result);   // 輸出: 8\n       }\n   }\n  在這個例子中，Add 函數只依賴於其輸入參數 a 和 b，不讀取或修改任何外部狀態（如全局變量或靜態變量），也不執行如打印到控制台或寫文件等有副作用的操作。因此，無論調用它多少次，只要輸入參數相同，它都會返回相同的結果。 \n      頭等物件 （Functions as First-class Citizens） ：是一個核心概念。這意味著在程式語言中，函數被視為一種可以存儲在變量中、作為參數傳遞給其他函數、從其他函數返回，甚至在運行時構造的值。這種對函數的處理方式提升了函數的靈活性，使得函數可以像其他數據類型一樣被操作和使用，從而促進了更高階的抽象和程式碼的重用。 \n   \n   特點   可存儲：函數可以被賦值給變量。  可傳遞：函數可以作為參數傳遞給其他函數。  可返回：函數可以作為其他函數的返回值。  運行時構造：函數可以在程式運行時動態創建。 \n  \n   \n   程式碼演示  C# 中會通過委託（Delegates）和表達式樹（Expression Trees）以及 Lambda 表達式支持將函數作為頭等物件。   函數賦值給變量\n     Func  \u003C  int  ,   int  ,   int  >   add   =   (  a  ,   b  )   =>   a   +   b;\n   int   result   =   add  (  2  ,   3  );   // result = 5\n  函數作為參數傳遞\n     List  \u003C  int  >   numbers   =   new   List  \u003C  int  > {   1  ,   2  ,   3  ,   4  ,   5   };\n   IEnumerable  \u003C  int  >   evenNumbers   =   numbers.  Where  (  n   =>   n   %   2   ==   0  );\n   // Where 接受一個函數作為參數\n  函數作為返回值\n     Func  \u003C  int  ,   Func  \u003C  int  ,   int  >>   createAdder   =   x   =>   y   =>   x   +   y;\n   var   addTwo   =   createAdder  (  2  );\n   int   result   =   addTwo  (  3  );   // result = 5\n  這些例子展示了在 C# 中如何利用 Lambda 表達式和委託來實現函數作為頭等物件的概念。這種處理方式不僅增加了程式碼的靈活性和可重用性，而且也支持了更複雜的函數式程式設計技術，如高階函數和函數組合。 \n     高階函數（Higher-order Functions） ：是函數式程式設計中的一個關鍵概念，指的是那些至少滿足下列一項條件的函數：   接受一個或多個函數作為參數。  返回另一個函數作為結果。  高階函數的存在允許函數操作其他函數，提供了極大的靈活性和表達力，使得開發者可以用更加宣告性的方式來構造程序。 \n   \n   優點    抽象化程度高 ：可以將操作抽象成函數，使得程式碼更加簡潔和可讀。   增強可重用性 ：通過將通用的操作抽象成函數，可以在不同的上下文中重用這些函數。   便於組合和重構 ：函數可以作為構件進行組合，以構建更複雜的操作和數據流。 \n  \n   \n   程式碼演示   接受函數作為參數的高階函數     public   static   void   ProcessList  (  List  \u003C  int  >   list  ,   Func  \u003C  int  ,   int  >   operation  )\n   {\n       for   (  int   i   =   0  ; i   \u003C   list.Count; i  ++  )\n       {\n           list[i]   =   operation  (list[i]);\n       }\n   }\n   \n   List  \u003C  int  >   numbers   =   new   List  \u003C  int  > {   1  ,   2  ,   3  ,   4  ,   5   };\n   ProcessList  (numbers,   x   =>   x   *   x);   // 將列表中每個數字平方\n  返回函數的高階函數     public   static   Func  \u003C  int  ,   int  >   MultiplyBy  (  int   factor  )\n   {\n       return   x   =>   x   *   factor;\n   }\n   \n   var   triple   =   MultiplyBy  (  3  );\n   int   result   =   triple  (  5  );   // result = 15\n \n     函數組合（Function Composition） ：是函數式程式設計的另一個核心概念，它涉及將兩個或更多的函數結合成一個新的函數。在這種模式下，一個函數的輸出成為另一個函數的輸入。這種方法允許開發者建立複雜的操作，透過組合簡單函數來實現，從而增加程式碼的模組化和重用性。 \n   \n   優點    模組化 ：透過將小的、專一的函數組合成一個複雜的函數，可以提高程式碼的模組化。   重用性 ：單獨的函數可以在不同的組合中重複使用，提高了程式碼的重用性。   可讀性和維護性 ：良好設計的函數組合可以使得程式碼更易於理解和維護。 \n  \n   \n   程式碼演示   函數組合在 C# 中的應用     // 定義兩個簡單函數\n   Func  \u003C  int  ,   int  >   doubleIt   =   x   =>   x   *   2  ;\n   Func  \u003C  int  ,   int  >   addOne   =   x   =>   x   +   1  ;\n   \n   // 函數組合\n   Func  \u003C  int  ,   int  >   doubleThenAddOne   =   x   =>   addOne  (  doubleIt  (x));\n   \n   // 使用組合函數\n   int   result   =   doubleThenAddOne  (  3  );   // 結果為 7\n  在這個例子中，doubleThenAddOne 就是一個組合了 doubleIt 和 addOne 的函數。首先對輸入執行 doubleIt 函數，然後將结果作為 addOne 函數的輸入。  C# 的鏈式調用     List  \u003C  int  >   numbers   =   new   List  \u003C  int  > {   1  ,   2  ,   3  ,   4  ,   5   };\n   var   result   =   numbers.  Select  (doubleIt).  Select  (addOne);\n   // result 是 IEnumerable\u003Cint>，包含 [3, 5, 7, 9, 11]\n  在這個例子中，Select 方法被用來應用 doubleIt 和 addOne 函數到列表的每個元素上。展現了如何使用 LINQ 來進行函數組合。 \n     遞迴（Recursion） ：是一種在程式設計中常見的技巧，其中一個函數直接或間接地呼叫自身。這種技術尤其在處理具有自然層次結構的數據，如文件系統的目錄結構、網頁的 DOM 結構、或是數據結構（例如樹和圖）時特別有用。 \n   \n   優點    模組化 ：透過將小的、專一的函數組合成一個複雜的函數，可以提高程式碼的模組化。   重用性 ：單獨的函數可以在不同的組合中重複使用，提高了程式碼的重用性。   可讀性和維護性 ：良好設計的函數組合可以使得程式碼更易於理解和維護。 \n  \n   \n   程式碼演示   計算階乘\n     public   static   int   Factorial  (  int   n  )\n   {\n       if   (n   \u003C=   1  )   // 基本情況\n           return   1  ;\n       else\n           return   n   *   Factorial  (n   -   1  );   // 遞歸步驟\n   }\n   \n   int   result   =   Factorial  (  5  );   // 計算 5!\n \n在這個例子中，Factorial 函數是一個遞歸函數，用來計算一個數的階乘。當 n 小於或等於 1 時，函數直接返回 1（這是基本情況，不需要進一步遞歸）。否則，函數透過 n * Factorial(n - 1) 進行遞歸調用自己（遞歸步驟），直到達到基本情況。 \n    響應式程式設計  響應式程式設計（Reactive Programming）是一種面向數據流和變化傳播的程式設計範式。這意味著，當使用響應式程式設計時，可以輕鬆地表達靜態或動態的數據流，並且當數據流發生變化時，相關計算模型會自動更新。這種程式設計模式在處理具有高度交互性的應用程序中特別有用，例如實時數據處理系統、動態用戶界面和其他需要即時響應數據變化的應用場景。  在C#中，響應式程式設計主要是通過 Reactive Extensions（Rx）庫來實現的。Rx 是一個使用可觀察序列來進行異步程式設計和基於事件的程序庫，它擴展了觀察者模式以支持數據/事件序列，並提供了豐富的操作符來創建、合成、過濾、轉換、以及對這些序列進行其他操作。  核心概念：     可觀察序列 （Observable） ：在 Rx 中，Observable 是表示異步數據流的主要抽象。你可以訂閱（Subscribe）一個 Observable，並且當數據流發出一個或多個值時，訂閱者（Subscriber）就會收到通知。這類似於枚舉（IEnumerable）和迭代器（IEnumerator）模式，但針對的是異步和基於事件的數據流。    訂閱者 （Subscriber） ：訂閱者對 Observable 發出的項目進行響應。在 Rx 中，訂閱者實現了一個或多個接口方法來處理數據流發出的項目、錯誤以及完成信號。   優點    非阻塞 ：數據流的處理是非阻塞的，這意味著你的應用程序可以更有效地使用系統資源，同時處理多個操作。   異步處理 ：Rx 天生支持異步操作，這使得處理時間密集型或 I/O 密集型任務變得更加簡單。   易於合成 ：通過 Rx 提供的豐富操作符，可以輕鬆地合成和轉換數據流，以實現複雜的業務邏輯。   錯誤處理 ：在 Rx 中，錯誤被視為數據流的一部分。這意味著錯誤和正常數據一樣可以被處理和轉換，使得錯誤處理變得更加靈活和強大。   適用場景    實時數據處理 ：如股票報價、實時分析等。   用戶界面開發 ：可以輕鬆實現對用戶操作的響應，如按鈕點擊、滑動事件等。   網路程式設計 ：處理HTTP請求、響應和其他異步I/O操作。   事件驅動系統 ：任何需要處理事件流的系統，如消息隊列消費者、監控系統等。   程式碼演示     using   System  ;\n   using   System  .  Reactive  .  Linq  ;\n   \n   namespace   ReactiveExample\n   {\n       class   Program\n       {\n           static   void   Main  (  string  []   args  )\n           {\n               // 創建一個可觀察序列，從1開始，每隔一秒發出一個數值\n               var   observable   =   Observable.  Interval  (TimeSpan.  FromSeconds  (  1  ))\n                                           .  Select  (  index   =>   $\"Value {  index  }\"  );\n               \n               // 訂閱可觀察序列，並打印出發出的值\n               var   subscription   =   observable.  Subscribe  (value =  >   Console.  WriteLine  (value));\n   \n               // 防止程序直接退出\n               Console.  WriteLine  (  \"Press any key to exit\"  );\n               Console.  ReadKey  ();\n   \n               // 取消訂閱\n               subscription.  Dispose  ();\n           }\n       }\n   }\n  在這個示例中，我們創建了一個每秒發出一個值的可觀察序列，然後通過 Subscribe 方法訂閱了這個序列。當序列發出一個值時，我們的訂閱者就會接收到這個值並將其打印出來。這個過程會一直持續，直到我們通過調用 subscription.Dispose() 手動取消訂閱，或者 Observable 序列自行結束（例如，發出了完成信號或錯誤信號）。   總結  html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"id":380,"path":381,"dir":351,"title":382,"description":7,"keywords":383,"body":390},"content:6.clean-arch:4.chapter4.md","/clean-arch/chapter4","04 結構化程式設計",[384,385,386,387,388,389,356],"證明","公告：這是有害的","功能分解","曾有正式的證明","依靠科學","測試","  證明  公告：這是有害的  功能分解  曾有正式的證明  依靠科學  測試  總結",{"id":392,"path":393,"dir":351,"title":394,"description":7,"keywords":395,"body":399},"content:6.clean-arch:5.chapter5.md","/clean-arch/chapter5","05 物件導向程式設計",[396,397,398,356],"封裝？","繼承？","多型？","  封裝？  繼承？  多型？  多型的威力  依賴反向  總結",{"id":401,"path":402,"dir":351,"title":403,"description":7,"keywords":404,"body":409},"content:6.clean-arch:6.chapter6.md","/clean-arch/chapter6","06 函數式程式設計",[405,406,407,408,356],"整數的平方","不可變與架構","可變性的分離","事件來源","  整數的平方  不可變與架構  可變性的分離  事件來源  總結",{"id":411,"path":412,"dir":351,"title":413,"description":414,"keywords":415,"body":419},"content:6.clean-arch:12.chapter12.md","/clean-arch/chapter12","12 元件","元件 (Component) 是部屬 (deployment) 的單位，也是系統在部屬中最小的部分實體。",[416,417,418],"元件簡史","程式碼 到 執行","Conclusion","  元件 (Component) 是部屬 (deployment) 的單位，也是系統在部屬中最小的部分實體。   在 Java 裡，它是 jar 檔  在 Ruby 中，它是 gem 檔  在 .Net 中，它是 DLL 檔  在 Compiled Language，它們是二進制檔的集合，在 Interpreted Language 中，它是原始檔的集合。  Component 經由 Link 合併到單一執行檔當中。也可以彙總到一個獨立的歸檔 (archive) 例如 .war 檔。或者他們可以部屬為動態載入 plugin，無論最終如何部屬，一個良好的設計始終保持著可獨立性。  元件簡史  早期軟體開發，程式設計師控制程式的記憶體位置和布局。程式的第一行當中會出現一個 origin 語句，是用來宣告程式要載入到哪個記憶體位置。  程式碼 到 執行  當computer要執行時高階語言，無法直接執行需要轉譯成機器語言。   高階語言會先compiler成組合語言。  接著被assembler組譯成機器語言。  再將object file由linker整理成執行檔(executable file)。  最後將執行檔裡的資料copy至適當的記憶體位址。(loader)  \n     連結器(linker)  linker 是一種特殊程序，它將編譯器/彙編器產生的目標檔案和其他程式碼片段組合起來，產生副檔名為 .exe 的可執行檔。在目標檔案中，連結器會搜尋並附加執行檔所需的所有函式庫。它調節保存每個模組程式碼的記憶體空間。它還合併兩個或多個單獨的目標程式並在它們之間建立連結。  linker 執行多項任務，包括：\n符號解析：連結器解析程式中在一個模組中定義並在另一個模組中引用的符號。\n程式碼最佳化：連結器最佳化編譯器產生的程式碼，以減少程式碼大小並提高程式效能。\n記憶體管理：連結器將記憶體位址分配給程式的程式碼和資料部分，並解決出現的任何衝突。\n庫管理：連結器可以將外部庫連結到可執行檔中以提供附加功能。  載入器(loader)   讀取執行檔的header來決定程式碼與資料區的大小。  並以此來產生足夠容納資料與程式碼的記憶體空間。  將資料copy至記憶體。  接著複製主程式的parameter至stack(argument放到local variable)。  初始化暫存器並將stack指標設定在第一個可用空間。  (這裡由os掌控)跳至start-up routine(啟動常式)呼叫要被執行的主程式，當主程式結束返回時，使用exit來結束執行並釋放硬體資源。  Conclusion  這些動態鏈結檔可以在 runtime 時，被插入在一起，這是我們的架構的軟體元件。它花了 50 年走到這裡，但我們已經處於一個新的位置，元件 plugin 架構現在很簡單就能當作是預設架構，而不是以往那種依靠艱鉅努力才能得到的架構。",{"id":421,"path":422,"dir":351,"title":423,"description":424,"keywords":425,"body":430},"content:6.clean-arch:13.chapter13.md","/clean-arch/chapter13","13 元件內聚性","哪個類別該屬於哪個元件? 讓我們來看看元件的 內聚性。",[426,427,428,429,418],"REP (Reuse/Release Equivalence Principle) 再使用性-發布等價原則","CCP (Common Closure Principle) 共同封閉原則","CRP (Common Reuse Principle) 共同重復使用原則","內聚性的張力圖","  哪個類別該屬於哪個元件? 讓我們來看看元件的   內聚性 。  元件可以從一個 library 或是一個 micro service 角度去理解 (by yohuan)。  REP (Reuse/Release Equivalence Principle) 再使用性-發布等價原則   再使用性的細微度就是發布的細微度。  在過去十年裡，已經流行起模組管理工具，幫助我們建立大量可重用的元件以及元件庫。而透過可追蹤的發布程序以及相對應的版本編號，使我們可以重用軟體元件。   發布程序必須產生適當的通知以及文件  讓使用者可以決定何時以及是否要整合新版本  從架構的角度來看，組成一個元件的類別和模組必須是一個有內聚力的的群組。   組合到一個元件中的類別和模組應該被一起發布  共享著相同的版號、相同的版本文件  CCP (Common Closure Principle) 共同封閉原則   將那些會因著相同理由、在相同時間發生變化的類別收集到相同的元件之中。  如同 SRP 規定一個類別不應該包括多個引起變化的原因; CCP 也規定一個元件不應該包含多個引起變化的原因。   如果兩個類別不管在物理上或是概念上有綁定關係，總是一起變化，那他們應該屬於同一個元件。  大多數形況下，可維護性大過可重用性，如果一個應用程式需要更改，那我們希望更改都集中在單一的元件當中，而不是分散在各地。   僅需要重新部署更改的元件  剩下的不用重新驗證以及部署  如此一來，當需求來臨時，我們將變化盡可能的限制在最小數量的元件當中。  CRP (Common Reuse Principle) 共同重復使用原則   不要強迫元件的使用者依賴他們不需要的東西。  換句話說，要將傾向於一起使用的類別和模組放入同一元件當中   Ex: 一個容器類別和其 iterator 應該放在同一元件當中。  如同 ISP 所言，我們不要依賴我們未使用的東西。  當一個元件使用另一個元件時，之間會建立一個依賴關係，也許只使用其中一個小類別，也無法消除這種依賴關係，每當依賴的元件發生變更時，使用的元件可能要   做出相對應的修改  或是重新編譯、驗證、部署   即使是我們沒使用到的小類別發生修改，我們依然無法避免。  這個原則除了告訴我們那些類別該放入相同元件裡，更告訴我們哪些類別 不要 放入相同元件裡。  內聚性的張力圖  上述三個內聚性原則像是在互相拉扯。   REP 和 CCP 是 包容性 原則，傾向使元件變得更大。  CRP 是個 排除性 原則，傾向使元件變得更小。  \n     架構師必須要在這個張力圖當中，找到團隊當前該關注的位置，並隨時做改變。   通常專案的開始會在圖的右側，也就是會犧牲 重用性，隨著時間慢慢往左移動。  假設在開發串接shopee的  元件 時   Hard to reuse:\n   一開始會先考慮時程，而放棄版號控制，並把不同業務邏輯的類別放在一起加速開法，這會導致使用  元件 的開發者不能鎖定版號，如遇到破壞性變更，將導致服務不可用的情況發生。  Too many unneeded releases:\n   因為發生多次不可用的情況，這時候加入版號控制，來因應需求的增長，因為未切割不同業務邏輯的類別，所以修改或增加一個類別就要增加版號，將導致多次不必要增加版號的發布。  Too many components change:\n   專案趨向於穩定，也重新規劃  元件 ，切割成多個不同業務邏輯的  元件 ，如 Auth Product Order...，但當Shopee更改Token驗證方式時，將導致對多個  元件 做修改，增加開發時程。  Conclusion  過去，我們對於內聚性只認為   一個模組只能執行一個功能 ，然而我們在選擇類別組成元件時，必須考慮到可重用性和可發展性的相對力量，並隨時保持動態平衡。",{"id":432,"path":433,"dir":351,"title":434,"description":435,"keywords":436,"body":445},"content:6.clean-arch:14.chapter14.md","/clean-arch/chapter14","14 元件耦合性","本章介紹三個元件之間該有的原則：",[437,438,439,440,441,442,443,444],"無環依賴原則(The Acyclic Dependencies Principle, ADP)","如果有環會造成什麼影響","Top-Down Design","穩定依賴原則(SDP)","穩定性","計算穩定性","並非所有元件都該是穩定的","穩定抽象原則","  本章介紹三個元件之間該有的原則：   相依要無環  相依要穩定  抽象介面要穩定  影響架構的作用力有三種：政治力、技術能力和揮發力  無環依賴原則(The Acyclic Dependencies Principle, ADP)   簡單說就是元件相依有向圖中，不得出現環  那這個原則是怎麼演化而來的呢？   很久很久以前，所有的工程師都編輯相同的一份程式碼，而他們都面臨一個「一覺醒來，發現自己在異世界」的問題。昨天下班前寫的程式明明是正常運作，怎麼早上來上班就壞了？！這是因為有人半夜連ＶＰＮ，修改了同一份的程式碼，意外地，把你昨天寫的東西弄壞了。\n為了解決這個夢魘，有識之士提出了 Ｗeekly Build 週建置\n星期一到星期四，人們在自己的機器上開發，避免編輯到同一份原始碼；星期五的時候，大家將各自的原始碼合併，然後建置新版的程式。因此，人們有四天可以專心開發，再也沒有「起床症候群」的困擾了。當專案很小或是人數很少的時候，大家過著幸福快樂的日子。可惜好景不常，一旦專案越來越大、人越來越多，合併原始碼所需的時間便越拉越長，單單星期五一天處理不完，於是建置時間從星期五、星期四漸漸的往週中延長。於是，時間都拿去處理合併，開發時間被壓縮自然地效率越降越低。\n很顯然週建置還不足以解決問題。  消除依賴還  首先我們要把整個開發分解成一個個的小元件，而且每個元件必須是單人或一組可以負責做完的工作份量。當元件穩定的時候，開發團隊釋出負責的元件並附上版號，讓別人使用釋出的版本，同時間，開發團隊可持續開發也不會影響到使用該元件的程式碼。\n如此一來，當別人釋出新的元件的時候，你可以選擇相依舊的或是新的元件，再也不會因為別人的改動讓你的程式壞掉了！我們解決了之前週建置遇到的問題：   別人修改的程式導致你的功能壞掉  不需要統一時間合併程式碼  但這個前提是  相依有向圖裡面不可以有環 ！否則還是會發生「起床症候群」。\n下圖的相依有向圖中，沒有任何的環。  \n     從相依有向圖，我們可以很清楚地解釋出元件的時候會對誰造成影響   Case: Release   main   在釋出新版的  main 的時候，我們完全不用擔心會影響到其他的元件  Case: Release   Presenters   更新   Presenters  會影響  main  &   view ，這兩個元件的開發者可以自己決定要不要跟著升級  相反的，在開發的時候只要確保相依的套件的版本是對的，就可以保證元件的功能可以正常運作。換句話說，開發者無須考慮全部的元件，只需要專注在相依的元件上即可。  當我們要釋出應用程式或是 linbrary 的時候，由底層向上層建置即可，建置程序非常的清晰明瞭。  如果有環會造成什麼影響  如果 Entities 相依於 Authorizer，使得相依有向圖變成下圖，會發生什麼事？  \n      對於   Database  來說，相依變多導致釋出   Database  的話，需要考慮的事情變多了   Entities ,   Authorizer  和   Interactors  等效上，變成了一個新的且龐大的元件   Entities ,   Authorizer  和   Interactors  之中的任何修改都會相互影響  不知道要怎麼建置   Entities  那我們要怎麼解決環的問題呢？使用 Dependency Inversion Principle (DIP)   在   Entities  裡面新增 interface   Permission  \n     \n     在開發過程中，我們發現元件結構經常改變，我們要時時刻刻確保有向相依圖中沒有環。  Top-Down Design  經由上面的例子，由上而下的設計模式是不存的。因為元件架構隨著開發的進程逐漸演進，不可能在開始一個專案的時候，就決定好元件的架構。隨著開發進程，我們會開始思考要如何重複利用寫好的原件，於是我們可以用 共同重復使用原則(common reuse principle, CRP) 來實作；當元件變小變多的時候，因為你不清楚別人怎麼使用你的原件，所以出現環狀相依的機率越來越大，而出現環狀相依的時候，我們會用 無環依賴原則(acyclic dependencies principle, ADP) 來解決環狀相依，而解決問題的副作用就是讓元件相依架構圖不穩定、時常改變。  另一方面，元件相依圖最重要的功能將「維護性」和「可建置性」圖像化。隨著開發我們的元件架構會越變越複雜，我們可以藉由元件相依圖來管理各個模組、元件的相依關係，以避免出現「起床症候群」，要避免「起床症候群」就要   各模組、元件內的修改所造成的影響範圍要本地化(越小越好)\n   所以在實作的時候，需要注意這兩個原則 共同封閉原則(common closure principle, CCP) 和 單一職責(single responsibility principle, SRP)。還有合併會互相影響的原件。  將系統內某元件經常變動的部分和不常變動的部分分開，讓常變動的元件相依不常變動元件\n   這樣才不會因為常變動的部分導致不變的部分壞掉  穩定依賴原則(SDP)   朝著穩定的方向進行依賴  透過符合共同封閉元則(Common Closure Principle, CCP)，我們會建立對某些變化類型敏感的元件，這些元件被設計成可變的，我們預期它們會變化。\n那麼對於一個預期是可變的元件，他就不應該讓另一個難以更改的元件依賴他，否則就會造原可變的元件難以更改\n這就是軟體的反常的特性，就算你的模組沒有任何的修改，只要別人對你有依賴，就可以讓你的程式變得難以更改。透過遵循 單一職責(single responsibility principle, SRP) 可以確保設計成可變的模組不會被更難以更改的模組所依賴。  穩定性   直立的硬幣：推倒很簡單 => 穩定性低  桌子：推倒不簡單 => 穩定性高  使軟體元件難以變更(變穩定)的因素很多，包括規模、複雜性、清晰程度等等，但我們要來著重在另一種因素，也就是「讓其他元件依賴他」。具有許多輸入依賴關係的元件是很穩定的，因為要使所有依賴它的元件能夠相容變更，往往需要非常大的工作量  下圖是一個穩定的元件 X，有 3 個元件依賴他，因此就有 3 個理由不去更改他，相反地 X 不被任何元件依賴，所有的外部影響都不會使其改變，我們稱 X 是無依賴性的。  \n     下圖是一個不穩定的元件，沒有任何元件依賴 Y，且 Y 依賴於 3 個元件，所以它具有 3 個外部更改源，我們稱 Y 是依賴性的  \n     計算穩定性   FadeIn：輸入依賴度 (被別人依賴)  FadeOut：輸出依賴度 (依賴別人)  I：不穩定性        I  =    F  a  d  e  O  u  t   F  a  d  e  I  n  +  F  a  d  e  O  u  t  I=\\dfrac{FadeOut}{FadeIn+FadeOut}     I   =              F  a  d  e  I  n   +   F  a  d  e  O  u  t        F  a  d  e  O  u  t  ​      \n     Cc 的不穩定性計算：        I  =   1   3  +  1  =   1  4  I=\\dfrac{1}{3+1}=\\dfrac{1}{4}     I   =              3   +   1        1  ​       =              4        1  ​      當 I = 1 時，代表沒有被任何元件依賴，而這個元件有依賴其他元件，這就是一個最不穩定的狀態。  當 I = 0 時，代表有被別的元件依賴但沒有依賴其他元件，它負有責任且無依賴性，處於最穩定的狀態  SDP 的規定是：一個元件的 I 值應該要大於所有他依賴的元件的 I 值 (穩定的元件不能依賴不穩定的元件)  並非所有元件都該是穩定的  下圖展示了理想的配置，把可改變的元件位於頂部並依賴於底部穩定的元件，任何向上的箭頭都意味著違反了 SDP (也會違反後面會提到的 ADP)。  \n     下圖展示了會違反了 SDP 的做法：  \n     我們打算讓 Flexible 元件易於更改，我們希望他是不穩定的，如果 Stable 的元件建立了對 Flexible 的依賴，就違反了 SDP，會讓 Flexible 不再易於更改。  要解決這個問題，就必須解除依賴，假設 Flexible 中有一個 class C 被 Stable 中的 class U 依賴  \n     我們可以用 Dependency Inversion Principle (DIP) 來修正這個問題。  \n     穩定抽象原則   元件的抽象程度應該與元件的穩定程度一致  高層設計的位置 (Where do we put the High-Level policy)  系統中的某些軟體不應該經常改變，我們不希望業務與架構決策是不穩定的，因此應該把「封裝系統高層設計的軟體」放入穩定的元件中 (I=0)，但這就表示這些決策的程式碼會難以更變，又會使得整體架構變得不靈活  我們要怎麼讓一個高穩定性 (I=0) 的元件足夠靈活足以承受變化呢？ 答案就是使用抽象類別來達成 Open-Close Principle (OCP)。  SAP 簡介   一個穩定的元件應該也是抽象的  一個不穩定的元件應該是具體的  DIP 是處理類別的原則，類別一定是抽象或是具體的，沒有灰色地帶  而 SAP 和 SDP 是針對元件的 DIP 原則，規定依賴應該朝著穩定且抽象的方向前進  計算抽象性   Nc：元件中類別總數  Na：元件中抽象類別及介面的總數  A：抽象性        A  =    N  a   N  c  A=\\dfrac{Na}{Nc}     A   =              N  c        N  a  ​      A = 0 就是沒有任何抽象類別，1 就是只有抽象類別  主序列 (The Main Sequence)  最穩定、最抽象的元件會位於左上角 (0,1)，最不穩定、最具體的元件會位於右下角 (1,0)，但並非所有元件都會落在這兩個位置，例如一個抽象類別衍生自另一個抽象類別 (抽象但不是最穩定狀態)，因此我們要來找元件不應該在的位置。  \n     痛苦地帶  在 (0,0) 附近的元件，是一個高度穩定且具體的元件，很難擴展也很難修改  database schema 就是這樣，易變、具體、高度被依賴  工具程式庫也是，例如 String 類別，具體、高度被依賴，但不易變動  越易變的元件在 (0,0) 就越痛苦  無用地帶  在 (1,1) 附近的元件，有抽象性卻沒有被依賴，就是無用的，通常是剩下沒被用到的抽象類別，沒有被人實作。  主序列  為了遠離無用地帶跟痛苦地帶 我們希望我們的元件可以分佈在(0,1)到(1,0)的線上 作者稱為主序列(Main Sequence)\n既然我們知道越靠近主序列越好 那麼對於評價一個元件而言 我們就可以計算這個元件到主序列的距離  \n     當我們量化了一個元件的好壞以後 我們就可以盡情地分析我們的系統啦  你可以用任何你會的統計知識 mean(平均數) variance(變異數) SD(標準差)等等 把一個系統內的所有元件的座標畫在座標圖上  \n     如上圖，我們可以針對超過一個標準差(Z>1)的元件做檢查  另一種是對某個元件依時間畫出 D 值的變化，並設定一個 threshold，並檢查超過 threshold 的元件  \n   ",{"id":447,"path":448,"dir":351,"title":449,"description":450,"keywords":451,"body":460},"content:6.clean-arch:15.chapter15.md","/clean-arch/chapter15","15 什麼是架構","“架構”這個詞彙讓人聯想到權力和神秘，並且讓人感覺是在進行重要決策和技術分析。軟體架構被視為技術成就的巔峰，軟體架構師則是被尊敬和羨慕的對象，許多年輕的工程師都夢想成為軟體架構師。然而，什麼是軟體架構？軟體架構師的具體職責和工作時間又是什麼呢？",[452,453,454,455,456,457,458,459,285],"DEVELOPMENT 開發","DEPLOYMENT 部署","OPERATION 運行","MAINTENANCE 維護","KEEPING OPTIONS OPEN 保持可選項","DEVICE INDEPENDENCE 設備無關性","JUNK MAIL 垃圾郵件","PHYSICAL ADDRESSING 物理尋址","  “架構”這個詞彙讓人聯想到權力和神秘，並且讓人感覺是在進行重要決策和技術分析。軟體架構被視為技術成就的巔峰，軟體架構師則是被尊敬和羨慕的對象，許多年輕的工程師都夢想成為軟體架構師。然而，什麼是軟體架構？軟體架構師的具體職責和工作時間又是什麼呢？  首先，軟體架構師應該是最優秀的程式設計師，他們不僅要寫程式碼，還需引導團隊達到最大化生產力的設計目標。軟體架構的本質是將系統劃分為組件，安排其排列和通信方式，以便於開發、部署、運行和維護。好的架構設計策略是盡可能長時間保留多種選項。雖然架構的首要目的是確保系統正常運行，但更重要的是支撐系統的整個生命周期，使其易於理解和維護。  軟體系統的架構與其能否正常運行關係不大，許多架構糟糕的系統依然能運行良好。真正的問題多出現在系統的開發、部署和維護中。雖然好的架構對系統行為有重要影響，但主要是被動和修飾性的。架構的主要目的是支援系統的全生命周期，使其易於理解、開發、維護和部署，最大化程式設計師的生產力並最小化系統的總運營成本。   1.  架構的角色：軟體架構的本質是決定系統的形狀，包括組件的劃分、排列和通信方式。其主要目的是支援系統的開發、部署、運行和維護，最大化選項的開放性。\n2.  架構師的角色：軟體架構師仍需是程式設計師，並持續參與編程工作。他們不僅要處理程式碼，還需引導團隊實現最大化生產力的設計。\n3.  架構的目標：好的架構應使系統易於理解、開發、維護和部署，目的是最大化程式設計師的生產力，最小化系統的生命周期成本。雖然架構對系統的行為有影響，但主要是被動和修飾性的。\n  DEVELOPMENT 開發  一個難以開發的軟體系統不可能有長久健康的生命周期，因此系統架構應該讓開發變得容易。對於小團隊來說，他們可以高效地開發單體系統，早期階段的架構反而可能成為障礙。這也是許多系統缺乏良好架構的原因之一。  相反，若是由多個團隊開發，則需要將系統分成明確的組件並設置穩定的接口。這樣的架構雖然對部署、運行和維護不一定最佳，但在開發進度的驅動下，團隊通常會選擇這種方式。  DEPLOYMENT 部署  為了有效運行，軟體系統必須易於部署，部署成本越高，系統的可用性就越低。因此，設計軟體架構的一個目標應是實現一鍵式的輕鬆部署。然而，開發初期很少考慮部署策略，這導致一些系統雖易於開發但難於部署。  例如，早期開發可能選擇“微服務架構”，雖然這使開發更簡單，但在實際部署時，微服務數量過多及其連接配置和啟動時間會成為主要問題。如果早期考慮到部署問題，架構師可能會選擇更少的服務，並採用更集成的管理方式。  OPERATION 運行  軟體架構對系統運行的影響遠不及它對開發、部署和維護的影響來得深遠。幾乎所有運行問題都可以通過增加硬體來解決，這避免了重新設計軟體架構的必要。  長期以來，我們經常目睹這種情況發生。對於那些因架構設計不佳而效率低下的系統，只需增加更多的存儲器和服務器，就能讓其有效運行。\n此外，硬體相對於人力成本更低，因此阻礙運行的架構問題不如阻礙開發、部署和維護的架構問題那麼昂貴。  雖然如此，我們仍應該優化軟體架構以更好地支持系統運行，但基於成本效益的考量，優化重點應偏向開發、部署和維護。\n此外，軟體架構還在系統運行中扮演另一重要角色，即清晰傳達系統的運行需求。  理想的架構設計應使系統的運行過程對開發者一目了然，將用例（Use Cases）、功能（Features）和必備行為（Required Behaviors） 提升為明顯的實體化，這不僅簡化了開發者對系統的理解，還大大有助於系統的開發和維護。  MAINTENANCE 維護  在軟體系統的各個方面中，維護成本最高。新功能的需求和系統缺陷的修改會消耗大量人力資源，主要成本來自於“探秘(spelunking)”和“風險(risk)”。“探秘”是指挖掘現有系統以確定新增功能或修復問題的最佳位置和方式，而“風險”則是指在修改過程中可能會引入新的問題。通過精心設計的架構，將系統切分為組件並用穩定的接口隔離，可以顯著降低未來功能添加的成本並減少意外故障的風險。  KEEPING OPTIONS OPEN 保持可選項  軟體有行為價值和架構價值兩種，其中架構價值更重要，因為它使軟體具有靈活性。軟體的靈活性取決於系統的形狀、元件的排列和它們之間的連接方式。保持軟體靈活的關鍵是盡可能長時間地保留多種選項，這些選項通常是不重要的細節設計。  所有軟體系統都可以分解為策略和細節兩部分。策略包含所有業務規則和流程，是系統的真正價值所在。細節則是為了與策略交互所需的技術細節，但不影響策略的行為。架構師的目標是創建一個以策略為核心、細節無關的系統形態，這樣可以延後對細節的決策。  舉例部分   1.  資料庫選擇：在開發初期不需要選擇資料庫系統，因為高層策略不應該關心使用哪種資料庫。實際上，如果架構師足夠小心，高層策略不需要在意資料庫是關聯型、分佈式、多層次還是單純的平面檔案。\n2.  Web 服務選擇：在開發初期不應選定是否使用 Web 服務架構，因為高層策略不應該知道自己會以何種形式發佈。如果高層策略與 HTML、AJAX、JSP、JSF 各項技術等脫鉤，那麼可以將對 Web 系統的選擇推遲到專案後期，甚至不必考慮是否以網頁形式發佈。\n3.  REST 模式選擇：開發初期不應採用 REST 模式，因為高層策略應該與外部介面無關。類似地，不應過早考慮微服務框架或 SOA 框架，高層策略不應該關心這些。\n4.  依賴注入框架：在開發初期不應採用依賴注入框架，因為高層策略不應該操心如何解析系統的依賴關係。\n  這樣做可以延後決策，讓系統有更多時間進行實驗並獲得更多資訊，從而做出更好的決策。如果已有人做出決策，優秀的架構師會假裝決策未定，儘可能延後或改變這些決策。  DEVICE INDEPENDENCE 設備無關性  在 20 世紀 60 年代，由於計算機行業還處於萌芽階段，大部分程式設計師來自數學或其他工程專業，並且有超過三分之一是女性。當時，我們犯了很多錯誤，其中之一就是將程式碼與 I/O 設備直接緊密地綁定在一起。例如，當我們要寫一段在電傳打印機上輸出的 PDP-8 程序時，需要用到一組特定的機器指令。這導致我們的程式碼高度依賴於特定設備。   PRTCHR, 0\n        TSF\n        JMP .-1\n        TLS\n        JMP I PRTCHR\n  這段程式碼中，PRTCHR 是電傳打印機上一段用來打印字符的子程序。首語句中的 0 是存儲其返回地址用的。TSF 指令告訴電傳打印機如果準備就緒，就跳過下一指令；如果繁忙，則繼續執行 JMP .-1 指令。一旦電傳打印機就緒，TSF 跳轉到 TLS 指令，將 A 寄存器中保存的字符發送給電傳打印機，然後 JMP I PRTCHR 指令返回給調用方。  改進措施  隨著時間的推移，我們學到了教訓，並提出了設備無關性這一概念。操作系統將 I/O 設備抽象成處理一條條記錄的標準軟件函數。程序通過調用操作系統提供的服務與這些抽象設備進行交互，從而實現程式碼不經修改即可讀寫卡片或磁帶。這樣，開閉原則 (OCP) 便誕生了。  JUNK MAIL 垃圾郵件  在20世紀60年代末期，我曾在一家為客戶打印群發垃圾郵件的公司工作。客戶會將消費者的名字和地址存儲在磁帶上並寄給我們，我們則負責編寫程序，從磁帶中提取這些信息並將其打印在個性化的廣告信紙上。這些信紙有幾千封，重量近500磅，我們必須一封一封地打印。起初，我們使用的是IBM 360自帶的單行打印機，每個工作日可以打印幾千封信，但這台昂貴的機器租金很高，每月達數萬美金。  為了降低成本，我們改用磁帶來替代單行打印機。這樣一來，IBM 360每10分鐘就可以寫滿一卷磁帶，足夠打印幾卷信紙。這些磁帶被取下後裝載到離線打印機上進行批量打印。我們有五台離線打印機，7x24小時不間斷工作，每週可打印數十萬封信。設備無關性的價值在這裡體現得淋漓盡致：我們的程序不需要關心具體的I/O設備，可以在本地打印機上調試，然後“打印”到磁帶上，最後交由離線打印機進行批量打印。  這段程序的架構設計實現了高層策略與底層實現細節的分離。策略部分負責格式化姓名和地址，細節部分負責操作具體的I/O設備。我們可以在最後階段決定具體使用哪種設備，這種設計大大提高了系統的靈活性和效率。   1. 設備無關性（Device Independence）\n在軟體架構中，設備無關性是指系統不依賴於特定的硬件設備。這在例子中體現為，程序可以不依賴於具體的I/O設備（如單行打印機或磁帶），而是通過操作系統提供的抽象接口進行操作。這種架構設計使得系統具有更大的靈活性，因為它可以根據需要更換不同的設備，而無需修改程序。\n\n2. 策略與細節分離（Separation of Policy and Detail）\n這是軟體設計中的一個重要原則，強調將高層策略（如業務邏輯）與底層實現細節（如具體技術）分離。在例子中，程序的高層策略是格式化姓名和地址，而具體的打印設備是底層的實現細節。這種分離使得系統可以更容易地維護和擴展，因為更改設備或技術不會影響到核心的業務邏輯。\n\n3. 延後決策（Deferring Decisions）\n這個原則強調在設計中儘可能延後對具體技術或設備的決策，以保持系統的靈活性。在例子中，我們可以在程序完成後，根據具體需求選擇是使用本地打印機還是離線打印機，這樣可以根據實際情況做出最優的決策，避免過早鎖定某種技術。\n  PHYSICAL ADDRESSING 物理尋址  在20世紀70年代早期，我參與開發了一個本地卡車工會的大型會計系統。系統存儲的Agent、Employer和Member等不同類型的記錄因尺寸各異，所以我們特別格式化了一塊25MB的硬碟，使其不同的柱面對應於不同記錄的尺寸。由於我們的軟件需硬編碼硬碟的具體結構，包括柱面、磁頭和扇區等詳細信息，這使得每當需要升級硬碟時，我們不得不重寫大量代碼以適應新的硬件結構。  一位經驗豐富的開發者加入給我們的設計思路帶來了轉機。他建議我們將硬碟視為一個巨大的線性扇區陣列，每個扇區由一個連續的整數地址來訪問，這樣我們就可以通過一個小的轉換程序實時地將相對地址轉換成柱面、磁頭和扇區的物理地址。我們採納了這個建議，從而使系統的高層策略不再依賴於硬碟的物理結構，這一改變極大地提高了系統的靈活性和可擴展性，並使我們能夠更容易地適應未來的硬碟升級。   1. 抽象與分離關注點：將系統的高層策略（如業務邏輯）與低層細節（如硬件結構）分離，通過抽象接口來實現。\n例子：將硬碟視為線性扇區陣列，通過相對地址與物理地址的轉換來實現對硬碟的操作。\n\n2. 設備無關性：設計系統時不依賴於特定的硬件設備，通過抽象層來屏蔽具體的硬件實現。\n例子：使用相對地址而非具體的柱面、磁頭、扇區號碼，使系統可以適應不同的硬碟設備。\n\n3. 延後技術決策：在系統開發的初期避免過早鎖定具體的技術實現，保持設計的靈活性，便於未來的技術升級和變更。\n例子：在初期設計中使用相對地址，推遲具體硬碟結構的決策，避免在硬件升級時需要大規模修改代碼。\n  本章小結  架構是指將軟體系統劃分為組件，並安排其排列和通信方式，以便於開發、部署、運行和維護。好的架構設計策略是盡可能長時間保留多種選項，確保系統在整個生命周期中易於理解和維護。  雖然架構的首要目的是確保系統正常運行，但更重要的是支撐系統的整個生命周期，最大化程式設計師的生產力並最小化系統的總運營成本。",{"id":462,"path":463,"dir":351,"title":464,"description":465,"keywords":466,"body":477},"content:6.clean-arch:16.chapter16.md","/clean-arch/chapter16","16 獨立性","正如我們之前提到的，一個良好的架構必須支持以下幾個方面：",[467,454,452,453,468,469,470,471,472,473,474,475,476],"USE CASES 用例","LEAVING OPTIONS OPEN 保留可選項","DECOUPLING LAYERS 按層解藕","DECOUPLING USE CASES 用例的解耦","DECOUPLING MODE 解耦的模式","INDEPENDENT DEVELOP-ABILITY 開發的獨立性","INDEPENDENT DEPLOYABILITY 部屬的獨立性","DUPLICATION 重複","DECOUPLING MODES (AGAIN) 再談解耦模式","CONCLUSION 本章小結","  正如我們之前提到的，一個良好的架構必須支持以下幾個方面：   系統的使用案例  系統的運行  系統的維護  系統的開發以及部署。  USE CASES 用例  系統的架構必須能夠支持其設計的目的。  以購物車應用為例，其架構應直接支持購物車的所有可能用例。  架構師首要關注的是確保架構能支撐這些用例。儘管架構對系統的行為影響不大，但良好的架構能夠清晰地展示和解釋這些行為，使得系統的意圖在架構層面上可見。  這樣的架構讓開發人員能夠容易地辨識系統行為，不需深入尋找。行為會透過類、函數或模塊等主要元素在架構中明顯顯示，並具有清楚描述其功能的名稱。  第21章「尖叫的架構」將對此進行更詳細的闡述。  OPERATION 運行  架構在支持系統運作方面扮演著實際而非僅是外觀的角色。例如，若系統每秒需處理100,000個用戶，則其架構必須支持這種吞吐量和對應用例所需的響應時間。同樣地，如果系統需要在毫秒級別快速查詢大數據庫，架構也應該支持這種操作。  針對不同的系統需求，架構的支持形式也會有所不同。對某些系統而言，這可能意味著將處理元素組織成一系列可在多個服務器上平行運行的小型服務。對其他系統來說，則可能採用許多輕量級線程共享單一處理器上單一進程的地址空間。還有一些系統可能僅需要幾個在獨立地址空間中運行的進程。甚至有些系統可以作為在單一進程中運行的簡單單體程序繼續存在。  儘管可能看似奇怪，這種決策也是優秀架構師應留有彈性的選擇之一。一個基於單體結構開發的系統，對該結構有依賴，將其改造為多進程、多線程或微服務可能並不容易。相較之下，一個在組件間進行適當隔離、且不預設組件間通信方式的架構，將更容易隨著系統運行需求的變化，過渡到不同的線程、進程或服務模式。  DEVELOPMENT 開發  在支持開發環境方面，系統架構扮演著重要角色，這時康威定律就顯得尤為重要。康威定律指出，任何設計系統的組織，其系統設計的結構往往會模仿該組織的溝通結構。這意味著組織內部的溝通方式將直接影響到系統架構的形式。  對於需要由多個團隊合作開發的系統，其架構必須促進團隊間的獨立操作，以避免開發過程中相互干擾。這可以通過將系統適當地劃分成隔離良好、可獨立開發的組件來實現。這些組件隨後可以分配給可以獨立工作的各個團隊，從而保證各隊的獨立性和效率。  DEPLOYMENT 部署  系統架構在決定系統部署的便捷性方面扮演著關鍵角色。目標是實現“立即部署”。一個優良的架構不應依賴於大量配置腳本和屬性文件的調整，也不需要手動創建必須精確配置的目錄或文件。一個好的架構應當支援在構建後即刻部署系統。  實現這一點，需通過適當的系統組件切分和隔離，包括那些將整個系統緊密聯合起來的主要組件。這些組件確保了系統中每個部分的正確啟動、整合與監控，從而支援系統的無縫部署。  LEAVING OPTIONS OPEN 保留可選項  一個優良的架構應該能夠有效地權衡所有先前提到的關注點，並構建一個能夠滿足所有這些需求的組件結構。這聽起來似乎很簡單，但實際上僅是論述上的簡單。  實際上，達成這種平衡相當困難。主要的挑戰是，我們大多數時間都無法完全預知所有的使用案例、運行限制、團隊結構或部署需求。更糟的是，即使我們能知道這些信息，隨著系統生命周期的發展，這些需求也會不可避免地發生變化。總的來說，我們需要達成的目標本身是不明確且變化無常的。  然而，情況並非毫無希望：一些實現成本較低的架構原則可以幫助我們在還沒有明確目標的情況下平衡這些擔憂。這些原則有助於我們將系統劃分為互相隔離良好的組件，使我們能夠在可能的情況下，盡可能長時間地保持多種選擇。  一個好的架構使系統容易進行必要的改變，通過保留選擇的彈性，應對所有必須進行的變更。  DECOUPLING LAYERS 按層解藕  從用例的角度來看，架構師的目標是讓系統結構支持所有必需的用例，但往往無法預知所有的用例。然而，架構師通常知道系統的基本設計意圖，例如是設計購物車系統、運輸清單系統或訂單處理系統。因此，架構師可以運用單一責任原則（SRP）和共同封閉原則（CCP）來隔離因不同原因變更的部分，並集成因相同原因變更的部分。  哪些部分因不同原因變更？在某些情況下這很明顯。例如，用戶界面的變更與業務規則無關，而業務用例則涵蓋這兩者。因此，一位優秀的架構師會將用例的用戶界面部分與業務規則部分分開，使它們可以獨立變更，同時保持用例的清晰和完整性。  業務規則本身可能與應用程序緊密相連，也可能更加通用。例如，輸入欄位的驗證是與應用程序本身緊密相連的業務規則。相反，計算帳戶利息和庫存統計是與特定領域更緊密相關的業務規則。這兩種不同類型的規則變更的速度和原因不同，因此應該將它們分開，以便可以獨立變更。  至於數據庫及其查詢語言，甚至是數據架構，這些都是與業務規則或用戶界面無關的技術細節。這些細節的變更速度和原因獨立於系統的其他方面。因此，架構應該將這些技術細節與系統的其他部分隔離，以便可以獨立進行變更。  通過這種方式，我們可以看到系統被劃分為多個解耦的水平層次——用戶界面、應用特定的業務規則、應用獨立的業務規則，以及數據庫等，僅舉幾例。  DECOUPLING USE CASES 用例的解耦  首先，用例本身也是因不同的原因而變更。例如，添加新訂單的用例與刪除訂單的用例在變更的原因和速率上幾乎肯定是不同的。因此，按照用例來劃分系統是一個非常自然的選擇。  同時，這些用例構成了貫穿系統水平分層的垂直切面。每個用例都會涉及一些用戶界面（UI）、特定應用的業務規則、應用無關的業務規則以及數據庫功能。所以，在我們將系統劃分為水平層的同時，也在按用例將其劃分為穿透這些層的狹窄垂直用例。  為了達到這種解耦，我們需要將添加訂單用例的用戶界面與刪除訂單用例的用戶界面分開處理。同樣的處理方式也適用於業務規則和數據庫，我們應保持用例在系統的垂直方向上保持分離。  從這些設計中，我們可以看到一個模式：如果我們根據不同的變更原因對系統元素進行解耦，那麼我們就可以在不干擾現有用例的情況下持續添加新的用例。如果我們還將支持這些用例的用戶界面和數據庫分組，使得每個用例使用UI和數據庫的不同方面，那麼添加新的用例就不太可能影響到舊有的用例。  DECOUPLING MODE 解耦的模式  首先，考慮這些解耦對系統運行的影響。如果不同方面的用例得到良好隔離，那麼需求高吞吐量的用例自然與需求低吞吐量的用例分開。此外，如果用戶界面和數據庫從業務規則中分離，它們便可以在不同的伺服器上運行。對於那些需要較高帶寬的應用，可以在多個伺服器上進行複製。  總之，為了用例而進行的解耦也有助於改善系統的運行效率。然而，要從運營上獲得好處，解耦必須採用適當的方式。例如，為了能在不同伺服器上運行，分離的組件不能依賴於同一處理器的同一地址空間，它們必須是獨立的服務，通過某種網絡進行通訊。  許多架構師將這些組件稱為“服務”或“微服務”，具體稱呼往往依賴於程式碼行數的模糊概念。基於服務的架構常被稱為面向服務的架構（Service-Oriented Architecture, SOA）。這裡提到的SOA或微服務並非是提倡它們就是最佳架構或未來趨勢，而是強調有時我們需要將組件分隔到服務層次。  請記住，優秀的架構會保持彈性選擇。解耦模式就是這些選擇之一。在深入探討這個話題之前，我們會回顧其他兩個設計目標。  INDEPENDENT DEVELOP-ABILITY 開發的獨立性  在架構設計的第三個目標——支持系統開發方面，當系統組件之間實現高度解耦後，開發團隊間的干擾會大幅減少。例如，如果業務規則與用戶界面（UI）無關，那麼專注於UI開發的團隊就不太可能對專注於業務規則的團隊造成影響。同理，如果各個用例之間也實現了解耦，專注於添加訂單（addOrder）用例的團隊不大可能干擾到處理刪除訂單（deleteOrder）用例的團隊。  只要系統的層與用例進行了適當的解耦，無論團隊是按功能、按組件、按層次還是其他任何方式組織，系統的架構都能支持多團隊合作的組織架構。這樣的解耦不僅提高了開發效率，也增加了各團隊之間的協作彈性。  INDEPENDENT DEPLOYABILITY 部屬的獨立性  這種按用例和水平分層的解耦同樣為系統的部署帶來了極高的靈活性。實際上，如果解耦工作做得很好，我們甚至可以在系統運行中進行各個分層實現和具體用例的熱切換。在這種情況下，增加新的用例可能僅僅需要在系統中添加一些新的jar文件或啟動一些服務，而不會影響到系統的其他部分。  DUPLICATION 重複  架構師常常會陷入一種困境，那就是害怕重複。在軟件開發中，重複通常被認為是壞事，我們不喜歡代碼的重複。當真正的代碼重複出現時，我們作為專業人士有責任去減少並消除這種重複。  然而，重複有不同的類型。真正的重複指的是當一個實例發生變更時，必須在該實例的所有副本上做出同樣的變更。還有一種是假的或偶然的重複，如果看似重複的代碼部分隨著時間沿著不同的路徑演進——它們以不同的速率變更，出於不同的原因——那麼它們就不是真正的重複。幾年後回顧，你會發現它們已經大不相同。  想像有兩個用例在屏幕結構上非常相似，架構師可能會強烈傾向於共用那些結構的代碼。但他們真的應該這麼做嗎？這是真正的重複還是偶然的重複？大多數情況下，這種重複很可能是偶然的。隨著時間的推移，這兩個屏幕可能會逐漸演變，最終變得完全不同。因此，應該小心避免將它們統一起來，否則未來分離它們將會是一個挑戰。  當我們按用例垂直切分系統時，這樣的問題會經常出現，並且我們常常會被誘惑去耦合這些用例，因為它們具有相似的屏幕結構、相似的算法或相似的數據庫查詢和/或架構。一定要小心，抵抗因應激性反應而消除重複的誘惑。確保這種消除僅針對真正的重複。  同樣，當我們對系統進行水平分層時，也可能會發現某個數據庫記錄的數據結構與某個屏幕視圖的數據結構非常相似。你可能會被誘惑，直接將數據庫記錄上傳到用戶界面，而不是創建一個看似相同的視圖模型並跨模型複製元素。請小心：這種重複幾乎肯定是偶然的。創建獨立的視圖模型並不會花費太多努力，並且它將幫助你保持層之間適當的解耦。  DECOUPLING MODES (AGAIN) 再談解耦模式  讓我們回顧解耦模式的問題。系統可以通過多種方式進行按水平分層和用例解耦，包括在源碼層次、二進制代碼（部署）層次以及執行單元（服務）層次。  在源碼層次解耦中，我們可以控制源代碼模組之間的依賴，從而使得對一個模組的變更不會強迫對其他模組進行改變或重新編譯。在這種模式下，所有組件都在同一個地址空間內執行，並通過簡單的函數調用彼此交互，系統以單一可執行文件的形式載入到計算機內存中。  部署層次解耦則涉及控制可部署單元（如Jar文件、DLL或共享庫）之間的依賴，使得單一模組的源碼變更不會引起其他模組的重建和重新部署。這些組件可能仍然位於同一地址空間中，並通過函數調用進行通訊。而一些其他組件可能存在於同一處理器下的不同進程中，並通過進程間通訊、套接字或共享內存進行通信。  服務層次解耦則將組件間的依賴降到數據結構層面，僅通過網絡數據包進行通信，使得每個執行單元對其他單元的源碼和二進制變更完全獨立。  選擇哪種模式最合適，取決於項目的早期階段難以確定。隨著項目的成熟，最佳模式可能會發生變化。例如，一個目前在單一服務器上運行良好的系統可能最終需要一些組件在不同的服務器上運行。  流行的解決方案之一是默認在服務層次進行解耦。這種方法成本較高，並且傾向於粗粒度解耦。即使微服務的粒度很小，解耦也可能不夠精細。  服務層次解耦不僅開發時間和系統資源成本高昂，處理不必要的服務邊界也是一種努力、記憶和週期的浪費。記憶和週期雖便宜，但人力成本始終較高。  我的偏好是將解耦推進到可以形成服務的地步，但只要可能，就讓組件保持在同一地址空間內。這樣做留給未來轉變為服務的選項。  這種方法起初在源碼層次對組件進行分離，這對項目的整個壽命可能已經足夠。然而，如果出現部署或開發問題，將一些解耦推進到部署層次可能就足夠了。  隨著開發、部署和運營問題的增加，我會仔細選擇哪些可部署單元轉化為服務，並逐步將系統向該方向轉移。隨著時間的推移，系統的運維需求可能會下降，曾經需要在服務層次進行解耦的系統現在可能只需在部署層次甚至是源碼層次解耦。  一個良好的架構應允許系統以單體結構出生，以單一文件部署，然後逐漸成長為一組相互獨立的可部署單元，最終發展到獨立的服務或微服務。後來，隨著情況變化，應允許系統逐步回退到單體結構。良好的架構還應保護大部分源碼免受這些變更的影響，將解耦模式保持為一個選項，使得大規模部署可以使用一種模式，小規模部署則可以使用另一種模式。  CONCLUSION 本章小結  要達成先前討論的解耦要求確實具有一定難度。我並未暗示系統的解耦模式應該只是一個簡單的配置選項（雖然在某些情況下這是適當的）。我想表達的是，系統的解耦模式很可能隨著時間的推移而變化，一位優秀的架構師應該能夠預見這些變化並適當地促進這些變化的發生。",{"id":479,"path":480,"dir":351,"title":481,"description":482,"keywords":483,"body":490},"content:6.clean-arch:18.chapter18.md","/clean-arch/chapter18","18 邊界剖析","一個系統的架構由一系列軟體元件及它們之間的邊界共同定義，這些邊界有著多種不同存在形式。\n在本章可以看看最常見的一些形式",[484,485,486,487,488,489],"BOUNDARY CROSSING 跨邊界調用","THE DREADED MONOLITH 令人生畏的單體結構","DEPLOYMENT COMPONENTS 部署層次的組件","THREADS 線程","LOCAL PROCESSES 本地進程","SERVICES 服務","  BOUNDARY ANATOMY (邊界剖析)  一個系統的架構由一系列軟體元件及它們之間的邊界共同定義，這些邊界有著多種不同存在形式。\n在本章可以看看最常見的一些形式   BOUNDARY CROSSING 跨邊界調用  跨邊界調用是軟體架構中的一個關鍵概念。\n在運行時,它實際上是邊界一側的函數調用另一側的函數,並在過程中傳遞數據。  這種調用看似簡單,但其背後涉及到複雜的程式碼依賴管理。  構建合適的跨邊界調用機制,核心在於如何妥善處理這些依賴關係。\n這不僅關係到程序的正常運行,更影響到整個系統的可維護性和擴展性。   來看個例子 訂單與庫存管理\n   尚未明確的邊界時     // 庫存管理類\n   class   InventoryManager   {\n       checkStock  (  productId  :   string  )  :   boolean   {\n           console.  log  (  `檢查產品   ${  productId  }   的庫存`  );\n           return   true  ;   // 假設總是有庫存\n       }\n   }\n   \n   // 訂單處理類\n   class   OrderProcessor   {\n       private   inventoryManager  :   InventoryManager  ;\n   \n       constructor  () {\n           this  .inventoryManager   =   new   InventoryManager  ();\n       }\n   \n       processOrder  (  productId  :   string  )  :   void   {\n           if   (  this  .inventoryManager.  checkStock  (productId)) {\n               console.  log  (  `處理產品   ${  productId  }   的訂單`  );\n           }   else   {\n               console.  log  (  `產品   ${  productId  }   缺貨`  );\n           }\n       }\n   }\n   \n   // 使用示例\n   const   orderProcessor   =   new   OrderProcessor  ();\n   orderProcessor.  processOrder  (  \"特大杯冰拿\"  );\n  引入邊界後的設計     // 庫存檢查介面\n   interface   StockChecker   {\n       checkStock  (  productId  :   string  )  :   boolean  ;\n   }\n   \n   // 庫存管理類實現 StockChecker 介面\n   class   InventoryManager   implements   StockChecker   {\n       checkStock  (  productId  :   string  )  :   boolean   {\n           console.  log  (  `檢查產品   ${  productId  }   的庫存`  );\n           return   true  ;   // 假設總是有庫存\n       }\n   }\n   \n   // 訂單處理類\n   class   OrderProcessor   {\n       constructor  (  private   stockChecker  :   StockChecker  ) {}\n   \n       processOrder  (  productId  :   string  )  :   void   {\n           if   (  this  .stockChecker.  checkStock  (productId)) {\n               console.  log  (  `處理產品   ${  productId  }   的訂單`  );\n           }   else   {\n               console.  log  (  `產品   ${  productId  }   缺貨`  );\n           }\n       }\n   }\n   \n   // 使用示例\n   const   inventoryManager   =   new   InventoryManager  ();\n   const   orderProcessor   =   new   OrderProcessor  (inventoryManager);\n   orderProcessor.  processOrder  (  \"特大杯冰拿\"  );\n  那麼,為什麼程式碼依賴管理如此重要呢?這是因為軟體開發是一個持續變化的過程。  當一個程式碼模組發生變更時,它可能會觸發一連串的反應:其他相關模組可能需要修改,重新編譯,甚至重新部署。  這種連鎖反應可能會導致系統不穩定,增加維護成本。  因此,邊界的主要作用就是在這些模塊之間建立\"防火牆\",以管理和控制這種變更的傳播。  通過合理設置邊界,我們可以將變更的影響限制在特定範圍內,從而提高系統的穩定性和可維護性。   THE DREADED MONOLITH 令人生畏的單體結構  單體架構是最常見的系統架構形式，它將  所有功能和數據劃分在同一個進程 和地址空間內。儘管這些邊界在部署時不可見，但對於獨立開發系統組件非常重要。  單體架構依賴  動態多態來管理 內部依賴關係，低層客戶端通常直接調用高層服務函數，而當高層需要低層服務時，則利用動態多態反轉依賴。  在單體架構中，組件間的通信是通過函數調用進行的，速度快且成本低。這些組件間的  通信頻繁且緊密耦合 ，系統的組件通常是以源碼形式進行交付，並在部署時經過編譯和靜態鏈接。  這種有紀律的分區設計有助於提升開發、測試和部署的效率。  單體結構雖然簡單直接，但隨著系統規模增長，會逐漸顯現出令人生畏的特性：   複雜性增加：隨著功能不斷添加，整個系統變得越來越複雜，難以理解和維護。  擴展性受限：無法針對特定模組單獨進行擴展，必須擴展整個系統。  部署風險：任何小改動都需要重新部署整個系統，增加了部署的風險和困難。  技術棧限制：整個應用被綁定到同一個技術棧，難以採用新技術。  團隊協作困難：大型單體應用可能導致代碼衝突和集成問題，影響開發效率。  可靠性問題：任何模組的錯誤可能影響整個系統的穩定性。  長期維護挑戰：隨著時間推移，可能形成\"大泥球\"，使得系統越來越難以維護和更新。  這些因素綜合起來，使得大型單體結構在長期發展中變得令人生畏，尤其是對於需要快速迭代、高度可擴展的現代應用來說更是如此。   DEPLOYMENT COMPONENTS 部署層次的組件  系統架構中的物理邊界常見形式是動態連結庫，例如 .Net 的 DLL、Java 的 jar 檔案等，這些元件在部署時以二進制形式交付，不需要重新編譯，實現了部署層次上的解耦模式。專案部署時，將所有可部署單元打包成便於操作的格式，如 WAR 檔案或目錄。  這類元件與單體結構相似，函數仍在同一程序和位址空間中，管理元件劃分和依賴關係的策略也一致。元件間的跨邊界調用是普通的函數調用，雖然動態連結或執行時載入會有一次性成本，但跨邊界通信調用仍然頻繁。   簡單例子  假設有一個簡單的線上購物系統，其中包含用戶管理、商品管理和訂單管理等功能。這些功能可以用不同的動態連結庫來實現，例如：    User.dll ：負責用戶註冊、登入、資料管理等功能。   Product.dll ：處理商品資訊的添加、修改、刪除等功能。   Order.dll ：管理訂單的創建、查詢、支付等操作。  在這個系統中，各個功能的實現被打包成獨立的 DLL 檔案，這樣的架構可以在不重新編譯整個系統的情況下更新單個功能模塊。這些 DLL 檔案在部署時作為獨立的二進制單元一起打包成最終的應用程式，例如放在同一個目錄中運行。系統內部，使用函數調用在不同 DLL 之間進行通信，例如訂單模塊需要查詢用戶資訊時，會通過普通的函數調用來訪問 User.dll 提供的功能。  這種方式與單體結構相似，所有 DLL 在同一個程序和位址空間中運行，跨邊界的調用就像普通的函數調用一樣，調用成本很低，實現了部署層次上的解耦。   THREADS 線程  單體結構和按部署層次劃分的元件都能使用線程。  線程既不是架構邊界，也不是部署單元，而是一種管理程序執行順序和調度的方式。  一個線程可以完全包含在單一元件內，也可以橫跨多個元件。   簡單例子  假設我們有一個網上商店應用程式，它包含以下幾個主要元件：   用戶界面（UI）元件  商品目錄元件  購物車元件  訂單處理元件  這個應用程式可能會使用多個線程來提高效能和用戶體驗：   主線程：負責處理用戶界面的更新和響應用戶操作。  商品加載線程：當用戶瀏覽商品目錄時，這個線程負責從資料庫加載商品信息，以不阻塞主線程的方式實現平滑的滾動和瀏覽體驗。  訂單處理線程：當用戶下單時，這個線程負責處理訂單，包括庫存檢查、支付處理等，讓用戶可以繼續瀏覽網站而不需等待訂單處理完成。  在這個例子中，商品加載線程可能會跨越 UI 元件和商品目錄元件，而訂單處理線程則可能跨越購物車元件和訂單處理元件。這些線程並不受元件邊界的限制，而是根據功能需求來組織的。   LOCAL PROCESSES 本地進程  本地進程是系統架構中的一個強隔離邊界，它們有獨立的地址空間，通常通過 socket 或操作系統提供的通信工具（如消息佇列）進行交互。每個進程可以是單體結構或由動態部署的組件組成。  進程間的隔離策略類似於單體結構和二進制組件，源碼依賴指向更高層級的組件，高層進程不應直接依賴低層進程。跨進程通信的成本較高，因此需謹慎控制頻率。   簡單例子  假設我們有一個網頁應用程式，它分為前端和後端兩個部分：    前端進程 ：負責顯示用戶介面，處理用戶的輸入，例如使用 JavaScript 和 HTML 來渲染網頁。   後端進程 ：負責處理數據邏輯，例如使用 Node.js 或 Python 來處理資料庫查詢和業務邏輯。  這兩個進程運行在不同的地址空間中，通過 HTTP 協議（如 REST API 或 WebSocket）來進行通信。前端發送請求到後端，後端處理後返回結果給前端。這樣的進程隔離讓前後端可以獨立運作並易於維護，也降低了相互間的依賴度。  比如，當前端需要顯示用戶資料時，它不會直接去查資料庫，而是通過 HTTP 請求讓後端進程去做查詢，這樣保持了進程之間的清晰邊界和隔離。   SERVICES 服務  在系統架構中，最強大的隔離方式是使用服務。簡單來說，服務就是一個獨立運行的程式，可以用命令行或系統指令啟動，不依賴於特定的硬體或地點。  服務之間的溝通是透過網路完成的，速度會比程式內部的函數呼叫慢很多，可能需要從幾十毫秒到幾秒不等的時間。  所以，在設計時，我們應該盡量減少這些服務之間的溝通次數，並且要能夠處理網路延遲的情況。  在服務的層次上，較低層的服務應該像「插件」一樣為較高層的服務提供功能，而較高層的服務不應該知道低層服務的具體細節，例如服務的位置或網址。  這樣的設計原則有助於保持系統的靈活性和擴充能力。   簡單例子  假設你在使用一個線上購物網站，這個網站背後有多個不同的服務在支援。比方說，這個網站有一個「商品庫存服務」負責管理商品的數量，還有一個「支付服務」負責處理用戶的付款。  網站同時連接到這兩個服務，但兩個服務之間並沒有直接的連接，反映了它們的獨立運行特性。 \n    \n      \n    html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"id":492,"path":493,"dir":351,"title":494,"description":495,"keywords":496,"body":498},"content:6.clean-arch:19.chapter19.md","/clean-arch/chapter19","19 POLICY AND LEVEL 策略與層次","基本上，所有的軟體都是一組策略聲明的集合。簡單來說電腦程式就是一組仔細描述如何將輸入轉換為輸出的策略聲明的集合。",[497,476],"Level（層次）","  基本上，所有的軟體都是一組策略聲明的集合。簡單來說電腦程式就是一組仔細描述如何將輸入轉換為輸出的策略聲明的集合。  大多數非小型的系統中，整體業務策略通常會細分為多組更小的策略聲明。   一部分用於描述計算部份的業務邏輯。  一部分負責描述計算報告的格式(即展示或呈現運算結果)。  還有一些用來描述如何驗證輸入資料的策略。  程式架構設計的重點之一。   將這些策略彼此分離，根據它們的變更方式進行重新分組。  變更原因、時間和層次相同的策略應該分在同一個組件中。  架構設計的工作通常需要將組件重新排列，組合成一個有向無環圖。  \n     \n  圖 19.1\n   圖 19.1 中的每個節點代表擁有相同層次策略的組件，每條單向鏈接代表一種組件之間的依賴關係，這些鏈接將不同層級的組件連接起來。  這裡的依賴關係是指程式碼層次上、編譯期的依賴關係。   在 Java 語言中就是指 import 陳述句。  在 C# 語言中就是指 using 陳述句。  在 Ruby 語言中就是指 require 語句。\n這些依賴關係都是在編譯過程中所必需的。  在設計良好的架構中，依賴關係的方向通常取決於它們所關聯的組件層次。通常，低層組件會被設計為依賴於高層組件。  Level（層次）  「層次」嚴格根據「輸入與輸出之間的距離」來定義。也就是說，一條策略距離系統的輸入/輸出越遠，它所屬的層次就越高。相反，直接管理輸入/輸出的策略，其在系統中的層次則是最低的。  高層次與低層次元件的概念   高層次元件：通常與具體的實作細節無關，專注於抽象邏輯或業務邏輯。它們不直接依賴於輸入或輸出的細節，而是依賴於較低層次的元件來完成這些任務。  低層次元件：與具體的實作細節密切相關，處理直接的硬體操作、I/O 或資料流操作，通常是與外部世界（像是使用者、硬體設備）互動的元件。  架構圖中的高低層次解釋  圖 19.2 是一個簡單加密程式的資料流向圖，該程式從輸入裝置讀取字符，然後用查表法轉換這些字符並將轉換後的字符輸出到輸出裝置。我們將圖中資料的流向用彎曲實心箭頭標識了出來，而對於經精妙設計過的源碼中的依賴關係則使用直虛線來標識。  \n     \n  圖 19.2\n    Translate 是高層次元件：該元件與具體的 I/O（輸入/輸出）操作無關。它專注於字元的「轉換」，這是一種抽象邏輯。Translate 不直接處理如何讀取字元或寫入字元，而是依賴低層次的 Read Char 和 Write Char 來完成這些任務。  Read Char 和 Write Char 是低層次元件：這兩個元件與具體的 I/O 操作直接相關。Read Char 負責從某個來源（例如：鍵盤、檔案）讀取字元，Write Char 則負責將字元寫到某個目的地（例如：螢幕、檔案）。這些操作屬於具體的實作細節，因此是低層次元件。  但我們很容易將這個加密程式寫成下面這樣，這就構成了一個不正確的架構：     function   encrypt  () {\n       while   (  true  )\n           writeChar  (  translate  (  readChar  ()));\n   }\n  為何架構不正確   過度依賴低層次元件：根據圖 19.2 中的架構，Translate 與 Read Char 和 Write Char 之間有「source code dependencies」（原始碼依賴）。這意味著 Translate 依賴於具體的 I/O 操作（Read Char 和 Write Char），這導致高層次元件對低層次元件的依賴性過高。   理想的架構應該是高層次元件不依賴低層次元件，反而應該是低層次元件依賴高層次元件來完成具體的任務。這樣可以讓高層次邏輯更加獨立，易於修改和擴展，而不受具體的輸入/輸出限制。  反轉依賴原則（Dependency Inversion Principle, DIP）違反：DIP 是一個常見的設計原則，強調高層次元件不應該依賴低層次元件，而是雙方都應該依賴於抽象（例如接口）。這裡 Translate 直接依賴了具體的 I/O 操作，違反了這個原則。  改進建議：  \n     \n  圖 19.3\n   圖 19.3 中展示了一個基於依賴倒轉原則（Dependency Inversion Principle, DIP）的設計，這個原則的核心理念是「高層次模組不應該依賴低層次模組，兩者都應依賴於抽象」。透過依賴注入或接口設計來隔離 Translate 與具體的 I/O 操作。  例如，Translate 可以依賴抽象的接口，而具體的 Read Char 和 Write Char 則實現這些接口。這樣可以讓高層次邏輯不依賴於具體的實作細節，使架構更具彈性。  \n      箭頭圖示說明   Encrypt 到 Interface 的箭頭（Encrypt 依賴 ICharReader 和 ICharWriter）\n   箭頭是實線，代表依賴關係。  Encrypt 直接 依賴 於 ICharReader 和 ICharWriter，這表示 Encrypt 會使用這兩個接口來進行操作。Encrypt 需要透過這兩個接口來執行具體的字元讀寫工作。  Console Reader/Writer 到 Interface 的箭頭（ConsoleCharReader 和 ConsoleCharWriter 依賴接口）\n   箭頭是虛線，表示實作或繼承關係。  ConsoleCharReader 和 ConsoleCharWriter 這兩個具體實作了 ICharReader 和 ICharWriter 接口，表示它們提供了這些接口的方法的具體實作。  \n      依賴方向說明   圖 19.3 中，高層次的元件是 Encrypt，它依賴於兩個抽象的元件：Char Reader 和 Char Writer。  Char Reader 和 Char Writer 是具體的操作（如讀取字元和寫入字元），但它們並非直接依賴於具體的實現，如 Console Reader 或 Console Writer，而是依賴於接口（I）。  Console Reader 和 Console Writer 是具體的實作，它們實現了 Char Reader 和 Char Writer 的接口。  這樣做的目的是：\n   Encrypt 作為高層次邏輯，僅依賴抽象（接口 ICharReader 和 ICharWriter），而不依賴具體的輸入/輸出操作（例如從控制台讀取或寫入）。  如果將來需要改變具體的 I/O 實現（例如從檔案讀取字元而不是從控制台），可以通過替換 Char Reader 和 Char Writer 的具體實現來實現，Encrypt 不需要任何改動。  \n      程式碼範例  定義接口（抽象）     public interface ICharReader\n   {\n       char   ReadChar  ();\n   }\n   \n   public interface ICharWriter\n   {\n       void   WriteChar  (  char   c);\n   }\n  低層次組件（具體實作）     public class ConsoleCharReader : ICharReader\n   {\n       public   char   ReadChar  ()\n       {\n           // 假設從控制台讀取一個字元\n           return   Console.  ReadKey  ().KeyChar;\n       }\n   }\n   \n   public class ConsoleCharWriter : ICharWriter\n   {\n       public   void   WriteChar  (  char   c)\n       {\n           // 將字元寫入控制台\n           Console.  Write  (c);\n       }\n   }\n  高層次組件（邏輯）     public class Encrypt\n   {\n       private readonly ICharReader _reader;\n       private readonly ICharWriter _writer;\n   \n       public   Encrypt  (ICharReader reader, ICharWriter writer)\n       {\n           _reader   =   reader;\n           _writer   =   writer;\n       }\n   \n       public   void   PerformEncryption  ()\n       {\n           while   (  true  )\n           {\n               // 讀取字元、加密（假設這裡做了某種轉換或加密操作）\n               var c   =   _reader.  ReadChar  ();\n               var encryptedChar   =   Translate  (c);   // 假設這是加密邏輯\n               _writer.  WriteChar  (encryptedChar);\n           }\n       }\n   \n       private   char   Translate  (  char   c)\n       {\n           // 簡單示範：將字元轉換為下一個字母（例如：a -> b）\n           return   (  char  )(c   +   1  );\n       }\n   }\n  執行範例     class Program\n   {\n       static   void   Main  ()\n       {\n           // 使用 ConsoleCharReader 和 ConsoleCharWriter 作為具體實現\n           ICharReader reader   =   new   ConsoleCharReader  ();\n           ICharWriter writer   =   new   ConsoleCharWriter  ();\n           \n           Encrypt encrypt   =   new   Encrypt  (reader, writer);\n           encrypt.  PerformEncryption  ();\n       }\n   }\n  依賴倒轉的具體原則   高層次模組（Encrypt）依賴於抽象：Encrypt 只知道 ICharReader 和 ICharWriter，不需要知道具體的實現。  低層次模組依賴於抽象：Console Reader 和 Console Writer 這些低層次實作依賴於接口（ICharReader 和 ICharWriter），它們負責具體的行為實作。  這樣的設計使得高層次模組和低層次模組之間的依賴不再是單向的（即高層模組依賴低層模組），而是透過抽象來進行解耦，實現依賴的倒轉。  透過依賴倒轉原則，我們使 Encrypt 依賴於介面（ICharReader 和 ICharWriter），這樣未來如果要替換具體的 Reader 或 Writer 實作（例如從檔案讀取或寫入），只需要實現新的類別即可，而不需要修改 Encrypt 類別。  這種設計提高了系統的彈性和可維護性，讓高層邏輯與低層細節實現解耦。  策略分組  如同開頭所提到的，我們應該根據策略發生變更的方式將它們分成不同的元件。變更原因和變更時間相同的策略應在 SRP 和 CCP 這兩個原則的指導下合併為同一組件。離輸入/輸出最遠的策略—高層策略—一般變更沒有那麼頻繁。即使發生變更，原因也比低層策略所在的元件更重大。反之，低層策略則很有可能會頻繁地進行一些小變更。  \n      SRP 和 CCP 補充   SRP - 單一職責原則（Single Responsibility Principle）\n   SRP 的核心理念是每個模組或類別應僅負責一個職責。也就是說，一個類別或模組應該只有一個引起它變更的原因。如果一個類別承擔了過多的職責，當其中一個職責發生變更時，這個類別就需要被修改，導致程式碼變得不穩定、難以維護。  範例：\n   假設有一個類別 Employee，既負責處理員工的薪資計算邏輯，又負責將員工資料儲存到資料庫。這樣一來，如果薪資計算邏輯發生變化，或是資料庫邏輯變更，Employee 類別都需要被修改，違反了 SRP。  改進：將薪資計算邏輯和資料庫保存邏輯分離到不同的類別中，這樣每個類別只負責一個職責，遵循了 SRP。  CCP - 共同封閉原則（Common Closure Principle）\n   CCP 的意思是應該把那些因為相同的原因而同時變化的類別封裝到同一個元件中。換句話說，一個元件中的所有類別應該一起修改，因為它們共享相同的變更原因。這樣可以減少對組件的修改次數，提升系統的穩定性。  範例：\n   假設我們有一個模組管理不同的使用者角色和權限。如果每次權限模型變更時，需要修改多個類，那麼這些類別應該在同一個元件中，因為它們的變更原因是一致的。  總結：   SRP 關注的是每個類別的職責，確保每個類別只做一件事。  CCP 關注的是組件的封閉性，確保由於相同原因變更的類別在一個組件中。   \n     \n  圖 19.4\n   \n      箭頭圖示說明   圖 19.4 中的箭頭是 從 IO Devices 指向 Encryption，這表明 IO Devices 依賴 Encryption。  IO Devices 知道並依賴於 Encryption，但 Encryption 並不依賴 IO Devices。這是依賴倒轉原則中的重要概念——高層次的組件不應該依賴低層次的具體實現。  \n      程式碼範例     public interface IIODevices\n   {\n       char   Read  ();\n       void   Write  (  char   c);\n   }\n   \n   public class ConsoleIODevices : IIODevices\n   {\n       public   char   Read  ()\n       {\n           // 讀取控制台的輸入\n           return   Console.  ReadKey  ().KeyChar;\n       }\n   \n       public   void   Write  (  char   c)\n       {\n           // 輸出到控制台\n           Console.  Write  (c);\n       }\n   }\n   \n   public class Encryption\n   {\n       public   char   EncryptChar  (  char   c)\n       {\n           // 簡單的加密邏輯，將字元轉換為下一個字母\n           return   (  char  )(c   +   1  );\n       }\n   }\n   \n   public class IOHandler\n   {\n       private readonly IIODevices _ioDevices;\n       private readonly Encryption _encryption;\n   \n       public   IOHandler  (IIODevices ioDevices, Encryption encryption)\n       {\n           _ioDevices   =   ioDevices;\n           _encryption   =   encryption;\n       }\n   \n       public   void   ProcessIO  ()\n       {\n           char   input   =   _ioDevices.  Read  ();\n           char   encrypted   =   _encryption.  EncryptChar  (input);\n           _ioDevices.  Write  (encrypted);\n       }\n   }\n   \n   class Program\n   {\n       static   void   Main  ()\n       {\n           IIODevices consoleIO   =   new   ConsoleIODevices  ();\n           Encryption encryption   =   new   Encryption  ();\n   \n           IOHandler ioHandler   =   new   IOHandler  (consoleIO, encryption);\n           ioHandler.  ProcessIO  ();\n       }\n   }\n   總結：   Encryption 不需要知道 IO Devices 的具體實作。它只專注於加密邏輯。  IO Devices（例如 ConsoleIO）依賴於 Encryption 來處理讀取到的數據。  這樣的設計遵循了依賴倒轉原則，保持了高層邏輯與具體實作之間的解耦，提供了靈活性和可維護性。   CONCLUSION 本章小結  本章針對策略的討論涉及單一職責原則（SRP）、共同閉包原則（CCP）、依賴反轉原則（DIP）。  html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"id":500,"path":501,"dir":351,"title":502,"description":503,"keywords":504,"body":507},"content:6.clean-arch:20.chapter20.md","/clean-arch/chapter20","20 BUSINESS RULES 業務邏輯","在應用程式中，我們經常會把程式劃分成業務邏輯和插件等不同部分。這裡的「業務邏輯」指的是那些對企業賺錢或節省成本有直接影響的邏輯。",[505,467,506,476],"ENTITIES 業務實體","REQUEST AND RESPONSE MODELS 請求和回應模型","  在應用程式中，我們經常會把程式劃分成業務邏輯和插件等不同部分。這裡的「業務邏輯」指的是那些對企業賺錢或節省成本有直接影響的邏輯。  舉個例子，銀行會根據一定的利率收取貸款的利息，這就是銀行賺錢的方式之一。無論這個利息是由電腦自動計算，還是由銀行職員手動計算，其實對賺錢這個過程來說，都是一樣的。這種涉及企業核心收入的邏輯，我們稱為「關鍵業務邏輯」。  「關鍵業務邏輯」通常會涉及到一些必須的數據，例如，針對貸款的邏輯，我們需要知道貸款金額、利率和還款計畫等信息。這些數據被稱為「關鍵業務數據」，因為不論是人工操作還是通過電腦來實現，這些數據都是必須存在的。  因為「關鍵業務邏輯」和「關鍵業務數據」是密不可分的，它們通常會放在同一個物件裡處理，而這種物件通常叫作「業務實體（Entity）」。   簡單就是說應用程式中的業務邏輯是企業賺錢或省錢的核心，無論是自動化還是人工執行，這些邏輯和數據都是必不可少的。將業務邏輯和業務數據放在一起，就是我們所謂的「業務實體」。  ENTITIES 業務實體  「業務實體」是指在電腦系統中的一種對象，它包含一系列用來操作關鍵資料的業務邏輯。這些實體物件要么直接包含了這些關鍵業務數據，要么能夠輕鬆訪問這些數據。實體的介面主要由那些負責執行業務邏輯、處理關鍵業務資料的函數組成。  \n     \n  圖20.1\n   例如，圖 20.1 中展示了一個「借貸」業務對應的業務實體類別 Loan 的 UML 圖。在這個類別中，你可以看到三個關鍵的業務數據，並且有三個操作這些數據的函數，這些函數實現了其關鍵業務邏輯。  創建這樣的類別，實際上是在將這個關鍵業務的具體實現聚集到一個單一的模組中，並將它與系統中其他部分（如資料庫、使用者介面、第三方框架）隔離開來。這個類別只負責代表該業務邏輯，並且不關心資料如何被儲存、系統如何呈現給使用者或如何運作。簡而言之，「業務實體」這個概念應該只關注業務邏輯，與其他部分無關。   這裡雖然是把業務實體定義為一個類別，但事實並非如此。業務實體並不一定要用物件導向的方式來實現，這個概念只是要求我們將關鍵的業務資料和業務邏輯封裝在一個獨立的軟體模組內即可。  USE CASES 用例  並非所有的業務邏輯都屬於「業務實體」。有些業務邏輯是專門針對自動化系統設計的，它們無法由人工來完成，必須通過系統來實現，這些邏輯屬於自動化系統中的業務流程控制。  例如，假設銀行有一個應用程式讓職員為客戶創建貸款。這個程式可能規定，職員必須先收集並驗證客戶的聯絡信息，確保客戶的信用評分超過500，然後才能進行貸款還款的預估。這就需要銀行在系統設計時確保兩點：一，客戶必須能夠通過系統輸入聯絡信息並通過驗證；二，只有信用評分符合要求時，客戶才能看到還款預估值。  這種描述實際上就是一個「用例」。用例是描述如何操作自動化系統的一個工具，定義了使用者需要提供的輸入、應得到的輸出，以及為了產生這些輸出所需的操作步驟。用例並非屬於業務實體的核心業務邏輯，而是特定應用場景下的操作流程。  例如，圖 20.2 中展示了一個用例，其中提到的「客戶」就是業務實體，它負責處理銀行與客戶之間的關係。  \n     \n  圖20.2\n   \n      程式碼範例     public class LoanApplication\n   {\n       public string Name { get; set; }                      //  姓名\n       public string Address { get; set; }                   //  地址\n       public DateTime Birthdate { get; set; }               //  出生日期\n       public string DLNumber { get; set; }                  //  駕照號碼\n       public string SSN { get; set; }                       //  社會安全號碼\n       public   int   CreditScore { get; set; }                  //  信用評分\n       public   bool   IsLoanEligible { get; private set; }      //  是否符合貸款條件\n   \n       //  建構函式\n       public   LoanApplication  (string name, string address, DateTime birthdate, string dlNumber, string ssn)\n       {\n           Name   =   name;\n           Address   =   address;\n           Birthdate   =   birthdate;\n           DLNumber   =   dlNumber;\n           SSN   =   ssn;\n       }\n   \n       //  驗證聯絡資訊\n       public   bool   ValidateContactInfo  ()\n       {\n           //  驗證邏輯\n           return   !  string.  IsNullOrEmpty  (Name)   &&\n                  !  string.  IsNullOrEmpty  (Address)   &&\n                  !  string.  IsNullOrEmpty  (DLNumber)   &&\n                  !  string.  IsNullOrEmpty  (SSN);\n       }\n   \n       //  檢索信用評分（存根函數，替換為實際邏輯）\n       public   void   GetCreditScore  ()\n       {\n           // 模擬獲取信用評分，實際情況應通過某種API或數據庫查詢獲得\n           CreditScore   =   new   Random  ().  Next  (  300  ,   850  );        // 假設信用評分在300到850之間\n           Console.  WriteLine  ($  \"{Name} 的信用評分：{CreditScore}。\"  );\n       }\n   \n       //  處理貸款申請\n       public   void   ProcessLoanApplication  ()\n       {\n           //  驗證聯絡資訊\n           if   (  !  ValidateContactInfo  ())\n           {\n               Console.  WriteLine  (  \"聯絡資訊驗證失敗。\"  );\n               return  ;\n           }\n   \n           //  檢索信用評分\n           GetCreditScore  ();\n           \n           if   (CreditScore   \u003C   500  )\n           {\n               //  拒絕\n               ActivateDenial  ();\n           }\n           else\n           {\n               CreateCustomer  ();\n               ActivateLoanEstimation  ();\n           }\n       }\n   \n       //  拒絕貸款\n       private   void   ActivateDenial  ()\n       {\n           IsLoanEligible   =   false  ;\n           Console.  WriteLine  (  \"貸款申請因信用評分低而被拒絕。\"  );\n       }\n   \n       //  建立客戶記錄\n       private   void   CreateCustomer  ()\n       {\n           Console.  WriteLine  (  \"已建立客戶記錄。\"  );\n       }\n   \n       //  啟動貸款估算\n       private   void   ActivateLoanEstimation  ()\n       {\n           IsLoanEligible   =   true  ;\n           Console.  WriteLine  (  \"貸款估算流程已啟動。\"  );\n       }\n   }\n   \n   class Program\n   {\n       static   void   Main  (string  []   args)\n       {\n           //  模擬客戶的聯絡信息\n           LoanApplication loanApp   =   new   LoanApplication  (\n               \"John Doe\"  ,\n               \"123 Main St\"  ,\n               new   DateTime  (  1985  ,   6  ,   15  ),\n               \"D1234567\"  ,\n               \"123-45-6789\"\n           );\n   \n           //  處理貸款申請\n           loanApp.  ProcessLoanApplication  ();\n       }\n   }\n   總結來說，用例控制了業務實體之間的互動方式。它並不關注使用者介面，也不涉及系統的具體交互方式，因此，我們從用例中無法看出系統是在 Web 平台上運行，還是以其他方式交付的。用例只描述了業務邏輯，而不是界面或交付方式。  在系統中，用例也是一個物件，它包含實現特定應用場景的業務邏輯、輸入和輸出資料，以及業務實體的引用。這樣，用例就能夠調用業務實體來實現其業務邏輯。  業務實體不需要知道是哪個用例在控制它們，這就是依賴反轉原則（DIP）的應用。業務實體作為高層概念，不應依賴用例這樣的低層概念；相反，用例這樣的低層概念需要依賴業務實體這樣的高層概念。  REQUEST AND RESPONSE MODELS 請求和回應模型  在大多數情況下，用例會接收輸入數據，並產生輸出數據。但在一個設計良好的架構中，用例物件不應該知道數據是如何展示給使用者或其他元件的。我們當然不希望在用例類別的程式碼中出現 HTML 或 SQL。  這種獨立性非常重要，因為如果請求和回應模型與其他元件耦合，這些用例就會依賴於這些模型所帶來的各種依賴關係。  有些人可能會覺得可以在資料結構中直接使用業務實體物件，因為業務實體與請求和回應模型之間有很多相同的數據。但這是不應該的！這兩個對象的存在意義完全不同，隨著時間推移，它們會因不同的原因和不同的變更速率而發生改變。如果將它們整合在一起，就違反了共同封閉原則（CCP）和單一職責原則（SRP）。這樣做最終可能會導致程式碼中出現大量的分支判斷和中間數據處理，讓程式變得複雜。  CONCLUSION 本章小結  本章針對策略的討論涉及單一職責原則（SRP）、共同閉包原則（CCP）、依賴反轉原則（DIP）。  html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"id":509,"path":510,"dir":351,"title":511,"description":7,"keywords":512,"body":518},"content:6.clean-arch:21.chapter21.md","/clean-arch/chapter21","21 會尖叫的架構",[65,513,514,515,516,517,356],"架構的主題","架構的目的","但是Web呢?","框架是工具，不是存在的方式","可測試的架構","  前言   案例1  阿宏最近看了一張室內設計圖並有興趣購買，你覺得他是想開一間餐廳、運動場還是想幹什麼呢?  \n      \n(5年房含車位，37萬/坪，總價1800萬，我只想說...)\n   案例2  看了底下某個專案目錄，你想到了什麼?  \n      \n          \n        (前端)\n     \n      \n          \n        (後端)\n     \n書中說: 這是一個XXX系統，而非是一個用XXX語言寫的系統。\n  架構的主題  軟體架構是支援系統使用案例的結構，就像住宅或圖書館的平面圖會對「這些建築物的使用案例」尖叫那樣，軟體的應用程式架構也應該對「應用程式的使用案例」尖叫。  架構不是(或不應該是)關於框架的，框架是工具，框架不是架構要順應的對象，如果你的架構是基於框架的，那它就不能基於你的使用案例。  架構的目的  良好的架構會以使用案例為中心去設計，且不必對框架、工具、環境做出承諾，想想房屋的平面圖案例，建築師最關心的問題是確保房子是可用的，而不是確保房子是磚頭做成的，事實上，建築師要努力確保屋主可以在平面圖能確保滿足使用案例後，才對於外部材料(磚、石材、柏木)做出決定。  架構應該使  框架是保持開放的選項 ，一個好的架構會讓專案直到後期之前都不需要對於採用框架、資料庫、Web服務器及其他環境問題(K8S? VM?)做出決定，架構強調的是使用者案例!  但是Web呢?  Web是架構嗎? 當然不是，Web只是一種交付機制 - 是一種IO設備，事實上，你的系統該如何被交付出去(或如何輸出結果)，這應該是最後才決定的事情!   悲慘案例 \n阿宏的公司很喜歡用winform這種桌面應用程式框架實作系統給User使用(輸出系統結果)，由於User的電腦環境千奇百怪，於是出現各種奇怪的報案(不能安裝、不能開啟、不能上傳檔案，轉圈圈很久)，有天老闆終於受不了了，希望我們全面轉Web，經工程師討論後我們發現...這需要極大的成本...。\n  原因: 沒有包成套件也沒有採用API，我們一開始就將「框架」跟「架構」綁在一起，所以只要動到GUI就會動到Business logic。  框架是工具，不是存在的方式  框架真的是太好用了，好用到你會讓它影響你的架構，看看前面winform的例子，由於它確實可以很快拉出一個版面，也正是因為這種方便，讓人們忽略應該把Business logic拆分出來，以為做任何系統都可以以這種低成本的方式製作，進而不斷產生一堆技術債...  可測試的架構  如果你的系統架構是關於使用案例的，且如果對框架保持一定的安全距離，那應該可以在沒有任何框架的情況下，對所有這些使用者案例做單元測試，你不需要在Web伺服器下運行你的測試，你也不必在連接資料庫的時候做測試，Entity應該是只依賴最原生套件(最純粹)所開發的物件，這樣，你的架構才是一個  可測試的架構 。  總結  你的架構應該告訴閱讀者關於這個系統的事情(Business)，而不是你系統中所使用的框架、Web服務器或是資料庫。",{"id":520,"path":521,"dir":351,"title":522,"description":7,"keywords":523,"body":533},"content:6.clean-arch:22.chapter22.md","/clean-arch/chapter22","22 整潔的架構",[65,524,525,526,527,528,529,530,531,532,356],"依賴規則","實體層","使用案例層","介面轉接層","框架和驅動層","只有四圈嗎?","跨越邊界","哪些資料跨越邊界?","一個典型的場景","  前言  過去幾十年，我們已經看過許多系統架構的一系列思維(Alistair Cockburn六角形架構、James Coplien&Trygve Reenskaug的DCI架構、Ivar Jacobson的BCE架構)，儘管這些架構在細節上有所不同，但它們都非常相似，那就是關注點的分離，它們都將軟體分成幾層(Layers)來實現這種分離; 業務規則、使用者及系統界面也都有屬於它們自己的layer存在著，layers具備以下特徵:   獨立於框架: 架構不依賴於一些現有的強大軟體程式庫，它允許你使用這樣的框架作為工具，但不是強迫你將系統塞進有限的約束之中。  可測試: 業務規則可以在沒有UI、資料庫、Web伺服器或任何其他外部元素的情況下，進行測試。  獨立於UI: 在無需更改系統其餘部分的情況下，UI應該容易做更改，如當把Web UI更換成主控台UI時，無須更改業務規則。  獨立於資料庫: 你可以將 Mongo、BigTable、CouchDB或其他資料儲存抽換成 Oracle 或 SQL server，你的業務規則不綁定到資料庫。  獨立於任何外部代理: 業務規則應該對外部的介面一無所知。  依賴規則  下圖的同心圓代表軟體的不同領域，一般來說，越裡面的軟體，其層次就越高，外圈是機制，內圈是策略，實現下圖最重要的規則是  依賴規則(Dependency Rule) :  \n      原始碼依賴關係只能指向內部，朝向更高層級的策略。  內圈不用了解外圈的任何事情，任何在外圈宣告的函式、類別、變數、資料格式等都不應該在內圈的程式碼中出現。  \n        Use Case Interactor 實現 Use Case Input Port。  Use Case Interactor 引用了(或依賴) Use Case Output Port。   錯誤案例     # presenter.py\n   \n   from pydantic import BaseModel\n   \n   class PersonModel(BaseModel):\n       id: int\n       name: str\n       age: int\n       grade: str\n     # controller.py\n   \n   from src.model.presenter.person_output import PersonModel\n   from src.services.test_service import get_person_info_srvs\n   from fastapi import APIRouter\n   \n   test_router = APIRouter(prefix=\"/api\", tags=[\"testTools\"])\n   \n   \"\"\"\n   1. response_model是一個presenter，且實踐了 PersonModel。\n   2. test_router是一個controller，且引用了 get_person_info_srvs。\n   \"\"\"\n   @test_router.get(\"/study/example\", response_model=PersonModel)\n   def get_person_info(id: int):\n       return get_person_info_srvs(id=id)\n     # interactor.py\n   \n   from src.model.presenter.person_output import PersonModel # 違反內層不能依賴外層的原則\n   \n   def get_person_info_srvs(id: str) -> PersonModel:\n       people_db = [{\n           \"id\": 1,\n           \"name\": \"John Doe\",\n           \"age\": 20,\n           \"grade\": \"A\"\n       }, {\n           \"id\": 2,\n           \"name\": \"Jane Smith\",\n           \"age\": 22,\n           \"grade\": \"B\"\n       }, {\n           \"id\": 3,\n           \"name\": \"Alice Johnson\",\n           \"age\": 21,\n           \"grade\": \"A\"\n       }]\n       for person in people_db:\n           if person[\"id\"] == id:\n               return PersonModel(**person)\n       not_existed_person = {\"id\": 0, \"name\": \"NA\", \"age\": 0, \"grade\": \"NA\"}\n       return PersonModel(**not_existed_person)\n  不好的架構:\n   實體層  Entity封裝企業級的關鍵業務規則，其可以是具有方法的物件，也可以是一組資料結構和函式，只要能被不同的應用程式使用即可。  將關鍵業務資料與關鍵業務規則一起放進一個單一的軟體模組中，如此而已，因此，既然它只是業務規則，當外部(GUI、資料庫等)發生變化時，它們是最不可能改變的。  使用案例層  封裝並實作系統所有的使用案例，指揮 Entity 層的物件來實現業務邏輯的計算。  而為什麼Entity是高層級的但使用者案例是低層級的呢? 因為使用案例是特定於單一應用程式的，它更接近系統的輸入與輸出，而entity則是可以使用在許多不同的應用程式的一種泛式表達，因此use case 依賴entity，但entity不依賴Use case。  因此，此層的變化不會影響到實體，更重要的是，GUI與資料庫的變更也不會影響到此層。  案例說明: 上面的PersonModel中，如果多一個屬性叫做alias，  其業務規則就是由id+name所組成 ，而在不同的使用者案例中我們都會用到此alias做不同的事情，此時我們應該可以考慮在Entity層中，新增類似PersonModel的物件，實作alias的邏輯，並給interactor(使用案例層)所使用。(應拆離出來)  介面轉接層  是一組轉接器，可以將使用案例和實體的資料格式轉換為某些外部代理所需的資料格式，反之亦然。例如包含GUI的MVC架構裡的表示器(presenter)、視圖(view)和控制器(controller)都屬於此層。  案例說明: 上面的controller.py、presenter.py 就是隸屬介面轉接層的一種物件。  框架和驅動層  此層由框架和工具(例如資料庫和Web框架)所組成。一般來說，這一層不需要編寫太多程式碼，有時候更多的是對框架裡面細節的設定。  案例說明: fastapi是一種Web API框架，因此隸屬這層。  只有四圈嗎?  或許你需要的不只四圈，也可能不需要四圈，本書沒有規定你一定要用四圈，但依賴規則始終是適用的。原始碼的依賴關係永遠要指向內部，隨著你朝內移動時，抽象和策略的層級會更高。  跨越邊界  我們通常利用依賴反向原則DIP來解決依賴關係與控制流相反的問題，在類似Java語言中，我們透過安排介面和繼承關係，來解決這種矛盾，例如，使用者案例需要呼叫表示器，但直接呼叫會違反依賴規則(外圈沒有任何函式、類別、變數等等可以出現在內圈的程式碼中。)，所以在內圈我們要有一個使用案例呼叫的介面(Use case Output)，並在外圈要有個表示器(Presenter)來實作它。\n   哪些資料跨越邊界?  通常跨越邊界的物件(class、hashmap等)是由簡單的資料結構組成的，簡單的資料結構跨越邊界傳遞的重點在於獨立，總之我們不希望資料結構有任何違背依賴規則的依賴關係，舉例來說，DB查詢回來的資料物件勢必會在Entity層或Use case層被使用，該物件應被設計成是內層容易使用的形式，而非配合外層。  一個典型的場景  下圖是一個基於Web的Java系統使用資料庫的典型場景，Web伺服器將使用者輸入的資料傳遞至Controller，Controller將這些資料打包成一個舊的Java物件POJO(plain old java object)，透過InputBoundary將這個物件傳給UseCaseInteractor，UseCaseInteractor透過呼叫Entities並傳入這些資料進行一些操作，在操作的過程中，Entities使用DataAccessInterface將會使用到的資料從資料庫傳遞到記憶體中進行操作便返回給UseCaseInteractor，UseCaseInteracto在將這些資料包裝成OutputData並建構另一個POJO，最後透過OutputBoundary傳給Presenter。  Presenter的工作是將OutputData重新打包為視圖形式的ViewModel，這是另外一個POJO，而View物件要做的事情只有將ViewModel中的資料移動到HTML頁面。   重點: 下圖所有的依賴關係在跨越邊界的時候，都是指向內圈的。  \n        範例     # test_router\n   from fastapi import File, UploadFile, Form, HTTPException, Response\n   from src.model.presenter.person_output import PersonPresenter\n   from src.utils.cloud_sql.test_sql import DataAccess\n   from src.services.test_service import person_use_case_interactor\n   \n   @test_router.get(\"/study/example\", response_model=PersonPresenter)\n   def get_person_info(id: int):\n       output_data = person_use_case_interactor(id=id, db=DataAccess())\n       return PersonPresenter(alias=output_data.alias, color='red')\n     # Presenter\n   from pydantic import BaseModel\n   \n   class PersonPresenter(BaseModel):\n       alias: str\n       color: str\n     # interactor\n   from abc import ABC, abstractmethod\n   from typing import Dict, Any\n   from src.services.entities.test_entity import PersonEntity\n   \n   class IDataAccess(ABC):\n   \n       @abstractmethod\n       def get_person(id: str) -> Dict[str, Any]:\n           pass\n   \n   \n   class OutputData:\n   \n       def __init__(self, alias: str):\n           self.__alias = alias\n   \n       @property\n       def alias(self):\n           return self.__alias\n   \n   \n   def person_use_case_interactor(id: str, db: IDataAccess) -> OutputData:\n       # 1. 可以再寫一個interface給person_use_case_interactor繼承，該interface就是Input Boundary。\n       person_temp = db.get_person(id=id)\n       person_entity = PersonEntity(id=person_temp['id'], name=person_temp['name'], age=person_temp['age'], grade=person_temp['grade'])\n       return OutputData(alias=person_entity.get_alias())\n     # Entity\n   class PersonEntity():\n   \n       def __init__(self, id: int, name: str, age: int, grade: str):\n           self.id = id\n           self.name = name\n           self.age = age\n           self.grade = grade\n   \n       def get_alias(self):\n           return self.name + ',' + self.grade\n     # DB\n   from src.services.test_service import IDataAccess\n   \n   class DataAccess(IDataAccess):\n   \n       def get_person(self, id: str):\n           people_db = [{\n               \"id\": 1,\n               \"name\": \"John Doe\",\n               \"age\": 20,\n               \"grade\": \"A\"\n           }, {\n               \"id\": 2,\n               \"name\": \"Jane Smith\",\n               \"age\": 22,\n               \"grade\": \"B\"\n           }, {\n               \"id\": 3,\n               \"name\": \"Alice Johnson\",\n               \"age\": 21,\n               \"grade\": \"A\"\n           }]\n           for person in people_db:\n               if person[\"id\"] == id:\n                   return person\n           not_existed_person = {\"id\": 0, \"name\": \"NA\", \"age\": 0, \"grade\": \"NA\"}\n           return not_existed_person\n  實作整理:   Controller: fastapi中router的部分就是請求得進入點，即Controller。  (DS)Input Data: Use Case Interactor所使用的資料集，就是範例person_use_case_interactor中函式的input。  (I)Input Boundary: 沒有實作。  Use Case Interactor: 範例的person_use_case_interactor。  Entities: 範例的PersonEntity。  (I)Data Access Interface: 範例的IDataAccess。  Data Access: 範例的DataAccess物件。  DataBase: 沒有實作，或是說就是範例中的people_db變數(Mock data)。  (DS)Output Boundary: 沒有實作。  (I)Output Data: 範例的OutputData。  Presenter: 範例的PersonPresenter。  View Model: 書上解釋\"Presenter的工作是將OutputData重新打包為視圖形式的ViewModel\"，所以Fastapi中的BaseModel應為ViewModel。   ChatGPT: \n在 FastAPI 中，BaseModel 是由 Pydantic 提供的一个基类，用于定义数据模型。它的主要作用是进行数据验证和序列化/反序列化。通过继承 BaseModel，可以方便地定义请求和响应的格式，并确保数据符合预期的结构和类型。\n\nBaseModel 在 Clean Architecture 中可以看作是一种 ViewModel，因为它负责在不同层之间传递数据，并确保数据的完整性和一致性。具体来说，BaseModel 主要用于以下几种情况：\n1. 请求数据验证：确保从客户端接收到的数据符合预期的格式和类型。\n2. 响应数据格式化：定义返回给客户端的数据格式。\n3. 数据转换：在不同层之间传递数据时，进行数据的转换和验证。\n   View: 只負責將資料呈現在畫面上，即Swagger。  總結  遵守這些簡單的規則並不困難，它可以給你帶來很多好處，其中的好處包括，這是一個可(容易)測試的系統、外部元件(例如換資料庫或Web框架)過時時，你也可以很輕鬆的換掉它。  html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"id":535,"path":536,"dir":351,"title":537,"description":7,"keywords":538,"body":545},"content:6.clean-arch:23.chapter23.md","/clean-arch/chapter23","23 Presenter與Humble Object",[65,539,540,541,542,543,544,356],"HUMBLE OBJECT模式","Presenter及View","測試和架構","資料庫閘道","資料映射器(ORM)","服務監聽器","  前言  上一章中，我們介紹了Presenter的概念，View是HUMBLE OBJECT(謙卑物件)模式的一種形式，它能幫助我們辨識和保護架構的邊界，事實上，上一章中充滿了HUMBLE OBJECT的實作。  HUMBLE OBJECT模式  HUMBLE OBJECT模式是一種設計模式，最初是用來協助單元測試人員將難以測試與容易測試的行為做分離，例如，GUI是難以被測試的，但GUI所呈現的資料與行為大多容易測試，所以我們將這兩種行為分為兩種不同的類別，分別稱之為Presenter與View。  備註: 換個方式說，要測試到Presenter要做整合測試，但要做到View的測試，就是E2E測試了，當然整合測試比起E2E更簡單些。  Presenter及View  Humble Object是指那些難以被測試的元件，所以View是一個Humble Object，其程式碼應盡可能地保持簡單，它將資料移到GUI但不處理資料。  Presenter是可測試的物件，它的工作是接受來自應用程式的資料，並將其格式化以便View可以簡單將之移動到螢幕上。用一個例子來說明，你有一個財務系統，當人的資產總額是負的時候，在該人名上面標示紅色做HL，此時，Presenter會在View Model中設置好人名、資產總額、是否HL等標示，View則是根據View Model的內容作GUI上面的呈現即可，由於View除了將View Model的資料載入到螢幕上之外，它沒做任何事情了，所以View是謙卑的(Humble)。  測試和架構  現在你知道  可測試性是良好架構的一個屬性 ，HUMBLE OBJECT就是一個很好的案例，將行為分為可測試與不可測試的兩個部分，其實也就定義了一個架構的邊界，Present/View邊界就是這樣的邊界之一，但還有許多其他的邊界。  資料庫閘道  Interactor和資料庫之間的是資料庫閘道，這些閘道是多型介面，包含新(C)增、查詢(R)、修改(U)、刪除(D)等操作的方法。我們也不允許SQL出現在Interactor層，閘道是由資料庫層的類別來實作的，這些實作是一個humble object，相反地，因為Interactor封裝了業務規則，所以它們並不是humble object，因此是可測試的，而閘道可以用適當的替身樁(stub)和test-double(Mock Object也是其中一種)來取代。  備註: 上一章中DataAccess物件就是阿宏為了Demo隨意寫的Mock Object，它並沒有連接資料庫，但還是可以提供一些資料讓Interactor被測試。  資料映射器(ORM)  大家覺得ORM應該是哪一層呢?  ORM(Object-Relational Mapping)應該是不存在的，物件裡面某些資料是不可見的，能見的只有那些設定公有(public)的屬性或函式，對用戶來說，物件是一些操作的集合，並非簡單的數據集合，所以物件不是數據集合。  但數據集合恰恰相反，它沒有隱藏任何東西，，所以ORM命名為資料映射器(data mappers)會更洽當些，因為他的職責就是從RDB table中將資料載到數據集中。  所以ORM應該放在哪層呢? 當然是放在資料庫層，ORM其實就是在閘道介面(Gateways)與資料庫(DB)之間建構了另一種HUMBLE OBJECT的邊界。  服務監聽器  如果你的程式必須與其他服務通訊，或者你的程式本身就提供了服務，那在相關服務的邊界也會看到HUMBLE OBJECT嗎?  答案是肯定的，輸出的部分，當你的程式將資料載入簡單的資料結構中，並將之跨越邊界傳遞給那些可以「格式化並傳遞至外部環境」的模組;輸入的部分，服務監聽器會負責從port拿取資料，並將其格式化成可以讓程式使用的形式。  整理: 服務監聽器是一個HUMBLE OBJECT的邊界，如同大部分框架的controller，在Unittest中你不會去測試controller是不是正常，而是只會測試use cases層拋出的資料結構是不是符合商業邏輯與API協議。  總結  架構中的每個邊界中，你會處處發現潛伏在某處的HUMBLE OBJECT，跨越邊界的通訊幾乎透過傳遞某種簡單的資料結構(格式)來達成，這些邊界很自然的將系統分成容易測試與難以測試的部分，因此，在邊界處運用HUMBLE OBJECT模式，可以大大提升系統的可測試性。  整理: 還記得嗎? 良好架構其中一個衡量的標準就是可測試性，因此，在邊界定義好HUMBLE OBJECT，會讓你的架構更上一層樓!",{"id":547,"path":548,"dir":351,"title":549,"description":7,"keywords":550,"body":554},"content:6.clean-arch:24.chapter24.md","/clean-arch/chapter24","24 不完全邊界",[65,551,552,553,356],"省下最後一步","單向的邊界","FACADE模式","  前言  建構完整的架構邊界是一件很耗成本的事情，你必須為系統設計雙向的Boundary介面、Input和Output資料結構，以及設計所有相關的依賴關係管理，以便將系統分割成可獨立編譯和部屬的元件，這需要大量的前期開發成本與後期維護成本。  好的架構師即使覺得這樣的成本太高，但考量日後可能還是會需要這些邊界，所以他們還是會希望能留下這些邊界。  這種預防性設計在敏捷社群中為人詬病，因為它明顯違反YAGNI:「You Aren't Going to Need it.(你並不需要它)」，這時候，架構師就會需要引入不完全邊界(partial boundary)的概念了。  整理: 簡單說，就是在保留邊界的情況下，透過以下的方式來省下一些成本。  省下最後一步  建構不完全邊界的一種方式就是在將系統分割成一系列可以獨立編譯、部屬的元件後，在將它們放在同一個元件之中做編譯和部屬，雖然兩種模式(放在同一個元件或分開不同元件)所需的程式碼(成本)是一樣的，但它至少省下多組件管理的部分工作，等於省去版號管理和發布管理方面的工作 - 這其中的工作量其實可不小。  這其實是FitNesse的早期策略，FitNesse的Web伺服器相關的元件被設計成可與Wiki分離，但我們不希望使用者必須下載兩個元件才能使用，因此我們決定讓使用者只下載一個jar檔案就可以執行它，這樣使用者不必再去找其他的Jar檔案，也不必考慮版本的相容性等等。  但隨著時間推移，將Web元件獨立的需求越來越少，Web元件與Wiki元件的隔離也弱化了，到如今，我們若真的要分離Web組件的話，會需要不少工作量。  我的理解是，即使當初的設計是分離的，但放在一起久了也很容易日漸耦合，我想這就是省下成本的代價吧。  單向的邊界  在設計一套完整的系統架構邊界時，往往需要用反向介面來維護邊界兩側元件的隔離性，這種雙向的隔離性通常不會是一次性工作，它需要持續地長期投入資源維護下去。  下圖中，你會看到一個臨時佔位的，將來可以被替換成完整邊界夠簡單的結構，這個結構採用了傳統的策略模式(Strategy pattern)，如你所見，client使用的是一個由service Impl實現Service Boundary介面。  \n       很明顯，上圖的設計已經為未來建構完整的架構邊界達下良好的基礎，必要的依賴反轉已經做完了。但我們從虛線箭頭也可發現未來可能很快會出現的隔離問題，由於沒有採用雙向反向介面，這部分只能依賴開發者和架構師的自律性來保證元件的永久隔離。   策略模式 wiki \n策略模式作爲一種軟體設計模式，指對象有某個行爲，但是在不同的場景中，該行爲有不同的實現算法。比如每個人都要「交個人所得稅」，但是「在美國交個人所得稅」和「在中華民國交個人所得稅」就有不同的算稅方法。\n  策略模式：   定義了一族算法（業務規則）；  封裝了每個算法；  這族的算法可互換代替（interchangeable）。  註記:   实线箭头：用于表示强依赖性、结构性的关系，如继承、实现、组合和聚合。  虚线箭头：用于表示弱依赖性、行为性的关系，如依赖和实现。  FACADE模式  採用FACADE模式是一個更簡單的架構邊界設計，如下圖所示，在這種模式下我們連依賴反轉的工作都省了(不必每個service類別都建立一個介面)，我們將邊界的定義交給一個Facade類別，如此，client就不會直接直接對到service。  請注意，Client會傳遞性依賴所有Service類，在靜態語言中，如果我們改了Service類別裡面的程式碼都會需要將Client做重新編譯。另外在這個架構中，我們要建立一個反向介面是很容易的。   FACADE範例  \n       總結  本章中介紹了三種實現不完全邊界的三種方式，當然，類似的實現方式還有很多種，本章只是列出常見的三種方式，每種方式都有對應的優點(獲益)與缺點(成本)，它們都是被用來充當最終完整架構邊界的一種臨時替代品，架構師的職責之一就是預判預來哪裡有可能會需要設計邊界，並決定該以完全或是不完全形式來實現它。",{"id":556,"path":557,"dir":351,"title":558,"description":7,"keywords":559,"body":564},"content:6.clean-arch:25.chapter25.md","/clean-arch/chapter25","25 層與邊界",[65,560,561,562,563,356],"Hunt the Wumpus 冒險遊戲","可否採用整潔的架構?","交匯資料流","資料流的分割","  前言  我們可以將系統簡單看作是由三個元件所組成: UI、業務規則和資料庫，對於一些簡單的系統來說，這樣已經足夠了，但對於大部分的系統來說，元件的數量要比它來的多。  Hunt the Wumpus 冒險遊戲  這是一款讓玩家在console上輸入指令(如:GO EAST、SHOOT WEST)後，電腦就會回應玩家看到、聞到、聽到和體驗到什麼東西，假設我們要保留基於文字的UI，但要將其與遊戲規則解偶開來，以便我們的遊戲可以輕易的在各國中以不同的語言執行，遊戲規則與UI的通訊是透過使用和語言無關的API，而UI會將API輸出結果轉換成適當的語系作呈現。  如果原始碼的依賴關係被正確管理，如下圖，那任何數量的UI元件都可以使用相同的遊戲規則，遊戲規則也不知道也不關心目前是哪一個國家的人在使用。  \n       接著，我們再次假設遊戲的狀態是保存在某種持久性儲存體上，或許是flash記憶體、雲端、也或者是RAM當中，不論哪種情況，我們都不希望遊戲規則知道這個資訊，因此，我們在建立一個API，使遊戲規則可以使用該API與資料儲存元件進行通訊，如下圖。  \n       可否採用整潔的架構?  上述遊戲的案例已經具備採用整潔架構的一切了，包括使用案例、邊界、實體和相對應的資料結構，但我們是否真的找到所有重要的架構邊界呢?  假設，語系並非UI上面唯一的變化因素，我們可能想改變文字的通訊方式，如用一個一般一點的SHELL視窗，或者聊天室應用程式，總之，可能存在多種可能性，於是我們多了些設計如下:  \n       虛線框表示它是定義了一個API的抽象元件，該API會由其附近的元件來實作(如English實作Language API)，另外看箭頭的方向可知，GameRules與Language之間的交互(通訊)是由GameRules定義的規則與實作Language API的元件來完成的。同樣的，Language與TextDelievery的交互也是通過由Language定義與實作TextDelivery API的元件來完成的，所以結論是，API的定義和維護都是由使用芳來負責的，而非實現方，在每一種情況下，由這些Boundary介面定義的API，其擁有者都是上游元件。  舉例: 誰要用誰就要負責維護，譬如業務邏輯會用到DB裡面的資料做運算，所以該資料的介面應該是定義在業務邏輯層，並由DB層的元件去實作它。  讓我們只保留API來簡化一下架構圖，如下所示:  \n       上圖的架構可分為左側與右側兩個資料流，左側關注在與使用者的通訊，右側則關注資料的持久性，兩個部分最終都會在頂部相遇，以GameRules作為最終處理器。   注意，箭頭是指原始碼的依賴方向，而非資料流的方向  交匯資料流  任何系統都只有兩個資料流嗎? 當然不是，想像一下如果我們還需要網路元件，就會變成是三個資料流，如下所示:  \n       所以隨著系統越來越複雜，元件結構就會被分割成更多的部分。  資料流的分割  如果你覺得所有的資料流最終都會彙整到一個組件上，那就太簡單了呀，但現實的架構往往不如你所願，譬如在Hunt the Wumpus這款遊戲中，有一部份的遊戲規則是處理有關地圖的機制，包括洞穴是如何連接的，以及每個洞穴中有哪些物體或怪獸，也掌管玩家必須處理的事件，此外，還有更高層級的遊戲規則，就是掌管玩家血量狀態的遊戲規則與特定事件的成本或收益，這些策略會讓玩家逐漸失血，或是發現食物來補血。  所以，這麼多的遊戲規則間，也可以區分為低層級或高層級的遊戲規則，而高層級的遊戲規則應該是那些根據玩家的狀態來決定輸贏的部分，如下圖:  \n       只有這樣嗎? 讓我們加入微服務讓事情變得更有趣些，想像這遊戲變成一個大型多人遊玩的網路遊戲，MoveManagement在玩家的電腦進行處理，但PlayerManagement由伺服器進行處理，PlayerManagement為所有連接過來的MoveManagement元件提供一個微服務API，下圖中，我們為該遊戲畫了一個簡單版的設計圖，現實中的Network元件通常會比圖中更複雜一些，從圖中我們可以看到MoveManagement與PlayerManagement之間存在一個完整的系統架構邊界。  \n       總結  這整章究竟要表達什麼呢? 為什麼我們要用一個如此簡單的遊戲來實作這麼瘋狂的架構呢?  其實我們想說的是，處處都存在架構的邊界，作為一個架構師，應該小心辨識何時需要用它們，但同時也要意識到，充分實作這些架構，其代價會非常高，過度的工程設計通常會比不足的工程設計還要來得糟糕，但另外一方面，如果我們發現某個位置確實需要一個架構邊界但卻又沒事先設計的時候，事後再添加這些邊界其成本和風險往往是很高的。  架構的邊界不是一次性的決定，在專案的開始階段，千萬不要草率的決定要在哪實作邊界或要忽略哪個邊界，你必須觀察，這是一個持續的過程，隨著系統發展或許有些地方就會需要(或不需要)邊界(也需考量成本)，總之，這真的需要好眼力。",{"id":566,"path":567,"dir":351,"title":568,"description":7,"keywords":569,"body":578},"content:6.clean-arch:30.chapter30.md","/clean-arch/chapter30","30 資料庫細節",[570,571,572,573,574,575,576,577],"細節","關聯式資料庫","資料庫系統為什麼如此普遍？","假設磁碟不存在會怎樣？","實現細節","但性能呢？","一段軼事","本章小结","  細節   參考   軟體架構的目標是什麼？  在 Clean Architecture 一書中，Uncle Bob 指出可維護性是優秀架構的關鍵目標，我們希望最大限度地減少構建和維護系統的開發工作：\n  軟體架構的目標是最大限度地減少構建和維護所需系統所需的人力資源 。  來源參考  讓我們回顧一下資料庫的觀點  Clean Architecture 生成獨立於資料庫的系統。更具體地說，業務規則未綁定到資料庫。   可測試性：我們可以對業務規則進行單元測試，因為業務規則是在用例和實體中實現的，而不是在資料庫中實現的  可維護性：我們可以降低維護成本，因為業務規則既是可測試的，而且我們的應用程式是模組化的（將用例與 I/O 問題分離）  可移植性：我們可以將一個資料庫換成另一個資料庫，例如，從 MySQL 切換到 SQL Server 再到 MongoDB 等。  \n       Clean Architecture 的概念由 Robert C. Martin （MARTIN， 2017） 在他的書中定義，題為“Clean Architecture： A Craftsman's Guide to Software Structure and Design”。在此體系結構中，系統可以分為兩個主要元素：  策略和詳細資訊 。策略是業務規則和過程，詳細資訊是執行策略所需的專案。（馬丁，2017 年）正是從這個劃分開始，Clean Architecture 開始將自己與其他架構模式區分開來。架構師必須創建一種方法，使系統能夠將策略識別為系統的主要元素，而將細節識別為與策略無關。  來源參考   在乾淨的架構中，   沒有必要在開發開始時選擇資料庫或框架，因為所有這些都是不會干擾策略的細節，因此可以隨著時間的推移而改變。  在 Clean Architecture 中，有一個明確定義的層劃分。該架構是獨立於框架的，即包含業務規則的內部層不依賴於任何第三方庫，這允許開發人員將框架用作工具，而不是調整系統以滿足特定技術的規範。Clean Architecture 的其他好處是：可測試性、UI 獨立性、資料庫獨立性和獨立於任何外部代理（業務規則不應瞭解外部世界的介面）。  為了說明所有這些概念，創建了下圖所示的圖表。  \n     \n圖中的每一層都表示軟體的不同區域，最內層是策略，最外層是機制。\n  關係圖的最外層通常由框架和資料庫組成。此層包含與 interface adapters 層建立通信的代碼。所有細節都在這一層，Web 是一個細節，資料庫是一個細節。所有這些元素都位於最外層，以避免干擾其他元素的風險（MARTIN，2017 年）。    本章重點   資料庫只是實現細節   資料庫不應該影響系統架構，它只是存取資料的工具。  資料庫在軟體架構中只是個工具，並不應該影響系統的核心架構。  資料庫只是硬碟與記憶體之間傳輸數據的手段，對系統架構無關緊要。  關聯式資料庫的歷史   關聯式資料庫自1970年代以來成為主要的資料儲存形式。  關聯模型雖然優秀，但仍然只是技術細節，應用程式不應該依賴其結構。  硬碟的影響   硬碟的發展推動了資料庫系統的普及，但硬碟的訪問速度限制了其性能。  一段軼事   作者分享了在創業公司中堅持不使用關聯型資料庫的經歷，最終認識到市場需求的重要性。  未來展望   假設磁碟不存在會怎樣：如果所有數據都存在記憶體中，資料庫的角色會變得不重要。  隨著RAM的普及，資料庫的角色可能會改變，系統架構應該對底層存儲技術保持中立。     資料模型對系統架構重要，而資料庫只是技術細節。     性能是考量標準，但應該在數據訪問層面解決，而非系統架構層面。  \n       \n       \n           原文翻譯   從架構的角度來看，資料庫並不是一個實體——它只是個細節，並沒有達到架構元素的層級。它與軟體系統架構的關係，就像門把手與房屋架構的關係。  我知道這些話可能會引起爭議。相信我，我已經爭論過很多次了。所以我要說清楚：我不是在討論資料模型。應用程式中的資料結構對系統架構來說非常重要，但資料庫不是資料模型。資料庫只是一款軟體，是用來存取資料的工具。從架構的角度來看，這個工具是無關緊要的，因為它只是個底層的實現細節。  一個好的架構師不會讓底層機制污染系統架構。  關聯式資料庫  Edgar Codd 在 1970 年定義了關聯式資料庫的原理。到了 1980 年代中期，關聯模型成為主要的資料儲存形式。這種流行是有原因的：關聯模型優雅、自律且穩健，  是一種優秀的資料儲存與存取技術。  但無論這種技術多麼出色、有用且符合數學原理，它仍然只是一種  技術 ，也就是說，它終究只是個  細節 。  雖然關聯表格在某些數據存取形式上可能很方便，但將數據排列成表格中的行並沒有任何架構上的重要性。你的應用程式的使用案例不應該知道或關心這些問題。  事實上，對數據表格結構的了解應該限制在架構外圍的最低層級的工具函數中。   許多數據存取框架允許將資料庫的行和表格作為物件在系統中傳遞。允許這樣做是一個架構上的錯誤。這會將使用案例、業務規則，甚至在某些情況下，將使用者介面與數據的關聯結構耦合在一起。  資料庫系統為什麼如此普遍？    圖表  \n       為什麼資料庫系統在軟體系統和企業軟體領域如此流行？Oracle、MySQL 和 SQL Server 這些產品廣泛流行的原因是什麼？  答案是硬碟。  帶有高速旋轉的磁盤，以磁感應方式讀取數據的硬碟在過去五十年成為數據存儲的主流手段，以至於最近幾代程式設計師對其他類型的數據存儲幾乎一無所知。而且硬碟技術一直在發展，硬碟技術從直徑 48 英吋、重達數千磅、容量僅 20 兆字節的巨大碟片堆疊，發展到直徑 3 英吋、重量僅幾克的薄薄一張單片薄圓片的硬碟就能存儲上 TB 的數據。這發展得實在是太快了！但是在硬碟的整個發展過程中，  程式設計師們始終被一個限制困擾著：磁碟的訪問速度太慢了！  在磁碟上，數據是按照環形軌道存儲的。這些軌道又會進一步被劃分成一系列扇區，這些扇區的大小通常是 4 KB。而每個盤片上都有幾百條軌道，整個硬碟可能由十幾個盤片組成。如果要從硬碟上讀取某一個特定字節，需要將磁頭移到正確的軌道上，等待盤片旋轉到正確的位置上，再將整個扇區讀入內存中，從內存中查詢對應的字節。這些過程當然需要時間，所以硬碟的訪問速度一般在毫秒級。  毫秒級的速度看起來好像並不是很慢，但這已經比大多數處理器的速度慢一百萬倍了。如果數據不在硬碟上，訪問速度通常就是納秒級，而不是毫秒級了。  為了應對硬碟訪問速度帶來的限制，必須使用索引、快取以及查詢優化器等技術。同時，我們還需要一種數據的標準展現格式，以便讓索引、快取及查詢優化器來使用。概括來說，我們需要的就是某種數據訪問與管理系統。過去幾十年內，業界逐漸發展出了兩種截然不同的系統：檔案系統與關聯式資料庫管理系統（RDBMS）。  檔案系統是基於文件格式的，它提供了一種便於存儲整個文件的方式。當需要按照名字存儲數據和查找一系列文件時，檔案系統很有用，但當我們需要檢索文件內容時，它就沒那麼有用了。也就是說，我們在檔案系統中查找一個名字為 login.c 的文件很容易，但要檢索出所有包括變數 x 的 .c 文件就很困難，速度也很慢。  而資料庫系統則主要關注的是內容，它提供了一種便於進行內容檢索的存儲方式。其最擅長的是根據某些共同屬性來檢索一系列記錄。然而，它對存儲和訪問內容不透明的文件的支持就沒那麼強了。  這兩種系統都是為了優化磁碟存儲而設計的，人們需要根據它們的特點來將數據組織成最便於訪問的模式。每個系統都有一套索引和安排數據的方式。同時，每種系統最終都會將數據快取在記憶體中，方便快速操作。  這些技術限制使得資料庫系統成為了優化資料存取性能的必要工具，從而在軟體系統和企業中變得如此普遍。  假設磁碟不存在會怎樣？  雖然硬碟現在還是很常見，但其實已經在走下坡路了。很快它們就會和磁帶、軟碟、CD 一樣成為歷史，RAM 正在替代一切。  現在，我們要來考慮一下：如果所有的數據都存在記憶體中，應該如何組織它們呢？需要按表格存儲並且用 SQL 查詢嗎？需要用文件形式存儲，然後按目錄查找嗎？  事實上，如果你再仔細想想，就會發現我們已經在這樣做了。  即使數據保存在資料庫或者檔案系統中，我們最終也會將其讀取到記憶體中，並按照最方便的形式將其組織成列表、集合、堆疊、佇列、樹等各種數據結構，繼續按文件和表格的形式來操作數據是非常少見的。  實現細節  這就是為什麼我們認為資料庫只是一種實現細節的原因。  資料庫終究只是在硬碟與記憶體之間相互傳輸數據的一種手段而已 ，它真的可以被認為只是一個長期存儲數據的、裝滿位元組的大桶。我們通常並不會真的以這種形式來使用數據。   因此，從系統架構的視角來看，真的不應該關心數據在旋轉的磁碟表面上以什麼樣的格式存在。實際上，系統架構應該對磁碟本身的存在完全不關心。  但性能呢？  性能難道不是系統架構的一個考量標準嗎？當然是——但當問題涉及數據存儲時，這方面的操作通常是被封裝起來，隔離在業務邏輯之外的。也就是說，  我們確實需要從數據存儲中快速地存取數據，但這終究只是底層實現問題。 我們完全可以在數據訪問這一較低的層面上解決這個問題，而不需要讓它與系統架構相關聯。  一段軼事    圖表  \n       在 20 世紀 80 年代末，我曾在一家創業公司中帶領一組軟體工程師開發和推廣一個用於監控 T1 線路通信質量的網絡管理系統。該系統從 T1 線路兩端的設備抓取數據，然後利用預測算法來檢測和匯報問題。  我們當時採用的是 UNIX 平台，並將數據存儲成簡單的可隨機訪問的格式。該項目當時也不需要用到關係型數據庫，因為數據之間幾乎沒有內容之間的關係，用樹以及鏈表的形式來存儲數據就夠了。簡單來說，我們的數據存儲格式是為了便於加載到記憶體中處理而設計的。  創業公司後來招聘了一個市場推廣經理。他人很好，知識也很全面。然而他告訴我的第一件事就是我們系統中必須有一個關係型數據庫。這容不得商量，也不是一個工程問題——而是一個市場問題。  這對我來說很難接受，為什麼我要將鏈表和樹重新按照表格與行模式重組，並且用 SQL 方式存儲呢？為什麼我們要在隨機訪問文件系統已經足夠用的情況下引入大型關係型數據庫系統？所以我一直和他針鋒相對，互不相讓。  後來公司內有一位硬體工程師被關係型數據庫大潮所感染：他堅信我們的軟體系統在技術上有必要採用關係型數據庫，他背著我召集了公司的管理層開會，在白板上畫了一間用幾根杆子支撐的房子，問道：「誰會把房子建在幾根杆子搭起來的地基上？」這背後的邏輯是：通過關係型數據庫將數據存儲於文件系統中，在某種程度上要比我們自己存儲這些文件更可靠。  我當然沒有放棄，一直不停地和他還有市場部鬥爭到底。我誓死捍衛了自己的工程原則，不停地開會、鬥爭。  最終，這位硬體工程師被提拔為軟體開發經理，最終，系統中也加入了一個關係型數據庫。最終，我不得不承認，他們是對的，而我是錯的。  這裡說的不是軟體工程問題：在這個問題上我仍然堅持自己沒有錯，在系統的核心架構中的確不應該引入關係型數據庫。這裡說我錯了的原因，是因為我們的客戶希望該系統中能有一個關係型數據庫。他們其實也不知道為什麼需要，因為他們自己是沒有任何機會使用這個關係型數據庫的。但這不是重點，問題的重點是我們的客戶需要一個關係型數據庫。它已經成為當時所有軟體購買合同中的一個必選項。這背後毫無工程邏輯——是不理智的。但儘管它是不理智的、外行的、毫無根基的需求，但卻是真實存在的。  這種需求是從哪裡來的？其實是來自於當時數據庫廠商非常有效的市場推廣。他們說服了企業高管，他們的“數據資產”需要某種保護，數據庫則提供了非常便捷的保護能力。  直到今天我們也能看到這種市場宣傳，例如“企業級”“面向服務的架構”這樣的措辭大部分都是市場宣傳噱頭，而跟實際的工程質量無關。  回頭想想，我在這個場景中應該怎麼做呢？事實上，我當時應該在系統的某個角落接上一個關係型數據庫，在維持系統核心數據結構的同時給關係型數據庫提供一些安全的、受限的數據訪問方式。但我沒這麼做，我辭職了，幹起了諮詢這一行。  本章小结  資料的組織結構，即資料模型，是系統架構中非常重要的一部分。而從旋轉磁碟上存取資料的技術和系統則沒那麼重要。關聯式資料庫系統強制我們將資料組織成表格並使用 SQL 存取，這更多是關於後者而非前者。資料本身是重要的，而資料庫只是個細節。",{"id":580,"path":581,"dir":351,"title":582,"description":7,"keywords":583,"body":585},"content:6.clean-arch:31.chapter31.md","/clean-arch/chapter31","31 WEB是細節",[584,356,577],"這無盡的鐘擺","    本章重點   Web技術的震盪   計算資源在集中式和分散式之間的擺動。  業務邏輯與UI的解耦   強調了將業務邏輯與UI分離的重要性，以應對市場變化。  歷史案例：通過Q公司和A公司的故事，展示了不解耦可能帶來的問題。     強調Web只是I/O設備的一種，應將其視為細節，並保持應用程式的設備無關性。  \n           原文翻譯     圖表  \n       \n       你是 90 年代的開發者嗎？還記得 Web 是如何改變一切的嗎？還記得我們在面對閃亮的新技術 Web 時，是如何鄙視那些老舊的客戶端/伺服器架構的嗎？  其實，Web 並沒有改變任何東西。或者至少，它不應該改變任何東西。Web 只是自 1960 年代以來，我們行業經歷的一系列震盪中的最新一次。這些震盪在將所有計算能力集中在中央伺服器和將所有計算能力分散到終端之間來回擺動。  在過去的十年左右，自從 Web 技術變得普及以來，我們已經見證了幾次這樣的震盪。一開始我們認為所有的計算能力應該集中在伺服器群中，而瀏覽器應該保持簡單。然後我們開始在瀏覽器中引入 Applets。但我們不喜歡這樣，所以我們將動態內容移回伺服器。但後來我們又不喜歡這樣，所以我們發明了 Web 2.0，並用 Ajax 和 JavaScript 將大量處理過程移回瀏覽器。我們甚至創建了整個龐大的應用程式在瀏覽器中執行。現在我們又非常興奮地將那些 JavaScript 代碼用 Node 技術移回伺服器上執行  （嘆氣）  這無盡的鐘擺  當然，這些震盪並不是從 Web 技術開始的。在 Web 出現之前，這種震盪在客戶端/伺服器架構中就很普遍。再往前，就是中央小型機與瘦終端的模型（這裡的瘦終端和現在我們所謂的現代瀏覽器非常相似）。再往前則是大型計算機與打孔卡……  而且這樣的故事還會繼續下去，我們似乎永遠也決定不了應該將計算資源放在哪裡。我們不停地在集中式和分散式之間來回切換。看起來，這樣的震盪還要再持續一段時間。  但從 IT 技術發展歷史的整體來看，我們會發現 Web 技術的出現並沒有改變任何東西。Web 技術的熱潮只是在這個早於我們出生，也肯定會超過我們職業生涯的震盪周期中的一瞬間。  作為系統架構師，我們必須著眼於長遠。這些震盪只是短期問題，我們應該將它們排除在核心業務規則之外。  讓我來講講 Q 公司的故事。Q 公司構建了一個非常流行的個人財務系統，這是一個具有非常實用 GUI 的桌面應用程式。我非常喜歡使用它。  然後 Web 技術的熱潮來了。在下一個版本中，Q 公司將 GUI 改成了類似瀏覽器的外觀和行為。我感到非常震驚！究竟是哪位市場天才決定讓一個桌面版的個人財務軟體看起來像瀏覽器？  當然，我非常討厭這個新介面。顯然其他人也這麼認為——因為在幾個版本之後，Q 公司逐漸移除了類似瀏覽器的設計，將其個人財務系統恢復成正常的桌面 GUI。  現在想像一下，你是 Q 公司的軟體架構師。假設某位市場天才說服了高層管理者，要求將整個 UI 改成類似 Web 的風格。你會怎麼做？或者說，在這種情況發生之前，你應該做些什麼來保護你的應用程式不受這種市場天才的影響？  你應該將業務規則與 UI 解耦。我不知道 Q 公司的架構師是否這麼做了。我很想聽聽他們的故事。如果當時我在，我一定會全力遊說他們將業務邏輯與 UI 解耦，因為你永遠不知道市場天才接下來會做什麼。  現在來看看 A 公司的故事，他們製造了一款很受歡迎的智能手機。最近他們發布了一個“操作系統”的升級版（談論手機內的操作系統本身就很奇怪）。這次“操作系統”的升級完全改變了所有應用程式的外觀和感覺。為什麼？我猜是某位市場天才的主意。  我對該設備內的軟體並不熟悉，所以不清楚這次改動是否對那些為 A 公司的手機開發應用程式的程式設計師造成了顯著困難。我只能希望 A 公司的架構師，以及應用程式的架構師，能將 UI 和業務邏輯分離，因為總有市場天才在等待抓住你創造的下一點耦合關係。  總結    圖表  \n       \n       總結來說：GUI 只是個細節，而 Web 是 GUI 的一種，所以 Web 也是個細節。作為軟體架構師，我們需要將這類細節與核心業務邏輯隔離開來。  其實我們可以這樣考慮這個問題：Web 只是一種 I/O 設備。早在 20 世紀 60 年代，我們就已經了解編寫設備無關應用程式的重要性。這種獨立性的重要性至今仍然沒有變化，Web 也不例外。  是這樣的嗎？有人可能會辯稱，像 Web 這樣的 GUI 是非常特殊且豐富的，追求設備無關的架構是荒謬的。當你考慮到 JavaScript 驗證的複雜性、可拖拽的 Ajax 調用，或是任何可以放在網頁上的小工具和小部件時，很容易認為設備無關性是不切實際的。  從某種程度上來說，這的確如此。應用程式和 GUI 之間的頻繁交互確實與 GUI 的類型密切相關。瀏覽器與 Web 應用之間的交互模式與桌面 GUI 和其應用程式之間的交互模式確實不同。試圖將這種交互抽象化，就像在 UNIX 中抽象化設備一樣，似乎是不可能的。  但我們其實可以從 UI 和應用程式之間的另一條邊界出發來進行抽象化。業務邏輯可以被視為一組用例的集合，每個用例都代表用戶執行某種操作。每個用例都可以根據輸入數據、處理過程和輸出數據來描述。  也就是說，在 UI 和應用程式之間的某個點上，輸入數據可以被認為是完整的，從而允許用例被執行。用例執行完畢後，生成的結果數據可以回饋到 UI 和應用程式之間的交互中。  這樣一來，完整的輸入數據和結果數據可以被標準化為數據結構，並作為執行用例的過程中的輸入值和輸出值。通過這種方法，我們可以認為每個用例都是以設備無關的方式在操作 UI 的 I/O 設備。  本章小结  這種抽象化處理並不簡單，可能需要多次嘗試才能達到理想的效果，但這是可行的。由於世界上充滿了行銷天才，很多時候這樣做確實是非常必要的。",{"id":587,"path":588,"dir":351,"title":589,"description":7,"keywords":590,"body":596},"content:6.clean-arch:32.chapter32.md","/clean-arch/chapter32","32 框架也只是細節",[591,592,593,594,595,577],"框架作者","嫁給框架-不對等的婚姻 (ASYMMETRIC MARRIAGE)","這是風險","解決方案","不得不接受的依賴","    本章重點   框架只是細節   框架並不等同於系統架構  框架作者的動機：框架作者通常是為了解決他們自己或周圍人的問題，並不一定適合你的需求。  \n       風險   不對等的婚姻：使用框架時，你需要做出巨大的承諾，但框架作者對你沒有任何承諾。  \n       解決方案   不要與框架耦合，保持一定距離，將框架視為架構外圈的一個細節。     一旦在專案中引入一個框架，很有可能在整個生命週期中都要依賴於它，不管後來情形怎麼變化，這個決定都很難更改了。  \n           原文翻譯   應用程式框架現在非常流行，通常來說這是一件好事。許多框架都非常強大、實用，而且是免費的。  然而，  框架並不等同於系統架構——儘管有些框架確實試圖成為系統架構。  框架作者  大部分框架的作者願意免費提供自己的工作成果，是因為他們想要幫助整個社群，想要回饋社會。這值得鼓勵，但不管這些作者的動機有多麼高尚，恐怕也沒有提供針對你個人的最佳方案。即使他們想，也做不到，因為他們並不了解你，也不了解你遇到的問題。  這些框架作者所了解的都是他們自己遇到的問題，可能還包括同事和朋友所遇到的。他們創造框架的目的是解決這些問題——而不是解決你遇到的問題。  當然，你所遇到的問題可能和其他人遇到的問題大致相同。如果不是這樣，框架也不會那麼流行。正是因為這種重合性的存在，框架才這麼有用。  嫁給框架-不對等的婚姻 (ASYMMETRIC MARRIAGE)  你和框架作者之間的關係是非常不對等的。你必須對框架做出巨大的承諾，但框架作者對你卻沒有任何承諾。  仔細想想這一點。當你使用一個框架時，你需要閱讀框架作者提供的文檔。在這些文檔中，框架作者和其他使用者會建議你如何將軟體與框架整合。通常，這意味著你需要將你的架構圍繞著框架來設計。作者會建議你從框架的基類派生，並將框架的功能引入到你的業務對象中。作者會敦促你盡可能緊密地將應用程式與框架結合。  此外，作者希望你與框架緊密結合，因為一旦這樣耦合，脫離框架就會變得非常困難。對框架作者來說，沒有什麼比一群用戶心甘情願地從作者的基類派生更令人滿意的了。  換句話說，框架作者想讓我們與框架訂終身——這相當於我們要對他們的框架做一個巨大而長期的承諾，而在任何情況下框架作者都不會對我們做出同樣的承諾。這種婚姻是單向的。我們要承擔所有的風險，而框架作者則沒有任何風險。  這是風險  那麼我們要承擔的風險究竟有哪些呢？我們可以想到的至少有以下這幾項：  框架本身的架構設計很多時候並不是特別正確的。框架本身可能經常違反依賴關係原則。譬如，框架可能會要求我們將代碼引入到業務對象中——甚至是業務實體中。框架可能會想要我們將框架耦合在最內圈代碼中。而我們一旦引入，就再也不會離開該框架了，這就像戴上結婚戒指一樣，從此一生不離不棄了。  框架可能會幫助我們實現一些應用程式的早期功能，但隨著產品的成熟，功能要求很可能超出框架所能提供的範圍。而且隨著時間的推移，我們也會發現在應用的開發過程中，自己與框架鬥爭的時間要比框架幫助我們的時間長得多。  框架本身可能朝著我們不需要的方向演進。也許我們會被迫升級到一個並不需要的新版本，甚至會發現自己之前所使用的舊功能突然消失了，或悄悄改變了行為。  未來我們可能會想要切換到一個更新、更好的框架上。  解決方案  解決方案是什麼呢？  請不要嫁給框架! (Don’t marry the framework!)  哦，你可以使用框架——但不要與它耦合。保持一定的距離。將框架視為架構外圈的一個細節，不要讓它進入內圈。  如果框架要求你根據它們的基類來創建派生類，就請不要這樣做！你可以創造一些代理類，並將這些代理類當作業務邏輯的插件來管理。  不要讓框架進入你的核心代碼。相反，應該依據依賴關係原則，將它們整合到作為核心代碼插件的組件中。  以 Spring 為例，它作為一個依賴注入框架是很不錯的，也許你會需要用 Spring 來自動連接應用程式中的各種依賴關係。這不要緊，但千萬別在業務對象裡到處寫 @autowired 註解。業務對象應該對 Spring 完全不知情才對。  相反，你可以利用 Spring 將依賴關係注入到 Main 組件中，畢竟 Main 組件作為系統架構中最低層、依賴最多的組件，它依賴於 Spring 並不是問題。  不得不接受的依賴  有一些框架是避免不了使用的。例如，如果你在用 C++，那麼 STL 就是很難避免使用的。如果你在用 Java，那麼標準類庫也是不太可能避免使用的。  這很正常——但這仍然應該是你主動選擇的結果。你必須明白，如果一旦在專案中引入一個框架，很有可能在整個生命週期中都要依賴於它，不管後來情形怎麼變化，這個決定都很難更改了。因此，不應該草率地做出決定。  本章小结  總而言之，當我們面臨框架選擇時，盡量不要草率地做出決定。在全身心投入之前，應該首先看看是否可以部分地採用以增加了解。另外，請盡可能長時間地將框架留在架構邊界之外，越久越好。因為誰知道呢，也許你可以不用買奶牛也能喝到牛奶。",{"id":598,"path":599,"dir":351,"title":600,"description":7,"keywords":601,"body":608},"content:6.clean-arch:33.chapter33.md","/clean-arch/chapter33","33 影片銷售系統案例研究",[602,603,604,605,606,607],"1. 案例研究的重要性","2. 產品概述","3. 架構設計的核心精神","4. 實務考量與權衡","5. 討論問題","6. 結論","  Clean Architecture 第33章：影片銷售系統案例研究  1. 案例研究的重要性  這章透過一個實際的影片銷售系統案例，展示了如何將 Clean Architecture 的原則應用到真實世界。這個案例特別重要，因為它：   展示了如何從需求和業務邏輯出發進行系統設計  說明了如何在實務中平衡架構原則和實際限制  示範了好的架構如何支援系統演進  2. 產品概述  本案例探討一個線上影片銷售網站的系統架構設計：   提供影片串流和下載服務  支援個人用戶和企業用戶不同的授權模式  個人用戶可以：\n   付費串流觀看  付較高價格永久下載擁有  企業用戶限定：\n   僅提供串流服務  可購買批量授權並享有數量折扣  3. 架構設計的核心精神  3.1 從 Use Case 開始設計  系統設計的第一步是識別 actors 和 use cases，這反映了 Clean Architecture 重視業務規則的核心理念。  圖 33.1 Use Case 分析    這張圖展示了系統的 Use Case 分析：    四個主要角色（Actors）   清楚地展示了系統的主要使用者群  每個角色都是系統變更的潛在來源   中心的抽象 Use Case    View Catalog  作為抽象 use case  被   View Catalog as Viewer  和   View Catalog as Purchaser  繼承  展示了如何處理相似但不完全相同的功能  這種抽象能降低代碼重複，但保持足夠的彈性   Use Case 的分布   每個角色都有其特定的 use cases  清楚顯示了每個角色的職責範圍  有助於理解系統的整體功能範圍  圖 33.2 元件架構    這張圖展示了系統的初步元件架構：    層次結構   使用雙線表示架構邊界  展示了典型的 Clean Architecture 分層：\n   Views（最外層）  Presenters  Interactors  Controllers（最內層）   依賴方向   控制流程從右到左  但大多數依賴箭頭指向左側  符合依賴規則：所有依賴都指向包含更高層級策略的元件   特殊處理   Catalog View 和 Catalog Presenter 的特殊元件  對應了圖 33.1 中的抽象 View Catalog use case  使用抽象類別來實現基礎功能   部署彈性   每個框代表潛在的 .jar 或 .dll 文件  可以根據需求選擇不同的部署策略  保持了架構的彈性  主要角色（Actors）：    觀看者（Viewer）   觀看購買的影片  瀏覽影片目錄   購買者（Purchaser）   購買影片授權  在企業情境中常與觀看者為不同人   作者（Author）   提供影片檔案  提供書面說明  上傳輔助材料   管理者（Administrator）   新增影片系列  管理影片內容  設定授權價格  特別注意：   系統架構應該清楚展現這些 use cases  例如   View Catalog  作為抽象 use case 的處理方式，展示了如何優雅地處理共用功能  這種設計讓系統的意圖一目了然  3.2 分離的藝術  系統採用兩個維度的分離策略：    水平分離（Layers）   基於 Clean Architecture 的同心圓架構  由外而內依序為：\n   Views（視圖層）  Presenters（展示層）  Interactors（互動層）  Controllers（控制層）  每層都有其明確職責   垂直分離（Actors）   基於不同的角色進行分離  確保對一個角色的改動不會影響其他角色  反映了 Conway 法則：系統架構應映射組織結構  這種分離方式的優勢：   每個元件都有明確的單一職責  變更能被限制在特定範圍內  支援多團隊並行開發  提高了系統的可維護性和擴展性  3.3 依賴管理的實踐  本案例展示了如何具體實踐依賴規則：    控制流程   從右到左流動  Controllers → Interactors → Presenters → Views   源碼依賴   從左到右指向  確保高層策略（業務規則）不依賴於低層細節  使用開放封閉原則（OCP）維護正確的依賴方向  3.4 部署彈性的預留  架構設計提供了多層次的部署選項：    完全分離   每個元件獨立部署為 jar/dll 檔案  最大化彈性，但部署複雜度較高   按層級分組   將相關元件組合成較大的部署單元  例如：前端層、業務層、資料層   簡化部署   可以簡化為前後端兩個主要部分  適合小型專案或初期開發  重要的是：   即使選擇合併部署，程式碼層面的分離仍然重要  這種彈性允許系統根據需求演進  體現了「保持選項開放」的原則  4. 實務考量與權衡    避免過度設計   作者不主張教條式的完全分離  承認可以根據實際需求調整部署策略  強調架構的目的是服務業務需求   演進的支援   架構設計應該支援系統隨時間演進  允許在不同部署策略間轉換  保持核心業務邏輯的獨立性   團隊協作   架構設計考慮到開發團隊的組織結構  支援多團隊並行開發  明確的介面定義減少團隊間的衝突  5. 討論問題   為什麼作者選擇以角色（Actor）作為系統分割的主要依據？  這個架構如何體現了清晰架構（Clean Architecture）的核心原則？  在實際專案中，如何權衡元件拆分的粒度？  如何在保持架構彈性和降低複雜度之間取得平衡？  這種架構設計如何支援敏捷開發和持續交付？  6. 結論  本案例研究展示了：   如何將抽象的架構原則落實到具體設計中  好的架構如何支援系統的長期發展  如何在理想架構和實務限制間取得平衡",{"id":610,"path":611,"dir":351,"title":612,"description":7,"keywords":613,"body":621},"content:6.clean-arch:34.chapter34.md","/clean-arch/chapter34","Clean Architecture 第34章：The Missing Chapter",[614,615,616,617,618,619,620],"1. 章節主旨","2. 核心問題：程式碼組織方式的選擇","3. 訪問修飾符的重要性","4. 進階解耦方式","5. 常見陷阱：Périphérique 反模式","6. 如何選擇合適的架構方式？","7. 結論","  Clean Architecture 第34章：The Missing Chapter  1. 章節主旨  本章由 Simon Brown 撰寫，探討了一個常被忽略但極其重要的話題：即使有良好的架構設計，如果在實作細節上處理不當，仍可能導致整個架構崩壞。作者通過一個線上書店的訂單查詢功能為例，展示了不同的程式碼組織方式及其優缺點。  2. 核心問題：程式碼組織方式的選擇  2.1 Package by Layer（按層級分包）     com.company.myapp  /\n       └── web  /\n           └── OrdersController.java          // 處理 HTTP 請求\n       └── service  /\n           └── OrdersService.java             // 服務層介面\n           └── OrdersServiceImpl.java         // 服務層實現\n       └── repository  /\n           └── OrdersRepository.java          // 資料訪問層介面\n           └── JdbcOrdersRepository.java      // 資料訪問層實現\n   特點 ：   按技術功能水平切分（Web、Service、Repository）  每層只依賴下一層  類似經典的三層架構   優點 ：   結構簡單，容易理解  適合小型專案快速開發  技術關注點分離清晰   缺點 ：   無法反映業務領域結構  當專案變大時難以維護  容易出現依賴跨層的情況     // 常見的錯誤：跨層調用\n   public   class   OrdersController   {\n       @  Autowired\n       private   OrdersRepository   repository;   // 直接依賴 Repository 層，跳過 Service 層\n   }\n  2.2 Package by Feature（按功能分包）     com.company.myapp  /\n       └── orders  /\n           └── OrdersController.java      // 所有訂單相關的類\n           └── OrdersService.java         // 都放在同一個包中\n           └── OrdersRepository.java\n       └── users  /\n           └── UsersController.java       // 所有用戶相關的類\n           └── UsersService.java          // 都放在同一個包中\n           └── UsersRepository.java\n   特點 ：   按業務功能垂直切分  相關的類都放在一起  體現了業務領域結構   優點 ：   容易找到相關程式碼  反映業務領域  降低跨包調用   缺點 ：   可能導致程式碼重複     // orders 套件\n   package   com.company.orders  ;\n   class   ValidationUtils   {\n       static   boolean   validateEmail  (  String   email  ) {\n           // email 驗證邏輯\n       }\n   }\n   \n   // users 套件\n   package   com.company.users  ;\n   class   ValidationUtils   {    // 重複的驗證邏輯\n       static   boolean   validateEmail  (  String   email  ) {\n           // 相同的 email 驗證邏輯\n       }\n   }\n   套件之間的界限可能不清晰     package   com.company.orders  ;\n   \n   public   class   OrderProcessor   {\n       private   UserService   userService;    // 應該屬於 users 套件？還是 orders 套件？\n       private   PaymentService   paymentService;    // 支付相關是否應該獨立成套件？\n       \n       public   void   processOrder  (  Order   order  ) {\n           // 訂單處理邏輯與用戶、支付邏輯混合\n       }\n   }\n   共享組件的位置可能難以決定     // 這些工具類應該放在哪個套件？\n   package   com.company.common  ;    // 創建新套件？\n   // 或是\n   package   com.company.orders  ;    // 放在既有套件？\n   // 或是\n   package   com.company.utils  ;    // 獨立工具套件？\n   \n   public   class   DateUtils   {\n       public   static   String   formatDate  (  Date   date  ) {...}\n   }\n  2.3 Ports and Adapters（端口和適配器）     com.company.myapp  /\n       └── domain  /                    // 核心業務邏輯（內部）\n           └── orders  /\n               └── Orders.java            // 領域模型\n               └── OrdersService.java     // 業務邏輯\n       └── infrastructure  /           // 技術實現（外部）\n           └── web  /\n               └── OrdersController.java      // Web 適配器\n           └── persistence  /\n               └── JdbcOrdersRepository.java   // 資料庫適配器\n   特點 ：   明確分離業務邏輯和技術實現  區分內部（domain）和外部（infrastructure）  使用領域術語進行命名   核心規則 ：   外部依賴內部  內部不知道外部的存在  使用依賴反轉原則    複雜度增加   需要更多的介面和轉接層  初期開發速度較慢  對團隊要求較高     // 一個簡單的查詢可能需要多層轉換\n   public   interface   OrderPort   {    // 領域端口\n       OrderDomain   findOrder  (  String   id  );\n   }\n   \n   public   interface   OrderRepository   {    // 持久化端口\n       OrderEntity   findById  (  String   id  );\n   }\n   \n   public   class   OrderAdapter   implements   OrderPort   {    // 轉接器\n       private   final   OrderRepository   repo;\n       private   final   OrderMapper   mapper;\n       \n       @  Override\n       public   OrderDomain   findOrder  (  String   id  ) {\n           OrderEntity   entity   =   repo.  findById  (id);\n           return   mapper.  toDomain  (entity);\n       }\n   }\n    過度設計風險   小型專案可能不需要這麼多層級  可能導致「架構過度症」(Architecture Astronaut)    效能考量     // 多層轉換可能影響效能\n   @  RestController\n   public   class   OrderController   {\n       private   final   OrderPort   orderPort;\n       \n       public   OrderDTO   getOrder  (  String   id  ) {\n           OrderDomain   domain   =   orderPort.  findOrder  (id);    // 第一次轉換\n           return   mapper.  toDTO  (domain);    // 第二次轉換\n       }\n   }\n  2.4 Package by Component（按組件分包）     com.company.myapp  /\n       └── web  /                             // 所有 web 相關的控制器\n           └── OrdersController.java\n       └── components  /\n           └── orders  /\n               └── api  /\n                   └── OrdersComponent.java      // 公開的介面\n               └── internal  /                     // 內部實現\n                   └── OrdersBusinessLogic.java\n                   └── OrdersDataAccess.java\n   特點 ：   將相關的業務邏輯和資料訪問封裝在一起  只暴露必要的公共介面  強調封裝性和組件化   優點 ：   更好的封裝性  清晰的組件邊界  可以通過編譯器強制執行架構規則   缺點 ：    元件粒度拿捏困難     // 太大的元件\n   package   com.company.sales  ;\n   public   interface   SalesComponent   {    // 責任太多\n       void   processOrder  ();\n       void   handleShipping  ();\n       void   manageInventory  ();\n       void   handleReturns  ();\n       void   processPayment  ();\n   }\n   \n   // 太小的元件\n   package   com.company.order.validation  ;\n   public   interface   OrderValidationComponent   {    // 過度切分\n       boolean   validateOrderAmount  ();\n   }\n    跨元件溝通複雜化     public   interface   OrderComponent   {\n       Order   createOrder  (  OrderRequest   request  );\n   }\n   \n   public   interface   InventoryComponent   {\n       void   updateStock  (  String   productId  ,   int   amount  );\n   }\n   \n   // 跨元件協作變得複雜\n   @  Service\n   public   class   OrderProcessor   {\n       private   final   OrderComponent   orderComponent;\n       private   final   InventoryComponent   inventoryComponent;\n       private   final   PaymentComponent   paymentComponent;\n       \n       public   void   processOrder  (  OrderRequest   request  ) {\n           // 需要協調多個元件的操作\n           Order   order   =   orderComponent.  createOrder  (request);\n           inventoryComponent.  updateStock  (order.  getProductId  (), order.  getAmount  ());\n           paymentComponent.  process  (order.  getPaymentInfo  ());\n       }\n   }\n    可能導致隱性耦合     // OrderComponent 可能因為共享資料結構而與其他元件產生耦合\n   public   interface   OrderComponent   {\n       void   createOrder  (  OrderRequest   request  );    // OrderRequest 可能包含其他元件的邏輯\n   }\n   \n   public   class   OrderRequest   {\n       private   CustomerInfo   customerInfo;    // 來自 Customer 元件\n       private   PaymentInfo   paymentInfo;     // 來自 Payment 元件\n       private   ShippingInfo   shippingInfo;   // 來自 Shipping 元件\n   }\n    測試複雜度增加     @  Test\n   public   void   testOrderCreation  () {\n       // 需要模擬多個相依元件\n       OrderComponent   orderComponent   =   mock  (OrderComponent.class);\n       InventoryComponent   inventoryComponent   =   mock  (InventoryComponent.class);\n       PaymentComponent   paymentComponent   =   mock  (PaymentComponent.class);\n       \n       // 設置期望行為變得複雜\n       when  (orderComponent.  createOrder  (  any  ())).  thenReturn  (  new   Order  ());\n       when  (inventoryComponent.  checkStock  (  any  ())).  thenReturn  (  true  );\n       \n       // 測試邏輯...\n   }\n  3. 訪問修飾符的重要性  3.1 核心概念  訪問修飾符是實現封裝的關鍵工具，它決定了架構規則是否能被切實執行。  問題情境  想像一下這個場景：     // 所有類都是 public\n   public   class   OrdersController   { ... }\n   public   class   OrdersService   { ... }\n   public   class   OrdersRepository   { ... }\n   public   class   OrdersServiceImpl   { ... }\n   public   class   JdbcOrdersRepository   { ... }\n  當所有類都是 public 時：   任何類都能被任意訪問  架構規則只能靠人為遵守  分包（package）失去意義，淪為純組織作用  這就像是：   蓋了一棟大樓，但所有門都不上鎖  設計了管制流程，但人人都有萬能鑰匙  規定要從一樓坐電梯，但樓梯間也不鎖門  3.2 正確的使用方式  A. 基本原則     package   com.company.orders  ;\n   \n   // 1. 對外開放的介面：public\n   public   interface   OrdersComponent   {\n       void   processOrder  (  Order   order  );\n   }\n   \n   // 2. 內部實現：package-private（不加修飾符）\n   class   OrdersBusinessLogic   implements   OrdersComponent   {\n       // 3. 內部細節：private\n       private   final   OrdersRepository   repository;\n       \n       OrdersBusinessLogic  (  OrdersRepository   repo  ) {\n           this  .repository   =   repo;\n       }\n       \n       @  Override\n       public   void   processOrder  (  Order   order  ) {\n           // 實現細節\n       }\n   }\n  B. 錯誤示例和修正     // 錯誤示例\n   package   com.company.orders.service  ;\n   \n   public   class   OrdersServiceImpl   {            // 錯誤：實現類不需要 public\n       public   OrdersRepository   repository;      // 錯誤：字段不應該是 public\n       \n       public   void   internalProcess  () { ... }   // 錯誤：內部方法不應該是 public\n   }\n   \n   // 正確示例\n   package   com.company.orders.service  ;\n   \n   class   OrdersServiceImpl   {                   // 正確：package-private\n       private   final   OrdersRepository   repository;   // 正確：private\n       \n       OrdersServiceImpl  (  OrdersRepository   repo  ) {   // 正確：package-private\n           this  .repository   =   repo;\n       }\n       \n       void   internalProcess  () { ... }          // 正確：package-private\n   }\n  3.3 各架構方式的具體應用  A. Package by Layer     // web 包\n   public   class   OrdersController   { ... }        // public：需要接收外部請求\n   \n   // service 包\n   public   interface   OrdersService   { ... }       // public：需要被 controller 使用\n   class   OrdersServiceImpl   { ... }              // package-private：實現細節\n   \n   // repository 包\n   public   interface   OrdersRepository   { ... }    // public：需要被 service 使用\n   class   JdbcOrdersRepository   { ... }           // package-private：實現細節\n  B. Package by Feature     package   com.company.orders  ;\n   \n   public   class   OrdersController   { ... }       // public：對外接口\n   class   OrdersService   { ... }                 // package-private：內部服務\n   class   OrdersRepository   { ... }              // package-private：內部存儲\n  C. Ports and Adapters     // domain 包\n   public   interface   Orders   { ... }             // public：核心領域接口\n   class   OrdersService   { ... }                 // package-private：領域邏輯\n   \n   // infrastructure 包\n   public   class   WebController   { ... }          // public：外部適配器\n   class   DatabaseRepository   { ... }            // package-private：實現細節\n  D. Package by Component     package   com.company.orders  ;\n   \n   // api 包\n   public   interface   OrdersComponent   { ... }     // public：組件對外接口\n   \n   // internal 包\n   class   OrdersLogic   { ... }                   // package-private：內部實現\n   class   OrdersData   { ... }                    // package-private：內部實現\n  3.4 訪問修飾符帶來的好處    編譯時強制執行架構規則     // 這樣的程式碼將無法通過編譯\n   public   class   OrdersController   {\n       private   JdbcOrdersRepository   repository;   // 錯誤：無法訪問 package-private 類\n   }\n    防止意外依賴     // 這樣的跨層調用會在編譯時被阻止\n   public   class   WebLayer   {\n       private   DatabaseLayer   db;   // 錯誤：無法訪問其他包的 package-private 類\n   }\n    明確的封裝邊界     package   com.company.orders  ;\n   \n   public   interface   OrdersComponent   {    // 這是唯一可以被外部使用的類\n       // 公開的 API\n   }\n   \n   // 所有實現細節都被封裝在包內\n   class   Implementation1   { ... }\n   class   Implementation2   { ... }\n   class   Helper   { ... }\n  3.5 實踐建議    設計原則 ：   預設使用最嚴格的訪問級別  只在必要時才開放訪問權限  優先使用 package-private   檢查清單 ：   是否真的需要將這個類設為 public？  這個方法需要被外部調用嗎？  這個字段可以設為 private 嗎？   常見錯誤 ：   習慣性地將所有類設為 public  為了單元測試而過度開放訪問權限  忽視 package-private 的封裝作用  通過正確使用訪問修飾符，我們可以：   讓編譯器幫助我們執行架構規則  提供更好的封裝性  防止架構被意外破壞  使程式碼更容易維護和重構  4. 進階解耦方式  4.1 模組系統  A. OSGi (Open Service Gateway initiative)     // OSGi bundle manifest 示例\n   Bundle  -  SymbolicName  :   com.mycompany.orders\n   Bundle  -  Version  :   1.0  .  0\n   Export  -  Package  :   com.mycompany.orders.api\n   Import  -  Package  :   com.mycompany.common.api\n   允許運行時動態加載/卸載模組  精確控制模組間可見性  模組生命週期管理  B. Java 9 模組系統     // module-info.java\n   module   com  .mycompany.orders {\n       exports   com.mycompany.orders.api;\n       requires   com.mycompany.common;\n       provides   OrderService   with   OrderServiceImpl;\n   }\n   明確定義模組邊界  區分 public 和 published 類型  通過 exports 控制可見性  4.2 源碼層面解耦  A. 完全分離方案     project-root/\n   ├── domain/                 # 業務領域程式碼\n   │   └── src/\n   │       └── OrdersService.java\n   ├── web/                    # Web 層程式碼\n   │   └── src/\n   │       └── OrdersController.java\n   └── persistence/           # 資料持久層程式碼\n       └── src/\n           └── JdbcOrdersRepository.java\n  B. 簡化的雙源碼樹方案     project-root/\n   ├── domain/                # 內部程式碼（領域核心）\n   └── infrastructure/        # 外部程式碼（技術實現）\n  5. 常見陷阱：Périphérique 反模式  5.1 問題描述   名稱來源：巴黎的外環道路（可以繞過市中心）  基礎設施層的組件直接互相調用，繞過領域層  5.2 示例     // 錯誤示例\n   public   class   OrdersController   {\n       @  Autowired\n       private   JdbcOrdersRepository   repository;   // 直接依賴其他基礎設施層程式碼\n       \n       public   void   handleRequest  () {\n           repository.  findOrder  (id);   // 繞過領域層直接訪問資料庫\n       }\n   }\n  5.3 如何避免   強制通過領域層通信  正確使用訪問修飾符  定期架構審查  6. 如何選擇合適的架構方式？  6.1 小型專案（3-10人團隊）    建議 ：Package by Feature   原因 ：開發快速、維護簡單  6.2 中型專案（10-30人團隊）    建議 ：Package by Component + Java 9 Modules   原因 ：良好封裝、清晰邊界  6.3 大型專案（30人以上團隊）    建議 ：Ports and Adapters + 分離源碼樹   原因 ：高度模組化、嚴格架構控制  7. 結論   架構的成功在於實作細節  訪問修飾符是關鍵工具  根據團隊規模選擇架構  警惕架構反模式  保持架構的持續改進  找到適合團隊的平衡點  html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"id":623,"path":624,"dir":351,"title":625,"description":626,"keywords":627,"body":628},"content:6.clean-arch:99.chapter99.md","/clean-arch/chapter99","99 來自於網路世界的書評","https://www.frederikbanke.com/book-review-clean-architecture/",[],"    https://www.frederikbanke.com/book-review-clean-architecture/   Clean Architecture 架構的四個主要商業價值：   提高開發效率：通過分離關注點、明確依賴關係和簡化測試，可以提高開發團隊的工作效率。  降低維護成本：系統更易於修改、減少技術債務，並降低換人成本，從而降低長期維護成本。  增強系統靈活性：可以延遲技術決策、易於更換組件，並能更好地適應需求變化，使系統更加靈活。  提升系統質量：通過強制執行架構規則、隔離不穩定部分，並讓開發人員能夠專注於核心業務邏輯，從而提高整體系統質量。   \n      Clean Architecture 歸納為商業模式圖 \n     ",{"id":630,"path":631,"dir":632,"title":633,"description":7,"keywords":634,"body":640},"content:7.sdi-aig:1.chapter1.md","/sdi-aig/chapter1","sdi-aig","01 從 0 到百萬用戶",[65,635,636,637,638,639],"單伺服器設置","從單伺服器獨立資料庫出來","負載平衡","資料庫複製","快取","  前言  從單一主機系統，隨著用戶與服務的擴展，到達百萬用戶的過程，會有哪些改變  單伺服器設置   包含的組件：Web server（應用程式、資料庫、快取）、網域、使用者端應用程式  網域(DNS)是由第三方提供的付費服務，不是由伺服器託管  資料流程：\n   使用者透過網域訪問網站或服務，例如：  api.mysite.com  透過第三方DNS取得主機IP位置，例如：15.125.23.214  取得IP位置後，向伺服器發送HTTP請求  伺服器回應HTML頁面或JSON內容   問題：你如何設計一個單伺服器系統來處理來自 Web 與移動端的請求？這樣的設計有什麼優缺點？   初期可以將所有組件（Web 伺服器、應用程式、資料庫、快取）部署在同一台機器上  流程上：用戶從 DNS 解析取得 IP，再直接發送 HTTP 請求給該伺服器，由伺服器產生 HTML 或 JSON 格式的回應  優點：部署簡單、成本較低  缺點：隨著流量成長，單機容易成為瓶頸且無法提供高可用性  從單伺服器獨立資料庫出來   隨著用戶的增長，單伺服器已經不夠，需要變成一個處理Web/App流量，另一個處理資料庫，因此將伺服器拆分成Web層與數據層，允許他們獨立發展  關聯式資料庫：   資料以表格（Tables）方式組成，每個表格由列（Columns）與行（Rows）構成  資料模式 (Schema) 預先定義，確保數據結構的一致性與規範化  提供主鍵 (Primary Key)、外鍵 (Foreign Key) 等機制，強制數據之間互相關聯的正確性  ACID 特性：交易具有原子性 (Atomicity)、一致性 (Consistency)、隔離性 (Isolation) 與持久性 (Durability)  確保在並發操作與系統故障時，數據能夠正確保存與回滾  範例：MySQL、PostgreSQL、Oracle、SQL Server  非關聯式資料庫：   關鍵值資料庫 (Key-Value Store)\n   以鍵（Key）對應值（Value）的方式儲存資料  查詢速度極快，適用於快取、會話資料等需要高效讀寫場景  範例：Redis、Memcached、DynamoDB（可作為鍵值儲存）  Columnar 資料庫 (Column-family Database)\n   假設儲存一個使用者資料，資料會根據欄族來切分，各欄族包含相關欄位資料，資料表：Users，利用 row key（使用者ID）來識別，每個 row 內可能包含多個欄族（如個人基本資料、聯絡資訊）  對於大量資料的讀寫及分析查詢有良好效能  範例：Apache Cassandra、HBase  圖形資料庫 (Graph Database)\n   以節點、邊、屬性來表示及儲存資料間的關係，用於推薦系統與社交網路，包含紀錄節點以及節點間的關係  負載平衡   垂直擴展：又稱為縱向擴展，透過增加單一個伺服器的計算能力，例如增加CPU、RAM來增加效能  水平擴展：又稱為橫向擴展，透過增加更多伺服器  負載平衡   使用者直接連線負載平衡伺服器，而不是直接連線Web伺服器  為了提高安全性，Web伺服器間的資訊流都是使用私有IP（10開頭、192開頭），確保使用者無法直接連線Web伺服器  透過Load balancer伺服器與兩個Web伺服器後，解決Web伺服器故障切換問題，提高伺服器的可用性，以滿足SLA要求  資料庫複製   透過負載平衡解決伺服器可用性的問題，但是資料庫要怎麼應付大流量，一般都會建立主從資料庫，寫入主資料庫(main DB)，讀從資料庫(Slave DB)，從資料庫定期從主資料庫同步資料  優勢：   更好的性能：在這個主／從模型中，所有的寫入和更新都發生在主節點，而所有的讀操作分佈在從節點。這種模型提高了性能，因為它允許平行處理更多的查詢。  可靠性：如果你的其中一臺資料庫被颱風、地震等自然災害破壞，資料仍然會被保留。你無需擔心資料遺失，因為資料被複製到多個位置。  高可用性：透過在不同位置複製資料，即使一臺資料庫離線，你的網站仍然可以運行，因為你可以存取儲存在另一個資料庫伺服器中的資料。  如果一個DB掛掉怎麼辦   如果只有一個從資料庫可用，但它離線，讀操作將臨時指向主資料庫。  如果有多個從資料庫可用，讀操作則會被轉發到其他健康的從資料庫。  如果主資料庫離線，一個從資料庫會被提升為新的主資料庫，所有的資料庫操作都會臨時在新的主資料庫上執行。  快取   快取是一個臨時儲存區，用於將昂貴的回應結果或頻繁存取的資料儲存在記憶體中，以便之後的請求能更快地處理  在接收到請求後，Web 伺服器首先檢查快取是否有可用的回應   如果有，它會將數據發送回客戶端  如果沒有，它將查詢資料庫，將回應結果保存到快取中，並將其發送回客戶端。  快取使用時的注意事項   決定何時使用快取：\n   當資料頻繁讀取但不經常修改時  由於快取資料存儲在易失的記憶體中，因此快取伺服器不適合持久化資料。例如，如果快取伺服器重啟，記憶體中所有的資料都會遺失，因此重要的資料應該保存在持久性資料存儲中。  過期策略：\n   一旦快取資料過期，就會從快取中刪除。若沒有過期策略，快取資料將會永久保存在記憶體中  建議不要將過期時間設定得太短，因為這會導致系統過於頻繁地從資料庫重新載入資料。同時，建議不要將過期時間設定得太長，因為資料可能會過時。  一致性：\n   快取與後端資料庫的同步機制(包含跨資料中心)  後端資料庫更新後，快取資料的更新機制   快取失效（Cache Invalidation） ：當資料庫中的資料更新時，對應的快取數據要自動標記為失效或直接清除，迫使用戶重新從資料庫獲取最新資料。   雙寫策略（Write-Through 或 Write-Behind） ：同步更新資料庫和快取，但這通常需要更精細的控制來避免鴿子失效或競爭條件。  減少故障：   單一快取伺服器代表著潛在的單點故障（SPOF），單一的快取伺服器如果故障，就會導致整個系統連帶出現問題  **多伺服器部署：**為了避免單點故障，可以在不同的資料中心或伺服器上部署多個快取伺服器   預留內存的策略 ：配置比所需大小多出一定百分比的記憶體，這在記憶體使用量上升時可起到緩衝效果  驅逐策略：   一旦快取滿了，任何嘗試向快取中添加內容的請求都可能導致現有項目被移除  最近最少使用（LRU）是最流行的快取驅逐策略  其他逐出策略，例如：最不常用（LFU）或先進先出（FIFO）  無狀態的Web層   現在是時候考慮橫向擴展 Web 層了，為此，我們需要將狀態（例如使用者會話資料）移出 Web 層。一個好的做法是將會話資料儲存於持久性存儲中，例如關聯式資料庫或 NoSQL。集群中的每個 Web 伺服器都可以從資料庫取得狀態資料，這種方式稱為無狀態的 Web 層。   有狀態架構\n   伺服器會記錄用戶相關識別資料 ex.Session，供下一個請求時記住用戶端數據(狀態)",{"id":642,"path":643,"dir":632,"title":644,"description":645,"keywords":646,"body":662},"content:7.sdi-aig:4.chapter4.md","/sdi-aig/chapter4","04 設計網路限速器","ref:https://github.com/Admol/SystemDesign/blob/main/CHAPTER 04：DESIGN A RATE LIMITER.md",[65,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661],"第一步驟—了解問題並確立設計的範圍","第二步驟—提出高階設計並取得認可","限速器要放在哪裡？","網路限速演算法","高階架構","第三步驟—深入設計","限速規則","超出速度限制","詳細的設計","分散式環境下的網路限速器","效能最佳化","設計資料密集型應用 9.1 一致性保證","監控","第四步驟—彙整總結","實際案例","  ref:  https://github.com/Admol/SystemDesign/blob/main/CHAPTER 04：DESIGN A RATE LIMITER.md  設計網路限速器  前言  在網路系統中，限速器(rate limiter) 可用來控制客戶端或服務端發送流量的速度。在HTTP的世界裡，網路線速器可以用來限制客戶端在指定時段內發送請求的數量。如果API請求數量超過網路線速器所定義的門檻值，所有額外的調用都會被擋下來。  面試官：   使用者每秒最多只能發出兩則貼文  每天最多只能用同一個IP位置建立10個帳號  每週最多只能用同一部設備領取5次獎勵  限速器好處：   避免DOS(Denial of Service:拒絕服務)攻擊造成資源不足的問題：  Twitter API: 300/3hr  Google API: 300/60s  降低成本：  針對過多的請求作出限制，可以降低伺服器的負擔，讓更多資源分配給具有高優先等級的API。限速的做法對於第三方付費API的公司而言非常重要。例如每次只要用到以下外部API：檢查可用額度、付款、檢索健康記錄等都會被收取一筆費用。以降低成本的角度來說，限制調用次數的做法非常重要。\n案例：  如果我們使用Google Maps API製作APP給使用者查詢地點與導航服務  •\t  每 1,000 次地圖載入 ：收費 $7 美元。  •\t  每 1,000 次地點搜尋 (Places API) ：收費約 $17 美元。  •\t  每 1,000 次導航路線查詢 (Directions API) ：收費約 $5 美元。  如果沒有限速可能會面臨非常貴的API成本  防止伺服器出現超載：  可以利用限速器篩選掉網路機器人或是不當使用者來降低負載  第一步驟—了解問題並確立設計的範圍  限速器的做法可以採用各種不同演算法來進行實作，每一種演算法都各有其利弊。面試官與應試者之間的互動，有助於釐清所要建立的是哪種類型的網路線速器。  面試官與應試者對話  應試者：我們要設計哪一種網路限速器？是客戶端網路限速器，還是伺服端API網路限速器？  面試官：很好的問題。我們就專注於伺服端API網路限速器吧。  應試者：網路限速器是否要根據IP、使用者ID或其他屬性，來限制API請求？  面試官：這個網路限速器應該要足夠靈活，以支援不同的限制規則組合。  應試者：系統的規模有多大？我們要針對新創公司，還是針對擁有龐大使用者的大公司來打造？  面試官：系統必須能夠處理大量請求。  應試者：系統要有能力在分散式環境中正常運作？  面試官：是的。  應試者：這個限速器是單獨的服務，還是應該在應用程式碼中進行實作？  面試官：這個設計決策有你來決定。  應試者：我們需要對受限制的使用者進行通知？  面試官：要。  需求   準確限制過多的請求。  低延遲。網路限速器不應減慢HTTP的回應時間。  盡可能少用一些記憶體。  採用分散式的網路限速做法。多部伺服器或多個process行程，可共用同一個網路限速器。  異常處理能力。當使用者請求受到限制時，要向使用者顯示明確的異常通知。  高容錯能力。如果網路限速器出現任何問題(例如快取伺服器離線)，也不能影響到整個系統。  第二步驟—提出高階設計並取得認可  限速器要放在哪裡？   客戶端實作：\n   一般來說，在客戶端實作網路限速器比較不可靠，因為客戶端的請求很容易被惡意行為惡搞。而且，我們有可能根本就無法控制客戶端的實作。  伺服器端實作：\n   位於伺服器的網路限速器，直接在ap層處理的限速器    中間網路限速器    案例：    假設API只能處理2 req/s 而客戶端卻在一秒鐘內發送了3次請求。前兩次請求會被轉送到API伺服器，但是第三次會被擋下來。並返回一個HTTP 429狀態碼。HTTP 429表示使用者發送過多請求。  基於雲端微服務的流行趨勢，通常可以在API gateway元件進行實作。API gateway是一種具有完整管理功能的服務，可以支援網路限速、SSL終止、身份驗證、IP白名單等功能，也可以針對靜態內容提供服務。目前我們只需要知道API gateway可以只緣網路限速功能就足夠了。  在設計網路限速器時，一定要先問自己究竟要在伺服端還是在API閘道器實作限速器？這個問題沒有絕對的答案。答案取決你的公司目前現有的技術、工程資源、優先順序、目標等等。  以下是一些通用原則：   評估目前現有的技術，例如擅長的程式語言、現有的快取服務等。\n請務必確認目前使用的程式語言是否可以在伺服端有效時做出網路限速器  確認符合業務需求的網路限速演算法。如果你是在伺服端實作所有的功能，就可以完全控制所有使用的演算法。但如果使用的是第三方API gateway，選則上可能就會受到一些限制  如果已經才用微服務架構，而且在設計中也有用到API gateway來執行身份驗證、IP白名單等功能，那麼就可以直接在API gateway添加網路限速功能。  打造自己的網路限速服務，需要花費一些時間。如果並沒有足夠的工程資源來實作網路限速器，也許採用商業化的API gateway是一個更好的選擇。  重點：   限速器有分客戶端與伺服端  客戶端與服務端中間還有一種元件叫做API gateway的元件可以使用或實作  沒時間或工程資源不足就買商業化的API gateway。  網路限速演算法  網路陷入可以用不同的演算法進行實作，而且每一種演算法都有各自的優缺點。本章並不打算專注於演算法，而是從比較高階的角度去理解這些演算法，這樣也有助於我們選擇更符合使用狀況的正確演算法或演算法組合。以下幾種比較受歡迎的演算法列表：   Token桶 (Token bucket)  Token桶演算法在網路限速方面受到很廣泛的應用。許多網路公司都是採用這種做法，Amazon與Stripe都是使用這種演算法來限制API請求。  Token桶演算法的工作原理如下:   Token桶指的是先定義好一個容器的容量。Token會以定期的方式、以預設的速度放入桶中。當桶子裝滿後，就不能再繼續放入Token中了。    Token桶的容量為4。重新填入器(Refiller)會以每秒的速度把Token放入桶中。桶子一但裝滿，而外的Token就會滿出來(overflow)。  每個請求都會消耗掉一個Token。每出現一個請求時，我們都會檢查桶子裡有沒有足夠的Token。     如果有足夠的Token，我們就會取出一個Token給每一個請求，然後讓該請求通過。  如果沒有足夠的Token，該請求就會被丟棄。  Token填充消費以及限速邏輯過程    Token桶的大小為4，重新填入的速度為每分鐘4個。  Token桶演算法會用到兩個參數：   桶子的大小：桶子裡可以放入的最大Token數量  重新填入的速度：定期放入桶中的Token數量  我們需要多少個桶子？這隨狀況而異，主要取決於限速規則。這裡有一些例子：   不同的API端點通常需要使用不同的桶子。舉例來說，如果可以讓使用者  每秒發佈1則貼文 、  每天增加150個朋友 、  每秒對5則貼文按讚 、那麼每個使用者就需要用到三個桶子。  如果我們需要根據IP位址對請求做出限制，那麼每個IP位址都需要一個桶子。  如果系統每秒最多可以接受10,000個請求，那麼所有請求共用一個桶子就是合理的做法。(如果系統的吞吐量已經是明確的，建立單一桶子會比建立數千個獨立桶子更簡單更有效率)  優點：   這個演算法很容易進行實作。  以記憶體的使用來說很有效率。  Token桶可接受短時間內出現流量爆炸的情況。只要桶子裡還有Token，就可以通過請求。  缺點：   這個演算法有兩個參數，分別是桶子的大小與Token重新填入的速度。要對這兩個參數做出適當的調整，可能蠻有挑戰性。  漏水桶 (Leaking bucket)  漏水桶演算法與Token桶的做法很類似，其不同之處在於請求是以固定的速度來進行處理。在進行實作時，通常是採用先進先出(FIFO)的佇列。這個演算法的工作原理如下：   每出現一個請求時系統會檢查佇列是否已滿。如果未滿，就把請求添加到queue中  如果queue已滿，這個請求就會被丟棄。  請求會從佇列被拉出來，然後以固定的間隔時間進行處理。    漏水桶演算法會採用以下兩個參數：   桶子大小：也就等於是queue大小。這個queue會把請求保存起來，然後以固定的速度進行處理。  流出的速度：定義的是固定時間間隔(通常是每秒)內處理請求的數量。  Spotify這家電子商務公司就是使用漏水桶演算法作為網路限速的機制。  優點：   queue大小是有限的，因此提高記憶體的使用效率。  請求是以固定的速度進行處理，因此很適合需要穩定流出的速度(outflow rate)的使用狀況。  缺點：   如果出現瞬間大量的流量，舊請求就會塞滿queue，此時若未能及時進行處理，新請求的處理速度就會收到影響。  這個演算法有兩個參數。想對這兩個參數做出恰當的調整，也許並沒有那麼容易。  固定視窗計數器 (Fixed window counter)  工作原理：   這個演算法會把時間軸劃分成固定大小的時間視窗，然後指定一個計數器給每個視窗使用。  每個請求都會讓計數器加一。  一但計數器達到預先定義的門檻值，新的請求就會被丟棄，直到下一次新的時間視窗開始，計數器歸零之後才能重新接受新的請求。    時間單位為1s，系統能接受的速度設定為3 req/s。在每秒的視窗中，如果收到3個以上的請求，而外的請求就會被丟棄。  這個演算法主要的問題是，時間視窗如果突然出現爆多的流量，實際上被接受的請求就有可能超過雲本設定的限制數量。請考慮以下情況：    系統可接受5 req/min請求，在切換到下一分鐘時就會重新設定，在2:00:00~2:01:00之間有5個請求。但如果觀察2:00:30到2:01:30這一分鐘的視窗，會發現一共通過了10個請求也就是已經到達可接受請求的兩倍。  優點：   以記憶體的使用來說很有效率。  容易理解。  在單位時間視窗結束時，都會重設可用的配額，這種做法對於某些使用狀況來說特別合適。  缺點：   如果視窗切換時流量激增，就有可能導致系統通過的請求數超過允許的配額。  滑動視窗日誌記錄 (Sliding window log)  因為固定視窗計數器演算法存在一個主要的問題：有可能在視窗切換的前後，接受過多的請求。滑動視窗日誌記錄演算法解決了此問題。  工作原理：   這個演算法會追蹤請求的時間戳。時間戳資料通常保存在快取(例如Redis的以排序集合)。  如果有新請求進來，就刪除所有過時的時間戳。所謂過時的時間戳，就是比目前時間視窗的開始時間更早的時間戳。  把新請求的時間戳添加到日誌中。  如果日誌記錄的數量等於或小於可接受的數量，就接受請求。否則請求就會被拒絕。    系統能接受2 req/min。通常日誌記錄所儲存的都是UnixTimestamp。   新請求在1:00:01抵達時，日誌是空的。因此請求被接受。  1:00:30 又來了一個請求，因此1:00:30這個時間戳被添加到日誌中。加入這個時間戳後，日誌記錄數量就變為2，還沒有超過可接受數量。因此請求同樣的被接受了。  1:00:50 又來了一個請求，這個時間戳同樣的被加入日誌中。加入這個時間戳之後，日誌記錄的數量就變成3了，超過可接受數量2。因此雖然時間戳還是繼續保留在日誌中，但這個請求會被拒絕。  1:01:40 又來了一個新的請求。從1:00:40到1:01:40這段時間內的請求，都還落在最後一分鐘的範圍內，但1:00:40之前的請求，全部都已經過時了，因此就會從日誌中被移除。移除操作完成之後，日誌記錄的數量就會變回2，於是最新的請求也被接受了。  優點：   用這個演算法實作出來的限速效果非常準確。在任何滾動的時間視窗內，請求的數量都不會超出限制。  缺點：   這個演算法會用到大量記憶體，因為即使請求被拒絕，時間戳記還是會被保存在記憶體中。  滑動視窗計數器 (Sliding window counter)  滑動視窗計數器演算法是一種融合”固定視窗計數器”與“滑動視窗日誌記錄”演算法的混合做法。這個演算法可以透過兩種不同的方式來進行實作。     書上寫7 github寫5  假設網路限速器每分鐘最多可接受 7 req/min。  假設這個限速器前一分鐘已經有5個請求，目前這一分鐘有3個請求。對於目前這一分鐘前30%位置出現的新請求來說，我們可以用以下公式來計算出滾動的一分鐘視窗內請求的數量：   目前視窗內請求的數量＋前一個視窗內請求數量＊滾動視窗與前一個視窗重疊的百分比。  使用此公式，我們就可以得到3+5*0.7%=6.5個請求。根據實際的使用狀況，數字可以選擇無條件進入。採用無條件捨去做法得出結果等於6。  由於網路限速器每分鐘最多可以接受7個請求，因此目前這個請求還可以被接受。不過如果在收到另一個請求，就會達到限制數量。  優點：   由於我們是根據前一個視窗平均速度來計算出餐考值，因此他可以減緩流量突然出現高峰的問題。  以記憶體的使用來說很有效率。  缺點：   這種做法只適用於較寬鬆的回溯視窗。實際上這種做法採用的是實際速度的近似值，因為這裡假設前一個視窗裡的請求，在時間上是均勻分佈。不過，在這個問題或許並不如想像中嚴重。根據cloudflare所做的實驗，在4億次請求中，只有0.003%的請求被錯誤地接受或拒絕。  高階架構  網路限速算法的基本架構想法其實很簡單。從比較高階的角度來說，我們需要一個計數器來追蹤同一個使用者、同一個IP位址等等所發送的請求數量。如果數量超過了限制，就不接受該請求。  我們應該要把計數器放哪裡？由於磁碟的儲存速度緩慢，因此使用者資料庫並不是好方法。通常會選擇以記憶體快取主要是因為速度快且支援時間過期，以Redis就是實作網路限速器的常見選擇。Redis把資料保存在記憶體的儲存系統提供兩個指令：INCR與EXPIRE。   INCR：把已儲存計數器值+1  EXPIRE：設定計數器的到期時間。如果超過到期時間，計數器就會自動被刪除。    實作網路限速器的高階架構工作原理：   客戶端向網路限速器發送請求  網路限速器會從Redis相應的桶子裡取的計數器的值，檢查看看有沒有超出限制。\n   如果超出限制，請求就會被拒絕。  如果未超出限制，就把請求發送到API伺服器。於此同時，系統會把計數器的值+1，然後把值存回Redis  第三步驟—深入設計  第二步驟的高階架構設計並沒有回答以下幾個問題：   如何建立網路限速器規則？規則要保存在哪裡？  因限速而被拒絕的請求，該做什麼樣的處置？  在本節會先回答關於限速規則的問題，然後再介紹如何處理被拒絕請求的策略。最後，我們會討論分散式環境下網路限速的做法、詳細的設計、效能最佳化與監控等主題。  限速規則  Lyft公開了他們的限速相關程式碼。因此我們可以窺探其內部程式碼，而且可以從中看到一些限速規則：     domain  :   messaging\n   descriptors  :\n     -   key  :   message_type\n       Value  :   marketing\n       rate_limit  :\n         unit  :   day\n         requests_per_unit  :   5\n  在上述例子中，系統貝貝摯為每天最多允許5則市場銷售相關訊息。這裡還有另一個範例：     domain  :   messaging\n   descriptors  :\n     -   key  :   message_type\n       Value  :   marketing\n       rate_limit  :\n         unit  :   day\n         requests_per_unit  :   5\n  這個規則的意思是，客戶端登入的次數一分鐘內不能超過5次。這些規則通常會被寫入設定檔案，然後保存至硬碟中。  超出速度限制  如果請求次數超出限制，API就會向客戶端送回HTTP回應碼429(過多請求)。根據不同的使用狀況，我們可能會把這些超出速度限制的請求加入queue中，以便稍後再處理。舉例來說，如果有某些訂單因為系統超載而被限速規則擋下來，我們可能還是要先把這個訂單保留起來，等到隨後在進行處理。  網路限速器回應的標頭   客戶怎麼知道它有沒有受到限制？客戶怎麼知道還能做出多少次請求，才會受到限制？答案就在HTTP回應的標頭內。網路限速器會把如下的HTTP標頭送回給客戶端：   X-Ratelimit-Remaining：目前的視窗內，還可接受多少次請求。  X-Ratelimit-Limit：客戶端在每個時間視窗內可進行多少次請求。  X-Ratelimit-Retry-After：接下來還要等待幾秒鐘，才能解除限制、再次發送出請求。  如果使用這發送出太多請求，就會有429過多請求的錯誤與X-Ratelimit-Retry-After標頭被送回給客戶。  詳細的設計    整個系統詳細設計圖   限速規則儲存在硬碟中。Worker工作程序會定期從硬碟取出規則，然後將這些規則儲存到快取中(Cached rules)  客戶端向伺服器端發送請求時，這個請求會先被發送到網路限速器。  網路限速器會從快取載入規則。然後他會從Redis快取取的計數器的值，以及前一次請求的時間戳。網路限速器可以根據回應做出以下的判斷：\n   如果請求並沒有受到速度的限制，就會被轉送到API伺服器。  如果請求受到了速度的限制，網路限速器就會把429過多請求的錯誤送回客戶端。於此同時，這個請求也會被丟棄(dropped)，或是轉送到一個queue。  分散式環境下的網路限速器  在單一伺服器的環境下，建立網路限速器並不困難。但如果想要擴展系統，支援多個伺服器與平行的執行緒，那就是另外一回事了。這裡會有兩個挑戰：   競爭狀況(Race-condition)  同步問題(Synchronization issue)  競爭狀況   如前所述，若從高階角度來看，網路限速器的運作方式如下：   從Radis讀取計數器的值。  檢查(計數器+1)有沒有超過門檻值。  如果沒有，就把Redis裡的計數值+1  在高度平行的環境下，就有可能發生競爭狀況。    假設Redis裡的計數器數值為3。如果有兩個請求都在寫回計數器的值之前讀取了計數器的值，隨後兩個請求都會把計數器的值+1，然後把值寫回計數器，這兩個請求都不會知道還有另一個執行緒的存在。這兩個請求(執行緒)都會認為，他們擁有正確的計數器值4。不過，其實正確的計數器值應該是5才對。  鎖定機制(Lock)是解決狀況最明顯的一種做法。不過，鎖定機制會大大降低系統的速度。另外還有兩種常用的策略，可以用來解決此問題：  Lua腳本與Redis以排序集合資料結構。  同步問題   在分散式環境下，同步是另一個必須考慮的重要因素。如果要支援好幾百萬的使用者，單獨一個網路限速伺服器恐怕不足以處理所有的流量。如果使用多個網路限速伺服器，則會有同步的問題。    客戶端#1會把請求發送到網路限速器#1，客戶端#2則會把請求發送到限速器#2，由於Web層是無狀態的(stateless)，因此客戶端也有可能把請求發送到另一個不同的網路限速器，如圖右側。如果沒有進行同步處理，網路限速器#1可能就不會有任何關於客戶端#2的資料。如此一來，網路限速器就無法正常運作了。  其中一種可能解決方案，就是使用所謂的黏性session(sticky session)，讓客戶端自動把流量發送到同一個網路限速器。不過我們建議不要使用這種解決方式，因為這種做法既無法擴展、也不夠靈活。更好的做法應該是使用Redis這類的集中式資料儲存系統。     1.  用戶端（Client）發出請求\n2.  請求會被分配到其中一個 Rate Limiter 節點（例如 #1 或 #2）\n3.  該節點不會自己保有限速狀態，而是：\n•   從 Redis 查詢該用戶的目前限速資料（例如當前的請求次數）\n•   執行判斷：是否超過限速門檻\n•   若未超過，則使用 Redis 的原子操作遞增請求計數並放行請求\n•   若已超過，則拒絕請求（可回應 429 Too Many Requests）\n   在 Figure 4-16 架構中，無論請求經由哪一台 Rate Limiter 節點處理，所有限速器都會先從 Redis 中讀取該用戶的限速狀態，並透過 Redis 的原子操作確保一致性。這種設計讓系統能在分散式架構下仍然維持同步，避免每個節點各自為政導致限速失效。  比起讓節點之間溝通，這種方式更簡單、更一致、也更容易擴展。  效能最佳化  效能最佳化是系統設計面試很常見的主題。我們所要討論的改進方式，會涵蓋兩個方面。  第一，多資料中心的配置方式對於網路限速器來說非常重要，因為遠離資料中心的使用者一定為面臨高延遲的問題。大多數的雲端服務供應商，都會在世界各地建立許多邊緣(edge)伺服器。舉例來說，截至2020年5月20日止，Cloudflare擁有194個邊緣伺服器，分別分佈在地理上各個不同的地區。流量會自動被轉送到距離最近的邊緣伺服器，以降低延遲狀況。    第二，可運用『終究一致性』(eventual consistency)模型來同步資料。如果你不大了解什麼是終究一致性模型，請參”見第6章：設計鍵值儲存系統”其中討論一致性的章節內容。  在之前的codesence設計資料密集應用中第5章與第9章也有提到eventual consistency當時的翻譯為最終一致性。看完本書第六章後發現資料密集應用中的最終一致性解釋的較詳細。   設計資料密集型應用  9.1 一致性保證  大多數複製的資料庫至少提供了最終一致性，這意味著如果你停止向資料庫寫入資料並等待一段不確定的時間，那麼最終所有的讀取請求都會返回相同的值。換句話說，不一致性是暫時的，最終會自行解決（假設網路中的任何故障最終都會被修復。  對於應用開發人員而言，最終一致性是很困難的，因為它與普通單執行緒程式中變數的行為有很大區別。如果將一個值賦給一個變數，然後很快地再次讀取，不可能讀到舊的值，或者讀取失敗。資料庫表面上看起來像一個你可以讀寫的變數，但實際上它有更複雜的語義。  我們在使用弱一致性的資料庫時必須了解弱一致性的局限性,因為錯誤常常很難找到也很難測試，因為應用可能在大多數情況下執行良好。當系統出現故障（例如網路中斷）或高併發時，最終一致性的邊緣情況才會顯現出來。      強一致性  弱一致性    解釋  強一至性表示，一旦一個操作（如寫入）完成，該操作的效果會立即反映在接下來所有的讀取操作上，無論這些讀取操作是從哪個節點發出的。  在弱一至性模型下，寫入操作的效果可能不會立即反映在隨後的讀取操作中，特別是來自其他節點的讀取操作。   例子  假設你在銀行轉帳，當錢從一個帳戶轉到另一個帳戶後，不管是你自己還是其他任何人，只要查詢餘額，都會立即看到最新的數字。  假設你更新了你的社交媒體狀態。你的朋友可能會在幾秒鐘後或幾分鐘後才看到這個更新，取決於各種因素（如網絡延遲、緩存等）。   模型   線性一致性(linearizability)最強   最終一致性(Eventual Consistency)   容錯性  少  高   所需資源  高  低   實踐  易  難  監控  網路限速器安排就緒後，彙整分析資料的工作也很重要，因為這樣才能檢查網路限速器是否以很有效率的方式正常運作。我們主要想去認的是：   網路限速演算法是否很有效率。  網路限速規則是否很有效率。  舉例來說，如果網路限速規則太嚴格，就會丟棄掉許多正確的請求。在這樣的情況下，我們就會想稍微放寬規則。我們還要注意另一種情況，例如流量突然增加時(比如限時搶購活動)，網路限速器可能也會失敗。在這樣的情況下，我們可以換一種演算法，以應付這種突發的流量。Token桶演算法就很適合這樣的狀況。  第四步驟—彙整總結  本章討論了幾種不同的網路限速演算法及其優缺點。   Token桶  漏水桶  固定視窗計數器  滑動視窗日誌記錄  滑動視窗計數器  然後，我們也討論了分散式環境下網路限速器的系統架構、效能最佳化與監控的做法。不管是哪一種系統設計面試問題都一樣，如果時間允許的話，你都可以提出一些其他想法：   網路限速器的硬性限制vs.軟性限制   硬性限制：請求數量絕不能超過門檻值  軟性限制：請求可以在短時間內超過門檻值  不同等級的限速做法。我們在本章只討論應用層(HTTP:第七層)的限速做法。其實在其他層也可以套用限速做法。舉例來說，你可以利用Iptable針對IP位址(IP:第三層)套用限速的做法。請注意：OSI(Open Systems Interconnection：開放系統互聯)模型共有7層，從第1層到第7層分別是—實體層、資料鏈結層、網路層、傳輸層、會話層、表現層、應用層。    我們也應該盡量避免自己在客戶端這遭受到速度上限制。在設計客戶端時，最佳的實務做法如下：   運用客戶端快取，避免過度頻繁調用API。  了解速度限制，不要在短時間內發送太多請求。  在程式碼中捕捉異常或錯誤狀況，好讓客戶端可以優雅地從異常狀況恢復過來。  添加足夠的退避時間(back off time)，好讓邏輯有機會重新進行嘗試。  實際案例     flowchart LR\n       A[Client 發出 HTTP Request] --> B[進入 Router]\n       B --> C[log Middleware（前置處理）]\n   \n       C --> PCM\n       subgraph PCM[concurrency Middleware （前置處理）]\n           direction TB\n           D1{Semaphore 有空位？}\n           D1 -- 是 --> D2[取得 Semaphore Token]\n           D1 -- 否 --> D3{Queue 有空位？}\n           D3 -- 是 --> D4[請求加入 Queue]\n           D4 --> D5[等待 notify / timeout]\n           D5 --> D6{排隊成功？}\n           D6 -- 是 --> D2\n           D6 -- 否 --> D7[返回 504 Gateway Timeout]\n           D3 -- 否 --> D8[返回 429 Too Many Requests]\n       end\n   \n       D2 --> F[Handler（主邏輯處理）]\n   \n           \n       F --> G1\n       subgraph CM1[concurrency Middleware （後置處理）]\n           direction TB\n               G1[釋放 Semaphore Token]\n           end\n       G1 --> G[log Middleware（後置處理）]\n       G --> H[回傳 HTTP Response 給 Client]\n  目標：週一維護時會有8小時的洗牌轉檔總共有32台主機會使用這個api、平時客戶端歷史調閱轉檔、自動地端影片救援  問題：轉檔期間cpu:99%持續超過3分鐘，且還要考慮再不增加node的情況將預算保持比使用gcp vm相當。  解法：使用middleware限速器限制api轉檔數量讓cpu保持在70%。   code     // streamConverterAPI/internal/middleware/concurrency_middleware.go\n   package   middleware\n   \n   import   (\n       \"  api/internal/model/response/common  \"\n       \"  api/internal/repository/logger  \"\n       \"  context  \"\n       \"  github.com/cockroachdb/errors  \"\n       \"  log  \"\n       \"  net/http  \"\n       \"  time  \"\n   \n       \"  github.com/gin-gonic/gin  \"\n   )\n   // RequestQueueItem 代表被排進佇列等待處理的請求，\n   // 使用 notify channel 來通知排到該請求可以開始執行。\n   type   RequestQueueItem   struct   {\n       notify   chan   struct  {}\n   }\n   \n   // ConcurrencyLimiterWithQueue 控制同時處理的請求數，\n   // 超過上限的請求會被放進 queue 中等待。\n   // 適合用來保護後端資源，例如資料庫、外部 API 等。\n   type   ConcurrencyLimiterWithQueue   struct   {\n       semaphore   chan   struct  {}           // 控制並發數量，作用類似限流 token\n       queue       chan   RequestQueueItem   // 請求等待佇列\n       ctx         context  .  Context\n       cancel      context  .  CancelFunc\n       loggers     logger  .  ILoggers\n   }\n   \n   // NewConcurrencyLimiterWithQueue 初始化限流器，並啟動處理 queue 的 goroutine。\n   func   NewConcurrencyLimiterWithQueue  (  loggers   logger  .  ILoggers  ,   maxConcurrency  ,   maxQueue   int  )   *  ConcurrencyLimiterWithQueue   {\n       ctx, cancel   :=   context.  WithCancel  (context.  Background  ())\n       cl   :=   &  ConcurrencyLimiterWithQueue  {\n           semaphore:   make  (  chan   struct  {}, maxConcurrency),\n           queue:       make  (  chan   RequestQueueItem  , maxQueue),\n           ctx:       ctx,\n           cancel:    cancel,\n           loggers:   loggers,\n       }\n   \n       // 背景 goroutine 處理 queue 中排隊的請求\n       go   cl.  processQueue  ()\n   \n       return   cl\n   }\n   \n   // Shutdown 清理資源與關閉 queue（應在服務關閉時呼叫）\n   func   (  cl   *  ConcurrencyLimiterWithQueue  )   Shutdown  () {\n       cl.  cancel  ()\n       close  (cl.queue)\n   }\n   \n   // processQueue 會不斷從 queue 中取出請求，\n   // 等待有空閒資源時發送通知讓請求繼續往下執行。\n   func   (  cl   *  ConcurrencyLimiterWithQueue  )   processQueue  () {\n       for   {\n           select   {\n           case   \u003C-  cl.ctx.  Done  ():\n               return\n           case   item, ok   :=   \u003C-  cl.queue:\n               if   !  ok {\n                   return\n               }\n               // 取得一個可用位元（token）\n               cl.semaphore   \u003C-   struct  {}{}\n               // 通知該請求可以執行\n               close  (item.notify)\n               cl.loggers.  Info  (  \"Request dequeued and allowed\"  ,   nil  )\n           }\n       }\n   }\n   \n   // Handle 是真正綁到 Gin 的 middleware function，\n   // 當併發達到上限時，請求會被送進 queue 等待，\n   // 若 queue 也滿了，就直接回應 429 Too Many Requests。\n   func   (  cl   *  ConcurrencyLimiterWithQueue  )   Handle  ()   gin  .  HandlerFunc   {\n       return   func  (  c   *  gin  .  Context  ) {\n           select   {\n           // 有空位直接執行\n           case   cl.semaphore   \u003C-   struct  {}{}:\n               cl.loggers.  Info  (  \"Request allowed\"  , c.Request.Body)\n               defer   func  () {\n                   \u003C-  cl.semaphore\n                   cl.loggers.  Info  (  \"Request completed and semaphore released\"  , c.Request.Body)\n               }()\n               c.  Next  ()\n               return\n   \n           // 沒有空位，放進 queue 嘗試排隊\n           default  :\n               notify   :=   make  (  chan   struct  {})\n               select   {\n               case   cl.queue   \u003C-   RequestQueueItem{notify: notify}:\n                   cl.loggers.  Info  (  \"Request enqueued\"  , c.Request.Body)\n   \n                   select   {\n                   // 被通知排到，繼續處理請求\n                   case   \u003C-  notify:\n                       cl.loggers.  Info  (  \"Request dequeued\"  , c.Request.Body)\n                       defer   func  () {\n                           \u003C-  cl.semaphore\n                           cl.loggers.  Info  (  \"Request completed and semaphore released\"  , c.Request.Body)\n                       }()\n                       c.  Next  ()\n   \n                   // 排隊超過時間，自動超時處理\n                   case   \u003C-  time.  After  (  60   *   time.Minute):\n                       log.  Println  (  \"Request timed out while waiting in queue\"  )\n                       c.  JSON  (http.StatusGatewayTimeout,   gin  .  H  {\n                           \"data\"  :   \"\"  ,\n                           \"status\"  :   gin  .  H  {\n                               \"code\"  :      \"70002\"  ,\n                               \"message\"  :   \"Request timed out\"  ,\n                           },\n                       })\n                       c.  Abort  ()\n                       return\n                   }\n   \n               // queue 也塞滿了，回應 429\n               default  :\n                   cl.loggers.  Warn  (  \"Request blocked: queue full\"  , c.Request.Body)\n                   c.  JSON  (http.StatusTooManyRequests, common.  NewResponseErr  (\n                       errors.  New  (  \"Too many requests\"  ),\n                       \"Request blocked: queue full\"  ,\n                       common.ErrTooManyConcurrency,\n                   ))\n                   c.  Abort  ()\n                   return\n               }\n           }\n       }\n   }\n  html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"id":664,"path":665,"dir":632,"title":666,"description":667,"keywords":668,"body":689},"content:7.sdi-aig:6.chapter6.md","/sdi-aig/chapter6","06 設計鍵值儲存系統","ref: CHAPTER 06：DESIGN A KEY-VALUE STORE.md",[669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688],"整體章節重點 (Bird‑Eye View)","本章要求","Session 1 ： 從零開始認識 Key‑Value Store","Session 2 ： 單機設計 v.s. 分散式挑戰","(1) 單一伺服器的鍵值儲存系統：","(2) 分散式鍵值儲存系統：","Session 3 ： 鍵值儲存系統的構成元素","資料分區：一致性雜湊與水平擴充","資料複製：高可用性的基石","一致性：Quorum 機制與最終一致性","不一致問題的解決方式：版本控制 ( Versioning ) 與向量時鐘 ( Vector Clock )","節點故障與容錯機制：Gossip 協議、Sloppy Quorum 與 Hinted Handoff","📌 回顧一下","📌 最後關注系統的實際架構實作","系統架構圖：整體架構與資料存儲引擎","寫入途徑：資料寫入路徑（Write Path）","讀取途徑：資料讀取路徑（Read Path）","Session 3 各個組件環環相扣關係的架構圖：","環環相扣的特點：","以下額外參考","  ref:   CHAPTER 06：DESIGN A KEY-VALUE STORE.md   ref:   https://tw.annas-archive.org/md5/e89be9442169fe8f89a2fda597797668  ref:   https://www.youtube.com/watch?v=8TE2DvpKxvA  ref:   https://youtu.be/o6q8zPmfVLU?si=M7Y-FWqM-tuORxn7    以 C# /.NET 8 與 MSSQL 為主要範例語言／平台，分 2 – 3 次（每次 40‑50 分鐘）循序漸進講解本章重點，並輔以可執行的程式碼片段與示意圖。  整體章節重點 (Bird‑Eye View)    Key‑Value Store 基礎 ：資料模型、操作介面 (  put  /   get )。   擴充挑戰 ：從單機記憶體雜湊表 → 分散式雜湊表 (DHT)。   CAP 與一致性 ：AP / CP 系統選擇、N / W / R 仲裁參數。   資料分區與複本 ：一致性雜湊、跨資料中心複寫。   衝突處理 ：Vector Clock、最終一致性。   容錯機制 ：Gossip 偵錯、Sloppy Quorum、Hinted Handoff、Anti‑Entropy & Merkle Tree。   讀寫流程 ：Commit Log → MemTable → SSTable；MemTable / Bloom Filter 多層快取。  本章要求   設計出一個可支援以下操作的鍵值儲存系統：       -   put  (key, value)   // 插入一組與「key」鍵相關聯的「vlaue」值\n     -   get  (key)   // 取出與「key」鍵相關聯的「vlaue」值\n   瞭解問題並確立設計的範圍   天底下沒有完美的設計。  \n每一種設計都必須在記憶體讀取、寫入與使用方面進行取捨，以達到某種特定的平衡。  \n在一致性與可用性之間，也需要進行權衡取捨。    設計的範圍設定：\n   鍵值對的尺寸很小，小於 10 KB。  有能力儲存大數據資料。  高可用性：即使發生故障，系統也可以快速回應。  高擴展性：系統可進行擴展，以支援大型資料集。  自動擴展：應該可以根據流量，自動添加/移除伺服器。  可調整系統一致性的程度。  低延遲。   Session 1 ： 從零開始認識 Key‑Value Store   鍵值資料庫（Key-Value Store，也稱鍵值存儲）是一種  非關聯式資料庫 ，以   「鍵-值對」  形式儲存資料。  每筆資料由   唯一的鍵（key）  識別並對應一個   值（value） ，類似 Dictionary 或 Hash Table 的概念。   鍵：  通常為短字串或其雜湊，須具唯一性以快速索引；   值：  可以是任意類型的二進位資料，系統通常將值視為不透明的blob（例如 Redis、Memcached 即採用此模型）。   主流產品 (Redis、DynamoDB…)     Session 2 ： 單機設計 v.s. 分散式挑戰   雜湊表 + 壓縮 + 冷資料落盤  何時需要走向分散式 → 大容量、高可用  (1) 單一伺服器的鍵值儲存系統：    簡單實作：   最直接的做法就是把「鍵值對」全部放到記憶體。  雜湊表（Hash Table） 在記憶體中保存所有鍵值對。  基本操作包括 put(key, value)（寫入/更新鍵的值）與 get(key)（讀取鍵的值）。   資料結構：   使用如 C# 的 Dictionary\u003Cstring, string> 即可達成 O(1) 的鍵查詢時間。   記憶體 vs 磁碟：   若資料量可能超過記憶體，可透過LRU快取策略將常用資料留在記憶體，不常用資料寫入磁碟檔案；  也可採用資料壓縮減少空間。   容量侷限：   即使有上述優化，單台伺服器的記憶體與磁碟總有上限，大規模資料最終會超出單機容量。   此時需要水平擴充（scale out）至分散式鍵值存儲架構。   (2) 分散式鍵值儲存系統：   分散式鍵值存儲（Distributed Key-Value Store）又稱分散式雜湊表（DHT），透過多部伺服器共同存放整體資料集。  \n設計分散式系統時，理解 CAP 定理 極其重要。   CAP 定理簡介   CAP 定理，又稱 Brewer 定理，由 Eric Brewer 於 2000 年提出，2002 年經 Seth Gilbert 與 Nancy Lynch 形式化證明。   定理指出：在一個分散式資料存儲系統中，只能在以下三者中最多同時滿足兩項，無法兼得三者。  (  腾讯云 )\n   一致性 ( Consistency )、  可用性 ( Availability )、  分區容錯性 ( Partition Tolerance )  三大屬性定義    一致性 ( Consistency, C )：   保證每次讀取都能拿到最新的寫入值，或者返回錯誤，  也就是「所有節點在同一時間看到相同資料」的強一致性承諾。(  腾讯云 )   可用性 ( Availability, A )：   系統對每次請求都能在有限時間內做出響應（非錯誤或超時）。  即便部分節點失效，健康節點依然對外提供服務。(  腾讯云 )   分區容錯性 ( Partition Tolerance, P )：   當系統節點間發生網路分區（訊息丟失或延遲）時，仍能繼續對外提供服務。  由於大規模分散式系統中網路分區幾乎不可避免，P 通常被視為必須保障的屬性。(  腾讯云 )  屬性兩兩組合與典型示例      CA（一致性 + 可用性，放棄 P）   適用於無須考慮網路分區容錯的場景，  例如：  單機或單資料中心部署 的關係型資料庫系統（如 SQL Server 等）。(  維基百科 )   CP（一致性 + 分區容錯，放棄 A）   在發生分區容錯時  優先保證強一致性，犧牲可用性。  例如：MongoDB，在分區容錯期間會  拒絕服務以確保一致性。 (  Stack Overflow )   AP（可用性 + 分區容錯，放棄 C）   在發生分區容錯時  優先保證可用性   允許短暫不一致，並透過最終一致性（Reconciliation）進行修復。  例如：Apache Cassandra，即使分區容錯期間也繼續響應請求，  可能讀到舊值。  (  Stack Overflow )  理想情況   \n在理想情況下，永遠不會發生網路分區容錯的問題：   寫入 n1 的資料自動就會複製到 n2、n3。  如此可達到一致性與可用性。   現實世界的分散式系統   \n在真實環境中，網路分區容錯的發生是不可避免的：   節點   n3  因故離線，無法與   n1 、  n2  溝通；  若客戶端將資料寫入   n1 ／  n2 ，則資料無法傳遞到   n3 ；  若寫入   n3  卻未傳至   n1 ／  n2 ，則後者只能保有舊版資料。    CP 系統（強一致性優先）   發生分區時，拒絕或延後對   n1 、  n2  的寫入，確保所有可用節點資料一致，犧牲可用性──  \n銀行系統即屬此類，必須保證餘額一定為最新狀態。   AP 系統（可用性優先）   分區期間仍接受讀寫，可能返回舊資料；待網路恢復後再同步至   n3 ，最終達成一致。  設計取捨與面試要點 - CAP    為何必須取捨？   分散式系統中網路分區（P）不可避免，  當發生分區時，系統無法同時保證既讀取最新值(C)、又對所有請求都做出響應(A)。   必須在一致性與可用性間做出取捨。 (  ruanyifeng.com )   如何依場景選擇？    金融支付 ：對資料一致性要求極高，常選 CP ，確保交易正確無誤。(  IBM )   社交推薦、日誌收集 ：對可用性和吞吐量要求優先，常選 AP 。(  IBM )   內部管理系統 ：網路分區風險較低，可選 CA 以簡化設計。   面試常見考察點   正確認識 C/A/P 三者含義及差異。  針對不同業務場景，說明為何在 C/A/P 間做出具體取捨。  以具體系統（如 Key-Value 儲存）為例，闡述如何實現 CAP（Quorum、Hinted Handoff、Anti-Entropy 等機制）。  透過以上整理，您可在系統設計面試中清晰闡述 CAP 定理，並結合業務需求給出合理架構選擇。    Session 3 ： 鍵值儲存系統的構成元素   我們打算在本節討論以下這些核心元素與技術，與建構出相應的鍵值儲存系統。   \n以下內容主要是以三個很受歡迎的鍵值儲存系統為基礎： Dynamo、Cassandra 與 BigTable   資料分區 ( data partition )  資料複製 ( data replication )  一致性  不一致問題的解決方式  故障的處理方式  系統架構圖  寫入途徑  讀取途徑  資料分區：一致性雜湊與水平擴充   當資料規模龐大時，將全部鍵值對存於單一節點(伺服器)，是一種不可行的做法。    資料分區（data partition）：  將資料拆分到多個節點(伺服器)存放。   資料分區面臨兩大挑戰：    資料均勻分布：  確保各節點負載均衡，避免有的節點存了過多資料、成為瓶頸。   動態伸縮：  節點數量增加或減少時，儘量減少資料在節點間搬移的成本。   為解決上述問題   一致性雜湊（Consistent Hashing）是一種經典技術。   一致性雜湊：將整個雜湊值空間組織成一個雜湊環（Hash Ring）然後將節點和資料鍵都映射到此環上。     首先，伺服器會被放到雜湊環上。在圖 6-4 中， s0、s1、....、s7 所代表的8部伺服器全都被放到雜湊環上。  接著，有一個鍵也進行了雜湊運算，而被放到了同一個雜湊環上。   存放規則：   從資料鍵映射點   順時針  遇到的第一個節點，即為此鍵的負責節點。  key0 運用存放規則的邏輯，最後保存到 s1 之中。   一致性雜湊的優點：    自動水平擴充：   可平滑地新增/移除節點而無需重新分配全部鍵。  加入節點，只需重新分配  鄰近區域 的一部分鍵；  移除節點，其負載自動攤分給鄰近節點。這滿足動態擴縮需求。   最小資料搬移：   新增或移除節點時，只需移動環上極少量鍵，不影響其它區段的資料。   負載均衡 ( 可因應 不均勻性/異質性 的問題 )：   結合  虛擬節點 機制，能讓節點根據性能配置不同數量的虛擬節點，避免因節點性能差異導致負載不均（容量越大的節點可在環上放置更多虛擬節點，獲取較大區間的鍵）。  例： 如果某個伺服器的容量比較大，只要指定比較多的虛擬節點給它就可以了。   資料複製：高可用性的基石   為了達到高可用性與可靠性 ( relibility )  為提升系統  容錯性 與  高可用性 ，分散式鍵值存儲通常將資料   複製（Replication）  至多個節點。  也就是對每個鍵值對保存多份拷貝，放置於  不同節點 ，當部分節點故障時仍能由其他副本提供服務。  常用的複製策略如下：   key0 會被複製到 s1 、 s2 、 s3 ( N=3 )      複製因子 N：就是儲存 N 份   每筆資料儲存 N 份（包含原始1份+ N-1份副本）  例如： N=3，此時系統至少需有 N 個節點才能容納所有副本。   副本選擇：必須確保節點不重複   Key0 先按上述一致性雜湊的方式找到  主存節點  再沿雜湊環  順時針方向選擇接續的 N-1 個 獨立節點作為副本存放處。\n   例如 N=3 時，  key0 可存於 s1（主）以及後續的 s2、s3 節點上，共三副本。  若遇到  虛擬節點映射同一實體節點的情況，則跳過重複實體以確保副本分散在不同實體節點。   跨機房分佈：提高可靠性   為防同機房故障導致副本同時損失，通常會將 N 個副本分布在  多個資料中心 。  例如：3個副本可放在三個不同機房，透過高速網路互聯同步。  透過適當的複製策略，可以實現讀取的高可用：  即使一兩個節點故障，資料的其他副本仍可提供服務。同時多副本也提高了資料耐久性（不易因單點損毀而永久丟失）。    註：  複製帶來  一致性維護 的挑戰 —— 當多個副本存在時，如何確保各副本的值一致，是後續「一致性」與「衝突解決」部分要解決的問題。   一致性：Quorum 機制與最終一致性   如果沒有額外措施，不同副本可能出現數據不一致（如有的收到更新，有的因網路問題未收到）。  如前所述，將資料複製到多個節點可以提高可用性，但也帶來  副本間數據同步 的難題。  為管理一致性，可採用   Quorum 共識機制 調節讀寫操作：    定義：    N：  副本的數量 ( 如下圖 6-6，N=3 )。   W：  寫入的最低門檻，寫入操作必須至少有 W 個副本回應確認，才視為寫入成功。   R：  讀取的最低門檻，讀取操作必須等待至少有 R 個副本回應，才視為讀取成功。   運作：  以圖 6-6 為例 N=3 資料被製到 s0、s1、s2。    協調者 ( coordinator ) ：  代表客戶端與節點之間代理者 ( proxy ) 的角色。   W=1：  每次  寫入 ，協調者  必須至少收到一個 ACK 確認 ，才能回覆客戶端成功。   不代表資料只寫入  1個節點  如果從 s1 收到了確認，就不需要再等待 s0 與 s2 的確認了。   W=2：  每次  寫入 ，協調者  必須至少收到二個 ACK 確認 ，才能回覆客戶端成功。   如果從 s1 收到了確認，要再等待 s0 或 s2 的確認。   R=2：  每次  讀取 ，協調者  會並行查詢至少2個副本，並等待回應，最後取其中最新版本後再返回 ，才能回覆客戶端成功。      延遲 vs 一致性：   W、R 的選擇代表延遲與一致性的權衡。  W或R越小：操作響應越快，但返回舊數據的風險增大。  W或R越大：數據一致性越好，但需等待更多節點，延遲提高。   強一致性保證的條件：    W=N + R=1 ：  就表示系統針對快速  讀取 進行最佳化。   W=1 + R=N ：  就表示系統針對快速  寫入 進行最佳化。   W + R > N ：  就可以  具有強一致性 。因為任意一筆資料的讀寫集合會在至少一個共同副本相交，確保讀一定能讀到最新寫入。(例如 N=3，W=R=2)   W + R \u003C= N ：  則  不能保證 具有強一致性。  設計取捨與面試要點 - 一致性模型  在設計鍵值儲存系統時，一致性模型確實是一個需要考慮的重要因素。  一致性模型定義的是資料一致性程度，而實際上確實存在各式各樣不同的一致性模型：    強一致性：  任何讀取操作所送回的值，都對應到最新寫入資料項的結果。  客戶端絕不會看到過時的資料。   弱一致性：  後續的讀取操作有可能看到的並不是最新的值。  客戶端可能不是最新的資料。   最終一致性 ( eventual consistency )：    弱一致性的殊殊形式 ，只要給定足夠的時間，所有資料更新終究都會被傳遞，且所有副本都會是一致的。  客戶端會現是等待最新資料的過程。    為了高可用性 ，預設採用   最終一致性 ( eventual consistency )  模型，  即 AP 模式：   允許在短暫時間內副本之間數據不一致，但保證最終（在沒有新的更新後經過足夠時間）所有副本趨於一致。  這意味：   系統在寫入時不強制所有副本立即同步成功即可返回成功（如採用 W \u003C N），因此短期內不同節點可能讀到  舊值 。  系統在背景持續同步副本，或透過讀取修復機制，確保  最終 各副本一致。   最終一致性容忍暫時不一致 ，換取故障情況下仍可接受讀/寫請求（提高可用性）。例如 Dynamo、Cassandra 即採此模型。  然而，最終一致性模型下，不可避免會出現  更新衝突 的問題：   當網路分區或並發寫入發生時，不同節點可能各自接受了對同一鍵的不同更新，導致系統存在多個版本的值。接下來我們將介紹如何檢測與解決這些衝突。  不一致問題的解決方式：版本控制 ( Versioning ) 與向量時鐘 ( Vector Clock )   複製：提高可用性，同時也導致副本間產生不一致的問題。  \n因此，我們需要一個能夠偵測衝突與協調衝突的版本控制系統。    版本控制：  把每次的資料修改，全都視為資料的一個全新不可變版本。   向量時鐘：  解決兩個版本衝突此類問題的常用技術。    版本衝突問題舉例：  圖6-7 －＞ 圖6-8   有 2 個副本 n1 與 n2 具有相同 key=\"name\" 並且值相同為 \"john\" ，我們稱為原始值。 ( 圖 6-7 )\n     某刻 Client A 通過節點 n1 將 name 更新為值 \"johnSanFrancisco\"  同時 Client B 通過節點 n2 將 name 更新為值 \"johnNewYork\"  由於 A、B 幾乎  同時寫入 ，且  寫入不同節點 。 2個副本各自被更新為  不同值 。( 圖 6-8 )\n    產生衝突版本：   key=\"name\" 產生兩個衝突值： v1（值=\"johnSanFrancisco\"）與 v2（值=\"johnNewYork\"）。  這種情況下，單靠時間戳無法明確判斷哪個版本為「最新」或「正確」，因為兩次寫入發生在不同節點且近乎同時。   解決衝突版本：    版本控制：    定義：  把每次的資料修改，全都視為資料的一個全新不可變版本。   向量時鐘：   為了解決  誰覆蓋誰 的問題。  為每個資料項維護一個多維版本向量，用來判定版本間的先後或並行關係。   定義：   向量時鐘是  與資料相關聯的一對資料 ，其形式為     節點(伺服器), 版本 。  可用來檢查某個版本是否為  最新版本 、是否為  成功的版本 、或是否  與其他版本衝突 。  如    D  的向量時鐘可能為    D([S1, v1], [S2, v2], ... [Sn, vn])  。    S1...Sn  是參與此資料項更新的節點ID    v1...vn  是該節點對此資料的更新次數。   更新規則：   每當資料    D  在某節點    Si  發生寫入\n   如果向量時鐘中已存在    [Si, vi]  ，則將    vi  加一（該節點對此資料的版本號+1）。  如果不存在    [Si, vi] ，則新增    [Si, 1]  條目，表示該節點首次創建此資料版本。   版本比較：   將兩個版本的向量時鐘逐分量比較  如果   版本X  的每個節點計數都 \u003C=   版本Y  的對應計數    版本X     D(［S0, 1］, [S1, 1])   版本Y     D(［S0, 1］, [S1, 2])  則 X 是 Y 的祖先版本（即 X 的所有更新都被 Y 包含，無衝突）。  如果   版本X  的某幾個節點計數比   版本Y  的對應計數或大或小    版本X     D(［S0, 1］, [S1, 2])   版本Y     D(［S0, 2］, [S1, 1])  則   版本X  與   版本Y  發生  並行衝突 （即各有部分更新彼此不包含）。   目前的應用結論   Amazon Dynamo 就成功在生產環境使用向量時鐘並未遇到不可控的時鐘膨脹問題。  向量時鐘仍是實用解決方案。    向量時鐘的缺點   透過以上機制，向量時鐘可以  避免隨意覆蓋 並發寫入造成的數據丟失，保留所有衝突版本供合併。值得注意的是，向量時鐘也有缺點：    增加客戶端複雜度：   需要客戶端（或應用層）實現衝突合併邏輯，對開發者提出更高要求。  一些系統（如 Cassandra）乾脆選擇更簡單的   最後寫入優先 (Last Write Wins)  策略，以縮減實現複雜度，但代價是可能捨棄較新的並發更新。   開銷隨節點數增加：   向量時鐘裡     server:version   這樣成對資料，其數量可能會  快速成長 。  在大規模系統中，維護數十上百的節點版本向量  會增加存儲和傳輸負擔 。  針對長度設定了一個  門檻值 。超過限制長度，就會  刪除掉最舊的成對資料 。  導致，無法正正確判斷前後代關系，造成重新協調效果不佳。     舉例： \n向量時鐘是用    D([S1, v1], [S2, v2], ... [Sn, vn])  來表式。\n   D   ：是資料項\n   Sn  ：是伺服器編號    vn  ：是版本計數器\n如果把資料項    D  寫入伺服器    Si  , 則系統就必須執行以下其中一個任務。\n   如果     [Si,vi]  存在，    vi  就加 1。  否則的話，就建立一個新的項目    [Si,1]  。  透過向量時鐘，我們能  檢測 衝突版本並進一步  合併 。  以下用實際例子 ( 圖 6-9 ) 說明其工作流程：  假設 N=3 節點 Sx, Sy, Sz     這張流程圖示範整個向量時鐘（Vector Clock）在多節點環境下，如何追蹤版本、偵測衝突並最終合併的機制。 *    具體來看：     D1 → D2 ：  都是透過    Sx  來寫入 ，    Sx  的版本計數器從 1 增到 2 。    D3 ：  有一客戶端在    Sy  寫入 ，繼承了   Sx:2 ，並在    Sy  新增   Sy:1  ( 因為   Sy    第一次處理  ) ，時鐘變成   (Sx:2, Sy:1)  。    D4 ：  另一個客戶端  同時在   Sz  上寫入 ，同樣繼承   Sx:2 ，並新增   Sz:1  ( 因為   Sz    第一次處理  )，時鐘變成   (Sx:2, Sz:1)  。   衝突偵測：  因為   (Sx:2, Sy:1)  跟   (Sx:2, Sz:1)  彼此各有對方沒有的  分量 ，無法互相包含，所以被判定為衝突版本。    D5  合併：  在   應用層  把   D3 、  D4  合併後，  由   Sx  再寫入一次 ，  Sx  的計數器再 +1（從 2 → 3）， 、合併後的時鐘為   (Sx:3, Sy:1, Sz:1)  ，它就   包含了之前所有分量，因此成為唯一合法的後繼版本  。  「  分量 」這個詞在這裡其實是從數學或向量概念延伸而來，指的是  向量中每個維度上的值 ，在向量時鐘（Vector Clock）中，這些「分量」就是  每個節點的版本號 。    🔍 為什麼叫「分量」？  數學背景：   向量（Vector）是一組有方向和大小的數字集合，例如        v  =  (  1  ,  2  ,  3  )  v = (1, 2, 3)     v   =     (  1  ,   2  ,   3  ) 。  向量的每個數字（例如 1、2、3）就是這個向量在各個軸上的「分量」（components）。  中文中「分量」是 vector component 的翻譯。  在向量時鐘中的意義：  向量時鐘用一組 key-value 組來記錄每個節點的版本：     {\n    \"Sx\"  :   2  ,\n    \"Sy\"  :   1  ,\n    \"Sz\"  :   0\n   }\n  這其實就是一個多維的向量，每個節點（如 Sx, Sy, Sz）對應一個「分量」：   Sx 的分量是 2  Sy 的分量是 1  Sz 的分量是 0   ✅ 用「分量」的好處：   可以用來比較兩個向量時鐘是否發生衝突  易於理解為「每個節點對這筆資料的貢獻度（或修改次數）」   📘 總結：  向量時鐘中的「分量」就是：   每個節點在向量中對應的版本值 。  叫它「分量」是因為這種結構本身就是一個向量，這個詞也承襲了數學中向量各個維度的稱呼。這在資料庫、分散式系統、版> 本控制等領域中是常見的術語。     詳細步驟說明：    初始寫入：  （   Sx  第1次 寫入此鍵）\n   客戶端把資料項   D1  寫入系統，且這個寫入是由伺服器   Sx  來處理。  因此，就有了向量時鐘為   D1([Sx, 1]) 。   後續覆寫：  （   Sx  第2次 更新此鍵，取代了舊版本）\n   另一客戶端讀取了最新的   D1  ，並把它改為   D2  ，仍然是由同一部伺服器   Sx  進行寫入處理把資料寫回伺服器。  因此，  D2  繼承自   D1 ，視為  同一演進鏈 ，故   Sx  將其版本遞增為   D2([Sx, 2]) 。   並行更新1：  （繼承了之前   Sx:2  的版本，  並加入   Sy:1  ）\n   第三個客戶端讀取最新   D2 ，更新為   D3 ，  透過  不同節點   Sy  寫入。   Sy  不曾寫過該鍵，於是   D3  的時鐘為   ([Sx,2], [Sy,1]) 。   並行更新2：   第四個客戶端也  讀取   D2 （與步驟3並行） ，更新為   D4 ，  透過  另一節點   Sz  寫入。得到   D4  的時鐘   ([Sx,2], [Sz,1]) 。   衝突產生：   現在系統內存在   D3  和   D4  兩條  平行分支 （皆從 D2 演化而來）。   D3  的時鐘   [Sx:2, Sy:1]  與   D4  的時鐘   [Sx:2, Sz:1]  互相無法比較誰包含誰（各自有對方沒有的分量），判斷為  衝突版本 。  此時若有讀取請求，\n   系統會  同時返回   D3  和   D4  兩個版本 給客戶端。  要求客戶端執行  應用層合併 （例如將兩個購物車列表合併）後提交一個最  終版本   D5([Sx,3], [Sy,1], [Sz,1]) 。   衝突合併：   客戶端解析衝突得到最終結果，再寫入系統（假設透過   Sx  ）。   Sx  將合併版本   D5  的時鐘更新為   ([Sx,3], [Sy,1], [Sz,1]) 。  因為   D5  包含了之前   D3 、  D4  的所有分量（   Sx:3 ≥2, Sy:1 ≥1, Sz:1 ≥1  ），  因此   D5  被視為  後繼版本 ，舊的   D3 、  D4  可被標記為過時並最終刪除。   節點故障與容錯機制：Gossip 協議、Sloppy Quorum 與 Hinted Handoff   在大型分散式系統中，  節點故障是常態而非例外 。\n因此，我們需要  機制 來  偵測故障 以及在  故障發生時維持系統的可用性 。\n本節介紹兩類機制：    故障偵測：   如何讓所有節點感知彼此的存活狀態，迅速發現節點宕機或網路隔離。\n   在分散式系統中，如果某個伺服器說另一伺服器已故障，實際上這樣並不足以認為該伺服器確實已故障。   原則：至少需要有兩個獨立來源提供相同資訊，我們才能把該伺服器標記為故障。   故障應對：   當部分節點無法服務時，如何暫時接管其職責以及在其恢復後進行數據補償。   （1）故障偵測 – Gossip 協議：    傳統做法：  是一個節點向全體其它節點定期發送心跳   （ all-to-all 心跳 ） ，但在節點很多時網絡開銷巨大。\n    改進方案是 Gossip（八卦）協議，其特點類似社交場合的流言傳播：   每個節點維護一份成員列表 (   去中心化  )   記錄集群中其他節點的 ID 及其心跳計數器等資訊。  時鐘1：定時（例如每隔 w 秒），表示「我還活著」。   各節點獨立地每隔固定時間增加自己的心跳計數。  時鐘2：定時（例如每隔 𝑡 秒），表示「刷存在感」。   每個節點從成員列表中隨機選擇幾個節點，  將自己的心跳資訊發送給接收者。\n   接收者：更新對應節點的心跳值，  接收者：繼續隨機節點轉發，類似病毒傳播地散播心跳消息。   節點收到某成員的信息：  會更新本地成員列表，確保彼此盡量最終收斂到一致的最新成員狀態。   判定失效：  若成員列表中某節點的心跳值在  超過一定時間沒有增長 （ 例如超過 5 個 heartbeat 週期 ），則認定該節點  離線 。   好處：   透過 Gossip 機制，整個集群能在較小網絡開銷下達成對故障節點的共識。  由於採隨機對等傳播，Gossip 在大規模節點時的消息數量只線性增加且具高度冗餘，不易有單點瓶頸。   圖 6-11 示範：  節點   s0  偵測到節點   s2  長時間沒心跳，   s0  將此資訊隨 Gossip 傳給其他節點，最終全網標記   s2  故障。    （2）臨時故障應對 – Sloppy Quorum & Hinted Handoff：   發現節點故障後，系統應繼續提供服務而不強制等待故障節點恢復。  這涉及兩個機制：    Sloppy Quorum（草率仲裁）：    為了提高可用性，暫時放寬 Quorum 的嚴格一致性要求。  在原本需要與特定 N 個副本交互的基礎上，改為：   寫入：   繞過故障節點  在雜湊環上選擇前 W 個運行狀良好的伺服器進行寫入，即算成功。   讀取：   繞過故障節點  在雜湊環上選擇前 R 個運行狀良好的伺服器進行讀取，即算成功。  只要  存活節點數量尚 >= W 或 R ，讀寫操作就不會因單節點故障而被阻斷，提高了可用性。   Hinted Handoff（提示換手）： \n寫入因 Sloppy Quorum 被臨時寫到代管節點，如何在故障節點恢復後補回缺失的更新？  答案是  提示換手 ：   代管寫入的節點會將針對故障節點的更新保存下來（附上「給節點 X 的資料變更」提示）。  等偵測到原故障節點重新上線後，代管者再將這些更新“轉交”回原設置應儲存該資料的節點，完成補寫。   圖 6-12 所示，  s2 故障期間 s3 代管了本屬於 s2 的更新，當 s2 恢復時，s3 將相關資料交還給 s2。    上述兩者的結合增強可用性   在短暫節點故障期間，系統仍能對外提供近乎正常的服務。  當節點恢復後又能將資料漸進糾正，實現最終一致。   犧牲了一定程度的一致性保證   Sloppy Quorum 犧牲了一定一致性保證，可能讀不到最新寫入（ 因寫到臨時節點 ），但換來的是更高的故障容忍和持續服務能力。   （3）永久故障應對 – 防止資料長期不一致： Merkle Tree   如果某節點  永久失效（ 例如硬碟損毀無法恢復數據 ） ，則使用上述  提示換手也無法把它「喚醒」 。  此時通常透過   反熵（Anti-Entropy）機制在存活節點間進行資料修復同步 。  典型手段是使用   Merkle Tree（梅克爾樹，又稱雜湊樹） 來高效比較大範圍資料：   透過對資料集合計算出  層次化 的雜湊，\n以  快速定位 哪個區段資料不一致，\n並能  最小化資料傳輸 。  Amazon Dynamo 實作  基於 Merkle 樹的反熵協議，確保副本間數據一致性。   將整個 key 空間分塊，\n(1) 各節點對每塊計算雜湊，構建一棵樹並從根比對，若根相同則所有子樹都相同；\n(2) 若根不同，則只需下探有差異的子樹分支，迅速找出不一致的鍵集合。\n(3) 找到差異後，再進行資料補償同步。\n(4) 透過定期運行 Merkle 樹比較，各副本可在後台修復長期分歧，最終達成一致。\n   維基百科 Merkle Tree   雜湊樹（hash tree；Merkle tree），\n在密碼學及電腦科學中是一種樹形資料結構，\n每個葉節點均以資料塊的雜湊作為標籤，\n而除了葉節點以外的節點則以其子節點標籤的加密雜湊作為標籤。\n\n雜湊樹能夠高效、安全地驗證大型資料結構的內容，是雜湊鏈的推廣形式。\n   \n二元雜湊樹範例。\n雜湊 0-0 和 0-1 分別是資料塊 L1 和 L2 的雜湊值。\n雜湊 0 是將雜湊 0-0 和 0-1 連接後所取得的雜湊值。   https://www.youtube.com/watch?v=pRwvZzxcH2w   🔍 Merkle 樹建構流程（舉例：鍵空間 1～12）  🎯 說明：   假設我們的鍵值範圍是   1 到 12 ，以下是如何建構出對應的 Merkle 樹。\n過程中若有異常（如資料不一致），會以  特別標記的方塊 來呈現。   🧩 Step 1：鍵值劃分桶子（圖 6-13）    將鍵的空間平均劃分為多個「桶子（bucket）」，本例中共   4 個桶子 。  每個桶子會對應一棵子樹的  根節點（root level node） ，後續用來維護這個桶內的資料結構。  📌   目的：  降低單一 Merkle 樹過大，提升定位異常的效率。\n    🔐 Step 2：鍵值雜湊（圖 6-14）    對每個桶子內的所有鍵值進行  統一的 hash 處理 。  這些 hash 結果將作為 Merkle Tree 的葉節點（leaf nodes）。\n    🌿 Step 3：建立桶子層級的雜湊節點（圖 6-15）    每個桶子產生一個對應的 hash node，代表該桶整體資料狀態。  這些節點會作為上層 Merkle Tree 的中繼節點（intermediate nodes）。\n    🌳 Step 4：建構最終的 Merkle 樹（圖 6-16）    從每個桶子的 hash node 開始，  兩兩組合並進行 hash ，逐層向上建構。  最終會得到一個全域的   Merkle Root ，可用來表示整體系統資料的一致性狀態。\n    ✅ Merkle Tree 優點整理     特性  說明    區域性更新  只需更新變更桶子對應的子樹及相關 hash，其他部分保持不變   快速偵測異常  比對 root hash，若不一致即可快速縮小範圍至異常桶子   易於擴充  桶子數可依據資料規模動態調整，適合大規模分散式系統   應用場景  區塊鏈、分散式檔案系統（如 IPFS）、資料一致性檢查與同步機制等     只要使用 Merkle Tree 的做法，\n需要同步的資料量就會與兩個副本之間的差異(而不是所包含資料量)成正比。\n\n在實際的系統中，\n桶子的尺寸有可能相當大。\n舉例來說，我們可能會設定每十億個鍵對應一百萬個桶子，如此一來，每個桶子就包含 1000 個鍵。\n    （4）資料中心服務中斷的問題： 跨多個資料中心複製資料的做法非常重要。     📌 回顧一下  Session 3 階段內容，至此我們聚焦在分散式鍵值存儲系統中  一致性與容錯 挑戰  (1) 資料分區：一致性雜湊與水平擴充  (2) 資料複製：高可用性的基石  (3) 一致性：Quorum 機制與最終一致性  (4) 不一致問題的解決方式：版本控制 ( Versioning ) 與向量時鐘 ( Vector Clock )  (5) 節點故障與容錯機制：Gossip 協議、Sloppy Quorum 與 Hinted Handoff  我們已構建起一個分散式鍵值存儲的完整藍圖，  涵蓋從資料分區、複製到一致性維護、故障處理的各方面。  主要收穫包括：    一致性模型選擇：  綜上，本系統採用  最終一致性 模型，強調高可用性與容錯。但我們也提供參數使其一致性  可調 （Consistency Tunable），例如客戶端可根據需求選擇不同 R、W 配置來實現從弱一致到強一致的切換。   調節一致性的策略：  Quorum 機制允許我們通過調整 W、R   在一致性與延遲間找到平衡點 。對追求最終一致性的系統，可選較小 W、R 以降低延遲；如需強一致，可設定 W+R > N。   衝突偵測與解決：  透過  向量時鐘 為每個鍵值跟蹤多副本的版本演進史，偵測並行更新造成的衝突。在需要時將多版本交由客戶端合併，確保沒有寫入被無故覆蓋或遺失。儘管增加實現複雜度，但對於高度可用系統而言，這是達成最終一致性的關鍵。   故障容錯機制：  Gossip 協議提供去中心化且高容錯的故障偵測方案，讓集群快速達成對節點存活狀態的共識。Sloppy Quorum 和 Hinted Handoff 則在節點臨時故障時維持服務不中斷，並在其恢復後自動補齊資料，提升整體可用性。永久性故障下，  借助 Merkle 樹等反熵演算法，系統能找出並同步遺失的資料片段。    📌 最後關注系統的實際架構實作   包括資料存儲引擎（寫入/讀取路徑）以及一個使用 C# + MSSQL 的簡易鍵值存儲 API 範例，並探討幾個現代鍵值存儲系統作比較。   系統架構圖  寫入途徑  讀取途徑   系統架構圖：整體架構與資料存儲引擎   綜合前述設計，\n我們的鍵值存儲系統架構可以描述如下：    無中心架構（Decentralized）：  集群中無主從之分，每個節點職責相當（均可作為請求協調者、均存儲部分資料）。這避免單點瓶頸，提升可靠性。   請求路由：  客戶端發出的請求（讀或寫），可透過  一致性雜湊 機制路由到對應的節點處理。實務中通常由客戶端或中介負責計算鍵的雜湊並選擇節點，或者設置每個節點都能轉發請求到正確節點。   協調者：  處理請求的節點扮演  協調者 角色（Coordinator），充當客戶端與存儲節點之間的代理。協調者負責與副本節點通信以完成讀寫 Quorum。   資料存儲層：  每個節點本地維護存儲引擎，包括  記憶體快取 （memtable）與  磁碟資料檔 （如 SSTable）等，用以快速存取和持久化資料。為性能，經常採用  基於日誌的結構存儲（LSM Tree）  模型。   組件回顧：    分區 ：一致性雜湊確定資料歸屬節點，支撐水平擴展。   複製 ：多副本存放與跨機房配置保證高可用。   一致性 ：Quorum 共識 + 調整 R/W 保證讀寫一致性。   衝突解決 ：向量時鐘 & 客戶端合併應對並發更新。   故障處理 ：Gossip 偵測 + Sloppy Quorum / Hinted Handoff 保證系統容錯與最終一致。  圖 6-17 為系統架構示意，展示了沒有中央控制節點下，各節點如何透過雜湊環、複製和協調機制協同工作：\n   圖 6-18 為分散式鍵值存儲系統的每個節點，去中心化的示意圖。\n   接下來重點說明\n  資料寫入與讀取路徑 在節點內的處理流程，這對  性能和可靠性 至關重要。   寫入途徑：資料寫入路徑（Write Path）  對於來自客戶端發出的每一次寫入請求，\n經路由到正確節點後，\n主要執行以下步驟（借鑑 Apache Cassandra 的實作）：\n     提交日誌 (Commit Log)：  協調節點首先將寫操作記錄追加到  提交日誌文件 中。這是一個  有序寫入的預寫日誌 ，確保即使隨後節點崩潰，重啟時也能從日誌重放恢復未刷入的資料。  有序寫磁碟相對快速，確保每次寫入先落盤，提高資料耐久性。   寫入記憶體表 (MemTable)：  同時，將此鍵值對寫入節點的  記憶體快取 (memtable) 。MemTable可理解為  存於記憶體的排序Map結構，累積近期寫入（尚未刷盤） 。   確認 & 複製：  協調者等待本地（及可能其他副本）的MemTable寫入操作成功返回（達到 W 個ACK）後，即可向客戶端回覆寫入成功。這時數據已經在記憶體中，以及至少 W 個副本的提交日誌中持久化。   MemTable 刷盤 (Flush to SSTable)：  隨著寫入不斷進行，MemTable 佔用內存越來越大。當其大小達到預設閾值或時間週期，就觸發  刷盤 ：把 MemTable 內容排序後寫出到磁碟上的   SSTable（Sorted String Table）  檔案。SSTable 是有序的不變（immutable）鍵值集合檔，每次刷盤生成一個新的檔案。  刷盤過程同時清空該 MemTable，並打上一個新的 MemTable 寫入後續資料。   檔案合併 (Compaction)： （可選）為避免 SSTable 檔過多或存在過時版本碎片，後台定期對多個 SSTable 檔案進行  壓實（Compaction） ，合併同一鍵的多版本記錄，棄掉被覆蓋或刪除的舊版本，減少查詢開銷。  以上流程確保了寫入既  快速 （記憶體操作+有序日誌）又  安全 （日誌在先，防故障丟失）。Cassandra 寫路徑的設計容許高吞吐的隨機寫操作，並通過後續順序刷盤優化磁碟訪問。    實現提示：  寫路徑強調  先寫日誌再寫內存 ，這與傳統關係資料庫 WAL + Buffer Pool 機制類似，稱為「  先寫日誌 」策略。這樣在crash時，可重播日誌保障資料不丟。Cassandra也採此法確保即使節點故障重啟，提交日誌能恢復自上次flush後失去的資料。   讀取途徑：資料讀取路徑（Read Path）  讀取請求的流程相對直觀，\n但需處理資料可能分散在記憶體和多個磁碟檔的情況。節點接到讀取某鍵的請求後：      查詢 MemTable：  優先在  記憶體快取 中查找該鍵。如果命中（資料尚未刷盤，存在於最新 MemTable），直接返回值，讀取結束。這是最快的情況（O(1)雜湊查找，無磁碟IO）。   查詢 Cache 未命中：  若鍵不在記憶體中（MemTable miss），說明資料要麼在舊的 SSTable 檔，要麼此鍵根本不存在。此時進入磁碟查找流程。      Bloom Filter 篩選：  為了避免遍歷所有 SSTable 檔案查找，可以使用  Bloom Filter 為每個 SSTable 加一層索引。Bloom Filter 是一種空間高效的機率型資料結構，可快速測試「某鍵是否  不在 某檔案」。每個 SSTable 存有對應的 Bloom Filter，當查找鍵K時，先讓 Bloom Filter 判斷該 SSTable 是否  可能 包含 K。   如果 Bloom Filter 判定「不可能在此 SSTable」，則可直接跳過該檔案。  若判定「可能存在」，才去該 SSTable 中透過檔內索引（二分搜尋等）實際查找。  透過Bloom Filter，可以大幅減少不必要的磁碟IO。例如某鍵可能僅存在最新幾個檔案，Bloom Filter 將排除大部分與該鍵無關的檔案。   SSTable 查找：  對於Bloom Filter結果為可能包含的那些 SSTable，打開檔案（典型為有序IO），利用其內部稽核（如稀疏索引或B樹索引）定位鍵的位置，讀出對應的值。由於SSTable內記錄有序，可以較快速地找到鍵。   數據返回與合併：  如果從多個副本節點讀取（R>1情況），協調者會收到多份值，需比較其版本（例如 vector clock 或時間戳）選擇最新的結果返回給客戶端。如果偵測到版本衝突（如之前所述出現多版本），則返回所有版本讓客戶端處理。   讀取快取：  讀取完成後，可將該鍵結果存入  頁面快取 或  鍵值快取 ，下次相同鍵訪問時直接命中，提高熱點讀性能。  總結來說，\n讀路徑的優化重點在於  記憶體命中優先 ，\n其次透過  Bloom Filter 減少磁碟掃描範圍。  經過這些步驟，\n大部分讀請求可在 O(1) 時間返回（記憶體命中）或以少量有序 IO 完成。\nBloom Filter 雖有極小機率誤判（回傳“可能存在”實際沒有），但不影響正確性，只是多一次空查找，而且誤判率可透過調整位元數控制。    Session 3 各個組件環環相扣關係的架構圖：    1.   起點：分散式需求   從單機無法滿足的需求出發（大容量、高可用、可擴展）  2.   基礎設計：資料分區與複製    資料分區 ：使用一致性雜湊將資料均勻分散到多個節點   資料複製 ：每份資料保存 N 個副本，確保可用性  兩者相輔相成：分區決定資料位置，複製保證容錯  3.   衍生挑戰：一致性問題   多副本帶來一致性挑戰  CAP 定理迫使我們在 AP 和 CP 之間選擇  選擇 AP 系統（最終一致性）以保證高可用  4.   解決方案：一致性與衝突處理    Quorum 機制 ：通過 N/W/R 參數調節一致性級別   向量時鐘 ：檢測並解決並發更新造成的版本衝突  5.   進階挑戰：節點故障    故障偵測 ：Gossip 協議去中心化地發現故障節點   臨時故障 ：Sloppy Quorum + Hinted Handoff 維持服務   永久故障 ：Merkle Tree 進行資料修復  6.   最終實現：完整系統架構    系統架構 ：去中心化設計，協調者模式   寫入路徑 ：Commit Log → MemTable → SSTable   讀取路徑 ：MemTable 優先，Bloom Filter 優化   節點內部 ：LSM Tree 結構，後台 Compaction  環環相扣的特點：    因果關係明確 ：每一層的設計都是為了解決上一層帶來的問題   相互依賴 ：各組件不是獨立存在，而是共同構成完整系統   持續優化 ：系統運行中的經驗會反饋到各層設計的改進  這個架構展示了分散式鍵值儲存系統設計的完整思路，從需求出發，逐步解決各種挑戰，最終形成一個高可用、可擴展的系統。   第六章 總結  經過三個階段的講解，我們完整覆蓋了  分散式鍵值存儲系統 從原理到實作的方方面面：    Session 1：  理解了鍵值資料庫的基本定義。   Session 2：  單機實現方式，以及擴展到分散式系統時需要面臨的 CAP 抉擇。   Session 3：  從資料分區/複製方法，到深入探討了為實現高可用而採取最終一致性的系統中，如何利用 quorum 共識調節一致性、用向量時鐘來侦測和解決衝突版本、用 Gossip 和 Hinted Handoff 等機制來實現故障容忍與自癒。並闡述了實際系統架構下資料讀寫的具體流程（MemTable+SSTable 和 Bloom Filter 等實作細節）。此外，我們將該系統與當今主流方案進行了對比，理解不同系統在 CAP 取捨和架構設計上的演進。   整章核心要點回顧：  (對照下表)   為處理  海量資料 ：採用  分區/一致性雜湊 分散鍵到多節點。  提供  高可用讀取 ：透過  多副本複製 ，甚至跨機房部署。  支撐  高可用寫入 ：允許  最終一致 ，使用向量時鐘進行版本控制與衝突解決。   可線性擴展 ：增加節點透過一致性雜湊自動攤平負載，虛擬節點支持異構性。  容忍  臨時故障 ：採用 Sloppy Quorum + Hinted Handoff 技術保證服務不中斷。  應對  永久故障 ：利用 Merkle Tree 進行反熵同步。   跨域高可用 ：多資料中心複製與故障轉移策略，保障局部機房宕機時系統整體仍存活。      C# 最小鍵值存儲 API 實作範例  下面我們將實作一個簡化的鍵值存儲服務，使用 C# (.NET 8 Minimal API) 提供 HTTP 接口，並以 MSSQL 資料庫作為持久層。此範例展示鍵值存儲的核心功能，包括資料的寫入、讀取和版本管理對應我們前述章節的概念。   資料表設計：  在 MSSQL 中建立一張   KeyValueStore  表：     CREATE TABLE KeyValueStore (\n       [Key]   NVARCHAR(256) NOT NULL PRIMARY KEY,\n       [Value] NVARCHAR(MAX) NULL,\n       [Version] INT NOT NULL DEFAULT 0\n   );\n    Key  欄位作為主鍵存放鍵名稱。   Value  欄位存放字串類型的值（實務中可用 VARBINARY(MAX) 存放二進位資料)。   Version  欄位記錄此鍵的版本號，用於簡單實現  版本控制 （每次更新遞增版本）。這對應前述向量時鐘的概念縮減版：我們僅用單一整數表示版本，雖無法處理多副本並發，但可用於樂觀鎖防止寫入覆蓋較新更新。  接著，我們使用 .NET 8 Minimal API 建立 HTTP 介面，包含   PUT （寫入/更新鍵）與   GET （讀取鍵）兩種操作：     using   Microsoft  .  Data  .  SqlClient  ;\n   using   Dapper  ;\n   \n   var   builder   =   WebApplication.  CreateBuilder  (args);\n   var   app   =   builder.  Build  ();\n   \n   // Database connection string (modify as appropriate for your environment)\n   string   connString   =   builder.Configuration.  GetConnectionString  (  \"KeyValueDb\"  );\n   \n   // 1. GET endpoint: retrieve value by key\n   app.  MapGet  (  \"/kv/{key}\"  ,   async   (  string   key  )   =>\n   {\n       const   string   sql   =   \"SELECT [Value], [Version] FROM KeyValueStore WHERE [Key] = @Key\"  ;\n       await   using   var   conn   =   new   SqlConnection  (connString);\n       var   result   =   await   conn.  QueryFirstOrDefaultAsync  \u003C(  string   Value  ,   int   Version  )>(sql,   new   { Key   =   key });\n       if   (result.Value   ==   null  )\n       {\n           return   Results.  NotFound  (  $\"Key '{  key  }' not found\"  );\n       }\n       // Return the value and version (as ETag header for concurrency control, e.g.)\n       return   Results.  Ok  (  new   { Key   =   key, Value   =   result.Value, Version   =   result.Version });\n   });\n   \n   // 2. PUT endpoint: insert or update a key-value\n   app.  MapPut  (  \"/kv/{key}\"  ,   async   (  string   key  ,   string   value  )   =>\n   {\n       await   using   var   conn   =   new   SqlConnection  (connString);\n       // Try update first\n       const   string   updateSql   =   @\"\n           UPDATE KeyValueStore\n           SET [Value] = @Value, [Version] = [Version] + 1\n           WHERE [Key] = @Key;\n           SELECT @@ROWCOUNT;\"  ;\n       var   rows   =   await   conn.  ExecuteScalarAsync  \u003C  int  >(updateSql,   new   { Key   =   key, Value   =   value });\n       if   (rows   ==   0  )\n       {\n           // If key not exists, insert new record\n           const   string   insertSql   =   \"INSERT INTO KeyValueStore([Key],[Value],[Version]) VALUES(@Key, @Value, 0);\"  ;\n           try\n           {\n               await   conn.  ExecuteAsync  (insertSql,   new   { Key   =   key, Value   =   value });\n           }\n           catch   (  SqlException   ex  )   when   (ex.Number   ==   2627  )   // PK violation\n           {\n               // Handle race condition: key was inserted by another request in the meantime\n               return   Results.  Conflict  (  $\"Key '{  key  }' was inserted by another client.\"  );\n           }\n       }\n       return   Results.  NoContent  ();\n   });\n   \n   app.  Run  ();\n  上述程式碼說明：   我們使用   Dapper  輕量ORM來執行 SQL 查詢，方便地將結果映射為 C# 型別。   查詢 (GET) ：透過主鍵查詢資料庫，若找到則返回 JSON，包括鍵和值以及版本號。版本號可以透過 HTTP ETag 頭返回，供客戶端做併發控制（比如更新時帶回去作條件比對）。   更新/插入 (PUT) ：先嘗試執行 SQL   UPDATE  語句（利用   @@ROWCOUNT  判斷是否有行受影響）更新現有記錄的值，並將版本   Version = Version + 1 。若   UPDATE  返回影響行數為0，表示該鍵不存在，進而嘗試執行   INSERT  新記錄。考慮到可能出現  併發 （兩請求幾乎同時插入同一鍵導致一方PK衝突），捕獲 PK 違規錯誤號2627並返回衝突結果，提醒客戶端該鍵已被其他請求寫入。  整個 PUT 操作未使用顯式交易（Transaction），但由於我們先嘗試 UPDATE 再 INSERT，兩步間依然有極小窗口可能導致重複插入。因此以 try-catch 捕捉 PK 衝突視為  最終一致性下的衝突解決 之一種：這裡簡單地告知客戶端衝突，由客戶端決定後續（類似 Dynamo 讀合併寫回模式）。   對應章節概念：  這段程式碼體現了數個我們在前面討論的概念：    Hash Table 存儲 ：我們以資料庫表的 PK 索引模擬鍵值對的 O(1) 查找，類似單機 HashTable。   持久化與 WAL ：SQL 資料庫本身在執行 UPDATE/INSERT 時已使用WAL日志確保原子性，我們不需另寫文件日誌。但原理類似我們在寫路徑中強調的先寫提交日誌。   版本號/衝突控制 ：我們透過   Version  欄位實現  版本控制 ，每次更新+1。這與向量時鐘理念類似但簡化為單一節點序號，用於檢測並發更新：若多客戶端同時讀取同一版本並各自修改，我們可以藉由版本是否匹配來檢測衝突（在此例中，可在 PUT 加入   WHERE Version = X  條件保護，若版本不符則UPDATE無效，提示客戶端重試）。   最終一致性與衝突 ：我們示範了簡單的  衝突處理 邏輯（PK衝突時返回 409），這映射到 Session 2 談的併發寫入衝突需要協調解決的場景。   高可用與容錯 ：本範例為簡化單節點服務，未實現 Gossip 等機制。但若擴展為多節點，我們可令每個節點部署此API服務並指向各自後端（如各自的資料庫分片），客戶端透過一致性哈希選節點請求，即可形成一個去中心的分散集群。再搭配資料庫層的同步機制或上層協調，可實現我們設計的 AP 模型。   讀寫流程 ：GET 端點優先查主鍵索引、PUT 端點先寫資料庫（相當於我們設計中寫MemTable和CommitLog），兩者都體現  先快取/內存再磁碟 的思路。雖然我們這裡讓SQL處理細節，但對應Cassandra架構中就是遵循「內存->磁碟、先log再flush」的流程。    附註：  實務中，可用 Entity Framework Core 或其他 ORM 簡化資料訪問，這裡用 Dapper 為直觀展現 SQL 操作。另外，為專注重點，程式碼省略了一些如日誌、更多錯誤處理，以及向量時鐘多節點部分（因單資料庫難以模擬多副本場景）。但其結構已足以作為Minimal Key-Value Store服務的雛形。    現代鍵值存儲框架演進與比較 (2023–2025)  如今市面上有眾多鍵值存儲解決方案，各具不同的架構與特性。我們選取其中具代表性的 DynamoDB、TiKV、FoundationDB、etcd、Redis Cluster，結合近年演進，做一番概覽與比較：     平台  架構/定位  CAP 類型  一致性協議  特點與近年發展     Amazon DynamoDB  AWS 雲托管鍵值資料庫，無縫擴展   AP  (預設)  提供可選強一致讀   單主 Multi-Paxos  每分片一主  高度可擴展（支援萬億級別請求），預設最終一致但可選  強一致讀 模式。近年新增  全局資料表 實現多區域複製、  交易 支持 (Transaction) 等功能，使之同時滿足嚴格一致場景需求。架構源於 Dynamo 論文但實際實現有別：採用  單主同步 複製（每分區一主節點）確保寫一致性，並以  多主Region 實現全球可用。    Apache Cassandra  分散式列族資料庫，也提供鍵值接口   AP  (可調一致性)   無主對等 + Gossip  使用   Hinted Handoff  等  Cassandra 基於 Dynamo 理念實作，無中心，多副本最終一致。特點是採用  可調一致性 （客戶端可指定 R/W），提供 TimeUUID 等順序支援。近年版本（4.0以後）加強了   Audit Logging  和   Backpressure  等特性。    Redis (Cluster 模式)  記憶體鍵值庫，支援集群分片   偏AP （高可用優先）   主從非同步  (Sentinel自動故障轉移)  Redis Cluster將資料通過  哈希槽分片 到多節點，每片一主多從。提供快速讀寫（記憶體操作），但  一致性較弱 ：主節點異步複製到從節點，故障時可能丟失最近更新。採用  Gossip 協議維護節點拓撲，Sentinel/Cluster Bus選舉新主。2023年後，Redis 7 引入  多線程IO 與  ACL 改進，但核心集群架構保持簡潔，以性能和簡易性見長。適用快取場景或對一致性要求不高的即時應用。    TiKV  分散式事務鍵值庫（TiDB 的KV層）   CP    (強一致)   Raft 共識  (每Region三副本)  TiKV 專注於強一致儲存，採用 Google Percolator 模型提供  分散式ACID交易 。透過 Raft 確保複製一致性，每筆資料三副本。近年 PingCAP 將 TiKV 發展為獨立項目，版本7引入  Partitioned RaftKV 引擎，降低寫放大提升效能。TiKV 在 Cloud Native 社區活躍，KubeCon 2023 發表其支援 PB 級數據與 QoS 改進。它適合作為新一代關係資料庫的存儲引擎或需要強一致性的元資料存儲。    FoundationDB  分散式多模 KV 庫（底層Key-Value，頂層可映射文檔、SQL等）   CP  (強一致)   主從 + 任務分離  (分層架構+協調服務)  FoundationDB 提供  嚴格 ACID 交易 的鍵值存儲，將事務處理與存儲層解耦。採用類似集中協調節點 (Resolvers) + 多存儲節點 (Storage Servers) 的設計，以  樂觀並發控制 執行交易，再用  多版本 確保只讀不鎖定。它的複製也使用類似 Raft 的容錯機制（協調器選舉）。Apple 開源後社群活躍，Snowflake 等公司在內部大量使用。近年版本7引入新存儲引擎 Redwood（B+樹結構）提升大值處理效率。FoundationDB 特點在於可透過「Layer」構建高層資料模型，如文檔、SQL，具  通用基礎存儲 潛力，同時保持高性能和分散式一致性。    etcd  分散式一致性KV，主要做配置/註冊   CP  (強一致)   Raft 共識  (3或5節點組)  etcd 是 Kubernetes 等系統的  配置中心 ，專注小型資料的強一致儲存。透過 Raft 保證線性一致讀寫，常以奇數節點組成（3或5）集群，任何變更需多數同意。等價於  決不容忍腦裂 、寧可停止服務保證一致性的策略。故在 CAP 上屬 CP 類型，其寫入需要等待同步，大部分操作延遲高於AP系統，但能保證嚴格一致。近年 etcd 強化了快照與壓縮機制減少長期運行時儲存膨脹，並在 Kubernetes 中驗證了其可靠性。不過由於追求一致性，在大量資料存儲上擴展性有限（通常建議數 GB 以內資料量）。  指出：  Redis/MySQL 僅本地存單副本，而 TiKV/etcd 在三個節點上存三副本（通過 Raft 協議） 。這直接反映了它們在 CAP 上的取捨差異——Redis/傳統資料庫多為 CA/單機擴展模式，追求可用性但透過主從複製仍可能有不一致；而 TiKV、etcd 堅持 CP 模式，每次寫都經過多節點一致同意。  總體而言，近年鍵值存儲領域趨勢可以概括為：    強一致性的崛起：  在雲原生環境下，像 FoundationDB、TiKV、etcd 等強一致KV廣受關注，因為它們簡化了上層應用的邏輯（不用擔心讀到舊數據）。這些系統大都基於共識演算法（Raft/Paxos）實現，隨著網路和算法優化，其性能也逐步提高，可支持越來越大的集群和資料量。   最終一致性的堅守：  傳統 Dynamo 風格的 AP 系統依然在大規模場景佔有一席，例如 DynamoDB、Cassandra、Riak 等。它們在全球部署、高流量容忍方面具備獨特優勢，並且透過引入  輔助功能 （如 DynamoDB 提供可選強一致讀），在CAP平衡上提供更多彈性。   內存與持久化融合：  Redis 從純內存Cache演進出 Redis Cluster、再到支援持久化AOF/快照，以及近期的Redis on Flash方案等，說明高速內存KV和持久存儲結合是需求所在。許多系統（如 TiKV 使用 RocksDB LSM 引擎）也著重  壓榨硬體 性能，包含NVMe、optane等新介質的應用。   多模與易用性：  新一代KV如 FoundationDB 主打「一個核心，多種模型」，TiKV + TiDB 則展示了將鍵值存儲升級到分散式SQL的可能。這些演進都圍繞讓開發者更容易使用鍵值存儲，同時享受分散式帶來的伸縮與可靠性優勢。   以下額外參考  資料分區：一致性雜湊   Virtual Node、負載平均、動態擴縮   程式範例 ：  ConsistentHashRing  (C# /.NET 8)     public   sealed   class   ConsistentHashRing  \u003C  T  >\n   {\n       private   readonly   SortedDictionary  \u003C  int  ,   T  >   _ring   =   new  ();\n       private   readonly   int   _replicas  ;\n       private   readonly   MD5   _md5   =   MD5.  Create  ();\n   \n       public   ConsistentHashRing  (  IEnumerable  \u003C  T  >   nodes  ,   int   replicas   =   160  )\n       {\n           _replicas   =   replicas;\n           foreach   (  var   node   in   nodes)   AddNode  (node);\n       }\n   \n       public   void   AddNode  (  T   node  )\n       {\n           for   (  int   i   =   0  ; i   \u003C   _replicas; i  ++  )\n           {\n               var   hash   =   Hash  (  $\"{  node  }-{  i  }\"  );\n               _ring[hash]   =   node;\n           }\n       }\n   \n       public   void   RemoveNode  (  T   node  )\n       {\n           for   (  int   i   =   0  ; i   \u003C   _replicas; i  ++  )\n           {\n               var   hash   =   Hash  (  $\"{  node  }-{  i  }\"  );\n               _ring.  Remove  (hash);\n           }\n       }\n   \n       public   T   GetNode  (  string   key  )\n       {\n           if   (  !  _ring.  Any  ())   throw   new   InvalidOperationException  (  \"Ring is empty\"  );\n           var   hash   =   Hash  (key);\n           var   kv   =   _ring.  FirstOrDefault  (  p   =>   p.Key   >=   hash);\n           return   kv.  Equals  (  default  (  KeyValuePair  \u003C  int  ,   T  >))   ?   _ring.  First  ().Value   :   kv.Value;\n       }\n   \n       private   int   Hash  (  string   input  )\n       {\n           var   bytes   =   _md5.  ComputeHash  (Encoding.UTF8.  GetBytes  (input));\n           return   BitConverter.  ToInt32  (bytes,   0  )   &   0x7FFFFFFF  ;   // positive int\n       }\n   }\n  html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"id":691,"path":692,"dir":632,"title":693,"description":7,"keywords":694,"body":699},"content:7.sdi-aig:7.chapter7.md","/sdi-aig/chapter7","07 設計可用於分散式系統的唯一ID生成器",[695,696,697,698],"第1步：了解問題並確立設計的範圍","第2步：提出高階設計並取得認可","第3步：深入設計","第4步：匯整總結","    第1步：了解問題並確立設計的範圍  為了釐清狀況而提出問題，是解決任何系統設計面試問題的第一步。這裡有一些應試者與面試官互動的例子:  應試者:唯一 ID 的特性是什麼?  面試官:ID 必須是唯一而不可重複，而且可進行排序。  應試者:針對每一個新記錄，ID 都要增加1嗎?  面試官:ID 要隨時間遞增，但不一定只遞增1。晚上所建立的ID，必須大於當天早上所建立的ID。  應試者:ID的值只能包含數值嗎?  面試官:是的，沒錯。  應試者:ID 的長度有什麼要求?  面試官:ID 應該為 64位元。  應試者:系統的規模有多大?  面試官:系統應該要能夠每秒生成10000個ID。  以上就是一些你可以詢問面試官的問題範例。重要的是一定要瞭解要求，並釐清所有不清楚的部分。針對這個面試問題，相應的要求列表如下:   ID 必须是唯一而不重複的。  ID 的值只能是數值。  ID 必須 64 位元(bits)。  ID 須依循時間的順序。  每秒要能夠生成10000個以上的唯一ID。  第2步：提出高階設計並取得認可  作者介紹了以下四種方式來實作：   多master複製(Multi-master replicaiton)  UUID (通用唯一標識符號)  票證伺服器 (Ticket Server)  Twitter雪片(snowflake)做法  多master複製(Multi-master replicaiton)    這種做法會用到資料庫的   auto_increment  功能。不過我們並不是把下一個ID增加1，而是增加，其中的就是所使用資料庫伺服器的數量。如圖7-2 所示，只要把同一個伺服器的前一個ID加上2，就可以生成下一個ID。這種做法解決了一些可擴展性的問題，因為 ID 可根據資料庫伺服器的數量進行擴展。  不過，這個策略有一些主要的缺點:   如果是採用多個資料中心，這種做法就難以進行擴展。📌每個資料庫都要調整  auto_increment 值。  由多部伺服器分別得出的ID，其值並不一定隨著時間而增加。  添加或移除伺服器時，擴展性並不好。   auto_increment  如下:     -- 設定全域步進值 (影響所有表)\n   SET @@auto_increment_increment = 2;\n   \n   -- 或者只影響當前 session\n   SET SESSION auto_increment_increment = 2;\n     CREATE TABLE users (\n       id INT AUTO_INCREMENT PRIMARY KEY，\n       name VARCHAR(50)\n   );\n   \n   SET SESSION auto_increment_increment = 3;  -- 每次 +3\n   \n   INSERT INTO users (name) VALUES ('Alice');\n   INSERT INTO users (name) VALUES ('Bob');\n   INSERT INTO users (name) VALUES ('Charlie');\n   \n   -- 結果：\n   -- id | name\n   --  1 | Alice\n   --  4 | Bob\n   --  7 | Charlie\n  UUID (通用唯一標識符號)  UUID 是取得唯一ID 的另一種簡便方法。UUID是一個128位元的數字，可用來標識電腦系統中的資訊。UUID 出現重複值的機率非常低。以下內容引自維基百科，「如果每秒產生10億個UUID，大概需要經過100年之後，出現一次重複值的機率才會來到50%」。  這裡就有一個 UUID 的範例:   09c93c62-5064-468d-bf8a-c07e1040bfb2UUID  可獨立生成，而不需要在伺服器之間進行協調。圖7-3呈現的就是UUID 的設計。    在這樣的設計下，每個 Web 伺服器都有一個ID生成器，而且每個 Web 伺服器都可以各自獨立生成ID。  優點:   UUID 的生成很簡單。伺服器之間完全不需要進行協調，因此不會出現任何同步問題。  這個系統很容易進行擴展，因為每個 Web 伺服器都可以自行生成所  要使用的ID。ID 生成器可隨著 Web 伺服器輕鬆進行擴展。  缺點:   ID 的長度為128位元，但我們的要求是64位元。  ID 的值並不會隨著時間遞增。  ID 有可能出現非數字的情況。  票證伺服器 (Ticket Server)  票證伺服器(ticket server)是另一種生成唯一ID 的有趣方式。票證伺服器是由 Flicker 所開發，可用來生成分散式的主鍵(primary key)。這個系統的運作方式很值得特別瞭解一下。    其構想就是在單一資料庫伺服器(票證伺服器)中，使用中央管理式(centralized)的 auto_increment 功能。如果想要瞭解更多相\n參考 Flicker 的工程部落格文章   2 。  優點:   數字 ID。  很容易進行實作，而且可適用於中小型應用。  缺點:   可能會有單點故障的問題。由於採用單一的票證伺服器，因此票證伺服器若出現故障，所有依賴它的系統全都會遇到問題。為了避免單點故障，我們可以設定多部票證伺服器。不過，這也會帶來新的挑戰，例如資料同步的問題。  Flickr: Ticket Servers — Distributed Unique Primary Keys on the Cheap  文章背景與目的    分片（Sharding）與主-主架構  \nFlickr 將資料分片儲存在多臺 MySQL 上，同時每個分片又採用主—主（master‑master）複製，以提升可靠性與擴充能力。這種架構下，必須確保所有分片生成的 ID 全域唯一，以避免主鍵衝突。   MySQL   AUTO_INCREMENT  無法跨節點保證唯一性  \n雖然單個 MySQL 實例可以便利地使用   AUTO_INCREMENT ，但這在多實例間並無全域唯一保障。  為何不使用 GUID（UUID）？    GUID 體積大、索引效能差  \nGUID 雖然可保證唯一，但其字串長度及非順序性，會使索引龐大且難以維持在記憶體，導致查詢性能下降。   缺乏順序性  \nFlickr 需要 ID 有一定順序性，有助於報表、debug，以及部分快取策略，GUID 雖唯一但不適合此處需求。  中央 Ticket Server：解決方案核心    使用中央 DB 生成 ID  \nFlickr 使用專門的「Ticket Server」，是一臺專用的 MySQL伺服器，只儲存簡化的 ID 表（如   Tickets64 ）。透過插入一條固定的 stub（stub 欄位）再取得   LAST_INSERT_ID() ，以產生全域唯一、連續的 ID。    REPLACE INTO  機制  \n他們使用 MySQL 的非標準語法   REPLACE INTO （如果 stub 已存在，則先刪除再插入），來確保每次插入都會自動產生新的   AUTO_INCREMENT  ID。  高可用設計：雙 Ticket Server（避免單點故障）    雙機部署 + 分隔 ID 空間  \n為了避免 Ticket Server 成為單點故障（SPOF），架設兩臺伺服器，並採用   auto_increment_increment = 2  搭配不同的   auto_increment_offset （分別為 1 與 2），使一台產生奇數 ID，另一台產生偶數 ID，再以 round-robin 分配請求。  其它實務設計細節    多種類別 ID (Sequences)  \nFlickr 不只一張   Tickets64  表，還為各種實體（如 Photos、Accounts、OfflineTasks、Groups）設計獨立序列，以避免彼此干擾與快速消耗。   簡單但有效的設計哲學  \n整體方案雖不優雅，但符合 Flickr 所講求的：「工程上最蠢但能運作」原則，且自 2006 年就投入生產並穩定運作。  Twitter雪片(snowflake)做法  前面所提到的一些做法，分別針對不同的ID 生成系統運作原理，提供了一些不同的構想。不過，這些做法全都不符合我們的特定要求，因此我們還需要另一種做法。Twitter 的唯一 ID 生成系統叫做「snowflake (雪片)」，其構想相當具有啟發性，而且可以滿足我們的要求。  分而治之(divide and conquer)的分治做法，可說是我們的好朋友。我們會把ID 分成不同的好幾段，而不是直接生成整個ID。圖7-5顯示的就是64位元 ID的佈局方式。    每一段的說明如下。    符號位元  : 1位元。永遠為0。保留以供未來使用。它也許可用來區分有正負號與無正負號的數字。   時間戳  : 41 位元。從某個時間點(可自定義)以來所經過的毫秒數。我們使用 Twitter 雪片的預設時間點1288834974657，這個時間就相當於 UTC 2010年11月4日01:42:54。   資料中心 ID  : 5位元，這裡可以讓我們用來區分2^ 5 = 32 個資料中心。   機器 ID  : 5位元，每個資料中心可以有2 ^ 5 = 32 部機器。   序列編號  : 12 位元。同一部機器/ process 行程所生成的每個ID，其序列編號每次都會以加1的方式逐漸遞增。這個數字每毫秒都會重設為0。  第3步：深入設計  在高階設計中，我們已針對分散式系統討論過一些可用來設計唯一 ID 生成器的各種選項。在這裡我們會選擇 Twitter 雪片 ID 生成器做為參考的做法。接著我們就來深入研究相應的設計。為了重新喚醒我們的記憶，下面重新列出相應的設計圖。    資料中心 ID 與機器 ID 在啟動時就已經選定，通常在系統啟動之後就固定不再改變了。資料中心 ID 與機器 ID的任何修改，都需要進行仔細的檢查，因為如果意外修改了這些值，就有可能導致ID 衝突的情況。ID 生成器在執行時，會生成相應的時間戳與序列編號。  時間戳  最重要的41位元，就是由時間戳所構成。時間戳會隨著時間而遞增，因此ID 也會隨著時間而遞增。圖7-7 顯示的就是如何把二進位表達方式轉換為 UTC的範例。你也可以用類似的方法，把UTC 轉換回二進位表達方式。    41 位元可以表達的最大時間戳為2 ^41-1 = 2199023255551 毫秒(ms)，相當於:~ 69年 = 2199023255551 毫秒 / 1000 秒 / 365天 / 24小時/3600 秒。這也就表示，這個ID 生成器可正常運作69年，如果改用自定義的方式，把起算時間(epoch time)設定成更接近今天的日期，還可以進一步延後這個可正常運作的時間。69年之後，我們就會需要另一個新的起算時間，或是改用其他技術來對這個ID 進行遷移轉換。  序列編號  序列編號有12位元，這給了我們2 ^ 12 = 4096種組合。除非同一部伺服器在一毫秒之內生成了多個ID，否則這個欄位的值就會是0。理論上來說，同一部機器在每毫秒內最多可生成4096個新的ID，即 sequence (每毫秒最多 4096 筆)。  第4步：匯整總結  我們在本章討論了好幾種設計唯一 ID生成器的不同做法:多 master 複製、UUID、票證伺服器，以及 Twitter 的雪片做法。我們最後選擇採用雪片的做法，因為它可支援我們所有的使用需求，而且可以在分散式環境下進行擴展。  如果在面試結束之前還有多餘的時間，這裡還有一些可以額外聊聊的想法:   時鐘同步 : 在我們的設計中，我們假設每一部ID 生成伺服器全都具有相同的時鐘。如果伺服器是在多個核心中執行，這個假設就有可能是不正確的。在使用多部機器的做法中，也存在相同的挑戰。時鐘同步解決方案並不在本書的討論範圍之內。不過，瞭解這個\n問題的存在是很重要的。採用網路時間協定(NTP;Network TimeProtocol)，就是解決此問題最受歡迎的一種做法。  各段長度的調整 : 舉例來說，改用比較短的序列編號、但採用比較多的時間戳位元，對於低並行性、跨越時間更長的應用而言，是一種有效而可行的做法。  高可用性 : 對於其他任務來說，ID 生成器是非常重要的系統，因此它必須具有很高的可用性。  html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"id":701,"path":702,"dir":632,"title":703,"description":7,"keywords":704,"body":709},"content:7.sdi-aig:8.chapter8.md","/sdi-aig/chapter8","08 設計短網址生成器",[705,706,707,708],"第一步驟：了解問題並確立設計的範圍","第二步驟：提出高階設計並取得認可","第三步驟：深入設計","第四步驟：彙整總結","  第一步驟：了解問題並確立設計的範圍  系統設計面試問題通常會刻意保持開放，不把細節說清楚。如果想做出一個精心設計的系統，詢問並釐清問題的過程至關重要。  問題釐清對話   應試者 ：關於短網址生成器的運作方式，你能舉個例子嗎？   面試官 ：假設原始的網址為   https://www.systeminterview.com/q=chatsystem&c=loggedin&v=v3&l=long ，你的服務就是要創建出一個長度比較短的等效網址，例如：  https://tinyurl.com/y7keocwj 。只要點擊這個短網址，就會把你重定向到原始的網址。   應試者 ：使用的流量會有多少呢？   面試官 ：每天要生成1億個網址。   應試者 ：短網址的長度能有多長？   面試官 ：越短越好。   應試者 ：短網址內可使用哪些字元？   面試官 ：短網址可以是數字(0-9)與大小寫字母(a-z、A-Z)的組合。   應試者 ：可以刪除或更新短網址嗎？   面試官 ：為了簡單起見，我們姑且假設不能刪除或更新短網址。  基本需求與核心功能    縮短網址 ：提供一個長網址，回傳一個短網址   網址重定向 ：使用一個短網址，能重新導向至原始的長網址   系統考量 ：應具備高可用性、可擴展性與容錯性  粗略的估算  寫入操作   每天生成：1億個網址  每秒寫入：1億 ÷ 24 ÷ 3600 ≈   1,160 次  讀取操作   假設讀寫比為 10:1  每秒讀取：1,160 × 10 =   11,600 次  儲存需求（10年）   總記錄數：1億 × 365 × 10 =   3,650億筆 記錄  儲存空間：3,650億 × 100 Bytes =   36.5 TB （假設平均網址長度為100字元）   第二步驟：提出高階設計並取得認可  API 端點  採用 REST 風格設計 API：  1. 縮短網址 (POST)    路徑 ：  POST api/v1/data/shorten   請求參數 ：  { \"longUrl\": \"longURLString\" }   回傳 ：短網址 (shortURL)  2. 網址重定向 (GET)    路徑 ：  GET api/v1/shortUrl   回傳 ：透過 HTTP 重定向回傳 longURL  網址重定向流程  當伺服器收到短網址請求時，會透過 HTTP 301 重定向回應，將瀏覽器導向至原始的長網址。     Request URL: https://tinyurl.com/qtj503u\n   Request Method: GET\n   Status Code: 301\n   Location: https://www.amazon.com/dp/dp/B817V4NTFA?pLink=63eaef76...\n    重定向類型比較    301 重定向（永久）   瀏覽器會快取此重定向  未來請求直接發送到長網址伺服器  可降低短網址伺服器的負載   302 重定向（臨時）   瀏覽器不會快取  每次請求都會先經過短網址伺服器  有利於進行點擊率等數據分析  網址縮短流程  透過雜湊函式 f(x) 將長網址轉換為雜湊值（hashValue），這個雜湊值就是短網址的一部分。   要求 ：   每個長網址都能轉換成一個雜湊值  每個雜湊值都能對應回原始的長網址     第三步驟：深入設計  資料模型  將   \u003CshortURL, longURL>  的對應關係保存在關聯式資料庫中。   簡化的資料表結構 ：     欄位  類型  說明    id  Primary Key  主鍵   shortURL  String  短網址   longURL  String  長網址  雜湊函式  雜湊值長度計算    字元集 ：  [0-9, a-z, A-Z] ，共 62 種可能字元   需求 ：支援 3,650 億筆記錄   計算 ：找到最小的 n 值，使得 62^n > 3,650億   結果 ：當 n=7 時，62^7 ≈ 3.5 兆，足以滿足需求  兩種實現方法  方法一：雜湊 + 衝突解決   使用 MD5 或 SHA-1 等雜湊函式處理長網址  截取前 7 個字元  若發生衝突，在長網址後附加預定義字串，重新計算  可使用 Bloom 篩選器優化效能    方法二：Base 62 轉換   為每筆新的長網址生成唯一 ID（資料庫主鍵）  將 ID 從 10 進位轉換為 62 進位  例如：ID   11157  →   2TX    方法比較     特性  雜湊+衝突解決  Base 62 轉換    短網址長度  固定  不固定，隨ID值遞增   ID生成器  不需要  需要唯一ID生成器   衝突處理  可能出現，需解決  不可能出現   安全性  不可預知下一個短網址  可預知，有安全性疑慮  深入研究短網址流程（採用 Base 62）    輸入 ：使用者提供長網址 (longURL)   檢查 ：系統檢查 longURL 是否已存在於資料庫   若存在 ：直接從資料庫取出對應的 shortURL 並回傳   若不存在 ：由唯一 ID 生成器產生新的唯一 ID   轉換 ：使用 Base 62 轉換方法將 ID 轉換為 shortURL   儲存 ：將新的 ID、shortURL 與 longURL 存入資料庫    📌   BASE62轉換動畫範例  深入探討網址重定向流程   使用者點擊短網址連結  負載平衡器將請求轉發至 Web 伺服器  伺服器檢查快取中是否存在該 shortURL\n   若存在：直接回傳 longURL  若不存在：從資料庫查詢  若資料庫中也不存在，表示這是無效的 shortURL  將查詢到的 longURL 送回給使用者，觸發重定向    📌   短網址製作動畫範例   第四步驟：彙整總結  可額外討論的要點    網路限速器 ：為防止惡意用戶大量請求，可根據 IP 位址等規則進行速率限制   Web 伺服器擴展 ：由於 Web 層是無狀態的，可以輕易地增加或移除伺服器來進行擴展   資料庫擴展 ：可使用資料庫複寫（replication）與分片（sharding）等技術來擴展   分析 ：整合分析功能，以追蹤連結的點擊次數與時間等資訊   可用性、一致性與可靠性 ：這些是任何大型系統成功的核心概念  html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"id":711,"path":712,"dir":632,"title":713,"description":7,"keywords":714,"body":717},"content:7.sdi-aig:9.chapter9.md","/sdi-aig/chapter9","09 設計網路爬蟲",[715,716,706,707,708],"什麼是網路爬蟲？","第一步驟：瞭解問題並確立設計的範圍","  什麼是網路爬蟲？  網路爬蟲，又稱 Spider 或 Robot，是一種自動化程式，用來在網際網路上 下載網頁內容、解析其中的連結，再把新連結加入待抓佇列，如此反覆迴圈地遍歷整個網站或更大的網址空間。    常見四大用途     用途  說明  代表例子     搜尋引擎索引  下載網頁並   建立  本地索引，供搜尋引擎快速查詢  Googlebot（Google 搜尋）    網路封存  (Web Archiving)  定期擷取網站內容並長期保存，作為數位典藏  美國國會圖書館、歐盟網路檔案館    網路挖掘  (Web Mining)  大規模收集公開資料，再進行資料探勘以擷取知識  金融機構抓取股東會與年度報告    網路監控  (Web Monitoring)  監測版權、商標或內容侵權，偵測並通報不當使用  Digimarc 掃描網路盜版作品  第一步驟：瞭解問題並確立設計的範圍  網路爬蟲基本的演算法其實很簡單:   給定一組網址, 然後把網址所對應的網頁全部下載下來  從這些網頁中提取所有的網址連結  把新的網址添加到所要下載的網址列表中. 重複執行以上這3個步驟.  問題釐清對話   應試者 ：爬蟲的主要目的是什麼？是搜尋引擎索引、資料挖掘，還是其他用途？   面試官 ：搜尋引擎索引。   應試者 ：網路爬蟲每個月需要抓取多少網頁？   面試官 ：10 億頁。   應試者 ：包含哪些內容類型？只有 HTML 嗎，還是也要處理 PDF、圖片？   面試官 ：只有 HTML。   應試者 ：是否需要偵測新增或已編輯的網頁？   面試官 ：是，需要考慮新增加或已修改的頁面。   應試者 ：抓取到的 HTML 需要保存多久？   面試官 ：保存最多 5 年。   應試者 ：如果遇到重複內容的頁面要怎麼辦？   面試官 ：重複內容應該忽略，不儲存。   ✔︎ 以上對話用來  釐清需求並確認假設 ，避免與面試官對「爬蟲的範圍與目標」產生落差。  優秀網路爬蟲的特徵    可伸縮性（Scalability）   網路規模極大，擁有數十億個網頁。  系統需支援   分散式與並行化  的抓取方式，以提升效率。   穩健性（Robustness）   網路充滿陷阱：錯誤的 HTML、無回應的伺服器、崩潰或惡意連結等。  爬蟲必須具備容錯機制來應對這些異常情況。   禮貌性（Politeness）   不應在短時間內對同一網站發出過多請求。  應遵守   robots.txt  規範並設定抓取延遲，避免造成伺服器負擔。   可擴展性（Extensibility）   系統設計應保持彈性，只需少量修改即可支援新的內容型態。  例如：未來若要抓取圖片檔案，不需重新設計整個架構。  基本需求與核心功能    網頁抓取 ：\n   依「種子 URL → 下載 → 解析 → 新 URL 入佇列」的迴圈流程遞迴抓取。   更新偵測 ：\n   能定期重新抓取已存在的頁面，以納入新增或已編輯的內容。   重複內容去除 ：\n   以內容雜湊（content hash）或 URL 去重機制，避免存儲同樣頁面。   資料儲存 ：\n   將 HTML   存放  最長 5 年，支援高吞吐量讀寫。   粗略的估算     項目  計算  結果     平均 QPS  10 億頁 ÷ 30 天 ÷ 24 小時 ÷ 3600 秒   ≈ 400 頁/秒    峰值 QPS  2 × 平均 QPS（假設尖峰為兩倍）   ≈ 800 頁/秒    月儲存量  10 億頁 × 500 KB/頁   ≈ 500 TB/月    五年儲存量  500 TB/月 × 12 月 × 5 年   ≈ 30 PB   【補充】實際值會依抓取頻率、壓縮率與去重效率略有差異；估算時應與面試官確認每一步的假設。   第二步驟：提出高階設計並取得認可  網路爬蟲的高層設計包含多個協同工作的組件，以下詳細說明各組件的功能與職責。  核心組件      種子網址（Seed URLs）   功能：作為爬蟲的起始點  選擇策略：\n   單一網站：使用該網站的域名作為種子  全網爬取：需要創意性的選擇方法\n   基於地理位置劃分（不同國家的熱門網站）  基於主題劃分（購物、體育、醫療等）  重點：好的種子URL能讓爬蟲遍歷最多的鏈接    網址邊境（URL Frontier）   功能：儲存待下載的URL隊列  實作方式：FIFO（先進先出）隊列  狀態管理：將爬蟲狀態分為「待下載」和「已下載」    HTML 下載器（HTML Downloader）   功能：從網路下載網頁內容  資料來源：從網址邊境（URL Frontier）獲取待下載的URL    DNS解析器（DNS Resolver）   功能：將URL轉換為IP地址  範例：  www.wikipedia.org  → 198.35.26.96    內容解析器（Content Parser）   功能：解析和驗證下載的網頁  設計考量：\n   獨立組件設計，避免拖慢爬蟲速度  過濾格式錯誤的網頁，節省儲存空間    看過的內容（Content Seen）   功能：檢測重複內容（研究顯示29%的網頁是重複的）  實作方式：使用哈希值比較，而非逐字比較  效益：\n   消除數據冗余  縮短處理時間    內容儲存（Content Storage）   儲存策略：\n   磁碟：儲存大部分內容（數據量太大無法全部放入記憶體）  記憶體：快取熱門內容以降低延遲  選擇依據：數據類型、大小、訪問頻率、生命週期    網址鏈結提取器（URL/Links Extractor）   功能：從HTML頁面解析並提取鏈接  處理：將相對路徑轉換為絕對URL    範例：\"/wiki/...\" → \"  https://en.wikipedia.org/wiki/ ...\"    網址篩選器（URL Filter）   功能：過濾不需要的URL  過濾條件：\n   特定內容類型  文件擴展名  錯誤Link  黑名單網站    看過的網址（URL Seen）   功能：追蹤已訪問過或已在Frontier中的URL  重要性：\n   避免重複添加相同URL  防止服務器過載  避免無限循環  實作技術：布隆過濾器（Bloom Filter）或哈希表    網址儲存系統（URL Storage）   功能：儲存已訪問過的URL記錄    網路爬蟲的工作流程     第 1 步：種子URL初始化, 將種子URL添加到URL Frontier隊列中，作為爬蟲的起始點。  第 2 步：獲取待處理URL, HTML下載器從URL Frontier獲取URL列表，準備進行下載。  第 3 步：解析與下載, HTML下載器透過DNS解析器獲取URL對應的IP地址，並開始下載網頁內容。  第 4 步：內容解析, Content Parser解析HTML頁面，檢查頁面格式是否正確，過濾掉格式錯誤的頁面。  第 5 步：傳遞已解析內容, 內容經過解析和驗證後，傳遞給「Content Seen?」組件進行去重檢查。  第 6 步：內容去重檢查「Content Seen?」組件檢查HTML頁面是否已存在於儲存系統中：\n   若已存在：表示相同內容已被處理過（可能來自不同URL），直接丟棄該HTML頁面  若不存在：表示是新內容，將內容傳遞給URL提取器繼續處理  第 7 步：提取鏈接, URL提取器從HTML頁面中解析並提取所有URL鏈接。  第 8 步：URL過濾, 將提取的URL傳遞給URL過濾器，過濾掉不需要的鏈接。  第 9 步：傳遞已過濾URL, URL過濾完成後，傳遞給「URL Seen?」組件。  第 10 步：URL去重檢查, 「URL Seen?」組件檢查URL是否已在儲存系統中：\n   若已存在：表示該URL已被處理過，無需採取任何行動  若不存在：進入下一步  第 11 步：加入待處理隊列, 如果URL之前沒有被處理過，將其添加到URL Frontier，等待下一輪處理。   第三步驟：深入設計  圖遍歷策略：DFS vs BFS   為何選擇BFS而非DFS   網路結構：網路可視為有向圖（網頁為節點，超鏈接為邊）  DFS問題：深度可能過深，容易陷入單一網站  BFS優勢：廣度優先，能更均勻地爬取不同網站     標準BFS的兩大問題   禮貌性問題：同一網頁的大部分鏈接指向同一主機，可能造成服務器請求氾濫  優先級問題：未考慮URL的重要性差異  💡 BFS與DFS的差異, 可以參考   BFS vs DFS  URL Frontier 詳細設計    禮貌性機制（Politeness）   核心原則：   避免短時間內向同一服務器發送過多請求  一次只從同一主機下載一個頁面  下載任務之間添加延遲   實現架構：   Queue router：確保每個隊列只包含同一主機的URL  Mapping table：主機名到隊列的映射表  FIFO隊列（b1-bn）：每個隊列對應一個主機  Queue selector：分配工作線程到特定隊列  Worker threads：依序下載，避免並發請求同一主機        優先級管理（Priority）   優先級依據：   PageRank分數  網站流量  更新頻率  內容重要性   實現組件：   Prioritizer：計算URL優先級  優先級隊列（f1-fn）：不同優先級的隊列  Queue selector：偏向選擇高優先級隊列      完整架構   前端隊列：管理優先級  後端隊列：管理禮貌性      新鮮度維護（Freshness）   優化策略：   基於歷史更新頻率調整重爬頻率  優先重爬重要頁面  動態調整爬取週期    儲存優化   混合儲存方案：   磁盤：儲存大部分URL（數億級別）  記憶體緩衝：加速入隊/出隊操作  定期同步：緩衝區數據定期寫入磁盤  HTML下載器優化    Robots.txt協議   功能：網站與爬蟲的溝通標準  實施：\n   爬取前檢查robots.txt  快取結果避免重複下載  定期更新快取  下面是取自   https://www.amazon.com/robots.txt  的robots.txt文件   User-agent: Googlebot\nDisallow: /creatorhub/*\nDisallow: /rss/people/*/reviews\nDisallow: /gp/pdp/rss/*/reviews\nDisallow: /gp/cdp/member-reviews/\nDisallow: /gp/aw/cr/\n    性能優化技術  分布式爬取   URL空間分割  多服務器並行處理  每個下載器負責特定URL子集     DNS優化   問題：DNS解析延遲（10-200ms）  解決方案：\n   維護DNS快取  域名到IP的映射表  定期更新（cron job）   地理位置優化   爬蟲服務器地理分布  就近爬取降低延遲  適用於所有系統組件   超時控制   設置最大等待時間  避免無響應服務器阻塞  快速切換到其他任務  系統穩健性（Robustness）   關鍵技術   一致性哈希：動態負載均衡，支持服務器增減  狀態持久化：定期保存爬取狀態，支持故障恢復  異常處理：優雅處理錯誤，防止系統崩潰  數據校驗：確保數據完整性和正確性  可擴展性（Extensibility）   插件化架構   模組化設計：支持動態添加新功能  範例擴展：\n   PNG下載器模組  網路監控模組（版權保護）  其他文件類型處理器    問題內容檢測與預防    重複內容   統計：約30%網頁重複  解決方案：哈希值或校驗和檢測    蜘蛛陷阱（Spider Traps）   定義：導致無限循環的網頁結構  預防措施：\n   設置URL最大長度  監控單一網站頁面數異常  手動識別並排除  自定義URL過濾器    垃圾數據   類型：廣告、代碼片段、垃圾URL  處理：識別並排除無價值內容  關鍵要點總結   BFS優於DFS：適合網路爬蟲的廣度遍歷  禮貌性至關重要：避免對服務器造成負擔  優先級管理：提高爬取效率和質量  混合儲存：平衡性能與成本  分布式架構：提升系統擴展性  多層優化：DNS、地理位置、超時控制  容錯設計：確保系統穩定運行  插件化擴展：適應未來需求變化  第四步驟：彙整總結   優秀爬蟲的四大特徵   可伸縮性（Scalability）：能夠處理大規模網路爬取  禮貌性（Politeness）：尊重網站服務器，避免造成負擔  可擴展性（Extensibility）：易於添加新功能和模組  穩健性（Robustness）：能夠處理各種異常情況   未涵蓋的重要議題   服務器端渲染（動態渲染）   問題：許多網站使用JavaScript、AJAX等腳本即時生成鏈接  挑戰：直接下載解析網頁無法獲取動態生成的鏈接  解決方案：在解析網頁前進行服務器端渲染   過濾不需要的頁面   原因：儲存容量和爬取資源有限  方法：使用反垃圾信息組件  目標：過濾低質量和垃圾頁面   數據庫複製和分片   目的：提高數據層的：\n   可用性  可擴展性  可靠性   水平擴展   規模：大規模爬取需要數百甚至數千台服務器  關鍵：保持服務器無狀態   系統核心概念   可用性（Availability）  一致性（Consistency）  可靠性（Reliability）\n這些是任何大型系統成功的核心要素   分析（Analytics）   重要性：任何系統的重要組成部分  用途：數據是系統微調的關鍵要素  內容：收集和分析系統運行數據   關鍵要點  建置可擴展的網路爬蟲並非簡單任務，因為：   網路規模極其龐大  存在各種陷阱和挑戰  需要考慮多個複雜的技術層面",{"id":719,"path":720,"dir":721,"title":722,"description":7,"keywords":723,"body":725},"content:8.2026-plan:1.overview.md","/2026-plan/overview","2026-plan","計畫總覽",[724],"Anthropic courses","  Anthropic courses   Claude Code in Action   Practical walkthrough of using Claude Code to accelerate your development workflow  Share Part 1 - Section 1 : Getting hands on  📢   分享者 ：  _____  |   GitHub ：  _____  🎯   學習目標 ：理解 Coding Assistant 的運作原理，完成 Claude Code 環境設定，學會在專案中加入上下文（context）並進行程式碼修改   What is a coding assistant  Claude Code in action  Claude Code setup  Project setup  Adding context  Making changes  Course satisfaction survey  Share Part 1 - Section 2 : Controlling context  📢   分享者 ：  _____  |   GitHub ：  _____  🎯   學習目標 ：掌握 context 管理策略以維持對話品質，建立 Custom Commands 自動化重複工作，整合 MCP servers 擴展工具能力，設定 GitHub 自動化 code review 流程   Controlling context  Custom commands  MCP servers with Claude Code  Github integration  Share Part 3 - Section 1 : Hooks & SDK  📢   分享者 ：  _____  |   GitHub ：  _____  🎯   學習目標 ：理解 Hooks 機制並實作自訂 hook，識別常見陷阱與注意事項，透過 Claude Code SDK 將 AI 能力整合進自有工具鏈與自動化流程   Introducing hooks  Defining hooks  Implementing a hook  Gotchas around hooks  Useful hooks! / Another useful hook  The Claude Code SDK   Share Part 4 - Section 2 : Claude Code Best Practice  📢   分享者 ：  _____  |   GitHub ：  _____  🎯   學習目標 ：掌握 Explore → Plan → Implement → Commit 工作流，學會有效管理 context window、設定驗證策略，並避免常見失敗模式（如 kitchen sink sessions、over-specified CLAUDE.md）   best-practices   Share Part 5 : Equipping agents for the real world with Agent Skills  📢   分享者 ：  _____  |   GitHub ：  _____  🎯   學習目標 ：理解 Agent Skills 架構與 SKILL.md 設計模式，掌握 Progressive Disclosure 原則，學會將領域知識結構化為可組合、可重用的 skill 資源   Equipping agents for the real world with Agent Skills   Claude 101   Learn how to use Claude for everyday work tasks, understand core features, and explore resources for more advanced learning on other topics.  Part 1 : Organizing your work and knowledge (with Claude Code in Action Share Part 5)  📢   分享者 ：  _____  |   GitHub ：  _____  🎯   學習目標 ：學會使用 Projects 組織工作與知識庫，透過 Artifacts 產出結構化內容，並利用 Skills 擴展 Claude 的能力範圍   Getting better results (AI Fluency)  Introduction to projects  Creating with artifacts  Working with skills  Part 2 - Section 1: Expanding Claude's reach  📢   分享者 ：  _____  |   GitHub ：  _____  🎯   學習目標 ：整合外部工具（Integrations）擴展 Claude 應用範圍，善用 Enterprise Search 搜尋企業內部知識，使用 Research Mode 進行深度資料探索與分析   Connecting your tools  Enterprise search  Research mode for deep dives  Part 2 - Section 2: Putting it all together  📢   分享者 ：  _____  |   GitHub ：  _____  🎯   學習目標 ：了解不同角色（工程師、PM、設計師等）如何在實際工作中運用 Claude，探索 API、Claude Code 等多元使用方式   Claude in action: use-cases by role  Other ways to work with Claude   AI Fluency: Framework & Foundations  Share Part 1 - Section 1 : Introduction to AI Fluency & Framework  📢   分享者 ：  _____  |   GitHub ：  _____  🎯   學習目標 ：理解 AI Fluency 的重要性與必要性，掌握 4D 框架（Description、Delegation、Discernment、Diligence）作為與 AI 有效協作的思維模型   Introduction to AI Fluency  Why do we need AI Fluency?  The 4D framework  Share Part 1 - Section 2 : Deep Dive 1: What is Generative AI?  📢   分享者 ：  _____  |   GitHub ：  _____  🎯   學習目標 ：理解生成式 AI 的基礎運作原理，清楚認識其能力邊界與限制，建立對 AI 輸出的合理預期   Generative AI fundamentals  Capabilities & limitations  Share Part 1 - Section 3 : Deep Dive 1: What is Generative AI?  📢   分享者 ：  _____  |   GitHub ：  _____  🎯   學習目標 ：深入掌握 4D 框架中的 Delegation（判斷哪些任務適合委派給 AI）與 Description（如何精確描述需求以獲得更好的結果）   Delegation & Description  Share Part 2 - Section 1 : Deep Dive 2: Effective prompting techniques  📢   分享者 ：  _____  |   GitHub ：  _____  🎯   學習目標 ：學習有效的 prompting 技巧，理解 Description-Discernment 迴圈如何迭代優化結果，培養對 AI 輸出品質的判斷力（Discernment）與持續改進的紀律（Diligence）   Effective Prompting Techniques  Discernment  The Description-Discernment loop  Diligence  Share Part 2 - Section 2 : Conclusion  📢   分享者 ：  _____  |   GitHub ：  _____  🎯   學習目標 ：總結 4D 框架的完整應用流程，回顧課程重點並討論如何將 AI Fluency 落實到日常工作中   Introduction to Model Context Protocol  Share Part 1 : Introduction  📢   分享者 ：  _____  |   GitHub ：  _____  🎯   學習目標 ：理解 MCP 架構設計與通訊機制，掌握 Tools（model-controlled）、Resources（app-controlled）、Prompts（user-controlled）三種 primitives 的定位與使用時機  Share Part 2 : Hands-on with MCP servers  📢   分享者 ：  _____  |   GitHub ：  _____  🎯   學習目標 ：使用 Python SDK 實作 MCP server，包含 tool 定義、resource 實作、field 型別提示，並透過 MCP Inspector 進行測試與除錯  Share Part 3 : Connecting with MCP clients  📢   分享者 ：  _____  |   GitHub ：  _____  🎯   學習目標 ：實作 MCP client 端整合，理解 client-server 連接模式與 transport 機制，掌握實務整合模式（如 autocomplete、context injection）  Share Part 4 : Assessment and wrap Up  📢   分享者 ：  _____  |   GitHub ：  _____  🎯   學習目標 ：綜合評量 MCP 核心知識，回顧 server/client 開發流程，討論 MCP 在實際專案中的應用場景與最佳實踐",{"id":727,"path":728,"dir":721,"title":729,"description":7,"keywords":730,"body":732},"content:8.2026-plan:2.next.md","/2026-plan/next","延伸探討",[731],"建議延伸探討資源","  建議延伸探討資源  以下資源建議在完成 Anthropic courses 後進一步探討，作為讀書會的進階學習方向。   Prompting Guide — Techniques   為什麼推薦這個資源？  Anthropic courses 中的 AI Fluency 課程介紹了 4D 框架與基礎 prompting 技巧，但涵蓋的技術深度有限。Prompting Guide 是目前社群中最完整的 prompt engineering 技術彙整，涵蓋了從基礎到進階的系統化技巧：    Zero-shot / Few-shot Prompting  — 無範例與少量範例的 prompting 基礎   Chain-of-Thought (CoT)  — 引導模型逐步推理，提升複雜問題的回答品質   Tree of Thoughts  — 系統性探索多條推理路徑，適合開放式問題   ReAct  — 結合推理（Reasoning）與行動（Action）的循環模式，是 AI Agent 的核心設計模式   Retrieval Augmented Generation (RAG)  — 結合外部文件檢索，突破模型知識邊界   Prompt Chaining  — 串接多個 prompt 處理複雜多步驟任務   Reflexion  — 讓模型自我反思並迭代改進輸出  這些技巧能直接強化成員在使用 Claude Code 與 Claude 時的 prompt 品質，也是理解 AI Agent 行為模式的基礎知識。   Agent Skills for Context Engineering   為什麼推薦這個資源？  在 Claude Code in Action 課程中我們會學到 context 管理與 Agent Skills，而這個 repository 則將   Context Engineering  提升到系統設計的層次。它解決的核心問題是：模型的能力不只受限於 token 數量，更受限於 context 的品質 — 包含「lost-in-the-middle」注意力衰減、認知資源稀缺等實際挑戰。  主要涵蓋三大面向：    Foundational Skills  — Context 基礎概念、退化模式（degradation patterns）、壓縮策略   Architectural Skills  — Multi-agent 協作架構、Memory 系統設計、Tool 設計最佳化、Filesystem-based context 管理   Operational Skills  — Context 最佳化技巧、評估框架、LLM-as-Judge 方法論  這個資源特別適合想深入理解「為什麼同樣的 prompt 有時候效果好、有時候不好」的成員，也為未來建構自己的 multi-agent 系統提供了實用的設計模式參考。",{"id":734,"path":735,"dir":736,"title":737,"description":7,"keywords":738,"body":745},"content:8.2026-plan:section1:1.overview.md","/2026-plan/section1/overview","section1","總覽",[739,740,741,742,743,744],"課程資訊","學習目標","對應 Lessons","四份筆記速覽","建議閱讀順序","預告","  課程資訊    課程 ：  Anthropic Courses — Claude Code in Action   章節 ：Part 1 · Section 1 — Getting hands on  📢   分享者 ：  _____  ｜   GitHub ：  _____  學習目標   理解 Coding Assistant 的運作原理，完成 Claude Code 環境設定，學會在專案中加入上下文（context）並進行程式碼修改  對應 Lessons     Lesson  主題  對應筆記    Lesson 1  What is a coding assistant   Coding Assistant 與 Tool Use   Lesson 2  Claude Code in action   Coding Assistant 與 Tool Use   Lesson 3  Claude Code setup   安裝與設定   Lesson 4  Project setup   安裝與設定   Lesson 5  Adding context   加入上下文   Lesson 6  Making changes   進行修改  四份筆記速覽  1. Coding Assistant 與 Tool Use  從   Coding Assistant 三步驟迴圈 （Gather context → Formulate a plan → Take an action）切入 Tool Use 機制，比較四大 CLI Agent（Claude Code / Codex CLI / Gemini CLI / Copilot CLI）的工具能力與社群實測評價。  →   前往閱讀  2. 安裝與設定  四家 CLI 安裝指令、定價比較、社群踩坑經驗，並提供「安裝完之後要做什麼」的 6 步進階指南：CLAUDE.md、必知快捷鍵、CLI 旗標、Alias、社群必裝套件、自訂指令。  →   前往閱讀  3. 加入上下文 Adding Context   /init  指令、CLAUDE.md 三層架構、四大 CLI Agent 的 Context 機制對照，並收錄 6 個真實世界 CLAUDE.md 範例（Boris Cherny、Anthropic 官方、HumanLayer、Snowplow、Bezy、centminmod）與「老手才會寫進指示檔的 10 種內容」。  →   前往閱讀  4. 進行修改 Making Changes  Plan Mode 深度指南（90/10 法則 + 任務分級 Triage）、  /effort  思考深度控制、社群實測 Skill 與套件、Boris Cherny 與 Boris Tane 的真實工作流，以及 Opus 4.6 時代「哪些 prompt patterns 仍有效、哪些已過時」。  →   前往閱讀  建議閱讀順序    先讀 Coding Assistant 與 Tool Use  — 理解 Agent 運作原理與工具呼叫機制   再讀 安裝與設定  — 完成環境建置並掌握必知快捷鍵   接著讀 加入上下文  — 學會用 CLAUDE.md 把 AI 變成「懂專案的同事」   最後讀 進行修改  — 用 Plan Mode 與 Thinking 模式拉高首次正確率  預告   Section 2  將進入   Controlling context ：對話控制、Custom Commands、MCP Servers、GitHub Integration。",{"id":747,"path":748,"dir":736,"title":749,"description":7,"keywords":750,"body":753},"content:8.2026-plan:section1:2.coding-assistant.md","/2026-plan/section1/coding-assistant","Coding Assistant 與 Tool Use",[751,752],"一、什麼是 Coding Assistant？（Lesson 1）","二、Claude Code in Action（Lesson 2）","  一、什麼是 Coding Assistant？（Lesson 1）  【官方】核心概念  Coding assistant 是利用**語言模型 + 工具使用（Tool Use）**來處理複雜程式任務的精密系統。      圖片說明 ：使用者的 Task 進入 Assistant 後，由 Language Model 搭配 Set of tools 執行三步驟迴圈：  Gather context → Formulate a plan → Take an action ，反覆迭代直到完成。     步驟  說明  需要工具？     1. 收集上下文  理解問題、找到相關檔案  是（讀檔、搜尋）    2. 制定計畫  決定解法、規劃步驟  否（純推理）    3. 採取行動  修改程式碼、執行指令  是（編輯、Shell）  【官方】Tool Use 運作原理  語言模型本身只能處理文字——無法讀檔或執行指令。是   tool use  機制讓它們能與外部世界互動。     使用者：「main.go 裡寫了什麼？」\n    ↓\nCoding Assistant 附加 tool 指令 → 送給 Language Model\n    ↓\nLanguage Model 回應：「ReadFile: main.go」\n    ↓\nCoding Assistant 讀取實際檔案 → 回傳內容\n    ↓\nLanguage Model 提供最終答案\n  【官方】Claude 的三大優勢     優勢  說明     處理更困難的任務  Claude 能組合不同工具處理複雜工作，甚至能使用從未見過的工具    可擴展平台  可輕鬆添加新工具，Claude 會隨工作流程演進而適應    更好的安全性  不需索引整個 codebase 即可瀏覽，避免將程式碼發送到外部伺服器  【補充】2026 年四大 CLI Coding Agent     工具  開發商  模型  GitHub Stars  開源  社群定位     Claude Code  Anthropic  Opus 4.6 / Sonnet 4.6（1M context）  ~60K  MIT  「最精準的全自動代理」    Codex CLI  OpenAI  GPT-5.3-Codex / GPT-5.4 mini  67.5K  Apache 2.0  「ChatGPT 生態系的 CLI 延伸」    Gemini CLI  Google  Gemini 3.1 Pro（1M context）  ~60K  Apache 2.0  「超大上下文 + 免費額度」    Copilot CLI  Microsoft  GPT-5.3-Codex / Claude / Gemini 多模型  N/A（  gh  外掛）  部分開源  「GitHub 生態系無縫整合」    IDE 陣營 （Cursor、Windsurf）走的是不同路線——GUI 為主、滑鼠操作、Vibe Coding 導向。本報告聚焦在終端機內的 Agent CLI 工具。\n  開源替代 ：  Aider （41K stars）支援任意 LLM + 本地模型，適合需要完全掌控或合規需求的開發者。   二、Claude Code in Action（Lesson 2）  【官方】Claude Code 的 15 個內建工具     工具  用途   工具  用途    Agent  啟動子代理   Read  讀取檔案   Bash  執行 shell 指令   TodoRead  讀取待辦事項   Edit  編輯檔案   TodoWrite  更新待辦清單   Glob  模式搜尋檔案   WebFetch  取得 URL 內容   Grep  搜尋檔案內容   WebSearch  搜尋網路   LS  列出目錄   Write  寫入檔案   MultiEdit  多處同時編輯   NotebookEdit  編輯 Notebook   NotebookRead  讀取 Notebook     【官方】四個實戰範例     範例  任務  展示的 Tool Use 特色     效能優化  對 Chalk library 做 benchmark + 找瓶頸 + 修復  Bash（benchmark、profiler）+ Edit    資料分析  分析 streaming.csv 用戶資料  NotebookEdit + Bash + Write    UI 樣式  改善 app 的聊天介面和 header  MCP（Playwright）+ Read + Edit    安全審查  發現 Lambda 將 PII 寫入共用 S3 Bucket  Read（Terraform + Lambda）跨程式碼與基礎設施  【補充】四大 CLI Agent 的 Tool Use 能力比較     能力  Claude Code  Codex CLI  Gemini CLI  Copilot CLI    讀取   Read （圖片/PDF/Notebook）  讀取 + 多模態（截圖/圖表）   read_file （圖片/PDF）   read_file   編輯   Edit （精確字串替換）  diff-based   replace   edit_file （diff）   多檔   MultiEdit  + Agent 子代理  沙盒內多檔操作  shell 組合  Agent mode 跨檔   Shell   Bash   OS 沙盒 （網路預設關閉）   run_shell_command   run_in_terminal   搜尋   WebSearch （Brave）  需開啟網路存取   內建 Google Search  透過 MCP   子代理   最多 10 個平行  無明確子代理  有限  Fleet 平行   安全性  subprocess 隔離   OS 層級沙盒 （最嚴格）  類似沙盒  依 IDE 環境  【補充】社群實測評價     工具  社群評價     Claude Code   工具呼叫最精準 。Plan-Act-Observe 迴圈清晰，子代理是 2026 最受讚譽功能    Codex CLI   沙盒安全性最強 。CI/CD 整合最好。但首次正確率（~60-70%）低於 Claude Code（~95%）    Gemini CLI   免費額度最慷慨 。1M+ context 直接塞大量程式碼。但自主執行毅力較低    Copilot CLI   GitHub 生態系無人能敵 （讀 Issue → 修復 → 產生 PR）。多模型可切換 ",{"id":755,"path":756,"dir":736,"title":757,"description":7,"keywords":758,"body":760},"content:8.2026-plan:section1:3.setup.md","/2026-plan/section1/setup","安裝與設定",[759],"三、安裝與設定（Lesson 3-4）","  三、安裝與設定（Lesson 3-4）  【官方】Claude Code 安裝     # macOS (Homebrew)\n   brew   install   --cask   claude-code\n   \n   # macOS / Linux / WSL\n   curl   -fsSL   https://claude.ai/install.sh   |   bash\n   \n   # Windows CMD\n   curl   -fsSL   https://claude.ai/install.cmd   -o   install.cmd   &&   install.cmd   &&   del   install.cmd\n   \n   # 首次執行\n   claude      # 會提示登入 Anthropic 帳號\n    前置需求 ：無（已移除 Node.js 依賴）   帳號 ：Anthropic（Pro        20  /  月預設  S  o  n  n  e  t  4.6  ｜  M  a  x  5  x  20/月預設 Sonnet 4.6 ｜ Max 5x      20/  月預設  S  o  nn  e  t  4.6  ｜  M  a  x  5  x 100/月 / Max 20x $200/月預設 Opus 4.6）  【補充】其他三家安裝   Codex CLI （OpenAI）：     # 已用 Rust 重寫，無需 Node.js\n   curl   -fsSL   https://openai.com/codex/install.sh   |   bash\n   \n   # 或使用 ChatGPT 帳號登入\n   codex      # 支援 device-code sign-in（v0.116.0+）\n   Gemini CLI （Google）：     # npm\n   npx   @google/gemini-cli\n   \n   # Homebrew\n   brew   install   gemini-cli\n   \n   # 首次執行（Google OAuth，無需 API Key）\n   gemini\n   Copilot CLI （GitHub）：     # 安裝 GitHub CLI 擴充套件\n   gh   extension   install   github/gh-copilot\n   \n   # 首次執行\n   gh   copilot      # 需 GitHub 帳號 + Copilot 訂閱\n  【補充】四家定價比較     工具  免費方案  主力方案  重度使用  前置需求     Claude Code  無  Pro $20/月  Max 5x        100  /  20  x  100 / 20x      100/20  x 200/月  無    Codex CLI  無（需 ChatGPT 訂閱或 API Key）  ChatGPT Plus $20/月含  ChatGPT Pro $200/月含  無（Rust 原生）    Gemini CLI   Google 帳號 1,000 次/天；AI Studio 免費 Key 250 次/天  BYOK  Vertex AI 企業  Node.js 18+    Copilot CLI   永久免費 （有限）  Pro $10/月  Pro+ $39/月  GitHub CLI (  gh )    最便宜入門 ：Gemini CLI（免費 1,000 次/天）或 Copilot（永久免費方案）\n  最佳 CP 值 ：Claude Code Max 5x        100  /  月或  C  o  d  e  x  C  h  a  t  G  P  T  P  l  u  s  100/月 或 Codex ChatGPT Plus      100/  月或  C  o  d  e  x  C  ha  tGPTPl  u  s 20/月  【補充】社群踩坑經驗     工具  從零到生產力  常見新手問題     Claude Code  ~15-30 分鐘  「土撥鼠之日」— 沒設 CLAUDE.md 每次重啟都忘；Context 滿了 AI 降智    Codex CLI  ~10-15 分鐘  沙盒太嚴格（網路預設關閉），需手動開啟   danger-full-access    Gemini CLI  ~5 分鐘  模型切換不直覺；免費額度用完無明確提示    Copilot CLI  ~5 分鐘  CLI 能力有限（主要是 shell 命令輔助），真正的 Agent 在 IDE 中    社群共識 ：不管用哪款，  必須建立專案規則檔 （CLAUDE.md / AGENTS.md / GEMINI.md），當成 AI 的「員工手冊」。  【進階】安裝完之後：讓 Claude Code 更有威力  原生 Claude Code 安裝完只是起點。以下是安裝後應該立刻做的事：  Step 1：設定 CLAUDE.md（  /init ）     claude\n   >   /init      # 自動掃描 codebase 生成 CLAUDE.md\n  Step 2：熟記必知快捷鍵     快捷鍵  功能     Shift+Tab   循環切換權限模式 （default → acceptEdits → plan → auto）    Esc  × 2   Rewind ：回溯到之前的時間點    Ctrl+V  貼上截圖    Ctrl+G  在文字編輯器中開啟 prompt    Alt+P  不清除 prompt 切換模型    /  斜線指令    !   直接執行 bash （輸出自動加入 context）    @  檔案路徑自動補全    #  Memory mode（編輯 CLAUDE.md）  Step 3：熟記必知 CLI 旗標     旗標  用途  範例     -p  非互動模式（Piping）   cat error.log | claude -p '解釋錯誤'    -c  繼續最近對話   claude -c    -w  在隔離 git worktree 中工作   claude -w feature-auth    --effort  設定思考深度   claude --effort max    --permission-mode  權限模式   --permission-mode plan （唯讀）  Step 4：設定終端 Alias     # ~/.zshrc\n   alias   c  =  \"claude\"\n   alias   cc  =  \"claude --continue\"\n   alias   cw  =  \"claude --worktree\"\n   alias   cr  =  \"claude --resume\"\n  Step 5：安裝社群必裝套件     套件  用途  為什麼需要      superpowers  強制注入資深工程師思考框架  brainstorm → write-plan → execute-plan 三步工作流     planning-with-files  持久化 Markdown 計畫  計畫不會隨 context 壓縮消失     claude-mem  跨 session 自動記憶  解決「土撥鼠之日」問題     claude-hud  HUD 顯示 context 用量  看到 Claude 在做什麼，不再黑箱     PUA  用職場壓力逼 AI 不放棄  修復成功率 +36%、工具使用 +50%  Step 6：建立自訂指令   .claude/commands/review.md ：     ---\n   name  :   review\n   description  :   安全性 + 效能 code review\n   allowed-tools  : [  \"Read\"  ,   \"Grep\"  ,   \"Glob\"  ]\n   ---\n   Review the code in $ARGUMENTS with focus on:\n   1.   Security vulnerabilities  2. Performance issues  3. Error handling gaps\n   Output findings with severity levels (P0/P1/P2).\n    Awesome Lists 入門必看 ：  awesome-claude-code （32K stars）、  awesome-claude-code-plugins （646 stars）   html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"id":762,"path":763,"dir":736,"title":764,"description":7,"keywords":765,"body":767},"content:8.2026-plan:section1:4.adding-context.md","/2026-plan/section1/adding-context","加入上下文",[766],"四、加入上下文 Adding Context（Lesson 5）","  四、加入上下文 Adding Context（Lesson 5）  【官方】  /init  指令     掃描 codebase → 建立摘要 → 寫入 CLAUDE.md → 每次請求自動包含。  【官方】CLAUDE.md 三層架構       檔案  用途  共享性     CLAUDE.md  由   /init  產生，描述專案架構  提交版本控制，團隊共享    CLAUDE.local.md  個人指示和客製化  不共享    ~/.claude/CLAUDE.md  所有專案通用指示  個人使用  【官方】快捷操作     功能  說明     # （memory mode）  自動將指示合併到 CLAUDE.md    @  提及檔案  自動包含檔案內容到請求中  【補充】四大 CLI Agent 的 Context 機制對照     工具  專案指示檔  自動掃描  機制特色     Claude Code   CLAUDE.md    /init  自動生成  三層架構（專案/本地/全域）+   #  memory mode    Codex CLI   AGENTS.md  /   CODEX.md  無  OS 沙盒內讀取，支援 MCP    Gemini CLI   GEMINI.md  無  JIT 載入，子目錄動態切換    Copilot CLI   .github/copilot-instructions.md  無  路徑拆分（  applyTo  glob），適合大型企業    跨工具趨勢 ：  AGENTS.md  通用標準崛起——多工具開發者推廣單一 AGENTS.md 作為 Single Source of Truth，讓同一個規則檔能被 Claude Code、Codex CLI、Gemini CLI 共用。  【進階】真實世界的 CLAUDE.md 範例    /init  產生的只是起點。真正的威力來自  持續經營 。以下是社群中實際在用的 CLAUDE.md 案例。  範例 1：Boris Cherny（Claude Code 創造者）的團隊 CLAUDE.md    來源     ## Workflow Orchestration\n   ### 1. Plan Mode Default\n   -   Enter plan mode for ANY non-trivial task (3+ steps or architectural decisions)\n   -   If something goes sideways, STOP and re-plan immediately\n   \n   ### 2. Self-Improvement Loop\n   -   After ANY correction from the user: update 'tasks/lessons.md' with the pattern\n   -   Write rules for yourself that prevent the same mistake\n   \n   ### 3. Verification Before Done\n   -   Never mark a task complete without proving it works\n   -   Ask yourself: \"Would a staff engineer approve this?\"\n   \n   ### 4. Autonomous Bug Fixing\n   -   When given a bug report: just fix it. Don't ask for hand-holding\n   -   Go fix failing CI tests without being told how\n   重點 ：他的團隊 CLAUDE.md 有 ~2.5k tokens，「每當 Claude 做錯就寫進去」— 複利經營。  範例 2：Anthropic 官方（anthropic-cookbook）    來源     ## Code Style\n   -   Line length: 100 characters\n   -   Quotes: Double quotes\n   -   Formatter: Ruff\n   \n   ## Key Rules\n   1.   API Keys: Never commit .env files. Use dotenv.load_dotenv()\n   2.   Dependencies: Use   `uv add \u003Cpackage>`  . Never edit pyproject.toml directly.\n   3.   Models: Use current Claude models. Never use dated model IDs\n      (e.g.,   `claude-sonnet-4-6-20250514`  ). Always use the non-dated alias.\n   5.   Quality checks: Run   `make check`   before committing.\n   重點 ：連 Anthropic 自己的 CLAUDE.md 都很精簡，聚焦在「Claude 會犯的具體錯誤」。  範例 3：HumanLayer（ASCII 架構圖一行抵千言）    來源  — 被社群引為「理想的 CLAUDE.md」（僅 57 行）     ## Architecture Flow\n   Claude Code → MCP Protocol → hlyr → JSON-RPC → hld → HumanLayer Cloud API\n                                            ↑         ↑\n                                       TUI ─┘         └─ WUI\n   \n   ## TODO Annotations\n   -   TODO(0): Critical - never merge\n   -   TODO(1): High - architectural flaws, major bugs\n   -   TODO(2): Medium - minor bugs, missing features\n   -   TODO(3): Low - polish, tests, documentation\n   重點 ：一行 ASCII 架構圖取代好幾段文字。TODO 分級讓 Claude 知道什麼該優先處理。  範例 4：Snowplow（領域術語強制）    來源     ## Snowplow terminology\n   -   Always capitalized: Event Studio, Snowtype, Snowplow CDI, Signals\n   -   Never capitalized: entities, events, schemas, data structures\n   -   Use \"entity\", never \"context\". Use \"self-describing event\", never \"unstructured event\".\n   重點 ：  領域術語強制 是 AI 絕對不會自己知道的東西。沒寫進指示檔，它一定會用錯。  範例 5：Bezy 字型編輯器（架構 Gotcha 防炸彈）    來源     ## CRITICAL: TUI Output Protection (HIGHEST PRIORITY)\n   **NEVER write code that outputs to stdout/stderr directly:**\n   -   NEVER use   `println!()`   - breaks TUI\n   -   NEVER use   `eprintln!()`   - breaks TUI\n   \n   ## Key Design Patterns\n   -   Transform vs FontIR:\n     -   Transform = temporary visual display only\n     -   FontIR = permanent font data (what gets saved)\n     -   Common bug: Updating only Transform means changes look correct but won't save\n   重點 ：  架構特有的地雷 。不寫進指示檔，AI 一定會踩到。  範例 6：centminmod（Memory Bank 系統）    來源 （2,100+ stars）     ## Memory Bank System\n   -   CLAUDE-activeContext.md - Current session state, goals, and progress\n   -   CLAUDE-patterns.md - Established code patterns and conventions\n   -   CLAUDE-decisions.md - Architecture decisions and rationale\n   -   CLAUDE-troubleshooting.md - Common issues and proven solutions\n   \n   ## BANNED - Never Use These Slow Tools\n   -   find → use fd or rg --files\n   -   grep → use rg instead\n   -   tree → NOT INSTALLED, use fd instead\n   -   cat file | grep → use rg pattern file\n   重點 ：  Memory Bank 模式 （多個 CLAUDE-*.md 檔案分工）適合大型專案。工具禁令讓 AI 不會用慢的指令。  老手才會寫進指示檔的 10 種內容     #  類型  範例  為什麼新手不會想到    1   常見錯誤排解   $ANDROID_HOME is not set → $HOME/Library/Android/sdk  每次新 session AI 都會踩到的坑   2   TODO 分級標註   TODO(0)  到   TODO(4)  嚴重度  AI 不知道哪個 TODO 該先修   3   ASCII 架構圖   A → MCP → B → API → C  一行取代好幾段描述   4   「Never X, Always Y」  \"entity not context\"、\"println breaks TUI\"  領域特有禁忌，AI 不可能自己知道   5   驗證指令順序  \"run tsc → test → lint → build\"  AI 不知道你的 CI 順序   6   自我改進迴圈  \"被糾正後更新 lessons.md\"  讓 AI 自己學習   7   Memory Bank 檔案   CLAUDE-decisions.md 、  CLAUDE-patterns.md  跨 session 持久化知識   8   工具禁令  \"never use find, use fd\"  AI 會選慢的工具   9   Git 安全規則  \"NEVER git checkout without confirmation\"  防止 AI 破壞性操作   10   AI vs 人類文件分離  \"AI reads CLAUDE.md, humans read README.md\"  避免指示檔污染 README  CLAUDE.md 撰寫原則   來源：  abhishekray07/claude-md-templates    注意力預算 ：Claude Code 的 system prompt 已有 ~50 條指令。模型能遵循約 150-200 條。你的 CLAUDE.md 只剩 ~100-150 個 slot。  每加一行都會讓其他行更不可能被遵循   甜蜜點 ：40-80 行，60 行以下最理想   Matt Pocock 的 plan loop ：加兩行就能大幅改善——「At the end of each plan, give me a list of unresolved questions to answer, if any.」   不要讓 LLM 做 linter 的事 ：縮排、trailing commas、import 排序不該放在指示檔，用 eslint/prettier/ruff   每個禁止都要附替代方案 ：❌ \"Don't use any\" → ✅ \"Don't use   any ; use   unknown  and narrow the type\"   Progressive disclosure ：CLAUDE.md 是索引，docs/ 是圖書館。AI 需要時自己去翻書   社群趨勢 ：    「負面約束」比「正面鼓勵」有效 ：❌ \"We prefer fetch\" → ✅   \"Do not use axios; use native fetch API\"    AGENTS.md  通用標準崛起 ：多工具開發者推廣單一 AGENTS.md 作為 Single Source of Truth   指示檔複利經營 ：每當 AI 犯錯，不只在對話中糾正，  立刻寫進指示檔 。但要  定期修剪 ——過時的規則會浪費注意力預算。大型專案考慮用 Memory Bank（多個 CLAUDE-*.md 動態加載）取代單體式 CLAUDE.md    更多範例 ：  awesome-claude-md  /   centminmod/my-claude-code-setup （2,100+ stars）  【進階】Session 管理心法     指令  用途     /compact  壓縮對話釋放 context    /compact focus on auth  壓縮時聚焦特定主題    /context  查看 context 使用狀況    /cost  監控 token 消耗    /rename  命名 session（方便日後恢復）    /clear  開新 session（舊的保留可恢復）   Context Window 70-75% 是死亡線 ：多個開發者記錄，context 到這個比例時 Claude 開始忘記決定、幻想檔案路徑。超過 80% 就   /compact 。   Handoff Strategy ：長對話讓 AI 降智時，請 Claude 寫   handoff.md （進度 + 架構決定 + 待辦），  /clear  後說「讀取 handoff.md 繼續工作」。  瞬間恢復 AI 智商。   html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"id":769,"path":770,"dir":736,"title":771,"description":7,"keywords":772,"body":774},"content:8.2026-plan:section1:5.making-changes.md","/2026-plan/section1/making-changes","進行修改",[773],"五、進行修改 Making Changes（Lesson 6）","  五、進行修改 Making Changes（Lesson 6）  【官方】使用截圖精確溝通  在 Claude 中貼上截圖使用   Ctrl+V ，讓 Claude 精確理解你要修改的介面區域。（Codex CLI 同樣支援多模態輸入——截圖、草圖、Figma 匯出）  【官方】Planning Mode（廣度）   啟用 ：按   Shift + Tab  兩次     行為    讀取專案中更多的檔案   建立詳細的實作計畫   展示打算做什麼    等待批准後才執行  【官方】Thinking / Effort 模式（深度）  三種等效方式控制 AI 思考深度：     方式  操作      /model  → 選 effort  low / medium / high / max（  最直覺，日常用這個 ）     /effort max  在 session 中持續設定    Prompt 關鍵字   think 、  think hard （社群非正式用法，Claude 會辨識）     Effort Level  約 Token 預算  適用場景     low  最少  簡單問答、樣板程式碼    medium  ~4-6k  日常 Debug、單純重構    high  ~10k  API 設計、資料庫最佳化    max  ~32k  系統遷移、架構重構、深層 Bug   社群舊稱的   ultrathink  就是   /effort max ，不是 GPT 的功能。  【補充】四大 CLI Agent 的 Planning / Thinking 比較     工具  Planning  Thinking / 推理  特殊能力     Claude Code   /plan （唯讀架構師模式）   /effort  控制（唯一明確等級）  交錯思考 + 最多 10 個子代理    Codex CLI  依 o-series 模型推理鏈  o3/o4 原生推理能力   OS 沙盒隔離  + CI/CD 整合    Gemini CLI  ReAct Loop（預設）  Deep Think（1M+ context）  超大上下文跨專案推理    Copilot CLI  Agent Mode  依模型（o1/o3 有推理）  GitHub 生態系整合（Issue→PR）   社群實測誰最強？     面向  勝出者    程式碼品質 & 首次正確率   Claude Code（effort max）   複雜演算法 & Edge Cases   Gemini Deep Think   CI/CD 整合 & 安全沙盒   Codex CLI   GitHub 生態系自動化   Copilot Agent   規劃最安全嚴謹   Claude Code /plan   【進階】Plan Mode 深度指南：從「會用」到「用好」  進入 Plan Mode 的四種方式     方式  說明     Shift+Tab × 2  第一下 = Auto-Accept，第二下 = Plan Mode     /plan  在 session 中輸入    CLI 旗標   claude --permission-mode plan    預設設定   .claude/settings.json  中設   {\"permissions\": {\"defaultMode\": \"plan\"}}  Plan Mode 下 Claude   可以 讀檔、搜尋、瀏覽網路、啟動研究子代理——但  不能 修改任何檔案。  四階段工作流   Phase 1: Explore（Plan Mode）→ 讀取相關程式碼，理解架構\nPhase 2: Plan（Plan Mode + /effort max）→ 產出詳細策略，你 review\nPhase 3: Implement（Shift+Tab 切到 Auto-Accept）→ 按計畫執行\nPhase 4: Commit → 建立描述性 commit\n   社群實測：沒 Plan → 改了 14 檔、壞 3 endpoint、重做 2 次（35+ 分鐘）。有 Plan → 同功能 12 分鐘搞定。  讓計畫品質更好的六個技巧     #  技巧    1   餵 Context 再規劃 ：先   @path/to/file  注入關鍵檔案 +   Ctrl+V  貼截圖   2    /effort max  + Plan Mode ：「終極組合」，讓 Sonnet 達到接近 Opus 推理深度   3   迭代修改計畫 ：  Ctrl+G  在編輯器中直接改（刪步驟、加約束），比對話式更精準   4   要求考慮 Edge Cases ：「考慮 race condition、空值、權限不足」   5   要求提出替代方案 ：「提出 3 種做法並比較 trade-offs」   6   引用計畫執行 ：切模式後說「  嚴格按計畫執行，不加計畫外的東西 」  Plan Mode 90/10 法則   花   90% 時間在 Plan Mode  讀檔、畫架構、列清單；確認後切 Auto-Accept 在   10% 時間 噴完 Code。   ❌ 直接寫 Code → 幻覺迴圈不斷修 Bug\n✅ Plan Mode 迭代到滿意 → Shift+Tab → 一口氣完成\n   注意：不是每件事都需要 90/10 。任務分級（Triage）很重要：     任務類型  建議模式  範例     簡單修改   /effort low  + Auto-Accept  改 CSS 顏色、修 null check、linter 錯誤    中等任務   /effort high  + 快速規劃  單檔重構、加一個 API endpoint    複雜工程   /effort max  + Plan Mode 90/10  跨檔重構、新功能、架構改動   不分級就用   /effort max  做所有事 = API 帳單爆炸。   【進階】10x 效率必裝套件與 Plan 增強工具  Plan 增強 Skills     Skill  用途  實際體驗      superpowers （113K stars）  brainstorm → write-plan → execute-plan  Jesse Vincent 發現   Cialdini 影響力原則對 LLM 有效 ：用「IMPORTANT: This is a real scenario」+ 模擬時間壓力推動 Claude 使用正確工具。  子代理隔離 讓 Claude 能自主工作數小時不偏離     planning-with-files （17K stars）  持久化 3 個 Markdown 計畫檔  Reddit 1,053 讚：「Manus $2B 的秘密就是 3 個 markdown —   task_plan.md  +   findings.md  +   progress.md 。Agent 每個決策前都重讀計畫。」解決   context drift     PUA  用職場壓力逼 AI 窮盡解法  實測 9 場景 18 對照：修復率 +36%、工具使用 +50%。Level 3 強制 7 項清單驅動發現 AI 之前完全忽略的問題    Plannotator （Hooks）  Plan→Execute 攔截器  在 Claude 準備修改檔案前，強迫先生成 PLAN_REVIEW.md 讓你確認   來自老手的警告 （Reddit 447 讚）：   「複雜的 agent 設定很爛。簡單永遠贏。不要複製貼上別人的 skill 當黑箱，理解原理後根據自己需求調整。」  社群推薦：自訂   /architect  指令     ---\n   name  :   architect\n   description  :   深度架構規劃\n   ---\n   1.   使用 /effort max 模式深度思考\n   2.   透過 Web Search 查閱相關依賴最新文件\n   3.   輸出包含 Mermaid 圖表的 ARCHITECTURE_PLAN.md\n   4.   列出風險、edge cases、替代方案\n   5.   等待確認後才 Coding\n   \n   分析以下需求：$ARGUMENTS\n  Git Worktree 平行開發     claude   -w   feature-auth       # Terminal 1\n   claude   -w   bugfix-123         # Terminal 2\n   claude   -w   refactor-api       # Terminal 3\n  Piping 自動化     cat   build-error.txt   |   claude   -p   '解釋這個 build 錯誤的根因'\n   git   diff   main   |   claude   -p   '檢查這些變更有沒有 bug'   >   review.txt\n  Hooks 全自動除錯  在   .claude/settings.json  設定每次 WriteFile 後自動執行   npm run lint --fix  或跑單測。測試失敗 Claude 自動讀錯誤繼續修，直到綠燈。   【進階】真實開發者的故事與教訓  Boris Cherny（Claude Code 創造者）的工作流   同時跑   10-15 個 Claude session ，10-20% 會被放棄（視為正常）   偏好最強模型 + thinking ：「不需要頻繁引導，幾乎總是更快」（Boris 帖子發於 2026/1-2 月，強調原則而非特定模型版本）   永遠先 Plan mode ，迭代到滿意才切 auto-accept  團隊 CLAUDE.md 有 2.5k tokens：「每當 Claude 做錯就寫進去」\n   「每次被糾正後，告訴 Claude：『更新你的 CLAUDE.md，讓你下次不再犯同樣的錯。』Claude 非常擅長為自己撰寫規則。」   給 Claude 驗證方式是最重要的一件事   「要從 Claude Code 得到好結果，最重要的一件事就是——給 Claude 一個驗證自己工作的方式。如果 Claude 有這個反饋迴圈，最終結果的品質會提升 2-3 倍。」    來源  Boris Tane 的「標註迴圈」   Research → Claude 寫 plan.md → 你在編輯器加 inline 標註\n→「處理標註，不要實作」→ 重複 1-6 輪 → 滿意後「全部實作」\n  最強 pattern：  「不要實作（don't implement yet）」 。沒這句 Claude 會規劃到一半就跳去寫 Code。    來源  失敗故事：$417 的教訓   「程式碼到 15K 行時，Claude 變成一直要你重講故事的朋友。第一週 ~       100  建了大部分，最後一週一個動畫修復就  100 建了大部分，最後一週一個動畫修復就      100  建了大部分，最後一週一個動畫修復就 20。」  最崩潰：「找到問題了！」→ 修了 → Bug 還在 →「啊，  真正的 問題是...」→ 循環到懷疑人生    來源  Prompt Patterns：哪些還有效、哪些已過時    Opus 4.6 時代的趨勢 ：模型已原生內建 Effort Level 動態思考機制，不再需要   ultrathink  等咒語。攻擊性語句（「CRITICAL!」「YOU MUST」「NEVER EVER」）在新模型上  反而降低品質 ，簡單直接的指令效果更好。\n  來源   仍然有效的 Patterns ：     Pattern  效果     「不要實作」護欄  防止規劃到一半跳去寫 Code（與模型版本無關）     /effort max  + Plan Mode  取代舊的   ultrathink  關鍵字，效果更穩定    PUA 升級機制  修復成功率 +36%（壓力框架仍有效）   已不建議的 Patterns（Opus 4.6 時代） ：     Pattern  為什麼不建議     反幻覺三重奏 （強制附引用等）  Opus 4.6 幻覺率已大幅下降，過度約束反而限制 AI 發揮    攻擊性語句 （CRITICAL!、YOU MUST）  新模型上  產生更差的結果    複雜角色扮演  把精力放在指示檔和拆解測試步驟上，比寫冗長 prompt 有效  Garry Tan 的 gstack（爭議）   gstack （48K stars）— YC CEO 宣稱 60 天 60 萬行。TechCrunch 報導：批評者說行數是虛榮指標，支持者說工作流結構（強制規劃 → review → QA）確實有用。  貫穿所有成功故事的唯一共同點    把規劃和執行分開，給 AI 反饋迴圈。 \n拿到最好結果的開發者都是拒絕讓 AI 在有 review 的計畫之前寫 Code。   【進階】十大效率技巧總結     #  技巧  效果    1   CLAUDE.md 複利經營  每次犯錯都寫進去，越用越聰明   2   Plan Mode 90/10  90% Plan + 10% Execute，避免幻覺迴圈   3    /effort max  按任務複雜度調整推理深度   4    -w  worktrees  3-5 個平行 Claude session   5   superpowers / planning-with-files  Plan 增強，計畫不隨 context 消失   6    Esc+Esc  rewind  回溯錯誤，不需重新開始   7   Hooks 自動化  WriteFile 後自動 lint/test，全自動除錯   8   Handoff Strategy  降智時寫 handoff.md → /clear → 恢復   9    -p  piping  整合到腳本、CI/CD、git hooks   10   PUA + 簡潔直接的 Prompt  逼 AI 不放棄 + Opus 4.6 時代簡單指令效果更好   html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"id":776,"path":777,"dir":778,"title":737,"description":7,"keywords":779,"body":782},"content:8.2026-plan:section2:1.overview.md","/2026-plan/section2/overview","section2",[739,740,780,781,743,744],"本章涵蓋主題與分享範圍","三份筆記速覽","  課程資訊    課程 ：  Anthropic Courses — Claude Code in Action   章節 ：Part 1 · Section 2 — Controlling context  📢   分享者 ：  \"spyua\"  ｜   GitHub ：  \"spyua\"  學習目標   掌握 context 管理策略以維持對話品質，建立 Custom Commands 自動化重複工作，整合 MCP servers 擴展工具能力，設定 GitHub 自動化 code review 流程  本章涵蓋主題與分享範圍     子主題  對應筆記  本次分享    Controlling context   對話控制與上下文管理  ✅   Custom commands   Custom Command 與 Skill  ✅   MCP servers with Claude Code   MCP servers  ✅   GitHub integration  —  🔀 獨立分享  三份筆記速覽  1. 對話控制與上下文管理  掌握   Ctrl+C / Rewind /   /compact  /   /clear  四個操作的使用時機與決策樹，讓 Claude 在出現誤解或效率下降時能精確調整對話脈絡。  →   前往閱讀  2. Custom Command 與 Skill  從   .claude/commands/  的 Custom Slash Command 八大組成區塊，到新一代 Skill 機制（自動載入、frontmatter 控制、檔案目錄支援）的決策對比與最佳場景。  →   前往閱讀  3. MCP（Model Context Protocol）  理解 MCP 作為「AI 的 USB-C」如何標準化 Agent ↔ Tool 協議，從 Agent 三層架構切入 CLI vs MCP 的根本差異，並以 Playwright MCP 為例展示完整 feedback loop。  →   前往閱讀  建議閱讀順序    先讀 Controlling context  — 對話控制是基本功，掌握後才能在偏離時即時修正   再讀 Custom Command 與 Skill  — 把重複任務結構化，提高協作效率   最後讀 MCP servers  — 擴展 Claude 能力到瀏覽器、資料庫、雲端等外部工具鏈  預告   GitHub integration  將由分享者另以獨立場次分享，本章節內不收錄。",{"id":784,"path":785,"dir":778,"title":786,"description":787,"keywords":788,"body":795},"content:8.2026-plan:section2:2.controlling-context.md","/2026-plan/section2/controlling-context","對話控制與上下文管理","核心目標： 掌握四個操作來中斷、回退、壓縮、清空對話，讓 Claude 在出現誤解或效率下降時，能精確調整對話脈絡。",[789,790,791,792,793,794,294],"為什麼需要這些操作？","1. Ctrl+C / Esc — 立即中斷","2. Esc+Esc / Rewind — 回退到歷史對話節點","3. /compact — 壓縮上下文","4. /clear — 徹底清空對話","決策流程表","   核心目標：  掌握四個操作來中斷、回退、壓縮、清空對話，讓 Claude 在出現誤解或效率下降時，能精確調整對話脈絡。  為什麼需要這些操作？  在長時間的開發過程中，Claude 可能出現以下情況：   開始往錯誤方向執行（例如改錯了檔案）  對需求的理解逐漸偏離正軌  上下文累積過多，導致回應變慢或不準確  對話脈絡已經污染到無法恢復  這四個上下文操作指令跟快捷鍵能解決AI失焦的問題。   1. Ctrl+C / Esc — 立即中斷  何時使用  當 Claude   正在執行錯誤的操作  時，不要等它做完，直接按   Ctrl+C  或   Esc  中斷。  典型場景 A：改錯檔案   情境： \n你原本要求：   請幫我調整 login API 的錯誤訊息，不要動認證流程。  但 Claude 開始輸出：   我會先重構 AuthService，調整 JWT 驗證邏輯，並修改 TokenProvider...   症狀：  它已經偏離你的需求，準備動認證核心流程，而不只是改錯誤訊息。   動作：  按 Ctrl+C 停止，然後補充：   不要，只改錯誤訊息就好，AuthService 不要動。\n  典型場景 B：一次做太多，範圍失控   情境： \n你問：   幫我看一下這個 Controller 有沒有問題。  但 Claude 回：   我會順便整理 Controller、Service、Repository，並補上 DTO、Mapper、Exception Handler 和測試。   症狀：  原本簡單的 code review 變成全系統重構計畫。   動作：  按 Ctrl+C，重新說明範圍：   先只檢查 Controller，不用做其他優化。\n  提醒    早發現，早中斷  — 一旦發現方向不對，別等著看它做完，直接中斷   搭配 Plan Mode  — 預先用 Plan Mode 確認任務列表，基本上能避免「範圍失控」   中斷後要補字  — Ctrl+C 停下後補充需要澄清正確方向   2. Esc+Esc / Rewind — 回退到歷史對話節點  何時使用  當對話已經走歪、Claude 誤解了需求、或你想   回到某個對話點  時，用 Rewind。  工作原理  Rewind 會：   打開對話歷史選單（checkpoint 列表）  讓你選擇回退到哪個訊息  從那個點開啟   新的對話分支 （fork）   不改動  你專案裡的實際檔案內容    Rewind 列出了所有可用的對話節點（checkpoint），編號從最早到當前的對話。    當我選擇測試-2對話節點。\u003C此時你可以問是否有測試3,4,5.基本上會回沒有>  對話 Fork 示意  假設你的對話流程是：   ① 你：我要做登入功能\n② Claude：我建議用 JWT\n③ 你：測試1\n④ Claude：開始根據「測試1」理解任務\n⑤ 你：不是，我其實要 OAuth\n⑥ Claude：好，那我改成 OAuth\n⑦ 你：再加 Keycloak\n⑧ Claude：好，我會整合 Keycloak\n  如果在第 ⑧ 訊息後，你意識到第 ④ 訊息時 Claude 已經誤解，你可以選擇   Rewind 到第 ③ 訊息之前 。   原本對話流程：   ① → ② → ③(測試1) → ④ → ⑤ → ⑥ → ⑦ → ⑧\n   Rewind 後（新分支開啟）：   ① → ② → ③'(新的第三訊息)\n  Claude 會「遺忘」第 ④ 到 ⑧ 的所有內容，重新等待你的下一個指示。  Rewind 有三個選項  當你在 Rewind 主選單選定了一個節點，會進入確認畫面：    Restore 選單提供三個選擇，各有不同效果。  選項 1：Restore conversation   還原   對話內容和檔案變更  就像時間回溯，對話回到那個時間點，已經修改過的檔案不會被還原   場景：  Claude 對需求理解走偏，但你想保留目前檔案內容，只想讓對話脈絡回到乾淨狀態。例如幫我把登入失敗時的錯誤訊息調整得更清楚，不要動驗證流程。  選項 2：Summarize from here   只還原   對話脈絡 ，不改程式碼  Claude 會產生一份「對話摘要」，包含：\n   你的原始需求（Primary Request）  關鍵技術概念（Key Technical Concepts）  涉及的檔案列表（Files）  遇過的錯誤（Errors）  問題解決過程（Problem Solving）  所有使用者訊息（All user messages）  待辦任務（Pending Tasks）  當前工作狀態（Current Work）    Summarize from here 選項有助於保留核心理解，同時開啟新分支。    摘要內容詳細，方便你快速把握對話核心，避免重新重複。  選項 3：Never mind   取消 Rewind 操作，維持現在狀態  當你按錯了或改變主意，用這個選項回到原對話    選擇 Never mind 會取消 Rewind，對話和檔案都維持不變。  Rewind 實戰範例    使用者在終端機送出訊息，Claude 記錄每一次互動。  假設你想驗證 Claude 的記憶，可以問：   我有說過「測試5」嗎？\n    Claude 正確回答「沒有」，表示它準確追蹤了對話歷史。  Rewind 的重點總結     特性  說明     對話狀態  Fork 成新分支，舊分支保留但不會用    程式碼檔案  除非選 Restore conversation，否則不變    上下文  Summarize 會幫你保留核心內容，避免重複    何時用  對話誤解、需求澄清、想回到乾淨決策點   💡   提示：  Summarize from here 是最常用的選擇，它既能保留核心理解，又能開啟新分支不被舊對話污染。   3. /compact — 壓縮上下文  何時使用  當對話變得   很長  但你仍想   保留任務理解  時，用   /compact  來壓縮上下文。  工作原理   /compact  會：   將過往對話和細節壓縮成摘要  保留核心任務理解、關鍵決策、已修改檔案列表  刪除冗餘細節（例如多次嘗試的失敗記錄）   不建立新分支 ，而是在原對話上進行壓縮  適用場景   對話已經進行 50+ 訊息，但核心任務還沒完成  Claude 的回應開始變慢（因為上下文過長）  你想「清理雜訊」但不想 fork 對話  比 Rewind 更溫和的「上下文清理」  與 Rewind 的差異     操作  是否建立新分支  檔案變更  何時用     Rewind  是（fork）  Summarize 不改  對話已誤解，需要重來    /compact  否  維持原狀  對話太長，但方向正確   ⚠️   注意：    /compact  適合「同一任務進度長對話」，Rewind 適合「對話方向有誤」。   4. /clear — 徹底清空對話  何時使用  當   對話脈絡已經太髒 、無法修復或無須保留時，直接   /clear  全新重開。  工作原理   /clear  會：   刪除所有對話歷史  重置上下文  保留   專案檔案內容 （程式碼不變）  完全重新開始，就像開啟一個新 Claude Code 工作階段  適用場景   對話路線分岔太多，無法用 Rewind 修復  上下文已經「污染」到無法信任 Claude 的理解  想在同一專案上換個新的對話軌跡  /compact 無法解決的「上下文混亂」  與 /compact 的差異     操作  保留內容  新增複雜度     /compact  對話摘要 + 上下文  仍有壓縮記憶    /clear  無（完全重開）  無（乾淨重開）   決策流程表  用這個表來快速判斷該用哪個操作：     症狀  應該用...  原因     正在執行錯誤操作（改錯檔案、超出範圍）   Ctrl+C  立即停止，最快的中斷方式    對話已經走歪，但程式碼還沒改太多   Rewind + Summarize  Fork 新分支，保留核心理解    對話很長但方向正確，只是上下文太長   /compact  壓縮雜訊，加快回應速度    對話脈絡徹底混亂，無法修復   /clear  完全重開，最激進的方案  常見決策樹   1. Claude 正在做壞事嗎？\n   ├─ 是 → Ctrl+C（停下來）\n   └─ 否 → 進入 2\n\n2. 對話方向還對嗎？\n   ├─ 不對 → Rewind（回到乾淨點）\n   └─ 還可以 → 進入 3\n\n3. 對話太長了嗎？\n   ├─ 是，但方向對 → /compact（壓縮）\n   ├─ 是，完全亂了 → /clear（重開）\n   └─ 不長 → 繼續對話\n   小結  掌握這四個操作，你就有了「對話意圖調整工具箱」：     工具  速度  保留程度  複雜度     Ctrl+C  最快  保留一切  最簡單    Rewind  快  可選（Fork）  中等    /compact  中  保留摘要  中等    /clear  慢  刪除全部  最簡單   核心心法：   問題越早發現越好用 Ctrl+C  想保留過程就用 Rewind  想加速就用 /compact  完全重開就用 /clear  習慣這四個操作，你會發現跟 Claude 的對話品質會大幅提升，因為你能在出錯時迅速調整方向，而不是被迫與誤解的 AI 繼續糾纏。",{"id":797,"path":798,"dir":778,"title":799,"description":7,"keywords":800,"body":809},"content:8.2026-plan:section2:3.custom-commands.md","/2026-plan/section2/custom-commands","Custom Command 與 Skill",[801,802,803,804,805,806,807,808,294],"什麼是 Custom Slash Command？","目錄結構：.claude/commands/","Custom Command 的 8 大組成區塊","實戰範例：Review PR Command","如何呼叫 Custom Command","Custom Command 與 Skill 的關係","Command vs Skill：決策對比表","何時用 Command，何時用 Skill？","  什麼是 Custom Slash Command？  在 Claude Code 中，你可以把常用的工作流程定義成自訂斜線命令（Custom Slash Command），例如   /audit 、  /write-test 、  /review-pr 。當你需要執行這些任務時，直接呼叫對應的命令，Claude 會按照你預先定義好的步驟進行操作。   與單純提示詞捷徑的差異：  Custom Command 不只是簡單的文本替換，而是把整個工作流程結構化，包含輸入參數、執行步驟、驗證方式等，讓 Claude 能更精確地執行你設計的任務。   💡   為什麼需要 Custom Command？   提高重複任務的執行效率  確保同一類型任務的一致性  可帶參數進行靈活的自訂  支援專案規範和最佳實踐的推行  目錄結構：.claude/commands/  Custom Command 的定義檔存放在   .claude/commands/  目錄下，每個   .md  檔代表一個命令。    圖 1：命令檔的組織方式。audit.md、write_tests.md 和 review_pr.md 分別對應 /audit、/write_tests 和 /review_pr 命令。  Custom Command 的 8 大組成區塊  一個設計完善的 Custom Command 應該包含以下區塊：     區塊  說明  必要性     任務目標  明確告訴 Claude 這個 command 要完成什麼  必要    輸入參數  使用   $ARGUMENTS  接收使用者輸入，支援靈活自訂  常用    執行步驟  列出 Claude 要依序執行的具體動作  必要    專案規範  補充測試、命名、架構、匯入路徑等規則  強烈建議    工具使用方式  指示 Claude 何時讀檔、查詢、執行測試或修改程式  視任務而定    輸出格式  定義 Claude 如何格式化回報結果  強烈建議    驗證方式  指定要執行的測試、lint、build 或 type check  強烈建議    失敗處理  當測試失敗或資訊不足時的處理方式  進階建議  實戰範例：Review PR Command  以下是一個完整的   review_pr.md  範例，展示如何整合上述 8 個區塊：     # Review PR\n   \n   請根據以下目標進行程式碼審查：\n   \n   $ARGUMENTS\n   \n   ## 任務目標\n   \n   你審查目前變更內容，找出設計、可讀性、安全性、測試覆蓋與維護性問題。\n   \n   ## 執行步驟\n   \n   1.   檢查 git diff。\n   2.   理解這次變更的目的。\n   3.   找出可能的 bug。\n   4.   檢查是否違反專案架構規範。\n   5.   檢查是否缺少測試。\n   6.   檢查是否有安全性風險。\n   7.   提出具體修改建議。\n   \n   ## 審查重點\n   \n   請特別注意：\n   -   是否有破壞既有 API 行為\n   -   是否有 null pointer / undefined 風險\n   -   是否有權限驗證缺漏\n   -   是否有不必要的耦合\n   -   是否有重複邏輯\n   -   是否缺少測試案例\n   -   是否有過度設計\n   \n   ## 輸出格式\n   \n   請用以下格式回報：\n   \n   ### 總結\n   簡短說明這次變更的整體品質。\n   \n   ### 必修問題\n   列出一定要修的問題。\n   \n   ### 建議改善\n   列出可以改善但不阻塞合併的問題。\n   \n   ### 測試建議\n   列出建議補上的測試。\n  如何呼叫 Custom Command  基本呼叫   /review_pr\n  帶參數呼叫   /review_pr focus on auth and permission risk\n  此時   $ARGUMENTS  會被替換為   focus on auth and permission risk ，Claude 便會在審查時重點關注權限驗證與安全風險。   提示：參數可以多行，支援複雜的指引與上下文。  Custom Command 與 Skill 的關係  官方說明：「custom commands have been merged into skills」  Claude Code 的最新版本將 Custom Command 整合到 Skill 機制中。這並不意味著   .claude/commands/  目錄被廢棄，而是引入了更強大的 Skill 系統。  新舊寫法對比  舊寫法：Command（.claude/commands/deploy.md）     # Deploy\n   \n   Deploy the application to production.\n   \n   ## 執行步驟\n   \n   1.   Run tests\n   2.   Build the application\n   3.   Deploy to production\n   4.   Report the result\n  新寫法：Skill（.claude/skills/deploy/）   .claude/skills/deploy/\n├── SKILL.md           # 任務定義（frontmatter + 內容）\n├── scripts/\n│   └── deploy.sh      # 可選的輔助腳本\n└── docs/\n    └── runbook.md     # 詳細文件\n  SKILL.md 可包含 frontmatter 控制自動觸發行為：     ---\n   name  :   deploy\n   description  :   Deploy application to production\n   category  :   deployment\n   ---\n   \n   # Deploy\n   \n   按照以下步驟部署應用...\n  Skill 的優勢    自動載入能力 ：Claude 可在相關情境自動判斷是否載入該 Skill，而非需要手動呼叫   檔案目錄支援 ：可組織多個檔案（腳本、文件、範例等）   Frontmatter 控制 ：透過元資料控制觸發條件、分類、依賴關係等   更好的文件化 ：支援獨立的文件目錄與範例  Command vs Skill：決策對比表     功能特性  Custom Command  Skill     觸發方式  手動呼叫   /命令名稱  手動呼叫或自動載入    自動載入  否  是（可透過 frontmatter 控制）    Frontmatter 控制  不支援  支援（name、description、category 等）    檔案目錄支援  單一 .md 檔  目錄結構（SKILL.md + 腳本 + 文件）    參數傳遞   $ARGUMENTS   $ARGUMENTS  + 額外變數支援    最佳應用場景  明確的、頻繁手動呼叫的任務  長期、多面向、可複用的能力  何時用 Command，何時用 Skill？  使用 Custom Command 的時機  ✓ 任務明確且場景單一  \n✓ 開發者會經常主動呼叫  \n✓ 任務流程相對簡單，不需要複雜的檔案結構  \n✓ 新手想快速體驗自訂命令的效果   例：    /review-pr 、  /audit 、  /format-code  使用 Skill 的時機  ✓ 能力較為複雜，涉及多個檔案或腳本  \n✓ 希望 Claude 能在相關情境自動觸發（不限手動呼叫）  \n✓ 任務需要配套文件、範例、最佳實踐  \n✓ 能力會在多個專案間複用  \n✓ 需要更精細的元資料控制（分類、依賴、觸發條件等）   例：  完整的部署流程、安全審計工具、重構工作流程  小結   Slash Command 是入口，Skill 是可被重複使用與擴充的能力包。  核心要點    Custom Command  是在   .claude/commands/  下定義的工作流程指令，支援參數化和結構化設計   8 大組成區塊 （任務目標、參數、步驟、規範、工具使用、輸出、驗證、失敗處理）是設計高品質命令的指導原則   Skill 是新一代機制 ，整合了 Command 的功能並加入自動載入、frontmatter 控制和檔案目錄支援   既有 Command 仍可運作 ，但建議新建能力時優先採用 Skill 模式   選擇標準很清楚 ：簡單明確的任務用 Command，複雜可複用的能力用 Skill  無論選擇哪種方式，關鍵都在於  結構化設計 和  清晰的指引 ，這樣 Claude 才能確實理解你的意圖並精確執行。   html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"id":811,"path":812,"dir":778,"title":813,"description":7,"keywords":814,"body":824},"content:8.2026-plan:section2:4.mcp-servers.md","/2026-plan/section2/mcp-servers","MCP servers",[815,816,817,818,819,820,821,822,823,294],"什麼是 MCP？","From Chat to MCP：AI Agent 的演進","Agent 的三層架構：MCP 落在哪一層？","CLI vs MCP：根本差異","Playwright MCP Server：瀏覽器自動化變成 AI 的眼睛","安裝與權限設定","Playwright MCP 工作流程","實戰範例：用 Playwright MCP 改善「元件生成 Prompt」","探索其他 MCP Servers","  什麼是 MCP？   MCP（Model Context Protocol）是一種讓 AI Agent 接外部工具的標準介面。  簡單來說，Claude Code 原本沒有某些能力——例如操作瀏覽器、查詢資料庫、打 API 測試、連接雲端服務。MCP 就是一套標準化的協定，讓 Claude 可以「安全、有序、可管控」地呼叫這些外部工具。  Claude Code 加 MCP 前後能力對比     原本 Claude Code 可能做不到  加 MCP 後可以做到    直接操作瀏覽器  可以用 Playwright 控制瀏覽器   連資料庫查資料  可以透過 DB MCP Server 查資料   打 API 測試  可以透過 API 工具做測試   操作雲端服務  可以透過雲端 MCP Server 串接   做 UI 自動化測試  可以用 Playwright MCP   核心價值 ：MCP 不是「又一個 API wrapper」——它是第一個為 Agent 設計的標準化協定，涵蓋 discovery、auth、context、雙向通訊，打破了不同廠商之間「各自為政」的狀態。   From Chat to MCP：AI Agent 的演進    MCP 的出現不是憑空而生，而是 AI 能力進化的必然結果。看看時間軸就一目了然：     里程碑  時間  特點  限制     Chat  2022.11  純文字對話，ChatGPT 問世  只能說，不能做    Function Calling  2023.6  LLM 輸出結構化指令，OpenAI 率先推出  能動手了，但各家不同標準    Agent  2024  自主推理 + 多步驟串接，AutoGPT、LangChain 爆發  LLM ↔ Tool 的橋接，每個專案各自解決    MCP  2024.11  標準化 Agent ↔ Tool 協議，Anthropic 開源   統一標準，AI 的 USB-C   關鍵演進 ：從「只會說」→「能動手」→「有多步驟」→「有標準介面」。前三個階段都在解決「能做什麼」，MCP 第一次真正解決「安全有序地做」。   Agent 的三層架構：MCP 落在哪一層？    要理解 MCP 的位置，必須認識 Agent 系統的三層分工。同一個 Agent 任務，每層各司其職——搞清楚層次，才能選對工具。     層級  角色  職責  內容  工具示例     Reasoning Layer  大腦  決定「要做什麼」  LLM 理解語意、規劃步驟、判斷工具  Claude / GPT / Gemini、System Prompt、Context Window    Orchestration Layer  記憶  定義「怎麼做」  預設工作流程、multi-step 模板、程序記憶  MCP Prompt Primitive、Agent Skills、Workflow Templates    Execution Layer  手腳  實際「做動作」  觸碰真實系統：資料庫、API、檔案系統  MCP Tools (governed)、CLI Commands (efficient)、Direct API Calls  為什麼要分三層？    Reasoning Layer ：天生隨機，這是 feature，不是 bug。LLM 的推理能力、創意、靈活應變都來自於這層的「不確定性」   Orchestration Layer ：標準化常用路徑，減少 token 浪費，加速工作流程   Execution Layer ：MCP 在這層提供治理與安全，讓外部工具呼叫變成「安全的、可控的、可稽核的」    MCP 在 Execution Layer，但影響三層 ：MCP 本身提供工具描述、權限邊界與可控的工具介面，所以它不只是執行層的工具，也是治理的中樞。   CLI vs MCP：根本差異    「Agent 也會叫 CLI——但 MCP 讓 Agent 知道有哪些工具、怎麼用、能拿到哪些資料。」這是 MCP 相比傳統 CLI 最重要的區別。     比較項目  CLI（Agent 叫命令去「做事」）  MCP（讓 Agent 知道「有什麼」）     執行模型  Agent 擬制指令 → 執行 CLI 讀 stdout 字串  Agent 直接呼叫 → 圍繞、結構化呼叫 API    Discovery  ❌ 沒有，Agent 需事先知道有什麼指令  ✅ 自動，Server 啟動時主動告知所有 Tool schema    回應格式  非結構化文字（stdout / stderr）  結構化 JSON、type、metadata    認證授權  依賴 shell 環境、原作者本地機制  OAuth 2.1 + Identity Propagation 標準化    雙向通訊  不友善，CLI 不太回應給 Agent  ✅ 支援，Server 可反向 Sampling / Elicitation  關鍵差異解釋   Discovery 的重要性 ：   CLI 時代，Agent 只能「猜」有哪些命令，或者靠開發者提前告訴它  MCP 時代，Server 啟動時直接宣告「我有這些工具、這些參數、這些返回類型」，Agent 可以自動「發現」能力邊界   Structured Context 的重要性 ：   CLI 回傳純文字，Agent 要靠自然語言推理「結果是什麼」  MCP 回傳結構化 JSON，Agent 直接讀取 type、metadata，有效減少誤解   Playwright MCP Server：瀏覽器自動化變成 AI 的眼睛  什麼是 Playwright？   Playwright  本來是一套常見的「瀏覽器自動化測試工具」，用於做 UI 自動化測試、爬蟲、截圖等任務。  當 Playwright 接到 Claude Code（透過 MCP），Claude 就可以做這些事：   打開瀏覽器，前往任意 URL（例如 localhost:3000）  點擊按鈕、輸入文字、瀏覽頁面  看實時畫面長什麼樣子  檢查 UI 元素、驗證邏輯是否正常  根據實際畫面結果回頭修改程式碼    白話講 ：Claude 不再只是「猜你的 UI 會長怎樣」，而是可以「真的打開來看」。  為什麼 Playwright MCP 很重要？  開發迴圈變短了。傳統流程是「寫程式碼 → 自己開瀏覽器看 → 反饋給 Claude → 再改」，現在 Claude 可以自己完整走過一遍，甚至自動驗證效果。   安裝與權限設定  安裝指令拆解  在你的  終端機 （Terminal）執行以下指令——  注意：不是在 Claude Code 對話框裡輸入 ：     claude   mcp   add   playwright   npx   @playwright/mcp@latest\n  這條指令做了兩件事：     區塊  意思     claude mcp add  告訴 Claude Code：我要新增一個 MCP Server    playwright  為這個 MCP Server 命名為   playwright （後續用來識別）    npx @playwright/mcp@latest  在你本機啟動這個 Server 的指令   ⚠️   注意 ：這條指令是在 OS 終端機執行，目的是把 Server 註冊到 Claude Code，而不是在對話內呼叫工具。  首次使用：權限提示  當你第一次叫 Claude 使用 Playwright MCP 工具時，Claude 會跳出權限提示（每次操作都會問一次）：   是否允許 Claude 使用這個工具？\n  因為這些工具會操作瀏覽器、讀取頁面、執行動作，涉及實際系統行為，需要明確同意。  預先核准：避免每次都被問  如果你不想每次都被問，可以  預先核准整個 MCP Server ，做法是編輯   .claude/settings.local.json ：     {\n     \"permissions\"  : {\n       \"allow\"  : [  \"mcp__playwright\"  ],\n       \"deny\"  : []\n     }\n   }\n  加進   allow  陣列後，Claude 使用 Playwright 的所有工具就不會再跳提示。  「兩個底線」很重要   mcp__playwright  中間是   兩個底線 （double underscores），不是一個：   ✅ 正確：  mcp__playwright  ❌ 錯誤：  mcp_playwright （一個底線會吃不到權限規則）  命名約定：    mcp  = 這個工具來自 MCP Server   __  = MCP 命名空間分隔符   playwright  = Server 名稱（對應   claude mcp add  時取的名字）   Playwright MCP 工作流程    一個典型的 Claude + Playwright 開發迴圈看起來像這樣：   1. Claude 打開 localhost:3000（你的 Web App）\n   ↓\n2. 操作你的 Web App（點按鈕、輸入文字、滾頁面）\n   ↓\n3. 產生一個新的 React 元件或修改現有元件\n   ↓\n4. 用 Playwright 瀏覽器觀察實際畫面效果\n   ↓\n5. 判斷「效果是否滿足需求」或「UI 是否夠好看」\n   ↓\n6. 如果不滿意，修改 generation.tsx 裡的 Prompt 或邏輯\n   ↓\n7. 再次產生元件，回到第 4 步驗證效果\n   ↓\n8. 迴圈直到滿意為止\n  這樣的 feedback loop 比傳統開發快得多——不需要人工介入確認，Claude 可以自動迭代。   實戰範例：用 Playwright MCP 改善「元件生成 Prompt」  這是一個實務上很常見的場景。傳統做法是：你寫一個 Prompt 讓 Claude 產生 React 元件，產出來覺得不夠好，再手動回去調整 Prompt，反覆來回好幾輪。  加上 Playwright MCP 後，整個流程可以這樣自動化：    Claude 打開瀏覽器 進到你的應用  產生一個測試元件   觀察視覺風格與程式品質  根據觀察結果  更新生成 Prompt  用新 Prompt 再產一個元件，  驗證改進效果  舉例你可以這樣對 Claude 說：   Navigate to localhost:3000, generate a basic component, review the styling, and update the generation prompt at @src/lib/prompts/generation.tsx to produce better components going forward.  Claude 會用 Playwright 工具操作你的應用、檢視產出的元件、然後直接修改你的 Prompt 檔案，引導未來生成更原創、有設計感的版本。  實際成果差異  關鍵優勢是：  Claude 看得到實際視覺輸出，不只是程式碼 。所以它能做出有畫面感的判斷，而不是猜的。  實務上這種做法常見的改進方向例如：     改進前（常見「無聊」風格）  改進後（Claude 觀察後的調整建議）    千篇一律的紫到藍漸層  暖色夕陽漸層（橘 → 粉 → 紫）   標準 Tailwind 配色  海洋深度主題（teal → emerald → cyan）   對稱、規矩的排版  非對稱設計、元素重疊   一致的間距  創意性的留白與非傳統佈局   💡   重點 ：這類改進不是 Claude 「想出來」的，而是它  看到 現在的 UI 太普通、再回頭調 Prompt——這是文字 AI 做不到的事。   探索其他 MCP Servers  Playwright 只是 MCP 生態裡的一個例子。隨著 Anthropic 開源 MCP 後，整個生態已經涵蓋很多面向：     類別  用途  適用場景     Database 互動  連 PostgreSQL、MySQL、SQLite 查資料  讓 Claude 直接讀資料、查 schema、跑分析    API 測試與監控  Postman 風格的 API 呼叫、回應檢視  開發 / 整合測試自動化    檔案系統操作  跨目錄讀寫、批次重命名、結構整理  專案重構、自動化整理    雲端服務整合  AWS、GCP、Azure 等控制平面  DevOps、Infra 自動化    開發工具自動化  GitHub、GitLab、Jira、Linear、Slack  PR 管理、issue 追蹤、團隊協作  挑選 MCP Server 時，建議從你  日常工具鏈中最花時間 的部分開始，例如：   每天都要跑 SQL 查報表 → 安裝 DB MCP  常常需要在 Slack 查訊息再回到程式碼 → Slack MCP  PR review 流程冗長 → GitHub MCP  裝上對的 MCP Server，Claude 就會從「程式助手」進化成「能操作整個工具鏈的開發夥伴」。   小結  MCP 的核心價值    統一標準 ：AI 的 USB-C，打破廠商之間的「各自為政」   安全有序 ：透過 discovery、auth、權限治理，讓 Agent 呼叫外部工具變成可控的   減少浪費 ：Orchestration Layer 預先規範常用路徑，減少推理浪費 token   雙向通訊 ：不只 Agent → Tool，Tool 也能反向給 Agent 反饋  Playwright MCP 的意義  Playwright MCP 是 MCP 生態中最直觀的例子——它讓 Claude 從「文本型 AI」進化成「能看、能做、能驗證」的 Agent，開發迴圈變成了「自動化 feedback loop」。  Claude 看得到實際視覺輸出 ，所以能做出有畫面感的判斷，而不只是猜程式碼會長怎樣。  從「程式助手」到「開發夥伴」  裝上不同的 MCP Server，Claude 就能直接跨足你的整個工具鏈——資料庫、API、雲端、CI/CD、團隊協作。挑你日常最耗時的環節先導入，效果最明顯。  展望  MCP 剛開始，已經有企業推出 Notion MCP、Google Workspace MCP、Slack MCP 等。未來幾年，MCP 會成為 AI 能力擴展的標準基礎設施，就像 HTTP 對網際網路一樣。  當你看到 AI 能力越來越強、應用越來越廣，背後很大一部分功勞就是 MCP 這樣的標準化協定。  html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",1777539271134]