別名演算法 Alias Method

 題目

每個伺服器支援不同的 TPM (transaction per minute)
當 request 來的時候, 系統需要馬上根據 TPM 的能力隨機找到一個適合的 server.
雖然稱為 "隨機", 但還是需要有 TPM 作為權重.

解法

別名演算法(Alias Method)是一種有效率地進行加權隨機選擇的演算法,尤其適用於當元素的權重分布固定,且需要頻繁進行選擇的情況。這種演算法特別適合於情況如抽選伺服器或其他資源分配的任務中,因為它能在O(1)時間內完成選擇,使得運算效率極高。

別名演算法的核心概念:
1. 正規化與縮放:

首先,將每個元素(如伺服器)的TPM值(或任何其他量度)轉換為概率值,並根據元素總數進行縮放。例如,如果有三個伺服器,其TPM值分別為100、200和300,則總TPM為600。每個伺服器的概率值將被計算為其TPM除以總TPM,然後乘以元素總數(在這個例子中是3)。
2. 創建概率和別名表:

使用兩個陣列,一個存儲概率(probability),另一個存儲別名(alias)。概率陣列中的每個索引對應一個伺服器,值表示直接選擇該伺服器的概率。如果這個概率小於1,則需要一個別名來調整概率使之平衡。
利用兩個臨時陣列,一個用於存儲概率小於1的索引(small),另一個用於存儲概率大於1的索引(large)。從large陣列中取元素來補充small陣列中的元素,直到所有元素的概率都調整到1。
3. 選擇過程:

在選擇過程中,首先隨機生成一個索引,然後檢查這個索引處的概率是否足以直接選擇。如果隨機數大於該概率值,則使用存儲在別名陣列中該索引處的值作為替代選擇。
實現的優勢:
別名演算法的主要優點是其選擇步驟的時間複雜度為O(1),非常適合需要快速響應的應用場景。初始化過程雖然較為複雜,但只需要在數據更新或初始設定時執行一次,之後的每次選擇都極為迅速。

使用場景:
這種演算法適用於任何需要加權隨機選擇的場景,如網絡服務的負載平衡,廣告投放,遊戲中的隨機事件生成等場合,特別是當選擇操作的頻率遠高於權重更新的頻率時,別名演算法顯得尤為有用。

程式

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

class Server {
    String name;
    Long tpm;

    public Server(String name, Long tpm) {
        this.name = name;
        this.tpm = tpm;
    }
}

public class ServerSelector {
    private List servers;
    private Random random = new Random();
    private int[] alias;
    private double[] probability;

    public ServerSelector(List servers) {
        this.servers = servers;
        initializeAliasTable();
    }

    private void initializeAliasTable() {
        int n = servers.size();
        double[] normalizedProbabilities = new double[n];
        List small = new ArrayList<>();
        List large = new ArrayList<>();
        long totalTPM = servers.stream().mapToLong(server -> server.tpm).sum();

        // Normalize probabilities and scale them by the number of servers
        for (int i = 0; i < n; i++) {
            normalizedProbabilities[i] = (double) servers.get(i).tpm / totalTPM * n;
            if (normalizedProbabilities[i] < 1.0) {
                small.add(i);
            } else {
                large.add(i);
            }
        }

        probability = new double[n];
        alias = new int[n];

        // Balance small and large lists to create probability and alias arrays
        while (!small.isEmpty() && !large.isEmpty()) {
            int less = small.remove(small.size() - 1);
            int more = large.remove(large.size() - 1);

            probability[less] = normalizedProbabilities[less];
            alias[less] = more;

            normalizedProbabilities[more] += normalizedProbabilities[less] - 1.0;
            if (normalizedProbabilities[more] < 1.0) {
                small.add(more);
            } else if (normalizedProbabilities[more] > 1.0) {
                large.add(more);
            }
        }

        // Ensure all remaining probabilities are set to 1
        for (int index : large) {
            probability[index] = 1.0;
        }
        for (int index : small) {
            probability[index] = 1.0;
        }
    }

    public Server selectServer() {
        int index = random.nextInt(servers.size());
        boolean useAlias = random.nextDouble() > probability[index];
        return servers.get(useAlias ? alias[index] : index);
    }
}

Java Class Loader 與 Maven Shade Plugin

ChatGPT generated 



Java Class Loader 是 Java Runtime Environment 的一部分,它動態地將 Java 類別加載到 Java Virtual Machine 中。通常,只有在需要時才會加載類別。Java 運行時系統不需要知道文件和文件系統,因為這些都被委派給了類別加載器。

在 Java 語言中,軟體庫通常被打包在 JAR 文件中。庫可以包含不同類型的對象。JAR 文件中包含的最重要類型的對象是 Java 類別。類別可以被視為一個有名稱的程式碼單元。類別加載器負責定位庫,讀取其內容,並加載庫中包含的類別。這種加載通常是 "按需" 進行的,也就是說,直到程式呼叫該類別時才會進行。給定名稱的類別只能被給定的類別加載器加載一次

每個 Java 類別都必須由類別加載器加載。此外,Java 程式可能會使用外部庫(也就是,由程式的作者以外的人寫的和提供的庫),或者至少部分由多個庫組成。

當 JVM 啟動時,會使用三個類別加載器:

  1. Bootstrap class loader
  2. Extensions class loader
  3. System class loader

Bootstrap class loader 加載位於 <JAVA_HOME>/jre/lib(或對於 Java 9 及以上版本,位於 <JAVA_HOME>/jmods)目錄中的核心 Java 庫。這個類別加載器是 JVM 的核心部分,是用原生碼寫的。
Extensions class loader 加載在擴展目錄(<JAVA_HOME>/jre/lib/ext,或由 java.ext.dirs 系統屬性指定的任何其他目錄)中的程式碼。
System class loader 加載在 java.class.path 上找到的程式碼,該路徑映射到 CLASSPATH 環境變量。


當 JVM 需要找到一個類別時,它會遵循以下的步驟:

  1. Bootstrap ClassLoader:首先,Bootstrap ClassLoader 會嘗試在 JVM 的 bootstrap classpath 中找到這個類別。這通常包括像 <JAVA_HOME>/jre/lib/rt.jar 這樣的核心 Java 類別庫。Bootstrap ClassLoader 是用原生碼實現的,並且是 JVM 中的最高級別的類別加載器。
  2. Extension ClassLoader:如果 Bootstrap ClassLoader 找不到該類別,那麼 Extension ClassLoader 會嘗試在擴展目錄(例如 <JAVA_HOME>/jre/lib/ext)中找到這個類別。
  3. Application ClassLoader:如果 Extension ClassLoader 也找不到該類別,那麼 Application ClassLoader(也稱為 System ClassLoader)會嘗試在應用的 classpath 中找到這個類別。這包括由 CLASSPATH 環境變量,-cp 或 -classpath 命令行選項,或者是應用的 Manifest 文件中的 Class-Path 屬性指定的所有目錄和 JAR 文件。

如果所有這些類別加載器都找不到該類別,那麼 JVM 會拋出一個 ClassNotFoundException。

在這個過程中,每個類別加載器都會遵循所謂的 "委託模型"。這意味著在一個類別加載器嘗試加載一個類別之前,它首先會將這個任務委託給它的父類別加載器。這確保了類別加載的唯一性和安全性,因為這樣可以確保核心 Java 類別不能被應用的類別覆蓋。

至於如何決定從哪個 JAR 文件中加載類別,這實際上取決於類別的完全限定名稱以及 JAR 文件在 classpath 中的順序。JVM 會按照 classpath 中的順序,從每個 JAR 文件中嘗試加載類別,直到找到匹配的類別為止。如果有多個 JAR 文件包含同一個類別,那麼 JVM 通常會使用它在 classpath 中首次找到的那個版本。

在 Java 中,class 載入的行為主要由 java.lang.ClassLoader class 定義。這個 class 提供了一個方法 loadClass(String name),它實現了 class 載入的基本機制。這個方法首先檢查該 class 是否已經被載入,如果已經被載入,則直接返回該 class。如果還沒有被載入,則該方法會將載入任務委託給 parent class loader。只有當 parent class loader 無法載入該 class 時,該方法才會嘗試自己載入該 class。


當你遇到多個 JAR 文件包含相同類別的情況:

  1. 檢查並更新你的 CLASSPATH:確保 CLASSPATH 中的 JAR 文件順序正確,並且你需要的類別版本在前面的 JAR 文件中。
  2. 移除重複的 JAR 文件:如果可能,從你的應用和 CLASSPATH 中移除包含重複類別的 JAR 文件。
  3. 使用 Maven 或 Gradle 這樣的依賴管理工具:這些工具可以幫助你管理你的依賴並避免版本衝突。例如,Maven 的 "dependency:tree" 命令可以幫助你找出哪些依賴包含了相同的類別。
  4. 使用 Maven Shading Plugin:這個插件可以將你的應用的所有依賴打包到一個 uber-JAR 中,並且可以選擇性地將包名重新命名(或 "shade")以避免命名空間衝突。
  5. 使用 OSGi 或 Java 9 的模組系統:這些技術可以讓你更精細地控制哪些類別和 JAR 文件被加載。
  6. 使用自定義的類別加載器:在某些情況下,你可能需要實現你自己的類別加載器來解決這個問題。這通常需要較深的 Java 知識,並且可能會引入其他的複雜性。

Maven shading plugin example
  1. 這個 repo 包含兩個 Maven module:example-lib 和 example-main。
  2. 兩個 module 都使用了 Google 的 Guava lib,但是版本不同。example-lib 使用的是 Guava 31,而 example-main 使用的是 Guava 16。
  3. 在 example-lib module 中,有一個名為 Preconditions31 的 class,它使用了 Guava 31 的 Preconditions class。
  4. 在 example-main module 中,有一個名為 Hello 的 class,它同時使用了 Guava 16 的 Preconditions class 和 example-lib module 中的 Preconditions31 class。
  5. 這個 repo 的主要目的是演示如何使用 Maven 的 Shade 插件來解決 JAR 包中的 class 衝突問題。
  6. 在這個例子中,Shade 插件被用來將 example-lib module 中的 Guava 31 的 Preconditions class 重新命名為 shaded.example.lib.Preconditions31。
  7. 通過這種方式,example-main module 可以同時使用 Guava 16 和 Guava 31 的 Preconditions class,而不會產生衝突。

Scrum Note

ChatGPT generated


 Scrum 是一種敏捷軟體開發的框架,它的核心包含了三個支柱 (Pillars)、五個價值觀 (Values) 和十個原則 (Principles)。


三個支柱 (Pillars) 包括:


  1. 透明度 (Transparency):所有的工作都必須對所有人可見,這樣才能確保所有人都對工作的進度有共識。
  2. 檢驗 (Inspection):Scrum 團隊必須定期檢查進行中的工作和工作成果,以確保向著目標前進。
  3. 調整 (Adaptation):如果在檢驗過程中發現實際的結果與預期的結果有偏差,團隊必須調整他們的行為或者計劃。

五個價值觀 (Values) 包括:


  1. 承諾 (Commitment):團隊成員對於達成共同目標的承諾。
  2. 勇氣 (Courage):面對困難和挑戰時,團隊成員需要有勇氣去做出改變。
  3. 專注 (Focus):團隊成員專注於工作和目標,並且優先處理最重要的事情。
  4. 開放 (Openness):團隊成員對於工作、挑戰和進度要保持開放的態度。
  5. 尊重 (Respect):團隊成員相互尊重,認識到每個人都是獨一無二的,並且有他們自己的技能和能力。

敏捷宣言 (Agile Manifesto) 的四個核心價值包括:


  1. 個人和互動高於流程和工具 (Individuals and interactions over processes and tools)
  2. 可用的軟體高於詳盡的文件 (Working software over comprehensive documentation)
  3. 客戶合作高於合約談判 (Customer collaboration over contract negotiation)
  4. 回應變化高於遵循計劃 (Responding to change over following a plan)

敏捷宣言的十二個原則包括:

  1. 我們最高優先的是透過提早並持續地交付有價值的軟體來滿足客戶。
  2. 即使在開發的後期,我們也歡迎改變需求。敏捷流程利用變化為客戶獲得競爭優勢。
  3. 經常地交付可工作的軟體,交付的頻率可以從幾週到幾個月,交付的時間間隔越短越好。
  4. 業務人員和開發人員必須每天都要一起工作,直到項目完成。
  5. 建立項目周圍的人們的熱情,給他們所需要的環境和支持,並且信任他們能完成工作。
  6. 最有效且最有效率的傳遞信息的方法,就是面對面的交談。
  7. 可工作的軟體是進度的主要度量標準。
  8. 敏捷流程提倡可持續的開發。贊助者、開發者和使用者應該能夠保持恆定的步伐。
  9. 持續關注技術優秀性和良好的設計增強敏捷性。
  10. 簡單——使未完成的工作最大化的藝術——是必要的。
  11. 最好的架構、需求和設計出自自組織的團隊。
  12. 團隊定期反思如何能夠更有效,然後調整和改變其行為。

刺蝟原則 (Hedgehog Concept) 是由吉姆·柯林斯 (Jim Collins) 在他的書《從優秀到偉大》(Good to Great) 中提出的。這個概念源於古希臘詩人伊索的寓言,故事中的狐狸雖然有許多聰明的策略來捕捉刺蝟,但刺蝟只做一件事,且做得很好:捲成一個球來保護自己。柯林斯將這個故事用來形容公司和個人應該專注於他們可以做得最好的事情。


刺蝟原則包含三個部分:

  1. 你最熱衷什麼?(What are you deeply passionate about?)
  2. 你在什麼能力上可以成為世界最頂尖的人?(What can you be the best in the world at?)
  3. 你的經濟引擎是什麼?(What drives your economic engine?)

Scrum 定義了五種主要的會議,每種都有其特定的目標和結構:


  1. Sprint 規劃會議 (Sprint Planning Meeting):在這個會議中,團隊會決定下一個 Sprint 要完成的工作。產品擁有者 (Product Owner) 會解釋他們希望團隊在下一個 Sprint 中完成的項目,並且團隊會決定他們能夠完成多少工作。
  2. 每日 Scrum 會議 (Daily Scrum Meeting):這是一個每天都會進行的短會議,通常只會持續 15 分鐘。在這個會議中,團隊會分享他們在前一天完成了什麼,今天計劃完成什麼,以及是否有任何阻礙他們工作的問題。
  3. Sprint 審查會議 (Sprint Review Meeting):在每個 Sprint 結束時,團隊會舉行一個審查會議,以展示他們在 Sprint 中完成的工作。這是一個讓所有利益相關者了解項目進度的機會。
  4. Sprint 回顧會議 (Sprint Retrospective Meeting):這是在每個 Sprint 結束後的一個會議,團隊會討論在過去的 Sprint 中什麼工作得,什麼不好,以及他們可以如何改進。
  5. 產品 Backlog 整理會議 (Product Backlog Refinement Meeting):這是一個可選的會議,用於審查和更新產品 Backlog。在這個會議中,團隊會評估 Backlog 項目的優先順序,並可能將大的項目分解成更小、更可管理的項目。

常見的對 Scrum 的詬病


  1. 過度的會議和管理:Scrum 需要定期的 Sprint 規劃、每日 Scrum、Sprint 審查和回顧會議。對於一些團隊來說,這可能會感覺像是過度的會議和管理,並可能導致工作時間的浪費。
  2. 難以應對變化:雖然 Scrum 是為了應對變化而設計的,但在 Sprint 進行中改變計劃可能會很困難。這可能會導致團隊在面對變化時感到壓力,或者在需要快速反應時無法做出適應。
  3. 過度依賴 Scrum Master:Scrum Master 的角色是幫助團隊遵循 Scrum 的規則和實踐,並解決阻礙團隊進步的問題。然而,如果團隊過度依賴 Scrum Master,可能會降低團隊的自我組織能力。
  4. 忽視技術實踐:Scrum 並未明確包含如測試驅動開發 (TDD)、持續集成 (CI)、重構等技術實踐。如果團隊僅僅遵循 Scrum 的流程,而忽視了這些重要的技術實踐,可能會導致軟體質量的問題。
  5. Scrum 不是萬能藥:Scrum 並不能解決所有的問題。它是一種工具,需要根據特定的情況和團隊來適當地使用。如果盲目地遵循 Scrum,而不考慮其是否適合特定的情況,可能會導致效果不佳。

Java GC Note

CMS GC


Java 中的垃圾收集(Garbage Collection)的基本概念。在 Java 中,當我們創建對象時,這些對象會被存儲在一個名為堆(Heap)的內存區域中。當這些對象不再被使用時,它們就變成了"垃圾",需要被清理出堆,以釋放空間供新的對象使用。這就是垃圾收集的工作。

CMS GC 是 Java 中的一種垃圾收集器,它的主要目標是減少垃圾收集導致的應用程序停頓時間。它的工作流程大致如下:

  1. 初始標記(Initial Mark):這是一個需要停止所有應用程序線程的階段,也就是我們所說的 "Stop-The-World"。在這個階段,GC 會標記出所有從根(Root)開始直接可達的對象。
  2. 並行標記(Concurrent Mark):在這個階段,GC 會在應用程序線程運行的同時,標記出所有從根對象開始間接可達的對象。這個階段的名稱 "並行",就是指 GC 能夠與應用程序線程同時運行。
  3. 重新標記(Remark):這也是一個 "Stop-The-World" 階段。因為在並行標記階段,應用程序線程還在修改對象引用,所以可能會有一些新的可達對象被遺漏,或者有一些已經不再可達的對象被錯誤地標記。因此,GC 需要再次停止所有應用程序線程,對這些對象進行重新標記。
  4. 並行清除(Concurrent Sweep):在這個階段,GC 會清除掉所有未被標記的對象,也就是不再被應用程序使用的對象。這個階段也是並行進行的,不會影響應用程序的運行。


在 CMS GC 中,一個物件可能會經歷以下的生命週期:

物件創建:當你在 Java 程式中創建一個新的物件,它會被存放在 Heap 中。
物件使用:在物件被創建後,你的程式可能會使用這個物件,這可能包括讀取物件的屬性,調用物件的方法,或者將物件作為參數傳遞給其他方法。
物件變為垃圾:當你的程式不再使用一個物件,並且沒有任何引用指向這個物件時,這個物件就變成了垃圾。這意味著這個物件不再可達,並且可以被垃圾收集器清理。
垃圾收集:在 CMS GC 的運作過程中,這個物件會被標記為垃圾,並在適當的時候被清理。如果物件在並行標記階段被標記,那麼它將在並行清除階段被清理。如果物件在重新標記階段被標記,那麼它將在下一個並行清除階段被清理。
物件清理:當物件被清理時,它佔用的內存空間將被釋放,並可以被用來存放新的物件。在這個過程中,物件的終結器(如果有的話)將被調用。

  1. 並行標記清除 GC (CMS)
    • 這是為了偏好較短的垃圾收集暫停的應用程式而設計的。
    • 它適合互動式的應用程式。
    • CMS GC 使用多個 GC 線程來掃描 heap。
    • 它的大部分工作是與應用程式線程並行進行的。
  2. Heap 結構
    • Heap 被劃分為:Eden, Survivor (S0 和 S1), Old。
    • 物件最初在 Eden 中分配。
    • 每次 Minor GC 後,活著的物件被移動到一個 Survivor 空間。
    • 在 Survivor 空間中生存足夠長時間的物件被移動到 Old generation。
  3. GC 過程
    • Minor GC:清理 Eden 和 Survivor 空間。
    • Major GC (也被稱為 Concurrent Mark Sweep GC):清理 Old generation。
    • Major GC 是並行的,這意味著它在應用程式運行的時候發生。
    • 如果 Old generation 變滿,將會發生 Full GC,這是一個停止世界的事件。
  4. 調整
    • CMS GC 是可調的,允許你在吞吐量和暫停時間之間取得平衡。
    • 重要的選項包括:-XX:ParallelCMSThreads, -XX:CMSInitiatingOccupancyFraction。

不過 CMS GC 執行久了會有記憶體碎片化的問題, 這問題主要來自於它的垃圾收集策略。CMS GC 是一種以減少暫停時間為目標的垃圾收集器,它的主要工作是在應用程式運行的同時進行垃圾收集。為了達到這個目標,CMS GC 在大部分時間內都避免進行記憶體整理。
在 CMS GC 中,當物件被回收時,它們佔用的記憶體空間會被釋放,但並不會立即被重新使用。這些釋放的記憶體空間會形成一個個的"空洞",這些"空洞"分散在整個堆中,形成了所謂的"碎片"。隨著時間的推移,這些碎片可能會變得越來越多,導致記憶體碎片化。
記憶體碎片化可能會導致一些問題。例如,當應用程式需要分配一個大物件時,可能會找不到足夠大的連續記憶體空間,即使整體的可用記憶體空間是足夠的。在這種情況下,CMS GC 可能需要進行一次完全的垃圾收集和記憶體整理,這會導致一次長時間的暫停。

雖然 CMS GC 會把物件在 S0 和 S1(Survivor 0 和 Survivor 1)搬移,但它們是用於物件年齡管理和晉升的。在 Minor GC 中,活著的物件會在 Eden、S0 和 S1 之間移動,並最終被晉升到 Old Generation。這個過程確實會進行一些記憶體整理,但這只影響到 Young Generation,並不能解決 Old Generation 的記憶體碎片化問題。

STW - Stop the world


"Stop the World" 是 Java 垃圾收集中的一個術語。當垃圾收集器需要進行某些工作時,它會要求所有的應用程式線程(也就是你的 Java 程式正在執行的線程)暫時停止執行,這就是所謂的 "Stop the World"。

你可以將其想像成在公路上的交通管制。當有重要的車輛(例如警車或救護車)需要通過時,所有其他的車輛都必須停下來,讓重要的車輛先行。在這個比喻中,重要的車輛就像垃圾收集器,而其他的車輛就像應用程式的線程。

"Stop the World" 的時間通常會盡量短,以減少對應用程式的影響。但是,如果垃圾收集器需要清理大量的垃圾,或者需要進行大規模的內存整理,那麼 "Stop the World" 的時間可能會比較長。這就是為什麼在設計和選擇垃圾收集器時,我們需要考慮到 "Stop the World" 的影響。

在 CMS (Concurrent Mark Sweep) GC 中,Eden Space 的 Minor GC 是 Stop-The-World (STW) 的主要原因是為了簡化設計和提高效率。

  1. 簡化設計:Minor GC 是一個複雜的過程,它需要確定哪些物件是活著的,哪些物件是死的,並且需要更新所有活著的物件的引用。如果在這個過程中允許應用程式並行運行,那麼就需要處理應用程式可能會改變物件狀態和引用的複雜情況。這會使得 GC 的設計和實現變得更加複雜。因此,為了簡化設計,選擇在 Minor GC 時暫停應用程式。
  2. 提高效率:Minor GC 通常只處理 Eden Space,這是一個相對較小的區域,並且由於 Java 的物件分配模型,大部分物件在 Eden Space 中不會存活很長時間。因此,Minor GC 通常可以很快完成。如果在這個過程中允許應用程式並行運行,那麼就需要付出額外的同步和協調的代價,這可能會降低 GC 的效率。

因此,儘管 STW 會導致應用程式的暫停,但在 Eden Space 的 Minor GC 中使用 STW 是一種權衡和妥協的結果。

而 CMS (Concurrent Mark Sweep) GC 的 Remark Phase 是 Stop-The-World (STW) 的原因主要是為了確保堆中所有的物件都被正確地標記。

在 CMS GC 的 Concurrent Marking Phase(並行標記階段)中,GC 會在應用程式運行的同時標記堆中的物件。然而,由於應用程式在運行,所以可能會有新的物件被創建,或者已經存在的物件的引用關係被改變。這些變化可能會導致一些物件的標記狀態變得不正確。

為了解決這個問題,CMS GC 在 Concurrent Marking Phase 之後會進行 Remark Phase(重新標記階段)。在這個階段中,GC 會暫停應用程式,並重新掃描堆中的物件,確保所有的物件都被正確地標記。這就是為什麼 Remark Phase 是 STW 的。

雖然 Remark Phase 是 STW 的,但是由於它只需要處理在 Concurrent Marking Phase 中變化的部分,所以通常可以很快完成。這樣,CMS GC 可以在保證正確性的同時,盡可能地減少 STW 的時間。

G1GC


CMS (Concurrent Mark Sweep) GC 和 G1 (Garbage-First) GC 都是針對減少垃圾收集暫停時間的垃圾收集器。然而,它們在設計和實現上有一些重要的區別,這些區別可能會影響到你選擇哪一種垃圾收集器。以下是一些可能需要從 CMS GC 轉換到 G1 GC 的情況:

  1. 大堆需求:如果你的應用程式需要一個非常大的堆(例如,數十GB或更大),那麼 G1 GC 可能是一個更好的選擇。G1 GC 的設計目標是支持大堆,並且能夠預測垃圾收集的暫停時間。相比之下,CMS GC 的暫停時間可能會隨著堆的大小增加而增加。
  2. 預測性暫停時間:如果你需要更好的控制垃圾收集的暫停時間,那麼 G1 GC 可能是一個更好的選擇。G1 GC 提供了一個暫停時間目標,你可以設定這個目標,讓 G1 GC 儘可能地達到這個目標。
  3. 更高的吞吐量:雖然 CMS GC 在減少暫停時間方面表現出色,但是它可能會導致吞吐量下降,因為它需要在垃圾收集和應用程式之間進行更多的上下文切換。相比之下,G1 GC 通常可以提供更高的吞吐量。
  4. 更好的記憶體整理:CMS GC 可能會導致記憶體碎片化,特別是在長時間運行的應用程式中。當記憶體碎片化變得嚴重時,CMS GC 可能會導致不可預測的長暫停時間。相比之下,G1 GC 透過定期的整理操作,可以更好地處理記憶體碎片化問題。

G1GC(Garbage-First Garbage Collector)是Java的一種垃圾收集器,它是為了滿足大數據量和低延遲需求而設計的。
以下是G1GC的主要特點和工作原理:

分區收集:G1GC將Java堆分成多個相同大小的區域(Region),每個區域可能是Eden區、Survivor區或Old區。這種分區方式使得G1GC能夠獨立並且並行地收集每個區域,從而減少了單次垃圾收集的停頓時間。
Garbage-First:G1GC的名稱中的"Garbage-First"意味著它優先收集垃圾最多的區域。這種策略使得G1GC能夠最大限度地釋放空間,並且在有限的時間內達到最好的垃圾收集效果。
可預測的停頓時間:G1GC允許你指定最大的垃圾收集停頓時間,它會儘可能地在這個時間限制內完成垃圾收集。這種特性使得G1GC非常適合需要低延遲的應用。
並行和並行階段:G1GC在執行垃圾收集時,有一些階段可以與應用程序線程並行運行,這可以減少垃圾收集對應用程序的影響。
記憶體壓縮:G1GC有一個稱為"記憶體壓縮"的階段,它會在背景中將活躍對象從一個區域移動到另一個區域,從而釋放更多的空間。
全面垃圾收集:當Java堆中的空間不足,或者垃圾收集無法在指定的停頓時間內完成時,G1GC會執行一次全面的垃圾收集(Full GC)。全面垃圾收集會停止所有的應用程序線程,並且收集整個Java堆中的垃圾。

  1. G1 GC
    • G1 GC 的設計目標是降低停頓時間並適應大記憶體。
    • 它適合需要大記憶體和短停頓時間的應用程式。
  2. Heap 結構
    • Heap 被分成多個 Region。
    • 每個 Region 可以是 Eden, Survivor 或 Old。
  3. GC 過程
    • Minor GC:清理 Eden 和 Survivor。
    • Major GC:清理 Old。
    • Major GC 進行並行標記和清除。
    • 如果需要,會有停頓世界的事件。
  4. 調整
    • G1 GC 是可調的。
    • 重要選項包括:-XX:MaxGCPauseMillis, -XX:G1NewSizePercent, -XX:G1MaxNewSizePercent, -XX:G1HeapRegionSize。

Shenandoah GC

Shenandoah GC 是一種低暫停時間的垃圾收集器,它透過與 Java 程式並行執行更多的垃圾收集工作來減少 GC 暫停時間。Shenandoah 的大部分 GC 工作都是並行進行的,包括並行壓縮,這意味著它的暫停時間不再直接與堆大小成比例。無論是對 200 GB 的堆進行垃圾收集,還是對 2 GB 的堆進行垃圾收集,都應該具有相似的低暫停行為。

在 Shenandoah GC 中,一個物件的生命週期會經歷以下階段:

  1. 初始化標記 (Init Mark):這個階段會開始並行標記,準備堆和應用程式線程進行並行標記,然後掃描根集。這是循環中的第一個暫停,最主要的時間消耗者是根集掃描。因此,其持續時間取決於根集的大小。
  2. 並行標記 (Concurrent Marking):這個階段會遍歷堆,並追蹤可達物件。這個階段與應用程式並行運行,其持續時間取決於活動物件的數量和堆中物件圖的結構。由於應用程式在此階段可以分配新數據,因此在並行標記期間,堆佔用率會上升。
  3. 最終標記 (Final Mark):這個階段會完成並行標記,通過排空所有待處理的標記/更新隊列並重新掃描根集。它還通過找出要撤離的區域(收集集),預撤離一些根,並一般準備運行時進入下一階段。這是循環中的第二個暫停,這裡的最主要時間消耗者是排空隊列和掃描根集。
  4. 並行清理 (Concurrent Cleanup):這個階段會回收立即垃圾區域,也就是在並行標記後檢測到沒有活動物件存在的區域。
  5. 並行撤離 (Concurrent Evacuation):這個階段會從收集集中的活動物件撤離到新區域。這個階段與應用程式並行運行,其持續時間取決於收集集中的活動物件數量。
  6. 初始化更新引用 (Init Update Refs):這個階段會準備堆和應用程式線程進行並行更新引用,然後掃描根集。這是循環中的第三個暫停,最主要的時間消耗者是根集掃描。
  7. 並行更新引用 (Concurrent Update Refs):這個階段會遍歷堆,並更新物件引用以指向撤離物件的新位置。這個階段與應用程式並行運行,其持續時間取決於堆中的物件數量。
  8. 最終更新引用 (Final Update Refs):這個階段會完成並行更新引用,通過排空所有待處理的更新隊列並重新掃描根集。這是循環中的第四個暫停,最主要的時間消耗者是排空隊列和掃描根集。
  9. 並行清理 (Concurrent Cleanup):這個階段會回收所有剩餘的垃圾區域,也就是在並行更新引用後檢測到沒有活動物件存在的區域。

這就是 Shenandoah GC 中一個物件的生命週期。在這個過程中,物件會被標記、撤離、更新引用,並最終被清理。這個過程的大部分階段都與應用程式並行運行,從而實現低暫停時間的垃圾收集。

在垃圾收集過程中,特別是在物件撤離(Evacuation)階段,物件可能會被移動到堆的其他位置。如果在物件被移動的同時,應用程式試圖讀取該物件的引用,那麼它可能會讀取到一個無效的引用,因為該物件已經不再位於原來的位置。
為了解決這個問題,Shenandoah GC 在應用程式讀取物件引用時插入了一個 "Read Barrier"。這個 "Read Barrier" 會檢查該物件是否正在被撤離。如果是,那麼 "Read Barrier" 會先等待撤離完成,然後返回物件新的位置。這樣,應用程式就可以正確地讀取物件的引用,即使該物件正在被移動。
"Read Barrier" 是一種低開銷的操作,因為它只需要檢查物件的撤離狀態,並且只有在物件正在被撤離時才需要等待。因此,它不會對應用程式的性能產生顯著影響。

Shenandoah GC 也有一些階段會產生 Stop-The-World (STW) 暫停,但這些暫停的時間通常非常短,並且與堆的大小無關。這是因為 Shenandoah GC 的設計目標是使大部分的垃圾收集工作都能與應用程式並行進行。

在 Shenandoah GC 中,以下四個階段會產生 STW 暫停:
初始化標記 (Init Mark):這個階段會開始並行標記,準備堆和應用程式線程進行並行標記,然後掃描根集。
最終標記 (Final Mark):這個階段會完成並行標記,通過排空所有待處理的標記/更新隊列並重新掃描根集。
初始化更新引用 (Init Update Refs):這個階段會準備堆和應用程式線程進行並行更新引用,然後掃描根集。
最終更新引用 (Final Update Refs):這個階段會完成並行更新引用,通過排空所有待處理的更新隊列並重新掃描根集。

這些 STW 暫停的時間主要取決於根集的大小,而不是整個堆的大小。根集是指那些可能引用堆中其他物件的物件,通常包括全局變數、執行緒堆疊等。根集的大小通常遠小於整個堆的大小,因此這些 STW 暫停的時間通常非常短。
至於 "更新引用" 的階段,這個階段是與應用程式並行進行的。在這個階段中,Shenandoah GC 會遍歷堆,並更新物件引用以指向撤離物件的新位置。由於這個階段是與應用程式並行進行的,所以它不會產生 STW 暫停,並且其持續時間與堆的大小無關。


G1 (Garbage-First) GC 和 Shenandoah GC 都是針對減少垃圾收集暫停時間的垃圾收集器,但它們在設計和實現上有一些重要的區別。以下是一些可能需要從 G1 GC 轉換到 Shenandoah GC 的情況:

  1. 更低的暫停時間:如果你的應用程式對垃圾收集的暫停時間有非常嚴格的要求,那麼 Shenandoah GC 可能是一個更好的選擇。Shenandoah GC 的設計目標是提供更低的暫停時間,它的大部分工作都是與應用程式並行進行的,包括壓縮(Compaction)。
  2. 大堆需求:如果你的應用程式需要一個非常大的堆(例如,數十GB或更大),那麼 Shenandoah GC 可能是一個更好的選擇。Shenandoah GC 的暫停時間與堆的大小無關,這使得它更適合於大堆的情況。
  3. 更好的記憶體整理:如果你的應用程式有大量的長生命周期物件,或者有大量的大物件,那麼可能會遇到記憶體碎片化的問題。在這種情況下,Shenandoah GC 可能是一個更好的選擇,因為它可以在應用程式運行的同時進行記憶體整理。

在應用程式使用非常大的記憶體時
G1 GC 確實可以處理大型堆(例如數十GB或更大),並且它的設計目標就是為了更好地處理大型堆。然而,與 Shenandoah GC 相比,G1 GC 在處理大型堆時可能會有一些限制。
G1 GC 的暫停時間通常與堆的大小有關。這是因為 G1 GC 在執行全局垃圾收集(也稱為"Stop-The-World"收集)時,需要遍歷整個堆。因此,堆越大,全局垃圾收集所需的時間就越長。
另一方面,Shenandoah GC 的設計目標是使暫停時間與堆的大小無關。這是因為 Shenandoah GC 的大部分工作(包括標記和整理)都是與應用程式並行進行的。因此,即使堆非常大,Shenandoah GC 的暫停時間也可以保持在一個較低的水平。
因此,如果你的應用程式需要一個非常大的堆,並且你希望垃圾收集的暫停時間盡可能地低,那麼 Shenandoah GC 可能是一個更好的選擇。然而,這並不意味著 G1 GC 不能處理大型堆,只是在某些情況下,Shenandoah GC 可能會表現得更好。

小心 spring.jpa.open-in-view 導致 DB connection 被拿光

 spring.jpa.open-in-view

  • spring boot 的 property, spring.jpa.open-in-view 預設是開啟的
  • 開啟的話, OpenSessionInViewInterceptor 就會介入
    • 收到 web request 的時候, 會開一個 Hibernate Session
    • 如果用到 DB, 就會拿一個 DB Connection
    • 完成 request, 就把 connection 關閉


目的
  • 原本一個 entity 有像是 OneToMany 的關聯時, 預設都會 lazy.
  • 需要特別啟用這個 collection, 就需要特別去初始化, 例如 Hibernate.initialize
  • 如果沒有初始化就會遇到 LazyInitializationException


問題
  • 如果處理一個 request 的時候會需要比較長的時間, 那 DB connection 就會被卡住
  • BTW, 預設 DB connection 是 10條 ( https://github.com/brettwooldridge/HikariCP )
  • 如果需要呼叫外部服務, 或是外部服務遇到 connection timeout 之類的問題, 會導致一個 request 花很多時間才結束
  • 而導致 connection 很快被拿光, 而且無法拿到新的 connection 而爆炸..


怎麼辦?
disable open-in-view
  • spring.jpa.open-in-view, 把這個設成 false, 可以避免 connection 跟著 request
  • 可是要注意如果有 lazy 的地方要 initialize 否則會遇到 lazy exception
限制 request 的時間
  • 把時間可能拉長的設計跟 DB access 分開來
  • 例如把對外部系統的呼叫分開來

Reference
可能會需要調整 HikariCP 的 pool size, 參考: https://github.com/brettwooldridge/HikariCP

新團隊快速貢獻

前篇

前言
自從開始與新團隊合作後, 產品也即將 GA release.
GA 之後又會有新的不同的挑戰. 在新挑戰之前, 是時候紀錄一下這段時間發生的事情.

挑戰
  1. 產品本身
    1. 由於架構改變加上優化, 整個 backend 幾乎全部改寫, 而且加上支援 HA. 有大量還沒經過 QA 驗證的程式
      (全部改寫, 都只有 unit test, 所以算是全部都還沒驗證過 :P).
    2. 這個產品開發需要了解另一個產品才可以整合
  2. 人員改變
    1. 原本的核心團員一人
    2. 新外包團隊五個人加入, 四個工程師與一個 QA.
    3. Functional QA 與 Performance QA
  3. 同時間 (同個禮拜加入)
    1. 需要帶領新團隊五人從無到有了解產品並加以貢獻
    2. Functional QA 開始列 functional testcase 測試, 這是公司內經驗老到的 QA
    3. Scaling QA 開始列 performance criteria & testcase 開始測試效能, 這也是公司內經驗老到的 QA
  4. 時程
    1. 由於來支援的 QA 手上還有其他重要的事情, 所以我們不該耽誤他們太久
    2. 因此不論 scaling QA or functional QA 的時間都非常寶貴 (大家的時間本來就都非常寶貴)
    3. 必須特別注意不可以有 blocking issue, 有 blocking issue 就需要立馬解決

接受挑戰
規劃
Thread 1-1: 新團隊成員
  1. 協助裝機, 介紹開發環境
  2. 講解原理, 提供文件
  3. 拍影片, 介紹產品基礎設定與整合測試方法
  4. 下載程式後, 介紹程式架構, 依照 use case 介紹程式位子
Thread 1-2: Functional QA
  1. 介紹產品背景與 trade-off
  2. 解釋功能, 提供文件
  3. 討論 testcase 以及測試的範圍
Thread 1-3: Scaling QA
  1. 介紹產品背景與 trade-off
  2. 解釋功能, 提供文件
  3. 討論需要的 capacity 以及效能測試的方法
Thread 2-1: 新團隊成員
  1. 確認基本操作完成後, 開始安排 Functional QA 開的 bug中比較單純的部分交給新成員處理
  2. 由於每個人開始透過不同的 bug 來熟悉系統, 為了快速掌握進度, 開始進行 daily meeting
  3. 透過由淺入深的 bug, 來觀察還有甚麼地方不懂, 就一次講解給所有人聽, 期望盡早把知識都散布出去
Thread 2-2: Scaling QA
  1. 一開始 Scaling QA 遇到的問題最棘手, 因為效能大多就是測 critical path
  2. scaling test 的特性是: 一旦遇到一個量上不去, 測試幾乎就卡住了, 需要立即處理
  3. 力求每天 Scaling QA 遇到新瓶頸, 當天或隔天就可以有新的 release 可以繼續測試
  4. 由於新團隊成員掌握度還不高, 因此這類問題都要先讓原本的隊員與我來處理
Thread 2-3: Functional QA
  1. Functional QA 開的 issue 很多, 需要盡早判斷是屬於新團隊成員可以練習的, 還是需要與原本的隊員一起先解決的
  2. 新隊員摸索時期大多是從 NodeJS or Angular 起頭去看 application logic, 因此就先都安排在 NodeJS 或 UI 上可以看到程式的 issue 給新成員
  3. 如果跟 Linux system call 或是 IPv6 或是 Java/Groovy 做的比較偏系統操作的 bug, 就由我與原始隊員先排除
  4. 初期雖然大部分時間都在與 Scaling QA 合作, 但隨時要注意 Functional QA 有沒有遇到 blocking issue
Thread 3-1: 新團隊成員逐漸上手
  1. 上手後能協助的問題就變多了
  2. 定調 release 的節奏為每週一次, 簡化流程
  3. 開始把一些比較系統面的工作也指派出去
  4. 當遇到問題的時候就一起看
  5. 有經驗的人與我一樣先挑比較麻煩的 bug, 讓新團隊可以先熟悉 application code.
  6. 慢慢有些麻煩的 issue 試著交給新團員後, 有問題就分享平常解決問題的方式
  7. 阻止外部的人催促新團隊成員時程, 爭取按步就班上手的時間
Thread 3-2: Scaling QA
  1. Scaling test 尾聲, 與 Scaling QA 討論測試的上限
  2. 完成測試
Thread 3-3: Functional QA
  1. 持續把新成員有能力處理的 issue 指派過去
  2. 持續自行處理棘手的 bug

成就
終於, 在經歷了一百多個大大小小的 bug 後, 得到 GA candidate
  1. 通過 Scaling QA 安排的測試
  2. 通過 Functional QA test pass rate over 96%
過程中
  1. 原本的隊員與我在過程中一度需要去支援別的案子, 當下壓力頗大.
  2. 秉持著 "培養新團員就有 capacity 來處理更多 issue" 的精神, 繼續投資時間在與新團隊的溝通與分享上
  3. 最終果然有的新成員逐漸上手, 可以把更多事情安排出去, 討論的內容也愈來愈進階
  4. 時程快到之前, 每天都看著 scrum board 沙盤推演誰的 issue 做完之後可以提升 pass rate
  5. 直到有天 pass rate 超過了 96%

插曲
  1. 原本新團隊的隊長, 突然要離開公司, 搬回馬來西亞
  2. 新團隊的 QA 節奏有點跟不上, 協助幾次之後, 在與新團隊成員討論之後, 提出了 PIP, 最終我們好聚好散
  3. 新隊長會再找人補上空缺

後續
  1. 為了要 GA, 新團隊還沒有接觸較底層的東西
  2. 新團隊即將成為產品的下個版本的主力開發人員, 需要盡早掌握所有部份的技術
  3. 新上任的隊長還沒有帶隊經驗, 因此還需要協助建立新的 working model
  4. 也要協助新隊長理解新產品需求, 並規劃好 milestones. 讓團隊每個人都能 on the same page.

心得
時間一樣短短的 (2-3個月), 想不到列下來發生這麼多事情.
有人支援是很好的事情, 可是也需要注意釋放出來的知識需要按部就班規劃.
同時間要頂住壓力, 避免 QA 的 issue 壓太多給新隊員導致消化不良, 又要避免卡住 QA 浪費時間.
讓每個人都能處在剛好可以提出貢獻, 又有一點挑戰的狀態就很重要.
為了做到這點, 需要不停地從他人的角度來看待, 持續溝通, 想辦法讓人 on the same page.

很感謝不論是 QA, 原核心成員與新團員都很給力, 在許多混亂的資訊攤在檯面上的時候,
依然能一起持續討論, 找出有意義的目標與問題加以克服.
之後還有很新挑戰, 又是一輪新的動態搭配.

Dependency Inversion Principle 避免過度依賴外部

Introduction
DIP 應該很常見, 只是常常在談的時候會發現大家忘記了.
因此特別紀錄一下使用 DIP 實質上的好處.

Assumption
  1. 我們大多會希望 business logic code 可以乾淨穩定
  2. 有乾淨穩定的 business logic code, 就可以寫穩定的 unit test code
  3. 如此未來商業邏輯有改變的時候, 如果邏輯有衝突, 或是程式有問題很快就可以 flush

Reality
  1. 在沒有仔細思考的情況下, 分層式規劃結果無法有乾淨的 business logic code
  2. 例如, 假設 DB 是 PostgreSQL, MQ 則是 Kafka
  3. 很容易程式會出現 MyDomainService depends on MyDomainRepositoryHibernateImpl
  4. 這樣的架構就變成 business logic code depends on implementation
  5. 如此, 無法寫出穩定的 business logic test code, 因為都"必須"直接由 domain code 直接依賴 Hibernate & Kafka...
  6. 一個是需要準備 Hibernate & Kafka 的環境 or mock, 可能有些細微的調整沒藏好, 會導致一點小調整都可以讓測試壞掉


Solution
  1. 為了區隔出乾淨的 business logic code, 我們可以替 repository 提出一個 interface
  2. 如此, business logic code 僅止於 MyDomainRepository 以及 MyDomainMessageSender
  3. 同時, 也要注意 interface 上不可以出現底層的相關資訊
  4. 例如 MyDomainMessageSender 的 interface 不該出現 topic 這種 Kafka 概念的資訊 (除非我們自己訂一層 abstraction layer)
  5. 這時候就會看到為何像是 spring-data 會支援直接訂一個 interface 就可以透過 convention access DB


Besides
  1. 同樣的概念也可以套用在不同的 component
  2. 例如 application 跟很多外部系統界接, 所以程式裡面有分成不同的 component
  3. 透過 DIP, 可以把其他 system 的實作與主要的 business logic 獨立開來
  4. 如此就可以直接在測試描述對於外部喜統的 expectation, 而且不用為了 business logic test code 準備外部系統的細節
    (在目前的例子, 不用準備 gRPC and Kafka)





Spring Cloud Stream Introduction - 1

Goal
描述如何套用 spring cloud stream + Kafka 以及概念.
適合只面對 Kafka, 不涵蓋進階議題

Example

Concepts
  • 一個 application 的架構: 外部系統(middleware) -> input -> application process -> output -> 外部系統
  • 在 Spring Cloud Stream
    • 透過 Binder 來處理外部系統的細節. ex. spring-cloud-stream-binder-kafka
    • 透過 Binding 來處理 input.

Example
  • Setup
    • Spring Boot Application
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

Kafka topic consumer
  • Consumer method (注意! 這裡的 method name: status 就是 Binding name, 會影響 application.properties)
@Bean
public Consumer<String> status() {
    return status -> System.out.println("Received " + status);
}
# 如果會從多個 topic 拿 message 就需要在 spring.cloud.function.definition 這個 attribute 指定, 並且用分號隔開. 這裡是預先指定一個
spring.cloud.function.definition=status

# 可以看到這的 pattern: spring.cloud.stream.bindings.{bindingName}-in-0.destination, 用來指定 topic name
# in 的部分是說 input topic
# 0 則是這個 binding 的第一個 input
spring.cloud.stream.bindings.status-in-0.destination=status
  • 再來就可以發訊息給 status 這個 topic

Function
透過 Function 可以處理 input -> process -> output
  • Function: 收到一個訊息後, 在尾巴貼上 random suffix, 然後回傳
@Bean
public Function<String, String> randomNumberSuffix() {
    return val -> val + " => append suffix " + Math.random();
}
  • 指定 input & output topic in application.properties
# 注意此時我們已經加上第二個 binding
spring.cloud.function.definition=status;randomNumberSuffix

# 指定 randomNumberSuffix 的 intput topic 是 randomNumberSuffix, output topic 則是 status, 也就是同一個 application 的另一個 binding
spring.cloud.stream.bindings.randomNumberSuffix-in-0.destination=randomNumberSuffix
spring.cloud.stream.bindings.randomNumberSuffix-out-0.destination=status
  • 發訊息給 randomNumberSuffix topic

Supplier
Supplier 是會被系統自動 polling, 預設一秒 poll 一次, 也可以指定 cron, 要 3.2 版之後才支援 custom binding polling configuration.
  • Supplier
@Bean
public Supplier<Date> mydate() {
    return () -> new Date();
}
  • 指定 output topic 給 mydate 這個 Supplier binding
# 此時已經增加第三個 binding definition
spring.cloud.function.definition=status;randomNumberSuffix;mydate

# 指定 output topic 為 status, 就是同個 app 的 topic
spring.cloud.stream.bindings.mydate-out-0.destination=status

# 改變預設的 poll config 為 2 秒 poll 一次
spring.cloud.stream.poller.fixed-delay=2000
  • Supplier 只要打開 app 就會自動被執行

Other Concepts
  • Consumer Group
    • 跟 Kafka Consumer Group 的概念一樣
    • 同一個 group 裡面只會有一個 consumer 收到 message
    • 不同 group 則都會收到訊息
    • 預設每個 consumer 都是不同的 group (anomymous group)
    • 透過 {binding}.group=xxx 來指定 groupName
  • Durability
    • 有指定 group, 則對 consumer 的 subscription 就會被保留, 即使這個 group 目前沒有 consumer, 等 consumer 回來, 就會接著收到訊息
    • anonymous group 的 subscription 就不會被保留, 因此 anonymous group 容易收到 duplicated message
  • Partition
    • 一個 topic 可以被切成多個 partition, 每個 partition 會由固定的一個 consumer 接收資料

 

分工不設限

 

前言

去年一些巧合, 跟團隊幾個人接手一個案子, 這個案子原本算是服務客戶特定需求的 POC, 但由於客戶愈來愈依賴這個工具, 因此交到我們手上.

PS. 在我加入之前, 已經有人辛苦耕耘了好一陣子. 不過也因緣際會離開這個案子. 一個案子要成功, 從來不是誰可以獨立勝任. 享受合作的當下, 同時也要記得這是很多默默付出的人辛勞的結果, 而非甚麼簡單的水到渠成.. 

挑戰

  1. 技術背景不同
    1. 案子用到的技術是 NodeJS, Angular, MongoDB
    2. 成員技術背景是 Java, Cassandra
  2. 需求大: 要把 MongoDB 換成 PostgreSQL
  3. 技術債: 由於是個 POC 的案子, 所以程式結構沒有特別維護, 整組就是典型的 callback hell, 經常性的 callback 到第四第五層
  4. 沒有統一的 build flow: 原本交付的方式就是從某個工程師的電腦打包出一個 image 給客戶安裝, 在 POC 也還合理, 但要產品化就無法接受了
  5. 概括承受 bug, 當前客戶在用的系統如果遇到問題, 就需要與 technical support, customer support 一起討論與解決問題

接受挑戰

初期

  1. Study: 由於技術不熟悉, 所以大家還是花時間學一下 NodeJS, Angular, MongoDB.
  2. List features: 一起把功能使用一番, 列出來

規劃

Thread-1-1: 換資料庫

  1. 一開始我們把所有的 table 列出來, 想要"縱切"來分開發方式. 也就是一個 table 一個 table 的把程式從面對 MongoDB 改為面對 PostgreSQL
  2. 做一段時間後, 深感部分 Data Access layer 面對 MongoDB, 部分面對 PostgreSQL, 常常碰到一些因為資料不一致出現的錯誤.
    這種錯誤讓人無法確認現在的開發是好了沒有, 所以我就決定開始衝刺把所有的 Data Access Layer 通通轉成面對 PostgreSQL, 而不理會功能是否損毀.
  3. 同時間, 另一個成員深受 callback hell 的困擾, 因此在討論後, 開始大幅度的將 callback hell 改為 controller -> service -> dao 這樣的三層式架構

Thread-1-2: build script

團隊內的大神基於興趣, 接手了 build flow 的規劃. 將原本由工程師在自己電腦上 build image 的方式, 轉為
  1. 準備 docker 環境
  2. 在 docker 內準備好 CentOS, PostgreSQL, NodeJS, Angular 的環境
  3. 安裝好後 build 出客戶需要的 vmware image
  4. 準備 CLI 讓客戶裝好 image 後, console 上能跳出 setup 的 CLI.
  5. 支援 join 第二台 PostgreSQL
  6. 在 Jenkins 上執行 build script

Thread-1-3: nginx

  1. 專案原本用 openresty 在 nginx 寫程式 access MongoDB
  2. 我們認為這設計導致不容易管理資料庫由誰存取
  3. 因此規劃改寫 openresty 從直接存取資料庫, 改為存取 API 來存取資料.

Thread-2-1: data migration

在 Data Access Layer 改為面向 PostgreSQL 之後, 接下來需要處理怎麼從 MongoDB 轉移過來, 因此規劃了
  1. 把 MongoDB 資料轉出
  2. 建構 PostgreSQL 的 migration script, 包含建立 schema 的 DDL
  3. 提供 user import MongoDB data 的功能

Thread-2-2: licensing

原本的案子沒有 license 的功能, 因此也照公司其他產品的方式規劃了 license

Thread-3-1: documentation

慢慢功能都快補好了, 開始需要與 document team 溝通與準備需要的文件

Thread-3-2: unit test

過去缺乏 unit test, 因此大量地補上

成就與心得

  1. 像這樣分很多個 thread 做了非常多的事情, 一切只發生在大概五個月內, 而且我們成員就五個人, 且大多都還有其他在進行的專案.
  2. 通常一個案子在進行的時候, 很多人可能會想要把要做的事情都規範好, 希望能夠營造一個 "我都規畫好了, 你照做就沒問題"
  3. 但我們的作法則是: "目標就這些, 一起來看怎麼處理"
  4. 在執行過程中, 每個人都在貢獻自己能做的事情, 而且當遇到覺得有問題的地方, 提出來後大家就能一起討論做決定, 接著繼續分頭進行
  5. 由於能夠參與決定, 大家就更有 ownership, 就會提出非常多很好的意見
  6. 我自己就很開心能享受到架構與流程的改善, 而且完全是大家自發性的改變, 回想這一段時間, 很短, 但很充實愉快

後記

這個案子, 因為一些因素, 人員變動, 在我們做完之後暫時告一段落.
大家繼續各自去忙不同的案子.
後來公司找了外包團隊.
所以接下來我還會一次與公司突然開始加入的外包團隊, functional QA & performance QA 一起合作.
正在如火如荼, 再次經歷一段美好的團隊合作經驗.

應用 XOR 特性取出相字元或數字

  1. XOR 的特性, 相同的值 XOR 會變成 0
0^0=0
1^0=1
0^1=1
1^1=0

// code
int n = 0;
for (int i = 0; i < 10000; i++) {
    n ^= i;
}
for (int i = 0; i < 10000; i++) {
    if (i == 999) continue;
    n ^= i;
}
System.out.println(n); // print 999
  1. 應用: 兩個字串只有一個字元不同的時候, 可以用來找出該不同的字元為何.
class Solution {
    // ex. s = "abc", t="ab", then ch = 'c'

    public char findTheDifference(String s, String t) {
        char ch = 0;
        for (Character c: s.toCharArray()) {
            ch ^= c;
        }
        for (Character c: t.toCharArray()) {
            ch ^= c;
        }
        return ch;
    }
}

Explain - LeetCode 525 Contiguous Array

題目: Given a binary array nums, return the maximum length of a contiguous subarray with an equal number of 0 and 1

解法:
  1. 當遇到 0 就 -1, 遇到 1 就 +1, 計算每個陣列位置的加總
  2. 最重要的概念就是: 當遇到相同的加總數字, 表示中間經歷了相同的 1 & 0.
  3. 因此解法就是:
    1. 走過所有的陣列, 計算每個位子的 count. 遇到 0 就 -1, 遇到 1 就 +1
    2. 最重要的概念是: 當過程中出現相同的 count (不管正負) 就表示過程中有相同的 0 與 1
    3. 因此解法就是一邊算與紀錄 count, 如果遇到相同的 count 就算距離, 把最長的距離記錄下來

Code
public class Solution {

    public static void main(String[] args) {
        new Solution().findMaxLength(new int[]{0,0,1,0,0,0,1,1});
    }

    public int findMaxLength(int[] nums) {
        Map<Integer, Integer> map = new HashMap<>();
        map.put(0, -1);
        int maxlen = 0, count = 0;
        for (int i = 0; i < nums.length; i++) {
            count = count + (nums[i] == 1 ? 1 : -1);
            if (map.containsKey(count)) { 2. 如果以前有過跟現在相同的 count, 表示過程中經歷了相同的 0 & 1
                maxlen = Math.max(maxlen, i - map.get(count)); // 3. 以此計算距離
            } else {
                map.put(count, i); // 1. 紀錄目前的 count 的位置
            }
        }
        return maxlen;
    }
}

PostgreSQL version schema performance comparison

Requirement
  1. Developers keep releasing new software, the version format was {major}.{minor}.{micro}.{build}
  2. When a device ask for upgrade information, we need figure out the latest versions of software
  3. But not only the latest one, we may need to know following version information, so that we can provide suitable recommendation
    1. is there are 3 newer versions of software?
    2. How many newer software versions?

Challenge
  1. Database sort text by "Natural Sorting"
  2. These 2 versions, the older version will be treated as the newer one if we use natural sorting
    1. 1.2.3.100 => Will be treated as older because 2 > 1
    2. 1.2.3.20 => So the version 20 will be treated as newer
Ideas
  1. Persist major, minor, micro, build in different columns
  2. Calculate versions to be a number, which can be sorted correctly
  3. Merge versions to be a single text, and need to be sorted correctly (We need padLeft 0 to let all versions become the same length)
  4. Persist major, minor, micro, build in a byte array (BLOB),
    assume the natural sorting will compare BLOB from the first element of an array.
Test Steps
  1. Prepare TestContainer for PostgreSQL testing
  2. Generate 10000 records
    1. major 0
    2. minor 0-9
    3. micro 0-9
    4. build 0-99
  3. Compare with a specified version: 0.6.7.58
  4. Compare query plan and query result (Query the latest 3 records)

Code for the test

Tables
Version1
DDL
CREATE TABLE version1 (
id uuid PRIMARY KEY,
major int,
minor int,
micro int,
build int
);

CREATE INDEX version1_major_idx ON version1 (major DESC);
CREATE INDEX version1_major_minor_idx ON version1 (major DESC, minor DESC);
CREATE INDEX version1_major_minor_micro_idx ON version1 (major DESC, minor DESC, micro DESC);
CREATE INDEX version1_major_minor_micro_build_idx ON version1 (major DESC, minor DESC, micro DESC, build DESC);
SQL
String sql = "SELECT * FROM Version1 " +
"WHERE (major > 0) " +
"OR (major = 0 AND minor > 6) " +
"OR (major = 0 AND minor = 6 AND micro > 7) " +
"OR (major = 0 AND minor = 6 AND micro = 7 AND build > 58) " +
"ORDER BY major DESC, minor DESC, micro DESC, build DESC " +
"LIMIT 3";
Query Plan
QUERY PLAN: Limit (cost=0.29..1.16 rows=3 width=32) (actual time=0.065..0.193 rows=3 loops=1)
QUERY PLAN: -> Index Scan using version1_major_minor_micro_build_idx on version1 (cost=0.29..982.84 rows=3366 width=32) (actual time=0.051..0.073 rows=3 loops=1)
QUERY PLAN: Filter: ((major > 0) OR ((major = 0) AND (minor > 6)) OR ((major = 0) AND (minor = 6) AND (micro > 7)) OR ((major = 0) AND (minor = 6) AND (micro = 7) AND (build > 58)))
QUERY PLAN: Planning Time: 0.920 ms
QUERY PLAN: Execution Time: 0.307 ms
QUERY RESULT: {major=0, minor=9, micro=9, build=99, id=7e5878ff-9d25-46dc-a0c0-79196fd8c5d3}
QUERY RESULT: {major=0, minor=9, micro=9, build=98, id=ee2097ef-fe86-491e-869b-afda5976a354}
QUERY RESULT: {major=0, minor=9, micro=9, build=97, id=ada64c48-72e2-4a39-a6b0-5eabda135d0d}

Version2
DDL
CREATE TABLE version2 (
id uuid PRIMARY KEY,
build int
);

CREATE INDEX version2_order_idx ON version2 (build DESC);
SQL
String sql = "SELECT * FROM Version2 " +
"WHERE build > " + getVersion2Number(0,6,7,58) +
" ORDER BY build DESC" +
" LIMIT 3";
Query Plan
QUERY PLAN: Limit (cost=0.29..0.61 rows=3 width=20) (actual time=0.053..0.124 rows=3 loops=1)
QUERY PLAN: -> Index Scan using version2_order_idx on version2 (cost=0.29..391.76 rows=3627 width=20) (actual time=0.037..0.060 rows=3 loops=1)
QUERY PLAN: Index Cond: (build > 6758)
QUERY PLAN: Planning Time: 0.388 ms
QUERY PLAN: Execution Time: 0.195 ms
QUERY RESULT: {build=9999, id=e32b8067-9cae-4c49-b776-372e8a2137e4}
QUERY RESULT: {build=9998, id=c2902963-6579-48d3-bb91-deac6a8528bd}
QUERY RESULT: {build=9997, id=de28843c-b245-4b46-8e2d-7e549f0b862e}

Version3
DDL
CREATE TABLE version3 (
id uuid PRIMARY KEY,
build bytea
);

CREATE INDEX version3_order_idx ON version3 (build DESC);
SQL
String sql = "SELECT * FROM Version3 " +
"WHERE build > ?" +
" ORDER BY build DESC" +
" LIMIT 3";
Query Plan
QUERY PLAN: Limit (cost=0.28..0.77 rows=3 width=48) (actual time=0.063..0.130 rows=3 loops=1)
QUERY PLAN: -> Index Scan using version3_order_idx on version3 (cost=0.28..368.24 rows=2283 width=48) (actual time=0.047..0.068 rows=3 loops=1)
QUERY PLAN: Index Cond: (build > '\x000506073a'::bytea)
QUERY PLAN: Planning Time: 0.329 ms
QUERY PLAN: Execution Time: 0.214 ms
[0, 9, 9, 99]
[0, 9, 9, 98]
[0, 9, 9, 97]

Version4
DDL
CREATE TABLE version4 (
id uuid PRIMARY KEY,
build VARCHAR
);

CREATE INDEX version4_build_idx ON version4 (build DESC);
SQL
String sql = "SELECT * FROM Version4 " +
"WHERE build > '" + getVersion4Text(0, 6, 7, 58) + "'" +
" ORDER BY build DESC" +
" LIMIT 3";
Query Plan
QUERY PLAN: Limit (cost=0.29..0.80 rows=3 width=48) (actual time=0.074..0.137 rows=3 loops=1)
QUERY PLAN: -> Index Scan using version4_build_idx on version4 (cost=0.29..512.72 rows=2996 width=48) (actual time=0.060..0.079 rows=3 loops=1)
QUERY PLAN: Index Cond: ((build)::text > '0000000600070058'::text)
QUERY PLAN: Planning Time: 0.313 ms
QUERY PLAN: Execution Time: 0.256 ms
QUERY RESULT: {build=0000000900090099, id=29f9f1a0-40eb-4a31-873f-ff04868fa3d1}
QUERY RESULT: {build=0000000900090098, id=1ead5f4d-73d9-4cda-b899-f745b90f8598}
QUERY RESULT: {build=0000000900090097, id=bc9a4dfe-12c9-4c34-8925-836e8c4a7ad3}

Comparison
OptionscostPros & Cons
Version1 (split columns)(cost=0.29..982.84 rows=3366 width=32)
Pros: Flexible, can change SQL easily
Cons: Slow
Version2
(calculate to number)
(cost=0.29..391.76 rows=3627 width=20)
Pros: Fast
Cons:
  1. Need calculate before persist and may need migrate if the logic to compare changed
  2. Sorting will be broken if the number exceed max number
Version3
(calculate to string)
(cost=0.28..368.24 rows=2283 width=48)
Pros: Fast and don't have max number issue
Cons:  Need calculate before persist and may need migrate if the logic to compare changed
Version4
(calculate to byte array)
(cost=0.29..512.72 rows=2996 width=48)
Pros: Don't need extra calculation
Cons:
  1. Easy to exceed max byte number, so still need extra calculation
  2. Slow


Kafka - Pick a transaction.id

Intention: Why I need Kafka Transaction
  • 需求中收到 message 並處理之後, application 需要另外傳送訊息出去給多個 topic.
  • 不管遇到任何錯誤, 我都希望訊息就不要送出去.
  • 除此之外, 原本 consume 的訊息也不要收下來

How it works
  1. Producer initTransaction with transaction.id
  2. TransactionCoordinator close existing pending transactions with same transaction.id
  3. Producer send message, the message will be wrote to topic
  4. Producer commitTransaction, the TransactionCoordinator will start the 2 phase commit process
    1. Write PREPARE_COMMIT to the "transactionLog" topic
    2. Mark "commit" status in topic partitions
    3. write COMMITTED to transactionLog
  5. After these steps, transaction was pretty much to be finished
Note: transactionLog 是 internal topic, 用 transaction.id 作為 partition key, 因此可以保證狀態的順序

如果有兩個 Producer 共用 transaction.id, 當 Kafka 發現有相同的 transaction.id 存在的時候, 就會把先前的 transaction close,
因此如果 transaction.id 沒有規劃好就會遇到 transaction 莫名的被 abort 的 error.

Atomic Read-Process-Write
  • Consumer 收訊息下來後透過呼叫 commit offset 來標記已經處理完
  • 這個 offset 其實也是個 topic
  • 藉由前面介紹的 transaction 處理機制, 可以讓 "commitOffset" 也只是發訊息到一個 topic, 也可以被包在同一個 transaction 中

適當的 transaction.id
  • transaction.id 需要夠 unique, 如此才能避免 Producer 共用 transaction.id 而被 close
  • 由於每個 transaction 都需要一些額外的 request 才能完成, 所以如果 transaction.id 定太細導致一堆 transaction 會使效能大幅降低
  • tx-{consumeTopic}-{consumePartition} 是一個折衷的 transactionid, 因為一個 consumer 只會對應到一個 topic 以及 partition.
    在 read-process-write 的 pattern 下, 這個 transaction.id 會被該 topic & partition 的 consumer 使用, 不會有 multi-thread producer with same transaction.id 的情況.
    也不會過於分散 (同樣是在 read-process-write pattern 下, 一個 consumer thread 會一個一個訊息處理, 每個 consumed message 都值得一個 transaction (id).

KafkaTemplate 有個設定: producerPerConsumerPartition 就是拿來建立 tx-{consumeTopic}-{consumePartition} 這樣的 transaction.id.

它的做法就是在接收訊息的時候, 把 topic & partition 記錄下來, 好在發送訊息的時候 append 到 transaction.id prefix 後面










SpringBoot + Flyway + Kafka + PostgreSQL + Testcontainers

Source Code: https://github.com/axxdeveloper/study-practice/tree/testcontainer

在一個 sharing session 分享如何使用 testcontainer 輔助 SpringBoot application 開發測試 Kafka & PostgresSQL  相關的邏輯.

用 TestContainer 沒甚麼問題, 主要是多個 test class 開關 Kafka & Postgres 之後要重新讓 SpringBoot 連線比較麻煩, 這時候用了 DirtiesContexts

 


用 protobuf Any 來 parse byte array

第一次錄影片分享技術議題.

Source code: https://github.com/axxdeveloper/study-practice/tree/gpb 

主要其實就是之後可以用 Any.pack( gpbEntity ).toByteArray 傳送出去.
接收端也適用 Any.parseFrom( byteArray ).unpack( gpbEntity.class ).

這樣可以用 Any.parseFrom (byteArray).is( gpbEntity.class ) 來判斷應該要用哪個 gpbEntity 來讀資料.


PostgreSQL Replication

 

  • WAL - Write Ahead Log, or xlog, or transaction log.
    • WAL 就像是 Cassandra 的 CommitLog, 會先被存起來, 再寫進資料庫, 使 Postgres 不管何時被關閉, 重啟後都可以恢復資料.
    • WAL 存在 pg_wal folder 下
    • Postgres 13, 放在 /var/lib/pgsql/13/data/pg_wal
    • WAL 檔案預設 16MB
    • WAL 是用 binary format 寫入的
  • Checkpoint
    • 用來清除 WAL. 確認 WAL 已經寫入 data 就可以把 WAL 清掉了
    • 由系統自動驅動, 不過可以在 postgresql.conf 裡面設定驅動的參數.
      例如可以指定 Checkpoint 之間的週期, 或是 wal 檔案大小的區間.
# - Checkpoints -
#checkpoint_timeout = 5min # range 30s-1d
max_wal_size = 1GB
min_wal_size = 80MB
#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 - 1.0
#checkpoint_flush_after = 256kB # measured in pages, 0 disables
#checkpoint_warning = 30s # 0 disables
    • WAL file 會被排序過再寫到硬碟, 來增加寫入的效能
    • 比較長的 Checkpoint 區間會減少 WAL file 的數量
  • PITR: point-in-time-recovery
    • 做 HA 需要 PITR backup/restore, 因為 standby node 在剛啟動的時候需要先有一個從 primary node 建立的 base backup
    • PITR 的設定在 postgresql.conf (擷取部分)
#------------------------------------------------------------------------------
# WRITE-AHEAD LOG
#------------------------------------------------------------------------------
wal_level = replica

#------------------------------------------------------------------------------
# REPLICATION
#------------------------------------------------------------------------------
max_wal_senders = 10
max_replication_slots = 10
    • wal_level = replica (https://docs.postgresql.tw/server-administration/server-configuration/write-ahead-log) 預設 replica 使能夠準備足夠的 transaction log 給 PITR restore 使用, 如果設定成
    • max_wal_senders = 10 (https://docs.postgresql.tw/server-administration/server-configuration/replication) 最大的寫量, 注意不能大於 max_connection
    • max_replication_slots = 10 (https://docs.postgresql.tw/server-administration/server-configuration/replication) 指定最大的 replication slot, 就是一份寫入的資料, 要 replica 到幾個 node 才可以 (或是 replica 到所有的 standby)
  • Archive transaction log
    • 設定在 postgresql.conf
# - Archiving -
archive_mode = on # enables archiving; off, on, or always
# (change requires restart)
archive_command = 'cp "%p" "/var/lib/pgsql/archivedir/%f"' # command to use to archive a logfile segment
# placeholders: %p = path of file to archive
# %f = file name only
# e.g. 'test ! -f /mnt/server/archivedir/%f && cp %p /mnt/server/archivedir/%f'
#archive_timeout = 0 # force a logfile segment switch after this
# number of seconds; 0 disables
    • 以這預設值, Postgres 會持續把 WAL 寫入 /var/lib/pgsql/archivedir/%f
[admin@rnd1 ~]$ sudo ls -l /var/lib/pgsql/13/data/pg_wal
total 32772
-rw------- 1 postgres postgres 337 Jun 9 08:40 000000010000000000000002.00000028.backup
-rw------- 1 postgres postgres 16777216 Jun 10 04:21 000000010000000000000003
-rw------- 1 postgres postgres 16777216 Jun 9 08:40 000000010000000000000004
drwx------ 2 postgres postgres 59 Jun 9 08:45 archive_status
[admin@rnd1 ~]$ sudo ls -l /var/lib/pgsql/archivedir/
total 49156
-rw------- 1 postgres postgres 16777216 Jun 9 08:40 000000010000000000000001
-rw------- 1 postgres postgres 16777216 Jun 9 08:40 000000010000000000000002
-rw------- 1 postgres postgres 337 Jun 9 08:40 000000010000000000000002.00000028.backup
-rw------- 1 postgres postgres 16777216 Jun 9 08:32 000000010000000000000003
[admin@rnd1 ~]$
  • pg_hba.conf (host-based authentication)
    • 如果要用 pg_basebackup 就需要設定 pg_hba.conf
local database user auth-method [auth-options]
host database user address auth-method [auth-options]
hostssl database user address auth-method [auth-options]
hostnossl database user address auth-method [auth-options]
hostgssenc database user address auth-method [auth-options]
hostnogssenc database user address auth-method [auth-options]
host database user IP-address IP-mask auth-method [auth-options]
hostssl database user IP-address IP-mask auth-method [auth-options]
hostnossl database user IP-address IP-mask auth-method [auth-options]
hostgssenc database user IP-address IP-mask auth-method [auth-options]
hostnogssenc database user IP-address IP-mask auth-method [auth-options]
Ex. 從 local or 127.0.0.1 or ::1 來的, 用來 replication 的 user 為 postgres 的連線都一律通過
# Allow replication connections from localhost, by a user with the
# replication privilege.
local replication postgres trust
host replication postgres 127.0.0.1/32 trust
host replication postgres ::1/128 trust
pg_basebackup -D /some_target_dir -h localhost --checkpoint=fast --wal-method=stream
  • PITR restore
    • 指定 restore_command 與 recovery_target_timeline
restore_command = 'cp /mnt/server/archivedir/%f "%p"'
recovery_target_timeline = latest
    • 啟動後等關鍵字 "consistent recovery state reached"
    • Ex. 用 pg_basebackup 要求 standby 去 sync data
pg_basebackup -D /target -h master.example.com --checkpoint=fast --wal-method=stream -R
    • 使用 pg_basebackup 的時候可以 -R, 如此會把 standby configuration 寫進 postgresql.conf
standby_mode = on
primary_conninfo = ' ...'
  • Debug
    • pg_stat_replication ( *_lsn, lsn 是 location 的意思, 所以 sent_lsn 就是 sent_location )
select * from pg_stat_replication;
pid | usesysid | usename | application_name | client_addr | client_hostname | client_port | backend_start | backend_xmin | state | sent_lsn | write_lsn | flush_lsn | replay_lsn | write_lag | flush_lag | replay_l
ag | sync_priority | sync_state | reply_time
-------+----------+---------+------------------+---------------+-----------------+-------------+-------------------------------+--------------+-----------+-----------+-----------+-----------+------------+-----------+-----------+---------
---+---------------+------------+-------------------------------
13594 | 16385 | repl | 10.206.79.240 | 10.206.79.240 | | 50462 | 2021-06-09 10:21:35.997181+00 | | streaming | 0/30255F8 | 0/30255F8 | 0/30255F8 | 0/30255F8 | | |
| 0 | async | 2021-06-13 18:06:50.870681+00
(1 row)
    • pg_stat_wal_receiver (可以在 standby 查)
select * from pg_stat_wal_receiver;
pid | status | receive_start_lsn | receive_start_tli | written_lsn | flushed_lsn | received_tli | last_msg_send_time | last_msg_receipt_time | latest_end_lsn | latest_end_time | slot_name | send
er_host | sender_port | conninfo
------+-----------+-------------------+-------------------+-------------+-------------+--------------+-------------------------------+-------------------------------+----------------+-------------------------------+---------------+------
---------+-------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------
1598 | streaming | 0/3000000 | 1 | 0/30255F8 | 0/30255F8 | 1 | 2021-06-13 18:12:41.476929+00 | 2021-06-13 18:12:41.477835+00 | 0/30255F8 | 2021-06-10 04:21:07.190989+00 | 10_206_79_240 | 10.20
6.79.197 | 5432 | user=repl passfile=/var/lib/pgsql/.pgpass channel_binding=prefer dbname=replication host=10.206.79.197 port=5432 application_name=10.206.79.240 fallback_application_name=walreceiver sslmode=prefer sslcompression=
0 ssl_min_protocol_version=TLSv1.2 gssencmode=prefer krbsrvname=postgres target_session_attrs=any
(1 row)
  • Timeline
    • 一開始 Postgres primary 的 timeline 是 1, transaction log 的檔名像是0000000100000000000000F5
    • 當 standby 被 promote 了, timeline 會變成 2, transaction log 的檔名像是0000000200000000000000F5
  • hot_standby_feedback = off
    • 由於 primary 與 standby 的狀態是單方面從 primary sync to standby, 所以如果 standby 做了一些 transaction 相關的事情, 會因為 primary 同時間也有動作而產生錯誤.
    • 為了能夠修正這個錯誤, 透過 hot_standby_feedback = on, 使 standby 可以定期傳"最後一筆 transaction log" 給 primary, 如此 primary 就有機會可以補救 (例如因為注意到 standby 的 transaction log 有差距而避免 delete data)
    • 不過為了避免 standby 長時間的 transaction 影響效能, hot_standby_feedback 是被關掉的 (相較於 transaction log streaming 效能好)
    • anyway, 如果在 OLTP 的情況下, select 花很久時間, 就可以考慮改這個設定

別名演算法 Alias Method

 題目 每個伺服器支援不同的 TPM (transaction per minute) 當 request 來的時候, 系統需要馬上根據 TPM 的能力隨機找到一個適合的 server. 雖然稱為 "隨機", 但還是需要有 TPM 作為權重. 解法 別名演算法...