開心一刻
剛剛和老婆吵架,氣到不行,想離婚
女兒突然站出來勸解道:難道你們就不能打一頓孩子消消氣,非要鬧離婚嗎?
我和老婆同時看向女兒,各自挽起了衣袖
女兒補充道:弟弟那麼小,打他,他又不會記仇
需求背景
專案基於 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 /PIDpid
/F /T
但這有個前提,需要知道 DataX 的 java 程序的 pid
,而 JDK8 中 Process
的方法如下
是沒有提供獲取 pid 的方法,在不調整 JDK 版本的情況下,我們如何獲取 DataX 程序的 pid?不同的作業系統獲取方式不一樣,我們分別對 Linux
和 Win
進行實現
-
Linux
實現就比較簡單了,僅僅基於 JDK 就可以實現
Field field = process.getClass().getDeclaredField("pid"); field.setAccessible(true); int pid = field.getInt(process);
透過反射獲取 process 實現類的成員變數
pid
的值;這段程式碼,你們應該都能看懂吧 -
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
我們來看下輸出結果
-
Linux
jar 輸出日誌如下
我們 ps 下程序
ps -ef|grep ping
-
Win
jar 輸出日誌如下
我們再看下工作管理員的 ping 程序
可以看出,不管是 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;
}
完整流程應該是
-
使用
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
-
透過
ProcessUtil#getProcessId
獲取 Process 的pid
,並與同步任務資訊繫結進行持久化透過任務id 可以查詢到對應的 pid
-
觸發任務
終止
,透過任務id找到對應的 pid,透過ProcessUtil#killProcessByPid
終止程序終止了程序也就終止了同步任務
如果 qsl-datax-hook
是單節點,上述處理方案是沒有問題的,但生產環境下,qsl-datax-hook 不可能是單節點,肯定是叢集部署,那麼上述方案就行不通了,為什麼呢?我舉個例子
假設 qsl-datax-hook 有 2 個節點:A、B,在 A 節點上啟動 DataX 同步任務(taskId = 666)並得到對應的 pid = 1488,終止任務 666 的請求被負載均衡到 B 節點,會發生什麼情況
- B 節點上沒有 pid = 1488 程序,那麼終止失敗,A、B 節點都不受影響
- B 節點上有 pid = 1488 程序,這個程序可能是 DataX 同步任務程序,也可能是其他程序,那麼這個終止操作就會產生可輕可重的故障了!
然而需要終止的同步任務卻還在 A 節點上安然無恙的執行著
所以叢集模式下,我們不僅需要將 pid 與任務進行繫結,還需要將任務執行的節點資訊也繫結進來,節點資訊可以是 節點ID
,也可以是 節點IP
,只要能唯一標識節點就行;具體實現方案,需要結合具體的負載均衡元件來做設計,由負載均衡元件將任務終止請求分發到正確的節點上,而不能採用常規的負載均衡策略進行分發了;因為負載均衡元件很多,所以實現方案沒法統一設計,需要你們結合自己的專案去實現,我相信對你們來說很簡單
總結
- 任務的啟動方式不同,終止方式也會有所不同,如何優雅的終止,是我們需要考慮的重點
- 直接殺程序的方式,簡單粗暴,但不夠優雅,一旦錯殺,問題可大可小,如果有其他方式,不建議選擇該方式
- 適用單節點的終止方式不一定適用於叢集,大家設計方案的時候一定要做全方位的考慮
- 示例程式碼:qsl-datax-hook