Linux 指令是由很多顶级程序员共同设计的,使用 Linux 指令解决问题的过程,就好像在体验一款优秀的产品。每次通过查资料使用 Linux 指令解决问题后,都会让我感到收获满满。在这个过程中,我不仅学会了一条指令,还从中体会到了软件设计的魅力:彼此独立,又互成一体。这就像每个 Linux 指令一样,专注、高效。回想起来,在我第一次看到管道、第一次使用 awk、第一次使用 sort,都曾有过这种感受。
通过前面的学习,相信你已经掌握了一些基础指令的使用方法,今天我们继续挑战一个更复杂的问题——用 Linux 指令管理一个集群。这属于 Linux 指令的高级技巧,所谓高级技巧并不是我们要学习更多的指令,而是要把之前所学的指令进行排列组合。当你从最初只能写几条指令、执行然后看结果,成长到具备书写一个拥有几十行、甚至上百行的 bash 脚本的能力时,就意味着你具备了解决复杂问题的能力。而最终的目标,是提升你对指令的熟练程度,锻炼工程能力。
本课时,我将带你朝着这个目标努力,通过把简单的指令组合起来,分层组织成最终的多个脚本文件,解决一个复杂的工程问题:在成百上千的集群中安装一个 Java 环境。接下来,请你带着这个目标,开启今天的学习。
第一步我们先搭建一个学习用的集群。这里简化一下模型。我在自己的电脑上装一个ubuntu
桌面版的虚拟机,然后再装两个ubuntu
服务器版的虚拟机。
相对于桌面版,服务器版对资源的消耗会少很多。我将教学材料中桌面版的ubuntu
命名为u1
,两个用来被管理的服务器版ubuntu
叫作v1
和v2
。
用桌面版的原因是:我喜欢ubuntu
漂亮的开源字体,这样会让我在给你准备素材的时候拥有一个好心情。如果你对此感兴趣,可以搜索ubuntu mono
,尝试把这个字体安装到自己的文本编辑器中。不过我还是觉得在ubuntu
中敲代码更有感觉。
注意,我在这里只用了 3 台服务器,但是接下来我们要写的脚本是可以在很多台服务器之间复用的。
你可以想象一个局域网中有很多服务器需要管理,它们彼此之间网络互通,我们通过一台主服务器对它们进行操作,即通过u1
操作v1
和v2
。
在主服务器上我们维护一个ip
地址的列表,保存成一个文件,如下图所示:
目前iplist
中只有两项,但是如果我们有足够的机器,可以在里面放成百上千项。接下来,请你思考shell
如何遍历这些ip
?
你可以先尝试实现一个最简单的程序,从文件iplist
中读出这些ip
并尝试用for
循环遍历这些ip
,具体程序如下:
1 | #!/usr/bin/bash |
首行的#!
叫作 Shebang。Linux 的程序加载器会分析 Shebang 的内容,决定执行脚本的程序。这里我们希望用bash
来执行这段程序,因为我们用到的 readarray 指令是bash 4.0
后才增加的能力。
readarray
指令将 iplist 文件中的每一行读取到变量ips
中。ips
是一个数组,可以用echo ${ips[@]}
打印其中全部的内容:@
代表取数组中的全部内容;$
符号是一个求值符号。不带$
的话,ips[@]
会被认为是一个字符串,而不是表达式。
for
循环遍历数组中的每个ip
地址,echo
把地址打印到屏幕上。
如果用shell
执行上面的程序会报错,因为readarray
是bash 4.0
后支持的能力,因此我们用chomd
为foreach.sh
增加执行权限,然后直接利用shebang
的能力用bash
执行,如下图所示:
为了方便集群管理,通常使用统一的用户名管理集群。这个账号在所有的集群中都需要保持命名一致。比如这个集群账号的名字就叫作lagou
。
接下来我们探索一下如何创建这个账户lagou
,如下图所示:
上面我们创建了lagou
账号,然后把lagou
加入sudo
分组。这样lagou
就有了sudo
成为root
的能力,如下图所示:
接下来,我们设置lagou
用户的初始化shell
是bash
,如下图所示:
这个时候如果使用命令su lagou
,可以切换到lagou
账号,但是你会发现命令行没有了颜色。因此我们可以将原来用户下面的.bashrc
文件拷贝到/home/lagou
目录下,如下图所示:
这样,我们就把一些自己平时用的设置拷贝了过去,包括终端颜色的设置。.bashrc
是启动bash
的时候会默认执行的一个脚本文件。
接下来,我们编辑一下/etc/sudoers
文件,增加一行lagou ALL=(ALL) NOPASSWD:ALL
表示lagou
账号 sudo 时可以免去密码输入环节,如下图所示:
我们可以把上面的完整过程整理成指令文件,create_lagou.sh
:
1 | sudo useradd -m -d /home/lagou lagou |
你可以删除用户lagou
,并清理/etc/sudoers
文件最后一行。用指令userdel lagou
删除账户,然后执行create_lagou.sh
重新创建回lagou
账户。如果发现结果一致,就代表create_lagou.sh
功能没有问题。
最后我们想在v1``v2
上都执行create_logou.sh
这个脚本。但是你不要忘记,我们的目标是让程序在成百上千台机器上传播,因此还需要一个脚本将create_lagou.sh
拷贝到需要执行的机器上去。
这里,可以对foreach.sh
稍做修改,然后分发create_lagou.sh
文件。
foreach.sh
1 | #!/usr/bin/bash |
这里,我们在循环中用scp
进行文件拷贝,然后分别去每台机器上执行create_lagou.sh
。
如果你的机器非常多,上述过程会变得非常烦琐。你可以先带着这个问题学习下面的Step 4
,然后再返回来重新思考这个问题,当然你也可以远程执行脚本。另外,还有一个叫作sshpass
的工具,可以帮你把密码传递给要远程执行的指令,如果你对这块内容感兴趣,可以自己研究下这个工具。
接下来我们需要打通从主服务器到v1
和v2
的权限。当然也可以每次都用ssh
输入用户名密码的方式登录,但这并不是长久之计。 如果我们有成百上千台服务器,输入用户名密码就成为一件繁重的工作。
这时候,你可以考虑利用主服务器的公钥在各个服务器间登录,避免输入密码。接下来我们聊聊具体的操作步骤:
首先,需要在u1
上用ssh-keygen
生成一个公私钥对,然后把公钥写入需要管理的每一台机器的authorized_keys
文件中。如下图所示:我们使用ssh-keygen
在主服务器u1
中生成公私钥对。
然后使用mkdir \-p
创建~/.ssh
目录,-p
的优势是当目录不存在时,才需要创建,且不会报错。~
代表当前家目录。 如果文件和目录名前面带有一个.
,就代表该文件或目录是一个需要隐藏的文件。平时用ls
的时候,并不会查看到该文件,通常这种文件拥有特别的含义,比如~/.ssh
目录下是对ssh
的配置。
我们用cd
切换到.ssh
目录,然后执行ssh-keygen
。这样会在~/.ssh
目录中生成两个文件,id_rsa.pub
公钥文件和is_rsa
私钥文件。 如下图所示:
可以看到id_rsa.pub
文件中是加密的字符串,我们可以把这些字符串拷贝到其他机器对应用户的~/.ssh/authorized_keys
文件中,当ssh
登录其他机器的时候,就不用重新输入密码了。 这个传播公钥的能力,可以用一个shell
脚本执行,这里我用transfer_key.sh
实现。
我们修改一下foreach.sh
,并写一个transfer_key.sh
配合foreach.sh
的工作。transfer_key.sh
内容如下:
foreach.sh
1 | #!/usr/bin/bash |
tranfer_key.sh
1 | ip=$1 |
在foreach.sh
中我们执行 transfer_key.sh,并且将 IP 地址通过参数传递过去。在 transfer_key.sh 中,用$1
读出 IP 地址参数, 再将公钥写入变量pubkey
,然后登录到对应的服务器,执行多行指令。用mkdir
指令检查.ssh
目录,如不存在就创建这个目录。最后我们将公钥追加写入目标机器的~/.ssh/authorized_keys
中。
chmod 700
和chmod 600
是因为某些特定的linux
版本需要.ssh
的目录为可读写执行,authorized_keys
文件的权限为只可读写。而为了保证安全性,组用户、所有用户都不可以访问这个文件。
此前,我们执行foreach.sh
需要输入两次密码。完成上述操作后,我们再登录这两台服务器就不需要输入密码了。
接下来,我们尝试一下免密登录,如下图所示:
可以发现,我们登录任何一台机器,都不再需要输入用户名和密码了。
在远程部署 Java 环境之前,我们先单机完成以下 Java 环境的安装,用来收集需要执行的脚本。
在ubuntu
上安装java
环境可以直接用apt
。
我们通过下面几个步骤脚本配置 Java 环境:
1 | sudo apt install openjdk-11-jdk |
经过一番等待我们已经安装好了java
,然后执行下面的脚本确认java
安装。
1 | which java |
根据最小权限原则,执行 Java 程序我们考虑再创建一个用户ujava
。
1 | sudo useradd -m -d /opt/ujava ujava |
这个用户可以不设置密码,因为我们不会真的登录到这个用户下去做任何事情。接下来我们为用户配置 Java 环境变量,如下图所示:
通过两次 ls 追查,可以发现java
可执行文件软连接到/etc/alternatives/java
然后再次软连接到/usr/lib/jvm/java-11-openjdk-amd64
下。
这样我们就可以通过下面的语句设置 JAVA_HOME 环境变量了。
1 | export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64/ |
Linux 的环境变量就好比全局可见的数据,这里我们使用 export 设置JAVA_HOME
环境变量的指向。如果你想看所有的环境变量的指向,可以使用env
指令。
其中有一个环境变量比较重要,就是PATH
。
如上图,我们可以使用shell
查看PATH
的值,PATH
中用:
分割,每一个目录都是linux
查找执行文件的目录。当用户在命令行输入一个命令,Linux 就会在PATH
中寻找对应的执行文件。
当然我们不希望JAVA_HOME
配置后重启一次电脑就消失,因此可以把这个环境变量加入ujava
用户的profile
中。这样只要发生用户登录,就有这个环境变量。
1 | sudo sh -c 'echo "export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64/" >> /opt/ujava/.bash_profile' |
将JAVA_HOME
加入bash_profile
,这样后续远程执行java
指令时就可以使用JAVA_HOME
环境变量了。
最后,我们将上面所有的指令整理起来,形成一个install_java.sh
。
1 | sudo apt -y install openjdk-11-jdk |
apt
后面增了一个-y
是为了让执行过程不弹出确认提示。
终于到了远程安装 Java 环境这一步,我们又需要用到foreach.sh
。为了避免每次修改,你可以考虑允许foreach.sh
带一个文件参数,指定需要远程执行的脚本。
foreach.sh
1 | #!/usr/bin/bash |
改写后的foreach
会读取第一个执行参数作为远程执行的脚本文件。 而bash \-s
会提示使用标准输入流作为命令的输入;< $script
负责将脚本文件内容重定向到远程bash
的标准输入流。
然后我们执行foreach.sh install_java.sh
,机器等待 1 分钟左右,在执行结束后,可以用下面这个脚本检测两个机器中的安装情况。
check.sh
1 | sudo -u ujava -i /bin/bash -c 'echo $JAVA_HOME' |
check.sh
中我们切换到ujava
用户去检查JAVA_HOME
环境变量和 Java 版本。执行的结果如下图所示:
这节课我们所讲的场景是自动化运维的一些皮毛。通过这样的场景练习,我们复习了很多之前学过的 Linux 指令。在尝试用脚本文件构建一个又一个小工具的过程中,可以发现复用很重要。
在工作中,优秀的工程师,总是善于积累和复用,而shell
脚本就是积累和复用的利器。如果你第一次安装java
环境,可以把今天的安装脚本保存在自己的笔记本中,下次再安装就能自动化完成了。除了积累和总结,另一个非常重要的就是你要尝试自己去查资料,包括使用man
工具熟悉各种指令的使用方法,用搜索引擎查阅资料等。
今天我会带你把《模块二:Linux 指令》中涉及的课后练习题,逐一讲解,并给出每个课时练习题的解题思路和答案。
【问题】 搜索文件系统中所有以包含 std
字符串且以.h
扩展名结尾的文件。
【解析】 这道题目比较简单,大家也比较活跃,我自己只写了一种方法,没想到留言中有挺多不错的方案,那我们一起来看下。
下面是我的方案,你学完模块二的内容后,应该知道查看全部文件需要sudo
,以管理员身份:
1 | sudo find / -name "*std*.h" |
我在留言中看到有的同学用的是-iname
,这样也是可以的,只是忽略了大小写。
也可以结合 grep 语句, 用管道实现,比如:
1 | sudo find / -name "*.h" |grep std |
【问题】 请问下面这段 Shell 程序的作用是什么?
1 | mkfifo pipe1 |
【解析】 这个题目是我在网上看到的一个比较有趣的问题。
前 2 行代码创建了两个管道文件。
从第 3 行开始,代码变得复杂。echo \-n run
就是向输出流中写入一个run
字符串(不带回车,所以用-n
)。通过管道,将这个结果传递给了cat
。cat
是 concatenate 的缩写,意思是把文件粘在一起。
cat
用>
重定向输出到一个管道文件时,如果没有其他进程从管道文件中读取内容,cat
会阻塞。cat
用<
读取一个管道内容时,如果管道中没有输入,也会阻塞。从这个角度来看,总共有 3 次重定向:
-
也就是输入流的内容和pipe1
内容合并重定向到pipe2
;pipe2
内容重定向到cat
;cat
的内容重定向到pipe1
。仔细观察下路径:pipe1
->pipe2
->pipe1
,构成了一个循环。 这样导致管道pipe1
管道pipe2
中总是有数据(没有数据的时间太短)。于是,就构成了一个无限循环。我们打开执行这个程序后,可以用htop
查看当前的 CPU 使用情况,会发现 CPU 占用率很高。
【问题】 如果一个目录是只读权限,那么这个目录下面的文件还可写吗?
【解析】 这类问题,你一定要去尝试,观察现象再得到结果。
你可以看到上图中,foo 目录不可读了,下面的foo/bar
文件还可以写。 即便它不可写了,下面的foo/bar
文件还是可以写。
但是想要创建新文件就会出现报错,因为创建新文件也需要改目录文件。这个例子说明 Linux 中的文件内容并没有存在目录中,目录中却有文件清单。
【问题】 如何查看正在 TIME_WAIT 状态的连接数量?
【解析】 注意,这里有个小坑,就是 netstat 会有两行表头,这两行可以用 tail 过滤掉,下面tail \-n +3
就是告诉你 tail 从第 3 行开始显示。-a
代表显示所有的 socket。
1 | netstat -a | tail -n +3 | wc -l |
【问题】 如果你在编译安装 MySQL 时,发现找不到libcrypt.so ,应该如何处理?
【解析】 遇到这类问题,首先应该去查资料。 比如查 StackOverflow,搜索关键词:libcrypt.so not found,或者带上自己的操作系统ubuntu
。下图是关于 Stackoverflow 的一个解答:
在这里我再多说两句,程序员成长最需要的是学习时间,如果在这前面加一个形容词,那就是大量的学习时间;而程序员最需要掌握的技能就是搜索和学习知识的能力。如果你看到今天的这篇内容,说明已经学完了《重学操作系统》专栏两个模块的知识,希望你可以坚持下去!
【问题 1 】 根据今天的 access_log 分析出有哪些终端访问了这个网站,并给出分组统计结果。
【解析】access_log
中有Debian
和Ubuntu
等等。我们可以利用下面的指令看到,第 12 列是终端,如下图所示:
我们还可以使用sort
和uniq
查看有哪些终端,如下图所示:
最后需要写一个脚本,进行统计:
1 | cat nginx_logs.txt |\ |
结果如下:
【问题 2】 根据今天的 access_log 分析出访问量 Top 前三的网页。
如果不需要 Substring 等复杂的处理,也可以使用sort
和uniq
的组合。如下图所示:
【问题】~/.bashrc ~/.bash_profile, ~/.profile 和 /etc/profile 的区别是什么?
【解析】 执行一个 shell 的时候分成login shell和non-login shell。顾名思义我们使用了sudo``su
切换到某个用户身份执行 shell,也就是login shell
。还有 ssh 远程执行指令也是 login shell,也就是伴随登录的意思——login shell
会触发很多文件执行,路径如下:
如果以当前用户身份正常执行一个 shell,比如说./a.sh
,就是一个non-login
的模式。 这时候不会触发上述的完整逻辑。
另外shell还有另一种分法,就是interactive
和non-interactive
。interactive 是交互式的意思,当用户打开一个终端命令行工具后,会进入一个输入命令得到结果的交互界面,这个时候,就是interactive shell
。
baserc
文件通常只在interactive
模式下才会执行,这是因为~/.bashrc
文件中通常有这样的语句,如下图所示:
这个语句通过$-
看到当前shell
的执行环境,如下图所示:
带 i 字符的就是interactive
,没有带i字符就不是。
因此, 如果你需要通过 ssh 远程 shell 执行一个文件,你就不是在 interactive 模式下,bashrc 不会触发。但是因为登录的原因,login shell 都会触发,也就是说 profile 文件依然会执行。
这个模块我们学习了 Linux 指令。我带大家入了个门,也和你一起感受了一次 Linux 指令的博大精深。Linux 虽然没有上下五千年的历史,但每次使用,依然让我感受到了它浓郁的历史气息,悠久的文化传承,自由的创造精神。希望这块知识可以陪伴大家,鼓励你成为优秀的程序员。虽然我们已经学了几十个指令,但还是沧海一粟。后续就需要你多查资料,多用man
手册,继续深造了。
好的,Linux 指令部分就告一段落。下一节课,我们将开启操作系统核心知识学习,请和我一起来学习“模块三:操作系统基础知识”吧。