任务7. 多节点任务处理

为了充分利用集群的运算性能,我们需要将资源分配至各个节点、协调各个节点的任务、整合多个结果等等。接下来我们来控制命令在多个主机上协同运行。目前集群节点有thumm01, thumm03~thumm05。

集群主机之间免密登录配置

请你用 linux shell 写一个脚本auto_autho.sh,实现各节点之间的免密登录。即实现thumm01分别到thumm03~thumm05的免密登录,使得运行该脚本后,可以通过ssh thumm0X从thumm01免密登录到X号节点。
思路:这个脚本做的事情是在thumm01上生成2个节点的公钥和私钥,然后把所有公钥加入到authorized_keys中,然后把各自的公钥私钥以及authorized_keys分发到各个节点。

auto_autho.sh

方便进行后续实验,在实现thumm01免密登录thumm05和thumm06,同时实现thumm05到thumm06的免密登录。

本脚本在第一次运行时需要确认连接(在查看ECDSA key fingerprint 后输入"yes")并输入口令,实验中服务器上没有安装expect,也没有权限安装,否则可以写成免交互脚本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#!/bin/bash

username="xxxxxxxxxx"
avaiable_servers=("thumm05" "thumm06")

#============= 让thumm01能登录05和06=================

# 生成密钥对
[ ! -f ~/.ssh/id_rsa.pub ] && ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa

# 发送公钥
for host in ${avaiable_servers[@]}
do
ssh-copy-id -i ~/.ssh/id_rsa.pub $username@$host
done

# 测试
for host in ${avaiable_servers[@]}
do
ssh $username@$host "echo thumm01免密登录 $host 登录成功"
done

# ============= 让thumm05和06能登录01=================
# 在另一个文件夹中创建另一队不对称密钥, thumm01保存公钥, thumm05和06存私钥
another_ssh_filedir=".ssh_extra"
if [ ! -d $another_ssh_filedir ]
then
mkdir -p $another_ssh_filedir
fi

if [ ! -f $another_ssh_filedir/id_rsa.pub ]
then
ssh-keygen -t rsa -P '' -f $another_ssh_filedir/id_rsa
cat $another_ssh_filedir/id_rsa.pub >> ~/.ssh/authorized_keys
fi

for host in ${avaiable_servers[@]}
do
scp $another_ssh_filedir/id_rsa $username@$host:~/.ssh
done

for host in ${avaiable_servers[@]}
do
ssh $username@$host "
ssh $username@thumm01 'echo $host免密登录thummm01成功'
"
done

公私钥这样发送非常不安全,简便起见,这里不考虑安全性了(

执行

需要在thumm05、thumm06上登录thumm01,建立起认证(回答yes)后该脚本才能顺利执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
xxxxxxxxxx@thumm01:~$ bash auto_autho.sh 
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/home/dsjxtjc/xxxxxxxxxx/.ssh/id_rsa.pub"
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed

/usr/bin/ssh-copy-id: WARNING: All keys were skipped because they already exist on the remote system.
(if you think this is a mistake, you may want to use -f option)

/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/home/dsjxtjc/xxxxxxxxxx/.ssh/id_rsa.pub"
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed

/usr/bin/ssh-copy-id: WARNING: All keys were skipped because they already exist on the remote system.
(if you think this is a mistake, you may want to use -f option)

thumm01免密登录 thumm05 登录成功
thumm01免密登录 thumm06 登录成功
id_rsa 100% 1675 1.6KB/s 00:00
id_rsa 100% 1675 1.6KB/s 00:00
thumm05免密登录thummm01成功
thumm06免密登录thummm01成功
xxxxxxxxxx@thumm01:~$

多结点任务处理

请仿照 wc_dataset.txt,制作2G左右的数据集(比如将 wc_dataset.txt 重复拼接)。在多主机运行一个简单的词频统计任务并汇总(即每个单词出现多少次),对比单机处理和多机处理的差异,可以包括任务执行结果、延迟等方面。

随机生成数据集

统计 wc_dataset.txt 中单词后再一个个随机生成的速度太慢,这里还是采用分块后随机选择分块拼凑更高效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/bin/bash

touch generated_dataset.txt
target_size=`expr 1024 \* 1024 \* 150` # 其实更推荐 $(( 1024 * 1024 * 150 ))
# 不要使用 wc -l wc_dataset.txt: 返回的是"2683500 wc_dataset.tx"
total_lines=$(cat wc_dataset.txt | wc -l)

# 太慢了: 10min生成14k数据
# # 生成大概150M的数据
# while (( $(ls -l generated_dataset.txt | awk '{print $5}') < $target_size ))
# do
# # $RANDOM 生成 [0, 32767] 的随机数
# let rand_line=100*$RANDOM
# # rand_line=$(( $RANDOM % $total_lines )) # 虽然total_lines大于32767, 但也不管了
# word_to_add=$(sed -n ${rand_line, rand_line+10000}p wc_dataset.txt) # 求出目标行数
# (echo $word_to_add) >> generated_dataset.txt
# done

split -n l/10 -d --additional-suffix=.txt wc_dataset.txt -a 2 x
while (( $(ls -l generated_dataset.txt | awk '{print $5}') < $target_size ))
do
cat x0$(( $RANDOM % 10)).txt >> generated_dataset.txt
done

rm x*

split 用法

1
2
3
4
5
6
7
8
split -n l/10 -d --additional-suffix=.txt wc_dataset.txt -a 2 x
split [OPTION]... [FILE [PREFIX]]
-l, --lines=NUMBER
put NUMBER lines/records per output file
l/N split into N files without splitting lines/records
-d use numeric suffixes starting at 0, not alphabetic
-a, --suffix-length=N
generate suffixes of length N (default 2)

单点处理

1
2
3
4
#!/bin/bash

filename="wc_dataset.txt"
cat $filename | tr -cs "[a-z][A-Z]" "\n" | tr A-Z a-z | awk '{ for(j=1;j<=NF;j++){count[$j]++} } END { for(k in count){print k" " count[k]} }'| sort -rnk2 > res_single.txt

tr 用法

1
2
3
4
5
6
7
tr -cs "[a-z][A-Z]" "\n"  # 将非大小写字母都转化为换行符
-c, -C, --complement
use the complement of SET1 # complement: 补集
-s, --squeeze-repeats
replace each sequence of a repeated character that is listed in the last specified SET, with a single occurrence of that character

# Translation occurs if -d is not given and both SET1 and SET2 appear. # -d表示删除

sort 用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sort -rnk2  # 根据第2个字段的数字大小进行逆序排序
-r, --reverse
reverse the result of comparisons
-n, --numeric-sort
compare according to string numerical value
-k, --key=KEYDEF
sort via a key; KEYDEF gives location and type

KEYDEF is F[.C][OPTS][,F[.C][OPTS]] for start and stop position, where F is a field
number and C a character position in the field; both are origin 1, and the stop po‐
sition defaults to the line's end. If neither -t nor -b is in effect, characters in
a field are counted from the beginning of the preceding whitespace. OPTS is one or
more single-letter ordering options [bdfgiMhnRrV], which override global ordering
options for that key. If no key is given, use the entire line as the key. Use
--debug to diagnose incorrect key usage.

多点处理

代码如下,对于thumm01来说,不要把其本身的数据处理过程放到循环中,这会带来较大的性能损失。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/bin/bash

filename="generated_dataset.txt"
username="xxxxxxxxxx"
avaiable_servers=("thumm05" "thumm06")

rm -rf mr*
rm x0*
mkdir mr

split -n l/3 -d --additional-suffix=.txt $filename

idx=0
for host in ${avaiable_servers[@]}
do
ssh $username@$host "
scp $username@thumm01:~/x0$idx.txt .
cat x0$idx.txt | tr -cs '[a-z][A-Z]' '\n' | tr A-Z a-z | sort | uniq -c | awk '{print \$2,\$1}' > mr-tmp-$idx
scp mr-tmp-$idx $username@thumm01:~/mr/
rm x0$idx.txt
rm mr-tmp-$idx
" &
((idx++))
done

cat x0$idx.txt | tr -cs '[a-z][A-Z]' '\n' | tr A-Z a-z | sort | uniq -c | awk '{print $2,$1}' > mr/mr-tmp-$idx

wait

cat mr/mr-tmp* | awk '{if($1 in count) count[$1]=count[$1] + $2; else count[$1]=$2;} END {for(k in count){print k" "count[k]}}' | sort -rnk 2 > res_multi.txt

uniq 用法

1
2
3
4
5
6
7
8
9
10
11
uniq -c  # 去除重复元素, 并使用出现次数来作为前缀
-c, --count prefix lines by the number of occurrences

# 注意一定要先排序:
(base) ➜ ~ echo "hello
dquote> hello
dquote> world
dquote> hello" | uniq -c
2 hello
1 world
1 hello

结果对比

正确性

服务器上未提供md5sha256等hash计算工具,只能肉眼大致看看,应该没问题。

效率

多点计算快一点,但是不多。

1
2
3
4
5
6
7
8
9
10
11
12
xxxxxxxxxx@thumm01:~$ time bash single_node_process.sh 

real 0m14.098s
user 0m16.420s
sys 0m1.968s

xxxxxxxxxx@thumm01:~$ time bash multi_nodes_process.sh

real 0m13.497s
user 0m12.096s
sys 0m0.648s
xxxxxxxxxx@thumm01:~$

Bonus

尝试提出一种可以加快多节点处理速度的方法并验证。需要注意的是,由于各节点间带宽存在实时波动,请在验证中论证“提出的方法不是因为网络波动带来的虚假数值增益”。

可以在每一台机器上通过多进程并行计算的方式加快处理速度。

通过 lscpu 等命令可以看到服务器使用24核cpu,不过这里我们只在每台服务器上创建两个线程来处理。

bonus.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#!/bin/bash

filename="generated_dataset.txt"
username="xxxxxxxxxx"
avaiable_servers=("thumm05" "thumm06")

rm -rf mr*
mkdir mr

split -n l/3 -d --additional-suffix=.txt $filename

idx=0
# 总结-括号嵌套问题:https://zhuanlan.zhihu.com/p/126262733
for host in ${avaiable_servers[@]}
do
ssh $username@$host " # 双引号仍能解析其中的变量
scp $username@thumm01:~/x0$idx.txt .;
split -n l/2 -d --additional-suffix=.txt x0$idx.txt -a 2 y;
for ((process=0;process<2;process++));
do
cat y0\${process}.txt | tr -cs '[a-z][A-Z]' '\n' | tr A-Z a-z | awk '{ for(j=1;j<=NF;j++){count[\$j]++} } END { for(k in count){print k\" \"count[k]} } ' > mr-tmp-$idx-\${process}.txt &
done;
wait;
cat mr-tmp-$idx-* | awk '{if(\$1 in count) count[\$1]=count[\$1] + \$2; else count[\$1]=\$2;} END {for(k in count){print k\" \"count[k]}}' | sort -rnk 2 > mr-tmp-$idx.txt;
scp mr-tmp-$idx.txt $username@thumm01:~/mr/;
rm x0$idx.txt;
rm y0*;
rm mr-tmp-$idx*.txt;
" &
((idx++))
done

split -n l/2 -d --additional-suffix=.txt x0$idx.txt -a 2 y
for((process=0;process<2;process++))
do
cat y0${process}.txt | tr -cs '[a-z][A-Z]' '\n' | tr A-Z a-z | awk '{ for(j=1;j<=NF;j++){count[$j]++} } END { for(k in count){print k" "count[k]} } ' > mr/mr-tmp-$idx-${process}.txt &
done

wait
cat mr/mr-tmp-$idx-* | awk '{if($1 in count) count[$1]=count[$1] + $2; else count[$1]=$2;} END {for(k in count){print k" "count[k]}}' | sort -rnk 2 > mr/mr-tmp-$idx.txt

rm mr/mr-tmp-$idx-*

cat mr/mr-tmp* | awk '{if($1 in count) count[$1]=count[$1] + $2; else count[$1]=$2;} END {for(k in count){print k" "count[k]}}' | sort -rnk 2 > res_bonus.txt

rm x0*
rm y0*

wait 会阻塞等待所有子进程运行结束。

结果对比

运行结果均保持一致。通过终端运行信息可以看到,bash bonus.sh 的运行时间比 bash multi_nodes_process.sh 短一半。可以通过创建更多的进程进一步提升执行速度,并通过试验找到效率最高的设置,但这里本实验就不继续尝试了。

问题记录

Q1: ssh commands变量转义问题

不明白这样写为啥m和inter_idx打印不出来,但是通过cat commands.sh | ssh host bash的方式可以打印

ssh命令使用不当,具体来说如果不对变量进行转义,会被bash解析后通过ssh传到服务器上执行,而很明显服务器上是没有此变量的。参考博客

Q2: 引号配对问题

image-20231015201645413

Shell引号配对较复杂,需理解其规则,建议阅读Shell 引号嵌套

简单来说:

1、在使用多重引号时系统是从前往后看的,能匹配就算一对,所以这样一对一对的断句将整个命令串分为若干部分;
2、为了使系统识别后(被识别的引号会消失)的命令串功能正确,需要使用转义字符手动的在合适的地方加入合适的引号;

1
2
xxxxxxxxxx@thumm01:/mnt/data/dsjxtjc/xxxxxxxxxx$ echo "'""'"
''

这里具体配对过程如上图所示,把中间的引号进行转义即可。

Q3: ssh commands 执行流程 + sh/bash差异

这里将 thumm01 的处理也放入循环中(这样很不优雅),代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
split -n l/3 -d --additional-suffix=.txt $output
idx=0
for node in ${nodes[@]}
do
ssh 2023214399@$node "
scp thumm01:~/x0$idx.txt .;
split -n l/3 -d --additional-suffix=.txt x0$idx.txt y
for inter_idx in \$(seq 0 2)
do
cat y0\${inter_idx}.txt | tr -cs '[a-z][A-Z]' '\n' | tr 'A-Z' 'a-z' | sort | uniq -c | awk '{print \$2,\$1}' > inter-process-\${inter_idx} &
done;
wait;
cat inter-process-* | awk '{if(\$1 in count) count[\$1]=count[\$1] + \$2; else count[\$1]=\$2;} END {for(k in count){print k\" \"count[k]}}' | sort -rnk 2 > tmp-$idx.txt;
scp tmp-$idx.txt thumm01:~/;
rm x0$idx.txt;
rm y0*;
rm inter-process*;
" &
idx=$(( $idx + 1 ))
done
wait
cat tmp* | awk '{if($1 in word_array) word_array[$1]=word_array[$1] + $2; else word_array[$1]=$2;} END {for(word in word_array){print word" "word_array[word]}}' | sort -rnk 2 > word_frequency_bonus.txt

bash bouns.sh运行此程序会报错"sh: 4: Syntax error: Bad for loop variable"。奇怪的是为啥我们明明是使用bash运行,却报sh的错误,而且行号如此奇怪。

为了解决这个问题,我们需要对ssh commands执行流程有更深入的认识。这条命令会将 commands 以字符串的方式传给服务器,服务器会使用默认shell创建一个解析器进程进行处理。thumm01 默认shell为 sh,而thumm05和06 默认shell为 bash。而 sh 不支持 for (()) 的语法,所以只有 thumm01 上会报错,保存行号为字符串中这条for 命令的所在行号。

那在for (()) 加上一个bash行不行呢?
答案是不行的,因为 bash 命令会创建一个新的 bash 解析器进程,而剩余的命令还是在 sh 解析器中解析。一种正确的改正方法是使用 echo "command" | bash(又会涉及到shell烦人的引号配对问题), 不过还是只用兼容性更好的for i in $(seq 0 2)