この記事について
この記事では、LAN内の複数PCを使って、ファイル処理を自動分散する仕組みを作った話をまとめます。
最初に考えていたのは、複数PCを完全に1台のPCとして扱うような大きい仕組みではありません。 もっと素朴に、
inputフォルダにファイルを入れる
↓
サーバーがジョブ化する
↓
空いているワーカーPCが処理する
↓
outputフォルダに結果が出る
という形です。
ユーザーはAPIやSSHを意識しません。 フォルダに置いたら、LAN内のどこかのPCが勝手に処理してくれる。 そのくらいOSに溶け込んだ感じを目指しました。
最初は画像を白黒化するだけのMVPでした。 そこから少しずつ拡張して、今は次のような処理までできるようになっています。
- 画像のグレースケール化
- 画像リサイズ
- 画像サムネイル作成
- PDFのページ画像化
- 動画サムネイル作成
- 動画変換
- 画像OCR
- PDF OCR
- 複数ワーカーへの自動割り当て
- Windowsサービス化
- ダッシュボード表示
小さい実験から始めたのに、気づいたらちょっとしたLAN内バッチ処理クラスタになっていました。
作りたかったもの
目的はこれです。
メインPC
ジョブ管理サーバー
input/outputフォルダを持つ
ワーカーPC
常駐エージェント
定期的に仕事を取りに行く
処理結果を返す
ユーザーの操作はこれだけです。
server/input にファイルを入れる
処理結果はここに出ます。
server/output
最初に作ったMVPは画像処理でした。
input/sample.png
↓
workerが処理
↓
output/1_sample_processed.png
処理内容は、画像をグレースケール化して、横幅が大きければ800pxに縮小するだけです。 でも、これだけでも分散処理システムとして必要な部品は一通り出てきます。
- フォルダ監視
- ジョブ登録
- ジョブ状態管理
- ワーカーのポーリング
- ファイルダウンロード
- 結果アップロード
- 失敗通知
- 二重取得防止
このあたりをちゃんと作ると、あとから画像以外にも広げられます。
全体構成
構成はシンプルです。
distributed-os-layer/
├─ server/
│ ├─ index.js
│ ├─ db.js
│ ├─ config.json
│ ├─ input/
│ ├─ output/
│ ├─ uploads/
│ └─ public/dashboard.html
│
└─ worker/
├─ worker.py
├─ config.json
└─ distributed-worker.exe
技術は次の通りです。
| 役割 | 技術 |
|---|---|
| メインサーバー | Node.js + Express |
| DB | SQLite |
| フォルダ監視 | chokidar |
| ワーカー | Python |
| 画像処理 | Pillow |
| PDF処理 | PyMuPDF |
| 動画処理 | FFmpeg |
| OCR | Tesseract OCR |
| サービス化 | NSSM |
| 通信 | HTTP API |
サーバーは input を監視します。
ファイルが追加されると、SQLiteの jobs テーブルにジョブを作ります。
ワーカーは常駐していて、定期的にサーバーへ問い合わせます。
仕事ありますか?
サーバーは、そのワーカーが処理できるジョブだけを渡します。
ジョブ状態
ジョブは次の状態を持ちます。
pending
running
done
failed
意味はこうです。
| 状態 | 意味 |
|---|---|
| pending | まだ誰も処理していない |
| running | どこかのワーカーが処理中 |
| done | 完了 |
| failed | 失敗 |
ワーカーがジョブを取得するときは、SQLiteのトランザクション内で pending から running に変えます。
これで、複数ワーカーが同時に問い合わせても、同じジョブを二重取得しないようにしています。
重要なのはここです。
SELECT pending job
↓
UPDATE status = running
これを1つのトランザクションで行います。
SQLiteなので巨大クラスタ向けではありません。 でも、LAN内で数台のPCを使うMVPとしてはかなり扱いやすいです。
最初のAPI設計
APIは最初かなり素朴でした。
GET /api/jobs/next
GET /api/jobs/:id/file
POST /api/jobs/:id/result
POST /api/jobs/:id/fail
GET /api/jobs
役割はこうです。
| API | 役割 |
|---|---|
/api/jobs/next |
次のジョブを1つ取得 |
/api/jobs/:id/file |
入力ファイルをダウンロード |
/api/jobs/:id/result |
結果ファイルをアップロード |
/api/jobs/:id/fail |
失敗内容を送信 |
/api/jobs |
ジョブ一覧 |
あとから機能が増えて、今は次もあります。
GET /api/stats
GET /api/workers
POST /api/workers/heartbeat
POST /api/jobs/:id/retry
heartbeat はワーカーが生きていることを知らせるためのAPIです。
これがあると、処理中にワーカーPCが落ちた場合でも、一定時間後にジョブを pending に戻せます。
inputサブフォルダで処理を切り替える
途中から、画像だけでなく動画やPDFも扱いたくなりました。
そこで job_type を追加しました。
例えばこうです。
image_grayscale
image_resize
image_thumbnail
image_ocr
pdf_to_images
pdf_ocr
video_thumbnail
video_transcode
ファイルの置き場所で処理を切り替えます。
server/input/sample.png
-> image_grayscale
server/input/resize/sample.png
-> image_resize
server/input/thumbnail/sample.png
-> image_thumbnail
server/input/ocr/sample.png
-> image_ocr
server/input/document.pdf
-> pdf_to_images
server/input/pdf-ocr/document.pdf
-> pdf_ocr
server/input/video-thumbnail/movie.mp4
-> video_thumbnail
server/input/video-transcode/movie.mp4
-> video_transcode
この方式はかなり使いやすいです。
ユーザーから見ると、処理したいフォルダにファイルを入れるだけです。 Web画面からジョブを作る必要もありません。 CLIでコマンドを打つ必要もありません。
フォルダ = 処理モード
というルールにしたことで、かなりOSに近い感覚になりました。
ワーカー能力の自動判定
動画処理にはFFmpegが必要です。 OCRにはTesseractが必要です。 PDF処理にはPyMuPDFが必要です。
でも、すべてのワーカーPCに全部入っているとは限りません。
そこで、ワーカーは起動時に自分の能力を自動判定します。
例えば、FFmpegが見つかればこうなります。
video_thumbnail
video_transcode
Tesseractが見つかればこうなります。
image_ocr
pdf_ocr
実際のログはこんな感じです。
capabilities=image_grayscale,image_resize,image_thumbnail,image_ocr,pdf_ocr,pdf_to_images,video_thumbnail,video_transcode
サーバーはこの capabilities を見て、処理できるジョブだけを渡します。
つまり、FFmpegが入っていないワーカーに動画ジョブは渡りません。 Tesseractが入っていないワーカーにOCRジョブも渡りません。
この仕組みを入れたことで、ワーカーPCごとの役割分担が自然にできます。
古いPC
画像リサイズだけ
そこそこ速いPC
PDFとOCR
強いPC
動画変換
みたいな構成もできます。
Windowsサービス化で常駐させる
手動でワーカーを起動するだけなら簡単です。
.\distributed-worker.exe
でも、これだとPowerShellを開きっぱなしにする必要があります。 PCを再起動したら止まります。
そこでNSSMを使ってWindowsサービス化しました。
DistributedOsLayerServer
DistributedOsLayerWorker
サービス化すると、PC起動時に自動で立ち上がります。
メインPC起動
↓
サーバー自動起動
ワーカーPC起動
↓
ワーカー自動起動
これで、普段はサービスの存在を意識しなくてよくなります。
NSSMで詰まったところ
今回かなりハマったのがNSSMのパス設定です。
特に重要なのはこの3つです。
nssm get DistributedOsLayerWorker Application
nssm get DistributedOsLayerWorker AppDirectory
nssm get DistributedOsLayerWorker AppEnvironmentExtra
期待する形はこうです。
Application = D:\DistributedWorker\distributed-worker.exe
AppDirectory = D:\DistributedWorker
CONFIG_PATH = D:\DistributedWorker\config.json
最初、K: のような割り当てドライブを使っていました。
でもWindowsサービスでは、ユーザーがログインしている時のドライブ割り当てが見えないことがあります。
そのため、サービスではできるだけ実ドライブのパスを使うことにしました。
K:\distributedworker
ではなく、
D:\DistributedWorker
のように揃えます。
さらに、FFmpegやTesseractをインストールしても、PowerShellから見えるだけでは足りません。
サービス環境の PATH にも入れる必要があります。
$ffmpegBin = Split-Path (Get-Command ffmpeg).Source
$tesseractBin = Split-Path (Get-Command tesseract).Source
nssm set DistributedOsLayerWorker AppEnvironmentExtra `
"CONFIG_PATH=D:\DistributedWorker\config.json" `
"PATH=C:\Windows\System32;C:\Windows;C:\Windows\System32\WindowsPowerShell\v1.0;$ffmpegBin;$tesseractBin"
この設定ができると、ワーカーの能力に動画とOCRが出ます。
video_thumbnail
video_transcode
image_ocr
pdf_ocr
ここは地味ですが、実運用ではかなり大事でした。
日本語OCR対応
OCRはTesseractを使っています。
英語だけなら eng で動きます。
日本語を読むには、日本語の言語データが必要です。
確認はこれです。
tesseract --list-langs
日本語が入っていれば、こう出ます。
jpn
jpn_vert
入っていない場合は、Tesseract公式の tessdata から jpn.traineddata を追加します。
$dest = "C:\Program Files\Tesseract-OCR\tessdata\jpn.traineddata"
$url = "https://github.com/tesseract-ocr/tessdata/raw/main/jpn.traineddata"
Invoke-WebRequest -Uri $url -OutFile $dest
縦書き用も入れるならこれです。
$dest = "C:\Program Files\Tesseract-OCR\tessdata\jpn_vert.traineddata"
$url = "https://github.com/tesseract-ocr/tessdata/raw/main/jpn_vert.traineddata"
Invoke-WebRequest -Uri $url -OutFile $dest
ワーカー設定はこうしました。
"ocr_lang": "jpn+eng"
これで日本語と英語が混ざった画像も読めます。
ダッシュボード
簡易ダッシュボードも作りました。
見えるものは次の通りです。
- pending / running / done / failed の件数
- worker一覧
- worker capabilities
- 最新ジョブ一覧
- failedジョブのRetryボタン
- エラー内容
ブラウザで開きます。
http://メインPCのIP:3000/
API tokenを入力すると、ジョブ状態が見えます。
最初はAPIだけでも十分だと思っていました。 でも、実際に使ってみると、ダッシュボードがあるだけでかなり安心感が違いました。
ジョブが作られているか
ワーカーが生きているか
どの能力を持っているか
失敗しているか
pendingのまま詰まっているか
このあたりがすぐ見えます。
特に capabilities は重要でした。
動画やOCRが動かない時、ワーカー能力に出ていないことがすぐ分かります。
いま実際にできること
今の状態でできることをまとめると、こうです。
画像
input\sample.png
-> 白黒画像
input\resize\sample.png
-> リサイズ画像
input\thumbnail\sample.png
-> サムネイルJPEG
input\ocr\sample.png
-> OCR結果txt
input\document.pdf
-> ページ画像zip
input\pdf-ocr\document.pdf
-> OCR結果txt
動画
input\video-thumbnail\movie.mp4
-> サムネイルJPEG
input\video-transcode\movie.mp4
-> 変換済みmp4
運用
複数ワーカー
自動ジョブ選別
ワーカー能力別割り当て
Windowsサービス常駐
ログローテーション
失敗ジョブ再実行
ここまで来ると、単なる画像処理スクリプトではなくなりました。
LAN内で動く小さい分散処理基盤です。
この仕組みが得意なこと
このシステムが得意なのは、バッチ処理です。
例えばこういうものです。
- 画像を大量にリサイズする
- 動画からサムネイルをまとめて作る
- PDFをページ画像に変換する
- 日本語OCRを大量に回す
- 条件の違うシミュレーションを並列実行する
- Blenderのレンダリングを複数PCに分ける
1つの巨大な処理を複数PCでリアルタイムに分けるより、独立したジョブをたくさん処理する方が向いています。
つまり、
1つの処理を10台で速くする
より、
100個の処理を10台にばらまく
方が得意です。
OCRや動画サムネイルのような処理は、かなり相性が良いです。
後編へ
ここまでで、画像、PDF、OCR、動画処理までをフォルダ投入型の分散ジョブにできました。
このあと、さらに遊び心のある拡張として、物理シミュレーションとBlenderレンダリングも追加しました。
その話は後編に分けます。
後編: 物理シミュレーションとBlenderレンダリングも分散ジョブにした話
詰まったところ
作っていて詰まったところは、処理ロジックそのものより運用まわりでした。
PowerShellの実行ポリシー
.ps1 を実行しようとすると止まりました。
このシステムではスクリプトの実行が無効になっているため
一時的に回避するならこうです。
powershell -ExecutionPolicy Bypass -File .\build-worker-exe.ps1
config.jsonのJSONミス
小さいミスですが、これでワーカーが即落ちしました。
間違い:
"server_url": ""http://192.168.1.7:3000",
正しい形:
"server_url": "http://192.168.1.7:3000",
ログにはこう出ました。
json.decoder.JSONDecodeError
サービスとPATH
PowerShellでは ffmpeg や tesseract が見えるのに、サービスでは見えないことがありました。
原因は、Windowsサービスが別の環境変数で動くからです。
そのため、NSSMの AppEnvironmentExtra に PATH を明示しました。
Kドライブ問題
ワーカーPCで K: として見えている場所が、サービスからは見えないことがありました。
サービスでは割り当てドライブより、実ドライブパスを使う方が安定します。
D:\DistributedWorker
のようにしました。
最低限の確認コマンド
サーバー確認:
Get-Service DistributedOsLayerServer
ワーカー確認:
Get-Service DistributedOsLayerWorker
NSSM設定確認:
nssm get DistributedOsLayerWorker Application
nssm get DistributedOsLayerWorker AppDirectory
nssm get DistributedOsLayerWorker AppEnvironmentExtra
ワーカーログ:
Get-Content D:\DistributedWorker\logs\worker.log -Tail 40
サーバーダッシュボード:
http://メインPCのIP:3000/
FFmpeg確認:
ffmpeg -version
Tesseract確認:
tesseract --version
tesseract --list-langs
公開前チェックリスト
この仕組みをLAN内で使う前に見るところです。
メインPC
- Node.jsが入っている
server/config.jsonがあるapiTokenを設定しているDistributedOsLayerServerがRunning- Windows Firewallで3000番を許可している
inputとoutputの場所を確認している
ワーカーPC
distributed-worker.exeがあるconfig.jsonがあるserver_urlがメインPCのIPになっているapi_tokenが一致しているDistributedOsLayerWorkerがRunning- NSSMの
CONFIG_PATHが正しい - FFmpeg/Tesseractを使う場合はサービスのPATHにも入っている
ダッシュボード
- workerが表示されている
last_seen_atが更新されているcapabilitiesが期待通り- ジョブが
pendingのまま止まっていない
今後やりたいこと
今後やるなら、このあたりが面白そうです。
- OCR前処理の強化
- 失敗理由ごとの再実行ルール
- ジョブ優先度
- outputファイルのダウンロードリンク
- ワーカーごとの処理時間表示
- 処理完了通知
- GPU OCRワーカー
特にOCRは、前処理でかなり精度が変わりそうです。
- 二値化
- 拡大
- 傾き補正
- ノイズ除去
- コントラスト調整
このあたりを image_ocr_preprocess のようなジョブにしてもよさそうです。
まとめ
最初は、画像を白黒にするだけの分散処理MVPでした。
でも、作っていくうちに、必要な部品がだんだん見えてきました。
フォルダ監視
ジョブ管理
ワーカー常駐
二重取得防止
heartbeat
サービス化
能力判定
ダッシュボード
このあたりが揃うと、処理内容はあとから増やせます。
結果として、今はこういう仕組みになりました。
ファイルをinputに入れる
↓
サーバーが種類を判定する
↓
処理できるワーカーに渡す
↓
ワーカーが処理する
↓
outputに結果が出る
大げさなクラスタではありません。 でも、LAN内の余っているPCを自然に使う仕組みとしては、かなり気持ちよく動きます。
こういう小さい分散処理は、身近なPCを少しだけ賢く使える感じがあって楽しいです。