本文的構建示例已在以下 Github 倉庫中公開。
- GitHub - mjun0812/MLflow-Docker
官方文檔如下。
- MLflow Documentation
- RustFS Documentation
引入背景
在機器學習項目中,我們需要在更改超參數、模型和數據集的同時進行各種實驗。此時,通過引入可以高效比較結果的實驗管理工具,我們可以專注於模型的開發。這類實驗管理工具有 Tensorboard 和 Weight and Bias (wandb) 等多種,但如果着眼於無需將數據發送到外部即可使用的“本地部署(On-Premise)”這一點,選擇並不多。因此,我打算嘗試構建可以在本地構建的代表性平台 MLflow。
什麼是 MLflow
MLflow 是一個開源的 MLOps 平台,支持以下 5 種場景:
- Tracking & Experiment Management: 管理實驗結果並進行比較
- Model Registry: 進行機器學習模型的版本管理
- Model Deployment: 進行機器學習模型的服務化
- ML Library Integration: 與機器學習庫集成
- Model Evaluation: 機器學習模型的性能評估
為了在這些場景中使用 MLflow,需要一個作為 backend store 保存參數的數據庫,以及一個作為 artifact store 保存模型權重和日誌文件等的對象文件存儲。因此,這次我們將使用 Docker Compose 採用以下配置,完全在本地構建 MLflow。
- backend store: MySQL
- artifact store: RustFS
由於我主要使用進行實驗管理的 Tracking Server,因此文章將以該部分為中心進行撰寫,但其他場景應該也可以基於本次的構建示例進行使用。
架構圖
這次我們將使用 Docker Compose,按以下配置構建 MLflow Server。
MLflow 的 Tracking Server 將實驗參數和結果保存到 MySQL 數據庫中,將 artifact 保存到 RustFS 中。此外,MLflow 的 WebUI 通過使用 Nginx Proxy 設置 Basic 認證,僅允許部分用户訪問。
快速開始 (Quick Start)
如果想快速構建,請克隆以下 GitHub 倉庫並按照 README.md 的步驟操作,或者執行以下命令。
git clone https://github.com/mjun0812/MLflow-Docker.git
cd MLflow-Docker
cp env.template .env
vim .env
編輯 .env 文件,指定監聽域名和 MLflow 的版本。
# 指定監聽域名。
# 如果僅為 localhost,則只能從本地訪問。
VIRTUAL_HOST=localhost
# 如果想指定 MLflow 的版本,請在此處指定
# 如果不指定,將使用最新版
MLFLOW_VERSION=
(可選) 如果要設置 Basic 認證,請在 nginx/htpasswd/localhost 文件中設置用户名和密碼。
htpasswd -c nginx/htpasswd/localhost [username]
接下來,執行以下命令進行鏡像構建和容器啓動。
docker compose up -d
這樣,就可以通過 localhost:15000 訪問 MLflow 的 WebUI 了。
如果從 Python 代碼中使用 MLflow,請按如下方式操作。
import os
import mlflow
# 如果設置了 Basic 認證
os.environ["MLFLOW_TRACKING_USERNAME"] = "username"
os.environ["MLFLOW_TRACKING_PASSWORD"] = "password"
# 通過環境變量設置的情況
os.environ["MLFLOW_TRACKING_URI"] = "http://localhost:15000"
mlflow.set_tracking_uri("http://localhost:15000")
mlflow.set_experiment("example")
with mlflow.start_run():
mlflow.log_param("param1", 1)
mlflow.log_metric("metric1", 1)
構建詳情
接下來,説明構建的詳細信息。首先展示文件的整體結構,然後查看各容器的設置。在本文的示例中,我們通過啓動以下容器進行構建。
- Nginx Proxy (jwilder/nginx-proxy)
- MLflow Server (自制 Dockerfile)
- MySQL
- RustFS
services:
nginx-proxy:
image:jwilder/nginx-proxy:latest
restart:unless-stopped
ports:
-"15000:80"
volumes:
-./nginx/htpasswd:/etc/nginx/htpasswd
-./nginx/conf.d/proxy.conf:/etc/nginx/conf.d/proxy.conf
-/var/run/docker.sock:/tmp/docker.sock:ro
networks:
-mlflow-net
mlflow:
build:
context:.
dockerfile:Dockerfile
args:
MLFLOW_VERSION:${MLFLOW_VERSION}
expose:
-"80"
restart:unless-stopped
depends_on:
db:
condition:service_healthy
rustfs-init:
condition:service_completed_successfully
env_file:
-.env
environment:
TZ:Asia/Tokyo
VIRTUAL_HOST:"${VIRTUAL_HOST:-localhost}"
MLFLOW_S3_ENDPOINT_URL:http://rustfs:9000
AWS_ACCESS_KEY_ID:rustfs-mlflow
AWS_SECRET_ACCESS_KEY:rustfs-mlflow
MLFLOW_BACKEND_STORE_URI:mysql+mysqldb://mlflow:mlflow@db:3306/mlflow
command:>
mlflow server
--backend-store-uri 'mysql+mysqldb://mlflow:mlflow@db:3306/mlflow'
--artifacts-destination 's3://mlflow/artifacts'
--serve-artifacts
--host 0.0.0.0
--port 80
networks:
-mlflow-net
-mlflow-internal-net
db:
image:mysql:latest
restart:unless-stopped
environment:
MYSQL_USER:mlflow
MYSQL_PASSWORD:mlflow
MYSQL_ROOT_PASSWORD:mlflow
MYSQL_DATABASE:mlflow
TZ:Asia/Tokyo
volumes:
-./mysql/data:/var/lib/mysql
-./mysql/my.cnf:/etc/mysql/conf.d/my.cnf
healthcheck:
test:["CMD","mysqladmin","ping","-h","localhost"]
interval:5s
timeout:10s
retries:5
networks:
-mlflow-internal-net
rustfs:
image:rustfs/rustfs:latest
security_opt:
-"no-new-privileges:true"
# ports:
# - "9000:9000" # S3 API port
environment:
-RUSTFS_VOLUMES=/data/rustfs
-RUSTFS_ADDRESS=0.0.0.0:9000
-RUSTFS_CONSOLE_ENABLE=false
-RUSTFS_EXTERNAL_ADDRESS=:9000
-RUSTFS_CORS_ALLOWED_ORIGINS=*
-RUSTFS_ACCESS_KEY=rustfs-mlflow
-RUSTFS_SECRET_KEY=rustfs-mlflow
-RUSTFS_OBS_LOGGER_LEVEL=info
# Object Cache
-RUSTFS_OBJECT_CACHE_ENABLE=true
-RUSTFS_OBJECT_CACHE_TTL_SECS=300
volumes:
-./rustfs:/data/rustfs
restart:unless-stopped
healthcheck:
test:["CMD","sh","-c","curl -f http://localhost:9000/health"]
interval:30s
timeout:10s
retries:3
start_period:40s
networks:
-mlflow-internal-net
# - mlflow-net
rustfs-init:
image:amazon/aws-cli:latest
depends_on:
rustfs:
condition:service_healthy
environment:
-AWS_ACCESS_KEY_ID=rustfs-mlflow
-AWS_SECRET_ACCESS_KEY=rustfs-mlflow
-AWS_DEFAULT_REGION=us-east-1
-AWS_REGION=us-east-1
entrypoint:/bin/sh
command:-c"aws --endpoint-url http://rustfs:9000 s3api create-bucket --bucket mlflow || true"
restart:"no"
networks:
-mlflow-internal-net
# RustFS volume permissions fixer service
volume-permission-helper:
image:alpine
volumes:
-./rustfs:/data
command:>
sh -c "
chown -R 10001:10001 /data &&
echo 'Volume Permissions fixed' &&
exit 0
"
restart:"no"
networks:
mlflow-net:
driver:bridge
mlflow-internal-net:
internal:true
nginx-proxy
Nginx Proxy 用於通過 Nginx 代理 MLflow 的 WebUI。這裏使用的 jwilder/nginx-proxy Docker 鏡像,幾乎不需要編寫 Nginx 配置文件,只需在 compose.yml 中進行描述、掛載特定卷並編輯環境變量,即可建立帶有 Basic 認證的 Nginx Proxy。
nginx-proxy:
image:jwilder/nginx-proxy:latest
restart:unless-stopped
ports:
-"15000:80"
volumes:
-./nginx/htpasswd:/etc/nginx/htpasswd
-./nginx/conf.d/proxy.conf:/etc/nginx/conf.d/proxy.conf
-/var/run/docker.sock:/tmp/docker.sock:ro
networks:
-mlflow-net
這次想要代理的服務只有 MLflow Server 一個,所以使用 nginx-proxy 看起來有些過頭,但因為它只需配置文件放置即可輕鬆切換監聽域名的指定和 Basic 認證的開啓/關閉,所以我們使用了它。首先,作為 nginx 的整體設置,在 nginx/conf.d/proxy.conf 中添加以下設置。
client_max_body_size 100g;
這是為了應對 MLflow Server 發送的文件尺寸較大的情況,以便能夠發送巨大的文件。接下來,在 mlflow 容器的環境變量 VIRTUAL_HOST 中描述監聽域名的指定和 Basic 認證的設置。
mlflow:
expose:
- "80"
environment:
VIRTUAL_HOST: "example.com,localhost"
該環境變量的值可以用逗號分隔指定多個域名。此外,通過 expose 指定想要代理的端口。這裏 expose 指定的端口會映射到 nginx-proxy 的端口,因此在 nginx-proxy 側按如下方式指定端口。
nginx-proxy:
ports:
- "15000:80"
這樣,就可以從外部通過 example.com:15000 和 localhost:15000 訪問 MLflow 的 WebUI 了。如果設置 Basic 認證,請在掛載到 nginx-proxy 卷的 nginx/htpasswd 文件中設置用户名和密碼。此時,Basic 認證的文件名應與域名相同。
cd nginx/htpasswd
htpasswd -c example.com [username]
cp example.com localhost
這樣,就可以從 example.com 和 localhost 訪問 MLflow 的 WebUI 了。即使更改監聽域名或不再需要 Basic 認證,nginx 的配置文件也會在容器啓動時更新,因此無需手動更改。
MLflow
MLflow Server 使用以下 Dockerfile 和 compose.yml 進行構建。可以通過環境變量 MLFLOW_VERSION 指定 MLflow 的版本。如果不指定,將使用最新版。由於 MLflow 使用 SQLAlchemy 連接 DB,因此需要適配 DB 的驅動程序,即 MySQL 的客户端庫 mysqlclient。此外,為了訪問 S3 兼容的對象文件存儲 RustFS,還需要安裝 boto3。
FROM python:3.13
ARG MLFLOW_VERSION=""
RUN if [ -n "$MLFLOW_VERSION" ]; then \
pip install --no-cache-dir mlflow=="$MLFLOW_VERSION" mysqlclient boto3; \
else \
pip install --no-cache-dir mlflow mysqlclient boto3; \
fi
在容器內執行 mlflow server 命令來啓動 MLflow Server。這裏,通過 --backend-store-uri 選項指定 MySQL 的連接信息,通過 --artifacts-destination 選項指定 RustFS 內的存儲桶和文件夾路徑。此外,通過 --serve-artifacts 選項,讓 artifact 從運行 MLflow Server 的容器保存到 RustFS。如果沒有此設置,客户端將直接訪問 S3 並保存 artifact。
mlflow:
build:
context:.
dockerfile:Dockerfile
args:
MLFLOW_VERSION:${MLFLOW_VERSION}
expose:
-"80"
restart:unless-stopped
depends_on:
db:
condition:service_healthy
rustfs-init:
condition:service_completed_successfully
env_file:
-.env
environment:
TZ:Asia/Tokyo
VIRTUAL_HOST:"${VIRTUAL_HOST:-localhost}"
MLFLOW_S3_ENDPOINT_URL:http://rustfs:9000
AWS_ACCESS_KEY_ID:rustfs-mlflow
AWS_SECRET_ACCESS_KEY:rustfs-mlflow
MLFLOW_BACKEND_STORE_URI:mysql+mysqldb://mlflow:mlflow@db:3306/mlflow
command:>
mlflow server
--backend-store-uri 'mysql+mysqldb://mlflow:mlflow@db:3306/mlflow'
--artifacts-destination 's3://mlflow/artifacts'
--serve-artifacts
--host 0.0.0.0
--port 80
networks:
-mlflow-net
-mlflow-internal-net
RustFS
RustFS 由僅在啓動時運行的 rustfs-init、volume-permission-helper 這 2 個容器,以及進行服務的容器 rustfs 共 3 個容器組成。
- rustfs-init: 用於在 RustFS 啓動時創建存儲桶的容器。
- volume-permission-helper: 用於修正 RustFS 卷權限的容器。當 RustFS 卷的權限不正確時運行。
- rustfs: RustFS 的容器。
rustfs:
image:rustfs/rustfs:latest
security_opt:
-"no-new-privileges:true"
# ports:
# - "9000:9000" # S3 API port
environment:
-RUSTFS_VOLUMES=/data/rustfs
-RUSTFS_ADDRESS=0.0.0.0:9000
-RUSTFS_CONSOLE_ENABLE=false
-RUSTFS_EXTERNAL_ADDRESS=:9000
-RUSTFS_CORS_ALLOWED_ORIGINS=*
-RUSTFS_ACCESS_KEY=rustfs-mlflow
-RUSTFS_SECRET_KEY=rustfs-mlflow
-RUSTFS_OBS_LOGGER_LEVEL=info
# Object Cache
-RUSTFS_OBJECT_CACHE_ENABLE=true
-RUSTFS_OBJECT_CACHE_TTL_SECS=300
volumes:
-./rustfs:/data/rustfs
restart:unless-stopped
healthcheck:
test:["CMD","sh","-c","curl -f http://localhost:9000/health"]
interval:30s
timeout:10s
retries:3
start_period:40s
networks:
-mlflow-internal-net
# - mlflow-net
rustfs-init:
image:amazon/aws-cli:latest
depends_on:
rustfs:
condition:service_healthy
environment:
-AWS_ACCESS_KEY_ID=rustfs-mlflow
-AWS_SECRET_ACCESS_KEY=rustfs-mlflow
-AWS_DEFAULT_REGION=us-east-1
-AWS_REGION=us-east-1
entrypoint:/bin/sh
command:-c"aws --endpoint-url http://rustfs:9000 s3api create-bucket --bucket mlflow || true"
restart:"no"
networks:
-mlflow-internal-net
# RustFS volume permissions fixer service
volume-permission-helper:
image:alpine
volumes:
-./rustfs:/data
command:>
sh -c "
chown -R 10001:10001 /data &&
echo 'Volume Permissions fixed' &&
exit 0
"
restart:"no"
在上面的示例中,RustFS 的 WebUI 被禁用了,但根據需要,可以按如下方式設置並啓用它。
nginx-proxy:
ports:
-"15001:9001"
rustfs:
image:rustfs/rustfs:latest
security_opt:
-"no-new-privileges:true"
ports:
# - "9000:9000" # S3 API port
# 追記 Nginx Proxy 的設置
expose:
-"9001"
environment:
# 指定監聽域名
-VIRTUAL_HOST=example.com,localhost
-RUSTFS_VOLUMES=/data/rustfs
-RUSTFS_ADDRESS=0.0.0.0:9000
-RUSTFS_EXTERNAL_ADDRESS=:9000
-RUSTFS_CORS_ALLOWED_ORIGINS=*
-RUSTFS_ACCESS_KEY=rustfs-mlflow
-RUSTFS_SECRET_KEY=rustfs-mlflow
-RUSTFS_OBS_LOGGER_LEVEL=info
# 追記 WebUI 的設置
-RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9001
-RUSTFS_CONSOLE_ENABLE=true
-RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS=*
# Object Cache
-RUSTFS_OBJECT_CACHE_ENABLE=true
-RUSTFS_OBJECT_CACHE_TTL_SECS=300
volumes:
-./rustfs:/data/rustfs
restart:unless-stopped
healthcheck:
test:["CMD","sh","-c","curl -f http://localhost:9000/health"]
interval:30s
timeout:10s
retries:3
start_period:40s
networks:
-mlflow-internal-net
在上述設置中,可以通過 example.com:15001 和 localhost:15001 訪問 RustFS 的 WebUI。
遷移到 RustFS 的方法
將數據遷移到 RustFS 時,使用 MinIO 開發的 mc 命令非常方便。
mc 命令具有存儲桶鏡像 (rsync) 功能,因此可以使用它輕鬆遷移數據。
- 確保可以訪問遷移源和遷移目標雙方的 S3 兼容存儲。
- 使用
--net host啓動 MinIO Client (mc) 容器。
docker run --rm -it --net host --entrypoint sh minio/mc
- 在容器內,設置遷移源和遷移目標的連接信息。
# 遷移源
mc alias set src http://host.docker.internal:10000 <ACCESS_KEY> <SECRET_KEY>
# 遷移目標
mc alias set dst http://host.docker.internal:9000 <ACCESS_KEY> <SECRET_KEY>
- 使用 mc mirror 命令複製數據。
mc mirror src/mlflow/artifacts dst/mlflow/artifacts