Seata 分佈式事務啓動配置分析

想要掌握 Seata 的配置,必須瞭解 Seata 的啓動過程,瞭解啓動時的各項配置,才能在配置時知道該幹什麼。

用到的配置

這裏先列出 Server 啓動過程中實際用到的配置配置,下文會具體分析。

屬性 讀取值
config.type file
config.file.name file.conf
metrics.enabled FALSE
recovery.asyn-committing-retry-period 1000
recovery.committing-retry-period 1000
recovery.rollbacking-retry-period 1000
recovery.timeout-retry-period 1000
registry.type file
service.max.commit.retry.timeout -1
service.max.rollback.retry.timeout -1
store.file.dir sessionStore
store.file.file-write-buffer-cache-size 16384
store.file.flush-disk-mode async
store.file.session.reload.read_size 100
store.mode file
transaction.undo.log.delete.period 86400000
transport.heartbeat TRUE
transport.server NIO
transport.thread-factory.boss-thread-prefix NettyBoss
transport.thread-factory.boss-thread-size 1
transport.thread-factory.share-boss-worker FALSE
transport.thread-factory.worker-thread-prefix NettyServerNIOWorker
transport.thread-factory.worker-thread-size 8
transport.type TCP

Server 入口

io.seata.server.Server 類是整個服務的入口,從這裏的 main 方式入手。

public static void main(String[] args) throws IOException {
    //initialize the metrics
    MetricsManager.get().init();//1

    //initialize the parameter parser
    ParameterParser parameterParser = new ParameterParser(args);

    System.setProperty(ConfigurationKeys.STORE_MODE, parameterParser.getStoreMode());

    RpcServer rpcServer = new RpcServer(WORKING_THREADS);
    //server port
    rpcServer.setListenPort(parameterParser.getPort());
    UUIDGenerator.init(parameterParser.getServerNode());
    //log store mode : file、db
    SessionHolder.init(parameterParser.getStoreMode());

    DefaultCoordinator coordinator = new DefaultCoordinator(rpcServer);
    coordinator.init();
    rpcServer.setHandler(coordinator);
    // register ShutdownHook
    ShutdownHook.getInstance().addDisposable(coordinator);

    //127.0.0.1 and 0.0.0.0 are not valid here.
    if (NetUtil.isValidIp(parameterParser.getHost(), false)) {
        XID.setIpAddress(parameterParser.getHost());
    } else {
        XID.setIpAddress(NetUtil.getLocalIp());
    }
    XID.setPort(rpcServer.getListenPort());

    rpcServer.init();

    System.exit(0);
}

下面逐層對上面代碼進行分析。

1. MetricsManager.get().init()

MetricsManager 是一個單例實現,在 init 方法中,首先調用了 ConfigurationFactory.getInstance() 方法,該方法是最重要的一個配置入口,繼續深入進去來看。

1.1 ConfigurationFactory.getInstance()

ConfigurationFactory 返回的 io.seata.config.Configuration<T> 的單例,該單例通過 buildConfiguration() 創建。

buildConfiguration() 中,又首先使用了 CURRENT_FILE_INSTANCE.getConfig 方法。

這裏的 CURRENT_FILE_INSTANCE 創建方式如下:

private static final String REGISTRY_CONF_PREFIX = "registry";
private static final String REGISTRY_CONF_SUFFIX = ".conf";
private static final String ENV_SYSTEM_KEY = "SEATA_ENV";
private static final String ENV_PROPERTY_KEY = "seataEnv";

private static final String SYSTEM_PROPERTY_SEATA_CONFIG_NAME = "seata.config.name";

private static final String ENV_SEATA_CONFIG_NAME = "SEATA_CONFIG_NAME";

public static final Configuration CURRENT_FILE_INSTANCE;

static {
    //0.8.1 增加這個配置後,配置文件不用侷限在 conf 目錄(類路徑cp)下面了,通過 file: 前綴可以配置爲任意的路徑
    String seataConfigName = System.getProperty(SYSTEM_PROPERTY_SEATA_CONFIG_NAME);
    if (null == seataConfigName) {
        seataConfigName = System.getenv(ENV_SEATA_CONFIG_NAME);
    }
    if (null == seataConfigName) {
        seataConfigName = REGISTRY_CONF_PREFIX;
    }
    String envValue = System.getProperty(ENV_PROPERTY_KEY);
    if (null == envValue) {
        envValue = System.getenv(ENV_SYSTEM_KEY);
    }
    CURRENT_FILE_INSTANCE = (null == envValue) ? new FileConfiguration(seataConfigName + REGISTRY_CONF_SUFFIX)
            : new FileConfiguration(seataConfigName + "-" + envValue + REGISTRY_CONF_SUFFIX);
}

2019-10-12 更新上述代碼爲 0.8.1 版本,0.8.1 之後配置的路徑沒有了限制,可以更方便的在容器或者K8S中動態配置。

默認的 registry.conf 配置文件如下:

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "file"
  # 爲了簡短,只保留默認的 file 相關內容
  file {
    name = "file.conf"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "file"
  file {
    name = "file.conf"
  }
}

1.2 獲取 seata 配置

通過上述方式得到了 CURRENT_FILE_INSTANCE,現在回到 buildConfiguration() 方法:

private static Configuration buildConfiguration() {
    ConfigType configType = null;
    String configTypeName = null;
    try {
        // 獲取 config.type 名稱,參考上面默認配置,值爲 file
        configTypeName = CURRENT_FILE_INSTANCE.getConfig(
            ConfigurationKeys.FILE_ROOT_CONFIG + ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR
                + ConfigurationKeys.FILE_ROOT_TYPE);
        // 對應枚舉值爲 File
        configType = ConfigType.getType(configTypeName);
    } catch (Exception e) {
        throw new NotSupportYetException("not support register type: " + configTypeName, e);
    }
    // 默認配置這裏是 File
    if (ConfigType.File == configType) {
        // 獲取 config.file.name 值
        String pathDataId = ConfigurationKeys.FILE_ROOT_CONFIG + ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR
            + FILE_TYPE + ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR
            + NAME_KEY;
        // 上述默認值爲 file.conf
        String name = CURRENT_FILE_INSTANCE.getConfig(pathDataId);
        // 讀取 file.conf 配置
        return new FileConfiguration(name);
    } else {
        return EnhancedServiceLoader.load(ConfigurationProvider.class, Objects.requireNonNull(configType).name())
            .provide();
    }
}

從上面邏輯看,這裏就是從支持的多種 registry 中獲取具體的配置,默認是 file。

1.3 處理 metrics 配置

回到上一層方法:

// ConfigurationFactory.getInstance() 中是 file.conf 配置
boolean enabled = ConfigurationFactory.getInstance().getBoolean(
            // 獲取該配置中的 metrics.enabled,默認值爲 false
            ConfigurationKeys.METRICS_PREFIX + ConfigurationKeys.METRICS_ENABLED, false);

file.confmetrics 部分配置如下:

## metrics settings
metrics {
  # 默認不啓用
  enabled = false
  registry-type = "compact"
  # multi exporters use comma divided
  exporter-list = "prometheus"
  exporter-prometheus-port = 9898
}

啓用後,會讀取 metrics. registry-type,目前僅支持 compact,然後通過下面方法:

EnhancedServiceLoader.load(Registry.class, Objects.requireNonNull(registryType).name());

獲取對應的實現返回,在這裏的 load 方法中也是單例形式,只會初始化一次。

然後在讀取 metrics.exporter-list(逗號隔開) 獲取所有支持的 Exporter(目前也只支持 prometheus)。

prometheus 對應的 PrometheusExporter 實現中會讀取上面的 exporter-prometheus-port 獲取 HTTP 服務的端口號。

2. 解析命令行參數

在進行了配置的初始化後,開始處理 main 方法的命令行參數:

// 解析參數使用了 http://www.jcommander.org/
//initialize the parameter parser
ParameterParser parameterParser = new ParameterParser(args);
// 獲取存儲方式("--storeMode", "-m"),默認爲 file,可選 db
System.setProperty(ConfigurationKeys.STORE_MODE, parameterParser.getStoreMode());

RpcServer rpcServer = new RpcServer(WORKING_THREADS);
// 獲取端口號("--port", "-p"),默認爲 8091
rpcServer.setListenPort(parameterParser.getPort());
// 獲取服務節點ID("--serverNode", "-n"),默認 1
UUIDGenerator.init(parameterParser.getServerNode());
// 初始化存儲
SessionHolder.init(parameterParser.getStoreMode());//3

3. 初始化 RpcServer

代碼如下:

/**
 * Instantiates a new Abstract rpc server.
 *
 * @param messageExecutor the message executor
 */
public RpcServer(ThreadPoolExecutor messageExecutor) {
    super(new NettyServerConfig(), messageExecutor);
}

這裏用到了 NettyServerConfig,這個類中存在大量類似下面的方法:

/**
 * Get boss thread prefix string.
 *
 * @return the string
 */
public String getBossThreadPrefix() {
    return CONFIG.getConfig("transport.thread-factory.boss-thread-prefix", DEFAULT_BOSS_THREAD_PREFIX);
}

這裏用到了下面部分的配置:

transport {
  # tcp udt unix-domain-socket
  type = "TCP"
  #NIO NATIVE
  server = "NIO"
  #enable heartbeat
  heartbeat = true
  #thread factory for netty
  thread-factory {
    boss-thread-prefix = "NettyBoss"
    worker-thread-prefix = "NettyServerNIOWorker"
    server-executor-thread-prefix = "NettyServerBizHandler"
    share-boss-worker = false
    client-selector-thread-prefix = "NettyClientSelector"
    client-selector-thread-size = 1
    client-worker-thread-prefix = "NettyClientWorkerThread"
    # netty boss thread size,will not be used for UDT
    boss-thread-size = 1
    #auto default pin or 8
    worker-thread-size = 8
  }
  shutdown {
    # when destroy server, wait seconds
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}

這裏是對 netty 的各種詳細配置。

繼續往下深入看 UUIDGenerator.init

4. UUIDGenerator.init

這裏根據節點ID定義了服務器 UUID 值的範圍,避免了節點間的衝突。

有沒有人覺得這段代碼存在疑問?

public static long generateUUID() {
    //假設在超過最大值,進入下面同步前獲取了10個id
    //在上面第一個 id 經過 UUID.set(id); 後又獲取了 10 個新id
    //這10個新id 會不會和上面的10個- UUID_INTERNAL 後出現部分重複?
    long id = UUID.incrementAndGet();
    if (id >= UUID_INTERNAL * (serverNodeId + 1)) {
        synchronized (UUID) {
            if (UUID.get() >= id) {
                id -= UUID_INTERNAL;
                UUID.set(id);
            }
        }
    }
    return id;
}

5. SessionHolder.init

首先參數中的 storeMode 可選,如果沒有設置會按下面方式獲取:

if (StringUtils.isBlank(mode)) {
    //use default
    mode = CONFIG.getConfig(ConfigurationKeys.STORE_MODE);
}

也就是前面 file.conf 中的 store.mode,這部分默認配置如下:

## transaction log store
store {
  ## store mode: file、db
  mode = "file"

  ## file store
  file {
    dir = "sessionStore"

    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    max-branch-session-size = 16384
    # globe session size , if exceeded throws exceptions
    max-global-session-size = 512
    # file buffer size , if exceeded allocate new buffer
    file-write-buffer-cache-size = 16384
    # when recover batch read size
    session.reload.read_size = 100
    # async, sync
    flush-disk-mode = async
  }

  ## database store
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
    datasource = "dbcp"
    ## mysql/oracle/h2/oceanbase etc.
    db-type = "mysql"
    driver-class-name = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://127.0.0.1:3306/seata"
    user = "mysql"
    password = "mysql"
    min-conn = 1
    max-conn = 3
    global.table = "global_table"
    branch.table = "branch_table"
    lock-table = "lock_table"
    query-limit = 100
  }
}

上面的默認值也是 file,如果使用 db 參考上面配置修改即可。

再往下就是針對 db 和 file 的兩種處理策略。

5.1 db 方式

ROOT_SESSION_MANAGER = EnhancedServiceLoader.load(SessionManager.class, StoreMode.DB.name());

這裏額外指定了最後一個參數 activateName,這會在默認的 META-INF/services/META-INF/seata/ 基礎上額外去 META-INF/seata/db 目錄加載資源,並且該資源會在隊列的最後一個位置。然後判斷所有的資源是否有 @LoadLevel 並且和 activateName 匹配,如果存在多個匹配的值,就會使用最後一個。如果沒有匹配的值,會使用所有實現的最後一個。

這裏的 db 類定義如下:

@LoadLevel(name = "db")
public class DataBaseSessionManager extends AbstractSessionManager
    implements SessionManager, SessionLifecycleListener, Initialize {

ASYNC_COMMITTING_SESSION_MANAGER 等3個只是增加了額外的參數,在初始化的時候會使用相應的構造方法進行創建。

5.2 file 方式

這裏首先讀取 store.file.dir 獲取存儲 file 的路徑,然後創建下面的實現:

@LoadLevel(name = "file")
public class FileBasedSessionManager extends DefaultSessionManager implements Reloadable {

在 db 和 file 中還涉及了 TransactionStoreManager 的實例化,這裏不再深入。

SessionHolder.init 最後還有一個針對 file 方式的 reload

回到 main 方法繼續。

6. coordinator.init()

// 創建協調者
DefaultCoordinator coordinator = new DefaultCoordinator(rpcServer);
// 初始化
coordinator.init();
rpcServer.setHandler(coordinator);
// register ShutdownHook
ShutdownHook.getInstance().addDisposable(coordinator);

DefaultCoordinator 實現中,使用下面部分的配置:

recovery {
  #schedule committing retry period in milliseconds
  committing-retry-period = 1000
  #schedule asyn committing retry period in milliseconds
  asyn-committing-retry-period = 1000
  #schedule rollbacking retry period in milliseconds
  rollbacking-retry-period = 1000
  #schedule timeout retry period in milliseconds
  timeout-retry-period = 1000
}

transaction {
  undo.data.validation = true
  undo.log.serialization = "jackson"
  undo.log.save.days = 7
  #schedule delete expired undo_log in milliseconds
  undo.log.delete.period = 86400000
  undo.log.table = "undo_log"
}

service {
  #vgroup->rgroup
  vgroup_mapping.my_test_tx_group = "default"
  #only support single node
  default.grouplist = "127.0.0.1:8091"
  #degrade current not support
  enableDegrade = false
  #disable
  disable = false
  #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
  max.commit.retry.timeout = "-1"
  max.rollback.retry.timeout = "-1"
}

7. 其他

// 服務綁定 IP("--host", "-h")
// 127.0.0.1 and 0.0.0.0 are not valid here.
if (NetUtil.isValidIp(parameterParser.getHost(), false)) {
    XID.setIpAddress(parameterParser.getHost());
} else {
    XID.setIpAddress(NetUtil.getLocalIp());
}
XID.setPort(rpcServer.getListenPort());
// 啓動服務,通過 netty hold 住
rpcServer.init();
// 退出
System.exit(0);

這一篇只是記錄下各種參數在何時使用,便於初始配置時理解和使用,後續看情況補充 seata 相關內容。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章