Digdagで大きいパラメータを登録すると後続の処理が重くなる

この記事は[ZOZOテクノロジーズ #5 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/zozo_tech5) 7日目の記事になります。

また、今年は全部で5つのAdvent Calendarが公開されています。

[ZOZOテクノロジーズ #1 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/zozo_tech)
[ZOZOテクノロジーズ #2 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/zozo_tech2)
[ZOZOテクノロジーズ #3 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/zozo_tech3)
[ZOZOテクノロジーズ #4 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/zozo_tech4)


# 概要
Digdagのstoreパラメータに大きな値を入れたときに後続のジョブがすべて遅くなってしまうと言うことがあったので紹介します。
考えてみれば大きな値を毎回DBから取り出してDigdag側でパース等しているので重くなるのは当たり前なのですが。

そこで今回は実際にどんな感じで遅くなるのかを紹介します。また、その回方法を紹介します。

# 計測
まずは、storeパラメータの大きさによってどれくらい処理のスピードが落ちるのか計測してみます。
以下のようなワークフローを利用してそれぞれ時間を計測しました。

## 計測用のコード
はじめにstoreに値を登録するRubyスクリプトを作成します。
aが100個並んだの文字列のリストを作成します。長さは10/100/1000/10000のものを用意します。

#### tasks/task.rb

```rb
class Task
  def store10
    list = ['a' * 100] * 10
    Digdag.env.store(LIST: list)
  end

  def store100
    list = ['a' * 100] * 100
    Digdag.env.store(LIST: list)
  end

  def store1000
    list = ['a' * 100] * 1000
    Digdag.env.store(LIST: list)
  end

  def store10000
    list = ['a' * 100] * 10000
    Digdag.env.store(LIST: list)
  end
end
```

続いて上記のスクリプトを実行後に"hello"を出力するようなワークフローを作成します。
XXXXには10/100/1000/10000の値が入ります。
#### storeXXXX.dig

```digdag
+task1:
  _export:
    docker:
      image: ruby
  rb>: Task.storeXXXX
  require: 'tasks/task'

+task2:
  echo>: 'hello'
```

## 実行結果
```
$ time digdag run store10.dig --rerun
2019-12-05 11:40:38 +0900: Digdag v0.9.39
(略)
2019-12-05 11:40:40 +0900 [INFO] (0018@[0:default]+store10+task1): rb>: Task.store10
2019-12-05 11:40:41 +0900 [INFO] (0018@[0:default]+store10+task2): echo>: hello
hello
Success. Task state is saved at /Users/katsuya.tajima/src/github.com/katsuyan/digdag-example/digdag-store/.digdag/status/20191205T000000+0000 directory.
  * Use --session <daily | hourly | "yyyy-MM-dd[ HH:mm:ss]"> to not reuse the last session time.
  * Use --rerun, --start +NAME, or --goal +NAME argument to rerun skipped tasks.
digdag run store10.dig --rerun  3.90s user 0.38s system 106% cpu 4.020 total
```

```
$ time digdag run store100.dig --rerun
2019-12-05 11:40:44 +0900: Digdag v0.9.39
(略)
2019-12-05 11:40:46 +0900 [INFO] (0018@[0:default]+store100+task1): rb>: Task.store100
2019-12-05 11:40:48 +0900 [INFO] (0018@[0:default]+store100+task2): echo>: hello
hello
Success. Task state is saved at /Users/katsuya.tajima/src/github.com/katsuyan/digdag-example/digdag-store/.digdag/status/20191205T000000+0000 directory.
  * Use --session <daily | hourly | "yyyy-MM-dd[ HH:mm:ss]"> to not reuse the last session time.
  * Use --rerun, --start +NAME, or --goal +NAME argument to rerun skipped tasks.
digdag run store100.dig --rerun  3.98s user 0.38s system 108% cpu 3.996 total
```

```
$ time digdag run store1000.dig --rerun
2019-12-05 11:40:50 +0900: Digdag v0.9.39
(略)
2019-12-05 11:40:52 +0900 [INFO] (0018@[0:default]+store1000+task1): rb>: Task.store1000
2019-12-05 11:40:55 +0900 [INFO] (0018@[0:default]+store1000+task2): echo>: hello
hello
Success. Task state is saved at /Users/katsuya.tajima/src/github.com/katsuyan/digdag-example/digdag-store/.digdag/status/20191205T000000+0000 directory.
  * Use --session <daily | hourly | "yyyy-MM-dd[ HH:mm:ss]"> to not reuse the last session time.
  * Use --rerun, --start +NAME, or --goal +NAME argument to rerun skipped tasks.
digdag run store1000.dig --rerun  5.01s user 0.42s system 113% cpu 4.795 total
```

```
$ time digdag run store10000.dig --rerun
2019-12-05 11:40:58 +0900: Digdag v0.9.39
(略)
2019-12-05 11:41:00 +0900 [INFO] (0018@[0:default]+store10000+task1): rb>: Task.store10000
2019-12-05 11:42:29 +0900 [INFO] (0018@[0:default]+store10000+task2): echo>: hello
hello
Success. Task state is saved at /Users/katsuya.tajima/src/github.com/katsuyan/digdag-example/digdag-store/.digdag/status/20191205T000000+0000 directory.
  * Use --session <daily | hourly | "yyyy-MM-dd[ HH:mm:ss]"> to not reuse the last session time.
  * Use --rerun, --start +NAME, or --goal +NAME argument to rerun skipped tasks.
digdag run store10000.dig --rerun  92.45s user 1.50s system 102% cpu 1:31.28 total
```

以上の結果からリストの長さが10000のときに明らかに`task2`の実行時間が伸びていることがわかります。
今回は、遅くなるということを示したかったのでどれくらいでどのくらい遅くなるのかということは示していません。

# パラメータの値が大きくなるケース
それでも、どうしても大きいリストをstoreに入れて利用したいという場合があると思います。
例えばBigQueryからパラメータをとってきて、それを`for_each`オペレータで並列処理させたいということがありました。
このときは以下のようなデータ構造でsotreに5000件ほどのパラメータを入れていました。

```ruby
[
  {
    id: 1,
    text: '文字列'
  },
  (略)
]
```

このやり方だと後続の処理がすごーく遅くなってしまったので、そこで使った回避策を紹介します。

# 回避策
## プライマリーキーの利用
上記のようなケースでは、事前にKVSやRDBなどに後続で利用したいパラメータをintなど比較的小さいPKと一緒に登録します。そしてstoreにはPKのリストのみ登録してやることで大きさを減らすことができます。

```digdag
# BigQueryからid/textを取得しKVSに登録
+get_bigquery_and_set_kvs:
  _export:
    docker:
      image: ruby
  rb>: Task.get_bigquery_and_set_kvs 
  require: 'tasks/task'

# PKでループを回しKVSからtextを取得し出力する
+loop
  for_each>:
    PK: ${PK_LIST}
  _parallel: true
  _do:

    # PKを利用してKVSからtextを取得
    +get_text_from_kvs
      _export:
        docker:
          image: ruby
      rb>: Task.get_text_from_kvs
      require: 'tasks/task'

    # textを出力
    +print_text
      _export:
        docker:
          image: ruby
      rb>: Task.print_text 
      require: 'tasks/task'
```

## storeの上書き
以上のような方法をとったとしてもPKのリストは大きくなってしまいます。その状態で都度PKのリストを参照してしまうため、後続のジョブすべての処理が結局遅くなってしまいます。そこですでに必要ない大きいパラメータを上書きすることで回避することができます。以下のように`for_each`でPKのリストを渡したあとに、後続の処理ではPKのリストを空文字列で上書きしてやると後続への影響をなくすことができます。

```digdag
+loop
  for_each>:
    PK: ${PK_LIST}
  _parallel: true
  _do:

    _export:
      PK: ''

    # PKを利用してKVSからtextを取得
    +get_text_from_kvs
      _export:
        docker:
          image: ruby
      rb>: Task.get_text_from_kvs
      require: 'tasks/task'
```

### 実験
実際にsotreを上書きすることで、処理が早くなるか実験してみます。
計測時に示した10000件のワークフローに、task2のタイミングで`LIST`を空文字で上書きしています。

#### ワークフロー
```digdag
+task1:
  _export:
    docker:
      image: ruby
  rb>: Task.task10000
  require: 'tasks/task'

+task2:
  _export:
    LIST: ''
  sh>: echo 'task2!!'
```

#### 結果
```s
$ time digdag run store10000_update_list.dig --rerun
2019-12-05 12:30:02 +0900: Digdag v0.9.39
(略)
2019-12-05 12:30:04 +0900 [INFO] (0018@[0:default]+store10000_update_list+task1): rb>: Task.store10000
2019-12-05 12:30:06 +0900 [INFO] (0018@[0:default]+store10000_update_list+task2): sh>: echo 'task2!!'
task2!!
Success. Task state is saved at /Users/katsuya.tajima/src/github.com/katsuyan/digdag-example/digdag-store/.digdag/status/20191205T000000+0000 directory.
  * Use --session <daily | hourly | "yyyy-MM-dd[ HH:mm:ss]"> to not reuse the last session time.
  * Use --rerun, --start +NAME, or --goal +NAME argument to rerun skipped tasks.
digdag run store10000_update_list.dig --rerun 4.06s user 0.40s system 107% cpu 4.162 total
```

以上の結果から上書きしてやることで全体の処理が早く終わることがわかりました。

# まとめ
今回はDigdagのパラメータにでかい値を登録すると後続のジョブが遅くなることについて紹介しました。
また、その回避策についてPKを利用する方法とstoreを上書きする方法について紹介しました。

ただし、そもそもでかい値をstoreに登録する時点でDigdagの用途とかけ離れてしまっているのかなとも感じたので、それをふまえて参考にしていただければと思います。


今日から3日連続Digdagネタです。明日は「[digdag-myjdbc-pluginを作ろうとした話](https://qiita.com/katsuyan/items/81d8d41111bfab73cb17)」になります。