語句の整理

同じような言葉が出てきて紛らわしいので、語句の整理をはじめにしておきます。

単に、Batchと書いた場合は、単語の意味(ひとまとめ、一群、一束)として扱います。

Azure Batchと書いたら、AzureのサービスのBatchとして扱います。

日本語でバッチと書いた場合は、かなり感覚に頼った言い方になりますが、いわゆるバッチでお願いします。

余談ですけど、バッチ処理の反対としてリアルタイム処理という話をよく聞くような気もしますが、なんとなくバッチ処理の反対はストリーミング処理なんじゃないかなあと思ってます。

どうなんでしょうね。まあ、相手に通じればそれでいい気もしますけど。

バッチ処理

さて、バッチ処理といっても、内容は千差万別だと思います。例えば売上げの計上処理かもしれませんし、不要なデータを削除するものかもしれません。

また、実行の頻度や実行の並列性も考慮する必要があります。例えば、このバッチは1日に1回夜間に行うとか、このバッチは常に1つしか実行させてはいけないとか。

今回、どうやって動かそうか悩んでいたのは、クロールバッチです。大量のページを高頻度で取得したいため、並列に大量の実行したいバッチ処理でした。

何を使おうか悩ましい

Azureでバッチ処理ができるものを考えると次の4つが浮かぶと思います。

  • WebJob
  • VM
  • CloudService
  • Azure Batch

WebJob使いやすくて好きなのですが、今回の場合、大量に同時実行したいためWebJob向きではないはずです。PaaSが良かったのでVMもなし。

CloudServiceを使うか悩みどころでしたが、Azure Batchのドキュメントに

Batch は、*バッチ処理*または*バッチ コンピューティング*のための管理されたサービスで、期待した結果が得られるように類似のタスクを大量に実行します。

引用:https://azure.microsoft.com/ja-jp/documentation/articles/batch-technical-overview/

と書いてあったので、使ってみることにしました。

Azure Batch

Azure Batch は、多くのコンピューティング処理を要する作業を仮想マシン (コンピューティング ノード) の管理されたコレクション上で実行するようにスケジュール設定するためのプラットフォーム サービスです。

引用:https://azure.microsoft.com/ja-jp/documentation/articles/batch-technical-overview/

BlenderのレンダリングをデモしていたAzure Batch Appsというサービスもありましたが、現在は作れないようです。

image

Azure Batch Appsは、SDKを使い、JobSplitterやTaskProcessorを実装したクラスライブラリをAzure Batch Apps ポータルからアップロードして使用しましたが、

新しいAzure Batch service APIを利用する場合は、REST APIでJobやタスクを作って実行させます。各タスクで実行させたいバイナリがあるときは、Storageにアップロードしておいて、タスクのResourceFileに指定しておきます。すると、タスクが実行時にStorageからダウンロードしてくれます。

引用: https://azure.microsoft.com/ja-jp/documentation/articles/batch-technical-overview/

コードで書いてみるとこんな感じ。(Storageへのアップロード部分は省略)

var credentials = new BatchSharedKeyCredentials("[account-url]", "[account-name]", "[account-key]");
var client = BatchClient.Open(credentials);

// Poolの作成
var pool = client.PoolOperations.CreatePool(
    poolId: "sample_pool",
    osFamily: "4",
    virtualMachineSize: "small",
    targetDedicated: 2
);
pool.MaxTasksPerComputeNode = 4;
pool.Commit();

// Jobの作成
var job = client.JobOperations.CreateJob("sample_job", new PoolInformation
{
    PoolId = "sample_pool"
});
job.Commit();

// タスクの作成
job = client.JobOperations.GetJob("sample_job");
var program = new ResourceFile("https://[account-name].blob.core.windows.net/container/program.exe", "program.exe");
for(var i = 0; i < TASK_SIZE; i++)
{
    var task = new CloudTask("sample_task" + i, "program.exe");
    task.ResourceFiles = new List { program };
    job.AddTask(task);
}
job.Commit();
job.Refresh();

// 全タスクが終了するまで待つ
client.Utilities
    .CreateTaskStateMonitor()
    .WaitAll(tasksToMonitor: job.ListTasks(),
               desiredState: TaskState.Completed,
                    timeout: TimeSpan.FromMinutes(30));
            
// 各タスクの標準出力をダンプ
foreach (CloudTask task in job.ListTasks())
{
    Console.WriteLine(task.Id + ": " + task.GetNodeFile(Constants.StandardOutFileName).ReadAsString());
}

// Taskの削除
foreach (CloudTask task in job.ListTasks())
{
    task.Delete();
}
// Jobの削除
client.JobOperations.DeleteJob(job.Id);

すごくMaster-Worker。それもそうかという気がしますが、これだとこのマスタプログラムをどこで動かせばいいのかという問題が出てきます。

そこで、ManagerTaskです。Jobには次の3つの特殊なTaskを指定できます。ManagerTaskはその中の1つです。

  • Preparation Task
  • Manager Task
  • Release Task

Preparation Taskは各ノードがタスクを実行する前に実行するタスクで、Release TaskはJobが完了する際にタスクを実行していた各ノードで実行されます。

Manager Taskはタスクの中で一番最初に実行されるタスクで、マスタプログラムを実行するのに使用できます。

仮にN個タスクのタスクを3つのノードで実行すると次の図のようになります。最初にManagerTaskがNode#1に割り振られますが、

Node#1はManager Taskを実行する前にPreparation Taskを実行します、Pareparation Taskが完了後Manager Taskが実行され、Task#1 ~ Task#NがJobに追加されます。

するとプールの各ノードにTaskを振り分けますが、このとき、はじめてタスクを実行するノードでもPreparation Taskが実行されます。すべてのタスクが完了し、Jobが完了するさい、タスクを実行したすべてのノードで、Release Taskが実行されます。

image

また、このPreparation Taskと、Manager Task、Release Taskを含んだJobをスケジュールに従って生成させるためのJobScheduleという機能もあります。

これを使用するとJobの定期実行が可能ですが、Jobを新規作成し続けるので、”JobListの中が完了したJobでいっぱい”みたいなことになってしまいますので、完了したJobを削除するJobとJobScheduleも必要になるのではと思います。

image

Manager Taskで自身を保有しているJobにタスクを追加するには、そのJobIdを取得しなくてはなりません。これは環境変数から取得できます。

ENVIRONMENT VARIABLE NAME

DESCRIPTION

AZ_BATCH_ACCOUNT_NAME タスクが所属するアカウントの名前。
AZ_BATCH_JOB_ID タスクが所属するジョブの ID。
AZ_BATCH_JOB_PREP_DIR ノード上のジョブ準備タスク ディレクトリの完全パス。
AZ_BATCH_JOB_PREP_WORKING_DIR ノード上のジョブ準備タスク作業ディレクトリの完全パス。
AZ_BATCH_NODE_ID タスクが実行されているノードの ID。
AZ_BATCH_NODE_ROOT_DIR ノード上のルート ディレクトリの完全パス。
AZ_BATCH_NODE_SHARED_DIR ノード上の共有ディレクトリの完全パス。
AZ_BATCH_NODE_STARTUP_DIR ノード上の計算ノードのスタートアップ タスク ディレクトリの完全パス。
AZ_BATCH_POOL_ID タスクが実行されているプールの ID。
AZ_BATCH_TASK_DIR ノード上のタスク ディレクトリの完全パス。
AZ_BATCH_TASK_ID 現在のタスクの ID。
AZ_BATCH_TASK_WORKING_DIR ノード上のタスク作業ディレクトリの完全パス。

引用:https://azure.microsoft.com/ja-jp/documentation/articles/batch-api-basics/#environment

また、ノードで同時に実行できるタスクはCPUコア数の4倍までになっています。そのため、今回の想定していたクロールのような、CPU処理よりも通信処理が多めの場合、リソースを使い切ってもいないのに同時実行数を増やすために大きなプールを用意しなくてはならなくなる可能性があります。

この点を考慮すると、CPUリソースを多く利用するバッチ以外はAzure Batchに向いていない気がする。

最終的に

リソースを使い切ってもいないのに同時実行数を増やすために大きなプールを用意しなくてはならない問題はとりあえず気にしないことにして、JobScheduleによってJobが新規作成され続けるためにJobを定期的に削除する必要がありそうだという問題をどうにかしたかった。

結果的には、JobScheduleを使用せず、WebJobでマスタプログラムを実行するというところに落ち着いた。WebJobでは賄いきれないコンピューティングリソースをAzure Batchで補うと形。

なので、Manager Taskも使わない。予想以上にStorageにプログラムと依存ライブラリをアップロードする、もしくはアップロードするプログラムを書くのが面倒だったので、できればワーカープログラムだけにしたかった。(CI、CDをちゃんとやればそうでもないのだろうけど。)

WebJobのページで稼働ログやステータス確認できるので、このWebJobとAzure Batchの組み合わせは割と気に入ってます。ただし、WebJob用のAppServicesの料金がかかるのがネック。

image

まとめると、Azure Batchは計算機用途のほうが主用途で、バッチの処理内容によっては、CloudServicesのほうがやりやすいかも。

LinuxVMをノードに使えるようになるらしいので、使えるようになったらさらに活用の幅が広がりそうです。

今更思い出したけどServiceFabricがあったんで今度はServiceFabricで検討してみます。

本当は、今回のような用途だと、MesosとかChronosのWindows Server Container対応が完了してくれるとほしい機能に近いので、来年中にはくるといいなあ。

CATEGORIES