<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[fcamel的程式開發心得 - Medium]]></title>
        <description><![CDATA[Notes about software development. - Medium]]></description>
        <link>https://medium.com/fcamels-notes?source=rss----fc4919ea89c8---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>fcamel的程式開發心得 - Medium</title>
            <link>https://medium.com/fcamels-notes?source=rss----fc4919ea89c8---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 24 May 2026 02:26:50 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/fcamels-notes" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[AI Code Editor 使用心得]]></title>
            <link>https://medium.com/fcamels-notes/ai-code-editor-%E4%BD%BF%E7%94%A8%E5%BF%83%E5%BE%97-812851644b2b?source=rss----fc4919ea89c8---4</link>
            <guid isPermaLink="false">https://medium.com/p/812851644b2b</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[software-development]]></category>
            <dc:creator><![CDATA[fcamel]]></dc:creator>
            <pubDate>Thu, 02 Jan 2025 05:59:57 GMT</pubDate>
            <atom:updated>2025-01-02T07:02:19.562Z</atom:updated>
            <content:encoded><![CDATA[<p>隨著 2022 底 ChatGPT 問世以來，寫程式的模式有了重大轉變：</p><ul><li>先是用 ChatGPT 生成代碼片段，人工手動複製貼到 IDE 裡。</li><li>隨後 GitHub Copilot 的進步，在 IDE 內可以即時自動補完程式碼。</li><li>最近一年來，各家廠商陸續<strong>「嵌入 ChatGPT 到 IDE 裡」</strong>，提供描述需求，由 AI 跨多個檔案整合修改，再<strong>由人閱讀後核準變更。</strong></li></ul><p>最新的開發方式，也有人稱為 Agent，也就是讓 AI 代理人類進行工作。</p><h3>整合 Prompt 開發的影響</h3><p>現代 AI IDE（如 <a href="https://codeium.com/windsurf">Windsurf</a> 或 <a href="https://www.cursor.com/">Cursor</a>）的一大特色是其基於 prompt 的整合。開發者簡單描述想法，即可在幾分鐘內獲得 prototype。</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*l1bwnESOycR1yYKnQPwGAQ.png" /><figcaption>寶可夢猜數字</figcaption></figure><p>上面的例子是用 Windsurf 作出來的，要使用者猜正確的圖案和順序。空格是猜的圖案，右側是猜的歷史記錄。</p><p>整個開發過程包含試玩大概一小時。過程中我不需寫任何程式，只依序作了以下描述：</p><ul><li>寫個 web game “Bulls &amp; Cows”，但是用圖片取代數字 ( 我有先備好圖片 )</li><li>提供輸入介面，讓我能設定圖片和空格數量</li><li>微調選擇圖片和取消選擇的方法</li><li>微調歷史記錄顯示方式</li><li>加上猜中後的煙火動畫，並且微調動畫效果</li></ul><h3>個案分析</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/884/1*jPYdNa7Q65f0o9jPe0YOpA.gif" /><figcaption>寶可夢加法</figcaption></figure><p>這是針對熱愛寶可夢小朋友的加法遊戲，上圖過程發生的事:</p><ol><li>輪入錯數字時，雷射會被彈開</li><li>答對黃色方塊後會引發大範圍消除</li><li>紫色是「魔王」，有三條命</li></ol><p>和遊戲相關的雜記：</p><ul><li>邊作邊改，能動的第一版只需三分鐘。整體累計開發時間大概 10h 左右</li><li>一開始的版本只有方塊，為了提升受測者的興趣，之後才補上寶可夢圖案</li><li>整個遊戲就一個網頁共 963 行</li><li>支援兩人連線對戰，需要執行跑 79 行的 python script 作為 server</li></ul><p>以下針對用到的技術各別討論。</p><h4>CSS 動畫</h4><p>我並不是 CSS 專家，動畫從來不在我的考慮內，<strong>但 AI IDE 輕易地擴展了「我的能力」。</strong>現在我只要描述我要的效果， AI 會自動產生需要的 HTML + CSS + JavaScript。</p><h4>JavaScript 控制遊戲流程</h4><p>我有寫程式的概念，也大概知道 JavaScript 語法，但要我從頭寫 web game，大半的心力會耗在語法錯誤或低能的 bug 上。AI Auto Completion 解決了這些問題，<strong>Prompt 更進一步提供更高層級的整合修改，減少因為各別修改而漏改的缺失。</strong></p><p><strong>例子一</strong>：黃色方塊會炸掉範圍內的方塊，還有紫色的「魔王」方塊都是事後用 prompt 補上的。</p><p><strong>例子二</strong>：整個遊戲作好後我才想到要補上寶可夢圖案，我只需下 prompt 說：</p><ul><li>我的圖片放在目錄的什麼位置，請在方塊下面隨機選張圖來畫</li><li>加上線段讓方塊看起來像氣球</li><li>圖片要往上跳一點再用 45 度角往牆壁方向墜落</li></ul><p>收到 prompt 後，<strong>AI 會同時修改 HTML、JavaScript 和 CSS</strong>，並說明改的過程。</p><h4>多人網頁遊戲</h4><p>AI IDE 另一大好處是填補知識空白的部份。以往要製作多人網頁遊戲時，需要了解 <a href="https://developer.mozilla.org/zh-TW/docs/Web/API/WebSocket">WebSocket</a> 和串接 WebSocket 的後端框架 (例如 <a href="https://fastapi.tiangolo.com/">FastAPI</a>)。如今只要一句「支援多人連線的 web game」，AI 自動生成可用的例子，可以試用滿意後再深入了解必要的知識。</p><h3>開發心得</h3><h4>起始的 prototype 很重要</h4><p>在用 Windsurf 嘗試了近十個迷你專案 ( 都是 web 互動小遊戲 ) 後，發覺關鍵是拿捏好 MVP (Minimum Viable Product)。例如我有次忘了提要作 web game，Windsurf 將判斷邏輯移到 python server，讓專案變得複雜。我要求改回純 web game 後，仍產生了不必要的檔案，後面 Windsurf 開始修改無關的檔案，很難拉回正軌。索性整個重來比較快。</p><p>和以往最大的差別是：用 AI IDE 加速了整個開發循環，因此發覺方向錯誤時，更願意砍掉重來。</p><h4>幫 AI 收斂修改的範圍</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*sbLWgvuZuojp5IyPKrYmcw.gif" /><figcaption>用動畫展示十進位和二進位的加法進位</figcaption></figure><p>這個例子是展示加法進位的過程，上圖是正確的結果。</p><p>原有的問題是：進位後「剩下的方塊」太早掉落，我想將這個步驟移到最後再播放。但 AI 無法正確地維護和時序相關的狀態，最後的解法是我先讀懂程式碼，將問題聚焦在很小的範圍，直接指示要作的事情，AI 才能修對。</p><p>下的 prompt 和 Windsurf 展示的修改如下圖：</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*F17n6HB1pqkynFfsj-3CfA.png" /><figcaption>修改動畫順序問題</figcaption></figure><p>剛開始我的角色是專案經理，要想好 MVP。隨著專案邏輯變複雜，我的角色轉為資深工程師，<strong>要縮小範圍並提供提示指導 AI 完善細節，而 AI 解放了我耗在細節的心力。</strong></p><h4>一步一步來</h4><p>在修改寶可夢加法時，原本只有方塊沒有圖片，我一開始在一個 prompt 裡要求加上圖片又描述改變動畫效果，結果作出不能動的東西。接著拆細步驟，先說要補圖片，微調好圖片大小和位置。接著再說要改動畫，收斂修改的範圍，AI 就能執行得很精確。</p><h4>Typing 對 AI 很有幫助</h4><p>除了閒暇玩 Windsurf 的經驗外，工作上我大量使用 GitHub Copilot 維護既有的專案，發覺 typing 相當地有幫助。</p><p>我只需大概知道 TypeScript，就可以維護 TypeScript 的專案。隨著 typing 愈寫愈明確後，重構的正確性也有所提升。VS Code + Copilot 重構 TypeScript 的效果和 IntelliJ IDEA 重構 Java 差不多穩定。</p><h3>總結</h3><p>使用 AI IDE 不僅能以 2x ~ 10x 的速度完成工作並提高品質，更讓我呀異的是 <strong>AI 擴展了我能完成的工作範圍</strong>。</p><p>對開發者來說，最重要的事和以前一樣：<strong>專注於釐清需求和解決問題，在理解整體系統的前提下，引導 AI 在正確的範圍內解決問題。</strong></p><p>雖然開發者仍需對所用技術有一定的基礎知識才能發揮 AI 的能力，但透過和 AI 問答從例子中學習的方式，20 ~ 60 分的門檻幾乎快被 AI 推平了。相當好奇十年後會迭代出什麼樣的學習和開發的模式呢？</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=812851644b2b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/fcamels-notes/ai-code-editor-%E4%BD%BF%E7%94%A8%E5%BF%83%E5%BE%97-812851644b2b">AI Code Editor 使用心得</a> was originally published in <a href="https://medium.com/fcamels-notes">fcamel的程式開發心得</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[AWS Redshift 設計架構筆記]]></title>
            <link>https://medium.com/fcamels-notes/aws-redshift-%E8%A8%AD%E8%A8%88%E6%9E%B6%E6%A7%8B%E7%AD%86%E8%A8%98-7ffddc018880?source=rss----fc4919ea89c8---4</link>
            <guid isPermaLink="false">https://medium.com/p/7ffddc018880</guid>
            <category><![CDATA[data-warehouse]]></category>
            <category><![CDATA[distributed-systems]]></category>
            <dc:creator><![CDATA[fcamel]]></dc:creator>
            <pubDate>Sat, 01 Jul 2023 16:02:37 GMT</pubDate>
            <atom:updated>2023-07-01T16:07:30.883Z</atom:updated>
            <content:encoded><![CDATA[<p>這篇是 “AWS re:Invent 2019: Deep dive and best practices for Amazon Redshift (ANT418)” 前半的讀後心得。</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2Flj8oaSpCFTc%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3Dlj8oaSpCFTc&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2Flj8oaSpCFTc%2Fhqdefault.jpg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/10cdc680af64afebc36e39026759470f/href">https://medium.com/media/10cdc680af64afebc36e39026759470f/href</a></iframe><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YEqlX0PymzVvVGbgrOsa3Q.png" /></figure><p>Redshift 的用途是 Data Warehouse，主要用作資料分析，強調平行計算大量資料 (PB 等級)，語法相容 PostgreSQL。</p><h3>Architecture: Computation and Storage</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*O4we0gi5EM_HmzkWSzyhHw.png" /></figure><p>Client 連到 Leader node 由 Leader node 發包請求，算是滿直覺的架構。只要資料之間沒有關聯性，就能讓 Compute nodes 同時處理。</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Kno12Mmz_ghYQCc_5tagWg.png" /></figure><p>Redshift 後來提供 Redshift Managed Storage (RMS)，分離 Compute node 和資料，透過這層 <a href="https://medium.com/fcamels-notes/level-of-indirection-%E7%9A%84%E5%8F%8D%E6%80%9D-d750bd7a47d8">indirection</a>，Client 可以享受動態增長資料上限的功能。以往則是得選擇使用特化計算或儲存空間的機器類型。就我的理解，這樣 Client 比較不會因為空間不足要加機器，而是因算力不足才要加機器。</p><p>後來 2021 出現的 Redshift Serverless，更進一步提升加機器的彈性，變成設定好一組機器對應固定的單位時間計算量，下 SQL 後才開起來使用，最低以一分鐘為單位收費。或許 2019 RMS 有在為 Redshift Serverless 佈局吧？</p><h3>Columnar Architecture</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*10ol89iTQulzRnzucHdv4g.png" /></figure><p>以「欄的方式儲存資料」可說是 data warehouse 的基本作法吧，多數 SQL 只需查詢部份欄位，在這樣的儲存方式下，大幅減少需要從硬碟讀取的資料量。</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4ZM8gp3i7iES13a7AumFjw.png" /></figure><p>更進一步，同一欄的資料格式一致，可以用更特化的壓縮方法。比方說上圖中的 loc (location) 的類型不多，可以用 BYTEDICT 壓縮，這個作法會另建一個 index 將數字對到名稱，在 256 個類型以內的情況下，只需 1 byte 即可表示。</p><p>注意 columnar architecture <a href="https://stackoverflow.com/a/32088418">無法建立傳達 RMDBS 的 index</a>，只能設 distribution key 和 sort key。</p><h3>Block, Zone maps, Sort key</h3><p>為了減少從硬碟取資料，Redshift 將資料以 Block 為單位儲存在硬碟裡。</p><p>Block:</p><ul><li>Column data is persisted to 1 MB immutable blocks</li><li>Blocks are individually encoded with 1 of 13 encodings</li><li>A full block can contain millions of values</li></ul><p>並且在 memory 裡存著每個 block 的 min/max 值，稱為 zone maps。於是可以透過 zone maps 濾出必須讀取的 blocks。</p><p>最後則是將 blocks 依 sort key 排序，如下圖所示：</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oEVVFr0Os5sjkdtF48OQJg.png" /></figure><p>於是在過濾條件有用到 sort key 時，可以直接跳過不相干的 blocks，如下圖所示：</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RQP8GUOGI5Su3SNIjIn5DA.png" /></figure><p>通常會依時間分析的資料，滿適合設定時間欄位為 sort key。這樣在計算週平均、月平均、年平均等數據時，只會讀到該時間範圍內的 blocks。</p><h3>Data Distribution</h3><p>前面提到如何最小化要讀取的資料，再來要考慮如何最大化同時讀取必須讀取的資料。</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zJnMcNpf4UXZsaFdLMGpHQ.png" /></figure><p>共有四種分配資料到 Node 的方法。在討論這些方法前，先談一下 Slice。</p><p>Redshift 在每個 Node 多加了一層 <a href="https://medium.com/fcamels-notes/level-of-indirection-%E7%9A%84%E5%8F%8D%E6%80%9D-d750bd7a47d8">indirection</a>: Slice，每筆資料分配到 Slice 而非 Node。依先前讀 <a href="https://medium.com/fcamels-notes/amazon-dynamodb-%E6%A6%82%E5%BF%B5%E7%AD%86%E8%A8%98-72a7636f2a79">DynamoDB</a>、<a href="https://medium.com/fcamels-notes/redis-%E5%92%8C-redis-cluster-%E6%A6%82%E5%BF%B5%E7%AD%86%E8%A8%98-fdc19a3117f3">Redis Cluster</a> 的心得，推測多了 Slice 會比較好搬資料。當某個 Node 硬碟快滿了，Redshift Managed Storage (RMS) 應該會自動補新的 Storage Node，然後將部份 Slice 搬到新的 Node。</p><p>我猜 Slice 會有固定上限大小，這樣才能確保搬移一個 Slice 花費時間的上限。印象中 DynamoDB 是限制 10GB，在 10Gbps 的的網路下，理論上搬動一個單位資料容器只需 10 秒上下。</p><p>接著回來談分配資料的方法，預設是 EVEN，採用 round robin 方式填資料，是最平均的作法。若想最大化平行取資料的速度，理論上都用 EVEN 就好了。那為什麼還要有其它方式？</p><h3>Collocated Join</h3><p>為了節省儲存空間，通常會有 Fact Table 和圍繞在 Fact Table 旁的 Dimension Tables (統稱為 <a href="https://en.wikipedia.org/wiki/Star_schema">Star Schema</a> )。透過 join 兩者，取得額外的資料。</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/998/1*P9PSWoGMpIWGazbBsm3RWA.png" /><figcaption><a href="https://en.wikipedia.org/wiki/Star_schema#/media/File:Star-schema.png">https://en.wikipedia.org/wiki/Star_schema#/media/File:Star-schema.png</a></figcaption></figure><p>PB 級的資料自然無法全部存在一個 Node 裡，此時如何將有效率地取得 join 所需的資料，可以大幅降低要讀取的資料。</p><p>若 Fact Table 和 Dimension Table 採用同一個欄位作為 distribution key，可以保證兩個 table 要合併的 data row 位在同一個 Slice。因此，只要 distribution key 不會造成嚴重的 data skew ( 過多資料集中在同個 Slice)，採用 KEY 的分配方式比 EVEN 綜合來說更有效率。</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HXAf6jvWxt3eafzWscjIuQ.png" /></figure><p>除 KEY、EVEN 外，ALL 表示全部資料放到全部 Node 的第一個 Slice。AUTO 表示先用 ALL，等資料量變大後再改用 EVEN。</p><h3>中途小結</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kZoeCWBr5cXWlOwGjQosBA.png" /></figure><p>這頁總結架構的優點和基於此架構的正確使用方式。比較有趣的一點是 “Avoid distribution keys on temporal columns”。前面提過 columnar architecture 只能設 distribution key 和 sort key，其中 distribution key 用作 collocated join，sort key 用來只取必要的 blocks。因此，若將時間欄位設成 distribution key，結果變成 data skew 很嚴重，比方說只取特定一天的資料，變成全部計算都在單一 Node 上，無法享受分散式系統的優勢。</p><p>想像一個例子是記錄使用者購買記錄，用帳號當 distribution key；日期當作 sort key。若要取最近 30 天前 100 名購買總金額最多的人，Redshift 會讓<strong>全部 Node 同時在各自 Node 用日期選出範圍內的 blocks</strong>，然後找出同一 Node 的金額欄位，加總算出再匯集排序得到結果。</p><p>DynamoDB 一樣有 distribution (partition) key 和 sort key，不過使用情境和用法就完全不同。DynamoDB 重視單筆讀寫立即返回結果，Redshift 則善用平行處理。</p><h3>Data storage, Ingestion, 和 ELT</h3><p>ELT 和 ETL 的差別是資料先進 Data Warehouse 再處理 (ELT) 或是先處理完再進 Data Warehouse 或是 Database (ETL)。關鍵在於 Dare Warehouse 具備強大的平行處理能力，所以資料量很大的時候，用 ELT 會比較有效率。</p><p>這部份講的概念滿直覺的，這裡摘要一下：</p><ul><li>每更動 5G 資料或 8h 後，會建一份新的 snapshot 到 S3</li><li>Temporary table 沒有備份，所以適合當作 staging area 操作</li><li>預設每個 SQL 是一個 transaction，如果要作一連串的修改，明確地用 BEGIN / COMMIT 包起來會比較有效率</li><li>支援直接從 S3 匯入資料到 Redshift。可以用 S3 當作一連串 ELT 的中繼點</li><li>採用 immutable block 的設計，因此 UPDATE = DELETE + INSERT。在高效率的系統，很常見這種 immutable 的設計。例如 <a href="https://medium.com/fcamels-notes/log-structured-file-systems-4520f26fb8e4">log structured file systems</a></li><li>Redshift 會自己找適合的時機作 vacuum 釋放已刪除的 blocks，通常沒必要自己手動觸發 vacuum</li></ul><p>後面就不描述細節，大致上投影片寫的滿清楚的。</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AlQ2W1K4jx9gpfESrcgeNA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GtNzws_f7fNdPIa2CjqmJQ.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_3JrJ8aJcRv9yCFtKc_6bA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*d14SxSzrDe-QBsy2LEBz2w.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*89ruv51Csg9EvFVQzvdHhA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ebFvi30aueMLn8dWcUvy6w.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XrcGwtcX-FkLGJMhkuBTUg.png" /></figure><p>特別提一下上面這頁投影片，這裡提供完整的 UPSERT 例子，綜合使用了前述的概念：</p><ul><li>用 TEMP table 作為 staging table 避免不必要的背景操作</li><li>從 S3 載入更新</li><li>用刪掉舊資料再寫入新資料的作法達到 UPSERT</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VsrvaiB0X8a1InvEI2NgBQ.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1fDhUD7GCXBAOvuJDODmdw.png" /></figure><h3>結語</h3><p>這樣反覆消化 Redshift 的設計後，有比較清楚 Data Warehouse 和 Database 的區別。</p><p>在高效能的需求下，會看到重覆的設計模式不斷的出現，例如：</p><ul><li>(Redshift, Redis Cluster) 使用 immutable block 作為最小單位，方便搬資料</li><li>(Redshift, DynamoDB) 使用 indirection 方便動態擴充計算和儲存的節點</li><li>(Redshift, DynamoDB, Redis) 使用 distribution key 降低相依性，從而達到完全平行處理</li><li>(Redshift, DynamoDB) 使用 sort key 濾出必要的資料</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7ffddc018880" width="1" height="1" alt=""><hr><p><a href="https://medium.com/fcamels-notes/aws-redshift-%E8%A8%AD%E8%A8%88%E6%9E%B6%E6%A7%8B%E7%AD%86%E8%A8%98-7ffddc018880">AWS Redshift 設計架構筆記</a> was originally published in <a href="https://medium.com/fcamels-notes">fcamel的程式開發心得</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Level of Indirection 的反思]]></title>
            <link>https://medium.com/fcamels-notes/level-of-indirection-%E7%9A%84%E5%8F%8D%E6%80%9D-d750bd7a47d8?source=rss----fc4919ea89c8---4</link>
            <guid isPermaLink="false">https://medium.com/p/d750bd7a47d8</guid>
            <category><![CDATA[software-development]]></category>
            <dc:creator><![CDATA[fcamel]]></dc:creator>
            <pubDate>Tue, 27 Jun 2023 14:18:54 GMT</pubDate>
            <atom:updated>2023-06-27T14:35:31.915Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VQkdft8yG_UYBSrwwcknjw.png" /><figcaption>Generated by MidJourney: /imagine A technological masterpiece encased in transparent glass, a computer reveals its electronic soul. The vibrant motherboard, a cityscape of capacitors and transistors, sits below spinning fans, humming softly. Glistening metal drives nestle nearby, archiving information silently. This glass sanctuary echoes an orchestra of silent, synchronized computing. — v 5</figcaption></figure><p>在計算機科學有這麼一句名言：</p><blockquote>All problems in computer science can be solved by another level of indirection</blockquote><p>甚至可以開玩笑地說，如果一層間接層無法解決，就再加一層！這個說法表達了抽象、間接性和解決問題的本質。</p><h3>間接性 (Indirection) 的威力</h3><p>間接性的核心是將兩個模組或計算方法解耦，使一個組件的變化不會影響到另一個組件。這個概念構成了模組設計的骨幹，從而堆疊出更強大的系統。</p><h4>耳熟能詳的實例：DNS</h4><p>在瀏覽器中輸入網址時，DNS 服務器將網址的網域 ( 例如 <a href="http://www.google.com">www.google.com</a> ) 換為 IP 地址，這是網站在 Internet 上的實際位置。這個轉換過程提供了一個間接層。管理者可以更新 DNS 記錄指向新的伺服器，而不需要要求所有使用者更新網站的位置。因此，管理者可以先更新好未上線服務的伺服器內容，再替換 DNS 指向更新好內容的伺服器，作到無縫更新網站內容。</p><h4>GeoDNS：具有地理感知的 DNS</h4><p>和傳統的 DNS 相比，GeoDNS 會依使用者所在的地理位置改變同一個網域查到的 IP。例如，歐洲的使用者嘗試訪問網站時，GeoDNS 可以將他們導向歐洲的伺服器，而不是北美的服務器，減少載入網站的時間。</p><p>在這個例子裡，間接性層不僅使系統更有彈性，還提升了效能。</p><h4>Anycast：多個位置，一個地址</h4><p>Anycast 又加了一層間接層。在 Anycast 網路中，多個伺服器可以共享同一個 IP 地址。當一個使用者嘗試訪問這個 IP 時，網路會將使用者導向距離最近的伺服器。</p><p>DNS 和 Anycast 的間接層還有其它好處，例如分散伺服器的負擔和提升可靠度，就不再詳述細節。</p><p>總結來說，DNS、GeoDNS 和 Anycast 展現出間接性的額外優點：不僅是解耦實作細節提供模組間的彈性，還能縮短反應時間和提升系統穩定性。</p><h4>其它例子</h4><p>諸如 Docker container、程式語言 (Go、Java、Python) 的 virtual machine (byte code) ，或是 Web Framework (React、Django、RoR) 等，都運用了間接層的概念。</p><h3>間接層造成除錯的困境</h3><h4>以 GeoDNS 和 Anycast 為例</h4><p>假設你正在運行一個使用 GeoDNS 和 Anycast 的全球服務。有一天，某些使用者回報服務變慢或無回應。這些使用者分布在全球各地，而且問題並不一致。一些使用者遇到問題，而其他人則沒有。</p><p>由於涉及到多個間接層，除錯會更為複雜：</p><ol><li><strong>GeoDNS：</strong>由於 GeoDNS 會依使用者地理位置導向最近的服務器，因此可能是最接近報告問題的使用者的伺服器有問題。但是，要確定這一點有些困難，因為你的位置（或你監控服務的位置）的 DNS 查詢可能會被導向不同的伺服器。</li><li><strong>Anycast：</strong>由於使用者會被導向在最近的服務器，因此有可能使用者導向一個正在遇到問題的服務器。網路路徑可以動態變化 ，可能很難重現使用者遇到的情況。</li></ol><p>在這兩種情況下，因為不容易重製一樣的問題，開發者不容易鎖定應用程式的錯誤。也許這個錯誤就剛好發生在和間接層另一側的連續互動 ( 比方剛好同一個使用者端向 DNS 連查兩次卻查到不同 IP)， 也有可能實際的問題不在應用程式本身，例如是 GeoDNS 的 bug！</p><p>為了有效地除錯這些問題，你需要了解這些系統的底層行為，比典型的應用程序除錯複雜得多。</p><h4>以 ORM 為例</h4><p>ORM 本質上提供了一個間接層，使處理資料庫像處理代碼中的物件一樣簡單。它們抽象了許多 SQL 和資料庫管理細節，提供更簡單的操作方式，例如 myTable.save()就能視情況轉成 INSERT或是 UPDATE的 SQL。</p><p>假設你需要用 ORM 刪除數千行資料。ORM 提供了一個簡單的函數來執行此操作，直接呼叫即可，一切看起來都很正常。然而，當操作開始運行時，你可能會發現它花費的時間異常地長，極端的情況下，甚至不可能完成。</p><p>為了排查問題，你必須深入了解幕後發生了什麼。也許你會發現 ORM 竟然產生了 10,000 個物件，再用迴圈走訪所有物件呼叫 delete，從而產生 10,000 的 DELETESQL，這個作法額外浪費了 10,000 個物件的記憶體，並且呼叫 DELETE10,000 次，對應用程式本身和資料庫都相當地無效率。實際上，你可能只需要下一個 DELETE 就能善用資料庫的 index 或是 table scan 刪除 10,000 筆資料。</p><h3>The Law of Leaky Abstraction</h3><p>Joel Spolsky 對此現象提出一個精闢的說法：<a href="https://www.joelonsoftware.com/2002/11/11/the-law-of-leaky-abstractions/">The Law of Leaky Abstraction</a>：</p><blockquote>All non-trivial abstractions, to some degree, are leaky.</blockquote><p>該法則認為，所有非微不足道的抽象，一定程度上都會「漏洞百出」，也就是說，它們不能完全封裝它們試圖隱藏的複雜性。</p><p>從 ORM 例子中可以看出，抽象化讓操作資料庫像是呼叫物件方法一樣，但是當在大規模操作中 (例如刪除數千行資料)，你開始看到抽象的限制或 “漏洞”。ORM的簡單刪除功能對於較小的任務來說很好，但對於大規模操作來說，並不是很有效。</p><p>因此，你被迫理解底層資料庫索引和解讀 SQL 的機制，以弄清楚為什麼基於ORM 的操作對這種例子並不有效。你發現自己不得不剝去抽象的層次，以便在更基本的層面上解決問題 — — 直接寫出對資料庫友善的 SQL。</p><p>本質上，The Law of Leaky Abstraction 強調：無論我們的工具和抽象層提供多麼高層級的功能，總會有情況需要深入到更低層的複雜性中解決某些問題。</p><h3>總結</h3><p>儘管間接層 在管理複雜性和提高效率方面提供了巨大的價值，它們也存在潛在的問題，例如加深除錯困難度或是增加團隊成員上手的門檻。從除錯的角度來說，可以看成另一種「技術債」( 用了框架卻「還沒」了解框架怎麼運作 )。</p><p>並不是說我們應該避免間接層，而是作好必須理解底層系統的準備。哪怕是系統上線出事時，或是交接找不到熟悉該套框架的人等忙碌時刻。因此，謹慎地考慮使用間接層，避免增加不必要的風險因子。</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d750bd7a47d8" width="1" height="1" alt=""><hr><p><a href="https://medium.com/fcamels-notes/level-of-indirection-%E7%9A%84%E5%8F%8D%E6%80%9D-d750bd7a47d8">Level of Indirection 的反思</a> was originally published in <a href="https://medium.com/fcamels-notes">fcamel的程式開發心得</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[快速學習的方法]]></title>
            <link>https://medium.com/fcamels-notes/%E5%BF%AB%E9%80%9F%E5%AD%B8%E7%BF%92%E7%9A%84%E6%96%B9%E6%B3%95-fad95b4f73ac?source=rss----fc4919ea89c8---4</link>
            <guid isPermaLink="false">https://medium.com/p/fad95b4f73ac</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[learning]]></category>
            <dc:creator><![CDATA[fcamel]]></dc:creator>
            <pubDate>Sun, 28 Feb 2021 03:17:46 GMT</pubDate>
            <atom:updated>2021-02-28T07:03:04.149Z</atom:updated>
            <content:encoded><![CDATA[<p>出社會以來，我一直有個疑問：要當通才還是專才？這個探索過程有很多值得分享的細節，有時間再整理成文章。直到在 CloudMosa 工作的某天，聽老闆說：「通才不是什麼都會一點，而是能在各領域快速成為專才」、「最重要的兩個能力是<strong>學得快</strong>和<strong>完成目標 (Get Sh*t Down)</strong>」 (上述非原文，是中文大意)。我才豁然開朗。</p><p>最近剛好看到劉謙用<a href="https://www.youtube.com/watch?v=caihpbonExU">「三週零基礎學給愛麗絲」</a>為例，說明如何快速學一個新領域的東西。這是我看過最簡短清楚的說明，推薦一看。</p><p>摘要他的說明如下：</p><ul><li>定非常<strong>明確的目標</strong> (會彈給愛麗絲 vs. 會彈鋼琴)：學得快才會有興趣，照基礎慢慢作容易磨掉熱情。</li><li>拆解：找出完成目標<strong>必要學習</strong>的部份，將完成目標的步驟拆成多個<strong>短時間內可完成</strong>的步驟 (認琴鍵位置、練某個段落)</li><li>刻意練習：每天<strong>固定時間針對弱點練習</strong>。刻意練習本身有不少書可參考，比方說…<a href="https://www.books.com.tw/products/0010752714">《刻意練習》</a>。不想花太長時間看書，可以參考<a href="https://www.youtube.com/watch?v=ebwmwDtEZO0">啾啾鞋的摘要</a>。</li></ul><p>等完成這目標後，之後察覺有必要補基礎時，一樣用目標導導向的方式回頭補需要的部份。</p><h3>實戰就是最好的練習</h3><p>看了劉謙的影片後，讓我理解實戰有效的原因，因為實戰通常會有明確待解的問題，時間也比較緊迫，自然會目標導向思考。</p><p>大家都在實戰，為什麼長久下來學習速度會有差異？我猜可能和以下幾點有關：</p><ul><li>是否<a href="https://youtu.be/ebwmwDtEZO0?t=435">常逼自己在學習圈</a>？</li><li>是否有系統地思考如何解決問題，降低試誤和運氣成份？</li></ul><p>舉例來說，我看過很強的高手在 debug 時，思考的時間遠大於碰鍵盤的時間。他會在腦中排演出最合理的解釋，在很複雜的系統裡，很少透過加 debug logs 反復收斂問題範圍，因此減少「編譯、部署、執行、看 log」的時間。長期下來，這種腦內排演的能力也會更強。</p><h3>我的學習例子</h3><h4>學用 React Native 寫 App</h4><p>React Native 很紅的時候，我想了解它的特性。剛好有和朋友出遊拆帳的需求 (當時沒順手的拆帳軟體)，所以<a href="https://github.com/fcamel/Go-Go-Dutch">用 React Native 寫一個</a>，藉這過程大概了解是怎麼回事。</p><p>後來工作上需要改 Reactive Native 開發的專案時，比較快上手。</p><h4>將 Redis 舊資料轉移到 DynamoDB</h4><p>工作上被指派這個任務，將問題拆解成</p><ul><li>學會舊資料在 Redis 存取的方法</li><li>學會開 DynamoDB 新 table 和針對需要格式的寫入方法</li><li>用簡化的環境模擬轉移的過程 (integration test)，方便開發和除錯</li><li>在 stage 環境執行</li><li>在 production 從較不重要的 cluster 開始逐步執行</li></ul><p>有了 Redis 和 DynamoDB 部份基礎後，之後透過其它工作補充其它塊知識，然後抽空閱讀一些基礎補完對整體的了解。寫成文章供日後備查 (<a href="https://medium.com/fcamels-notes/redis-%E5%AF%A6%E6%88%B0%E5%BF%83%E5%BE%97-b8ea52e0bab4">Redis</a>, <a href="https://medium.com/fcamels-notes/amazon-dynamodb-%E5%88%9D%E6%AD%A5%E4%BD%BF%E7%94%A8%E5%BF%83%E5%BE%97-c734d8445ae2">DynamoDB</a>)。</p><h4>中文語意分類</h4><p>這是十年前在工研院時期的一個長期專案，第一年研究、第二年作產品，後來團隊 spin off 成一家公司。</p><p>當時每天固定流程是</p><ul><li>早上頭腦最清楚的第一小時看 Stanford 線上 machine learning 課程。</li><li>做專案內容的開發和實驗，記下不懂但短期無法看懂的內容。</li></ul><p>當時的資訊比現在少很多，有些不懂的東西會在掃線上課程的過程中學到。</p><p>後來要作網站 demo 餐廳食記文章內的重點，有效能上的需求，每天的流程改成</p><ul><li>白天開發 data pipeline、設計資料庫、開發網站</li><li>晚上讀 <a href="https://www.tenlong.com.tw/products/9787121198854">High Performance MySQL</a>。學到有用東西時，回頭改設計。</li></ul><h4>實作 Block Chain 的 Consensus Algorithm</h4><p>前公司已有一套 Consensus Algorithm，當時雖然有了第二代的作法，但不確定需花多久才能完成。</p><p>我將這個目標拆成多個小步驟，有了明確成果後，比較好估工時，讓上層能判斷是否可執行。</p><ul><li>自己讀論文，寫成 pseudo code</li><li>和原作者討論，修改 pseudo code</li><li>寫成 prototype</li><li>和既有架構接起來</li><li>切分工作給團隊成員</li></ul><p>針對第一個目標，再拆解成多個子問題。讀論文時針對不懂的部份查網路上的說明，後來發覺 Ethereum 有詳細的文件，大多參考他們的文件。完成主要功能後，再找時間將短期吸收的知識整理成<a href="https://medium.com/fcamels-notes/pala-a-simple-partially-synchronous-blockchain-%E8%AE%80%E5%BE%8C%E7%AD%86%E8%A8%98-d8e9e14fa526">三篇筆記</a>備查。有了這樣的基礎，後來 Facebook 出了 <a href="https://medium.com/fcamels-notes/state-machine-replication-in-the-libra-blockchain-%E8%AE%80%E5%BE%8C%E7%AD%86%E8%A8%98-6f071b36e237">LibraBFT</a>，讀起來就比較順了。</p><h3>學習的來源</h3><p>現在資料來源很多，挑適合自己的就好。同樣的主題，看 A 的說明兩遍還是不懂，可以改找 B 的說明，總會找到最適合你的說明。有時不需要太詳細的說明，只要有個概念。</p><p>書本、他人筆記 (blogs)、podcast、影片都是不錯的來源，通勤時加減聽 podcast 可能會接收到一些關鍵字。這些媒介的知識密度和資訊量不同，像影片是資訊量最多的 (不只是畫面，講者的語氣、斷句都有助於抓重點)，但知識密度不如書本。有時比較難理解的抽象觀念，用影片比較好理解。有時想快速掌握大致架構，他人的筆記就很有用。不用擔心別人搞錯或自己讀錯，拿多個來源交叉比對，可降低誤解。</p><p>若有機會認識該領域的人，和對方問些有用的 keywords，用這些 keywords 容易找到有用的文章。例如最近和 data team 的同事合作，聽他介紹 data pipleline 後，得知 Glue、Athena、Redshift，三個字合在一起加些關鍵字，找到這篇<a href="https://ithelp.ithome.com.tw/articles/10237293">《AWS 資料處理的相關服務》</a>，相當適合現階段我想知道的事。</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=fad95b4f73ac" width="1" height="1" alt=""><hr><p><a href="https://medium.com/fcamels-notes/%E5%BF%AB%E9%80%9F%E5%AD%B8%E7%BF%92%E7%9A%84%E6%96%B9%E6%B3%95-fad95b4f73ac">快速學習的方法</a> was originally published in <a href="https://medium.com/fcamels-notes">fcamel的程式開發心得</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[用 Caffeine 和 Redis 管理 Cache 案例分析]]></title>
            <link>https://medium.com/fcamels-notes/%E7%94%A8-caffeine-%E5%92%8C-redis-%E7%AE%A1%E7%90%86-cache-%E6%A1%88%E4%BE%8B%E5%88%86%E6%9E%90-23e88291b289?source=rss----fc4919ea89c8---4</link>
            <guid isPermaLink="false">https://medium.com/p/23e88291b289</guid>
            <category><![CDATA[redis]]></category>
            <category><![CDATA[distributed-systems]]></category>
            <category><![CDATA[cache]]></category>
            <dc:creator><![CDATA[fcamel]]></dc:creator>
            <pubDate>Sat, 14 Nov 2020 08:26:39 GMT</pubDate>
            <atom:updated>2020-11-16T09:23:18.051Z</atom:updated>
            <content:encoded><![CDATA[<p>資料使用模式千百種，必須因事制宜地使用 Cache。網路上有許多不錯的整理，例如：</p><ul><li><a href="https://kkc.github.io/2020/03/27/cache-note/">有關 Cache 的一些筆記</a></li><li><a href="https://www.jyt0532.com/2018/09/23/cache-mechanism/">緩存讀寫機制</a></li><li><a href="https://coolshell.cn/articles/17416.html">缓存更新的套路</a></li></ul><p>這篇就不多作類似的整理，而是備忘最近碰到有意思的實例。</p><p>以下是本篇名詞的意思：</p><ul><li>server：指 web server 或 API server。</li><li>local cache：server 的 in-memory cache。</li><li>external cache：多台 servers 共用的 cache server，可以是一台或一組。</li><li>database：儲存原始資料的 database server，可以是一台或一組。</li></ul><p>其中一種作法是 local cache 用 <a href="https://github.com/ben-manes/caffeine">Caffeine</a>，external cache 用 <a href="https://medium.com/fcamels-notes/redis-%E5%92%8C-redis-cluster-%E6%A6%82%E5%BF%B5%E7%AD%86%E8%A8%98-fdc19a3117f3">Redis</a>。</p><h3>重啟 Server 後造成 Database 過載</h3><p>重開 server 後需要一些時間才能填回 local cache，這段時間會增加 database 的負載。如果 servers 數量夠多，一台台慢慢地重開，不會造成 database 過載。但每次更新 server 的時間會拉很長。</p><p>解法是將 local cache 的資料寫入 external cache，重開後從 external cache 讀回來。細節實作有很多選擇，比方說原本的架構已將 local cache、external cache、database 作成一體的 inline cache。理論上重開前後的差異只是暫時增加 external cache 負載。</p><p>為降低 external cache 的負載，可以在 local cache 發出請求時多過一層 Caffeine 的 <a href="https://github.com/ben-manes/caffeine/wiki/Population#asynchronous-manual">async load</a>，用來統合同一個 key 多筆查詢成一筆對 external cache/database 的查詢。若有作 partition (後述)，等於來自同時間全部 servers 收到同一個 key 的查詢，總共只會發一次查詢到 external cache/database。</p><pre>假設一次重開十台機器，十台機器每秒收到 100 筆同樣的查詢，等於將 1000 筆對 database 的查詢降為對 external cache 的 1 筆查詢。</pre><p>注意即使有作 partition，server 用 multi-threads 處理多個 client requests 時，仍有可能同時查 local cache 發現 cache miss，然後同時對 external cache 發出多個查詢，查詢同一個 key。使用 Caffeine 的 <a href="https://github.com/ben-manes/caffeine/wiki/Population#asynchronous-manual">async load</a> 可確保只查詢 external cache 一次。</p><h3>資料一致性和 Scalability</h3><p>如果每台 server 都用自己 local cache，N 台 server 的 cache pool size 和一台一樣，不具 scalability。並且需要處理資料一致性：server A 改了某個 key 的值，要通知其它 server 更新 local cache。</p><p>分散式系統的兩個核心模式是 <a href="https://medium.com/fcamels-notes/designing-data-intensive-applications-%E8%AE%80%E5%BE%8C%E5%BF%83%E5%BE%97-1-58a4d2d21d2a">partition (sharding) 和 replication</a>。用 partition 可同時解決讀寫 scalability 問題，每個 key 只有唯一的 server 負責處理；用 replication 可解決 read scalability 並提供 high availability (HA)。</p><p>用 replication 的缺點是要處理 read consistency，我個人的偏好是即使用 partition + replication，也不要從 replicas 讀資料。換句話說，如果多作 replication，目的只是提高 HA，藉此避開 read consistency 的問題。</p><p>像這樣使用 partition 的 distributed cache 應該是常見解法？<a href="https://github.com/golang/groupcache">groupcache 是一個例子</a>。更多的說明可見 <a href="https://blog.gslin.org/archives/2016/07/26/6688/google-%E6%8F%90%E5%87%BA%E7%9A%84-jump-consistent-hash/">Consistent Hash</a>。</p><h3>昂貴操作和 Cache Refresh</h3><p>有些服務需要查詢大量 database 資料作複雜的計算後得出結果。自然會用 cache 減輕負擔。但是當資料過期的時候，第一筆請求仍需等一段時間。</p><p>解法是在資料快過期前先在背景更新資料。例如用 <a href="https://github.com/ben-manes/caffeine/wiki/Refresh">Caffeine 的 refresh</a>，可設 10 分鐘後資料 stale，10 ~ 20 分鐘內有人存取，先回 stale data，同時在背景 refresh data。反之，若 10 ~ 20 分鐘內沒人存取，就刪除 stale data。</p><p>若想減低重開機第一次請求的等待時間 (還有降低 server 重開的負擔)，可以針對第一次請求用輕量化的實作，然後等一段隨機的時間後，在背景 refresh 執行原本的實作。例如 API 是提供最近一天內取樣的某種統計，輕量化實作可以只取 1/10 資料。</p><h3>高頻率、高數量、不常更動的資料</h3><p>由於讀取頻率很高，希望在 external cache 存愈多資料愈好，減輕 database 壓力。由於 key 的數量很高 (例如千萬筆或億筆)，若忽然有一批查詢的資料不在 external cache 內，可能會讓 database 過載。因此，必須有效率地在 external cache 內存資料。</p><p>直覺的作法是用 LRU 管理 external cache。以 Redis 來說，用 <a href="https://redis.io/commands/setex">setex</a> 設值和 TTL，然後每次 <a href="https://redis.io/commands/get">get</a> 後順便用 <a href="https://redis.io/commands/expire">expire</a> 更新 TTL。</p><p>這作法有幾個小缺點：</p><ul><li><a href="https://docs.redislabs.com/latest/ri/memory-optimizations/#combine-smaller-strings-to-hashes">每個 key 額外多占 90 bytes </a>，10M 筆會多占近 1G 記憶體。</li><li>常見操作變兩倍貴 （get + expire)。<a href="https://github.com/redis/redis/issues/2762">官方有個 issue 要求 getex</a> 好多年了，還是沒結果。</li><li>不易掌握 keys 的數量。</li></ul><p>前兩個效能問題不見得是必須解決的問題，最後一個可以用 scan 跑個數分鐘，也不嚴重。唯一要留意的是若同時有大量 keys 過期，會卡住 Redis 一段時間，因為 Redis 是固定時間<a href="https://redis.io/commands/expire">隨機抽查 expired keys，不斷地清理直到數量降到 1/4。</a>設 TTL 時，務必加上一段隨機值。</p><h4>Generational LRU</h4><p>若資料很少會更新，有個有趣的解法，可以解決上述三者問題：用類似 <a href="https://medium.com/fcamels-notes/java-garbage-collection-%E7%B0%A1%E4%BB%8B-c52e5c5dbd55">generational GC</a> 的概念，將資料存在多個 hash。</p><p>概念是每個 hash 有個流水號，假設依序為 h0、h1、h2、…。查詢的作法如下：</p><ul><li>先查 h0，有資料就返回。</li><li>查 h1，有資料就返回，順便「搬」到 h0 (hset h0 key value+ hdel h1 key)。</li><li>查 h2、h3 … 以此類推。</li></ul><p>每個 hN 表示一段時間內的資料，N 愈大表示愈舊。所以若只想保留 N 筆，可以定期清掉 hN+1 和之後的 hash。用 <a href="https://redis.io/commands/unlink">unlink</a> 清的話，不會占用 Redis main thread 的時間。</p><p>那要怎麼保持 h0、h1、… 有由新到舊的資料？可以用時間算出 hash 名稱，比方說 h20201114 存 2020/11/14 的資料，每天存到不同 hash。過了七天，就刪掉七天前的 hash。這樣不用像 garbage collector 在執行 garbage collection 後，要將剩下活著的資料搬到其它 region。</p><p>這作法常見情境只需呼叫一次 get，偶而會需要 2 ~ N 個 get + 1 個 set + 1 個 delete。比較貴的操作是 invalidate，需要 N 個 delete，所以比較適合用在不常更新的資料。</p><h3>總結</h3><p>要提供 scalable 和穩定的 cache 服務，架構上需要：</p><ul><li>server 用 shared local cache (partition / sharding) 確保每個 key 只有一個節點可寫入。</li><li>local cache 使用 Caffeine async load 確保忽然有大量同筆 key 的查詢時，只會對後端發出一次請求。</li><li>local cache 後面接一層 shared external cache server (Redis)，減少 database 的負擔以及處理重開 server 的負載。</li><li>Redis 可用 generational hashes 節省記憶體空間。</li><li>針對可接受 stale data 且費時的操作，用 Caffeine refresh 省時。</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=23e88291b289" width="1" height="1" alt=""><hr><p><a href="https://medium.com/fcamels-notes/%E7%94%A8-caffeine-%E5%92%8C-redis-%E7%AE%A1%E7%90%86-cache-%E6%A1%88%E4%BE%8B%E5%88%86%E6%9E%90-23e88291b289">用 Caffeine 和 Redis 管理 Cache 案例分析</a> was originally published in <a href="https://medium.com/fcamels-notes">fcamel的程式開發心得</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Amazon DynamoDB 的 Consumed Units 注意事項]]></title>
            <link>https://medium.com/fcamels-notes/amazon-dynamodb-%E7%9A%84-consumed-units-%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A0%85-e1fbda5687b4?source=rss----fc4919ea89c8---4</link>
            <guid isPermaLink="false">https://medium.com/p/e1fbda5687b4</guid>
            <category><![CDATA[dynamodb]]></category>
            <category><![CDATA[distributed-systems]]></category>
            <dc:creator><![CDATA[fcamel]]></dc:creator>
            <pubDate>Sat, 07 Nov 2020 17:10:44 GMT</pubDate>
            <atom:updated>2020-12-09T11:46:54.737Z</atom:updated>
            <content:encoded><![CDATA[<p>DynamoDB 是依 read/write 耗用多少資源收費，有許多眉角要留意，這篇記錄我覺得比較重要的部份。</p><h3>TL; DR</h3><p>舉個極端的例子說明「意外」花費有多驚人：</p><ul><li>table 有 A、B、C、D 四個欄位，A 是 primary key，B 是 sort key。</li><li>A ~ C 各占 4 bytes，D 占 10 KB。</li><li>用 A、C 建 LSI，projection 是全部。</li><li>用 B、C 建 GSI，projection 是全部。</li></ul><p>更新一筆資料的 B，會花多少 WCU 呢？直覺認為是 1 WCU，因為只更新了 4 bytes。但是實際花費是：</p><ul><li>table: 11 (11 KB / 1KB)</li><li>LSI: 22 (delete + write)</li><li>GSI: 22 (delete + write)</li></ul><p>共 55 WCU，<strong>是直覺的 55 倍貴。</strong></p><p>原因如下：</p><ul><li>所有計費都是以 item size (該筆資料總大小) 計算。局部更新、conditional write、使用 filter 讀資料等，都是用「接觸到」的總資料計費，不會比較省。</li><li>Read 以 4KB 為單位累進計費。Write 以 1KB 為單位。</li><li>更新 index 欄位 (GSI/LSI 的 primary/sort key) 時，計費兩次 (delete + write)。</li></ul><p>其它注意事項：</p><ul><li>使用 Provisioned mode + auto scaling 並且避免超過兩分鐘的 spike (短暫大幅上升的使用量)，花費只有 On-Demand mode 的 1/5。</li><li>Write 是 Read 五倍貴。</li><li>Consistent read 是 eventually consistent read 兩倍貴。</li><li>可使用 Contributor Insights 找 hot keys，但要額外收費。</li><li>長遠來說，自行用 returned consumed unit 計算是必要的。因為 web console 無法得知自家 application code 不同操作使用的 RCU/WCU；web console 也沒有顯示 LSI 的 RCU/WCU (table 和 LSI 合在 table metrics 裡)。</li></ul><h3>基本單位</h3><p>基本的單位是 Read Consumed Unit (RCU) 和 Write Consumed Unit (WCU)，不同 AWS regions 收費不同，大致上 WCU 是 RCU 的五倍貴。可以在 AWS web console 的 metrics 看到詳細資訊，在 capacity 調整 RCU/WCU 使用的上下限。</p><p>粗略來說，可將每秒 RCU/WCU 上限看成每秒能處理 read/write 的上限。不過 eventually consistent read 只需花 0.5 RCU；consistent read 則花 1 RCU。</p><h3>On-Demand vs. Provisioned Mode</h3><p>使用 RCU/WCU 的模式有兩種：</p><ul><li>On-Demand mode: DynamoDB 全程自動調整，可承受 4,000 WCU 而不會發生 throttled。可以隨時處理「上一個 peak 」的兩倍請求。最高可到 40,000 WCU。</li><li>Provisioned mode: 指定固定的數據 (例如 1000 RCU, 500 WCU)。在這個模式下可以加開 auto scaling，設定一個範圍和指定的使用率 (5% ~ 90%)，低於使用率太多就降 provisioned RCU/WCU，高於就升。</li></ul><p>兩者收費的主要差異是 Provisioned mode 往上調沒有限制 (不管是手調或 auto scaling)，但每日往下調的次數有限 (例如四次)，超過次數後，變成每小時只能降一次。所以在使用量常有 spike (短暫大幅上升的使用量) 的情況，會有些尷尬。</p><ul><li>如果沒開 auto scaling，spike 可能會被 throttled (後述)，導致操作失敗。</li><li>有開 auto scaling 時，會在偵測到 spike 維持一段時間後 (例如用量&gt;70%維持兩分鐘)，自動往上調。但 spike 消失後卻要一小時才能調回合適的值。假設因為 spike 多加 1000 provisioned WCU，就浪費一小時的 1000 WCU。</li></ul><p>Provisioned mode 看起來很麻煩，那為什麼不用 On-Demand mode 就好？原因是 <a href="https://www.serverless.com/blog/dynamodb-on-demand-serverless/">On-Demand mode 費用約是 Provisioned mode 6.94x</a>。若使用率定在 70% (target utilization percentage)，仍是貴了近五倍。若一小時內變化沒到五倍以上，用 provisioned mode 比較省。舉例來說，若一小時內 RCU 變化是 100 ~ 400，設 RCU=400 還是比 on-demand 便宜。</p><p>反之，多數情況沒什麼流量，偶而會有 spike，較適合用 On-Demand mode。</p><p>可以從 DynamoDB web console 的 metrics 看 throttled read/write requests/events。量不大時，不需擔心，因為 AWS SDK v2 會自動重試。但重試太多次後會丟出 provisioned throttled exception (web console 沒有統計此數量)，若 app 沒有處理，操作就失敗了。</p><p>在 On-Demand/Provisioned mode 之間切換不會立即有流量不夠的問題。唯一要留意的是 Provisioned mode 不足 4000 WCU 時，切到 On-Demand mode 要一陣子才會備妥 4000 WCU。</p><p>見<a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadWriteCapacityMode.html#HowItWorks.OnDemand">官網 On-Demand Mode</a> 在 “Initial Throughput for On-Demand Capacity Mode” 和 “Table Behavior while Switching Read/Write Capacity Mode” 的說明。</p><h3>備妥機器的時間</h3><p>若知道接下來會有大量讀寫時，實測最妥當的作法，是使用 provisioned mode 並關掉 auto scaling，手動指定數字，然後等 DynamoDB 備妥足夠機器。後台可從 metrics 看是否已備妥 provisioned capacity units。不過在使用量少的時候，metrics 更新不即時，要留意一下。等大量讀寫穩定發生後，再開啟 auto scaling，這樣等使用量降低後，DynamoDB 會自動降低 RCU/WCU 省錢。</p><p>On-demand 初始可容許 WCU=4,000、RCW=12,000。兩者模式互換時不用擔心影響 throughput：</p><ul><li>Provisioned → On-demand: 用過去歷史記錄的 peak / 2 作為 on-demand 的基準。</li><li>On-demand → Provisioned: 後台會提供目前的 RCU/WCU 作為參考。</li></ul><p><a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadWriteCapacityMode.html#HowItWorks.InitialThroughput">資料來源</a>：</p><blockquote><strong><em>Newly created table with on-demand capacity mode:</em></strong><em> The previous peak is 2,000 write request units or 6,000 read request units. You can drive up to double the previous peak immediately, which enables newly created on-demand tables to serve up to 4,000 write request units or 12,000 read request units, or any linear combination of the two.</em></blockquote><blockquote><strong><em>Existing table switched to on-demand capacity mode:</em></strong><em> The previous peak is half the maximum write capacity units and read capacity units provisioned since the table was created, or the settings for a newly created table with on-demand capacity mode, whichever is higher. In other words, your table will deliver at least as much throughput as it did prior to switching to on-demand capacity mode.</em></blockquote><p>用 Provisioned mode + auto scaling 時，大致上要等個五分鐘，才會備妥機器。使用量上升速度太快時，就會不斷地有 throttled 然後等幾分鐘 provisioned consumed units 才上升。若想盡速處理完突來的需要，此時人工介入關掉 auto scaling 一次調足 RCU/WCU 會比較適當。</p><h3>Adaptive Capacity</h3><p>偶而出現大量讀寫不是問題，DynamoDB 使用 <a href="https://medium.com/fcamels-notes/amazon-dynamodb-%E6%A6%82%E5%BF%B5%E7%AD%86%E8%A8%98-72a7636f2a79">token bucket</a> 計算 capacity units，<a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-partition-key-design.html#bp-partition-key-throughput-bursting">Best Practices for Designing and Using Partition Keys Effectively</a> 提到會保留五分鐘未用到的 capacity，供未來發生 burst 時使用：</p><blockquote><em>DynamoDB provides some flexibility in your per-partition throughput provisioning by providing burst capacity. Whenever you’re not fully using a partition’s throughput, DynamoDB reserves a portion of that unused capacity for later bursts of throughput to handle usage spikes.</em></blockquote><blockquote><em>DynamoDB currently retains up to 5 minutes (300 seconds) of unused read and write capacity. During an occasional burst of read or write activity, these extra capacity units can be consumed quickly — even faster than the per-second provisioned throughput capacity that you’ve defined for your table.</em></blockquote><p>在總量不變的前提下，會動態調整不同 partitions 用的 capacity，讓 hot partition 能多用一些：</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/950/0*QOkjkokhYk9EK5j7.png" /><figcaption><a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-partition-key-design.html#bp-partition-key-throughput-bursting">https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-partition-key-design.html#bp-partition-key-throughput-bursting</a></figcaption></figure><p>若一直超出使用上限，會動態拆開 partition 成不同 partitions。最終單一 key 獨占一個 partition，然後一樣會有 3000 RCU / 1000 WCU 上限：</p><blockquote><em>If your application drives consistently high traffic to a single item, adaptive capacity might rebalance your data such that a partition contains only that single, frequently accessed item. In this case, DynamoDB can deliver throughput up to the partition maximum of 3,000 RCUs or 1,000 WCUs to that single item’s primary key.</em></blockquote><h3>Global Secondary Index (GSI) 和 Local Secondary Index (LSI)</h3><p>GSI 的計算是獨立的，使用 auto scaling 時可以選擇是否除了 table 以外，要一併對 GSI 啟用。LSI 則是和 table 用量合在一起計算，無法從 web console 得知。若想了解 LSI 實際用量，要自己用 API 計算 (後述)。</p><p>更新 index keys 時，GSI/LSI 都需要兩倍費用 (delete + write)。由於 table 無法更新 primary key 和 sort key，比較不會誤用「額外」花費。但更新到作為 index 的欄位時，可能沒有意識到，對 index 的花費是兩倍。</p><p>舉例來說， table 有個欄位 T 用來存 timestamp，建了一個 GSI 和一個 LSI，都用 T 當 sort key，projection 都是全部。更新 T 的時候，相比只有 table 沒有 index，會花五倍費用 (table update + GSI delete + GSI write + LSI delete + LSI write)。</p><h3>Consumed Units 用 Item Size 計算</h3><p><a href="https://aws.amazon.com/dynamodb/pricing/provisioned/">官方文件提到資料大小會影響 RCU (Read 和 Write 的單位是 4 KB 和 1 KB)：</a></p><blockquote><em>Read capacity unit (RCU): Each API call to read data from your table is a read request. Read requests can be strongly consistent, eventually consistent, or transactional. For items up to 4 KB in size, one RCU can perform one strongly consistent read request per second. Items larger than 4 KB require additional RCUs. For items up to 4 KB in size, one RCU can perform two eventually consistent read requests per second. Transactional read requests require two RCUs to perform one read per second for items up to 4 KB. For example, a strongly consistent read of an 8 KB item would require two RCUs, an eventually consistent read of an 8 KB item would require one RCU, and a transactional read of an 8 KB item would require four RCUs. See </em><a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadConsistency.html"><em>Read Consistency</em></a><em> for more details.</em></blockquote><p><a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html#Query.CapacityUnits">官方文件提到取部份欄位和取全部欄位的算法一樣：</a></p><blockquote><em>DynamoDB calculates the number of read capacity units consumed based on item size, not on the amount of data that is returned to an application. For this reason, </em><strong><em>the number of capacity units consumed is the same</em></strong><em> whether you request all of the attributes (the default behavior) or just some of them (using a projection expression). The number is also the same whether or not you use a filter expression.</em></blockquote><p>RCU 是依 item size 計算，不是傳回的大小。所以使用 filter 或是 item 內有某個欄位有很大的值，都會造成意外的花費。</p><p>比方說 item 有欄位 A, B, C, D, E，其中 A ~ D 很小，加起來不到 1 KB，但 E 占了 399 KB，但通常只會取出 A ~ D。結果是都會以使用 A ~ E (400KB) 的大小計算 RCU。</p><p>局部更新一個欄位，<a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ProvisionedThroughput.html#ProvisionedThroughput.CapacityUnits.Write">花費是以該筆資料整筆資料大小計算 (取更改前後的最大值)</a>：</p><blockquote>UpdateItem—Modifies a single item in the table. DynamoDB considers the size of the item as it appears before and after the update. The provisioned throughput consumed reflects the larger of these item sizes. Even if you update just a subset of the item&#39;s attributes, UpdateItem will still consume the full amount of provisioned throughput (the larger of the &quot;before&quot; and &quot;after&quot; item sizes).</blockquote><p>以前面同個例子來說，更新 A 的結果，會花 400 WCU ( 400 KB / 1KB )。</p><p>結論是：同一筆資料不要混存很大的欄位和很小的欄位，這樣會讓讀寫小欄位使用意外多的費用。</p><h3>Contributor Insights (Hot Key Issues)</h3><p>網路上常有人提到用 DynamoDB 不表示就能 scale out。設計不當時，容易大量讀寫同一個 partition，以前沒提供工具讓開發者找 hot partitions，造成昂貴的開銷。</p><p>現在 DynamoDB 後台有提供付費新功能： <a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/contributorinsights_HowItWorks.html">Contributor Insights</a>。可以分析 hot keys，讓開發者知道問題出在那。試用後滿有幫助的。對指定的 table 開啟後，會新增四張表：</p><ul><li>Most accessed items (partition key) (rule: DynamoDBContributorInsights-PKT…)</li><li>Most throttled keys (partition key)</li><li>Most accessed items (partition key and sort keys)</li><li>Most throttled keys (partition key and sort keys)</li></ul><p>下圖是官方文件的範例：</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*hFM5fB23RQnhhrYn.png" /><figcaption><a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/contributorinsights_HowItWorks.html">https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/contributorinsights_HowItWorks.html</a></figcaption></figure><p>比較惱人的是 read 和 write capacity units 合在一起算，一個 write 算成三次，判讀是 hot read 或 hot write 時不太方便。</p><p><a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/contributorinsights_HowItWorks.html#contributorinsights_HowItWorks.Billing">價格是以事件次數計費</a>。事件的定義如下，注意 sort key 會多算一次：</p><blockquote><em>For tables and global secondary indexes with CloudWatch Contributor Insights for DynamoDB enabled, each item that is written or read via a data plane operation represents one event.</em></blockquote><blockquote><em>If a table or global secondary index includes a sort key, each item that is read or written represents two events. This is because DynamoDB is identifying top contributors from separate time series: one for partitions keys only, and one for partition and sort key pairs.</em></blockquote><p><a href="https://aws.amazon.com/cloudwatch/pricing/">價格不貴但也不算便宜</a>，以 us-west-2 為例，每百萬筆事件收費美金 $0.03。使用後記得要關閉。</p><h3>自行記錄 Consumed Units</h3><p>DynamoDB web console 的 metrics 雖然已滿詳細了，但最終還是要自己記錄，才能回答以下問題：</p><ul><li>自家 application code 不同操作各用了多少 RCU/WCU？</li><li>LSI 用了多少？</li><li>在一頁裡看所有 AWS regions 所有 tables 的異常用量。</li></ul><p>作這件事並不難，用 AWS SDK 時，設定 ReturnConsumedCapacity: INDEXES 要求所有操作回傳 consumed units (<a href="https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html#API_Query_RequestSyntax">這是 Query 的例子</a>)，回傳值就會帶有 <a href="https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_ConsumedCapacity.html">ConsumedCapacity</a>。用 CapacityUnits 減去 GSI 和 LSI 的花費，就是 table 的花費。</p><p>以 Java 為例，具體作法是：</p><ul><li>自訂一個 class 包住 SDK 提供的 DynamoDbAsyncClient。</li><li>定義用到的 methods，改成接受 “label” 和各種 request 的 builder。</li><li>用 builder 呼叫 returnConsumedCapacity(ReturnConsumedCapacity.<em>INDEXES</em>)再產生 request。</li><li>取得 response，取出 consumed units，配合 label 用 prometheus 記錄。</li></ul><p>有個統一的 wrapper 呼叫 dynamo API，還可以順便記錄發生的 exceptions、執行的時間，藉此了解其它問題。</p><h3>相關文章</h3><ul><li><a href="https://medium.com/fcamels-notes/amazon-dynamodb-%E5%88%9D%E6%AD%A5%E4%BD%BF%E7%94%A8%E5%BF%83%E5%BE%97-c734d8445ae2">Amazon DynamoDB 初步使用心得</a></li><li><a href="https://medium.com/fcamels-notes/amazon-dynamodb-%E6%A6%82%E5%BF%B5%E7%AD%86%E8%A8%98-72a7636f2a79">Amazon DynamoDB 概念筆記</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e1fbda5687b4" width="1" height="1" alt=""><hr><p><a href="https://medium.com/fcamels-notes/amazon-dynamodb-%E7%9A%84-consumed-units-%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A0%85-e1fbda5687b4">Amazon DynamoDB 的 Consumed Units 注意事項</a> was originally published in <a href="https://medium.com/fcamels-notes">fcamel的程式開發心得</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Java Garbage Collection (GC) 簡介]]></title>
            <link>https://medium.com/fcamels-notes/java-garbage-collection-%E7%B0%A1%E4%BB%8B-c52e5c5dbd55?source=rss----fc4919ea89c8---4</link>
            <guid isPermaLink="false">https://medium.com/p/c52e5c5dbd55</guid>
            <category><![CDATA[go]]></category>
            <category><![CDATA[java]]></category>
            <category><![CDATA[garbage-collection]]></category>
            <dc:creator><![CDATA[fcamel]]></dc:creator>
            <pubDate>Fri, 02 Oct 2020 04:37:20 GMT</pubDate>
            <atom:updated>2020-10-04T09:51:08.878Z</atom:updated>
            <content:encoded><![CDATA[<p>Java 發展多年，有多種不同 Garbage Collection (GC) 的實作，JVM 有提供參數可以換不同實作還有微調各實作的參數。這篇文章說明大方向的觀念，並提供詳細說明的連結。</p><h3>TL; DR</h3><ul><li>如果必須限制 GC 暫停時間在 10 ms 內，使用 Java 15 + ZGC。或是改用 Go 吧。</li><li>Java 9 開始預設用 G1 GC，如果希望多數情況控制在 100ms 內，調整 G1 GC 參數 -XX:MaxGCPauseMillis=100 。</li><li>如果可接受通常 200ms 暫停時間， G1 GC 預設參數已是不錯的配置。</li></ul><h3>使用者在意的事</h3><p>從使用者角度來看，我們關心：</p><ul><li>需要多少記憶體才能有效率地作 GC？</li><li>應用程式是否會有短暫的暫停 (stop-the-world pause)？</li><li>多少比例的 CPU 時間花在 GC？</li></ul><p>由後兩者的取捨，衍生出兩大陣營的作法：</p><ul><li>Concurrent GC: 目標是幾乎不會有 stop-the-world (STW)</li><li>Generational GC: 目標是減少因 GC 而占用的 CPU 時間</li></ul><h3>Java GC</h3><p>Java 長期以來是用 Generational GC，自 Java 9 開始預設用 G1 GC。其特色是：</p><ul><li>盡可能縮短單次 stop-the-world (STW) 的時間，並提供參數決定期望 stop-the-world 的時間上限，預設 200ms。但這是 soft limit。</li><li>參數較少，方便調整。</li></ul><p>以我自身的例子來看，G1 滿準地讓 STW 的時間落在 200ms 左右，大概 5s 觸發一次 GC，占用 CPU 的時間 &lt;4%。但數十台機器在一週內會有數次暫停到 2~3s。理論上降低 200ms 為 100ms 可讓程式有更快反應，副作用大概是會更頻繁觸發 GC，不確定占用 CPU 的時間會增加多少。</p><p>最新的 <a href="https://wiki.openjdk.java.net/display/zgc/Main">ZGC</a> 則是 Concurrent GC，目標是 STW &lt; 10ms：</p><blockquote>* Max pause times of <strong>a few milliseconds</strong> <strong>(*)</strong></blockquote><blockquote>* Pause times <strong>do not</strong> increase with the heap or live-set size <strong>(*)</strong></blockquote><blockquote>* Handle heaps ranging from a <strong>8MB </strong>to <strong>16TB</strong> in size</blockquote><p>由於它不是 generational GC，不確定在有大量長駐 objects (如 cache) 時，是否會浪費太多 CPU 時間。若服務需要提供 &lt;10ms 回應的保證，值得試看看 ZGC。</p><p><a href="https://wiki.openjdk.java.net/display/zgc/Main">Java 15 開始，ZGC 已可在產品中使用</a>：</p><blockquote>Use the -XX:+UseZGC options to enable ZGC.</blockquote><blockquote><strong>NOTE!</strong> Prior to JDK 15 you also had to supply the -XX:+UnlockExperimentalVMOptions option. As of JDK 15 this is no longer needed, since ZGC is now a production ready (non-experimental) feature.</blockquote><p><a href="https://ionutbalosin.com/2019/12/jvm-garbage-collectors-benchmarks-report-19-12/">“JVM Garbage Collectors Benchmarks Report 19.12”</a> 針對許多情境對全部 Java GC 作 benchmark，可以參考看看。</p><h3>Java GC Metrics</h3><p>JVM 有參數可開啟 GC logs，Java 9 和 Java 9 之前參數不同。不過要大量觀察的話，<a href="https://www.robustperception.io/measuring-java-garbage-collection-with-prometheus">用 Prometheus 收集</a>，然後用 <a href="https://grafana.com/">Grafana</a> 看比較方便。</p><p>重要的 metrics 如下：</p><ul><li>rate(jvm_gc_collection_seconds_sum[1m]) : GC 占用 CPU 時間比例，顯示一分鐘取樣數據的平均值。</li><li>rate(jvm_gc_collection_seconds_sum[1m]) / rate(jvm_gc_collection_seconds_count[1m]) ：GC 暫停的平均時間。</li><li>rate(jvm_gc_collection_seconds_count[1m]) ：GC 發生的頻率。</li></ul><p>其它輔助數據，不一定需要：</p><ul><li>jvm_threads_current ：thread 數量。</li><li>jvm_memory_bytes_used ：使用的記憶體。</li><li>jvm_memory_pool_bytes_used ：GC 有分區時，可看到各區記憶體用量。</li></ul><p>注意 Prometheus 是週期性收集的資料，比方說每 30s 向 Java server 收集 GC metrics，資料已是 30s 內的平均值。若在 30s 內有大幅變化，得分析 raw logs 才知道。</p><h3>調整 GC 參數的問題</h3><p>有 metrics 可觀察變化後，才知道調整參數是否有改善。但調整參數是很困難的事。舉例來說，對 Generational GC 來說，-XX:MaxTenuringThreshold 設定經過幾次 Young Generation GC 後，會將物件搬到 Old Generation，減少 Young Generation GC 要處理的物件。預設是 15，<a href="https://support.oracle.com/knowledge/Middleware/1283267_1.html">往上調會造成永遠不會搬到 Old Generation。</a></p><p>假設應用程式的行為如下：</p><ul><li>平均 5s 作一次 Young Generation GC。</li><li>多數物件存活 30s 後就會繼續使用。</li></ul><p>可能會想將 MaxTenuringThreshold 從 15 調成 6。但尖峰和離峰時間使用記憶體的方式不同，可能在最熱門時段 2s 就作一次 GC，調成 6 會變成 12s 就搬到 Old Generation，反而增加 Old Generation GC 發生頻率，造成單次更久的 stop-the-world。</p><p>此外，系統變化太多，隨著時間演進，舊的修改可能未來變得有害。一般建議不要在早期調 GC 參數，只有在有問題時再針對問題修改，確保不會造成其它不好的副作用。</p><h3>Garbage First Garbage Collector (G1 GC)</h3><p>Generational GC 的假設是大部份物件在 new 出來後很快就沒用了，所以只要回收新出生的物件，就有足夠記憶體可用。偶而再檢查剩下的物件，藉此減少占用 CPU 時間。</p><p>G1 GC 在這前提下，目標是盡可能多作 young generation GC，有需要時抽空順便處理一些 old generation GC (mixed GC)，逼不得已時才作 full GC。</p><p>G1 GC 另外支援移除重覆的 strings (deduplicate duplicate strings)，預設沒有開啟。server 接收大量 requests 時，很容易 deserialize 資料後產生重覆字串。可用 -XX:+G1EnableStringDeduplication 啟用。</p><p>以我自身的例子來說，抽查三天內數台機器的 raw logs，沒有看到 full GC。大多是 young generation GC，少數 mixd GC。偶而會因 memory fragmantation 必須作 GC (<a href="https://plumbr.io/handbook/gc-tuning-in-practice/other-examples/humongous-allocations">humongous allocations</a>)。String deduplication 效果很好，可以消除 80% 以上 String 物件。</p><p>理論上 G1 GC 只需要調整 MaxGCPauseMillis，簡化調整 GC 參數的困難和意外的副作用。</p><p>G1 GC 的設計滿有趣的，有興趣的人可以參考以下文章了解：</p><ul><li>GC 設計時要處理那些事 (例如 memory defragmentation)。</li><li>不同作法的取捨。</li><li>調整 GC 行為的參數。</li></ul><ul><li><a href="https://www.oracle.com/technical-resources/articles/java/g1gc.html">Garbage First Garbage Collector Tuning</a></li><li><a href="https://medium.com/@marknienaber/jvm-tuning-with-g1-gc-76f27535f054">JVM Tuning with G1 GC</a></li><li><a href="https://www.redhat.com/en/blog/part-1-introduction-g1-garbage-collector">Part 1: Introduction to the G1 Garbage Collector</a></li><li><a href="https://plumbr.io/handbook/garbage-collection-algorithms-implementations#g1">GC Algorithms: Implementations | Plumbr - User Experience &amp; Application Performance Monitoring</a></li></ul><h3>Go GC</h3><p>Go GC 聲稱暫停時間 &lt;1ms，聽起來比 Java GC 強大許多，但這其實是不同取捨的結果。Java 需要考慮多種使用情境 (mobile app, desktop app, server, etc)，程式可能占用整個機器，或和其它程式共用資源。</p><p>而 Go 的目標是開發 backend server，理論上可占用整個機器資源。在這個前提下，Go 可用最大的資源換取 &lt;1ms 的暫停時間。</p><p>這篇 2016 的文章有點舊，不過有點出許多 GC 設計的取捨，滿有意思的：</p><p><a href="https://blog.plan99.net/modern-garbage-collection-911ef4f8bd8e">Modern garbage collection</a></p><p>針對文中一點補充，後來 Go 有在長時間的 loop 裡加中斷點，讓 Go runtime 有機會 context switch，因此像 “base64 decoding a large blob in a single goroutine can cause pause times to go up” 就不再是問題了。</p><p>另外 <a href="https://medium.com/@mayurwadekar2/escape-analysis-in-golang-ee40a1c064c1">Go compiler 有作 escape analysis</a>，所以局部變數在離開 stack 後就「回收」了，減少 GC 的工作。好奇查了一下，Java 也有作一樣的事，不過我不確定作得如何。</p><h3>參考資料</h3><p>如果只看一篇的話，Plumbr 的 Java Garbage Collection handbook 最為完整，PDF 檔的版本有 75 頁，內容淺顯易懂，附有大量圖片說明。</p><p><a href="https://plumbr.io/java-garbage-collection-handbook">Java Garbage Collection handbook | Plumbr - User Experience &amp; Application Performance Monitoring</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c52e5c5dbd55" width="1" height="1" alt=""><hr><p><a href="https://medium.com/fcamels-notes/java-garbage-collection-%E7%B0%A1%E4%BB%8B-c52e5c5dbd55">Java Garbage Collection (GC) 簡介</a> was originally published in <a href="https://medium.com/fcamels-notes">fcamel的程式開發心得</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Redis latency 相關雜記]]></title>
            <link>https://medium.com/fcamels-notes/redis-latency-%E7%9B%B8%E9%97%9C%E9%9B%9C%E8%A8%98-4ac4ad2c414c?source=rss----fc4919ea89c8---4</link>
            <guid isPermaLink="false">https://medium.com/p/4ac4ad2c414c</guid>
            <category><![CDATA[redis]]></category>
            <category><![CDATA[database]]></category>
            <category><![CDATA[distributed-systems]]></category>
            <dc:creator><![CDATA[fcamel]]></dc:creator>
            <pubDate>Wed, 22 Jul 2020 15:27:39 GMT</pubDate>
            <atom:updated>2020-07-22T15:27:39.062Z</atom:updated>
            <content:encoded><![CDATA[<h3>Latency 除錯指南</h3><p>官網的 “<a href="https://redis.io/topics/latency">Redis latency problems troubleshooting</a>” 介紹相當詳細，若需要自己架 Redis，務必讀過一遍，有些必要設定不是 OS 和 Redis 預設值。</p><p>懶得讀的話，至少學會開啟 <a href="https://redis.io/topics/latency-monitor">latency monitor</a>：</p><pre>CONFIG SET latency-monitor-threshold 100</pre><p>收集一段時間資料後，然後執行 <a href="https://redis.io/commands/latency-doctor">latency doctor</a>：</p><pre>127.0.0.1:6379&gt; latency doctor<br><br>Dave, I have observed latency spikes in this Redis instance.<br>You don&#39;t mind talking about it, do you Dave?<br><br>1. command: 5 latency spikes (average 300ms, mean deviation 120ms,<br>    period 73.40 sec). Worst all time event 500ms.<br><br>I have a few advices for you:<br>...</pre><p>頗蠢的設計，不過用起來滿方便的，大概可以解決不少問題。</p><p>不用擔心 latency monitor 會影響執行效能。預設沒開啟是因為官方覺得會占用一點點記憶體且是非必要功能，所以沒有像 SLOWLOG 那樣預設開啟。</p><h3>統計使用頻率</h3><p>另外一種拖慢校能的原因不是單一指令太慢，而是執行太多指令。可以用 <a href="https://redis.io/commands/monitor">redis-cli monitor</a> 收集一段時間，再自行寫 script 分析出最常執行的指令和 keys。</p><h3>Fork 的問題</h3><p>Redis 實作 isolation snapshot 的方式很有趣，不像<a href="https://en.wikipedia.org/wiki/List_of_databases_using_MVCC">一堆 database 使用 MVCC</a>，Redis 因為全部資料都在 RAM，採用 fork 然後由 child process 備份。</p><p>Linux 的 fork 會採用 copy-on-write 的機制，所以 child process 有和 main process (Redis server) 一模一樣的內容。將 child process 在記憶體內的資料寫入硬碟，就完成備份了。</p><p>聽起來相當聰明和簡單的作法，但在記憶體量過大時 (例如 100 GB)，會因為 fork 而卡住 Redis server 數秒。對一個 10ms 都嫌太慢的服務來說，卡住數秒滿糟糕的。唯一的解法是不要讓單一 Redis node 使用太大記憶體，也就是使用 Redis Cluster，控制每個 node 使用少量的記憶體。我實測的情況，Redis 使用 5 GB 時，差不多會花 0.1s fork。</p><h3>Expie 的問題</h3><p>Redis 有兩種時機會刪除 expired keys：</p><ul><li>存取 key 之前檢查，過期了就刪掉。</li><li>每 100ms 檢查一次，隨機抽查數個 keys，刪掉過期的 keys。重覆抽查直到過期的 keys &lt; 25% 為止。</li></ul><p>這作法滿有趣的，理論上不用擔心設定 <a href="https://redis.io/commands/expire">expire</a> 會太花時間，畢竟這是相當常使用的指令。還可以維持占用穩定的 CPU 時間清垃圾，確保不會積太多垃圾占據記憶體。</p><p>但這表示若不小心設定大量的 keys 在同一時間過期，過期時會卡住許久，直到過期的 keys 刪到 &lt; 25%。</p><h3>備忘摘要</h3><p>文末附上部份讀後的摘要。</p><ul><li><strong>即使用 AOF 仍會用 fork 備份</strong></li></ul><p><a href="https://redis.io/topics/latency#latency-generated-by-fork">https://redis.io/topics/latency#latency-generated-by-fork</a></p><blockquote>In order to generate the RDB file in background, or to rewrite the Append Only File if AOF persistence is enabled, Redis has to fork background processes. The fork operation (running in the main thread) can induce latency by itself.</blockquote><ul><li><strong>Xen 的 fork 更慢，不過 AWS 已修正此問題</strong></li></ul><p><a href="https://redis.io/topics/latency#fork-time-in-different-systems">https://redis.io/topics/latency#fork-time-in-different-systems</a></p><blockquote>Modern hardware is pretty fast at copying the page table, but Xen is not. The problem with Xen is not virtualization-specific, but Xen-specific. For instance using VMware or Virtual Box does not result into slow fork time.<br>…<br>However the good news is that new types of EC2 HVM based instances are much better with fork times, almost on par with physical servers, so for example using m3.medium (or better) instances will provide good results.</blockquote><ul><li><strong>Redis 會嘗試避免 fsync 卡住 write，但仍有可能發生</strong></li></ul><p><a href="https://redis.io/topics/latency#latency-due-to-aof-and-disk-io">https://redis.io/topics/latency#latency-due-to-aof-and-disk-io</a></p><blockquote>When appendfsync is set to the value of everysec Redis performs an fsync every second. It uses a different thread, and if the fsync is still in progress Redis uses a buffer to delay the write(2) call up to two seconds (since write would block on Linux if an fsync is in progress against the same file). However if the fsync is taking too long Redis will eventually perform the write(2) call even if the fsync is still in progress, and this can be a source of latency.</blockquote><ul><li><strong>清 expired keys可能會卡住 Redis</strong></li></ul><p><a href="https://redis.io/topics/latency#latency-generated-by-expires">https://redis.io/topics/latency#latency-generated-by-expires</a></p><blockquote>Basically this means that if the database has many many keys expiring in the same second, and these make up at least 25% of the current population of keys with an expire set, Redis can block in order to get the percentage of keys already expired below 25%.<br>…<br>In short: be aware that many keys expiring at the same moment can be a source of latency.</blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4ac4ad2c414c" width="1" height="1" alt=""><hr><p><a href="https://medium.com/fcamels-notes/redis-latency-%E7%9B%B8%E9%97%9C%E9%9B%9C%E8%A8%98-4ac4ad2c414c">Redis latency 相關雜記</a> was originally published in <a href="https://medium.com/fcamels-notes">fcamel的程式開發心得</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Redis 實戰心得]]></title>
            <link>https://medium.com/fcamels-notes/redis-%E5%AF%A6%E6%88%B0%E5%BF%83%E5%BE%97-b8ea52e0bab4?source=rss----fc4919ea89c8---4</link>
            <guid isPermaLink="false">https://medium.com/p/b8ea52e0bab4</guid>
            <category><![CDATA[distributed-systems]]></category>
            <category><![CDATA[redis]]></category>
            <category><![CDATA[cache]]></category>
            <dc:creator><![CDATA[fcamel]]></dc:creator>
            <pubDate>Tue, 07 Jul 2020 16:27:21 GMT</pubDate>
            <atom:updated>2020-08-23T07:42:36.669Z</atom:updated>
            <content:encoded><![CDATA[<p>網路上很多這類心得，像<a href="https://mp.weixin.qq.com/s?__biz=MzA4MTc4NTUxNQ==&amp;mid=2650519808&amp;idx=1&amp;sn=78d404047311bdc83a3a9f650e045f06">這篇</a>提供許多有用的建議。本篇是我最近實戰後的隨手記。</p><h3>資料分群</h3><p>Redis 沒有 table，資料存在同一個地方。<a href="https://www.mikeperham.com/2015/09/24/storing-data-with-redis/">這篇</a>分析三種依服務類型存資料的方法：</p><ul><li>namespace: 在 key 的前面加上如 user: 的 prefix，所有資料放在一個 Redis 內。</li><li>使用 database: 用指令 select 切換用不同的 “database”。</li><li>用不同的 Redis servers。</li></ul><p>Redis Cluster 只能用 database 0，所以為了長遠 scalability 考量，database 並不適用。<a href="http://blog.kankanan.com/article/52ff7528-redis-7684591a5e93.html">Redis 作者甚至認為 database 是他作過 Redis 設計裡最糟的決定。</a></p><p>用不同 Redis servers 管理成本較高。或許分成兩群 Redis Cluster 會不錯？例如一群設成 LRU mode 存 cache 資料；另一群存 persistent data。這樣可以各別設適合的 shards、replicas 和 OOM 時的處理方式。</p><p>關於 namespace 的作法，建議用 : 區分，因為<a href="https://dzone.com/articles/the-top-6-free-redis-memory-analysis-tools">不少 keys 分析的工具，假設用 </a><a href="https://dzone.com/articles/the-top-6-free-redis-memory-analysis-tools">: 為 keys 分群</a>。如果已有大量資料沒用 : 分群，可以用 <a href="https://github.com/snmaynard/redis-audit">redis-audit</a>，它會自動分群，並可加入部份規則，減少自動分群花的時間。</p><h3>Keys 的格式</h3><p>可以的話，不要用 binary 會比較方便。像 redis-cli --scan 無法處理 non-printable 字元。要掃出全部 keys 得另寫小程式。雖然很多事都可寫小程式解決，但是分析和除錯時，就沒那麼方便。例如前面提到的 redis-audit，無法處理 binary keys，在 parsing error 的地方得加上 key.scrub(‘’)才能用。</p><h3>使用 Expiration</h3><p>資料多了以後，很難管理。能加上 expiration 就加，避免無止盡的堆垃圾到記憶體裡。</p><p>Redis 只支援第一層 keys 加 expiration，若需要用 hash 但 hash 部份內容需要加 expiration，最好將一個大 hash 攤成多個小 hash 或是 string，這樣才能善用 expiration。附帶一提，<a href="https://redis.io/topics/memory-optimization?fbclid=IwAR3RHWhFNiB6sUYuEgXf1O241MAVhm2SNsMpaaw3ERdMgvWaivSeyyCX1Qs#using-hashes-to-abstract-a-very-memory-efficient-plain-key-value-store-on-top-of-redis">多個小 hash 比一個大 hash 更省空間，因為 Redis 有針對小 hash 最佳化儲存方式</a>。</p><h3>分析占空間的 keys</h3><p>除前面提的<a href="https://dzone.com/articles/the-top-6-free-redis-memory-analysis-tools">分析工具</a>外 ( 我是用 <a href="https://github.com/snmaynard/redis-audit">redis-audit</a> )，用 redis-cli 也可以找出顯著的問題：</p><ul><li>用 redis-cli scan 0 count 100 取樣 100 筆，找出常見的 keys，看看數量分佈是否合理。0 可以換成其它數字，redis-cli 仍能正常運作，藉此當作簡易取樣。</li><li>用 redis-cli --bigkeys，這<a href="https://redis.io/topics/rediscli#scanning-for-big-keys">指令</a>會取樣找出各資料類型最大的 keys。</li><li>用 <a href="https://redis.io/commands/debug-object">DEBUG OBJECT KEY</a> 看 KEY serialized 後的大小。</li><li>用 <a href="https://redis.io/commands/object">OBJECT IDLETIME KEY</a> 看 KEY 多久沒有被存取。注意，預設設定下，它不會傳回正確的值。</li></ul><h3>Latency</h3><p><a href="https://redis.io/topics/latency">官方文件 latency</a> 有落落長的教學，另<a href="https://medium.com/fcamels-notes/redis-latency-%E7%9B%B8%E9%97%9C%E9%9B%9C%E8%A8%98-4ac4ad2c414c">寫一篇摘要重要的部份</a>。</p><p>我自己有用到的是：</p><ul><li>使用 <a href="https://redis.io/topics/benchmarks">benchmark</a> 了解自己機器合理的 op/s。</li><li>用 <a href="https://redis.io/commands/slowlog">slowlog</a> 找 ≥ 10ms 的操作。Redis 只有main thread 處理資料，有 100 個 10ms 操作，一秒就只能處理 100 個指令而已了。 10ms 是很久的。</li><li><a href="https://stackoverflow.com/questions/45818371/is-the-unlink-command-always-better-than-del-command/45820714">使用 unlink 取代 del 刪除 key</a>。Redis 會用 background thread 處理 unlink 後的 keys，不會像 del 卡到 main thread。試想砍一個 ≥ 1M 筆資料的 hash / zset，可能會花 10s 以上。這表示 Redis 服務會停擺 10s。</li><li>留意 Redis client lib 是否有自己的 thread pool。如果是在 lib 的 thread pool 處理 Redis 返回值，要將花 CPU 時間的操作轉移到別的 thread 執行，避免卡到 Redis client lib 的 thread pool。</li><li>開啟 <a href="https://redis.io/topics/latency-monitor">latency monitor</a>，使用 <a href="https://redis.io/commands/latency-doctor">latency doctor</a> 檢查有何可改善之處。</li></ul><h3>Lua Script</h3><ul><li>Lua 是唯一能妥善作好 atomic 操作的方法，並且可減少 roundtrip time。熟悉使用 Lua 長遠來說是好事。</li><li>但 Lua 沒用好會卡住 Redis 很久，也不能隨意中止 Lua script 執行。使用時要非常小心，不要寫複雜的操作。</li><li><a href="https://redis.io/topics/cluster-tutorial#redis-cluster-data-sharding">使用 hash tag ( “{…}”) 將 Lua script 用到的 keys 分到同一個 hash slot</a>。</li><li>Lua 的 index 從 1 開始。</li><li>“ 和 ‘ 不同，雙引號才會將 \x12 表示成 16 進位。</li><li>Lua 是 dynamic typing，table 是主要的容器 (associative array)。在 table 找不到資料時，會回傳 nil。</li><li><a href="https://redislabs.com/blog/5-6-7-methods-for-tracing-and-debugging-redis-lua-scripts/">除錯方法：可以用 redis.log() 寫資訊到 Redis server log 或存到 table 裡，在 script 結束後回傳 table</a>。</li><li>redis-cli 會將 non-printable 字元轉成 16 進位印出來。在 redis-cli 可用<a href="https://redis.io/commands/eval#available-libraries">eval + pack/unpack</a> 編碼解碼。像這樣：</li></ul><pre>127.0.0.1:6379&gt; eval &#39;return struct.unpack(&quot;&gt;i4&quot;, ARGV[1])&#39; 0 &quot;\x01\x02\x03\x04&quot;<br>(integer) 16909060</pre><h3>同步資料</h3><p>Redis 有 cluster mode 也有單節點的 master-replica 模式。有些別人寫好的工具，可用來同步 standalone node 和 redis cluster，例如 <a href="https://github.com/alibaba/RedisShake">redis-shake</a>。在升級單一節點為 redis cluster 時，可以減少停機時間。</p><h3>Redis Cluster Client Library 的設定</h3><p>Redis 有多種模式，像是單機、Master-Replica、Sentinel、Cluster，這些模式 <a href="https://lettuce.io/">Lettuce</a> 都支援。但要留意 cluster mode 預設只有建立第一個連線後會取得 cluster node topology，之後不會處理 topology change。也就是說，failover 後，app (即 redis client) 不會知道要連往新的 master，導致服務繼續處於異常狀態。</p><p><a href="https://github.com/lettuce-io/lettuce-core/wiki/Redis-Cluster#refreshing-the-cluster-topology-view">Lettuce 提供三種方式 refresh topology</a>：</p><ol><li>App 手動呼叫 RedisClusterClient.reloadPartitions()</li><li>定期在背景呼叫 reloadPartitions()</li><li>動態偵測某些條件 (例如收到 MOVED 或是重連數次) 後呼叫 reloadPartitions()</li></ol><p>無腦地在背景呼叫最單純，但<a href="https://groups.google.com/u/1/g/lettuce-redis-client-users/c/jdJrR0PqJOg">效能不佳</a>，因為所有 redis client 會連往所有它們知道的節點。也可以<a href="https://lettuce.io/core/release/api/io/lettuce/core/cluster/ClusterTopologyRefreshOptions.Builder.html#dynamicRefreshSources-boolean-">設定使用 initial seeds 而非 discovered nodes</a>，減少負擔。</p><p>綜合一些考量，設定 connection/command timeout，並在發生 connection/command timeout 後自己手動呼叫 reloadPartitions()，行為比較好控制。或是遇到意外狀況時，重建 client 也行。</p><p>之後有什麼適合的內容，再持續補完。</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b8ea52e0bab4" width="1" height="1" alt=""><hr><p><a href="https://medium.com/fcamels-notes/redis-%E5%AF%A6%E6%88%B0%E5%BF%83%E5%BE%97-b8ea52e0bab4">Redis 實戰心得</a> was originally published in <a href="https://medium.com/fcamels-notes">fcamel的程式開發心得</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Redis 和 Redis Cluster 概念筆記]]></title>
            <link>https://medium.com/fcamels-notes/redis-%E5%92%8C-redis-cluster-%E6%A6%82%E5%BF%B5%E7%AD%86%E8%A8%98-fdc19a3117f3?source=rss----fc4919ea89c8---4</link>
            <guid isPermaLink="false">https://medium.com/p/fdc19a3117f3</guid>
            <category><![CDATA[cache]]></category>
            <category><![CDATA[redis]]></category>
            <category><![CDATA[distributed-systems]]></category>
            <dc:creator><![CDATA[fcamel]]></dc:creator>
            <pubDate>Sat, 27 Jun 2020 16:40:04 GMT</pubDate>
            <atom:updated>2020-11-14T12:22:45.380Z</atom:updated>
            <content:encoded><![CDATA[<p>官網寫得簡潔又清楚，多數文件看官網即可。安裝 Redis 也很簡單，server 和 redis-cli 一包裝好，不需要用線上的 playground。推薦先看 <a href="https://redis.io/topics/faq">FAQ</a>，對 Redis 有個概念。</p><p>Redis 可以單機跑，也可以跑 cluster mode。本文若沒有特別提及，均是指兩者共同行為。</p><h3>需求分析</h3><p>從 <a href="https://medium.com/fcamels-notes/designing-data-intensive-applications-%E8%AE%80%E5%BE%8C%E5%BF%83%E5%BE%97-4-662a94185e50">Designing Data-Intensive Applications 提到分散式系統的需求</a>來分析:</p><ul><li>Reliable: Redis Cluster 提供 sharding 和 replicas。Redis 使用 RDB 和 AOF 備份。前者是壓縮後的 snapshot。後者是 append-only logs，盡可能備份最新資料。</li><li>Scalable: 對 keys 用 CRC16 產生 16384 個 hash slots，Redis Cluster 將 hash slots 和 replicas 分配給不同的 primary nodes，可以動態調整分配，服務不會受影響。</li><li>Maintainable: <a href="https://redislabs.com/redis-enterprise-cloud/">Redis Enterprise Cloud</a> 和 <a href="https://aws.amazon.com/elasticache/redis/">AWS ElastiCache for Redis</a> 以 serverless 的方式提供服務，不需人力維護系統。兩者記費方式不同。Redis Enterprise Cloud 是用 ops/s 或 memory size 計價；ElastiCache 用 EC2 instances 型別和數量計價。</li></ul><p>Redis Cluster (open source 版本) 解決了前兩個需求，有指令可以調整 hash slots、replicas 和 shards 的分配。Redis Enterprise Cloud 和 AWS ElastiCache for Redis 則是幫你監控和執行必要的指令。整體來說，Redis 是很完備的分散式系統。</p><p><a href="https://redis.io/topics/cluster-tutorial">Redis cluster tutorial</a> 從使用者角度說明簡要的資訊；<a href="https://redis.io/topics/cluster-spec">Redis Cluster Specification</a> 將設計細節描述得很清楚，包含搬 shard 期間發生什麼事 ，如何作 failover 等。看完後覺得「理論上」應該沒有需要擔心的事。<a href="https://mp.weixin.qq.com/s?__biz=MzA4MTc4NTUxNQ==&amp;mid=2650520049&amp;idx=1&amp;sn=17a260f877b3ac4ff2b393981213ff94">這篇</a>也提供許多有用的資訊。</p><h3>基本概念</h3><ul><li>Redis 是 <strong>persistent</strong> storage，<strong>全部資料</strong>存在記憶體內，所以資料大小上限受限於記憶體。資料會週期性備份到硬碟上 (RDB) 或是將所有更新寫入 append-only logs (AOF)。因此可以提供最快的操作。</li><li>預設資料<strong>不會 expire</strong>。可以用 <a href="https://redis.io/commands/expire">expire</a> 指定 expire 時間。預設是記憶滿了就不能寫入資料，設定 <a href="https://redis.io/topics/lru-cache">LRU</a> 的模式可以決定是只刪有設 expire 的 keys 或是都刪。例如 volatile-ttl 會照 TTL 優先刪掉 TTL 最小的 key 且只會刪有設 expire 的 keys。</li><li>使用 multi-data mode，提供多種常用資料結構如 <a href="https://redis.io/commands#sorted_set">sorted set</a>、hash、geospatial、publish/subscribe events。可裝 plugin 使用其它資料結構。<a href="https://www.youtube.com/watch?v=ELk_W9BBTDU">《Redis Data Structures for Non-Redis Users》</a>有為入門者的介紹，講得簡單易懂。</li><li>主程式在單一 thread 執行，不用擔心 race conditions。但要留意執行太慢的操作會卡住整個系統。官方文件有寫明所有操作的 time complexity，很好評估操作效率。</li><li>因為記憶體操作超快，減少呼叫 Redis 的次數是效率關鍵。有提供 <a href="https://redis.io/commands/incr">incr</a> 這類操作減少 read → write。要作更多操作時，可用 lua script。</li><li>提供 <a href="https://redis.io/commands/eval">Lua script</a> 作 atomic 操作。執行 <a href="https://redis.io/commands/eval">Lua script</a> 前會檢查，不適合執行的 script 會直接失敗。像是呼叫 nondeterministic 函式之後寫入資料，或是cluster mode 用到放到不同 slots 的 keys。<a href="https://redislabs.com/blog/5-6-7-methods-for-tracing-and-debugging-redis-lua-scripts/">這裡有介紹 Lua script 除錯的小技巧</a>。要小心不要執行太費時的 lua script 卡住其它請求。</li></ul><h3>和 Memcached 比較</h3><ul><li>Memcached 用 multi-thread 處理請求，Redis 只有 main thread。</li><li>Memcached 只支援簡單的資料型別。</li></ul><p>如果只需要簡單的資料型別，memcached 較能善用 CPU。例如 AWS EC2 的 CPU 和 memory 是同步成長的，為了用更多 memory 而租高級的型別，會浪費一堆沒在用的 CPU。</p><h3>效能分析和除錯</h3><ul><li><a href="https://redis.io/topics/rediscli">redis-cli</a> 提供很多有用的工具，例如 redis-cli monitor可以看打入 Redis 的指令，方便 debug。推薦好好地讀完 <a href="https://redis.io/topics/rediscli">redis-cli 的文件</a>。</li><li><a href="https://redis.io/commands/slowlog">slowlog</a> 會取出最近較慢的操作。預設會記錄 ≥ 10ms 的指令。</li><li><a href="https://redis.io/topics/benchmarks">redis-benchmark</a> 用來測效能。</li><li><a href="https://dzone.com/articles/the-top-6-free-redis-memory-analysis-tools">這篇</a>介紹六個免費分析記憶體使用量的工具。我試了 <a href="https://github.com/snmaynard/redis-audit">Redis-audit</a>，滿好用的，可以自動對 keys 分群。若有手動加入已知的分群規則，可以大幅縮短分析時間。若 keys 用 binary 而非 Unicode 表示，需要 patch code 在遇到 parsing error 的地方加上 key.scrub(‘’)。</li></ul><h3>Redis Cluster</h3><ul><li>提供 hash tags 的語法將相關的 keys 放到同一個 shard。例如 key “hello{world}” 會用 “world” 計算 hash slot。因此，”hello{world}” 和 “googlebye{world}” 會放在同一個 node。</li><li>讓 client 記住 hash slots 和 Redis nodes 的對應，因此 client 使用單台 Redis 和 Redis Cluster 的 response time 是一樣的。</li><li>沒有支援 strong consistency。對於重要的資料，可用 <a href="https://redis.io/commands/wait">WAIT</a> 強迫 N 個 replicas 同步資料後才返回。</li><li>預設 replicas 會將 client 導向 primary node。可以用 <a href="https://redis.io/commands/readonly">readonly</a> 向 replicas 讀資料。</li><li>多數 nodes 在 2 * NODE_TIMEOUT 的時間內發覺 node A ≥ NODE_TIMEOUT 沒有回應，會對 A 作 failover。</li><li>搬 shard 的期間，Redis node 會回覆 client 暫時性處理該 hash slot 的 node addree。搬完後會回覆 client 已永久性搬到的 node address。client 可用 <a href="https://redis.io/commands/cluster-slots">CLUSTER SLOTS</a> 取得最新的全部對應。</li></ul><p>使用 sharding 可同時分散讀和寫，加上 Redis 反應很快，從效能的角度來看，可能沒有用 replicas 的必要。但從 high availability 的角度來看，用 replicas 可縮短 failover 時間，因為 Redis server 載入 RDB 需要一段時間，資料量有 100GB 時，可能會到十分鐘以上。</p><h3>其它雜記</h3><ul><li>Redis Enterprise Cloud 提供 <a href="https://redislabs.com/redis-enterprise/technology/redis-on-flash/">Redis on Flash</a>，將 keys 和 hot values 存在 RAM，warm values 存在 Flash。可滿足 working set 不大但 dataset 很大且多數 values 不大的需求。Redis on Flash 會用額外的 threads 存取放在 flash 的資料。</li><li>因為全部資料都在記憶體，需要備份時，fork child process，然後讓 child process 將全部資料寫入硬碟即可。沒有想過 isolation snapshot 這麼容易實作。</li><li><a href="https://podcasts.google.com/feed/aHR0cHM6Ly9vcGVuc291cmNldW5kZXJkb2dzLmNvbS9mZWVkLw/episode/aHR0cHM6Ly9vcGVuc291cmNldW5kZXJkb2dzLmNvbS8_cD0xMjQ4?fbclid=IwAR0Q9R0W5vPY-NoKmVvbua0fuoiosbycch_GrNpcXn-2gwwSv7igCUxcU4Y">Episode 20: Redis Labs — Database for the Instant Experience with Ofer Bengal</a>：這則 podcast 介紹 Redis Labs 的商業策略。command、datatype 這類功能 open source，但是 deployment、operation 要用他們的商用版。一般用戶可以安心用免費版，規模夠大時就有錢買商用版解決 scalability 的痛點。</li><li><a href="https://www.youtube.com/watch?v=NymIgA7Wa78">Box 於 2018 說明他們如何使用 redis cluster</a>：主要管 session data (30 days login) 和 recent files。使用 sharding + 2 replicas。replicas 只用提升 HA，讀寫都用 primary 避免 read consistency 問題。使用 AOF: appendonly + fsync per 30s。在 slave 額外使用 RDB (snapshot) 作 backup per hour。</li><li>使用 key-value store 時，最好幫 keys 加上 namespace，像是 A:hello 表示是類型 A 用的 key hello。之後比較方便分析那種類型的 keys 占比較多資源，要砍 keys 也比較不用擔心砍錯。</li><li><a href="https://stackoverflow.com/questions/45818371/is-the-unlink-command-always-better-than-del-command/45820714#45820714">用 unlink 取代 del</a>，避免在刪大量資料時占住 main thread。</li><li><a href="https://redis.io/commands/expire">Redis 砍掉 expired keys 的方法是存取到時才砍，並且定期取樣檢查。</a>定期取樣檢查是滿有意思的設計，可以穩定地使用 CPU 資源處理過期資料。</li><li><a href="https://mp.weixin.qq.com/s?__biz=MzA4MTc4NTUxNQ==&amp;mid=2650519808&amp;idx=1&amp;sn=78d404047311bdc83a3a9f650e045f06">Redis Best Practice</a>: 有許多中肯實用的建議。</li></ul><h3>結語</h3><p>Redis 實在太好上手了，官方文件也清楚，反而不太知道要寫什麼…。初用 Redis 最困擾我的問題是資料是否可存在記憶外？是否永久儲存？和 memcached 有什麼差別？這些都寫在前面了。</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=fdc19a3117f3" width="1" height="1" alt=""><hr><p><a href="https://medium.com/fcamels-notes/redis-%E5%92%8C-redis-cluster-%E6%A6%82%E5%BF%B5%E7%AD%86%E8%A8%98-fdc19a3117f3">Redis 和 Redis Cluster 概念筆記</a> was originally published in <a href="https://medium.com/fcamels-notes">fcamel的程式開發心得</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>