異源資料同步 → DataX 同步啟動後如何手動終止?

青石路發表於2024-11-05

開心一刻

剛剛和老婆吵架,氣到不行,想離婚
女兒突然站出來勸解道:難道你們就不能打一頓孩子消消氣,非要鬧離婚嗎?
我和老婆同時看向女兒,各自挽起了衣袖
女兒補充道:弟弟那麼小,打他,他又不會記仇

開心一刻

需求背景

專案基於 DataX 來實現異源之間的資料離線同步,我對 Datax 進行了一些梳理與改造

異構資料來源同步之資料同步 → datax 改造,有點意思
異構資料來源同步之資料同步 → datax 再改造,開始觸及原始碼
異構資料來源同步之資料同步 → DataX 使用細節
異構資料來源資料同步 → 從原始碼分析 DataX 敏感資訊的加解密
異源資料同步 → DataX 為什麼要支援 kafka?
異源資料同步 → 如何獲取 DataX 已同步資料量?

本以為離線同步告一段落,不會再有新的需求,可打臉來的非常快,產品經理很快找到我,說了如下一段話

昨天我在測試開發環境試用了一下離線同步功能,很好的實現了我提的需求,給你點贊!
但是使用過程中我遇到個情況,有張的表的資料量很大,一開始我沒關注其資料量,所以配置了全量同步,啟動同步後遲遲沒有同步完成,我才意識到表的資料量非常大,一查才知道 2 億多條資料,我想終止同步卻發現沒有地方可以進行終止操作
所以需要加個功能:同步中的任務可以進行終止操作

這話術算是被產品經理給玩明白了,先對我進行肯定,然後指出使用中的痛點,針對該痛點提出新的功能,讓我一點反駁的餘地都沒有;作為一個講道理的開發人員,面對一個很合理的需求,我們還是很樂意接受的,你們說是不是?

需求一接,問題就來了

如何終止同步

思考這個問題之前,我們先來回顧下 DataX 的啟動;還記得我們是怎麼整合 DataX 的嗎,異構資料來源同步之資料同步 → datax 再改造,開始觸及原始碼 中有說明,新增 qsl-datax-hook 模組,該模組中透過命令

Process process = Runtime.getRuntime().exec(realCommand);
realCommand 就是啟動 DataX 的 java 命令,類似

java -server -Xms1g -Xmx1g -XX:+HeapDumpOnOutOfMemoryError -Ddatax.home=/datax -classpath /datax/lib/* com.alibaba.datax.core.Engine -mode standalone -jobid -1 -job job.json

來啟動 DataX,也就是給 DataX 單獨啟動一個 java 程序;那麼如何停止 DataX,思路是不是就有了?問題是不是就轉換成了

如何終止 java 程序

終止程序

如何終止程序,這個我相信你們都會

Linux:kill -9 pid
Win:cmd.exe /c taskkill /PID pid /F /T

但這有個前提,需要知道 DataX 的 java 程序的 pid,而 JDK8 中 Process 的方法如下

Process方法

是沒有提供獲取 pid 的方法,在不調整 JDK 版本的情況下,我們如何獲取 DataX 程序的 pid?不同的作業系統獲取方式不一樣,我們分別對 LinuxWin 進行實現

  1. Linux

    實現就比較簡單了,僅僅基於 JDK 就可以實現

    Field field = process.getClass().getDeclaredField("pid");
    field.setAccessible(true);
    int pid = field.getInt(process);
    

    透過反射獲取 process 實現類的成員變數 pid 的值;這段程式碼,你們應該都能看懂吧

  2. Win

    Win 系統下,則需要依賴第三方工具 oshi

    <dependency>
        <groupId>com.github.oshi</groupId>
        <artifactId>oshi-core</artifactId>
        <version>6.6.5</version>
    </dependency>
    

    獲取 pid 實現如下

    Field field = process.getClass().getDeclaredField("handle");
    field.setAccessible(true);
    long handle = field.getLong(process);
    WinNT.HANDLE winntHandle = new WinNT.HANDLE();
    winntHandle.setPointer(Pointer.createConstant(handle));
    int pid = Kernel32.INSTANCE.GetProcessId(winntHandle);
    

    同樣用到了反射,還用到了 oshi 提供的方法

合併起來即得到獲取 pid 的方法

/**
 * 獲取程序ID
 * @param process 程序
 * @return 程序id,-1表示獲取失敗
 * @author 青石路
 */
public static int getProcessId(Process process) {
    int pid = NULL_PROCESS_ID;
    Field field;
    if (Platform.isWindows()) {
        try {
            field = process.getClass().getDeclaredField("handle");
            field.setAccessible(true);
            long handle = field.getLong(process);
            WinNT.HANDLE winntHandle = new WinNT.HANDLE();
            winntHandle.setPointer(Pointer.createConstant(handle));
            pid = Kernel32.INSTANCE.GetProcessId(winntHandle);
        } catch (Exception e) {
            LOGGER.error("獲取程序id失敗,異常資訊:", e);
        }
    } else if (Platform.isLinux() || Platform.isAIX()) {
        try {
            field = process.getClass().getDeclaredField("pid");
            field.setAccessible(true);
            pid = field.getInt(process);
        } catch (Exception e) {
            LOGGER.error("獲取程序id失敗,異常資訊:", e);
        }
    }
    LOGGER.info("程序id={}", pid);
    return pid;
}

得到的 pid 是不是正確的,我們是不是得驗證一下?寫個 mainClass

/**
 * mainClass
 * @author 青石路
 */
public class HookMain {

    public static void main(String[] args) throws Exception {
        String command = "";
        if (Platform.isWindows()) {
            command = "ping -n 1000 localhost";
        } else if (Platform.isLinux() || Platform.isAIX()) {
            command = "ping -c 1000 localhost";
        }
        Process process = Runtime.getRuntime().exec(command);
        int processId = ProcessUtil.getProcessId(process);
        System.out.println("ping 程序id = " + processId);
        new Thread(() -> {
            try (BufferedReader reader = new BufferedReader(
                    new InputStreamReader(process.getInputStream(), System.getProperty("sun.jnu.encoding")))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    System.out.println(line);
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }).start();
    }
}

利用 maven 打包成可執行 jar 包

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <configuration>
                <archive>
                    <manifest>
                        <addClasspath>true</addClasspath>
                        <classpathPrefix>lib/</classpathPrefix>
                        <mainClass>com.qsl.hook.HookMain</mainClass>
                    </manifest>
                </archive>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-dependency-plugin</artifactId>
            <executions>
                <execution>
                    <id>copy-dependencies</id>
                    <phase>package</phase>
                    <goals>
                        <goal>copy-dependencies</goal>
                    </goals>
                    <configuration>
                        <outputDirectory>target/lib</outputDirectory>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

然後執行 jar

java -jar qsl-datax-hook-0.0.1-SNAPSHOT.jar

我們來看下輸出結果

  1. Linux

    jar 輸出日誌如下

    Linux_輸出

    我們 ps 下程序

    ps -ef|grep ping
    
    Linux_驗證
  2. Win

    jar 輸出日誌如下

    win_輸出

    我們再看下工作管理員的 ping 程序

    win_驗證

可以看出,不管是 Linux 還是 Win,得到的 pid 都是正確的;得到 pid 後,終止程序就簡單了

/**
 * 終止程序
 * @param pid 程序的PID
 * @return true:成功,false:失敗
 */
public static boolean killProcessByPid(int pid) {
    if (NULL_PROCESS_ID == pid) {
        LOGGER.error("pid[{}]異常", pid);
        return false;
    }
    String command = "kill -9 " + pid;
    boolean result;
    if (Platform.isWindows()) {
        command = "cmd.exe /c taskkill /PID " + pid + " /F /T ";
    }
    Process process  = null;
    try {
        process = Runtime.getRuntime().exec(command);
    } catch (IOException e) {
        LOGGER.error("終止程序[pid={}]異常:", pid, e);
        return false;
    }
    try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
        //殺掉程序
        String line;
        while ((line = reader.readLine()) != null) {
            LOGGER.info(line);
        }
        result = true;
    } catch (Exception e) {
        LOGGER.error("終止程序[pid={}]異常:", pid, e);
        result = false;
    } finally {
        if (!Objects.isNull(process)) {
            process.destroy();
        }
    }
    return result;
}

完整流程應該是

  1. 使用 Runtime.getRuntime().exec(java命令) 啟動 DataX,並獲取到 Process

    java 命令指的是啟動 DataX 的 java 命令,例如

    java -server -Xms1g -Xmx1g -XX:+HeapDumpOnOutOfMemoryError -Ddatax.home=/datax -classpath /datax/lib/* com.alibaba.datax.core.Engine -mode standalone -jobid -1 -job job.json
    
  2. 透過 ProcessUtil#getProcessId 獲取 Process 的 pid,並與同步任務資訊繫結進行持久化

    透過任務id 可以查詢到對應的 pid

  3. 觸發任務 終止,透過任務id找到對應的 pid,透過 ProcessUtil#killProcessByPid 終止程序

    終止了程序也就終止了同步任務

如果 qsl-datax-hook 是單節點,上述處理方案是沒有問題的,但生產環境下,qsl-datax-hook 不可能是單節點,肯定是叢集部署,那麼上述方案就行不通了,為什麼呢?我舉個例子

假設 qsl-datax-hook 有 2 個節點:A、B,在 A 節點上啟動 DataX 同步任務(taskId = 666)並得到對應的 pid = 1488,終止任務 666 的請求被負載均衡到 B 節點,會發生什麼情況

  1. B 節點上沒有 pid = 1488 程序,那麼終止失敗,A、B 節點都不受影響
  2. B 節點上有 pid = 1488 程序,這個程序可能是 DataX 同步任務程序,也可能是其他程序,那麼這個終止操作就會產生可輕可重的故障了!

然而需要終止的同步任務卻還在 A 節點上安然無恙的執行著

所以叢集模式下,我們不僅需要將 pid 與任務進行繫結,還需要將任務執行的節點資訊也繫結進來,節點資訊可以是 節點ID,也可以是 節點IP,只要能唯一標識節點就行;具體實現方案,需要結合具體的負載均衡元件來做設計,由負載均衡元件將任務終止請求分發到正確的節點上,而不能採用常規的負載均衡策略進行分發了;因為負載均衡元件很多,所以實現方案沒法統一設計,需要你們結合自己的專案去實現,我相信對你們來說很簡單

你懂我意思吧_懂

總結

  1. 任務的啟動方式不同,終止方式也會有所不同,如何優雅的終止,是我們需要考慮的重點
  2. 直接殺程序的方式,簡單粗暴,但不夠優雅,一旦錯殺,問題可大可小,如果有其他方式,不建議選擇該方式
  3. 適用單節點的終止方式不一定適用於叢集,大家設計方案的時候一定要做全方位的考慮
  4. 示例程式碼:qsl-datax-hook

相關文章