今回はStandaloneモードとkubernetesのクラスタモードとを比較してみました。Standaloneモードはkubernetesのようなクラスタを管理するシステムを使わず、sparkに内包されているクラスタマネージャを使用してクラスタを構築します。今回は、dockerを使って1台のMACに1つのマスターとワーカー を立ててみました。マスター、ワーカー を実行するためのイメージを構築するDockerfileはこちらを参考に以下のように作成しました。TensorFlowOnSparkもインストールしています。今回は特にminioやHDFSを使用しませんので、hadoopやaws関連のインストールは行っていません。SPARK_NO_DAEMONIZEをyesに設定しているのは、マスター、ワーカー のプロセスをフォアグラウンドで実行し、dockerコンテナがすぐに終了しないようするためです。
FROM amazoncorretto:8u212
#ENV SPARK_VERSION=3.0.0-preview2
ENV SPARK_VERSION=2.4.5
ENV HADOOP_VERSION=2.7
RUN yum install -y tar gzip \
&& curl -O http://apache.mirror.iphh.net/spark/spark-${SPARK_VERSION}/spark-${SPARK_VERSION}-bin-hadoop${HADOOP_VERSION}.tgz \
&& tar xzf spark-${SPARK_VERSION}-bin-hadoop${HADOOP_VERSION}.tgz \
&& mv spark-${SPARK_VERSION}-bin-hadoop${HADOOP_VERSION} /spark \
&& rm spark-${SPARK_VERSION}-bin-hadoop${HADOOP_VERSION}.tgz
RUN yum -y update && \
yum install -y python3 python3-pip && \
pip3 install --upgrade pip setuptools
RUN pip3 install numpy
RUN pip3 install tensorflow
RUN pip3 install tensorflowonspark
RUN pip3 install tensorflow_datasets
ENV PYSPARK_PYTHON=/usr/bin/python3
ENV SPARK_NO_DAEMONIZE=yes
ADD ./TensorFlowOnSpark/examples /examples
EXPOSE 8080
マスター、ワーカー は下記のようなdocker-compose.ymlを作成してビルド、起動しました。先ほどのページを参考にさせていただいています。学習データやモデルファイルをおく場所として、sharedと言うボリュームを作成し、/mydataにマウントしています。
version: '2'
services:
master:
build: ./
networks:
- back
ports:
- 4040:4040
- 8080:8080
command: /spark/sbin/start-master.sh
volumes:
- shared:/mydata
environment:
- SPARK_MASTER_HOST=0.0.0.0
worker:
build: ./
networks:
- back
depends_on:
- master
ports:
- 8081-8089:8081
command: /spark/sbin/start-slave.sh --host 0.0.0.0 spark://master:7077
volumes:
- shared:/mydata
networks:
back:
volumes:
shared:
そして、mnistのサンプルソースを使って下記のようにデータを生成、訓練を実行しました。epochsは学習するときのエポック数です。指定しないと3になります。
# mnistのデータを準備
docker-compose exec master \
/spark/bin/spark-submit \
--master spark://localhost:7077 \
--conf spark.executor.instances=1 \
local:///examples/mnist/mnist_data_setup.py \
--num_partitions=1 \
--output /mydata/mnist/data
# 学習を実行
docker-compose exec master \
/spark/bin/spark-submit \
--master spark://localhost:7077 \
--conf spark.executor.instances=1 \
local:///examples/mnist/keras/mnist_spark.py \
--images_labels /mydata/mnist/data/csv/train \
--epochs 10 \
--export_dir /mydata/mnist/export
所要時間を比較すると、以下のようになりました。
| cluster | executor数 | epochs=3 | epochs=10 |
| Standalone | 1 | 6分15秒 | 19分21秒 |
| Kubernetes | 1 | 7分55秒 | 24分11秒 |
| Kubernetes | 5 | 5分53秒 | 16分13秒 |
| Kubernetes | 7 | 7分31秒 | 21分39秒 |
あまり細かい分析はしていませんが、単純にexecutorの数を増やしてもそれほど効果はなさそうです。また、増やしすぎると逆効果になりますね。Kubernetesのクラスタを構成しているノードは同じPCに複数存在しているVM(KVM、VirtualBoxを使用)なので、CPU数が足りなくなっている可能性もあります。また、Kubernetesのexecutor=1の場合、Standaloneよりもかなり遅いですが、これはKubernetesの方は適当に作ったプライベートなクラスタで、ノードのスペックもバラバラのため、たまたまスペックの低いノードに当たってしまった可能性もあります。また、Kubernetesのクラスタの方は、HDFSを使用していたり、executor、driverのインスタンス間のネットワークが物理的に離れている分、遅くなっているのかもしれません。
補足ですが、Kubernetesでexecutor数を3にして実行時間を測定したかったのですが、何回やっても以下のようなtensorflowのエラーが出てしまうので、測定できませんでした。executor数を7に変更すると全くエラーが出なくなりましたので、executorが1つのタスクの実行に時間がかかると発生するバグではないか、と思ってます。
UnavailableError: Socket closed Additional GRPC error information: {"created":"@1579711911.023658059","description":"Error received from peer","file":"external/grpc/src/core/lib/surface/call.cc","file_line":1039,"grpc_message":"Socket closed","grpc_status":14}