下面是根据实际验证通过的版本(dask/distributed 2024.6.2 + pandas 2.2.3 + numpy 1.26.4 + pyarrow 21.0.0)整理的完整部署教程。
部署前提
- 两台机器:
10.21.80.38(DSS 主机,UPCDIPPDTKW02)、10.21.80.39(副节点,UPCDIPPDTKW01) - 系统:Ubuntu 22.04
- 用户:
pcd_pdtk_op(主组users,gid=100) - 阿里云同 VPC 同安全组,内网带宽 2.7 Gbps
- DSS 13.3.2 已部署在 38,pyenv 为 Python 3.10(pandas 2.2.3)
关键包版本速查(三端必须一致)
| 包 | 版本 | 说明 |
|---|---|---|
| dask | 2024.6.2 | 核心,支持 pandas 2.x |
| distributed | 2024.6.2 | 核心 |
| cloudpickle | 3.1.2 | 序列化核心,必须显式钉版本 |
| msgpack | 1.1.2 | 通信协议序列化 |
| lz4 | 3.1.3 | 通信压缩 |
| pandas | 2.2.3 | 数据序列化 |
| numpy | 1.26.4 | 数据序列化 |
| pyarrow | 21.0.0 | 列式存储 |
第 0 阶段:环境检查(两台都做)
# 系统版本
lsb_release -a
# 内存大小
free -h
# 磁盘空间
df -h /
# 互通验证
ping -c 3 10.21.80.38 # 在 39 上执行
ping -c 3 10.21.80.39 # 在 38 上执行
记下两台的实际内存,决定后面 worker 内存限额。
第 1 阶段:Python 环境(两台都做)
DSS pyenv 是 Python 3.10,worker 端使用 Python 3.9(小版本差异兼容,序列化无影响)。
# 检查 Python 3.9 是否可用
python3.9 --version
# 应输出: Python 3.9.x
第 2 阶段:安装 Dask 集群端(两台都做)
# 1. 创建虚拟环境
python3.9 -m venv ~/dask-env
# 2. 激活
source ~/dask-env/bin/activate
# 3. 升级 pip + 配置阿里云镜像(可选)
pip install --upgrade pip -i https://mirrors.aliyun.com/pypi/simple/
pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/
# 4. 安装与 DSS pyenv 对齐的版本
pip install \
"dask==2024.6.2" \
"distributed==2024.6.2" \
"cloudpickle==3.1.2" \
"msgpack==1.1.2" \
"lz4==3.1.3" \
"pandas==2.2.3" \
"numpy==1.26.4" \
"pyarrow==21.0.0" \
"bokeh==2.4.3" \
"sqlalchemy==2.0.30"
# 5. 验证版本(两台输出必须完全一致)
pip list 2>/dev/null | grep -iE "^dask |^distributed|^pandas |^pyarrow|^numpy |^cloudpickle|^msgpack|^lz4 "
预期输出:
cloudpickle 3.1.2
dask 2024.6.2
distributed 2024.6.2
lz4 3.1.3
msgpack 1.1.2
numpy 1.26.4
pandas 2.2.3
pyarrow 21.0.0
⚠️ 重要:
- 跑
pip check,确认No broken requirements found. cloudpickle必须显式指定 3.1.2,不指定的话 pip 可能装其他版本,与 DSS 端不一致会导致 KilledWorker
第 3 阶段:Dask 配置文件(两台都做)
# 创建配置目录
mkdir -p ~/.config/dask
# 写入配置
cat > ~/.config/dask/distributed.yaml <<'EOF'
distributed:
scheduler:
work-stealing: false # 关闭 work-stealing,配合机器亲和性调度
worker:
memory:
target: 0.75 # 75% 触发主动溢出
spill: 0.85 # 85% 强制溢出到磁盘
pause: 0.90 # 90% 暂停接新任务
terminate: 0.98 # 98% 杀进程
comm:
compression: lz4 # 跨机传输用 lz4 压缩
timeouts:
connect: 30s
tcp: 30s
EOF
# 验证
cat ~/.config/dask/distributed.yaml
第 4 阶段:准备目录(两台都做)
# Spill 目录(如果 /data 有足够空间用 /data,否则用 /tmp)
mkdir -p /tmp/dask-spill
# 日志目录
sudo mkdir -p /data/dask
sudo chown pcd_pdtk_op:users /data/dask
sudo chmod 755 /data/dask
第 5 阶段:前台启动测试(先验证能跑)
5.1 机器 38:启动 Scheduler
SSH 到 38,开 tmux:
tmux new -s dask-scheduler
source ~/dask-env/bin/activate
dask scheduler --host 0.0.0.0 --port 8786 --dashboard-address :8787
⚠️ dask 2024.x 推荐用 dask scheduler(空格),dask-scheduler(连字符)也仍然可用
看到如下输出说明成功:
distributed.scheduler - INFO - Scheduler at: tcp://10.21.80.38:8786
distributed.scheduler - INFO - dashboard at: :8787
按 Ctrl+B 然后 D 脱离 tmux。
5.2 机器 38:启动 4 个主力 Worker
新开终端:
tmux new -s dask-worker
source ~/dask-env/bin/activate
dask worker tcp://10.21.80.38:8786 \
--nworkers 4 \
--nthreads 4 \
--memory-limit 60GB \
--local-directory /tmp/dask-spill \
--resources "primary=1 machine_38=1" \
--name primary
⚠️ 关键点:
- dask 2024.x 用
dask worker(空格)或dask-worker(连字符),两者都可用 - 必须用
--nworkers(--nprocs在 2024.x 中已移除) --name primary(自动追加-0、-1、-2、-3)--resources "primary=1 machine_38=1":machine_38标签用于 Job 级别机器亲和性调度
看到 4 行 Registered to: tcp://10.21.80.38:8786 说明 OK。
5.3 机器 39:启动 Secondary Worker
SSH 到 39:
tmux new -s dask-worker
source ~/dask-env/bin/activate
dask worker tcp://10.21.80.38:8786 \
--nworkers 1 \
--nthreads 4 \
--memory-limit 50GB \
--local-directory /tmp/dask-spill \
--resources "secondary=1 machine_39=1" \
--name secondary-39
5.4 验证集群
回到 38 任意终端:
source ~/dask-env/bin/activate
python3 -c "
from dask.distributed import Client
c = Client('tcp://10.21.80.38:8786')
info = c.scheduler_info()
print(f'Workers: {len(info[\"workers\"])}/5')
for w in info['workers'].values():
res = w.get('resources', {})
mem = w['memory_limit']/1e9
print(f' {w[\"name\"]}: nthreads={w[\"nthreads\"]}, mem={mem:.0f}GB, resources={res}')
"
预期:
Workers: 5/5
primary-0: nthreads=4, mem=60GB, resources={'primary': 1.0, 'machine_38': 1.0}
primary-1: nthreads=4, mem=60GB, resources={'primary': 1.0, 'machine_38': 1.0}
primary-2: nthreads=4, mem=60GB, resources={'primary': 1.0, 'machine_38': 1.0}
primary-3: nthreads=4, mem=60GB, resources={'primary': 1.0, 'machine_38': 1.0}
secondary-39: nthreads=4, mem=XXGB, resources={'secondary': 1.0, 'machine_39': 1.0}
5.5 简单计算测试
import dask.array as da
from dask.distributed import Client
from dask import annotate
c = Client("tcp://10.21.80.38:8786")
with annotate(resources={'primary': 1}):
x = da.random.random((50000, 25000), chunks=(5000, 5000))
result = x.mean().compute()
print(f"Mean: {result}")
print("✓ 集群工作正常")
跑通后进入 tmux 各窗口按 Ctrl+C 关掉前台进程(或 tmux kill-server),准备做 systemd 部署。
第 6 阶段:systemd 服务部署
6.1 机器 38:Scheduler Service
sudo tee /etc/systemd/system/dask-scheduler.service > /dev/null <<'EOF'
[Unit]
Description=Dask Scheduler
After=network.target
Wants=network-online.target
[Service]
Type=simple
User=pcd_pdtk_op
Group=users
WorkingDirectory=/home/pcd_pdtk_op
Environment="PATH=/home/pcd_pdtk_op/dask-env/bin:/usr/local/bin:/usr/bin:/bin"
ExecStart=/home/pcd_pdtk_op/dask-env/bin/dask-scheduler \
--host 0.0.0.0 \
--port 8786 \
--dashboard-address :8787
Restart=always
RestartSec=10
StandardOutput=append:/data/dask/scheduler.log
StandardError=append:/data/dask/scheduler.err
[Install]
WantedBy=multi-user.target
EOF
⚠️ 关键点:
Group=users(不是Group=pcd_pdtk_op)- systemd ExecStart 建议用
dask-scheduler(连字符),直接指向 bin 下的可执行文件 - 日志路径
/data/dask/
6.2 机器 38:Worker Service
sudo tee /etc/systemd/system/dask-worker.service > /dev/null <<'EOF'
[Unit]
Description=Dask Worker (Primary on 38)
After=network.target dask-scheduler.service
Requires=dask-scheduler.service
[Service]
Type=simple
User=pcd_pdtk_op
Group=users
WorkingDirectory=/home/pcd_pdtk_op
Environment="PATH=/home/pcd_pdtk_op/dask-env/bin:/usr/local/bin:/usr/bin:/bin"
ExecStart=/home/pcd_pdtk_op/dask-env/bin/dask-worker tcp://10.21.80.38:8786 \
--nworkers 4 \
--nthreads 4 \
--memory-limit 60GB \
--local-directory /tmp/dask-spill \
--resources "primary=1 machine_38=1" \
--name primary
Restart=always
RestartSec=15
StandardOutput=append:/data/dask/worker.log
StandardError=append:/data/dask/worker.err
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
EOF
⚠️ dask 2024.x 必须用 --nworkers,--nprocs 已被移除,会导致 exit code 2。
6.3 机器 39:Worker Service
SSH 到 39:
sudo tee /etc/systemd/system/dask-worker.service > /dev/null <<'EOF'
[Unit]
Description=Dask Worker (Secondary on 39)
After=network.target
Wants=network-online.target
[Service]
Type=simple
User=pcd_pdtk_op
Group=users
WorkingDirectory=/home/pcd_pdtk_op
Environment="PATH=/home/pcd_pdtk_op/dask-env/bin:/usr/local/bin:/usr/bin:/bin"
ExecStart=/home/pcd_pdtk_op/dask-env/bin/dask-worker tcp://10.21.80.38:8786 \
--nworkers 1 \
--nthreads 4 \
--memory-limit 50GB \
--local-directory /tmp/dask-spill \
--resources "secondary=1 machine_39=1" \
--name secondary-39
Restart=always
RestartSec=15
StandardOutput=append:/data/dask/worker.log
StandardError=append:/data/dask/worker.err
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
EOF
6.4 启动服务
机器 38:
sudo systemctl daemon-reload
sudo systemctl enable dask-scheduler dask-worker
sudo systemctl start dask-scheduler
sleep 5
sudo systemctl start dask-worker
# 查看状态
sudo systemctl status dask-scheduler --no-pager
sudo systemctl status dask-worker --no-pager
机器 39:
sudo systemctl daemon-reload
sudo systemctl enable dask-worker
sudo systemctl start dask-worker
sudo systemctl status dask-worker --no-pager
均应看到 Active: active (running)。
6.5 验证服务化集群
source ~/dask-env/bin/activate
python3 -c "
from dask.distributed import Client
c = Client('tcp://10.21.80.38:8786')
info = c.scheduler_info()
print(f'Workers: {len(info[\"workers\"])}/5')
for w in info['workers'].values():
print(f' {w[\"name\"]}: {w[\"memory_limit\"]/1e9:.0f}GB, {w.get(\"resources\")}')
"
第 7 阶段:DSS Code Env 集成
7.1 方式 A:使用 DSS 内置 pyenv(推荐)
如果 DSS 的 pyenv(/data/DATA_DIR/pyenv)已包含 pandas 2.2.3、numpy 1.26.4,只需确认 dask 相关包已安装:
/data/DATA_DIR/pyenv/bin/pip install \
"dask==2024.6.2" \
"distributed==2024.6.2" \
"cloudpickle==3.1.2" \
"msgpack==1.1.2" \
"lz4==3.1.3"
7.2 方式 B:通过 DSS Code Env 管理界面
Administration → Code envs → 对应 code env → Packages to install
在已有 requirements 末尾追加:
dask==2024.6.2
distributed==2024.6.2
cloudpickle==3.1.2
msgpack==1.1.2
lz4==3.1.3
⚠️ 必须显式写 cloudpickle、msgpack、lz4:
cloudpickle不钉版本的话 pip 可能解析到其他版本,与 worker 端不一致会导致 KilledWorkermsgpack和lz4同理,钉死确保三端一致
⚠️ 不要写:
pandas/pyarrow/numpy(已由 DSS 环境锁定,不要覆盖)bokeh(client 端不需要)
点 Save and update,等 2-5 分钟。
7.3 处理 GitHub 依赖拉取失败(如遇到)
若 code env 含 GitHub 私有库依赖(如 lvmh-group/atom-ds-core-library),可能因 GnuTLS 报错。处理方案:
# 方案 A:重试几次
# 方案 B:给 git 配代理(DSS 运行用户身份执行)
git config --global http.proxy http://127.0.0.1:7890
git config --global https.proxy http://127.0.0.1:7890
# 方案 C:用 ghproxy 镜像
git config --global url."https://ghproxy.com/https://github.com/".insteadOf "https://github.com/"
7.4 测试 DSS 到集群的连接
DSS 里新建 Python recipe:
import pandas, dask, distributed, cloudpickle, msgpack
print(f"pandas: {pandas.__version__} (期望 2.2.3)")
print(f"numpy: {__import__('numpy').__version__} (期望 1.26.4)")
print(f"dask: {dask.__version__} (期望 2024.6.2)")
print(f"distributed: {distributed.__version__} (期望 2024.6.2)")
print(f"cloudpickle: {cloudpickle.__version__} (期望 3.1.2)")
print(f"msgpack: {msgpack.version} (期望 (1, 1, 2))")
from dask.distributed import Client
c = Client("tcp://10.21.80.38:8786")
print(c)
info = c.scheduler_info()
print(f"\nWorkers: {len(info['workers'])}/5")
for w in info['workers'].values():
print(f" {w['name']}: {w['memory_limit']/1e9:.0f}GB, {w.get('resources')}")
c.close()
第 8 阶段:日常运维
启动 / 停止 / 重启
# 38 上
sudo systemctl restart dask-scheduler
sudo systemctl restart dask-worker
sudo systemctl stop dask-worker
# 39 上
sudo systemctl restart dask-worker
查看日志
# systemd 日志
sudo journalctl -u dask-scheduler -f
sudo journalctl -u dask-worker -f
# 应用日志文件
tail -f /data/dask/scheduler.log
tail -f /data/dask/worker.log
Dashboard 监控
浏览器访问 http://10.21.80.38:8787(需阿里云安全组放通 8787 入方向)
本地无法直接访问可用 SSH 端口转发:
ssh -L 8787:10.21.80.38:8787 pcd_pdtk_op@10.21.80.38
# 浏览器打开 http://localhost:8787
完整集群重启
# 在 39 上停 worker
ssh pcd_pdtk_op@10.21.80.39 "sudo systemctl stop dask-worker"
# 在 38 上重启
sudo systemctl restart dask-scheduler
sleep 5
sudo systemctl restart dask-worker
# 在 39 上启 worker
ssh pcd_pdtk_op@10.21.80.39 "sudo systemctl start dask-worker"
第 9 阶段:DSS Recipe 模板(Job 级别机器亲和性调度)
"""
Dask on DSS — Job 级别机器亲和性调度
====================================
调度逻辑:
- 每个 Job 自动选最空闲的机器
- scatter + annotate 双重钉机器,数据零跨机传输
- 2 个 Job 并行时各占一台
"""
import dataiku
import dask
import dask.dataframe as dd
import pandas as pd
import numpy as np
from dask.distributed import Client, wait
from dask import delayed
# ============ 配置 ============
SCHEDULER_ADDR = "tcp://10.21.80.38:8786"
MACHINE_TAGS = ["machine_38", "machine_39"]
OUTPUT_DATASET = "your_output_dataset" # ← 改成你 recipe 的 output dataset 名
NUM_TEST_ROWS = 2_000_000 # 测试数据行数,正式使用时替换为 DSS 数据读取
CHUNK_SIZE = 500_000
# ============ 1. 连接 ============
client = Client(SCHEDULER_ADDR)
print(client)
# ============ 2. 版本校验 ============
def get_worker_versions():
import sys, pandas, numpy, dask, distributed, cloudpickle
return {
"python": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
"pandas": pandas.__version__,
"numpy": numpy.__version__,
"dask": dask.__version__,
"distributed": distributed.__version__,
"cloudpickle": cloudpickle.__version__,
}
import sys
client_ver = {
"python": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
"pandas": pd.__version__, "numpy": np.__version__,
"dask": dask.__version__, "distributed": __import__('distributed').__version__,
"cloudpickle": __import__('cloudpickle').__version__,
}
worker_results = client.run(get_worker_versions)
if not worker_results:
raise RuntimeError("没有 worker 连接到集群,请检查 worker 服务状态")
worker_ver = list(worker_results.values())[0]
print(f"\n{'Package':<15} {'Client':<18} {'Worker':<18} {'Match'}")
print("-" * 60)
for pkg in client_ver:
cv, wv = client_ver[pkg], worker_ver.get(pkg, "?")
print(f"{pkg:<15} {cv:<18} {wv:<18} {'✓' if cv == wv else '✗'}")
print()
# ============ 3. 选机器 ============
def pick_machine(client, machine_tags):
workers = client.scheduler_info()["workers"]
machine_stats = {}
for tag in machine_tags:
addrs, free_mem = [], 0
for addr, info in workers.items():
if info.get("resources", {}).get(tag, 0) > 0:
addrs.append(addr)
ml = info.get("memory_limit", 0)
m = info.get("metrics", {})
free_mem += max(0, ml - m.get("managed_bytes", 0) - m.get("unmanaged_old_bytes", 0))
if addrs:
machine_stats[tag] = {"addrs": addrs, "free_mem": free_mem}
if not machine_stats:
raise RuntimeError(f"没有带 {machine_tags} 标签的 worker")
for tag, s in machine_stats.items():
print(f" {tag}: {len(s['addrs'])} workers, 可用 {s['free_mem']/1e9:.1f} GB")
chosen_tag = max(machine_stats, key=lambda t: machine_stats[t]["free_mem"])
chosen = machine_stats[chosen_tag]
print(f"✓ 选定: {chosen_tag} ({len(chosen['addrs'])} workers, {chosen['free_mem']/1e9:.1f} GB)\n")
return chosen_tag, chosen["addrs"]
machine_tag, target_workers = pick_machine(client, MACHINE_TAGS)
# ============ 4A. 生成测试数据(验证用,正式使用时替换为 4B) ============
print(f"生成 {NUM_TEST_ROWS:,} 行测试数据...")
rng = np.random.RandomState(42)
futures = []
rows_done = 0
chunk_id = 0
while rows_done < NUM_TEST_ROWS:
n = min(CHUNK_SIZE, NUM_TEST_ROWS - rows_done)
chunk = pd.DataFrame({
"store_id": rng.randint(1, 101, size=n),
"amount": rng.uniform(10, 500, size=n).round(2),
"qty": rng.randint(1, 20, size=n),
"date": pd.Timestamp("2025-01-01") + pd.to_timedelta(rng.randint(0, 365, size=n), unit="D"),
})
future = client.scatter(chunk, workers=target_workers, broadcast=False)
futures.append(future)
rows_done += n
chunk_id += 1
del chunk
# ============ 4B. 从 DSS Dataset 读取(正式使用时取消注释,删除 4A) ============
# INPUT_DATASET = "your_input_dataset" # ← 改成你 recipe 的 input dataset 名
# input_ds = dataiku.Dataset(INPUT_DATASET)
# futures = []
# chunk_id = 0
# for chunk in input_ds.iter_dataframes(chunksize=CHUNK_SIZE):
# future = client.scatter(chunk, workers=target_workers, broadcast=False)
# futures.append(future)
# chunk_id += 1
# del chunk
wait(futures)
print(f"✓ scatter 完成: {chunk_id} chunks → {len(target_workers)} workers")
# 验证数据分布
workers_info = client.scheduler_info()["workers"]
w2m = {}
for tag in MACHINE_TAGS:
for addr, info in workers_info.items():
if info.get("resources", {}).get(tag, 0) > 0:
w2m[addr] = tag
tag_counts = {t: 0 for t in MACHINE_TAGS}
for f in futures:
for key, addrs in client.who_has(f).items():
for a in addrs:
t = w2m.get(a, "unknown")
if t in tag_counts:
tag_counts[t] += 1
for t, c in tag_counts.items():
print(f" {t}: {c} partitions")
# 构建 dask DataFrame
meta = futures[0].result().iloc[:0]
ddf = dd.from_delayed([delayed(lambda f: f)(f) for f in futures], meta=meta)
print(f"✓ dask DataFrame: {ddf.npartitions} partitions\n")
# ============ 5. 业务计算(钉在目标机器) ============
print("开始计算...")
with dask.annotate(resources={machine_tag: 1}):
result = ddf.groupby("store_id").agg({
"amount": "sum",
"qty": "sum",
}).reset_index()
# ============ 6. 写出到 DSS ============
# 方式 A:结果较小(< 1GB),直接 compute 回本地写入
with dask.annotate(resources={machine_tag: 1}):
result_pd = result.compute()
print(f"✓ 计算完成: {len(result_pd)} 行, "
f"{result_pd.memory_usage(deep=True).sum()/1e6:.1f} MB")
print(result_pd.head(10))
output_ds = dataiku.Dataset(OUTPUT_DATASET)
output_ds.write_with_schema(result_pd)
print(f"\n✓ 已写入 {OUTPUT_DATASET}")
# 方式 B:结果较大(> 1GB),分批写,避免 DSS recipe 进程 OOM
# output_ds = dataiku.Dataset(OUTPUT_DATASET)
# output_ds.write_schema_from_dataframe(result.head(100))
# with output_ds.get_writer() as writer:
# for part in result.to_delayed():
# with dask.annotate(resources={machine_tag: 1}):
# part_pd = part.compute()
# writer.write_dataframe(part_pd)
# del part_pd
client.close()
print("✓ Done")
第 10 阶段:故障排查速查表
| 症状 | 检查/处理 |
|---|---|
| systemd 启动 status=216/GROUP | Group 不存在,改 Group=users |
| Worker exit code=2 | --nprocs 在 2024.x 中已移除,改成 --nworkers |
| Worker 连不上 scheduler | sudo systemctl status dask-scheduler + 阿里云安全组 8786 端口 |
| KilledWorker / 版本协议错误 | pip list 对比 cloudpickle、msgpack、lz4 三端必须完全一致 |
StringMethods AttributeError | dask 版本过旧不兼容 pandas 2.x,升级到 dask 2024.1.0+ |
IndexError: list index out of range (版本校验时) | 没有 worker 连上来,先检查 scheduler 和 worker 服务状态 |
| Worker 频繁重启 | sudo journalctl -u dask-worker -n 100 看错误 |
| annotate 后 task 挂起不执行 | worker --resources 缺少 machine tag,检查是否有 machine_38=1 / machine_39=1 |
| 任务在 39 上跑了 | 38 内存满了或 partition 调度不平衡,是正常溢出行为 |
| 任务全在 38 上跑,39 不动 | 正常,39 是溢出补充用的 |
| Dashboard 打不开 | 阿里云安全组放通 8787 入方向 |
compute() 时 DSS OOM | 结果太大,改用分批写(方式 B)或 to_parquet 写盘 |
| Dataset xxx cannot be used | dataset 名没在 recipe 的 Input/Output 面板里声明,或名字拼错了 |
| 插件 GitHub 依赖拉取失败 | 配 git proxy 或用 ghproxy 镜像 |
第 11 阶段:阿里云安全组
两台 ECS 加入同一安全组,组内规则放通:
| 协议 | 端口 | 授权对象 |
|---|---|---|
| TCP | 8786/8787 | 同安全组 |
| TCP | 1024-65535 | 同安全组(worker 随机端口) |
| ICMP | 全部 | 同安全组 |
最终验证清单
- 两台
pip list关键包版本完全一致(dask / distributed / cloudpickle / msgpack / lz4 / pandas / numpy / pyarrow) ~/.config/dask/distributed.yaml两台存在且一致- 38 上
dask-scheduler.service和dask-worker.servicerunning - 39 上
dask-worker.servicerunning - Worker 使用
--nworkers(不是--nprocs) - Worker resources 包含 machine tag(
machine_38=1/machine_39=1) - Python 客户端能看到 5 个 worker
- Dashboard
http://10.21.80.38:8787能访问 - DSS code env 装好 dask / distributed / cloudpickle / msgpack / lz4
- DSS recipe 里能
Client("tcp://10.21.80.38:8786")成功 - 重启服务后能自动注册到集群
关键注意事项汇总
⚠️ dask 2024.6.2 相比 2022.5.0 的变化:
--nprocs已移除,必须用--nworkersdask scheduler/dask worker(空格命令)是推荐写法,连字符写法仍兼容- 原生支持 pandas 2.x,不再有
StringMethods兼容性问题 - 支持 P2P shuffle,大表 groupby/join 性能更好
⚠️ DSS Code Env 的约束:
- cloudpickle==3.1.2、msgpack==1.1.2、lz4==3.1.3 必须显式钉版本
- pandas / numpy / pyarrow 由 DSS 环境管理,不要在 dask 配置中覆盖
- 数据读取走 DSS API + scatter,不走
dd.read_sql_*
⚠️ 机器亲和性调度:
- Worker 必须带
machine_38=1/machine_39=1资源标签 - Recipe 代码通过
MACHINE_TAGS引用这些标签 scatter(workers=target_workers)钉数据位置dask.annotate(resources={machine_tag: 1})钉计算位置- 两个 Job 并行时自动按可用内存分配到不同机器
⚠️ 优化建议:
- SQL 端预聚合优先于 Dask shuffle
- partition 大小控制在 100-500 MB
- 结果大于 1GB 用分批写(方式 B)而不是
compute()收回 client
第 12 阶段:生产内存保护与监控加固
12.1 故障背景
2026-08-12,10.21.80.38 发生 Linux 全局 OOM:
- 多个 UID
1005的 Python/Dask 进程占用大量内存; - 内核多次执行
Out of memory: Killed process; - Dask Worker Event Loop 长时间无响应;
- Dataiku 端口
11000健康检查失败; - 重启服务器后 Dataiku 恢复。
原配置:
38:4 workers × 60GB
39:1 worker × 50GB
Dask 的 --memory-limit 按每个 Worker 计算,38 节点理论上可使用约 240GB,与 Dataiku JVM、系统及其他进程发生内存争抢。
12.2 统一后的生产配置
两台服务器统一使用:
Dask 版本:2024.6.2
Workers:4
Threads per worker:4
Memory limit per worker:40GB
每台服务器 Dask 总目标内存:4 × 40GB = 160GB
Dask 日志中每个 Worker 显示约:
37.25GiB
节点配置:
| 节点 | 地址 | Worker 名称 | 资源标签 |
|---|---|---|---|
| 主节点 | 10.21.80.38 | primary-0 ~ primary-3 | primary=1 machine_38=1 |
| 副节点 | 10.21.80.39 | secondary-39-0 ~ secondary-39-3 | secondary=1 machine_39=1 |
集群总状态:
8 workers
32 threads
每个 Worker 37.25GiB
12.3 Spill 目录
两台服务器均使用数据盘:
sudo install -d -o pcd_pdtk_op -g users -m 0750 /data/dask-spill
Worker 启动参数:
--local-directory /data/dask-spill
不再使用:
/tmp/dask-spill
12.4 systemd 内存保护
两台服务器的 dask-worker.service 均增加:
MemoryAccounting=yes
MemoryHigh=160G
MemoryMax=175G
说明:
MemoryHigh=160G:达到后触发内存回收压力;MemoryMax=175G:Dask 服务硬限制;- 限制作用于整个 Dask Worker 服务;
- 防止异常任务拖垮整台服务器。
查看配置:
sudo systemctl show dask-worker.service \
-p MemoryAccounting -p MemoryHigh -p MemoryMax
预期:
MemoryAccounting=yes
MemoryHigh=171798691840
MemoryMax=187904819200
12.5 38 节点 Worker 配置
ExecStart=/home/pcd_pdtk_op/dask-env/bin/dask-worker tcp://10.21.80.38:8786 --nworkers 4 --nthreads 4 --memory-limit 40GB --local-directory /data/dask-spill --resources "primary=1 machine_38=1" --name primary
12.6 39 节点 Worker 配置
ExecStart=/home/pcd_pdtk_op/dask-env/bin/dask-worker tcp://10.21.80.38:8786 --nworkers 4 --nthreads 4 --memory-limit 40GB --local-directory /data/dask-spill --resources "secondary=1 machine_39=1" --name secondary-39
12.7 Dask 内存阈值
两台服务器的:
/home/pcd_pdtk_op/.config/dask/distributed.yaml
统一配置为:
distributed:
scheduler:
work-stealing: false
worker:
memory:
target: 0.75
spill: 0.85
pause: 0.90
terminate: 0.98
comm:
compression: lz4
timeouts:
connect: 30s
tcp: 30s
12.8 服务重载与验证
sudo systemctl daemon-reload
sudo systemctl restart dask-worker.service
在 38 上验证集群:
/home/pcd_pdtk_op/dask-env/bin/python - <<'PY'
from dask.distributed import Client
c = Client("tcp://10.21.80.38:8786", timeout="10s")
info = c.scheduler_info()
print("workers:", len(info["workers"]))
for address, w in sorted(info["workers"].items(), key=lambda x: x[1]["name"]):
print(
f'{w["name"]}: '
f'threads={w["nthreads"]}, '
f'memory={w["memory_limit"]/2**30:.2f}GiB, '
f'resources={w.get("resources", {})}'
)
c.close()
PY
预期:
workers: 8
每个 Worker: threads=4
每个 Worker: memory=37.25GiB
验证 Dataiku:
curl -sS -o /dev/null -w '11000 HTTP %{http_code}\n' \
http://10.21.80.38:11000/
预期:
11000 HTTP 200
验证 spill 路径:
sudo grep -E 'Worker name:|Memory:|Local Directory:' \
/data/dask/worker.err | tail -24
12.9 主机监控脚本
监控脚本:
/data/check.sh
监控日志:
/data/daily_check.log
监控内容:
- CPU 使用率;
- 系统负载;
- 内存使用率;
- Swap 使用率;
- 内存占用最高进程;
- CPU 占用最高进程;
- Python 进程;
- 最近 10 分钟 OOM 日志。
Cron 配置:
*/5 * * * * /data/check.sh
脚本使用锁文件防止重复执行:
/tmp/daily_check.lock
确认任务:
sudo crontab -u pcd_pdtk_op -l
12.10 监控日志轮转
配置文件:
/etc/logrotate.d/daily-check
内容:
/data/daily_check.log {
daily
rotate 14
maxsize 100M
compress
delaycompress
missingok
notifempty
copytruncate
}
说明:
- 每天检查一次;
- 日志超过 100MB 时提前轮转;
- 保留 14 份;
- 历史日志压缩;
- 不需要重启监控脚本。
验证:
sudo logrotate -d /etc/logrotate.d/daily-check
12.11 日常检查命令
free -h
sudo journalctl -k --since '1 hour ago' --no-pager | \
grep -Ei 'oom|out of memory|killed process'
ps -eo pid,ppid,user,%cpu,%mem,rss,etime,cmd --sort=-rss | head -30
sudo systemctl status dask-worker.service --no-pager
sudo grep -Ei 'signal 9|signal 15|memory|spill|pause|terminate|unresponsive' \
/data/dask/worker.err | tail -100
12.12 回滚方案
如需恢复原 Worker 配置:
sudo rm -f /etc/systemd/system/dask-worker.service.d/override.conf
sudo systemctl daemon-reload
sudo systemctl restart dask-worker.service
如需恢复原日志查询窗口:
sudo sed -i 's/--since "10 minutes ago"/--since "1 hour ago"/' /data/check.sh
12.13 当前验证结论
当前双机 Dask 集群已验证:
38 和 39 共 8 个 Worker
每个 Worker 4 个线程
每个 Worker 37.25GiB
spill 使用 /data/dask-spill
Dask 服务具备 MemoryHigh/MemoryMax 保护
Dataiku 11000 返回 HTTP 200
该配置用于避免 Dask 异常任务再次触发主机级 OOM。