Hank Lin

A new blog

用Terracotta 去Distribute Objects

| Comments

Terracotta DSO

Terracotta又來啦! 這次是講所謂的Terracotta Distributed Shared Object(DSO). 上次有提到用terracotta去作tomcat cluster, 把HttpSession裡的東西在cluster間分享, 不過在一般Java application也想有這種distributed objects要怎麼作? 上一篇畢竟是初次體驗, 對terracotta有什麼本事還沒有說得很完整. 這次我要比較詳細的說明terracotta的架構和原理, 將來如果要開始設計一個distributed applications時, 可以試著用terracotta的programming model去看問題. 不一定要用terracotta, 但是多瞭解一種clustering的方法總是好的. 如果還沒裝terracotta, 可以看上一篇把terracotta裝起來. 先來看一個很簡單的例子:

DsoMain.java

package com.hanklin;

import java.util.Random;

public class DsoMain {
    private Integer number = new Random().nextInt();

    public static void main(String[] args) {
        DsoMain tc1 = new DsoMain();
        DsoMain tc2 = new DsoMain();
        print(tc1.number, tc2.number);
        tc1.number = new Integer(tc2.number);
        print(tc1.number, tc2.number);
    }

    private static void print(Integer n1, Integer n2) {
        System.out.println("n1=" + n1 + ", n2=" + n2);
        System.out.println("n1==n2 ? " + (n1 == n2));
        System.out.println("n1.equals(n2)? " + (n1.equals(n2)));
    }
}

看起來是再簡單不過了, 執行看看:

$ java -cp . com.hanklin.DsoMain
n1=174724645, n2=1254665546
n1==n2 ? false
n1.equals(n2)? false
n1=1254665546, n2=1254665546
n1==n2 ? false
n1.equals(n2)? true

好咩, 一切都在你的預料之中. 但是這一切在terracotta加入後就會改變了. 先來看terracotta configuration xml: tc-dso.xml



  
  
    
      %(user.home)/terracotta/server-data
      %(user.home)/terracotta/server-logs
    
  

  
    %(user.home)/terracotta/client-logs 
  
  
  
    
      
        
          com.hanklin.*..*
        
      
      
        
          com.hanklin.DsoMain.number
          number
        
      
    
    

上面的設定檔最重要的地方是區塊, 在這邊設定com.hanklin.DsoMain.number是terracotta root, terracotta root 下的所有objects都會被distributed. 存好了之後, 再來開terracotta server:

$ start-tc-server.sh -f /opt/tc/tc-dso.xml
2010-04-18 18:45:06,485 INFO - Terracotta 3.2.1, as of 20100302-130324 (Revision 14673 by cruise@su10mo5 from 3.2)
2010-04-18 18:45:06,878 INFO - Configuration loaded from the file at '/opt/tc/tc-config.xml'.
2010-04-18 18:45:07,058 INFO - Log file: '/root/terracotta/server-logs/terracotta-server.log'.
2010-04-18 18:45:09,473 INFO - Available Max Runtime Memory: 490MB
2010-04-18 18:45:11,903 INFO - JMX Server started. Available at URL[service:jmx:jmxmp://0.0.0.0:9520]
2010-04-18 18:45:12,470 INFO - Terracotta Server instance has started up as ACTIVE node on 0.0.0.0:9510 successfully, and is now ready for work.

一樣, 最好每次都用-f明確指定configuration file. 現在來看看distributed 版本要怎麼run. 一樣是用java, 但是主要多了3個參數-Xbootclasspath/p:, -Dtc.config, -Dtc.install-root. terracotta的文件是用dso-java.sh 去執行. 例如:

$ dso-java.sh -cp . com.hanklin.DsoMain

但是之前的經驗是它在處理command line arguments內含空白會有問題, 所以我都沒有用它, 看了一下dso-java.sh的內容, 其實就是給這3個參數而已, 所以後來我就都手動給這3個參數, 如果嫌麻煩就設定環境變數吧: Linux版:

$ export my_tc_setting="-Xbootclasspath/p:/opt/tc/lib/dso-boot/dso-boot-hotspot_linux_160_18.jar -Dtc.install-root=/opt/tc  -Dtc.config=localhost:9510"

Windows版:

$ set my_tc_setting=-Xbootclasspath/p:C:libterracotta-3.2.1libdso-bootdso-boot-hotspot_win32_160_18.jar -Dtc.install-root=C:libterracotta-3.2.1  -Dtc.config=localhost:9510

好的, 執行看看咩:

$ java -cp . $my_tc_setting com.hanklin.DsoMain
2010-04-18 18:49:08,635 INFO - Terracotta 3.2.1, as of 20100302-130324 (Revision 14673 by cruise@su10mo5 from 3.2)
2010-04-18 18:49:09,138 INFO - Configuration loaded from the server at 'localhost:9510'.
2010-04-18 18:49:09,413 INFO - Log file: '/root/terracotta/client-logs/terracotta-client.log'.
2010-04-18 18:49:10,933 INFO - Connection successfully established to server at 127.0.0.1:9510
n1=1220249279, n2=1220249279
n1==n2 ? true
n1.equals(n2)? true
n1=1220249279, n2=1220249279
n1==n2 ? true
n1.equals(n2)? true

結果好像令人大吃一驚! 而且再invoke一次也是一樣, 這是為啥咪咧? 以下就要講解一下Terracotta 的實際作法了.

解說terracotta

要在cluster內share objects一直是個難題. Java一般的作法是RMI, JMS, 或是distributed caches. 由於network與memory相比是慢非常多的, 所以儘量減少network traffic是增進效能和scalability的作法. 不過一般的framework都是要靠Java Serialization, 所以沒有辦法作fine-grained optimize. Terracotta不是把objects serialize後再傳來傳去, 而是有兩種作法: physically和logically.

  • physically managed: terracotta 記錄object的fields 的變化, 然後在其它clustered objects寫入.
  • logically managed: terracotta 記錄method calls 及參數, 在clustered objects “replay”. logically managed 一般來說效能較好, 而且不會和不同版本的Java 的實作細節綁死. 如java.util.HashMap等collections就是logically managed.

另外, 所有terracotta applications(L1)都只和terracotta server(L2)溝通, network traffic不會成指數上昇. (岔題一下, L2 server要達成HA/scalability又是另一個大問題了, 以後再討論.) terracotta application 可以得知fine-grained 改變(到byte-level), 把這資訊送到terracotta server(L2), L2再決定要通知哪些L1. 如果L2的memory放不下shared objects的話會寫到file system. 所以Java heap size可以馬上從1,2G提昇了一檔次到1,200G! 在Scaling Your Java EE Applications – Part 2裡有提到terracotta在10個nodes以下時有很好的scalability. 不過那是有一點久以前的文章了, terracotta針對L2 write files, network transfer有作很多optimizations, 以後我有空也會來測測, 要自己親身體驗一下才算數, 您說是吧!

雖然說Terracotta號稱是a transparent clustering service. 但是transparent是指沒有API侵入的transparent(如: Serializable, RMI, JMS…), 在programming model上還是要確實瞭解terracotta的行為, 才不會有像上面一樣令人驚訝的事發生. Terracotta的transparency是由class loading 時, 對需要的classes作instrumentation達成的. 所以是用AOP去實作的. Terracotta在選擇join point的地方是用AspectWerkz語法. 而且不但要改client code(也就是我們的code)的行為, 連JRE library也要改, 所以這也就是為什麼一定要給bootstrap classpath(-Xbootclasspath/p:)的原因了. 在各種bytecode instructions中, terracotta會改的最多的地方就是read/write to memory這兩類instructions, read就是找accessor(.), write就是assignment(=). 另外還有constructor instructions, threading instructions(lock, unlock)等也是terracotta要處理的. 有寫過AOP程式的人就知道, 如果要weaving的code base很大的話, 有可能會作很久(看是在什麼時候作weaving). 所以最好是只寫有需要被instrumented就好, 可以的話用package來分是最好的, 也能加快weaving的速度.

在上面的例子有看到區塊, 告訴terracotta我們要cluster什麼classes. Terracotta roots的定義是: A top-level object in a clustered object graph. root object graph 內不可以有Non-Portable Classes, 否則就會造成這個root也是Non-portable. Terracotta就會給你一缸子的Exceptions. 解決方法是改用portable class, 或是標成terracotta transient. Terracotta transient和Java transient 不太一樣. Java source code 裡的transient keyword不會自動被terracotta 視為transient, 所以要用以下的設定:


  
    true
    com.company.pckg.*
  
  ...

或是直接標示transient:


  
    com.company.ClassA.fieldA
    com.company.pckg.ClassB.fieldB
  
  ...

roots的初始化行為很特別, 會受到terracotta的特別照顧:

  • 當JVM第一次assign值給root的時候, terracotta在cluster內依據assign的值建立這個root object.
  • 一但root建立好了之後, 之後所有root的assignment都會被忽略. 如果root是Terracotta literals的話, 值可以變, 但是其它的class 的root reference則不可改變.
  • root 的top-level object不會被terracotta distributed GC所回收.

所以這就是在上面的例子裡, 為什麼改不動一個instance field的原因了. 在實務上, 通常都是用Map或是Collection來作root, 不過要注意有一些collections是不支援的(Non-Portable Classes) 另外, 應該shared objects是data而不是application logic, 如果你需要用很多transient field, 可能就是share了不合適的objects.

Terracotta常見的用途

  • distributed cache: 不受memory大小的限制, 而是由L2 的disk 大小限制. 還有因為Map類的key不會被faulted out of the JVM, 所以keys 一定要放得進memory才行.(通常可以咩! 又不是facebook, twitter)
  • session replication: 上次那一篇有提到這個用法.
  • workload partitioning: 這一篇老文章: Implementing Master-Worker with Terracotta, 有提到這個用法.

我在這邊作一個超簡單的workload partitioning 的POC, 先來一個Master: DsoMasterMain.java

package com.hanklin;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class DsoMasterMain {
    static BlockingQueue queue = new LinkedBlockingQueue(3);

    public static void main(String[] args) {
        String[] works = { "A", "B", "C", "D", "E", "F", "G", "H", "0", "0"};
        for (String work : works) {
            try {
                System.out.println("put work " + work);
                queue.put(work);
                Thread.sleep(999);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

用一個LinkedBlockingQueue作為work queue, 在這邊就是我們的terracotta root. 再來是Worker: DsoWorkerMain.java

package com.hanklin;

public class DsoWorkerMain {
    public static void main(String[] args) {
        String work = null;
        while (!"0".equals(work = DsoMasterMain.queue.poll())) {
            try {
                if (work == null) {
                    Thread.sleep(350);
                } else {
                    System.out.println("doing work " + work);
                }
            } catch (Exception e1) {
                e1.printStackTrace();
            }
        }
    }
}

worker收到”0“則代表沒有work了. 然後是terracotta configuration xml, 只要改那邊就好:


    com.hanklin.DsoMasterMain.queue
    queue

改好了之後, 先重開terracotta server:

$ start-tc-server.sh -f /opt/tc/tc-dso.xml

然後, 我是先開2個worker啦:

$ java -cp . $my_tc_setting com.hanklin.DsoWorkerMain
$ java -cp . $my_tc_setting com.hanklin.DsoWorkerMain

最後, 開個master來看看:

$ java -cp . $my_tc_setting com.hanklin.DsoMasterMain

我的結果是第一個worker:

doing work A
doing work G

第二個worker:

doing work B
doing work C
doing work D
doing work E
doing work F
doing work H

的確有簡單咩, 您說是吧?