第5次实验:Docker、Swarm、Hadoop与Drill
1.镜像、容器与分布式查询
课程原文:Docker使用操作系统级虚拟化,把应用及其依赖放入容器。镜像包含代码、库、工具和依赖。容器是镜像的运行实例。第5次实验使用Docker部署Hadoop、Spark和Drill。实验使用Docker Swarm的覆盖网络连接运行Docker守护进程的多台主机。
- 镜像(image):只读的分层模板。一个镜像可以创建多个容器。
- 容器(container):镜像的运行实例。容器具有独立的进程视图和可写层。
- Swarm管理节点(manager):初始化Swarm。管理节点签发加入令牌。管理节点维护节点成员关系。
- 工作节点(worker):使用管理节点给出的令牌和地址加入Swarm。
- 覆盖网络:使不同Docker主机上的容器进入同一个逻辑二层网络。
可验证推导:容器能够启动,只能说明本机容器运行时可用。容器通过名称访问另一台主机上的容器,还依赖Swarm成员关系、覆盖网络和网络端口。必须按层检查故障。
- 运行
docker ps -a。检查容器状态。
- 在管理节点运行
docker node ls。检查Swarm节点。
- 运行
docker network ls。检查 hadoop-net。
- 进入容器。测试名称解析和
ping。
- 检查应用进程、端口和日志。
2.第5次实验的启动顺序和检查项
课程原文:管理节点和工作节点必须先能互相 ping。管理节点初始化Swarm。管理节点创建可附加的 hadoop-net。工作节点使用生成的令牌加入Swarm。然后启动Hadoop容器、Hadoop守护进程、ZooKeeper和Drillbit。
管理节点
docker system prune
master/init_swarm.sh
master/init_network.sh
docker node ls
docker network ls
export WORKER_NUMBER=N
master/start.sh
master/start_hadoop.sh
jps
./run-wordcount.sh
每个工作节点
docker system prune
worker/join_swarm.sh <JOIN_TOKEN> <MANAGER_IP>
export WORKER_NUMBER=N
export WORKER_ID=X
worker/start.sh
docker ps -a
需要区分:N 表示工作节点总数。X 表示当前工作节点的编号。源材料没有规定所有集群都使用同一个具体数值。
风险提示:docker system prune 会删除未使用的Docker对象。课程步骤包含该命令,但真实环境中应先检查待删除对象;不要把清理命令当作无副作用的“重启”。
3. Hadoop四层角色
课程原文:第5次实验在主容器中启动 NameNode、DataNode、ResourceManager 和 NodeManager。课程的Hadoop材料进一步区分HDFS、YARN和MapReduce。
| 层 | 角色 | 职责 | 典型观察 |
| HDFS | NameNode | 维护文件系统命名空间和块元数据。 | jps、NameNode网页界面 |
| HDFS | DataNode | 存储数据块并执行块读写。 | hdfs dfs -ls / |
| YARN | ResourceManager | 在集群范围分配资源。 | YARN网页界面 |
| YARN | NodeManager | 管理单节点资源并运行任务容器。 | jps、任务日志 |
| MapReduce | 映射 → 混洗和排序 → 归约 | 映射键值。按键分组并传输数据。聚合数据。 | 作业输出目录 |
hdfs dfs -mkdir /input
hdfs dfs -put local.csv /input
hdfs dfs -ls /input
hdfs dfs -cat /input/local.csv
hdfs dfs -get /output ./local_output
需要区分:HDFS路径不是本机路径。hdfs dfs -put 从本机复制文件到HDFS。-get 从HDFS复制文件到本机。Hadoop Streaming的输出目录必须事先不存在。
hadoop jar "$HADOOP_HOME/share/hadoop/tools/lib/hadoop-streaming-3.5.0.jar" \
-files mapper.py,reducer.py \
-input /user/hadoop/input \
-output /user/hadoop/output \
-mapper mapper.py \
-reducer reducer.py
4. Drill:读取数据时确定模式的分布式SQL
课程原文:Apache Drill是面向大数据探索的低延迟分布式SQL查询引擎,可查询JSON、Parquet、CSV与Avro等数据。分布式模式中,Drillbit是查询执行服务,ZooKeeper用于协调与服务发现。
./build/docker-build-drill-image.sh
drill/start_zookeeper.sh
drill/start_drillbit.sh 1
# 同一主机再启动一个 Drillbit 时,宿主机端口必须不同
DRILL_HTTP_PORT=8048 DRILL_USER_PORT=31020 drill/start_drillbit.sh 2
docker exec hadoop-drillbit-1 /usr/local/drill/bin/sqlline \
-u jdbc:drill:drillbit=localhost \
--outputformat=tsv --silent=true \
-e 'SELECT * FROM sys.drillbits;'
课程原文:在 sys.drillbits 的正常示例中,各Drillbit的状态为 ONLINE。恰有一个节点的 current 为 true,表示当前连接的Drillbit。该值不表示集群领导节点。上面的命令在容器 hadoop-drillbit-1 内通过 jdbc:drill:drillbit=localhost 建立直连,所以运行实例是 hadoop-drillbit-1。如果使用ZooKeeper连接字符串,则通过查询结果中 current=true 的行识别实际实例。
CSV的显式字段解释
SELECT *
FROM (
SELECT
columns[0] AS name,
columns[1] AS id,
CAST(columns[2] AS INT) AS grade
FROM dfs.`/path/grades.csv`
)
WHERE name = 'Tad Wyze'
ORDER BY grade DESC;
需要区分:读取时确定模式(schema-on-read)不表示所有字段会自动获得正确类型。文本CSV的 columns[n] 初始为字符串;执行数值排序或聚合前,应显式使用 CAST 转换为数值类型。
5.三道航班Drill题:仓库中的实际SQL
仓库事实:labs/l5/src/lab5_drill.sql 查询HDFS上的 dfs.root.`/user/hadoopuser/lab5/flights_parquet`。实际列名是 UniqueCarrier、DepDelay、Origin 与 WeatherDelay;第一题按正的出发延误次数排名,第二题按天气延误分钟总和排名,第三题求各承运人的最大出发延误。
-- 1:正的出发延误次数最多的承运人(仓库保留前 10)
SELECT UniqueCarrier, COUNT(*) AS late_count
FROM dfs.root.`/user/hadoopuser/lab5/flights_parquet`
WHERE DepDelay > 0
GROUP BY UniqueCarrier
ORDER BY late_count DESC
LIMIT 10;
-- 2:天气延误分钟总和最大的三个出发地
SELECT Origin, SUM(WeatherDelay) AS weather_delay
FROM dfs.root.`/user/hadoopuser/lab5/flights_parquet`
WHERE WeatherDelay > 0
GROUP BY Origin
ORDER BY weather_delay DESC
LIMIT 3;
-- 3:每个承运人的最大出发延误
SELECT UniqueCarrier, MAX(DepDelay) AS max_dep_delay
FROM dfs.root.`/user/hadoopuser/lab5/flights_parquet`
GROUP BY UniqueCarrier
ORDER BY UniqueCarrier;
仓库结果:第一题首位是 DL(8,064,705次);第二题前三名为 ATL、DFW、ORD,对应天气延误总和2,875,797、2,420,428、1,981,125。注意“最常晚点”也可被解释为晚点比例,但本仓库明确实现的是 DepDelay > 0 的次数。
6.故障定位清单
- 工作节点无法加入:先测试管理节点的传输控制协议(TCP)端口
2377。课程材料建议允许 2377。覆盖网络还需要 7946 和 4789。
- Drillbit退出:运行
docker ps --filter name=hadoop-drillbit 和 docker logs hadoop-drillbit-1。
- Drill内存不足:课程示例用
DRILL_HEAP=512M DRILL_MAX_DIRECT_MEMORY=1G 降低限制后启动。
- ZooKeeper异常:运行
docker ps --filter name=hadoop-zookeeper 与相应 docker logs。
可验证推导:宿主机端口映射冲突和容器内部服务端口属于不同问题。在同一台主机运行多个Drillbit时,必须使用不同的宿主机端口。不同主机可以使用相同的宿主机端口号。
第6次实验:SPD、LU与Cholesky分解
1.为什么分解,不直接求逆
课程原文:数值线性代数实现通过矩阵分解求解线性系统 Ax = b,不显式计算 A−1。先做分解的代价是 O(n3);同一 A 对多个右端项求解时,每次三角求解只需 O(n2)。
若 A = LU,则先解 Ly = b(前代),再解 Ux = y(回代)。Doolittle约定 L 的对角线全为1,从而固定LU缩放自由度。
2. SPD的判定与反例
定义:实矩阵 A 是对称正定矩阵(symmetric positive definite,SPD),当且仅当:
A = AT,并且对所有 x ≠ 0,xTAx > 0。
课程原文:对实对称矩阵,以下两种方法可检查正定性:
- 全部特征值严格大于0;
- Sylvester判据:从左上角开始的所有顺序主子式严格大于0。
可验证推导:若 A = LLT 且 L 可逆,则对 x ≠ 0,
xTAx = xTLLTx = ‖LTx‖22 > 0。
因此正对角的非奇异Cholesky因子直接证明 A 正定。
需要区分:
- A = AT 只证明对称,不能证明正定。例如
diag(1, -1) 对称但不定。
- 所有对角元素为正也不够。例如 [[1,2],[2,1]] 的行列式为 −3,不正定。
- ATA 总是半正定;只有当 A 满列秩时才正定。
3.从Doolittle到LDLᵀ,再到Cholesky
课程原文与可验证推导:以下等式逐步给出Cholesky的来源。
- 从Doolittle分解开始:A = LU,其中 L 为单位下三角,U 为上三角。
- 因为 A 对称,LU = A = AT = UTLT。
- 把 U 的对角抽出:U = DÛ,其中 D = diag(U11,…,Unn),Û 为单位上三角。
- 于是 LDÛ = ÛTDLT。Doolittle分解的唯一性给出 L = ÛT。
- 因此 A = LDLT。
- 若 A 为正定,则每个枢轴 Dii > 0,实数平方根 D1/2 存在。
- 令 C = LD1/2,得到 A = CCT。后续公式把 C 重新记作 L。
需要区分:某些非正定对称矩阵仍可能存在 LDLT 分解。只有从 D 取实平方根并得到标准Cholesky分解时,才需要正定条件。
4.从矩阵乘法推导元素公式
由 A = LLT,对任意 i,j:
aij = Σk=1min(i,j) likljk。
先计算第 j 列的对角元。再计算该对角元下方的元素:
ljj = √(ajj − Σk=1j−1ljk2)
lij = (aij − Σk=1j−1likljk) / ljj,其中 i > j。
def cholesky(A):
n = len(A)
L = [[0.0] * n for _ in range(n)]
for j in range(n):
diagonal_residual = A[j][j] - sum(L[j][k] ** 2 for k in range(j))
if diagonal_residual <= 0:
raise ValueError("matrix is not SPD")
L[j][j] = diagonal_residual ** 0.5
for i in range(j + 1, n):
cross = sum(L[i][k] * L[j][k] for k in range(j))
L[i][j] = (A[i][j] - cross) / L[j][j]
return L
可验证推导:循环不变量如下:开始计算第 j 列之前,L 的前 j−1 列已经满足对应的 LLT 元素等式。对角残差必须严格大于0。
5. 3×3完整手算
课程原文:
A = [[25,15,−5],[15,18,0],[−5,0,11]]
- l11 = √25 = 5
- l21 = 15/5 = 3,l31 = −5/5 = −1
- l22 = √(18−32) = 3
- l32 = (0−(−1)(3))/3 = 1
- l33 = √(11−(−1)2−12) = 3
L = [[5,0,0],[3,3,0],[−1,1,3]]
验算:第 (2,3) 项为 3(−1)+3(1)+0(3)=0;第 (3,3) 项为 (−1)2+12+32=11。故 LLT=A。
同一分解如何解方程
可验证推导:若取 b=[35,33,6]T,则先解 Ly=b:
- 5y1=35 ⇒ y1=7
- 3y1+3y2=33 ⇒ y2=4
- −y1+y2+3y3=6 ⇒ y3=3
再解 LTx=y,得到 x=[1,1,1]T。直接乘 Ax 可验证结果。
6.复杂度与适用范围
| 性质 | LU(Doolittle) | Cholesky |
| 适用矩阵 | 任意可逆方阵 | SPD矩阵 |
| 主导浮点运算 | 约 (2/3)n³ | 约 (1/3)n³ |
| 存储 | L与U | 只存L,另一半为Lᵀ |
| 行交换 | 稠密数值实现采用部分主元法,以降低小主元带来的误差放大 | SPD情形不需要主元交换 |
课程原文:Cholesky的主导运算量约为稠密LU的一半。它只计算一个三角因子,并利用对称性省去另一半。
第7次实验:Scala、Spark、PCA与L-BFGS
1. Scala基本语法
课程原文:Scala是静态类型、多范式语言,可与Java互操作,同时支持面向对象和函数式编程。
val fixed: Int = 10 // 不可重新赋值
var mutable: String = "Hello" // 可以重新赋值
mutable = "World"
def add(a: Int, b: Int): Int = a + b
val doubled = List(1, 2, 3).map(_ * 2)
val total = List(1, 2, 3).reduce(_ + _)
case class Point(x: Int, y: Int)
val sameValue = Point(1, 2) == Point(1, 2) // true
val description = fixed match {
case 1 => "one"
case _ => "other"
}
| 结构 | 含义 | 陷阱 |
val | 绑定不可重新赋值 | 对象内部是否可变取决于对象类型。 |
var | 绑定可以重新赋值 | 分布式闭包中依赖共享可变变量会产生错误直觉。 |
trait | 可混入的接口与实现 | 类用 extends A with B。 |
case class | 自动提供值相等、模式匹配等便利 | == 比较结构值,不等同Java引用比较。 |
map | 逐元素变换并保留结构 | 它不是键值表 Map 的专属操作。 |
object HelloWorld {
def main(args: Array[String]): Unit = {
println("Hello, world!")
}
}
scalac HelloWorld.scala
scala HelloWorld
2. Spark:驱动程序、RDD与DataFrame
课程原文:每个Spark应用都有驱动程序(driver program)。驱动程序运行用户的 main 方法,并组织集群上的并行操作。弹性分布式数据集(RDD)是可容错、分区的分布式元素集合。DataFrame是带模式的分布式表。Spark SQL使用Catalyst优化器处理DataFrame查询。
val textFile = spark.sparkContext.textFile("path/to/text.txt")
val wordCounts = textFile
.flatMap(line => line.split(" "))
.map(word => (word, 1))
.reduceByKey(_ + _)
wordCounts.collect().foreach(println)
可验证推导:map 和 flatMap 等转换操作建立计算血缘。collect 等动作触发执行。collect 把全部结果传回驱动程序,所以只适用于能够装入驱动程序内存的结果。
需要区分:当聚合函数满足结合律并可在映射端合并时,reduceByKey 会在混洗前执行本地合并,因此比先执行 groupByKey 再求和传输更少的数据。
3. PBMC数据预处理
课程原文:Lab 7使用预处理后的PBMC mRNA表。数据有1882个特征;目标是用PCA降维,并用细胞类型标签做分类实验。
spark_df = (
spark.read
.options(header="true", inferSchema="true")
.csv("datasets/PBMC_16k_RNA.csv")
)
spark_df.agg({"KLHL17": "max"}).show()
spark_df.createOrReplaceTempView("data")
spark.sql("SELECT max(KLHL17) FROM data").show()
课程原文:KLHL17 的范围约为 −0.11到10,HES4 的范围约为 −1.33到9.47。课程答案认为两者范围相近,可以跳过标准化,也可以为了稳妥执行标准化。
可验证推导:仅检查两个特征不足以证明全部1882个特征尺度相近。PCA对尺度敏感;严谨流程应统计所有输入列,或在含义允许时使用 StandardScaler。
from pyspark.ml.feature import VectorAssembler, StandardScaler
feature_cols = [c for c in spark_df.columns if c != "index"]
assembled = VectorAssembler(
inputCols=feature_cols,
outputCol="features"
).transform(spark_df)
scaler = StandardScaler(
inputCol="features",
outputCol="standardized_features",
withMean=True,
withStd=True
)
standardized_df = scaler.fit(assembled).transform(assembled)
未给定:源材料没有提供完整列清单,也没有确认索引列的精确名称。上例的 "index" 是占位名称。实际执行前必须检查数据模式,再确定需要排除的列。
4. PCA:目标、公式与课程结果
可验证推导:对中心化数据矩阵 X,协方差矩阵可写为 S = XTX/(n−1)。第一主成分方向 w1 解约束优化:
拉格朗日条件给出 Sw = λw,因此主成分是协方差矩阵按特征值从大到小排列的特征向量。第 j 个解释方差比为 λj/Σλi。
from pyspark.ml.feature import PCA
pca = PCA(
k=2,
inputCol="standardized_features",
outputCol="pca_features"
)
pca_model = pca.fit(standardized_df)
pca_df = pca_model.transform(standardized_df)
print(pca_model.explainedVariance.toArray())
课程原文:前两个解释方差比分别约为 0.02942326 与 0.01307317,合计约 0.04249643,即4.25%。课程答案随后写“2% 的方差”;这与前述两个数值之和不一致。复习时应优先按给定数值求和,并指出原文表述冲突。
课程原文:在本实验按标签着色的前两个主成分散点图中,同类细胞形成聚集区域;随机选择两个原始列时,同类细胞更分散。该对比说明PCA可用于本数据集的低维可视化。
需要区分:较低的解释方差不表示PCA计算错误。它只说明二维投影丢失了大部分总方差。分类效果还取决于被丢弃的方向是否包含分类信息。
5.逻辑回归、BFGS与L-BFGS
课程原文:第7次实验把样本分为A类和非A类,并要求使用两个主成分训练逻辑回归。课程计数结果显示 CD4+ T 数量最多,共5262个样本,因此把它定义为A类。训练集和测试集按70% 和30% 划分。
可验证推导:二分类逻辑回归令
p(y=1|x)=σ(z)=1/(1+e−z),其中 z=wTx+b。
J(w,b)=−Σ[y log p +(1−y)log(1−p)]。
∇wJ = XT(p−y)(忽略平均因子与正则项)。
- 梯度下降:只用一阶梯度,沿负梯度方向更新。
- Newton方法:使用Hessian矩阵求解步长。高维问题的存储和求解成本较高。
- BFGS方法:使用梯度差和步长更新逆Hessian近似,不显式计算真实Hessian矩阵。
- L-BFGS方法:只保留最近有限次 (sk,yk) 历史,以降低高维问题的内存成本。
from pyspark.mllib.classification import LogisticRegressionWithLBFGS
from pyspark.mllib.linalg import Vectors as OldVectors
from pyspark.mllib.regression import LabeledPoint
# 前置条件:classified_df 已含数值 label 与 ML Vector 类型的 pca_features
labeled_points = classified_df.select("label", "pca_features").rdd.map(
lambda row: LabeledPoint(
float(row["label"]),
OldVectors.dense(row["pca_features"].toArray())
)
)
training, test = labeled_points.randomSplit([0.7, 0.3], seed=42)
model = LogisticRegressionWithLBFGS.train(
training,
iterations=100,
numClasses=2
)
prediction_and_label = test.map(
lambda point: (model.predict(point.features), point.label)
)
test_error = prediction_and_label.filter(
lambda pair: pair[0] != pair[1]
).count() / float(test.count())
课程原文:一次实验在100次迭代后得到25.59% 测试错误率。课程答案认为二维图上 CD4+ T 与其他类型很接近,并建议加入更多主成分。
未给定:25.59% 不是可复现的固定标准答案。源材料没有给出随机种子、精确预处理、特征组装、正则化参数和数据版本。结果应作为一次实验观测,而不是算法常数。
改进和验证步骤
- 在验证集上选择主成分数 k。不要只比较训练误差。
- 固定随机种子。报告训练样本量和测试样本量。
- 标准化特征。只在训练集上拟合缩放器和PCA,以防止测试信息泄漏。
- 调节正则化强度和迭代次数。检查目标函数是否收敛。
- 类别不平衡时,同时报告精确率、召回率、F1值、受试者工作特征曲线下面积(ROC-AUC)和混淆矩阵。不要只报告准确率。
- 如果二维线性边界不足,则增加主成分数,或比较非线性模型。
需要区分:先使用全部数据拟合PCA,再切分训练集和测试集,会使测试集参与特征空间学习。PCA不使用标签,但该做法仍然造成数据泄漏。
综合自测与答案
先独立作答,再展开答案。题目同时检查事实记忆和推导能力。
1.镜像、容器、Swarm节点与Drillbit分别是什么?
答案:镜像是静态分层模板;容器是镜像的运行实例;Swarm节点是加入同一Swarm的Docker主机;Drillbit是Drill的查询执行服务。一个Swarm节点可以运行多个容器,也可以运行多个Drillbit,但同一主机的宿主机端口映射不能冲突。
2.工作节点能ping管理节点,但 docker node ls 不显示工作节点。下一步检查什么?
答案:检查工作节点是否使用正确的令牌和管理节点地址执行加入操作。测试管理节点的TCP端口2377。检查防火墙。ping 成功只证明网际协议(IP)层可达,不证明Swarm控制端口可达。
3.为什么Drill查询CSV时常需要 CAST?
答案:读取时确定模式允许直接读取文本,但原始 columns[n] 可能按字符串解释。数值聚合、比较和排序需要显式转换为 INT、DOUBLE 等类型。
4. NameNode、ResourceManager与NodeManager有什么区别?
答案:NameNode管HDFS命名空间与块元数据;ResourceManager在集群范围分配YARN资源;NodeManager管单个节点的本地资源并运行任务容器。它们属于不同层。
5.证明 [[4,2],[2,3]] 为SPD,并求Cholesky因子。
答案:矩阵对称;顺序主子式为4与 4·3−2²=8,均大于0,所以按Sylvester判据为SPD。l11=2,l21=1,l22=√(3−1)=√2。故 L=[[2,0],[1,√2]]。
6. Cholesky为什么约为LU一半运算量?
答案:SPD的对称性使上三角信息等于下三角因子的转置。Cholesky只更新一个三角区域,主导运算约为 n³/3;稠密LU同时形成两个三角因子,约为 2n³/3。
7.若计算 ljj 时根号内为0或负数,说明什么?
答案:对标准、无主元的实Cholesky而言,输入不满足严格SPD,或浮点误差已使数值结果失去正定性。0对应非严格正定/奇异边界;负值不能产生实正对角因子。
8. val xs = List(1,2,3); xs.map(_*2) 是否修改 xs?
答案:不会。List 是不可变集合,map 返回新列表 List(2,4,6);xs 仍指向原列表。
9. PCA前两个解释方差比为0.02942326和0.01307317。合计是多少?
答案:0.04249643,约4.25%。课程材料中的“2%”与这两个给定数之和冲突,作答时应展示加法并指出冲突。
10. PCA为什么可能改善可视化,却使分类效果不理想?
答案:PCA保留总方差最大的无监督方向,不使用类别标签。二维投影可能呈现宏观聚类,但判别类别所需的小方差方向可能被丢弃;线性逻辑回归还只能学习线性边界。
11.为什么L-BFGS比完整BFGS更适合高维问题?
答案:完整BFGS维护一个 d×d 稠密逆Hessian近似,内存为 O(d²)。L-BFGS只保存最近 m 组步长和梯度差,内存约为 O(md),其中 m 远小于维度 d。
12. 25.59% 测试错误率能否视为Lab 7的固定答案?
答案:不能。它是课程材料记录的一次实验结果。缺少随机种子、完整预处理、参数与数据版本时,结果不具唯一可复现性。报告时应写成“课程示例观测”,并附实验配置。