0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【kSQL Flow #3】毎朝の無人実行の前に — kintone バッチを 200 → 20,000 → 100,000 件で鍛える

0
Posted at

as-is / no support: 本ツールは MIT ライセンスで現状有姿のまま公開しており、サポート・動作保証・修正の約束はありません。本番投入は必ず --dry-run とステージング検証を経て、自己責任でお願いします。

仕様書の性能表は「目標値」のままだった。 無人運用(毎朝のスケジュール実行)に載せる前に、実測で裏を取ることにしました — レコードを 200 → 2,000 → 20,000 → 100,000 件と増やし、途中でバッチを強制切断してリランで収束するかまで。結果: 10 分以内という時間目標は実測 3.3 分 — 目標時間の約 1/3 で完走。ただし API 消費は、目標値の方が間違っていました

なぜ「量」の検証か

#2 までで、AI がジョブを作り、検査が守り、人間が承認する流れは実機で一巡しました。ただしそこまでの実行はすべて数件〜数十件です。少数件ですら、ルックアップの参照先トークン併送や ASSERT の比較バグなど罠が続出したのだから、桁を上げればまた何か出るはず — そして無人運用は「何か出たとき誰も見ていない」運用です。

公開仕様書の 11 章には当時、こんな表が載っていました。

データ規模 目標所要時間 目安 API 消費
読取 10 万件 → 書込 1 万件 10 分以内 約 305 回

「未保証の設計目標値。実測値ではありません」 という注釈つきです。目標を公表したまま実測しないのは気持ちが悪い。これを実測で裏付ける(または修正する)ことを検証のゴールにしました。

検証の設計 — 期待値を「式」で決める

kintone 標準の営業支援(SFA)パック構成(#1・#2 と同じ)に、テストデータを段階投入します。

段階 案件件数 会社数 狙い
S 200 20 基準線・境界値(101 件で 2 リクエスト分割)
M 2,000 200 読取カーソル(500 件/回)・書込チャンク(100 件/回)の複数回転
L 20,000 2,000 上限系・強制切断 → リラン
XL 100,000 10,000 仕様書に掲載していた「読取 10 万件 → 書込 1 万件」と同一条件での実測

設計の要は 2 つです。

① 期待値を式で決める。 会社 i の案件 j の売上を i×1000+j にすると、全体の総売上が式で出ます(10 万件で 500,050,550,000)。突合は「案件アプリを再集計した結果と、バッチが顧客アプリへ書いた集計値が全社一致し、総額が式と一致する」ことを ASSERT の連打で機械判定 — 10 万件でも合否は SQL 1 本です。

② 検証専用ジョブに分離する。 集計ジョブの ASSERT・集計・書込すべてをテストデータのプレフィックス(KSQL-FLOW-TEST-)に限定し、dry-run の合否基準に「プレフィックス外の書込 0 件」を追加。同居している実データには読取しか起きないことを毎段階で確認しながら進めます。テストデータの投入はエンジン CLI の CSV IMPORT(10 万件 = 5.99 MiB・38 分)、片付けは完全一致 DELETE で毎回ゼロに戻します。

ツール一式(データ生成・突合・片付け・計測)はテンプレートリポジトリの dev/scale/ に公開してあり、同じ検証を再現できます。

実測結果 — 10 万件まで急激な劣化なし

集計ジョブ(読取 → 会社別集計 → キー指定 UPSERT)の実測です。ウォームアップ 1 回を除く 3 回計測の中央値。

規模(読取 → 書込) 所要時間 API 消費 ピーク RSS
200 → 20 1.6 秒 9 回
2,000 → 200 5.2 秒 17 回 78 MB
20,000 → 2,000 31.4 秒 107 回 115 MB
100,000 → 10,000 3.3 分 511 回 260 MB

API 消費はほぼ件数に比例し、処理時間・メモリも少なくとも 10 万件までの測定範囲では急激な劣化は観測されませんでした(件数 5 倍の 20,000 → 100,000 で時間は約 6.4 倍)。10 万件を通して 429(レート制限)も 5xx も発生ゼロ。XL の 3 回計測はばらつき 1.8%(199.2〜202.9 秒)で、参考値として信頼できる安定度でした。

ハイライト①: 実測が「目標値の誤り」を見つけた

公表目標との対比です。

項目 目標 実測 判定
所要時間 10 分以内 3.3 分 達成(目標時間の約 1/3)
API 消費 約 305 回 511 回 目標側が誤り

API が目標を 1.68 倍超過 — ランナーが非効率なのか? 差分 206 回を全部数えたら、こうなりました。

区分 目標の想定 実測
読取(案件 10 万件・500 件/回) 200 200 ✓
UPSERT の事前 GET(1 万キー ÷ 50 キー/回) 計上なし 200
書込(顧客 1 万件・100 件/回) 100 100 ✓
その他(再読取・ログ・ロック) 5 11

つまり目標値が UPSERT の重複判定に使う事前 GET を数え忘れていたのです。実装の推定式(dry-run や EXPLAIN が出す値)は正しく含んでいて、誤っていたのは仕様書の性能表だけ。仕様書 11 章はこの実測 4 点の表に差し替え、算出漏れの経緯ごと注記しました。「実測は自分の目標値の粗も見つける」— 検証をやってよかった瞬間です。

ハイライト②: 動いているバッチを Ctrl+C で殺す

仕様のコア主張「失敗したら同じジョブをもう一度流せば収束する」を、一番乱暴な方法で確かめます。2 万件を UPDATE する書込ジョブ(会社名 IN (250 社) の UPDATE 文 8 個 ≒ 1 文 2,500 件・書込は合計 200 チャンク・約 8 分)を実行中に Ctrl+C で強制切断しました。先ほどの集計ジョブ(2 万件で 31.4 秒)とは別物で、読取でなく 2 万件すべてを書き換えるジョブです — 中断時に「書きかけ」が最も残りやすい条件を選んでいます。

UPDATE を文分割しているのは検証特有の事情です。書込系の WHERE は kintone へ押し下げ可能な述語に限定される(LIKE 不可 — これも安全設計)ため、テストデータだけを誤爆ゼロで狙うには会社名の完全一致 IN しかなく、IN の長さに収めるための分割です。どのくらいの長さで壊れるかも実測しました — 2,000 社を 1 つの IN に入れる(SQL 46KB)と kintone 手前の nginx が HTTP 414 で入口拒否します(今回の環境ではエンコード後 URL が約 8KB に近づくと拒否が始まり、250 社は通過・270 社で 431・300 社で 414。読取はカーソル = POST ボディなので無縁で、書込系の候補取得だけがこの制限を受けます)。

なお「1 文で 2 万件」は条件を短くすれば可能です。当月の日付範囲だけを条件にした単文 UPDATE(実データ保護は事前 ASSERT で担保)で 20,000 件を完走させたところ、秒/チャンクは分割版とほぼ同値(2.53 vs 2.51)でした。書込はいずれも 100 件単位のリクエストに刻まれるため、少なくとも今回の測定では、SQL 文の分割自体は所要時間の支配項になりません — 時間を決めるのは文の数ではなく書込チャンクの総数です。

2026-08-23_14h05_16.png

切断直後の状態がこれです。

  • 実行ログアプリ: ステータス RUNNING のまま・チェックポイント last_written_key = chunk:72
  • ローカル JSONL: 実際の書込は chunk:81 まで記録

2026-08-23_14h06_13.png

チェックポイントは 25 チャンク / 60 秒間隔のスロットルつきなので、ログアプリの値は実際より少し古い。「last_written_key は再開位置ではなく診断情報」という仕様どおりの挙動が、そのまま観測できました。ちなみにこのスクリーンショットのエンジン版数が「flow 0.1.0」なのは撮影時点のバージョン表示バグで、後述の発見表に登場します — バグの証拠写真でもあります。

ここで即座に同じコマンドを再実行すると — Exit 5(多重起動)で拒否されます。死んだプロセスの分散ロック(ログアプリの RUNNING レコード)がまだ生きているからです。unlock コマンドで解除対象の一覧を確認して解放し、同一 as-of で再実行:

=> SUCCESS (exit 0) 読取 20000 件 / 書込 20000 件 / API 259 回

突合の結果、20,000 件すべてが正しく更新され、重複も欠落もゼロrun-all --resume 経路も同様に試し、こちらは「元セッションの as-of を引き継ぎます」とコンソールに明示された上で、中断バッチの続きが同じ基準時刻で収束しました。

2026-08-23_14h36_16.png

もう一つ、API 上限による強制停止も再現しました。--max-api-calls 70 で実行すると API ちょうど 70 回で Exit 3 停止。このときログアプリへの終了記録すら書けない(上限を使い切っている)ため、終了情報はローカルの pending へ退避され、次の実行の冒頭で自動再送されて RUNNING が解消される — という復旧チェーンまで、設計どおりに動きました。

上限と fail-closed の実演

読取上限も意図的に踏みました。20,000 件の集計を上限未設定(既定 10,000)で流すと:

安全停止: 集計の正しい結果には完全な候補集合が必要です。complete input reason: GROUP_BY, AGGREGATE。
取得件数が上限(10000 件)を超えました。WHERE 句で絞り込むか、maxRecords を引き上げてください。

書き込みゼロで、原因と対処を言って止まる。設定(limits.maxReadRows / maxTempRows)を引き上げれば完走します。ちなみにこの設定、検証の企画時点ではランナーから渡す手段がないと思っていたら、エンジン側に実装済みなのに文書化されていなかったことが判明して即日解決した、という小話つきです(v0.2.0 で設定可能化)。

検証が見つけたもの — 「実測 → 起票 → 修正」

この検証シリーズ全体(#2 の AI 実走を含む)で見つかった問題は、すべて実測起点で起票し、多くが同日修正されています。なお表中の「44.6 分」は集計ベンチマーク(10 万件読取 → 1 万件書込 = 3.3 分)とは別の試験 — 10 万件そのものを書き換える書込ジョブの実測です。読む量ではなく書く量が時間を支配する、という性能モデルがここから見えます。

発見 顛末
ASSERT の大小比較が辞書順評価(3 <= 10 が偽になる fail-open) エンジンへ再現マトリクス付きで起票 → 同日修正リリース(過去の公開例にも影響する本質バグでした)
読取上限オプションの文書化漏れ 起票 → 実装済みと判明・文書化とテストが追加
書込が時間の支配項(10 万件の UPDATE = 複数文に分割・合計 1,000 チャンクで 44.6 分・2.67 秒/チャンク) kintone bulkRequest 対応(書込リクエスト最大 1/20)を実測付きで起票
ランナーのバージョン表示が旧値のまま ハードコードが原因。即修正
書込系の WHERE に長大な IN を渡すと kintone 入口の nginx が拒否(HTTP 414/431) 今回の環境で拒否が始まる境界を実測(エンコード後 URL 約 8KB・会社名 20 字なら IN 250 要素は通過・270 で拒否)。読取(カーソル = POST)は無縁で書込系の候補取得だけが対象。分割設計の妥当性を裏付け
検証環境に前世代のテストデータが残存 → 突合が誤検出 突合の ASSERT が環境異常を正しく止めた。恒久対応として「seed 前の残存ゼロチェック」を手順に追加
CSV IMPORT のクライアントタイムアウト 失敗 exit でもサーバ側は書込完了していることがあるという罠。失敗時は件数実測 → 片付け → 再投入の手順を規約化

成功ログよりも、この失敗と修正の記録の方が検証の価値だと思っています。

無人運用ゲート — 最後の 3 点

スケジュール実行(次回)へ進む前提として、最後にこれだけ確認しました。

  1. 二重起動: 実行中に同じジョブをもう一度起動 → Exit 5。防御は二層で、プロセス生存中はローカルロック、プロセス消滅後はログアプリの重複禁止フィールド — つまり別マシンからの多重起動も防げる
  2. 通知: 失敗時 Webhook と成功 heartbeat の実受信を確認
  3. 実行環境: 対話シェル以外からの同一コマンド実行を確認

まとめ

  • 200 → 100,000 件まで、API 消費はほぼ件数比例。処理時間・メモリも10 万件までの測定範囲では急激な劣化なし。公表目標「10 分以内」は 3.3 分で達成、429 は発生ゼロ
  • API の目標値の算出漏れを実測が発見 → 仕様書を実測 4 点の表に改訂
  • 強制切断 → unlock → 同一 as-of リランで完全収束。チェックポイント・pending 再送・二重ロックの復旧チェーンも設計どおり
  • 検証ツールはテンプレートに公開。期待値を式で決めれば、10 万件の合否も SQL 1 本で判定できる

数値はすべて特定環境(Windows 11 / Node.js v24 / 家庭用回線 / kintone 開発環境)での参考実測であり、性能の保証値ではありません。

次回

ここまで鍛えたジョブを、Windows タスクスケジューラで毎朝動かします。実機検証で踏んだ「PowerShell が 0.1 秒で無言死する罠」を含む運用編です。

連載

  1. #1: kintone のバッチ処理を SQL 1 本で書けるランナーの紹介
  2. #2: AI エージェントに kintone のバッチジョブを書かせる — MCP + Claude Code
  3. #3(本記事): 毎朝の無人実行の前に — kintone バッチを 200 → 20,000 → 100,000 件で鍛える
  4. #4: タスクスケジューラで毎朝動かす — 実機で踏んだ罠 2 連発つき(予定)
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?