kettle學習筆記及最佳實踐


最近在用kettle遷移數據,從對kettle一點不會到比較熟悉,對於期間的一些問題和坑做了記錄和總結,內容涵蓋了使用的經驗和技巧,踩到的坑、最佳實踐和優化前后結果對比。

常用轉換組件

計算形成新字段:只限算術運算,並且選擇固定
過濾記錄:元表某字段按照某個條件分流,滿足條件的到一個表,不滿足的到另一個表,這兩個目標表都必須有。
Switch/Case:和過濾記錄類似,可以多個條件判斷,並且有默認轉向條件,可以完美替換過濾記錄組建
記錄分組:group by 組建未能正常按照預期理解運行
設置為NULL:將某個特定值設置為NULL
行扁平化:行扁平化,使用與某條件下某名稱對應的行數相同的情況
行列轉換:行轉成列,使用Row Normalizer組件,事先一定要是根據分組字段排好序,關鍵字段就是name列字段,分組字段就是按照什么分組,目標字段就是行轉列之后形成的字段列表。 8.字段選擇:選擇需要的目的列到目標表,並且量表的對應字段不一樣時可以用來做字段映射
排序:分組前先排序可以提高效率
條件分發:根據條件分發,相當與informatica的router組件
值映射:相當與oracle的decode函數,源和目標字段同名的話,只要寫源字段就可以了
#常用輸入組件

表輸入:源表輸入
文本文件輸入:文本文件輸入
xml文件輸入:使用Get Data From XML組件,可以在其中使用xpath來選擇數據
JsonInput:貌似在中文環境下組件面板里看不到,切換到英文模式就看到了
#常用輸出組件

表輸出:表輸出
文本文件輸出:文本文件輸出
XML文件輸出:輸出的XML文件是按照記錄行存儲的,字段名為元素名
Excel文件輸出:輸出的excel文件是按照記錄行存儲的,字段名為元素名
刪除:符合比較條件的記錄將刪除
更新:注意兩個表都要有主鍵才可以
插入/更新:速度太慢,不建議使用
檢查字段是否存在:若在則家一個標志位,值可以是Y/N
等值連接:有關聯關系字段可以關聯,其它的不關聯。
笛卡爾連接:所有兩邊的記錄交叉連接
write to log:把數據輸出到控制台日志里,一般調試時很常用
空操作:很常用,比如過濾數據,未過濾走正常流程,濾除的數據就轉向空操作。我喜歡在轉換里用它做開始和結束之類需要分發或匯聚數據流的場景
#內置變量

Internal.Transformation.Name  當前轉換的名字
Internal.Job.Name 當前job名字
Internal.Job.Filename.Name job的文件名
#需要修改的配置

在java8里-XX:MaxPermSize,-XX:PermSize已經去掉了,需要修改成-XX:MetaspaceSize 和 -XX:MaxMetaspaceSize

 

生產環境和開發環境使用不同的數據庫連接

~/.kettle/kettle.properties里設置key=value

在kettle.properties中添加變量,然后在類似數據庫連接的地方可以用${key}來使用,這樣可以實現開發環境和生產環境配置的差異,就算往資源庫里提交也可以互不影響了

 

kettle分頁問題

kettle循環分頁

首先弄一個轉換A,根據源表獲取記錄數,頁數,每頁記錄數,然后寫入系統變量,然后在job里調用轉換A,再加一個轉換B來遷移數據(其中查詢sql要使用轉換A生成的系統變量),最后在job里用一個javascript腳本來判斷查詢記錄數是否是0,如果是0就走執行成功,否則就繼續執行轉換B。

最關鍵的是判斷的js腳本,可以參考

var prevRow=previous_result.getRows();//獲取上一個傳遞的結果,這種方案需要在轉換B中將記錄集復制為結果,如果記錄集較多會造成內存溢出。就算在job里執行也是如此
完整代碼:

if (prevRow==null && prevRow.size()==0){
    false;
}else{
    var startRow=parseInt(parent_job.getVariable("START_ROW", 0));
    var pageSize=parseInt(parent_job.getVariable("PAGE_SIZE",1000));
    startRow=startRow+pageSize;
    parent_job.setVariable("START_ROW", startRow);
    true;
}

 

kettle分頁循環的更高效的改進方案

在轉換里,每執行一次有個SUCC_COUNT環境變量就+1,在job中用js腳本判斷成功數是否>=總記錄數,是就終止循環,否就起始行+每頁記錄數,下面是代碼

var startRow=parseInt(parent_job.getVariable("startrow"));
var totalItemCount=parseInt(parent_job.getVariable("totalitemcount"));
if (startRow >= totalItemCount){
    false;
}else{
    true;
}

對比前一種方案,改進方案一次遷移一萬條數據沒有壓力,而且cpu穩定在20%以下。

 

 

參數和變量

全局變量參數

在kettle.properties中配置,通過獲取環境變量組件來讀取,一般用來做數據庫連接配置等

 

位置參數(arguments參數)

最多支持10個,通過命令行參數的位置來區別,不是太好用

 

命名參數(named params)

通過 -param:name=value的方式設置參數,如果傳多個參數需要

-param:name1:value1 -param:name2:value2
配置方法

在轉換中雙擊空白處添加命名參數arg3,arg4,用的時候可以 ${arg3},${arg4}來使用,注意:如果不直接執行轉換就不要配置轉換命名參數(轉換的命名參數和全局參數在調試時有時候會出現莫名其妙的沖突),建議使用全局參數來替代
在job中雙擊轉換,切換到命名參數頁,點擊獲取參數(arg3和arg4會出現到列表里)注意:在使用全局參數的時候這步可以省略
在job中雙擊空白處添加命名參數arg3,arg4,然后在調用kitchen.sh時通過 -param:arg3=abc -param:arg4=def來使用,注意:-param傳遞的命名參數一定要在job中事先定義才可以。
命名參數可以做變量使用,即${var}的方式來調用,如果是日期這樣必須包含'的場景,可以用-param:date="2018-1-1 0:0:0"來表示,在sql里用'${var}'來表示

 

kitchen常用命令

命令行執行job(repository模式)

./kitchen.sh -listrep

kitchen.sh -rep=<respository名字> -user=<respository登錄用戶名> -pass=<respository密碼> -level=<日志級別> -job=<job名字> -logfile=<日志文件路徑>

kitchen.sh -rep=olpbdb01 -user=admin -pass=admin -level=Basic -dir=/demo1 -job=demo1   //會執行repository上 /demo1/demo1.kjb

 

命令行執行job(文件模式)

kitchen.sh -file=/home/job/demo.kjb >> /home/job/log/demo.log

 

命令行執行轉換(respository模式)

pan.sh -rep=mysql -user=admin -pass=admin -dir=/fixbug -trans=f_loan_update -level=Basic -logfile=/data/kettle.log

 

命令行執行轉換(文件模式)

pan.sh -file /data/kettle/demo1/t_test_rep_mysql.ktr

 

三種增量同步的模式

時間戳增量同步:表中增加一個時間戳字段,每次更新值查詢update_time>上次更新時間的記錄。優點速度快,實現簡單,缺點是對數據庫有侵入性,對於業務系統也需要更新時間戳,增加了復雜性。
觸發器增量同步:使用觸發器來監控數據變化,對數據庫有侵入性並且實現難度較大
全表增量同步:主要是用合並記錄來比對,優點是數據庫侵入較小,實現簡單,缺點是性能較差。 個人觀點是全表比對要好一點,如果按照分頁的方式的化,二十幾萬條數據20分鍾可以全同步完成。但全表增量同步只適合對實時性要求不高的場景。

 

幾個常用組件的用途

1.字段選擇:比如上一步驟有10個字段,下一步驟需要對其中某個字段做處理,就用字段選擇來選擇那個字段。還有,如果要合並記錄,也會在數據流中使用字段選擇選擇一下字段。還有就是字段選擇自帶刪除字段和修改字段類型和格式的功能

2.寫日志:在處理數據時用寫日志組建來記錄logger是個不錯的方法。

3.Switch/Case:和合並記錄配合使用可以實現增量的數據插入/更新和刪除。用過濾記錄也可以實現同樣功能

4.表輸出:實際就是向表里insert數據,里面有個[返回自動產生的關鍵字]功能很好用,相當與insert后立刻查詢的到剛剛自增的ID,省去了一部查詢操作。

5.更新,刪除:和名字一個意思

6.空操作:這個也很有用。

7.記錄集連接:類似sql中的join操作,把兩個數據流的字段(類型相同,列數相同,位置相同且已經排序過)拼合到一起。

8.分組:類似sql里的group by,構成分組的字段是分組條件(如果沒有組可分但又要把每一行的數據都拼成一個串,可以不設置分組條件),聚合部分的字段是類似在select部分需要用聚合函數處理的字段。在拼合in 條件時很有用。

9.javascript腳本:這個不好用,能不用就不要用了。javascript組件支持將js變量轉成輸出字段。注意在轉換里js腳本是每行執行一次。

10.獲取變量:如果有外部傳入的命名參數或者有環境變量,最好獲取變量是做為流程的起點來使用。

11.設置變量:把某個字段轉成變量時可以用。

12.表輸入

12.1.一般提供一個復雜的sql查詢,而且如果表輸入需要參數,那么前一步驟一定是個獲取變量。

12.2.如果需要實現動態sql(即拼一個sql存入變量A,然后在表輸入里執行${A}),必須用兩個轉換實現。

12.3.如果需要實現每行查詢一次(盡量避免這樣做,太慢),可以在表輸入中選中從步驟插入數據,並勾選執行每一行,在表輸入的前一個步驟使用選擇選擇表輸入的參數,在表輸入中用占位符?來表示字段選擇中選擇的字段。

12.4.如果有可能,盡量一次性的用表輸入完成所有的各類計算,轉換,排序,而盡量避免使用kettle自帶組件,因為這樣速度快。

13.映射

13.1.可以在轉換里調用另一個轉換,轉換中通過映射輸入規范來接收入參數(實際就是個表記錄集,在輸入規范里定義的都是字段),用映射輸出規范來定義輸出數據集。這樣整個映射就可以作為一個步驟整合到一個轉換里(有輸入和輸出)。映射可以實現轉換流程邏輯的復用。

13.2..關於在同一個轉換的不同步驟中先修改變量然后再獲取變量(取得的是轉換剛開始執行時的值)不正確的問題,官方是這樣解釋的,在轉換開始時會有一些變量初始化,初始化之后一些轉換中的步驟並不是順次執行的,所以無法做到同一個轉換中在一個步驟。對於這種情況需要拆成兩個抓換,先定義和初始化變量,然后再另一個轉換中獲取變量,需要注意的是,如果是轉換中定義變量在子映射的獲取的話也是不行的。

14.執行結果里面的Preview data非常好用,可以跑起來查看每個步驟的處理結果,如果發現一個步驟有數據,下一個步驟沒數據了,那么可能是有問題了。

15.對於執行時有錯誤的情況,最好采用一張表來存儲執行除錯的數據,這對於無人職守遷移數據很重要。可以做成一個子轉換來實現功能的復用。

16.對於javascript的調試,最好使用第三方的js開發工具來做,kettle自帶的js編輯器太垃圾了。

17.合並記錄時總是報NullPointerException,原因是合並記錄的兩個來源可能有不存在的情況,也可能是兩個數據來源的排序不一致

18.轉換的配置里的日志可以在線上部署的時候先禁用掉,有問題的時候可以再打開(通過點擊連接線)

 

kettle的最佳實踐

啟動時

kettle不能加入到PATH里去,加了執行 kitchen.sh -listrep找不到資源庫
在~/.kettle里有重要的kettle.properties和repositories.xml文件,服務器部署的時候需要拷貝上去
spoon圖形界面一般用來調試,跑多條數據會很慢
個人認為文件模式比repository模式好用點,repository模式總是莫名其妙的出問題,並且repository無法保留變更歷史,但文件模式+git就可以做到
Unable to get module class path. (java.lang.RuntimeException: Unable to open JAR file, probably deleted: error in opening zip file) 需要刪掉 <kettle_home>/system/karaf/caches/下的所有文件
啟動時閃退時需要刪掉~/.kettle/db.cache打頭的文件就可以了。

 

防內存溢出和提高性能的處理辦法

數據量較大時一定要使用分頁機制,控制每個批次導入5000~10000
需要在分頁循環中首先用一個獨立的轉換來計算出當前批次的用戶ID數組,頁碼數量,總記錄數以及維度表的數據,比如有日期維度表,那么就需要算出當前批次要處理的日期時間數組,最后把這些數據存入到全局變量里面去。這樣在后續步驟就可以取出這些全局變量內容按照分頁批次進行遷移了。 2.分頁要通過一個表輸入根據傳入的每頁記錄數動態計算出總頁數,並把總頁數,總記錄數存入全局變量,然后每處理一行計數器加1,截止條件就是總記錄數<=處理過的記錄數,從而實現的分頁循環。
分頁變量務必要通過命名參數-param來傳遞,這樣在生產環境萬一碰到了數據過大造成內存泄漏,可以通過參數快速調整
分頁需要動態在模型中計算出頁碼數和總記錄數,可以用個sql來搞定
select count(1) totalitemcount,
       round(CEIL(count(1)/${pagesize})) pagecount
  from table_name
 where create_time between unix_timestamp('${startdate}') and unix_timestamp('${enddate}')
之后的結果(totalitemcount,pagecount)用設置變量組件存入變量里就ok了 5. 注意數據量較大時不要使用記錄復制到結果組件,不然一定會內存溢出 6. kettle的很多功能都有對應的純sql實現方法,比如加字段,比如排序和空值的處理,純sql的實現方式要比kettle的方式快很多,而且對內存的消耗也會小很多。 7. 可以設置幾個變量來優化性能 KETTLE_MAX_LOG_SIZE_IN_LINES=5000 #內存里最多記錄多少行日志 KETTLE_MAX_LOG_TIMEOUT_IN_MINUTES=1440 #kettle日志的保留時間,單位是分鍾 KETTLE_MAX_JOB_ENTRIES_LOGGED=1000 #內存中保留多少實體返回結果日志 KETTLE_MAX_JOB_TRACKER_SIZE=1000 #內存里最多保留多少job跟蹤記錄 KETTLE_MAX_LOGGING_REGISTRY_SIZE=1000 #內存里記錄多少實體 來優化內使用情況(在~/.kettle/kettle.properties里設置)

 

遷移模型的設計原則

整個模型必須是job+多個轉換(除非是一次性工作可以沒有job)
job可以認為是表級處理(即一次處理多行,所有組件都是對於多行的處理組件),轉換可以認為是行級處理(即一次處理一行,所有組件都是一次一行)
轉換分兩組,初始化變量用的轉換(至少有一個,也可能有多個,主看是否有新變量,因為新變量無法在同一個轉換里使用),和遷移數據用的轉換(看情況,一般一個就夠了)
命名參數的選擇,即通過-param:varname=value的參數,一般需要有startdate,enddate,startrow,pagesize幾個就夠了
遷移的模式一般來說需要一個獨立的轉換根據日期區間計算出本次需要處理的業務ID數組(即先鎖定該批次要處理交易),然后第二個轉換根據事先鎖定的交易ID數組提取出日期時間數組,用戶ID數組,地區數組等。第三個轉換再使用前面兩步轉換里提取的變量查詢數據進行遷移
在遷移i數據時,數據流分成了新數據流(針對業務表)和舊數據流(針對事實/維度表),新數據流通過排序、分組、字段選擇和連接數據集join起來,然后通過合並記錄組件計算出每行記錄的flagfield(new/changed/deleted/identical的評判結論),然后通過Switch/Case或過濾記錄分別針對每種情況進行處理(調用表輸出/更新/刪除)
如果轉換時涉及多個類別的數據要遷移到一張事實/維度表,不要拆成兩組job,可以在一個job里依次調用一組轉換,執行完一組再執行下一組,千萬不要並行,因為設置的全局變量名字都一樣,會出現沖突問題

 

變量的的使用

${Internal.Entry.Current.Directory}/test.ktr可以表示當前目錄下的test.ktr,同時適配repository模式和local文件模式
關於變量的使和編程語言中的變量不太一樣,無法使用在同一個轉換中定義和獲取當前轉換內修改過的變量,變通方法是拆成兩個轉換來使用,這問題卡了好幾天才找到原因。
在job/轉換通過-param:varname=value的方式傳參時,如果發現變量無法解析,那么一定是job和轉換的命名參數里沒有配置(雙擊空白處,有個命名參數頁簽....)
在job/轉換開始執行的時候通過日志輸出一下用到的變量是個很好的習慣
作業和轉換都要有命名參數startrow,pagesize,startdate,enddate幾個,這樣可以在調用的時候靈活控制分頁以及起止時間,靈活實現全量和增量遷移
對變量沖突的問題要小心,特別是同一個job並行處理多個轉換時更是如此,因此在job里並行執行轉換時要格外小心。
寫變量時有對變量作用域的設置,推薦設置成Valid in the root job,不推薦Valid in the Java Virtual Matchine。

 

表輸入的處理

表輸入有個功能,可以每行都執行一次查詢,這個功能不要用,太慢對內存占用很高。
推薦使用記錄集連接的方法,比如A,B,C三個表要通過外鍵拼接在一起插入到D表中,那么可以A,B,C三個表分別通過表輸入查詢出來,然后通過連接記錄集拼接到一起做為新數據(排序、字段對齊、類型要一致),然后查詢出D表做為老數據(排序、字段對齊、類型要一致),然后通過合並記錄的方式對比新老數據,並根據flagfield的四個狀態值(new/update/delete/identical)來通過Switch/Case組件分別處理插入/更新/刪除和無變化四種情況。處理完成后,記得startrow要加一,這樣做會顯著提升遷移性能。
表輸入最好選中忽略插入錯誤選項並且設置自動產生的關鍵字字段名稱,並且在下一步驟用Switch/Case判斷下這個自動產生的關鍵字字段的內容是否是null,不這樣做,當插入出錯時(不會在表輸出步驟報錯),錯誤會在表輸出的下一步驟報錯。

 

全量和增量遷移

全量遷移和增量遷移做到一起可以通過合並記錄+Switch/Case來判斷flagfied的值分別實現對應的插入/更新/刪除/無變化四種類型的數據處理。千萬不要將全量遷移和增量遷移分開,維護工作量太大了

 

映射功能

映射功能可以提升整個模型的復用度,映射中的輸入就是外部查詢的業務數據(不同的業務數據sql不同,但對於初始化的維度數據必須一致),這樣可以實現業務轉換和通用轉換的分離,極大的降低整個模型的復雜度和維護難度。
每個維度表要使用一個獨立的轉換(內部實現新增/修改/刪除等功能),在事實表中調用維度表轉換(每個維度字段都要對應一個轉換)在業務模型查詢出本頁里面需要處理的維度數據集,然后傳入映射(子轉換)並交由子轉換(輸入規范組件)做處理,每個子轉換處理一個維度表,處理完的結果在子轉換中通過映射輸出規范組件輸出到父模型里,然后父模型可以繼續往下處理。
映射調試:首先輸入數據用文本輸出組件輸出成A.txt,然后在映射中同時添加映射輸入規范和文件輸入(使用A.txt)通過點擊連接禁用和啟用輸入規范和文件輸入實現調試映射和在父轉換中調試映射。
映射可以提升模型的復用度,比如類似日期處理,地區處理,用戶處理這些一般都要抽取成可復用的模型,通過映射嵌入到別的模型里去,這樣模型的層次比較清晰和簡潔,而且不顯得那么亂
映射時的變量我推薦勾選默認的“從父轉換集成所有變量”,而不要每個轉換里都定義,當子轉換和父轉換中的變量同名時很容易出現稀奇古怪的問題
映射偶爾會出現不返回數據的情況(重復執行可能又正常了,估計是kettle的bug),經過測試,在傳參的上一步加一個文件輸出會有改善(連接使用復制就可以)

 

模型調試

完整模型是由一個job,多個轉換組成(轉換也分成了業務轉換和共用轉換),執行遷移的時候通過job來執行(不要直接執行轉換),從邏輯上只要關注全局變量的內容和轉換的結果就可以了。這樣對於調試來說效率較高。
整個完整的轉換中,全局變量是統一的,主要包含startdate,enddate,pagesize,所有轉換和job都使用這三個命名參數,在通過kitchen.sh執行時要通過-param傳入這三個變量
某個共用轉換需要另一個轉換中的數據的時候,可以使用文件保存輸出數據,然后在共用轉換中用文件輸入替代映射輸入規范的輸入參數

 

合並記錄時的幾個注意事項。

貌似kettle是用數據字段匹配的,關鍵字段必須可以唯一確定一條數據(類似聯合唯一索引的作用,但不要選事實表主鍵),如果關鍵字段是空,那么合並的結果可能多條會合並成一條。數據字段是標識新舊數據需要比對哪些字段(也就是通過哪些字段來的到new/update/delete/identical的評判結論)。
合並記錄時數據字段的多少並不影響合並后的結果。
合並記錄最大的作用是對比兩個數據流的數據變化,自動識別出需要插入/更新/刪除和無變化的數據行,再配合Switch/Case組件分別實現insert、update和delete。
注意在合並記錄中不能有更新時間,否則會出現很奇怪的結果
合並記錄時需要注意一個是新舊數據源的排序必須相同,第二就是關鍵字和數據字段的選擇,這兩點做到了結果就是對的。
合並記錄后某些記錄的flagfield總是不正確(已經存在的數據flagfield是new,造成插入時唯一索引沖突),這說明前面步驟的排序方式不對
合並記錄如果出現相同的兩條數據flagfield一個是new另一個是deleted,那說明關鍵字定義的幾個字段有差異
合並記錄后處理更新和刪除時條件部分選擇合並記錄的關鍵字保持一致

 

表輸出的處理

表輸出有個[返回一個自動產生的關鍵字]在insert后,可以將主鍵值自動獲取到並填入到一個新字段,在后續的步驟可以通過給字段賦值來回寫到主鍵字段,再通過字段選擇移除這個臨時生成的新字段,可以減少一次查詢表的過程。
表輸出時有個選項是忽略插入錯誤,這個一定要打開,但要有區分,一般唯一索引異常是要忽略掉的,但其余不能忽略,這個問題可以通過添加自定義錯誤來解決。
合並之前一定要用選擇字段對新舊數據流里面的字段做一致化處理(元數據名字,類型,長度,精度,Binary to Normal)甚至字段的數量和順序都要嚴格匹配
合並之前字段一定要排序,並且排序規則完全一致
合並記錄后,flagfield=identical的記錄主鍵會是0,這時候需要把老數據重新連接到合並之后的數據流就可以了(注意排序和字段名字)

 

異常處理

表輸出異常,表輸出是支持異常的,組件上右鍵菜單里選“定義錯誤處理”,可以設置錯誤列名,字段名,描述等信息,在后續步驟中可以使用過濾記錄來做甄別,比如主鍵沖突錯誤要Contains Duplicate entry就忽略掉異常,其余的錯誤就停止,這樣可以提高容錯性。

 

排序

很多組件都要實現對數據排序,比如分組、合並記錄、連接數據集等,如果不排序會出現一些稀奇古怪的問題
有條件一定用sql排序,只有中間步驟沒法使用sql排序的情況下才使用kettle自帶的排序。

 

異常和報錯

you're mixing rows with different storage types. Field [VISIT_NO String(13)<binary-string>] does not have the same storage type as field [VISIT_NO String(13)]
有兩種辦法,一種是兩處數據流在查詢的時候,字段類型不一致,可以在sql里做下cast轉換解決。
另外一種是通過字段選擇,在元數據頁添加一致的類型(長度,格式,最重要的是Binary to Normal改成true)
java.lang.NullPointerException:
這種一般是有合並記錄步驟,需要兩個輸入步驟,但其中有的輸入步驟不存在,就會報這個錯
字段選擇提示字段不存在,但字段明明存在:
因為在選擇和修改頁把字段修改了名字,在元數據頁填修改后的新名字就不報錯了

 

優化前后效果對比

優化前

里面有大量的每行查詢一次的功能,一次遷移最多2000條,多了就內存溢出
全量遷移一次要2天
全量和增量遷移是分開的,修改起來很羅嗦
沒有使用變量,limit限制條件都是寫死的,每次執行都要先調整模型的limit參數
沒有使用子轉換,模型間沒有復用,模型都是復制來復制去,很容易出錯
缺少異常處理

 

優化后

增加命名參數,遷移的時候非常靈活
改用分頁機制,每頁數據量可以通過參數傳遞
將全量模型和增量模型整合在一起,通過時間參數來控制
將用戶,時間,地區等復用度高的模型寫成了子轉換,通過映射的方式提高模型復用度,極大的簡化了模型。
有異常處理,同時對插入數據做了容錯(索引沖突不報錯,認為是成功)
增加了完善的數據調試機制

 

優化后每次遷移可以5000~10000條(通過參數設置,如果中間出錯還可以從上次斷開的地方繼續遷移),自動全量遷移,相同的數據量遷移一次只要1小時就搞定。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM