スクリプトの処理時間短縮について

Yuki21
Tera Expert

お世話になっております。
以下のQAでご教示いただいた方法でテーブルに添付されたCSVファイルをテーブルへ取り込むスクリプト実行に関して、
大量のレコードを含むCSVファイルを処理するケースの処理時間短縮が検討事項として挙がっております。
※約50万件のデータ取込みに45分程かかっています

スクリプトを使わない方法で処理時間短縮が見込める方法はあるのでしょうか?

変換マップの使用については、結局CSV→テーブルのインポートが必要になり、他のマスタテーブル参照が必要になることで難しいと判断しています。

<参考QA>
https://community.servicenow.com/community?id=community_question&sys_id=90b968971bb04550cdd555fa234b...

1 ACCEPTED SOLUTION

ScriptAction は 並列には実行されないと思っています。並列処理できるか試してみてください。ひとつ処理が終わるまで次のScriptActionは実行されないかと思います。
 
簡単な並列化のScriptは ScheduleOnce を使うことです。これは使い捨てのScheduleJobを作成できます。公式Docsは無いですがScript include にあります、検索してみてください。
参考Script
function script(){
    //ScheduleJob
    gs.info('Test OK');
} 
var sched = new ScheduleOnce();
sched.script = '('+script+')();';
sched.schedule();

ScheduleJobをScript includeに保存する話が別でありますが、そうするべき重要な理由があります。
ScheduleJobはUpdateSetに保存されません。これでは開発運用上問題があります。そのためUpdateSetに保存されるScript includeにほぼすべてのScriptを記載して、ScheduleJobは呼び出すだけにするのが開発の運用上、良い方法です。ScheduleJobはXMLファイルで適用するか強制的にUpdateSetに記録する。

Performanceの話も出てますが、今回のケースではほぼ気にならない程度のはずです。50万件をScheduleJobで処理しているときも、動作速度は変わらなかったはずです。その程度のScheduleJobがひとつあるだけなら体感的な影響はほぼなしです。注意しなければならないのは、ScheduleJobの同時に処理できる数は少ないことと、長時間占有すると、順番待ちしているScheduleJobが実行できないだけでなく、順番が来てもとっくに開始時間が過ぎていているため開始しないことがあります。そのためScheduleJobの大量作成と長時間占有には注意して制御するべきです。結果的に夜間に実行することが多いですが夜間に多くのScheduleJobの滞留が発生すると処理できない場合があります。

これら難しい処理を、ServiceNowの知識が乏しい時には、挑戦しない方が良いです。自分の力量を大きく上回る実装は見送る事も大切な事です。出来る範囲でシステムを使って、仕組みがわかってから改良していくことが良いです。良くわからない謎の技術で動いているScriptが完成しても、それは制御できない非常に危険なシステムです。それと自分しかわからない特殊なScriptも危険です。無理しないことが安全です。

View solution in original post

20 REPLIES 20

iwai
Giga Sage
  • 『スクリプトの処理時間短縮について』

単純な実験をしてRecord作成にかかる時間を測定して、そこからどのような改善するべきか考えました。その開発環境でもどの処理にどのくらいの時間がかかるのか把握して対応策を検討してみてください。

  • 実験1、参考QAのCSV読み込みScriptを使って10万件CSVを1ファイル読み込みにかかった時間
    • 4分29秒(269秒)でした。50万件なら5倍として22分25秒(1345秒)です。
    • この結果から考えてると、45分もかかるとしたら、相当無駄な処理が他にあると考えられます。
  • 実験2、参考QAのCSV読み込みScriptに1行加えてBusinessRule停止"setWorkflow(false)"を加えて時間を測定
    • 3分26秒(206秒)でした。50万件なら17分10秒(1030秒)です。BusinessRuleの実行を止めるとある程度速度が速くなります。この場合BusinessRuleがどのくらい処理を重くしているかにもよります。
  • 実験3、参考QAのCSV読み込みScript のループをしないでBusinessRuleも止めて、Record作成だけ10万件処理しました。これが理論的にRecord作成の最速だと思います。
    • 2分54秒(174秒)でした。50万件なら14分30秒(870秒)です。単純なRecord作成だけでこれだけ時間がかかると言えます。
    • Record作成を並列に2つに分割すればそれは理論的には半分になります。3つ、4つと分割すれば、理論的には早くなります。
  • 実験4、参考QAのCSV読み込みScript の Record作成"insert()"をコメントアウトして、Record作成しないで10万件CSVを読み込むだけの処理時間
    • 0分9秒でした。50万件なら45秒です。数秒なのでRecord作成以外は改善はほぼ考えなくて良さそうです。

ServiceNowはRecordの作成や削除が極端に遅いです。それは作成や削除から関連して動作する処理が膨大にあり、それぞれが遅いからです。

この実験から考えられるのは、50万件のRecord Insertの最速は14分30秒。ほかの処理を一切しなくてもこれより早くはなりそうにないので、後は並列処理で2つ同時にRecord Insertするなどして処理速度を短縮する。

BusinessRuleを止めるだけでも、速度改善効果は高い。BusinessRuleが必要かどうか検討する。

45分もかかっているのは、一般的なRecord insertの22分25秒と比較しても圧倒的に遅いので、極端に遅いBusinessRuleがあるか、そもそも45分かかる処理内容が今回の実験とは異なる場合もある。例えば複数のRecordのCSVファイルの処理をしていて単純な1ファイルの50万件ではない場合など。

参考にしてみてください。

Yuki21
Tera Expert

Ozawa様、Iwai様

ご返信いただきありがとうございます。
Insert以外にスクリプトの中でファイルの妥当性チェックを行っており、その処理時間も加算されているように思います。

「BusinessRule停止」
 ビジネスルールを確認しましたが、当該テーブルに設定されているビジネスルールはありませんでした。

「並列処理」
 添付ファイルをレコード数、もしくはファイルサイズによって事前に分割し、スクリプトで並列処理可能か検討いたします。

それ以外に、「テーブルのインデックスを外す※Insertは早くなる?」、「ファイル形式の変更(CSV→JSON)※速くなるかは不明」について検討中です。

1. 次は遅いです。(実行時間:30ms)

function find_sys_id(table_name, field_name, find_val) {

    var id_rec = new GlideRecord(table_name); // レコード作成対象テーブル
    id_rec.addQuery('location.name', find_val);
    id_rec.query();
    if (id_rec.next()) {
       return id_rec.sys_id;
    }
}

var timer = new OCTimer();
timer.start("test");

var sys_id = find_sys_id('sys_user', 'location', 'Tokyo');

timer.stop('test');
var result = timer.result();
gs.info(result);

次ぎのようにする。(実行時間:2ms)

function find_sys_id(table_name, field_name, find_val) {

    var id_rec = new GlideRecord(table_name); // レコード作成対象テーブル
    id_rec.addQuery(field_name + '.name', find_val); // 参照フィール名(name)と比較する
    id_rec.query();
    if (id_rec.next()) {
       return id_rec.sys_id;
    }
    return 'not found';
}

var timer = new OCTimer();
timer.start("test");

var sys_id= find_sys_id('sys_user', 'location', 'Tokyo');
gs.info(sys_id);

timer.stop('test');
var result = timer.result();
gs.info(result);

2. GlideRecord.getRowCount()の代わりにGlideAggregate('COUNT')を使う

var grAtt = new GlideRecord('sys_attachment');
...
rec_count = grAtt.getRowCount();

https://docs.servicenow.com/bundle/rome-application-development/page/script/glide-server-apis/topic/...

 

3. 質問とは無関係ですが入力ミスがあります。

rec.intialize(); -> rec.initialize();

そのScriptのRecord検索で検索条件なしにquery()してnext()でループしている処理のほとんどが致命的に非効率な処理を繰り返しています。全面的に処理を見直したほうが良いです。

検索条件なしにQueryしてifで判断するのは極力止めましょう。どれだけ考えてもほかに手段がないときにだけ遅いことを承知で利用してください。少なくとも今回は適切な検索条件が組めそうです。

deleteRecordもこの場合、遅いので別処理の並列に処理するか、DeleteRecordしないで全件チェックしてからRecord作成するなどしたほうが良いです。

GlideRecord('sys_attachment');を検索して、再度getAttachments()で同じsys_attachment Tableを検索するのは無駄な処理です。

フィールドの正常性Checkも不要かもしれません、マスターテーブルを検索するとき完全一致なので、一致するものがあれば正常ですし、該当するものが無ければエラーではないでしょうか。日付形式もそのまま格納した後、値が格納されてなければ入力値の誤りなのでエラーなのはわかります。あえてチェックする必要性がありません。

まだまだ改善するべき個所が多くあります。

1.query()の結果が1つより多い場合はエラーを返しているので「grAtt.orderBy('sys_created_on');」は不要。

2. 細かいことですが、「var insert_rect = new Array()」宣言は不要。

var timer = new OCTimer();

var loopCnt = 500000;

timer.start("new");
var arr = new Array();
for(var i=0; i<loopCnt ; i++){arr=[]};
timer.stop('new');

timer.start("bracket");
for(var i=0; i<loopCnt ; i++){var arr = []};
timer.stop('bracket');

var result = timer.result();
gs.info(result);

3. 性能の問題ではありませんがwhileループの前に変数err_flagが宣言及びfalseに設定されていません。

function getCsv(grImp) {
        //CSVファイル1行目以降が空欄でなければレコード作成
        var err_flg = false;  //  <<-- 変数err_flgを宣言
        while ((csvLine = reader.readLine()) != null) { //「.readLine()」で1行ずつ読み込む。空の行は「null」になる。

            if (format_flg == false) { //書式が相違している場合の処理
                err_flg = true; //添付ファイル削除回避
                break;
            }
        }

        //CSVがヘッダーのみの場合の処理
        if (csv_row_count == 0) {
            err_flg = true; //添付ファイル削除回避
        }

        //エラーが発生せずに正常にレコード作成ができた添付ファイルは削除。
        if (err_flg == false) {
        }
    }
}

4.繰り返して実行されるのはfind_sys_id()とformat_check()なので、この2つの関数を改善すると実行時間を短縮できると思います。

format_check()を修正。データベースに登録する場合は日付の形式を統一した方がよいのでyyyy-mm-dd HH:MM:SSに統一させる。正規表現チェックが減るので少し早くなります。

var timer = new OCTimer();

var loopCnt = 50000;

var record = ['123','123456','12345','2022/01/14 07:47:00'];

timer.start("new");

    //各列の書式を確認するための正規表現を配列に格納する。
    //CSVファイルのA列~が0番目~の要素とリンクしている。
    const rec_regex = [
        /\d{3}$/, //CSVファイルA列用【数字3桁】
        /\d{6}$/, //CSVファイルB列用【数字6桁】
        /\d{5}$/, //CSVファイルC列用【数字5桁】
        /[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])\s([01]?[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$/, //CSVファイルD列用【yyyy-mm-dd HH:MM:SS形式】
    ];


function format_check(record) {

    var much_flg = true; //設定した正規表現と一致しているかの確認用フラグ。


    //for文でCSVファイルの各列の値の書式チェックを行う。1つでも一致しなければ「false」を返して処理終了。
    for (var i = 0; i < rec_regex.length; i++) {
        much_flg = rec_regex[i].test(record[i]); //指定した正規表現と「record」配列の内容が一致しているか確認し、「true」or「false」を返す
        if (!much_flg) return false;
    }


    //全て一致していると「true」を返す
    return true;
}
record[3] = record[3].replace(/\//g,'-');
gs.info(record[3]);
for (var i=0;i<loopCnt;i++) {
//record = record.replace('\/','-');

var check_result = format_check(record);
}

timer.stop('new');
gs.info('function result:' + check_result);

var result = timer.result();
gs.info(result);