1. Linux系统配置初始化

1.1 背景

新购买的多台服务器并已安装Linux操作

1.2 需求

(1)设置时区并同步时间

因为多数的服务器都需要设置时间,方便日志查询和做同步操作。需要定期同步时间,采用crontab.

(2)禁用selinux

因为Selinux是一个安全机制,如果使用不熟练,容易造成一些莫名其妙的错误。

(3)关闭***,清空***默认策略,根据业务添加规则

(4)历史命令显示操作时间(便于审计)

(5)禁止root远程登录(创建普通用户,加入sudo用户组)

(6)禁止定时任务发送邮件(因为定时任务产生的事件容易占用磁盘空间)

(7)设置最大打开文件数(满足某些应用的需求)

(8)减少Swap使用(当物理内存不够用时,会使用swap,但是swap是磁盘上的一个空间,其读写性能较内存低很多)

(9)系统内核参数优化(如TCP连接)

(10)安装系统性能分析工具

以上需求,全部写在一个脚本内,进行一键部署。

1.3 编写脚本

# 打开两个终端,在Xshell或者其他ssh客户端上
[root@localhost ~]# mkdir shell_scripts && cd shell_scripts/ && vim sysconfig.sh

完整脚本的内容如下:

#/bin/bash

# 设置时区并同步时间
ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
if ! crontab -l |grep ntpdate &>/dev/null ; then
    (echo "* 1 * * * ntpdate time.windows.com >/dev/null 2>&1";crontab -l) |crontab 
fi

# 禁用selinux
sed -i '/SELINUX/{s/enforcing/disabled/}' /etc/selinux/config

# 关闭***
if egrep "7.[0-9]" /etc/redhat-release &>/dev/null; then
    systemctl stop firewalld
    systemctl disable firewalld
elif egrep "6.[0-9]" /etc/redhat-release &>/dev/null; then
    service iptables stop
    chkconfig iptables off
fi

# 历史命令显示操作时间
if ! grep HISTTIMEFORMAT /etc/bashrc; then
    echo 'export HISTTIMEFORMAT="%F %T `whoami` "' >> /etc/bashrc
    source /etc/bashrc
fi

# SSH超时时间
if ! grep "TMOUT=600" /etc/profile &>/dev/null; then
    echo "export TMOUT=600" >> /etc/profile
fi

# 禁止root远程登录
sed -i 's/#PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config

# 禁止定时任务向发送邮件
sed -i 's/^MAILTO=root/MAILTO=""/' /etc/crontab 

# 设置最大打开文件数
if ! grep "* soft nofile 65535" /etc/security/limits.conf &>/dev/null; then
cat >> /etc/security/limits.conf << EOF
    * soft nofile 65535
    * hard nofile 65535
EOF
fi

# 系统内核优化
cat >> /etc/sysctl.conf << EOF
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_tw_buckets = 20480
net.ipv4.tcp_max_syn_backlog = 20480
net.core.netdev_max_backlog = 262144
net.ipv4.tcp_fin_timeout = 20  
EOF

# 减少SWAP使用
echo "0" > /proc/sys/vm/swappine***系统性能分析工具及其他
yum install gcc make autoconf vim sysstat net-tools iostat iftop iotp lrzsz -y

1.3.1 设置时区

  • 默认安装完成后时区是UTC的
[root@localhost shell_scripts]# date
Thu Jun 27 16:04:51 CST 2019
  • 改成国内亚洲上海的时区
# 将时区做一个软链接即可
ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
  • 同步时间
# 在终端执行以下命令,与Windows同步即可
ntpdate time.windows.com
  • 定时同步时间(同步后,未来有可能出现抖动)
# 设置每天同步一次时间,下述* 1 * * *表示每天一点同步。这是crontab的日期格式
if ! crontab -l |grep ntpdate &>/dev/null ; then
    (echo "* 1 * * * ntpdate time.windows.com >/dev/null 2>&1";crontab -l) |crontab 
fi
# 由于脚本不能交互,所以,采用重定向的方式,将错误都写进/dev/null,即丢弃掉。

1.3.2 关闭Selinux

因为selinux不好使用,并且它对一些服务由权限控制,容易出现不易排错的故障报错。

# 采用sed流编辑器,对文本/etc/selinux/config中SELINUX字段设置为disabled
sed -i '/SELINUX/{s/enforcing/disabled/}' /etc/selinux/config

1.3.3 关闭***

清空***默认规则,根据业务设置满足自身需求的规则。

# 首先判断redhat的版本,由于CentOS 6 和CentOS 7是主流操作系统,所以此处,只过滤这两个。
# 如果是CentOS 7.X则执行第一个if,将标准输出和错误输出为空
if egrep "7.[0-9]" /etc/redhat-release &>/dev/null; then
    systemctl stop firewalld
    systemctl disable firewalld
# else 如果是CentOS 6.X则执行下述操作,这是因为6.X版本是使用iptables作为***的。
elif egrep "6.[0-9]" /etc/redhat-release &>/dev/null; then
    service iptables stop
    chkconfig iptables off
fi

1.3.4 历史命令显示操作时间

# 历史命令显示操作时间,将HISTTIMEFORMAT导出为系统变量,使其生效,其中%F 表示日期,%T表示时间
if ! grep HISTTIMEFORMAT /etc/bashrc; then
    echo 'export HISTTIMEFORMAT="%F %T `whoami` "' >> /etc/bashrc
fi
  • 测试
[root@localhost ~]# export HISTTIMEFORMAT="%F %T `whoami` "[root@localhost ~]# history    1  2019-06-27 15:17:45 root ping 8.8.8.8    2  2019-06-27 15:17:45 root exit    3  2019-06-27 15:17:45 root ifconfig    4  2019-06-27 15:17:45 root yum install net-tools    5  2019-06-27 15:17:45 root ]ifconfig......

1.3.5 SSH超时时间

用户通过SSH登录服务器客户端,如果10分钟仍旧没有操作,则认为超时,将会自动关闭。

if ! grep "TMOUT=600" /etc/profile &>/dev/null; then    echo "export TMOUT=600" >> /etc/profilefi

1.3.6 禁止Root登录

  • 为了防止用户采用root身份在远程SSH登录做了不必要的操作

通过修改/etc/ssh/sshd_config中的PermitRootLogin值,默认是被注释掉的,是允许root远程登录的。事实上还有好几种选项:

禁止登陆、禁止密码登录、仅允许密钥登陆和开放登陆,以下是对可选项的概括:

参数类别 是否允许ssh登陆 登录方式 交互shell
yes 允许 没有限制 没有限制
without-password 允许 除密码以外 没有限制
forced-commands-only 允许 仅允许使用密钥 仅允许已授权的命令
no 不允许 N/A N/A

以上选项中,yes和no的功能显而易见,只是很粗暴的允许、禁止root用户进行登陆。without-password在yes的基础上,禁止了root用户使用密码登陆。

# 禁止root远程登录sed -i 's/#PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
  • 注意,在操作之前,一定要添加一个普通用户有sudo权限,否则不能修改某些文件

创建普通用户并加入sudo用户组

# 如创建gzr普通用户,并设置密码,加入sudo用户组[root@localhost ~]# useradd gzr[root@localhost ~]# passwd gzrChanging password for user gzr.New password: BAD PASSWORD: The password contains the user name in some formRetype new password: passwd: all authentication tokens updated successfully.
  • 将用户加入sudo用户组,并且能够免密
[root@localhost ~]# vim /etc/passwd# 修改gzr用户的uid和gid为0,然后重启,变成rootroot:x:0:0:root:/root:/bin/bashbin:x:1:1:bin:/bin:/sbin/nologindaemon:x:2:2:daemon:/sbin:/sbin/nologinadm:x:3:4:adm:/var/adm:/sbin/nologinlp:x:4:7:lp:/var/spool/lpd:/sbin/nologinsync:x:5:0:sync:/sbin:/bin/syncshutdown:x:6:0:shutdown:/sbin:/sbin/shutdownhalt:x:7:0:halt:/sbin:/sbin/haltmail:x:8:12:mail:/var/spool/mail:/sbin/nologinoperator:x:11:0:operator:/root:/sbin/nologingames:x:12:100:games:/usr/games:/sbin/nologinftp:x:14:50:FTP User:/var/ftp:/sbin/nologinnobody:x:99:99:Nobody:/:/sbin/nologinsystemd-network:x:192:192:systemd Network Management:/:/sbin/nologindbus:x:81:81:System message bus:/:/sbin/nologinpolkitd:x:999:998:User for polkitd:/:/sbin/nologinsshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologinpostfix:x:89:89::/var/spool/postfix:/sbin/nologinnginx:x:998:996:nginx user:/var/cache/nginx:/sbin/nologingzr:x:0:0::/home/gzr:/bin/bash# 让sudoers属于root组chown root:root /etc/sudoers# 编辑/etc/sudoers,在root ALL=(ALL:ALL) ALL的下方添加如下字段gzr ALL=(ALL:ALL) NOPASSWD: ALL# 然后重新/etc/passwd的内容,改为用户之前的uid与gid,并reboot,进入系统后发现可以使用sudo执行命令,并且不再需要密码

1.3.7 禁止定时任务发送邮件

# 因为在很多情况下,在/var/mail会有很多小文件占用磁盘,强制MAILTO的值为空,使得不会发送邮件sed -i 's/^MAILTO=root/MAILTO=""/' /etc/crontab

1.3.8 设置最大打开文件数

由于很多应用程序,并发数很高,很容易达到一个值,所以并发文件打开数设置大一些

# 下述脚本的作用,实际上是先判断soft nofile 65535是否在/etc/security/limits.conf文件中,如果不存在则在其末尾添加下述两行内容。* soft nofile 65535* hard nofile 65535# 注意采用if ! 表示如果命令执行的结果为0,结果为真,则执行。if ! grep "* soft nofile 65535" /etc/security/limits.conf &>/dev/null; thencat >> /etc/security/limits.conf << EOF    * soft nofile 65535    * hard nofile 65535EOFfi

1.3.9 系统内核优化

cat >> /etc/sysctl.conf << EOF# 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;net.ipv4.tcp_syncookies = 1# 表示系统同时保持TIME_WAIT套接字的最大数量,如果超过这个数字,TIME_WAIT套接字将立刻被清除并打印警告信息。net.ipv4.tcp_max_tw_buckets = 20480# 表示SYN队列的长度,默认为1024,加大队列长度为8192,可以容纳更多等待连接的网络连接数。net.ipv4.tcp_max_syn_backlog = 20480# 每个网络接口接收数据包的速率比内核处理这些包的速率快时,允许送到队列的数据包的最大数目。net.core.netdev_max_backlog = 262144# 表示如果套接字由本端要求关闭,这个参数决定了它保持在FIN-WAIT-2状态的时间,默认是30。net.ipv4.tcp_fin_timeout = 20  EOF
  • 上述的所有字段的值,都可以查看到,比如,要查看默认值
[root@localhost ~]# sysctl -a | grep tw-bucketssysctl: reading key "net.ipv6.conf.all.stable_secret"sysctl: reading key "net.ipv6.conf.default.stable_secret"sysctl: reading key "net.ipv6.conf.eth0.stable_secret"sysctl: reading key "net.ipv6.conf.lo.stable_secret"[root@localhost ~]# sysctl -a | grep syn_backlogsysctl: reading key "net.ipv6.conf.all.stable_secret"net.ipv4.tcp_max_syn_backlog = 128......

1.3.10 减少swap的使用

#echo "0" > /proc/sys/vm/swappiness

Linux内核提供了一个接口,可以修改swap使用情况

# 下述“30”是一个权重值,设置的越大,使用swap的概率越大。[root@localhost ~]# cat /proc/sys/vm/swappiness 30

1.3.11 安装性能分析工具及其他

# autoconf 是一个用于包,以适应多种Unix类系统的 shell脚本的工具。# sysstat是一个软件包,包含监测系统性能及效率的一组工具,这些工具对于我们收集系统性能数据,比如CPU使用率、硬盘和网络吞吐数据,这些数据的收集和分析,有利于我们判断系统是否正常运行,是提高系统运行效率、安全运行服务器的得力助手。# iostat 命令用来监视系统输入/输出设备负载,这通过观察与它们的平均传送速率相关的物理磁盘的活动时间来实现.# iftop是类似于linux下面top的实时流量监控工具。# iotp# lrzsz 是一款在linux里可代替ftp上传和下载的程序,安装后可以直接拖拽小文件到Xshell中,上传至服务器。yum install gcc make autoconf vim sysstat net-tools iostat iftop iotp lrzsz -y

1.4 执行并验证脚本功能

[root@localhost shell_scripts]# chmod +x sysconfig.sh# 安装epel源[root@localhost ~]# yum install -y epel-release[root@localhost ~]# yum makecache && yum update# 安装dos2unix,因为脚本是在Windows下编写的,可能存在一些符号问题,采用dos2unix来转换[root@localhost ~]# yum install dos2unix -y[root@localhost shell_scripts]# dos2unix sysconfig.shdos2unix: converting file sysconfig.sh to Unix format ...[root@localhost shell_scripts]# ./sysconfig.sh ln: failed to create symbolic link ‘/etc/localtime’: File exists......Complete!

(1)验证

# 验证history[root@localhost ~]# cat /etc/bashrc# 末尾确实多了一行export HISTTIMEFORMAT="%F %T `whoami` "# 但是需要source才能生效[root@localhost ~]# source /etc/bashrc[root@localhost ~]# history    1  2019-06-27 21:40:38 root ping 8.8.8.8......# 验证swap配置[root@localhost ~]# cat /proc/sys/vm/swappiness 0# 验证定时任务的MAILTO确实为空,之前值是为root[root@localhost ~]# cat /etc/crontab SHELL=/bin/bashPATH=/sbin:/bin:/usr/sbin:/usr/binMAILTO=""# For details see man 4 crontabs# Example of job definition:# .---------------- minute (0 - 59)# |  .------------- hour (0 - 23)# |  |  .---------- day of month (1 - 31)# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat# |  |  |  |  |# *  *  *  *  * user-name  command to be executed

2. Linux系统发送告警邮件

在Linux中需要发送邮件,一般有两种方式,安装CentMail或mailx,但是这两种方式都不太理想,要么延迟大,要么可能引起不必要的攻击。

完整的脚本mailAlert.sh:

yum install mailxvim /etc/mail.rc  set from=491376192@qq.com smtp=smtp.qq.comset smtp-auth-user=491376192@qq.com smtp-auth-password=nkdowzxokerhbjgfset smtp-auth=login

该案例使用外部邮箱服务器

# 安装mailx,它是一个小型的邮件发送程序[root@localhost ~]# yum install -y mailx# 修改mailx的配置文件/etc/mail.rc[root@localhost ~]# vim /etc/mail.rc# 添加以下内容到文件中set from=491376192@qq.com smtp=smtp.qq.com# 注意:目前大部分的外部邮件服务使用第三方客户端时,都需要使用授权码,上面的smtp-auth-password使用的就是授权码,而不是邮件帐号的密码。set smtp-auth-user=491376192@qq.com smtp-auth-password=nkdowzxokerhbjgfset smtp-auth=login
  • 邮箱开启授权密码登录(以qq邮箱为例)

1561647022482

1561646943533

  • 测试邮件
[root@localhost ~]# echo "邮箱报警测试" | mail -s "monitor" gezirong@126.com

1561647658239

  • 上述打开邮件,发现确实发送了一封测试邮件。

3. 批量创建100个用户并设置随机密码

首先需要创建用户,并生成密码,免交互实现,最后将密码存放到一个文件中。完整的shell脚本batchCreateUser.sh如下:

#!/bin/bashDATE=$@USER_FILE=user.txtfor USER in $USER_LIST; do    if ! id $USER &>/dev/null; then        PASS=$(echo $RANDOM |md5sum |cut -c 1-8)        useradd $USER        echo $PASS |passwd --stdin $USER &>/dev/null        echo "$USER   $PASS" >> $USER_FILE        echo "$USER User create successful."    else        echo "$USER User already exists!"    fidone   
  • 仔细查看设置密码的命令,可以发现,passwd允许接收标准输入即stdin。所以在shell脚本中,可以实现自动为用户设置密码,例如:echo 123456 | passwd --stdin zhangsan
[root@localhost ~]# passwd --helpUsage: passwd [OPTION...] <accountName>  -k, --keep-tokens       keep non-expired authentication tokens  -d, --delete            delete the password for the named account (root only)  -l, --lock              lock the password for the named account (root only)  -u, --unlock            unlock the password for the named account (root only)  -e, --expire            expire the password for the named account (root only)  -f, --force             force operation  -x, --maximum=DAYS      maximum password lifetime (root only)  -n, --minimum=DAYS      minimum password lifetime (root only)  -w, --warning=DAYS      number of days warning users receives before password expiration (root only)  -i, --inactive=DAYS     number of days after password expiration when an account becomes disabled (root only)  -S, --status            report password status on the named account (root only)  --stdin                 read new tokens from stdin (root only)Help options:  -?, --help              Show this help message  --usage                 Display brief usage message

3.1生成随机密码

Linux有个RANDOM函数能够随机生成一些数,并且我们可以采用md5sum工具对其进行加密,截取md5sum的某几位作为用户的密码。

[root@localhost ~]# echo $RANDOM26512[root@localhost ~]# echo $RANDOM13943# 采用md5sum加密[root@localhost ~]# echo $RANDOM |md5sum94c7639f8a421f8e26ca78d9786af144  -# 截取1-8位作为密码[root@localhost ~]# echo $RANDOM |md5sum | cut -c 1-8e7faa81e

3.2 批量创建用户

# 首先需要判断该用户是否已存在,采用id命令,并采用echo $?来判断上一条命令正确执行与否。[root@localhost shell_scripts]# id gzruid=1000(gzr) gid=1000(gzr) groups=1000(gzr)[root@localhost shell_scripts]# echo $?0[root@localhost shell_scripts]# id zhangsanid: zhangsan: no such user[root@localhost shell_scripts]# echo $?1# $()表示执行一个命令#!/bin/bashUSER_FILE=./user.infofor USER in user{1..100}; do   if ! id $USER &>/dev/null; then      PASS=$(echo $RANDOM |md5sum | cut -c 1-8)      useradd $USER      echo $PASS | passwd --stdin $USER      echo "$USER $PASS" >> $USER_FILE      echo "$USER User has created sucessfull!"   else      echo "$USER User already exists!"   fidone

3.3 升级版

实际生产环境中,由于用户名和密码不尽相同,所以需要事先定义一个用户名表,来创建用户名和密码。

#!/bin/bashUSER_LIST=$@USER_FILE=./user.infofor USER in $USER_LIST; do   if ! id $USER &>/dev/null; then      PASS=$(echo $RANDOM |md5sum | cut -c 1-8)      useradd $USER      echo $PASS | passwd --stdin $USER &>/dev/null      echo "$USER $PASS" >> $USER_FILE      echo "$USER User has created sucessfull!"   else      echo "$USER User already exists!"   fidone# 命名上述脚本为test.sh,并赋予执行权限,并在命令里传入用户名,其中$@表示会接收所有的传入参数[root@localhost shell_scripts]# chmod +x test.sh[root@localhost shell_scripts]# ./test.sh zhangsan lisi wangwuChanging password for user zhangsan.passwd: all authentication tokens updated successfully../test.sh: line 8: $USER_FILE: ambiguous redirectzhangsan User has created sucessfull!Changing password for user lisi.passwd: all authentication tokens updated successfully../test.sh: line 8: $USER_FILE: ambiguous redirectlisi User has created sucessfull!Changing password for user wangwu.passwd: all authentication tokens updated successfully../test.sh: line 8: $USER_FILE: ambiguous redirectwangwu User has created sucessfull!# 查看用户家目录,发现确实创建成功了,[root@localhost shell_scripts]# cd /home/[root@localhost home]# lsgzr  lisi  wangwu  wwwroot  zhangsan[root@localhost shell_scripts]# cat user.infozhangsan e1fb3ddblisi 059434bewangwu 0db26b65# 切换用户至zhangsan确实可以了。[gzr@localhost shell_scripts]$ su zhangsanPassword: [zhangsan@localhost shell_scripts]$ 

4. 一键查看服务器资源利用率

假设我们的服务器承载的是一个网站,首先是查看系统资源,然后检查网络资源。

  • CPU资源(60%以上,相应会比较慢)
  • 内存(内存剩余率)
  • 硬盘(磁盘利用率)
  • TCP连接状态(TCP并发情况)

完整的脚本resourceUsage.sh

#!/bin/bashfunction cpu() {    NUM=1    while [ $NUM -le 3 ]; do        util=`vmstat |awk '{if(NR==3)print 100-$15"%"}'`        user=`vmstat |awk '{if(NR==3)print $13"%"}'`        sys=`vmstat |awk '{if(NR==3)print $14"%"}'`        iowait=`vmstat |awk '{if(NR==3)print $16"%"}'`        echo "CPU - 使用率: $util , 等待磁盘IO响应使用率: $iowait"        let NUM++        sleep 1    done}function memory() {    total=`free -m |awk '{if(NR==2)printf "%.1f",$2/1024}'`    used=`free -m |awk '{if(NR==2) printf "%.1f",($2-$NF)/1024}'`    available=`free -m |awk '{if(NR==2) printf "%.1f",$NF/1024}'`    echo "内存 - 总大小: ${total}G , 使用: ${used}G , 剩余: ${available}G"}function disk() {    fs=$(df -h |awk '/^\/dev/{print $1}')    for p in $fs; do        mounted=$(df -h |awk '$1=="'$p'"{print $NF}')        size=$(df -h |awk '$1=="'$p'"{print $2}')        used=$(df -h |awk '$1=="'$p'"{print $3}')        used_percent=$(df -h |awk '$1=="'$p'"{print $5}')        echo "硬盘 - 挂载点: $mounted , 总大小: $size , 使用: $used , 使用率: $used_percent"    done}function tcp_status() {    summary=$(ss -antp |awk '{status[$1]++}END{for(i in status) printf i":"status[i]" "}')    echo "TCP连接状态 - $summary"}cpumemorydisktcp_status

脚本执行测试

[root@localhost shell_scripts]# ./resourceUsage.shCPU - 使用率: 1% , 等待磁盘IO响应使用率: 0%CPU - 使用率: 1% , 等待磁盘IO响应使用率: 0%CPU - 使用率: 1% , 等待磁盘IO响应使用率: 0%内存 - 总大小: 3.7G , 使用: 0.4G , 剩余: 3.3G硬盘 - 挂载点: / , 总大小: 26G , 使用: 1.9G , 使用率: 7%硬盘 - 挂载点: /boot , 总大小: 1014M , 使用: 219M , 使用率: 22%TCP连接状态 - LISTEN:5 ESTAB:1 State:1 

4.1 CPU利用率

4.1.1 Top

1561686328200

%Cpu(s)一栏内容,其中us+sy表示此刻CPU使用率,wa+id表示CPU剩余资源。wa是CPU等待磁盘响应的百分比(这个值如果在3%-5%说明已经是高负载了,此时磁盘读写会变得很慢)

4.1.2 vmstat

[gzr@localhost ~]$ vmstatprocs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st 1  0      0 2572160   4172 1146396    0    0     4    13   25   24  0  0 99  0  0# 最后几个值也是CPU利用率us 用户态使用sy 系统态使用id 空闲wa 等待磁盘响应st

(1)采用awk进行切割

[root@localhost ~]# vmstat |awk '{print $13}'us0

(2)CPU利用率

#!/bin/bashfunction cpu() {    NUM=1    while [ $NUM -le 3 ]; do        util=`vmstat |awk '{if(NR==3)print 100-$15"%"}'`        user=`vmstat |awk '{if(NR==3)print $13"%"}'`        sys=`vmstat |awk '{if(NR==3)print $14"%"}'`        iowait=`vmstat |awk '{if(NR==3)print $16"%"}'`        echo "CPU - 使用率: $util , 等待磁盘IO响应使用率: $iowait"        let NUM++        sleep 1    done}

4.2 内存利用率

4.2.1 free

# 内存分为物理内存和Swap,其中swap实际上是磁盘空间,当物理内存不够用是,会被当做物理内存使用的一种机制。[root@localhost ~]# free -m              total        used        free      shared  buff/cache   availableMem:           3763         128        2510           8        1123        3334Swap:          3071           0        3071

(1)总内存利用率

[root@localhost ~]# free -m | awk '{if(NR==2)print $2}'3763# 只保留小数点后一位[root@localhost ~]# free -m | awk '{if(NR==2)printf "%.1f",$2/1024}'3.7

(2)使用内存

[root@localhost ~]# free -m              total        used        free      shared  buff/cache   availableMem:           3763         125        2514           8        1123        3338Swap:          3071           0        3071# $NF表示取最后一行available的值,用total-available的值就是剩余值[root@localhost ~]# free -m | awk '{if(NR==2)printf "%.1f",$2-$NF}'425.0# 425.0[root@localhost ~]# free -m | awk '{if(NR==2)printf "%.1f",($2-$NF)/1024}'0.4# 可用内存[root@localhost ~]# free -m | awk '{if(NR==2)printf "%.1f",$NF/1024}'3.3

(3)打印情况

echo "内存 - 总大小: ${total}G, 已使用: ${used}G, 剩余: ${avaiable}G"

memory.sh脚本如下:

#!/bin/bashfunction memory() {    total=`free -m |awk '{if(NR==2)printf "%.1f",$2/1024}'`    used=`free -m |awk '{if(NR==2) printf "%.1f",($2-$NF)/1024}'`    available=`free -m |awk '{if(NR==2) printf "%.1f",$NF/1024}'`    echo "内存 - 总大小: ${total}G , 使用: ${used}G , 剩余: ${available}G"}# 调用memory函数memory[root@localhost ~]# ./memory.sh 内存 - 总大小: 3.7G , 使用: 0.4G , 剩余: 3.3G

4.3 硬盘利用率

4.3.1 df

完整的diskUsage.sh脚本如下:

#!/bin/bashfunction disk() {    fs=$(df -h |awk '/^\/dev/{print $1}')    for p in $fs; do        mounted=$(df -h |awk '$1=="'$p'"{print $NF}')        size=$(df -h |awk '$1=="'$p'"{print $2}')        used=$(df -h |awk '$1=="'$p'"{print $3}')        used_percent=$(df -h |awk '$1=="'$p'"{print $5}')        echo "硬盘 - 挂载点: $mounted , 总大小: $size , 已使用: $used , 使用率: $used_percent"    done}disk

(1)查看只包含/dev的磁盘

# 我们关心的是以/dev开头的硬盘其使用率[root@localhost ~]# df -hFilesystem               Size  Used Avail Use% Mounted on/dev/mapper/centos-root   26G  1.9G   25G   7% /devtmpfs                 1.9G     0  1.9G   0% /devtmpfs                    1.9G     0  1.9G   0% /dev/shmtmpfs                    1.9G  9.0M  1.9G   1% /runtmpfs                    1.9G     0  1.9G   0% /sys/fs/cgroup/dev/xvda1              1014M  219M  796M  22% /boottmpfs                    377M     0  377M   0% /run/user/0
# 先匹配以/dev开头的设备,/^\/dev期间反斜杠是说明采用了转义。[root@localhost ~]# df -h |awk '/^\/dev/{print $1}'/dev/mapper/centos-root/dev/xvda1

(2)遍历分区

遍历分区,$1表示第一列数值for p in $fs; do        mounted=$(df -h |awk '$1=="'$p'"{print $NF}')        size=$(df -h |awk '$1=="'$p'"{print $2}')        used=$(df -h |awk '$1=="'$p'"{print $3}')        used_percent=$(df -h |awk '$1=="'$p'"{print $5}')        echo "硬盘 - 挂载点: $mounted , 总大小: $size , 已使用: $used , 使用率: $used_percent"done

(3)执行验证

[root@localhost ~]# chmod +x diskUsage.sh [root@localhost ~]# ./diskUsage.sh 硬盘 - 挂载点: / , 总大小: 26G , 已使用: 1.9G , 使用率: 7%硬盘 - 挂载点: /boot , 总大小: 1014M , 已使用: 219M , 使用率: 22%

4.4 TCP连接状态

4.4.1 netstat

# -a 表示显示所有监听,-t 表示监听tcp -n表示以数值形式显示   -p以进程ID号来显示[root@localhost ~]# netstat -antpActive Internet connections (servers and established)Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    tcp        0      0 127.0.0.1:199           0.0.0.0:*               LISTEN      4568/snmpd          tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      4566/sshd           tcp        0      0 127.0.0.1:25            0.0.0.0:*               LISTEN      4945/master         tcp        0     52 172.16.4.11:22          210.26.55.133:10934     ESTABLISHED 50080/sshd: root@pt tcp6       0      0 :::22                   :::*                    LISTEN      4566/sshd           tcp6       0      0 ::1:25                  :::*                    LISTEN      4945/master       

(1)完整脚本tcp_status.sh如下:

#!/bin/bashfunction tcp_status() {    summary=$(ss -antp |awk '{status[$1]++}END{for(i in status) printf i":"status[i]" "}')    echo "TCP连接状态 - $summary"}tcp_status

#!/bin/bashfunction tcp_status() {   summary=$(netstat -antp |awk '{a[$6]++}END{for(i in a)printf i":"a[i]" "}')   echo "TCP连接状态 - $summary"}tcp_status             

(2)单元介绍

# netstat -antp中的第6列(即state)的数值# a[$6]++表示将其视为一个数组,遍历循环叠加[root@localhost ~]# netstat -antp |awk '{a[$6]++}END{for(i in a)print i,a[i]}'LISTEN 5ESTABLISHED 1established) 1Foreign 1# 打印在一行内[root@localhost ~]# netstat -antp |awk '{a[$6]++}END{for(i in a)printf i":"a[i]" "}'LISTEN:5 ESTABLISHED:1 established):1 Foreign:1 # 将值放进summary中summary=$(netstat -antp |awk '{a[$6]++}END{for(i in a)printf i":"a[i]" "}')echo "TCP连接状态 - summary"

(3)测试脚本

[root@localhost ~]# ./tcp_status.shTCP连接状态 - LISTEN:5 ESTAB:1 State:1 [root@localhost ~]# ./tcp_status.sh TCP连接状态 - LISTEN:5 ESTABLISHED:1 established):1 Foreign:1

5. 找出占用CPU、内存过高的进程

如果一台web服务器,我们通过案例4发现出现进程过高的现象,那么需要进一步确定哪些进程占用很高,

由于top命令虽然可以看到进程占用率,但是它是一个实时动态的,所以此处采用ps aux,并结合awk进行字段截取,采用sort进行排序,最后采用head命令查看占用率前10的进程。

首先给出完整shell脚本,CGHighUsage.sh

echo "------------------CPU Top 10---------------------"ps -eo user,pid,pcpu,pmem,args --sort=-pcpu  |head -n 10echo "-----------------Memory Top 10-------------------"ps -eo user,pid,pcpu,pmem,args --sort=-pmem  |head -n 10

(1)ps工具的使用

[root@localhost ~]# ps aux | awk '{print $3}' |sort -r |head -n 10%CPU0.10.10.00.00.00.00.00.00.0
  • 自定义输出
[root@localhost ~]# ps -o pid   PID 51455 51560# 只列出ps中的pid,CPU利用率,内存利用率,进程名称[root@localhost ~]# ps -eo pid,pcpu,pmem,args   PID %CPU %MEM COMMAND     1  0.0  0.1 /usr/lib/systemd/systemd --system --deserialize 16     2  0.0  0.0 [kthreadd]     3  0.0  0.0 [ksoftirqd/0]     5  0.0  0.0 [kworker/0:0H]     6  0.0  0.0 [kworker/u256:0]     7  0.0  0.0 [migration/0]     8  0.0  0.0 [rcu_bh]     9  0.0  0.0 [rcu_sched]    10  0.0  0.0 [lru-add-drain]......# 追加sort排序,根据CPU[root@localhost ~]# ps -eo pid,pcpu,pmem,args --sort=-pcpu   PID %CPU %MEM COMMAND 51537  0.1  0.0 [kworker/1:2] 51576  0.1  0.0 [kworker/1:0].....# 追加前10[root@localhost ~]# ps -eo pid,pcpu,pmem,args --sort=-pcpu |head -n 10   PID %CPU %MEM COMMAND 51537  0.1  0.0 [kworker/1:2] 51576  0.1  0.0 [kworker/1:0]     1  0.0  0.1 /usr/lib/systemd/systemd --system --deserialize 16     2  0.0  0.0 [kthreadd]     3  0.0  0.0 [ksoftirqd/0]     5  0.0  0.0 [kworker/0:0H]     6  0.0  0.0 [kworker/u256:0]     7  0.0  0.0 [migration/0]     8  0.0  0.0 [rcu_bh]

(2)内存的利用率进程排序

# 原理同CPU[root@localhost ~]# ps -eo pid,pcpu,pmem,args --sort=-pmem |head -n 10   PID %CPU %MEM COMMAND  4567  0.0  0.4 /usr/bin/python2 -Es /usr/sbin/tuned -l -P  4073  0.0  0.3 /usr/lib/polkit-1/polkitd --no-debug  4568  0.0  0.2 /usr/sbin/snmpd -LS0-6d -f  4563  0.0  0.2 /usr/sbin/rsyslogd -n  4116  0.0  0.2 /usr/sbin/NetworkManager --no-daemon  2064  0.0  0.2 /usr/lib/systemd/systemd-journald     1  0.0  0.1 /usr/lib/systemd/systemd --system --deserialize 16 51451  0.0  0.1 sshd: root@pts/0 51589  0.0  0.1 local -t unix

(3)执行验证脚本

[root@localhost ~]# chmod +x CGHighUsage.sh[root@localhost ~]# ./CGHighUsage.sh ------------------CPU Top 10---------------------USER        PID %CPU %MEM COMMANDroot      51576  0.1  0.0 [kworker/1:0]root      51590  0.1  0.0 [kworker/1:1]root          1  0.0  0.1 /usr/lib/systemd/systemd --system --deserialize 16root          2  0.0  0.0 [kthreadd]root          3  0.0  0.0 [ksoftirqd/0]root          5  0.0  0.0 [kworker/0:0H]root          6  0.0  0.0 [kworker/u256:0]root          7  0.0  0.0 [migration/0]root          8  0.0  0.0 [rcu_bh]-----------------Memory Top 10-------------------USER        PID %CPU %MEM COMMANDroot       4567  0.0  0.4 /usr/bin/python2 -Es /usr/sbin/tuned -l -Ppolkitd    4073  0.0  0.3 /usr/lib/polkit-1/polkitd --no-debugroot       4568  0.0  0.2 /usr/sbin/snmpd -LS0-6d -froot       4563  0.0  0.2 /usr/sbin/rsyslogd -nroot       4116  0.0  0.2 /usr/sbin/NetworkManager --no-daemonroot       2064  0.0  0.2 /usr/lib/systemd/systemd-journaldroot          1  0.0  0.1 /usr/lib/systemd/systemd --system --deserialize 16root      51451  0.0  0.1 sshd: root@pts/0root       5384  0.0  0.1 /systemd/systemd-udevd

6. 查看网卡实时流量

查看当前服务器有哪些数据传输,传输的量是什么样的。

其中我们常用的ifconfig命令,能够看到一些传送数据包的情况。

完整的脚本NICRealTimeTraffic.sh

#!/bin/bashNIC=$1echo -e " In ------ Out"while true; do    OLD_IN=$(awk '$0~"'$NIC'"{print $2}' /proc/net/dev)    OLD_OUT=$(awk '$0~"'$NIC'"{print $10}' /proc/net/dev)    sleep 1    NEW_IN=$(awk  '$0~"'$NIC'"{print $2}' /proc/net/dev)    NEW_OUT=$(awk '$0~"'$NIC'"{print $10}' /proc/net/dev)    IN=$(printf "%.1f%s" "$((($NEW_IN-$OLD_IN)/1024))" "KB/s")    OUT=$(printf "%.1f%s" "$((($NEW_OUT-$OLD_OUT)/1024))" "KB/s")    echo "$IN $OUT"    sleep 1done

(1)ifconfig

# 其中RX为接收数据量,TX为传送数据量[root@localhost ~]# ifconfigeth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500        inet 172.16.4.11  netmask 255.255.255.0  broadcast 172.16.4.255        inet6 fe80::abb9:c2a:d512:23e1  prefixlen 64  scopeid 0x20<link>        ether 6a:7e:a7:35:ab:16  txqueuelen 1000  (Ethernet)        RX packets 53022  bytes 200947727 (191.6 MiB)        RX errors 0  dropped 0  overruns 0  frame 0        TX packets 36795  bytes 4317684 (4.1 MiB)        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0......

(2)采用iftop

如果安装了iftop,那么可以列出一些socket产生的实时流量,假设我们现在需要查看服务器每秒产生的流量:

一般来说,是从/proc/net/dev(一般ifconfig或者netstat也都是读取该目录下的值)下获取,它记录了所有的网络设备网络传输的情况。

# 其中Receive中的bytes表示网卡从装完系统,总共累计接收的数据量[root@localhost ~]# cat /proc/net/devInter-|   Receive                                                |  Transmit face |bytes    packets errs drop fifo frame compressed multicast|bytes    packets errs drop fifo colls carrier compressed  eth0: 200985834   53481    0    0    0     0          0         0  4360177   37069    0    0    0     0       0          0    lo:   25872     460    0    0    0     0          0         0    25872     460    0    0    0     0       0          0# 取出eth 0网卡接收的数据量[root@localhost ~]# cat /proc/net/dev | awk '/eth0/{print $2}'200996165# 取出eth 0网卡发送的数据量[root@localhost ~]# cat /proc/net/dev | awk '/eth0/{print $10}'4370385# 细心的同学会发现,上述接收和发送的,与之前查询的结果不一样,这是因为这是一个实时刷新的值。所以如果我们要获取每秒的流量,只需要和上一秒对比相减即可

(3)编写脚本逻辑

[root@localhost ~]# awk '$0~"'eth0'"{print $2}' /proc/net/dev201010738# $0,表示匹配一整行,$NIC表示输入网卡名# 先取到前一秒的流进流出的流量OLD_IN=$(awk '$0~"'$NIC'"{print $2}' /proc/net/dev)OLD_OUT=$(awk '$0~"'$NIC'"{print $10}' /proc/net/dev)# 休眠1秒sleep 1# 再次获取新的流量NEW_IN=$(awk  '$0~"'$NIC'"{print $2}' /proc/net/dev)NEW_OUT=$(awk '$0~"'$NIC'"{print $10}' /proc/net/dev)# 相减,获取这一秒的值,%s是一个占位符,表示后面的值,两层括号,外层括号代表一个运算。IN=$(printf "%.1f%s" "$((($NEW_IN-$OLD_IN)/1024))" "KB/s")OUT=$(printf "%.1f%s" "$((($NEW_OUT-$OLD_OUT)/1024))" "KB/s")# 举例[root@localhost ~]# printf "%.1f%s" 1.12341.1# 返回值echo "$IN $OUT"sleep 1

(2)测试运行脚本

[root@localhost ~]# ./NICRealTimeTraffic.sh eth0 In ------ Out0.0KB/s 0.0KB/s0.0KB/s 0.0KB/s0.0KB/s 0.0KB/s4956.0KB/s 80.0KB/s

1561713582925

  • 通过向服务器拖拽比较大的文件,能够更加直观地感受流量流出流入的变化。

7. 监控100台服务器磁盘利用率

当磁盘利用率过高时,新的数据无法再写入。问题在于怎么获取到100台服务器的数据

完整的脚本hundredDiskUsage.sh

#!/bin/bashHOST_INFO=host.infofor IP in $(awk '/^[^#]/{print $1}' $HOST_INFO); do    USER=$(awk -v ip=$IP 'ip==$1{print $2}' $HOST_INFO)    PORT=$(awk -v ip=$IP 'ip==$1{print $3}' $HOST_INFO)    TMP_FILE=/tmp/disk.tmp    ssh -p $PORT $USER@$IP 'df -h' > $TMP_FILE    USE_RATE_LIST=$(awk 'BEGIN{OFS="="}/^\/dev/{print $NF,int($5)}' $TMP_FILE)    for USE_RATE in $USE_RATE_LIST; do        PART_NAME=${USE_RATE%=*}        USE_RATE=${USE_RATE#*=}        if [ $USE_RATE -ge 80 ]; then            echo "Warning: $PART_NAME Partition usage $USE_RATE%!"        fi    donedone

7.1 设置主从服务器间免密登录

7.1.1 环境准备

由于服务器资源限制,此处只以3台服务器作为演示,扩展至100台,原理相似:

IP 主机名 用途
172.16.4.18 elk-es1 监控主机
172.16.4.19 elk-es2 被监控主机
172.16.4.20 elk-es3 被监控主机

7.1.2 服务器间免密设置

# 在主监控服务器上生成秘钥[root@elk-es1 ~]# ssh-keygenGenerating public/private rsa key pair.Enter file in which to save the key (/root/.ssh/id_rsa): Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /root/.ssh/id_rsa.Your public key has been saved in /root/.ssh/id_rsa.pub.The key fingerprint is:SHA256:8Z7ajr7qM6OBUYDJBV7wPhkTE1ZgaJpxO+dCfr3m2fw root@elk-es1The key's randomart image is:+---[RSA 2048]----+|.=*X+.           ||=+*.o            ||o= =.   .        ||o =.=    o       || o.B .  S .      ||  oo+ .  . .     ||  .o.  .  o      ||     .*+ +       ||    .==*B+E      |+----[SHA256]-----+# 生成的秘钥在.ssh下[root@elk-es1 ~]# ls .ssh/id_rsa  id_rsa.pub  known_hosts# 采用ssh-copy-id即可传送过去,期间可能要求输入密码,elk-es3的加入方式类似[root@elk-es1 ~]# ssh-copy-id root@172.16.4.19# 加入完成后,应该可以直接ssh到其余两台服务器[root@elk-es1 ~]# ssh 172.16.4.19Last login: Sat Jun 29 09:09:27 2019 from 210.26.55.133[root@elk-es1 ~]# ssh 172.16.4.20Last login: Sat Jun 29 09:09:28 2019 from 210.26.55.133# 这是因为在对端已经生成了同样的公钥[root@elk-es3 ~]# cat .ssh/authorized_keys ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC5thMdLNWJZk14IZYMG4oL8x4RK+GVjuwIzKyUxWJM7I3F1Bl2Hl/q7krOvt0/sm0t7WBLGltM5LTDVvfJtgH8VlD7RcZS2Mkmuu85TL50o3D4bdywfA7Hng8P8kj5FvFU8auSN8/fwn2wE35or3NZCr1jBtR1EKdwrZhd7OZXsAaYzHjTOncEkdBt+krop/PgEq3ijfq1k80MVF2/O/ENYYlEEyuBechJrYyui5/rKWm75TEH3K3t3uAqLZajqO8TIBHtSjtovxE5i3sVzOEtBXElS7nFyQh7cZijplBWNz5hI7qWa456/XvIFAAl1DDBxgy3ML+m3U5sfvRUfCML root@elk-es1

7.2 编写服务器列表

由于实际生产环境中待监控服务器数量众多,通常的做法是将服务器的基本信息写入一个列表内。host.info文件共分为三列:IP,用户名,端口号(默认为22)

172.16.4.19 root 22172.16.4.20 root 22

7.3 编写脚本

# 取出host.info中的第一列,即IP信息。采用循环遍历此处/^[^#]表示监控除了开头为#号的所有机器,因为有可能存在注释信息[root@elk-es1 ~]# awk '/^[^#]/{print $1}' host.info172.16.4.19172.16.4.20# 打印主机名,如果ip选定,可以根据ip打印改行对应的第2列[root@elk-es1 ~]# awk -v ip=172.16.4.19 '$1==ip{print $2}' host.inforoot# 同上可以打印端口号[root@elk-es1 ~]# awk -v ip=172.16.4.19 '$1==ip{print $3}' host.info22# 采用临时文件暂存TMP_FILE=/tmp/disk.tmp    ssh -p $PORT $USER@$IP 'df -h' > $TMP_FILE# 匹配含/dev的磁盘行记录[root@elk-es1 ~]# df -h |awk '/^\/dev/{print $0}'/dev/mapper/centos-root   50G  2.4G   48G   5% //dev/xvda1              1014M  145M  870M  15% /boot/dev/mapper/centos-home   42G   33M   42G   1% /home[root@elk-es1 ~]# df -h |awk '/^\/dev/{print $NF,$5}'/ 5%/boot 15%/home 1%# 由于在数值比对时,带百分号不易于比较,所以此处采用强制转换去除[root@elk-es1 ~]# df -h |awk '/^\/dev/{print $NF,int($5)}'/ 5/boot 15/home 1#让第五列和第六列整体作为一个值,方便比较[root@elk-es1 ~]# df -h |awk '/^\/dev/{print $NF"="int($5)}'/=5/boot=15/home=1# 采用OFS内置的变量,实际上效果是一样的[root@elk-es1 ~]# df -h |awk -v OFS="=" '/^\/dev/{print $NF,int($5)}'/=5/boot=15/home=1# 采用循环遍历其值,其中%=*表示,出去等号左边的值,即上面的/, /boot, /home,可以简单写个脚本测试,内容如下abc=$(df -h |awk -v OFS="=" '/^\/dev/{print $NF,int($5)}')for i in $abc; do    echo ${i%=*}done[root@elk-es1 ~]# ./test.sh //boot/home# 而#*=则表示只取等号右边的值,修改上述脚本中的%=*为#*=,重新执行脚本[root@elk-es1 ~]# ./test.sh 5151

7.4 执行并测试脚本

[root@elk-es1 ~]# chmod +x hundredDiskUsage.sh # bash -x可以查看脚本执行的过程[root@elk-es1 ~]# bash -x hundredDiskUsage.sh + HOST_INFO=host.info++ awk '/^[^#]/{print $1}' host.info+ for IP in '$(awk '\''/^[^#]/{print $1}'\'' $HOST_INFO)'++ awk -v ip=172.16.4.19 'ip==$1{print $2}' host.info+ USER=root++ awk -v ip=172.16.4.19 'ip==$1{print $3}' host.info+ PORT=22+ TMP_FILE=/tmp/disk.tmp+ ssh -p 22 root@172.16.4.19 'df -h'++ awk 'BEGIN{OFS="="}/^\/dev/{print $NF,int($5)}' /tmp/disk.tmp+ USE_RATE_LIST='/=5/boot=15/home=1'+ for USE_RATE in '$USE_RATE_LIST'+ PART_NAME=/+ USE_RATE=5+ '[' 5 -ge 80 ']'+ for USE_RATE in '$USE_RATE_LIST'+ PART_NAME=/boot+ USE_RATE=15+ '[' 15 -ge 80 ']'+ for USE_RATE in '$USE_RATE_LIST'+ PART_NAME=/home+ USE_RATE=1+ '[' 1 -ge 80 ']'+ for IP in '$(awk '\''/^[^#]/{print $1}'\'' $HOST_INFO)'++ awk -v ip=172.16.4.20 'ip==$1{print $2}' host.info+ USER=root++ awk -v ip=172.16.4.20 'ip==$1{print $3}' host.info+ PORT=22+ TMP_FILE=/tmp/disk.tmp+ ssh -p 22 root@172.16.4.20 'df -h'++ awk 'BEGIN{OFS="="}/^\/dev/{print $NF,int($5)}' /tmp/disk.tmp+ USE_RATE_LIST='/=5/boot=15/home=1'+ for USE_RATE in '$USE_RATE_LIST'+ PART_NAME=/+ USE_RATE=5+ '[' 5 -ge 80 ']'+ for USE_RATE in '$USE_RATE_LIST'+ PART_NAME=/boot+ USE_RATE=15+ '[' 15 -ge 80 ']'+ for USE_RATE in '$USE_RATE_LIST'+ PART_NAME=/home+ USE_RATE=1+ '[' 1 -ge 80 ']'# 为了脚本的有效性,我们先暂时把阈值改为10%,看其是否会产生报警信息,脚本内容为:#!/bin/bashHOST_INFO=host.infofor IP in $(awk '/^[^#]/{print $1}' $HOST_INFO); do    USER=$(awk -v ip=$IP 'ip==$1{print $2}' $HOST_INFO)    PORT=$(awk -v ip=$IP 'ip==$1{print $3}' $HOST_INFO)    TMP_FILE=/tmp/disk.tmp    ssh -p $PORT $USER@$IP 'df -h' > $TMP_FILE    USE_RATE_LIST=$(awk 'BEGIN{OFS="="}/^\/dev/{print $NF,int($5)}' $TMP_FILE)    for USE_RATE in $USE_RATE_LIST; do        PART_NAME=${USE_RATE%=*}        USE_RATE=${USE_RATE#*=}        if [ $USE_RATE -ge 10 ]; then            echo -e "$IP \n Warning: $PART_NAME Partition usage $USE_RATE%!"        else            echo "It is OK"        fi    donedone[root@elk-es1 ~]# ./hundredDiskUsage.sh It is OK172.16.4.19  Warning: /boot Partition usage 15%!It is OKIt is OK172.16.4.20  Warning: /boot Partition usage 15%!It is OK

8. 批量检查网站是否异常

多数情况下,我们可能会采用curl命令来探测网站正常与否

[root@localhost ~]# curl -I www.baidu.comHTTP/1.1 200 OKAccept-Ranges: bytesCache-Control: private, no-cache, no-store, proxy-revalidate, no-transformConnection: Keep-AliveContent-Length: 277Content-Type: text/htmlDate: Sat, 29 Jun 2019 02:20:44 GMTEtag: "575e1f60-115"Last-Modified: Mon, 13 Jun 2016 02:50:08 GMTPragma: no-cacheServer: bfe/1.0.8.18

8.1 完整的webProbe.sh

#!/bin/bash  URL_LIST="www.baidu.com www.google.com.cn"for URL in $URL_LIST; do    FAIL_COUNT=1    for ((i=1;i<=3;i++)); do        HTTP_CODE=$(curl -o /dev/null --connect-timeout 3 -s -w "%{http_code}" $URL)        if [ $HTTP_CODE -eq 200 ]; then            echo "$URL OK"            break        else            echo "$URL retry $FAIL_COUNT"            let FAIL_COUNT++        fi    done    if [ $FAIL_COUNT -eq 4 ]; then        echo "Warning: $URL Access failure!"    fidone

8.2 思路历程

我们的思路是,通过curl探测网站,然后通过返回的http状态码来判断网站的可访问性

# -o 将输出内容重定向为空,-s表示静默方式访问,-w指定输出的字段[root@localhost ~]# curl -o /dev/null -s -w "%{http_code}" www.baidu.com200

8.2.1 let累加值探测

但是由于高峰期,打开网站超时,或者打开不对,所以我们选择探测次数为3.

[root@localhost ~]# a=1[root@localhost ~]# let a++[root@localhost ~]# echo $a2

8.2.2 循环探测

# 设置超时时间为3[root@localhost ~]# curl -o /dev/null --connect-timeout 3 -s -w "%{http_code}" www.baidu.com200# 采用一个变量计数,当其值达到3,状态码返回值仍旧未正常返回时,触发报警FAIL_COUNT=0    for ((i=1;i<=3;i++)); do        HTTP_CODE=$(curl -o /dev/null --connect-timeout 3 -s -w "%{http_code}" $URL)        if [ $HTTP_CODE -eq 200 ]; then            echo "$URL OK"            break        else            echo "$URL retry $FAIL_COUNT"            let FAIL_COUNT++        fi    done

8.2.3 探测失败

# 当FAIL_COUNT等于4 ,说明3次探测失败if [ $FAIL_COUNT -eq 4 ]; then      echo "Warning: $URL Access failure!"fi

8.3 测试脚本

[root@localhost shell_scripts]# ./webProbe.sh www.baidu.com OKwww.google.com.cn has retried 1 timeswww.google.com.cn has retried 2 timeswww.google.com.cn has retried 3 timesWarning: www.google.com.cn Access failure!

9. 批量主机执行命令

事实上,Xshell在内的ssh客户端工具,支持同时发送命令到多个终端,此处采用脚本来实现,在公司中会有一些操作需要在多主机间执行,虽然市场上,已经有一些优秀批处理主机管理工具,此处是为了学习expect免交互工具。

9.1 完整脚本

编写脚本batch_host_cmd.sh

#!/bin/bashCOMMAND=$*HOST_INFO=host.infofor IP in $(awk '/^[^#]/{print $1}' $HOST_INFO); do    USER=$(awk -v ip=$IP 'ip==$1{print $2}' $HOST_INFO)    PORT=$(awk -v ip=$IP 'ip==$1{print $3}' $HOST_INFO)    PASS=$(awk -v ip=$IP 'ip==$1{print $4}' $HOST_INFO)    expect -c "       spawn ssh -p $PORT $USER@$IP       expect {          \"(yes/no)\" {send \"yes\r\"; exp_continue}          \"password:\" {send \"$PASS\r\"; exp_continue}          \"$USER@*\" {send \"$COMMAND\r exit\r\"; exp_continue}       }    "    echo "-------------------"done

9.2 基本需求

IP 主机名 用途
172.16.4.11 gzr 命令执行主机
172.16.4.18 elk-es1 被动执行命令主机
172.16.4.19 elk-es2 被动执行命令主机
172.16.4.20 elk-es3 被动执行命令主机

如上,我们希望在localhost上执行命令,能够分发到其余3台主机上,并将执行结果返回到本机上。

9.3 ssh免密执行命令

# 在案例7中,我们已经将这三台主机设置了免密登录,如果在elk-es1要查看elk-es2的磁盘利用率,就可以采用以下命令:[root@elk-es1 ~]# ssh root@172.16.4.19 "df -h"Filesystem               Size  Used Avail Use% Mounted on/dev/mapper/centos-root   48G  2.1G   46G   5% /devtmpfs                 7.8G     0  7.8G   0% /devtmpfs                    7.8G     0  7.8G   0% /dev/shmtmpfs                    7.8G  9.0M  7.8G   1% /runtmpfs                    7.8G     0  7.8G   0% /sys/fs/cgroup/dev/xvda1              1014M  145M  870M  15% /boot/dev/mapper/centos-home   24G   33M   24G   1% /hometmpfs                    1.6G     0  1.6G   0% /run/user/0# 在主机es2上,发现和上述结果一致[root@elk-es2 ~]# df -hFilesystem               Size  Used Avail Use% Mounted on/dev/mapper/centos-root   48G  2.1G   46G   5% /devtmpfs                 7.8G     0  7.8G   0% /devtmpfs                    7.8G     0  7.8G   0% /dev/shmtmpfs                    7.8G  9.0M  7.8G   1% /runtmpfs                    7.8G     0  7.8G   0% /sys/fs/cgroup/dev/xvda1              1014M  145M  870M  15% /boot/dev/mapper/centos-home   24G   33M   24G   1% /hometmpfs                    1.6G     0  1.6G   0% /run/user/0

9.4 expect自动填充密码执行

9.4.1 安装expect

[root@gzr ~]# yum install -y expect

9.4.2 编写hostinfo

# 为了使得expect能够帮我自动填充,免交互使用密码,host.info包含4列内容,IP ,用户名,端口号,密码172.16.4.18 root 22 password172.16.4.19 root 22 password172.16.4.20 root 22 password

9.4.3遍历hostinfo(同案例7)

# 由于expect本身也是一种脚本语言,需要嵌套在shell中,那么必须制定expect -cexpect -c "       spawn ssh -p $PORT $USER@$IP       # 正则匹配       expect {          # 初次匹配需要保存指纹          \"(yes/no)\" {send \"yes\r\"; exp_continue}          # 需要输入密码,exp_continue表示继续执行下一条指令          \"password:\" {send \"$PASS\r\"; exp_continue}          # 匹配用户和输入命令,因为登录后,经常是[root@elk-es1 ~]这种格式。          \"$USER@*\" {send \"$COMMAND\r exit\r\"; exp_continue}       }    "

9.4.4 命令接收

# 接收所有命令的位置参数COMMAND=$*

9.5 执行验证脚本

# 发现依次登录并执行了命令[root@gzr ~]# bash batch_host_cmd.sh free -mspawn ssh -p 22 root@172.16.4.18root@172.16.4.18's password: Last login: Sat Jun 29 11:36:12 2019 from 172.16.4.11[root@elk-es1 ~]# free -m              total        used        free      shared  buff/cache   availableMem:          15859        1836       12399           9        1622       13644Swap:          8063           0        8063[root@elk-es1 ~]#  exitlogoutfree -m exitConnection to 172.16.4.18 closed.--------------------------------spawn ssh -p 22 root@172.16.4.19root@172.16.4.19's password: Last login: Sat Jun 29 11:36:13 2019 from 172.16.4.11[root@elk-es2 ~]# free -m              total        used        free      shared  buff/cache   availableMem:          15859        1877       12710           8        1271       13654Swap:          8063           0        8063[root@elk-es2 ~]#  exitlogoutfree -m exitConnection to 172.16.4.19 closed.--------------------------------spawn ssh -p 22 root@172.16.4.20root@172.16.4.20's password: Last login: Sat Jun 29 11:36:12 2019 from 172.16.4.11[root@elk-es3 ~]# free -m              total        used        free      shared  buff/cache   availableMem:          15859        1881       12689           8        1287       13648Swap:          8063           0        8063[root@elk-es3 ~]#  exitlogoutConnection to 172.16.4.20 closed.--------------------------------

10. 一键部署LNMP网站平台

LNMP:Linux, Nginx, MySQL, PHP这一一个开源高性能的Web组合

工作流程:用户通过请求Nginx , 然后Nginx通过一个模块转发到PHP,如果涉及到数据库读写,那么会请求MySQL。

以CentOS为例,安装软件一般有以下三种方式:

  • 通过yum安装,能够自行解决依赖项,快速地安装
  • 源码编译,根据语言编译
    • ./configure,首先需要对当前平台的进行检测,检测该软件是否支持该环境,包括依赖
    • make,开始编译工作
    • make install将编译出来的文件进行安装。
  • 二进制安装,官方已经编译好的

10.1 完整脚本

编写完整的LNMP.sh,内容如下:

#!/bin/bashNGINX_V=1.17.0PHP_V=7.3.6TMP_DIR=/tmpINSTALL_DIR=/usr/localPWD_C=$PWDechoecho -e "\tMenu\n"echo -e "1. Install Nginx"echo -e "2. Install PHP"echo -e "3. Install MySQL"echo -e "4. Deploy LNMP"echo -e "9. Quit"# 如果上述命令执行失败,即echo$?的返回值不为0function command_status_check() {	if [ $? -ne 0 ]; then		echo $1		exit	fi }function install_nginx() {    cd $TMP_DIR    yum install -y gcc gcc-c++ make openssl-devel pcre-devel wget    wget http://nginx.org/download/nginx-${NGINX_V}.tar.gz    tar zxf nginx-${NGINX_V}.tar.gz    cd nginx-${NGINX_V}    ./configure --prefix=$INSTALL_DIR/nginx \    --with-http_ssl_module \    --with-http_stub_status_module \    --with-stream    command_status_check "Nginx - 平台环境检查失败!"    make -j 4     command_status_check "Nginx - 编译失败!"    make install    command_status_check "Nginx - 安装失败!"    mkdir -p $INSTALL_DIR/nginx/conf/vhost    alias cp=cp ; cp -rf $PWD_C/nginx.conf $INSTALL_DIR/nginx/conf    rm -rf $INSTALL_DIR/nginx/html/*    echo "ok" > $INSTALL_DIR/nginx/html/status.html    echo '<?php echo "ok"?>' > $INSTALL_DIR/nginx/html/status.php    $INSTALL_DIR/nginx/sbin/nginx    command_status_check "Nginx - 启动失败!"}function install_php() {	cd $TMP_DIR    yum install -y gcc gcc-c++ make gd-devel libxml2-devel \        libcurl-devel libjpeg-devel libpng-devel openssl-devel \        libmcrypt-devel libxslt-devel libtidy-devel    wget http://docs.php.net/distributions/php-${PHP_V}.tar.gz    tar zxf php-${PHP_V}.tar.gz    cd php-${PHP_V}    ./configure --prefix=$INSTALL_DIR/php \    --with-config-file-path=$INSTALL_DIR/php/etc \    --enable-fpm --enable-opcache \    --with-mysql --with-mysqli --with-pdo-mysql \    --with-openssl --with-zlib --with-curl --with-gd \    --with-jpeg-dir --with-png-dir --with-freetype-dir \    --enable-mbstring --enable-hash    command_status_check "PHP - 平台环境检查失败!"    make -j 4     command_status_check "PHP - 编译失败!"    make install    command_status_check "PHP - 安装失败!"    cp php.ini-production $INSTALL_DIR/php/etc/php.ini    cp sapi/fpm/php-fpm.conf $INSTALL_DIR/php/etc/php-fpm.conf    cp sapi/fpm/init.d.php-fpm /etc/init.d/php-fpm    chmod +x /etc/init.d/php-fpm    /etc/init.d/php-fpm start    command_status_check "PHP - 启动失败!"}read -p "请输入编号:" numbercase $number in    1)        install_nginx;;    2)        install_php;;    3)        install_mysql;;    4)        install_nginx        install_php        ;;    9)        exit;;esac

10.2 思路

10.2.1 定义安装软件版本变量

# 因为脚本可能随时需要更新安装软件的版本NGINX_V=1.15.6PHP_V=5.6.36# 并设定临时工作目录TMP_DIR=/tmp

10.2.2 编写安装nginx的函数

function install_nginx() {    cd $TMP_DIR    # 安装依赖库,openssl-dev是nginx的https的一个支持,pcre-devel是nginx正则的库    yum install -y gcc gcc-c++ make openssl-devel pcre-devel wget    wget http://nginx.org/download/nginx-${NGINX_V}.tar.gz    tar zxvf nginx-${NGINX_V}.tar.gz    cd nginx-${NGINX_V}    ./configure --prefix=$INSTALL_DIR/nginx \    --with-http_ssl_module \    --with-http_stub_status_module \    --with-stream    command_status_check "Nginx - 平台环境检查失败!"    # 编译时的并发数    make -j 4     command_status_check "Nginx - 编译失败!"    make install    command_status_check "Nginx - 安装失败!"    mkdir -p $INSTALL_DIR/nginx/conf/vhost    alias cp=cp ; cp -rf $PWD_C/nginx.conf $INSTALL_DIR/nginx/conf    rm -rf $INSTALL_DIR/nginx/html/*    echo "ok" > $INSTALL_DIR/nginx/html/status.html    echo '<?php echo "ok"?>' > $INSTALL_DIR/nginx/html/status.php    $INSTALL_DIR/nginx/sbin/nginx    command_status_check "Nginx - 启动失败!"}

10.2.3 编写安装PHP的函数

# 基本安装过程同上function install_php() {	cd $TMP_DIR    yum install -y gcc gcc-c++ make gd-devel libxml2-devel \        libcurl-devel libjpeg-devel libpng-devel openssl-devel \        libmcrypt-devel libxslt-devel libtidy-devel    wget http://docs.php.net/distributions/php-${PHP_V}.tar.gz    tar zxf php-${PHP_V}.tar.gz    cd php-${PHP_V}    ./configure --prefix=$INSTALL_DIR/php \    --with-config-file-path=$INSTALL_DIR/php/etc \    --enable-fpm --enable-opcache \    --with-mysql --with-mysqli --with-pdo-mysql \    --with-openssl --with-zlib --with-curl --with-gd \    --with-jpeg-dir --with-png-dir --with-freetype-dir \    --enable-mbstring --enable-hash    command_status_check "PHP - 平台环境检查失败!"    make -j 4     command_status_check "PHP - 编译失败!"    make install    command_status_check "PHP - 安装失败!"    cp php.ini-production $INSTALL_DIR/php/etc/php.ini    cp sapi/fpm/php-fpm.conf $INSTALL_DIR/php/etc/php-fpm.conf    # 其启动脚本    cp sapi/fpm/init.d.php-fpm /etc/init.d/php-fpm    # 赋予启动脚本执行权限    chmod +x /etc/init.d/php-fpm    /etc/init.d/php-fpm start    command_status_check "PHP - 启动失败!"}

10.2.4 编写安装MySQL函数

10.2.5 编写分支函数

echoecho -e "\tMenu\n"echo -e "1. Install Nginx"echo -e "2. Install PHP"echo -e "3. Install MySQL"echo -e "4. Deploy LNMP"echo -e "9. Quit"function......read -p "请输入编号:" numbercase $number in    1)        install_nginx;;    2)        install_php;;    3)        install_mysql;;    4)        install_nginx        install_php        ;;    9)        exit;;esac

10.3 测试脚本功能

10.3.1 搭建容器环境

此处为了保证主机的环境,采用在容器中安装LNMP环境,效果同虚拟主机和物理机是一样的

[root@k8s-master ~]# docker run -itd centos:7[root@k8s-master ~]# docker psCONTAINER ID        IMAGE               COMMAND             CREATED              STATUS              PORTS               NAMESdfa9193fb970        centos:7            "/bin/bash"         About a minute ago   Up About a minute                       elegant_hoover[root@k8s-master ~]# docker exec -it dfa9193fb970 bash[root@dfa9193fb970 /]# cat /etc/redhat-release CentOS Linux release 7.6.1810 (Core) 

10.3.2 执行脚本

[root@dfa9193fb970 /]# chmod +x LNMP.sh# 执行脚本,并选择编号4,安装Nginx PHP MySQL[root@dfa9193fb970 /]# bash LNMP.sh# 由于采用源码编译安装,执行时间会比较长。大约7-8分钟,取决于主机性能,可以查看是否安装成功[root@dfa9193fb970 /]# ps -ef | grep nginxroot       2937      0  0 07:15 ?        00:00:00 nginx: master process /usr/local/nginx/sbin/nginxnobody     2939   2937  0 07:15 ?        00:00:00 nginx: worker processroot     113260     16  0 07:21 pts/1    00:00:00 grep --color=auto nginx[root@dfa9193fb970 /]# ps -ef | grep php  root     113256      0  0 07:21 ?        00:00:00 php-fpm: master process (/usr/local/php/etc/php-fpm.conf)nobody   113257 113256  0 07:21 ?        00:00:00 php-fpm: pool wwwnobody   113258 113256  0 07:21 ?        00:00:00 php-fpm: pool wwwroot     113262     16  0 07:21 pts/1    00:00:00 grep --color=auto php[root@dfa9193fb970 /]# curl 127.0.0.1/status.htmlok[root@dfa9193fb970 /]# ls /usr/local/bin/     etc/     games/   include/ lib/     lib64/   libexec/ nginx/   php/     sbin/    share/   src/[root@dfa9193fb970 /]# curl 127.0.0.1/status.phpok# 手动书写phpinfo页面[root@dfa9193fb970 /]# vi /usr/local/nginx/html/phpinfo.php# 内容如下,保存退出<?php phpinfo();?>[root@dfa9193fb970 /]# curl 127.0.0.1/phpinfo.php | more# 如果有内容输出,格式为php页面,这说明访问成功,也可以通过查看php版本来确定安装成功[root@dfa9193fb970 /]# /usr/local/php/bin/php -vPHP 5.6.36 (cli) (built: Jun 29 2019 07:20:02) Copyright (c) 1997-2016 The PHP GroupZend Engine v2.6.0, Copyright (c) 1998-2016 Zend Technologies# 查看nginx版本[root@dfa9193fb970 /]# /usr/local/nginx/sbin/nginx -vnginx version: nginx/1.15.6# 此时已经安装好了,只要将页面放置/usr/local/nginx/html下就可以正常访问了。

11. 监控MySQL主从状态是否异常

MySQL是一款优秀的数据库软件,在集群环境下,通常需要采用一主多从的方式来解决高并发和读写分离的问题。

mysql的主从同步,其中Master:是根据binlog日志的(它记录所有的写操作),而Slave是基于binlog日志来同步信息的。

  • 工作流程

如果有新数据写入master,实际上是写入master的binlog,而Slaver通过读取master上的binlog,将数据写到本地的relaylog, 从而在本地的mysql实例上执行,从而实现数据同步的一致性,保证主从同步。

实际上是两个线程,其中一个是Slave_IO_Running(从master的binlog日志读取数据),另外一个是Slave_SQL_Running进程(将读取到的数据,在Slave上重新执行一遍)

  • 如何判断主从同步的状态

11.1 完整脚本maste-slave_syn.sh

#!/bin/bash  USER=rootPASSWD=Gzr_2019.lzuIO_SQL_STATUS=$(mysql -u$USER -p$PASSWD -e 'show slave status\G' 2>/dev/null |awk '/Slave_.*_Running:/{print $1$2}')for i in $IO_SQL_STATUS; do    THREAD_STATUS_NAME=${i%:*}    THREAD_STATUS=${i#*:}    if [ "$THREAD_STATUS" != "Yes" ]; then        echo "Error: MySQL Master-Slave $THREAD_STATUS_NAME status is $THREAD_STATUS!"    else        echo "$THREAD_STATUS_NAME process is running normally!"    fidone

11.2 环境配置

IP 主机名 节点类型
172.16.4.21 mysql-master 主数据库节点
172.16.4.22 mysql-slave 从数据库节点

11.3安装mysql

该操作,需要在主从两台节点上都要运行。

[root@mysql-master ~]# wget http://dev.mysql.com/get/mysql57-community-release-el7-7.noarch.rpm# 下载完后就是一个mysql57-community-release-el7-7.noarch.rpm的文件,可以用以下命令查看该文件都包含哪些包:[root@mysql-master ~]# rpm -qpl mysql57-community-release-el7-7.noarch.rpm# 安装rpm包[root@mysql-master ~]# rpm -ivh mysql57-community-release-el7-7.noarch.rpm#安装完上述包后,查看yum库,# yum list Mysql* 就会在yum库里生成以下几个包:[root@mysql-master ~]# yum repolist enabled | grep "mysql.*-community.*"[root@mysql-master ~]# yum install -y mysql-community-server# 这样做的好处在于,可以用yum管理MySQL的包,尤其是可以把MySQL的安装包生成到YUM库里,更多MYSQL的安装方式。[root@mysql-master ~]# systemctl start mysqld

11.4 修改root本地登录密码

(1)查看mysql临时密码

# 为了加强安全性,安装MySQL5.7后会为root用户随机生成了一个密码,在error log中,关于error log的位置,如果安装的是RPM包,则默认是/var/log/mysqld.log。[root@mysql-master ~]# grep 'temporary password' /var/log/mysqld.log2019-06-29T08:09:40.924027Z 1 [Note] A temporary password is generated for root@localhost: bToxbdrwR9)O

(2)连接mysql

# 输入上述生成的密码回车[root@mysql-master ~]# mysql -uroot -pEnter password: Welcome to the MySQL monitor.  Commands end with ; or \g.Your MySQL connection id is 2Server version: 5.7.26Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.Oracle is a registered trademark of Oracle Corporation and/or itsaffiliates. Other names may be trademarks of their respectiveowners.Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.# 设置密码,临时密码马上会失效mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY 'Gzr_2019.lzu';Query OK, 0 rows affected (0.00 sec)# 更新权限,并退出,下次再登录时,则需要新密码mysql> flush privileges;Query OK, 0 rows affected (0.00 sec)mysql> exit

11.5 主从同步配置

11.5.1 配置主库

(1)授权给从数据库服务器

mysql> GRANT REPLICATION SLAVE ON *.* to 'rep1'@172.16.4.22 identified by 'Gzr_2019.lzu';Query OK, 0 rows affected, 1 warning (0.00 sec)

(2)修改主库配置文件,开启binlog,并设置server-id, 每次修改配置文件都要重启mysql服务才会生效

[root@mysql-master ~]# vim /etc/my.cnf[mysqld]log_bin =/var/lib/mysql/binlogserver-id=1binlog-do-db = cmdbdatadir=/var/lib/mysqlsocket=/var/lib/mysql/mysql.socksymbolic-links=0log-error=/var/log/mysqld.logpid-file=/var/run/mysqld/mysqld.pid.....# 重启mysql服务,使配置生效[root@mysql-master ~]# systemctl restart mysqld# 查看主服务器当前二进制日志名和偏移量,方便在从数据库启动后,从这个点开始进行数据的恢复mysql> show master status;+---------------+----------+--------------+------------------+-------------------+| File          | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |+---------------+----------+--------------+------------------+-------------------+| binlog.000001 |      447 | cmdb         |                  |                   |+---------------+----------+--------------+------------------+-------------------+1 row in set (0.00 sec)# 到此,主服务器已经配置好
  • server-id:master端的ID号;

    log-bin:同步的日志路径及文件名,一定注意这个目录要是mysql有权限写入的(我这里是偷懒了,直接放在了下面那个datadir下面);

    binlog-do-db:要同步的数据库名

    还可以显示 设置不同步的数据库:

    binlog-ignore-db = mysql 不同步mysql库和test库 binlog-ignore-db = test

11.5.2 配置从库

(1)采用命令设置

[root@mysql-master ~]# vim /etc/my.cnf[mysqld]server-id=2.....mysql> change master to   master_host='172.16.4.21',   master_port=3306,   master_user='rep1',   master_password='Gzr_2019.lzu',   master_log_file='binlog.000001',   master_log_pos=447;Query OK, 0 rows affected, 2 warnings (0.01 sec)

(2)启动slave进程

mysql> start slave;Query OK, 0 rows affected (0.00 sec)

(3)查看slave的状态,如果下面两项值为YES,则表示配置正确:

Slave_IO_Running: Yes

Slave_SQL_Running: Yes

mysql> show slave status\G;*************************** 1. row ***************************               Slave_IO_State: Waiting for master to send event                  Master_Host: 172.16.4.21                  Master_User: rep1                  Master_Port: 3306                Connect_Retry: 60              Master_Log_File: binlog.000001          Read_Master_Log_Pos: 447               Relay_Log_File: mysql-slave-relay-bin.000002                Relay_Log_Pos: 317        Relay_Master_Log_File: binlog.000001             Slave_IO_Running: Yes            Slave_SQL_Running: Yes......

1561815648521

11.6 免交互输入命令

[root@mysql-slave ~]# mysql -uroot -pGzr_2019.lzu -e "show slave status\G;"mysql: [Warning] Using a password on the command line interface can be insecure.*************************** 1. row ***************************               Slave_IO_State: Waiting for master to send event                  Master_Host: 172.16.4.21                  Master_User: rep1                  Master_Port: 3306                Connect_Retry: 60              Master_Log_File: binlog.000001          Read_Master_Log_Pos: 447               Relay_Log_File: mysql-slave-relay-bin.000002                Relay_Log_Pos: 317        Relay_Master_Log_File: binlog.000001             Slave_IO_Running: Yes            Slave_SQL_Running: Yes......# awk截取输出我们关心的两行,其中.[root@mysql-slave ~]# mysql -uroot -pGzr_2019.lzu -e "show slave status\G;" | awk '/Slave_.*_Running:/'mysql: [Warning] Using a password on the command line interface can be insecure.             Slave_IO_Running: Yes            Slave_SQL_Running: Yes# 使其作为两行记录,否则会被认为是4个值[root@mysql-slave ~]# mysql -uroot -pGzr_2019.lzu -e "show slave status\G;" | awk '/Slave_.*_Running:/{print $1$2}'mysql: [Warning] Using a password on the command line interface can be insecure.Slave_IO_Running:YesSlave_SQL_Running:Yes# 由于系统会认为在界面上显式地出现密码,是不安全的,那么我们可以将该输出重定向为空[root@mysql-slave ~]# mysql -uroot -pGzr_2019.lzu -e "show slave status\G;" 2>/dev/null | awk '/Slave_.*_Running:/{print $1$2}'Slave_IO_Running:YesSlave_SQL_Running:Yes# 分隔取值,比如取冒号左边的值,就采用%:*,取冒号右边的就采用#*:[root@mysql-slave ~]# a=Slave_IO_Running:Yes[root@mysql-slave ~]# echo ${a%:*}Slave_IO_Running[root@mysql-slave ~]# echo ${a#*:}Yes

11.7 编写循环遍历其状态

# 如果线程状态名不是YES,那么认为主从复制出错for i in $IO_SQL_STATUS; do    THREAD_STATUS_NAME=${i%:*}    THREAD_STATUS=${i#*:}    if [ "$THREAD_STATUS" != "Yes" ]; then        echo "Error: MySQL Master-Slave $THREAD_STATUS_NAME status is $THREAD_STATUS!" |mail -s "Master-Slave Staus" xxx@163.com    fidone

11.8 测试脚本

[root@mysql-slave ~]# bash master-slave_syn.shSlave_IO_Running process is running normally!Slave_SQL_Running process is running normally!

12. MySQL数据库备份(分库分表)

数据为王,必须保证数据安全性,备份必不可少(保存到多个位置)有两种备份方式,基于数据库和基于表的备份。

mysqldump(全局恢复)一般不是很适用,通常分库分表细粒度备份。

12.1 完整shell脚本DBBackup.sh

#!/bin/bashDATE=$(date +%F_%H-%M-%S)USER=rootPASS=Gzr_2019.lzuBACKUP_DIR=/data/db_backupDB_LIST=$(mysql -u$USER -p$PASS -s -e "show databases;" 2>/dev/null |egrep -v "Database|information_schema|mysql|performance_schema|sys")for DB in $DB_LIST; do    BACKUP_NAME=$BACKUP_DIR/${DB}_${DATE}.sql    if ! mysqldump -u$USER -p$PASS -B $DB > $BACKUP_NAME 2>/dev/null; then        echo "$BACKUP_NAME 备份失败!"    fidone

12.2 思路历程

12.2.1 备份库

# 创建待备份数据库host_monitor和test用于测试mysql> CREATE DATABASE host_monitor;Query OK, 1 row affected (0.00 sec)mysql> CREATE DATABASE test;Query OK, 1 row affected (0.01 sec)# 备份命令mysqldump -uroot -pxxx -B Databasename > xxx.sql

12.2.2 备份表

mysqldump -uroot -pxxx Databasename Tablename > xxxx.sql

12.2.3 给备份添加时间

[root@mysql-master ~]# date +%F_%H-%M-%S2019-06-30_10-11-57

12.2.4 免交互提取字段

[root@mysql-slave ~]# mysql -uroot -pGzr_2019.lzu -s -e "show databases;"mysql: [Warning] Using a password on the command line interface can be insecure.Databaseinformation_schemahost_monitormysqlperformance_schemaschoolsystest# 将系统自带的库排除,其余的是我们自己手动创建的库。[root@mysql-slave ~]# mysql -uroot -pGzr_2019.lzu -s -e "show databases;" |egrep -v "infor|perfor|sys|mysql"host_monitorschooltest

12.3 测试并备份数据库(完整备份)

# 创建mysql备份目录/data/db_backup[root@mysql-slave ~]# mkdir -p /data/db_backup/# 执行脚本,查看内容[root@mysql-slave ~]# ./DBBackup.sh [root@mysql-slave db_backup]# lshost_monitor_2019-06-30_10-24-04.sql  school_2019-06-30_10-24-04.sql  test_2019-06-30_10-24-04.sql

12.4 分表备份

完整shell脚本TableBackup.sh代码

#!/bin/bashDATE=$(date +%F_%H-%M-%S)USER=rootPASS=Gzr_2019.lzuBACKUP_DIR=/data/db_backupDB_LIST=$(mysql -u$USER -p$PASS -s -e "show databases;" 2>/dev/null |egrep -v "Database|information_schema|mysql|performance_schema|sys")# 第一层循环,遍历每个库,然后遍历每个库的每个表for DB in $DB_LIST; do    BACKUP_DB_DIR=$BACKUP_DIR/${DB}_${DATE}    # 如果该目录没有创建,则手动创建下    [ ! -d $BACKUP_DB_DIR ] && mkdir -p $BACKUP_DB_DIR &>/dev/null    TABLE_LIST=$(mysql -u$USER -p$PASS -s -e "use $DB;show tables;" 2>/dev/null)    for TABLE in $TABLE_LIST; do        BACKUP_NAME=$BACKUP_DB_DIR/${TABLE}.sql         if ! mysqldump -u$USER -p$PASS $DB $TABLE > $BACKUP_NAME 2>/dev/null; then            echo "$BACKUP_NAME 备份失败!"        fi    donedone
  • 学者可以自己在库中添加几个表格试试看。

13. Nginx访问日志分析

正向代理和反向代理,web服务器

13.1 完整脚本

完整shell脚本nginx_access_log.sh内容如下:

#!/bin/bash# nginx默认日志格式: $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for"LOG_FILE=$1echo "统计访问最多的10个IP"awk '{a[$1]++}END{print "UV:",length(a);for(v in a)print v,a[v]}' $LOG_FILE |sort -k2 -nr |head -10echo "----------------------"echo "统计时间段访问最多的IP"awk '$4>="[30/Jun/2019:11:00:00 +0800]" && $4<="[30/Jun/2019:12:00:00 +0800]"{a[$1]++}END{for(v in a)print v,a[v]}' $LOG_FILE |sort -k2 -nr|head -10echo "----------------------"echo "统计访问最多的10个页面"awk '{a[$7]++}END{print "PV:",length(a);for(v in a){if(a[v]>10)print v,a[v]}}' $LOG_FILE |sort -k2 -nrecho "----------------------"echo "统计访问页面状态码数量"# 超过5次的才打印awk '{a[$7" "$9]++}END{for(v in a){if(a[v]>5)print v,a[v]}}' $LOG_FILE |sort -k3 -nr

13.2 安装nginx

yum install -y gcc gcc-c++ make openssl-devel pcre-devel wget[root@gzr ~]# wget http://nginx.org/download/nginx-1.17.0.tar.gz[root@gzr ~]# tar zxvf nginx-1.17.0.tar.gz[root@gzr ~]# cd nginx-1.17.0[root@gzr nginx-1.17.0]# ./configure --prefix=/usr/local/nginx --with-http_ssl_module --with-http_stub_status_module --with-stream[root@gzr nginx-1.17.0]# make -j 4 && make install[root@gzr nginx-1.17.0]# vim /etc/profilePATH=$PATH:/usr/local/nginx/sbinexport PATH[root@gzr nginx-1.17.0]# source /etc/profile

13.3 测试访问

1561863884638

[root@gzr logs]# cat /var/log/nginx/access.log210.26.55.133 - - [30/Jun/2019:11:05:03 +0800] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36" "-"210.26.55.133 - - [30/Jun/2019:11:05:06 +0800] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36" "-"210.26.55.133 - - [30/Jun/2019:11:05:10 +0800] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36" "-"210.26.55.133 - - [30/Jun/2019:11:05:12 +0800] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36" "-"
  • nginx日志默认存放在/var/log/nginx下

13.3.1 访问最多的IP

# 采用awk去重统计,因为IP是第一列,$1,采用数组累加其值[root@gzr ~]# awk '{a[$1]++}END{for(v in a)print a[v],v}' /var/log/nginx/access.log6 210.26.55.133

13.3.2 访问最多的页面

[root@gzr ~]# awk '{a[$7]++}END{for(v in a)print a[v],v}' /var/log/nginx/access.log5 /1 /favicon.ico

13.3.3 统计访问状态码的数量

# 其中日志第9列为状态码[root@gzr ~]# awk '{a[$7" "$9]++}END{for(v in a)print a[v],v}' /var/log/nginx/access.log4 / 3041 / 2001 /favicon.ico 404

13.3.4 根据时间段来查看访问最多的IP

# 其中$4为访问时间,可以根据需求来设定,$1为IP[root@gzr ~]# awk '$4>="[30/Jun/2019:11:05:03 +0800]" && $4<="[30/Jun/2019:11:10:03 +0800]" {a[$1]++}END{for(v in a)print a[v],v}' /var/log/nginx/access.log3 210.26.55.133

13.3.5 统计UV和PV

UV:用户访问次数(天),PV:总页面访问次数(天)

# 其中length是awk一个内置函数,可以统计IP的来源,此处采用IP来划分用户,实际上并不准确,存在用户共享路由器,同用一个IP进行上网的方式。[root@gzr ~]# awk '{a[$1]++}END{print "PV:",length(a);for(v in a)print a[v],v}' /var/log/nginx/access.logPV: 47 172.16.4.251 172.16.4.211 172.16.4.226 210.26.55.133

13.4 排序输出

# 安装IP访问次数,逆序输出[root@gzr ~]# awk '{a[$1]++}END{print "UV:",length(a);for(v in a)print v,a[v]}' /var/log/nginx/access.log |sort -k2 -nr |head -10210.26.55.133 28172.16.4.21 24172.16.4.25 7UV: 4172.16.4.22 1# 其余不再演示

13.5 测试脚本

[root@gzr ~]# ./nginx_access_log.sh /var/log/nginx/access.log统计访问最多的10个IP210.26.55.133 28172.16.4.21 24172.16.4.25 7UV: 4172.16.4.22 1----------------------统计时间段访问最多的IP210.26.55.133 28172.16.4.21 24172.16.4.25 7172.16.4.22 1----------------------统计访问最多的10个页面/ 57PV: 2----------------------统计访问页面状态码数量/ 304 30/ 200 27

14. Nginx访问日志自动按天切割

lograte工具可以帮助日志切割,在大型企业中,日志必须切割,因为时间过久,日志全部写入access.log中,会导致文件剧增,影响分析效率,不利于归档。

14.1 完整脚本nginx_log_cutting.sh

#!/bin/bashLOG_DIR=/var/log/nginxYESTERDAY_TIME=$(date -d "yesterday" +%F)LOG_MONTH_DIR=$LOG_DIR/$(date +"%Y-%m")LOG_FILE_LIST="access.log"for LOG_FILE in $LOG_FILE_LIST; do    [ ! -d $LOG_MONTH_DIR ] && mkdir -p $LOG_MONTH_DIR    mv $LOG_DIR/$LOG_FILE $LOG_MONTH_DIR/${LOG_FILE}_${YESTERDAY_TIME}donekill -USR1 $(cat /var/run/nginx.pid)

采用nginx自带的机制

通过查看nginx的pid,激活nginx写入日志

[root@gzr ~]# cat /var/run/nginx.pid 4813# 先把现有的日志移走[root@gzr nginx]# mv access.log /tmp/[root@gzr nginx]# lsaccess.log-20190501  error.log  error.log-20190501  mrtg# 通过给nginx的pid发送USR1信号使其接收日志[root@gzr nginx]# kill -USR1 $(cat /var/run/nginx.pid)[root@gzr nginx]# lsaccess.log  access.log-20190501  error.log  error.log-20190501  mrtg# 重新打开浏览器,生成访问日志

14.2 思路历程

# 定义日志目录LOG_DIR=/var/log/nginx# 获取前一天的时间[root@gzr ~]# date -d "yesterday" +%F2019-06-29# 获取月,可以按月来划分目录[root@gzr ~]# date +"%Y-%m"2019-06# 遍历循环,当月的目录不存在时,手动创建for LOG_FILE in $LOG_FILE_LIST; do    [ ! -d $LOG_MONTH_DIR ] && mkdir -p $LOG_MONTH_DIR    mv $LOG_DIR/$LOG_FILE $LOG_MONTH_DIR/${LOG_FILE}_${YESTERDAY_TIME}done

14.3 测试脚本

[root@gzr ~]# chmod +x nginx_log_cutting.sh[root@gzr ~]# ./nginx_log_cutting.sh [root@gzr ~]# cd /var/log/nginx/2019-06/             access.log           access.log-20190501  error.log            error.log-20190501   mrtg/[root@gzr ~]# cd /var/log/nginx/2019-06/[root@gzr 2019-06]# lsaccess.log_2019-06-29# 发现原先目录下没有日志[root@gzr nginx]# cat access.log# 执行脚本,发现日志被归档到了[root@gzr ~]# ./nginx_log_cutting.sh[root@gzr 2019-06]# cat access.log_2019-06-29 210.26.55.133 - - [30/Jun/2019:15:12:07 +0800] "GET /favicon.ico HTTP/1.1" 404 555 "http://172.16.4.11/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36" "-"210.26.55.133 - - [30/Jun/2019:15:12:35 +0800] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36" "-"210.26.55.133 - - [30/Jun/2019:15:13:06 +0800] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36" "-"# 可以每天零点归档日志,采用crontab形成定时任务0 0 * * * /bin/bash /shell/nginx_log_cutting.sh

15. 自动发布Java项目(Tomcat)

15.1 完整脚本

完整java_tomacat.sh内容如下:

#!/bin/bashDATE=$(date +%F_%T)TOMCAT_NAME=$1TOMCAT_DIR=/usr/local/$TOMCAT_NAMEROOT=$TOMCAT_DIR/webapps/ROOTBACKUP_DIR=/data/backupWORK_DIR=/tmpPROJECT_NAME=tomcat-java-demo# 拉取代码cd $WORK_DIRif [ ! -d $PROJECT_NAME ]; then   git clone https://github.com/Colin-Root/tomcat-java-demo.git   cd $PROJECT_NAMEelse   cd $PROJECT_NAME   git pullfi# 构建mvn clean package -Dmaven.test.skip=trueif [ $? -ne 0 ]; then   echo "maven build failure!"   exit 1fi# 部署TOMCAT_PID=$(ps -ef |grep "$TOMCAT_NAME" |egrep -v "grep|$$" |awk 'NR==1{print $2}')[ -n "$TOMCAT_PID" ] && kill -9 $TOMCAT_PID[ -d $ROOT ] && mv $ROOT $BACKUP_DIR/${TOMCAT_NAME}_ROOT$DATEunzip $WORK_DIR/$PROJECT_NAME/target/*.war -d $ROOT$TOMCAT_DIR/bin/startup.sh

15.2 基本需求

代码已经提交到版本仓库,执行shell脚本一键部署,java是通过“jar包或者war包”,然后部署到tomcat或则resin容器中。

15.3 准备环境

(1)安装java

官方地址:https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html

# 下载安装包[root@gzr ~]# wget https://download.oracle.com/otn/java/jdk/8u211-b12/478a62b7d4e34b78b671c754eaaf38ab/jdk-8u211-linux-x64.tar.gz?AuthParam=1561879971_bf4be7ea27c8888f2a582eece07577e6# 创建安装目录[root@gzr ~]# mkdir -p /usr/local/java/# 解压至安装目录[root@gzr ~]# tar -zxvf jdk-8u211-linux-x64.tar.gz -C /usr/local/java/# 设置环境变量,在/etc/profile末尾添加:export JAVA_HOME=/usr/local/java/jdk1.8.0_211export JRE_HOME=${JAVA_HOME}/jreexport CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/libexport PATH=${JAVA_HOME}/bin:$PATH# 使环境变量生效source /etc/profile# 添加软链接[root@gzr java]# ln -s /usr/local/java/jdk1.8.0_211/bin/java /usr/bin/java # 检查版本[root@gzr java]# java -versionjava version "1.8.0_211"Java(TM) SE Runtime Environment (build 1.8.0_211-b12)Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)

(2)安装tomcat

官方地址:http://tomcat.apache.org/

# 下载tomcat[root@gzr ~]# wget https://www-eu.apache.org/dist/tomcat/tomcat-8/v8.5.42/bin/apache-tomcat-8.5.42.tar.gz# 解压tomcat[root@gzr ~]# tar zxvf apache-tomcat-8.5.42.tar.gz# 创建安装目录[root@gzr ~]# mkdir -p /usr/local/tomcat# 移动至安装目录[root@gzr ~]# mv apache-tomcat-8.5.42/* /usr/local/tomcat# 测试tomcat启动是否正常[root@gzr ~]# /usr/local/tomcat/bin/./startup.sh ......Tomcat started.[root@gzr ~]# tail /usr/local/tomcat/logs/catalina.out......30-Jun-2019 15:51:28.228 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in 521 ms

1561881376121

15.4 思路历程

(1)目录准备

# 首先是一个当前的时间获取DATE=$(date +%F_%T)# tomcat的名称,/usr/local下命名的名称TOMCAT_NAME=$1TOMCAT_DIR=/usr/local/$TOMCAT_NAME# tomcat的根目录ROOT=$TOMCAT_DIR/webapps/ROOT# 备份的目录[root@gzr bin]# mkdir -p /data/backupBACKUP_DIR=/data/backupWORK_DIR=/tmpPROJECT_NAME=tomcat-java-demo

(2)拉取代码

# 在拉取代码前需要安装git工具[root@gzr ~]# yum install -y git# 拉取官方镜像,存放到私人镜像仓库[root@k8s-master ~]# docker pull tomcat:latest[root@k8s-master ~]# docker login -u gezr17Password: [root@k8s-master ~]# docker tag tomcat:latest gezr17/tomcat[root@k8s-master ~]# docker push gezr17/tomcat# 正式拉取代码cd $WORK_DIRif [ ! -d $PROJECT_NAME ]; then   git clone https://github.com/Colin-Root/tomcat-java-demo.git   cd $PROJECT_NAMEelse   cd $PROJECT_NAME   # 第二次拉取会拉取增量更新   git pullfi

(3)构建并部署项目

# 通常采用mvn构建java项目# 首先下载安装maven,其官方地址为:http://maven.apache.org/download.cgi[root@gzr ~]# wget https://www-us.apache.org/dist/maven/maven-3/3.6.1/binaries/apache-maven-3.6.1-bin.tar.gz# 解压到并移动到安装目录下[root@gzr ~]# tar zxvf apache-maven-3.6.1-bin.tar.gz[root@gzr ~]# mv apache-maven-3.6.1 /usr/local/maven3.6# 将maven命令写到环境变量配置文件/etc/profile中# 拼接到已存在的path中即可PATH=$PATH:/usr/local/nginx/sbin:/usr/local/maven3.6/bin# 使其生效source /etc/profile# 检查maven的版本,验证配置正确[root@gzr bin]# mvn -vApache Maven 3.6.1 (d66c9c0b3152b2e69ee9bac180bb8fcc8e6af555; 2019-04-05T03:00:29+08:00)Maven home: /usr/local/maven3.6Java version: 1.8.0_211, vendor: Oracle Corporation, runtime: /usr/local/java/jdk1.8.0_211/jreDefault locale: en_US, platform encoding: UTF-8OS name: "linux", version: "3.10.0-957.10.1.el7.x86_64", arch: "amd64", family: "unix"# 如果maven构建成功,则进行下一步,否则返回“maven build failure!”,跳过测试用例构建[root@gzr tomcat-java-demo]# mvn clean package -Dmaven.test.skip=true......[INFO] Packaging webapp[INFO] Assembling webapp [ly-simple-tomcat] in [/root/tomcat-java-demo/target/ly-simple-tomcat-0.0.1-SNAPSHOT][INFO] Processing war project[INFO] Webapp assembled in [130 msecs][INFO] Building war: /root/tomcat-java-demo/target/ly-simple-tomcat-0.0.1-SNAPSHOT.war[INFO] ------------------------------------------------------------------------[INFO] BUILD SUCCESS[INFO] ------------------------------------------------------------------------[INFO] Total time:  02:07 min[INFO] Finished at: 2019-06-30T17:09:39+08:00[INFO] ------------------------------------------------------------------------# 此时在目录下会生成一个target目录,其下的war包就是编译好的可以直接部署的包[root@gzr tomcat-java-demo]# ls target/classes  generated-sources  ly-simple-tomcat-0.0.1-SNAPSHOT  ly-simple-tomcat-0.0.1-SNAPSHOT.war  maven-archiver  maven-statusmvn clean package -Dmaven.test.skip=trueif [ $? -ne 0 ]; then   echo "maven build failure!"   exit 1fi# 安装unzip工具[root@gzr bin]# yum install unzip -y# 查看网站根目录的[root@gzr tomcat-java-demo]# ls /usr/local/tomcat/webapps/ROOT/asf-logo-wide.svg  bg-middle.png    bg-na***    favicon.ico  RELEASE-NOTES.txt  tomcat.gif  tomcat-power.gif  WEB-INFbg-button.png      bg-nav-item.png  bg-upper.png  index.jsp    tomcat.css         tomcat.png  tomcat.svg# 获取tomcat的pid,如果不为空,就kill掉TOMCAT_PID=$(ps -ef |grep "$TOMCAT_NAME" |egrep -v "grep|$$" |awk 'NR==1{print $2}')[ -n "$TOMCAT_PID" ] && kill -9 $TOMCAT_PID[ -d $ROOT ] && mv $ROOT $BACKUP_DIR/${TOMCAT_NAME}_ROOT$DATEunzip $WORK_DIR/$PROJECT_NAME/target/*.war -d $ROOT$TOMCAT_DIR/bin/startup.sh

15.5 测试脚本

# 由于在实际应用场合可能会存在多个tomcat,比如在/usr/local/demo-tomcat  /usr/local/jenkins-tomcat,所以此处设定一个传入参数,方便使用[root@gzr ~]# ./java_tomacat.sh tomcatAlready up-to-date.[INFO] Scanning for projects.........[INFO] ------------------------------------------------------------------------[INFO] BUILD SUCCESS[INFO] ------------------------------------------------------------------------[INFO] Total time:  3.811 s[INFO] Finished at: 2019-07-01T10:24:47+08:00[INFO] ------------------------------------------------------------------------Archive:  /tmp/tomcat-java-demo/target/ly-simple-tomcat-0.0.1-SNAPSHOT.war.......Tomcat started.# 此时tomcat应该启动成功了[root@gzr ~]# ps -ef | grep tomcatroot      14378      1  4 10:24 pts/0    00:00:14 /usr/local/java/jdk1.8.0_211/jre/bin/java -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dorg.apache.catalina.security.SecurityListener.UMASK=0027 -Dignore.endorsed.dirs= -classpath /usr/local/tomcat/bin/bootstrap.jar:/usr/local/tomcat/bin/tomcat-juli.jar -Dcatalina.base=/usr/local/tomcat -Dcatalina.home=/usr/local/tomcat -Djava.io.tmpdir=/usr/local/tomcat/temp org.apache.catalina.startup.Bootstrap startroot      14439  14108  0 10:29 pts/0    00:00:00 grep --color=auto tomcat
  • 在浏览器打开测试访问正常

1561948387161

15.6 备份目录

[root@gzr ~]# ls /data/backup/tomcat_ROOT2019-07-01_10:24:41# 可以在github上修改某些文件,然后重新执行脚本,使其拉取最新修改的文件,然后实现同步部署功能

16. 自动发布PHP项目

PHP不需要编译构建,思路:拉取代码,同步代码,相对于案例15,少了一步编译构建。

16.1 完整shell脚本php_web.sh

#!/bin/bashDATE=$(date +%F_%T)WWWROOT=/usr/local/nginx/html/$1BACKUP_DIR=/data/backupWORK_DIR=/tmpPROJECT_NAME=php-demo# 拉取代码cd $WORK_DIRif [ ! -d $PROJECT_NAME ]; then   git clone https://github.com/Colin-Root/php-demo.git   cd $PROJECT_NAMEelse   cd $PROJECT_NAME   git pullfi# 部署if [ ! -d $WWWROOT ]; then   mkdir -p $WWWROOT   rsync -avz --exclude=.git $WORK_DIR/$PROJECT_NAME/* $WWWROOTelse   rsync -avz --exclude=.git $WORK_DIR/$PROJECT_NAME/* $WWWROOTfi

16.2 思路历程

(1)创建所需目录

# 其中$1表示指定哪个项目,需要在执行脚本时指定WWWROOT=/usr/local/nginx/html/$1BACKUP_DIR=/data/backupWORK_DIR=/tmp# 项目名PROJECT_NAME=php-demo

(2)拉取代码

# 拉取代码cd $WORK_DIRif [ ! -d $PROJECT_NAME ]; then   git clone https://github.com/Colin-Root/php-demo.git   cd $PROJECT_NAMEelse   cd $PROJECT_NAME   git pullfi

(3)配置nginx环境

# 编辑/usr/local/nginx/conf/nginx.conf文件添加如下两个虚拟主机   server {        listen       80;        server_name  localhost;        access_log logs/demo1.access.log main;        location / {            root   html/demo1;            index  index.html index.htm;        }        location ~ \.php$ {            root           html/demo1;            fastcgi_pass   127.0.0.1:9000;            fastcgi_index  index.php;            fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;            include        fastcgi_params;        }    }    server {        listen       81;        server_name  localhost;        access_log logs/demo2.access.log main;              location / {            root   html/demo2;            index  index.html index.htm;        }        error_page   500 502 503 504  /50x.html;        location = /50x.html {            root   html;        }        location ~ \.php$ {            root           html/demo2;            fastcgi_pass   127.0.0.1:9000;            fastcgi_index  index.php;            fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;            include        fastcgi_params;        }    }# 重载nginx配置,我已经把nginx命令加入环境变量,并启动[root@gzr ~]# nginx -t[root@gzr ~]# nginx -s reload

(3)部署

此处采用rsync来同步,它是做文件增量式同步的工具

# 部署,rsync是从git拉取之后,直接同步到项目目录下if [ ! -d $WWWROOT ]; then   mkdir -p $WWWROOT   rsync -avz --exclude=.git $WORK_DIR/$PROJECT_NAME/* $WWWROOTelse   # 同步项目名下的所有文件到根目录下   rsync -avz --exclude=.git $WORK_DIR/$PROJECT_NAME/* $WWWROOTfi

16.3 测试脚本

[root@gzr ~]# bash php_web.sh demo1Already up-to-date.sending incremental file listREADME.mdindex.phpsent 202 bytes  received 54 bytes  512.00 bytes/sectotal size is 53  speedup is 0.21[root@gzr ~]# ls /usr/local/nginx/html/demo1/index.php  README.md
  • 在浏览器打开即可访问phpinfo页面

17. DOS攻击防范

DOS: 它是一种拒绝服务的攻击,属于点对点的攻击,通常是一台或者几台(通常是伪造了虚拟IP)。典型的攻击就是TCP半连接。现在已经演变成了DDOS攻击(泛洪攻击),需要上高防

17.1 需求背景

查询出异常IP,屏蔽异常访问IP,如果在一分钟之内超过100次,认为是异常。CC攻击()

17.2 完整Shell脚本

编写shell脚本dos_attack.sh

#!/bin/bashDATE=$(date +%d/%b/%Y:%H:%M)LOG_FILE=/var/log/nginx/access.logABNORMAL_IP=$(tail -n5000 $LOG_FILE |grep $DATE |awk '{a[$1]++}END{for(i in a)if(a[i]>100)print i}')for IP in $ABNORMAL_IP; do    if [ $(iptables -vnL |grep -c "$IP") -eq 0 ]; then        iptables -I INPUT -s $IP -j DROP        echo "$(date +'%F_%T') $IP" >> /tmp/drop_ip.log    fidone

17.3 思路历程

# 获取当前时间[root@gzr ~]# date +%d/%b/%Y:%H:%M01/Jul/2019:20:19# 获取当前一分钟的日志[root@gzr ~]# grep $(date +%d/%b/%Y:%H:%M) /var/log/nginx/access.log 210.26.55.133 - - [01/Jul/2019:20:22:23 +0800] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36" "-"# 取出这一分钟的日志的次数[root@gzr ~]# grep $(date +%d/%b/%Y:%H:%M) /var/log/nginx/access.log  |awk '{a[$1]++}END{for(i in a)print a[i],i}'29 210.26.55.133# 考虑到如果网站访问量比较大,那么生成的日志数可能会比较多,所以tail -n5000,取出日志中的后5000行,当然可以根据自身网站的访问值,动态地去修改此值。如果这个IP在1分钟内大于100,则判定为异常tail -n5000 $LOG_FILE |grep $DATE |awk '{a[$1]++}END{for(i in a)if(a[i]>100)print i}'# 采用iptables禁用IP访问,将异常访问的IP,直接DROP掉。for IP in $ABNORMAL_IP; do    # 判断是否已经把IP屏蔽过,如果等于0 说明没有    if [ $(iptables -vnL |grep -c "$IP") -eq 0 ]; then        iptables -I INPUT -s $IP -j DROP        echo "$(date +'%F_%T') $IP" >> /tmp/drop_ip.log    fidone

17.4 测试脚本

# 为了方便测试,我们先把阈值调整为10,并先把iptables -I INPUT -s $IP -j DROP此行注释掉,多刷新几次[root@gzr ~]# bash -x dos_attack.sh ++ date +%d/%b/%Y:%H:%M+ DATE=01/Jul/2019:21:01+ LOG_FILE=/var/log/nginx/access.log++ tail -n5000 /var/log/nginx/access.log++ grep 01/Jul/2019:21:01++ awk '{a[$1]++}END{for(i in a)if(a[i]>10)print i}'+ ABNORMAL_IP=210.26.55.133+ for IP in '$ABNORMAL_IP'++ grep -c 210.26.55.133++ iptables -vnL+ '[' 0 -eq 0 ']'++ date +%F_%T+ echo '2019-07-01_21:01:23 210.26.55.133'[root@gzr ~]# cat /tmp/drop_ip.log 2019-07-01_21:01:23 210.26.55.133# 将注释行取消,然后重新刷新浏览器进行访问,使其次数超过10次
  • 注意如果210.26.55.133是ssh远程登录的主机,那么可能会出现断开Xshell的情况,因为IP被屏蔽掉了

1561986535438

  • 浏览器访问nginx主页会发现也打不开了

1561986582249

  • 解决办法:可以通过另一个IP,SSH远程登录到该台虚拟机,清空其iptables规则。或者重新接受这个IP
iptables -D INPUT -s 210.26.55.133 -j DROP
  • 此案例也可以用于ssh防暴力破解。只要在/var/log/audit/audit.log能读到ssh访问是否成功的次数

18. 目录文件变化监控与实时文件同步

实现对某个目录进行监控,防止删除、创建目录。在现在比较流行的病毒中,比如挖矿病毒(在系统里植入后门,采用应用程序和系统漏洞),勒索病毒。

监听敏感目录,比如/usr/bin 和/wwwroot等。

18.1 完整脚本

编写shell脚本dir_detection.sh

#!/bin/bashMON_DIR=/optinotifywait -mqr --format %f -e create $MON_DIR |\while read files; do   rsync -avz /opt /tmp/opt   echo "$(date + '%F %T') $files" >> file.mon.log   #echo "$(date +'%F %T') create $files" | mail -s "dir monitor" xxx@163.comdone

18.2 inotfy工具

(1)inotfy介绍

Inotify 是一个 Linux特性,它监控文件系统操作,比如读取、写入和创建。Inotify 反应灵敏,用法非常简单,并且比 cron 任务的繁忙轮询高效得多。学习如何将 inotify 集成到您的应用程序中,并发现一组可用来进一步自动化系统治理的命令行工具。

(2)应用场景

文件监控可以配合rsync实现文件自动同步,例如监听某个目录,当文件变化时,使用rsync命令将变化的文件同步。(可用于代码自动发布)

(3)inotifywait命令使用

参数 说明
-m 持续监听
-r 使用递归形式监视目录
-q 减少冗余信息,只打印出需要的信息
-e 指定要监视的事件,多个时间使用逗号隔开
--timefmt 时间格式
--format 监听到的文件变化的信息
  • ​ --timefmt 说明:

ymd分别表示年月日,H表示小时,M表示分钟

  • --format 说明:
参数 说明
%w 表示发生事件的目录
%f 表示发生事件的文件
%e 表示发生的事件
%Xe 事件以“X”分隔
%T 使用由–timefmt定义的时间格式
  • 可监听的时间
参数 说明
access 访问,读取文件。
modify 修改,文件内容被修改。
attrib 属性,文件元数据被修改。
move 移动,对文件进行移动操作。
create 创建,生成新文件
open 打开,对文件进行打开操作。
close 关闭,对文件进行关闭操作。
delete 删除,文件被删除。

(4)安装inotfy

[root@gzr ~]# yum install -y inotify-tools

18.3 思路历程

# inotfy所监听的结果,会作为while的输入,此处只监听是否有新文件目录在/opt下创建inotifywait -mqr --format %f -e create $MON_DIR |\while read files; do   rsync -avz /opt /tmp/opt   #echo "$(date +'%F %T') create $files" | mail -s "dir monitor" xxx@163.comdone

18.4 测试脚本

# 运行dir_detection.sh,[root@gzr ~]# chmod +x dir_detection.sh [root@gzr ~]# bash -x dir_detection.sh + MON_DIR=/opt+ read files+ inotifywait -mqr --format %f -e create /opt# 重开一个终端,在/opt下创建一个test文件,会发现检测到了新建的文件[root@gzr ~]# bash -x dir_detection.sh + MON_DIR=/opt+ read files+ inotifywait -mqr --format %f -e create /opt+ rsync -avz /opt /tmp/optsending incremental file listcreated directory /tmp/optopt/opt/testsent 114 bytes  received 70 bytes  368.00 bytes/sectotal size is 0  speedup is 0.00+ read files# 将写入的信息记录下来[root@gzr ~]# cat file.mon.log  test# 如果有文件更新,实现告警通知echo "$(date +'%F %T') create $files" | mail -s "dir monitor" xxx@163.com# 可以实现实时的文件同步,下述可以实现/opt 和/tmp/opt的文件rsync -avz /opt /tmp/opt[root@gzr ~]# ls /opt/test  test1[root@gzr ~]# ls /tmp/opt/opt/test  test1
  • 要做实时同步,一般来说是采用守护进程的方式执行
[root@gzr ~]# nohup bash dir_detection.sh &>/dev/null &[1] 5031[root@gzr ~]# ps -ef | grep dir_detection.sh root       5031   4811  0 22:00 pts/0    00:00:00 bash dir_detection.shroot       5033   5031  0 22:00 pts/0    00:00:00 bash dir_detection.sh

19. 获取随机字符串或数字

19.1 应用场景

在一些应用场景中,通常需要生成一些随机规定长度的字符串,比如给公司新来的员工分配用户名和密码

19.2 思路历程

19.2.1 采用RANDOM函数

shell有一个环境变量RANDOM,范围是0–32767,md5sum命令用于生成和校验文件的md5值。它会逐位对文件的内容进行校验。是文件的内容,与文件名无关,也就是文件内容相同,其md5值相同。md5值是一个128位的二进制数据,转换成16进制则是32(128/4)位的进制值。

md5校验,有很小的概率不同的文件生成的md5可能相同。比md5更安全的校验算法还有SHA*系列的。

[root@gzr ~]# echo $RANDOM |md5sum |cut -c 1-883a0d20f

19.2.2 采用date函数

这个也是我们经常用到的,可以说时间是唯一的,也不会重复的,从这个里面获得同一时间的唯一值。适应所有程序里面了。

# 获取时间戳,当前到1970-01-01 00:00:00 相隔的秒数#如果用它做随机数,相同一秒的数据是一样的。在做循环处理,多线程里面基本不能满足要求了。[root@gzr ~]# date +%s1562122809# 获取当前时间的纳秒数据,精确到亿分之一秒,这个相当精确了,就算在多cpu,大量循环里面,同一秒里面,也很难出现相同结果,不过不同时间里面还会有大量重复碰撞[root@gzr ~]# date +%N005513180#这个可以说比较完美了,加入了时间戳,又加上了纳秒[root@gzr ~]# date +%s%N1562123320789794522[root@gzr ~]# date +%s |md5sum |cut -c 1-8da9dca0b

19.2.3 采用/dev/random, urandom

/dev/random设备,存储着系统当前运行的环境的实时数据。它可以看作是系统某个时候,唯一值数据,因此可以用作随机数元数据。我们可以通过文件读取方式,读得里面数据。/dev/urandom这个设备数据与random里面一样。只是,它是非阻塞的随机数发生器,读取操作不会产生阻塞。

# cut以“ ”分割,然后得到分割的第一个字段数据[root@gzr ~]# head -1 /dev/urandom |cksum |cut -f1 -d" "2135286271

19.2.4 uuid

UUID码全称是通用唯一识别码 (Universally Unique Identifier, UUID),它 是一个软件建构的标准,亦为自由软件基金会 (Open Software Foundation, OSF) 的组织在分布式计算环境 (Distributed Computing Environment, DCE) 领域的一部份。

UUID 的目的,是让分布式系统中的所有元素,都能有唯一的辨识信息,而不需要通过中央控制端来做辨识信息的指定。如此一来,每个人都可以创建不与其它人冲突的 UUID。在这样的情况下,就不需考虑数据库创建时的名称重复问题。它会让网络任何一台计算机所生成的uuid码,都是互联网整个服务器网络中唯一的。它的原信息会加入硬件,时间,机器当前运行信息等等。

**UUID格式是:**包含32个16进位数字,以“-”连接号分为五段,形式为8-4-4-4-12的32个字符。范例;550e8400-e29b-41d4-a716-446655440000 ,所以:UUID理论上的总数为216 x 8=2128,约等于3.4 x 1038。 也就是说若每奈秒产生1兆个UUID,要花100亿年才会将所有UUID用完。

其实,大家做数据库设计时候,肯定听说过,guid(全局唯一标识符)码,它其实是与uuid类似,由微软支持。 这里编码,基本有操作系统内核产生。大家记得把,在windows里面,无论数据库,还是其它软件,很容易得到这个uuid编码。

# linux的uuid码也是有内核提供的,在/proc/sys/kernel/random/uuid这个文件内。其实,random目录,里面还有很多其它文件,都与生成uuid有关系的。# 连续2次读取,得到的uuid是不同的[root@gzr ~]# cat /proc/sys/kernel/random/uuid 9c0f07d3-8416-4aa7-8f0b-a8dde78ba214You have new mail in /var/spool/mail/root[root@gzr ~]# cat /proc/sys/kernel/random/uuid 31592bf3-63b5-4df0-bb00-0ecd0e1c1dea[root@gzr ~]# cat /proc/sys/kernel/random/uuid |cksum |cut -f1 -d" "3282048115

19.2.5 openssl

openssl rand 用于产生指定长度个bytes的随机字符。

# 采用ssl获取8位数字[root@gzr ~]# openssl rand -base64 4 |cksum |cut -c 1-881216616# 获取8位字符[root@gzr ~]# openssl rand -base64 4sUZl9A==
  • cksum:打印CRC校验和统计字节

20. 判断输入是否为IP

完整shell脚本check_ip.sh

function check_ip(){     local IP=$1     VALID_CHECK=$(echo $IP|awk -F. '$1<=255&&$2<=255&&$3<=255&&$4<=255{print "yes"}')    if echo $IP|grep -E "^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$" >  /dev/null; then         if [ $VALID_CHECK == "yes" ]; then             return 0        else             echo "$IP not available!"             return 1         fi     else         echo "Format error! Please input again."         return 1     fi } while true; do     read -p "Please enter IP: " IP     check_ip $IP     [ $? -eq 0 ] && break || continue done

21. 统计5个100以内的数的和、最小和最大

21.1 比较

21.1.1 数值比较

比较 描述
n1 -eq n2 检查n1是否与n2相等
n1 -ge n2 检查n1是否大于等于n2
n1 -gt n2 检查n1是否大于n2
n1 -le n2 检查n1是否小于等于n2
n1 -lt n2 检查n1是否小于n2
n1 -ne n2 检查n1是否不等于n2

完整的shell脚本5_sum.sh

#!/bin/bashCOUNT=1 SUM=0 while [ $COUNT -le 5 ]; do     read -p "请输入1-10个整数:" INT     if [[ ! $INT =~ ^[0-9]+$ ]]; then         echo "输入必须是整数!"         exit 1     elif [[ $INT -gt 100 ]]; then         echo "输入必须是100以内!"         exit 1     else        SUM=$(($SUM+$INT))        if [[ $COUNT -eq 1 ]]; then            MIN=$INT            MAX=$INT            echo "SUM: $SUM"             echo "MIN: $MIN"             echo "MAX: $MAX"        else            if [[ $MIN -ge $INT ]];then                MIN=$INT            elif [[ $MAX -le $INT ]];then                MAX=$INT            else                MAX=$MAX                MIN=$MIN            fi            echo "SUM: $SUM"             echo "MIN: $MIN"             echo "MAX: $MAX"        fi     fi     let COUNT++done 

21.1.2 字符串比较

比较 测试
str1 = str2 检查str1是否与str2相同
str1 != str2 检查str1是否与str2不同
str1 < str2 检查str1是否比str2
str1 > str2 检查str1是否比str2大
-n str1 检查str1的长度是否非0
-z str1 检查str1的长度是否为0
  • 注意,当要比较一个字符串是否比另外一个字符串大或者小时,“>”和“<”必须转义,因为脚本容易将其误认为是重定向符号
  • 空的和未初始化的变量会对Shell脚本测试造成灾难性的影响,如果不是很确定一个变量的内容,最好将其用于数值或者字符串比较前先通过-n或-z来测试一下变量是否含有值。

21.1.3 文件比较(重点!!!)

此类比较测试是Shell编程中最为强大,也是应用最多的比较形式,它允许你测试Linux文件系统上文件和目录的状态。详情如下:

比较 描述
-d file 检查file是否存在并且是否是一个目录
-e file 检查file是否存在
-f file 检查file是否存在并是一个文件
-r file 检查file是否存在并可读
-s file 检查file
-w file 检查file
-x file 检查file是否存在并可执行
-O file 检查file是否存在并属当前用户所有
-G file 检查file是否存在并且默认组与当前用户相同
file1 -nt file2 检查file1是否比file2新
file1 -ot file2 检查file1是否比file2旧

22. 管理用户账户

22.1 功能描述

删除用户在管理账户工作中比较复杂,在删除用户时,至少需要4个步骤:

(1)获得正确的待删除用户账户名;

(2)杀死正在系统上运行的属于该账户的进程;

(3)确认系统中属于该账户的所有文件;

(4)删除该用户账户。

22.2 完整脚本

#!/bin/bashfunction get_answer {unset ANSWERASK_COUNT=0while [ -z "$ANSWER" ]; do    ASK_COUNT=$[ $ASK_COUNT + 1 ]    case $ASK_COUNT in    2)        echo        echo "Please answer the question."        echo     ;;    3)        echo        echo "One last try...please answer the question."        echo    ;;    4)        echo        echo "Since you refuse to answer the the question..."        echo "exiting program."        echo         exit    ;;    esac    echo    if [ -n "$LINE2" ]; then        echo $LINE1        echo -e $LINE2" \c"    else        echo -e $LINE1" \c"    fi    read -t 60 ANSWERdoneunset LINE1unset LINE2}function process_answer {case $ANSWER iny|Y|YES|yes|Yes|yEs|yeS|YEs|yES);;*)    echo    echo $EXIT_LINE1    echo $EXIT_LINE2    echo    exit;;esacunset EXIT_LINE1unset EXIT_LINE2}echo "Step #1 - Determine User Account name to Delete "echoLINE1="Please enter the username of the user "LINE2="account you wish to delete from system:"get_answerUSER_ACCOUNT=$ANSWERLINE1="Is $USER_ACCOUNT the user account "LINE2="you wish to delete from the system? [y/n]"get_answerEXIT_LINE1="Because the account, $USER_ACCOUNT, is not "EXIT_LINE2="the one you wish to delete, we are leaving the script..."process_answer#--------------Check that USER_ACCOUNT is really an account on the system---------#USER_ACCOUNT_RECORD=$(cat /etc/passwd |grep -w $USER_ACCOUNT)if [ $? -eq 1 ]; then    echo    echo "Account, $USER_ACCOUNT, not found. "    echo "Leaving the script..."    echo    exitfiechoecho "I found this record:"echo $USER_ACCOUNT_RECORDLINE1="Is this the correct User Account? [y/n]"get_answerEXIT_LINE1="Because the account, $USER_ACCOUNT, is not "EXIT_LINE2="the one you wish to delete, we are leaving the script..."process_answer#--------Search for any running processes that belong to the User Account--------#echoecho "Step #2 - Find process on system belonging to user account"echops -u $USER_ACCOUNT >/dev/null case $? in1)    echo "There are no processes for this account currently running."    echo ;;0)      echo "$USER_ACCOUNT has the following processes running: "    echo    ps -u $USER_ACCOUNT        LINE1="Would you like me to kill the process(es)? [y/n]"    get_answer    case $ANSWER in    y|Y|YES|yes|Yes|yEs|yeS|YEs|yES)    echo    echo "Killing off process(es)..."        COMMAND_1="ps -u $USER_ACCOUNT --no-heading"    COMMAND_3="xargs -d \\n /usr/bin/sudo /bin/kill -9"        $COMMAND_1 |gawk '{print $1}' |$COMMAND_3    echo    echo "Process(es) killed."    ;;    *)    echo    echo "Will not kill the process(es)"    echo    ;;    esac;;esac#---------Create a report of all files owned by User Account--------------------#echoecho "Step #3 - Find files on system belonging to user account"echoecho "Creating a report of all files owned by $USER_ACCOUNT."echoecho "It is  recommended that you backup/archive these files, and then do one of two things:"echo " 1) Delete the files"echo " 2) Change the files'  ownership to a current user account."echoecho "Please wait. This may take a while..."REPORT_DATE=$(date +%y%m%d)REPORT_FILES=$USER_ACCOUNT"_FILES_"$REPORT_DATE".rpt"find / -user $USER_ACCOUNT > $REPORT_FILE 2>/dev/nullecho echo "REPORT is complete."echo "Name of report:      $REPORT_FILE"echo "Location of report:  $(pwd)"echo #------------------------Remove User Account-------------------------------#echoecho "Step #4 - Remove user account"echoLINE1="Remove $USER_ACCOUNT's account from system? [y/n]"get_answerEXIT_LINE1="Since you do not wish to remove the user account,"EXIT_LINE2="$USER_ACCOUNT at this time, exiting the script..."process_answeruserdel $USER_ACCOUNTechoecho "USER account, $USER_ACCOUNT, has been removed"echoexit

22.3 思路历程

22.3.1 获取正确的账户名

账户删除过程中的第一步重要:获取待删除的用户账户的正确名称,由于脚本采用的是交互式脚本,所以可以用read命令获取账户名称,如果脚本用户一直没有给出答复,可以在read命令中用-t选项,在超时前给用户60秒时间来回答问题

echo "Please enter the username of the user"echo -e "account you wish to delete from system: \c"read -t 60 ANSWER

给用户三次机会来回答需要删除的账户,采用while循环,并加-z选项,判断用户输入的ANSWEER变量是否为空。在脚本第一次进入While循环时,ANSWER变量的内容为空,用来给变量赋值的提问位于循环的底部。

while [ -z "$ANSWER" ]do[...]echo "Please enter the username of the user"echo -e "account you wish to delete from system: \c"read -t 60 ANSWERdone

当第一次提问出现超时,当只剩下一次回答机会时,或当出现其他情况下,需要跟脚本用户交互,case语句是最适合这里的结构化命令,通过给ASK_COUNT变量增值,可以设定不同的消息来回应脚本。这部分代码如下:

case $ASK_COUNT in    2)        echo        echo "Please answer the question."        echo     ;;    3)        echo        echo "One last try...please answer the question."        echo    ;;    4)        echo        echo "Since you refuse to answer the the question..."        echo "exiting program."        echo         exit    ;;    esac

为了实现代码的重用,我们将上述代码放入一个函数中。

22.3.2 创建函数获取正确的账户名

首先,声明函数名get_answer。然后用unset清除脚本用户之前给出的答案。完成这两件事的代码如下:

function get_answer {unset ANSWER

在原来的代码中你要修改的另一处地方是对用户脚本的提问。这个脚本不会每次都问同一个问题,所以我们创建两个新的变量LINE1和LINE2来处理问题。

echo $LINE1echo -e $LINE2" \c"

但是并不是所有问题都要两行显示,有些只要一行,所以,可以采用if结构解决,这个函数会测试LINE2是否为空,如果为空,则只用LINE1.

if [ -n "$LINE2" ]; then    echo $LINE1    echo -e $LINE2" \c"else    echo -e $LINE1" \c"fi

最终函数需要清空LINE1和LINE2变量来清除自己,整个函数如下:

function get_answer {unset ANSWERASK_COUNT=0while [ -z "$ANSWER" ]; do    ASK_COUNT=$[ $ASK_COUNT + 1 ]    case $ASK_COUNT in    2)        echo        echo "Please answer the question."        echo     ;;    3)        echo        echo "One last try...please answer the question."        echo    ;;    4)        echo        echo "Since you refuse to answer the the question..."        echo "exiting program."        echo         exit    ;;    esac    echo    if [ -n "$LINE2" ]; then        echo $LINE1        echo -e $LINE2" \c"    else        echo -e $LINE1" \c"    fi    read -t 60 ANSWERdoneunset LINE1unset LINE2}
  • 要问脚本用户删除哪个用户,需要设置一些变量,然后调用get_answer函数,使用新函数让脚本代码清爽不少。
LINE1="Please enter the username of the user "LINE2="account you wish to delete from system:"get_answerUSER_ACCOUNT=$ANSWER

22.3.3 验证输入的用户名

鉴于可能存在输入错误,应验证用户输入的账户,需要能够提问脚本用户。

LINE1="Is $USER_ACCOUNT the user account "LINE2="you wish to delete from the system? [y/n]"get_answer

提问后,脚本必须处理答案,变量ANSWER再次将脚本用户的回答带回问题中。如果为yes,就得到了要删除的正确的账户,脚本就可以继续执行,否则退出脚本。

case $ANSWER iny|Y|YES|yes|Yes|yEs|yeS|YEs|yES);;*)    echo    echo "Because the account, $USER_ACCOUNT, is not "    echo "the one you wish to delete, we are leaving the script..."    echo    exit;;esac
  • 由于这个脚本有时需要处理多次用户的yes/no回答。因此需要创建一个函数来处理这个任务。只要稍作改动即可,必须声明函数名,给case语句加两个变量,EXIT_LINE1和EXIT_LINE2。这些修改及最后一些变量清理工作就是process_answer函数的全部。
function process_answer {case $ANSWER iny|Y|YES|yes|Yes|yEs|yeS|YEs|yES);;*)    echo    echo $EXIT_LINE1    echo $EXIT_LINE2    echo    exit;;esacunset EXIT_LINE1unset EXIT_LINE2}

现在只用调用函数就可以处理答案了。

EXIT_LINE1="Because the account, $USER_ACCOUNT, is not "EXIT_LINE2="the one you wish to delete, we are leaving the script..."process_answer

22.3.4 确定账户存在

用户确认就是这个账户,但是这个账户未必就真的存在于系统里,仍旧需要核实,并且将完整的账户记录显示给脚本用户。为此需要使用变量USER_ACCOUNT_RECORD,将它设成grep,在/etc/passwd文件中查找该用户账户的输出。-w选项是对这个特定用户进行精确匹配。

USER_ACCOUNT_RECORD=$( cat /etc/passwd |grep -w $USER_ACCOUNT)
  • 如果在/etc/passwd下没有找到该账户,则意味着有可能该账户已被删除或者从来就没存在过,不管怎样,都需要通知脚本用户,并退出。grep命令的退出状态码可以在这里起作用。如果没找到该账户,echo $?的值应该为1。
if [ $? -eq 1 ]; then    echo    echo "Account, $USER_ACCOUNT, not found. "    echo "Leaving the script..."    echo    exitfi
  • 如果该账户存在,仍然需要验证这个账户,我们之前建立的函数可以起作用,需要做的只是设置正确的变量并调用函数
echoecho "I found this record:"echo $USER_ACCOUNT_RECORDLINE1="Is this the correct User Account? [y/n]"get_answerEXIT_LINE1="Because the account, $USER_ACCOUNT, is not "EXIT_LINE2="the one you wish to delete, we are leaving the script..."process_answer

22.3.5 删除属于用户账户的进程

到目前为止,已经验证了要删除用户账户的正确名称,为了从系统删除该用户,该账户不能有任何进程执行,为此,在真正删除该用户之前,必须查找并终止这些进程。

查找用户进程较简单,采用ps -u username即可准确定位该用户下的所有进程,同时采用输出重定向到/dev/null,使用户看不到输出信息,因为如果没有找到相关进程,ps命令只会显示一个标题,容易把脚本用户搞糊涂。

ps -u $USER_ACCOUNT >/dev/null 		

同时可以采用ps命令的退出码和case结构来决定下一步执行

case $? in1)    echo "There are no processes for this account currently running."    echo ;;0)      echo "$USER_ACCOUNT has the following processes running: "    echo    ps -u $USER_ACCOUNT        LINE1="Would you like me to kill the process(es)? [y/n]"    get_answer...... esac
  • 如果ps状态码返回1,则表明系统上没有该账户的进程在运行
  • 如果为0,则需要询问是否杀死该账户下的进程,用get_answer函数来完成。

在调用process_answer之前,我们需要嵌入另一个case语句来处理脚本用户的答案,这看起来和process_answer函数很像。

 case $ANSWER in    y|Y|YES|yes|Yes|yEs|yeS|YEs|yES)    echo    echo "Killing off process(es)..."        COMMAND_1="ps -u $USER_ACCOUNT --no-heading"    COMMAND_3="xargs -d \\n /usr/bin/sudo /bin/kill -9"        $COMMAND_1 |gawk '{print $1}' |$COMMAND_3    echo    echo "Process(es) killed."    ;;    *)    echo    echo "Will not kill the process(es)"    echo    ;;    esac;;
  • 要实现杀死用户账户下的进程,需要3条命令。

    • 首先,再使用ps命令,收集当前处于运行状态,属于该用户账户的进程ID(PID)。命令的输出被保存在变量COMMAND_1中。

       COMMAND_1="ps -u $USER_ACCOUNT --no-heading"
      
    • 第二条命令用来提取PID,采用gawk截取第一个字段即可

      gawk '{print $1}'
      
    • 第三条命令是xargs,它能够构建并执行标准输入STDIN命令,非常适合用在管道末尾处,xargs命令负责杀死PID所对应的进程。

       COMMAND_3="xargs -d \\n /usr/bin/sudo /bin/kill -9"
      

      xargs命令被保存在变量COMMAND_3中,采用选项-d指明分隔符,因为xargs命令接收多个项作为输入,这里\n(换行符)是分隔符,当每个PID发送给xargs时,它将PID作为单个项来处理。由于xargs命令被赋给了一个变量,所以,换行符(\n)需要转义,就必须再加一个反斜杠。

      • 注意,在处理PID时,xargs命令需要使用命令的完整路径名,sudo命令和kill命令用于杀死用户账户的运行进程,另外需要注意kill命令使用了信号-9。

    这三条命令通过管道符串联在一起,ps命令生成了处于运行状态的用户进程列表,其中包括每个进程的PID,gawk命令将ps命令的标准输出(STDOUT)作为自己的STDIN,然后从中只提取PID,xargs命名将gawk命令生成的每个PID作为STDIN,创建并执行kill命令,杀死用户所有的运行进程。所以这个管道命令如下:

    $COMMAND_1 |gawk '{print $1}' |$COMMAND_3
    

至此,杀死用户账户所有运行进程的完整case语句如下:

case $ANSWER in    y|Y|YES|yes|Yes|yEs|yeS|YEs|yES)    echo    echo "Killing off process(es)..."        COMMAND_1="ps -u $USER_ACCOUNT --no-heading"    COMMAND_3="xargs -d \\n /usr/bin/sudo /bin/kill -9"        $COMMAND_1 |gawk '{print $1}' |$COMMAND_3    echo    echo "Process(es) killed."    ;;

22.3.6 查找属于账户的文件

从系统上删除用户账户时,最好将属于该用户的所有文件归档,另外,还有一点比较重要的是,得删除这些文件或将文件所属关系分配给其他账户。例如,我们要删除的账户UID为1001,而没有删除或修改它们的所属关系,那么下一个创建的UID为1001的账户会拥有这些文件,这有很大数据泄露安全隐患。

此脚本通过创建一个Daily_Archive.sh脚本作为备份配置文件的报告,可以用这个报告帮助你删除文件或重新分配文件的所属关系。

要找到用户文件,可以用find命令,-u选项查找整个文件系统,它能够准确地找到属于该用户的所有文件。

find / -user $USER_ACCOUNT > $REPORT_FILE

22.3.7 删除账户

对删除系统中的用户账户,谨慎是最好的,所以可以再一次不厌其烦地询问一次脚本用户是否真的想删除该账户:

LINE1="Remove $USER_ACCOUNT's account from system? [y/n]"get_answerEXIT_LINE1="Since you do not wish to remove the user account,"EXIT_LINE2="$USER_ACCOUNT at this time, exiting the script..."process_answer
  • 最后采用userdel 命令真正删除用户。

22.4 验证测试脚本

[root@gzr ~]# ./Delete_User.sh Step #1 - Determine User Account name to Delete Please enter the username of the useraccount you wish to delete from system: lisiIs lisi the user accountyou wish to delete from the system? [y/n] yI found this record:lisi:x:1002:1002::/home/lisi:/bin/bashIs this the correct User Account? [y/n] yStep #2 - Find process on system belonging to user accountlisi has the following processes running:    PID TTY          TIME CMD 33583 ?        00:00:00 sshd 33584 pts/1    00:00:00 bashWould you like me to kill the process(es)? [y/n] yKilling off process(es)...Process(es) killed.Step #3 - Find files on system belonging to user accountCreating a report of all files owned by lisi.It is  recommended that you backup/archive these files, and then do one of two things: 1) Delete the files 2) Change the files'  ownership to a current user account.Please wait. This may take a while..../Delete_User.sh: line 161: $REPORT_FILE: ambiguous redirectREPORT is complete.Name of report:      Location of report:  /rootStep #4 - Remove user accountRemove lisi's account from system? [y/n] yUSER account, lisi, has been removed

23. 如何进行两个整数相加

V1=1;V2=2# 使用letlet V3=$V1+$V2echo $V3# 使用双小括号echo $(($V1+$V2))# 使用单中括号echo $[$V1+$V2]# 使用bcecho $V1+$V2 | bc# 使用awkawk 'BEGIN{print '"$V1"' + '"$V2"'}'

附录:写好shell脚本的技巧

  • 提供--help标记
  • 检查所有命令的可用性
  • 独立于当前工作目录
  • 如何读取输入:环境变量 vs. 标记
  • 打印对系统执行的所有操作
  • 如果有必要,提供--silent选项
  • 重新开启显示
  • 用动画的方式显示进度
  • 用颜色编码输出
  • 出现错误立即退出脚本
  • 自己执行清理工作
  • 在退出时使用不同的错误码
  • 在结束时打印一个新行

技巧1:提供--help标记

#!/bin/shif [ ${#@} -ne 0 ] && [ "${@#"--help"}" = "" ]; then printf -- '...help...\n'; exit 0;fi;

这段脚本先计算参数长度(${#@} -ne 0),只有当参数长度不为零时才会检查--help标记。下一个条件会检查参数中是否存在字符串“--help” 。第一个条件是必需的,如果参数长度为零则不需要打印帮助信息。

#!/bin/sh_=$(command -v docker);if [ "$?" != "0" ]; then printf -- 'You don\'t seem to have Docker installed.\n'; printf -- 'Get it: https://www.docker.com/community-edition\n'; printf -- 'Exiting with code 127...\n'; exit 127;fi;

脚本通常会调用其他脚本或二进制文件。在调用可能不存在的命令时,请先检查它们是否可用。可以使用“command -v 二进制文件名称”来执行此操作,看看它的退出代码是否为零。如果命令不可用,可以告诉用户应该如何获得这个二进制文件:

技巧3:独立于当前工作目录

从不同的目录执行脚本可能会发生错误,这样的脚本没有人会喜欢。要解决这个问题,请使用绝对路径(/path/to/something)和脚本的相对路径(如下所示)。

可以使用dirname $0引用脚本的当前路径:

#!/bin/shCURR_DIR="$(dirname $0);"printf -- 'moving application to /opt/app.jar';mv "${CURR_DIR}/application.jar" /opt/app.jar;

技巧4:如何读取输入:环境变量 vs. 标记

脚本通过两种方式接受输入:环境变量和选项标记(参数)。根据经验,对于不影响脚本行为的值,可以使用环境变量,而对于可能触发脚本不同流程的值,可以使用脚本参数。

不影响脚本行为的变量可能是访问令牌和 ID 之类的东西:

#!/bin/sh# do thisexport AWS_ACCESS_TOKEN='xxxxxxxxxxxx';./provision-everything# and not./provisiong-everything --token 'xxxxxxxxxxx';

影响脚本行为的变量可能是需要运行实例的数量、是异步还是同步运行、是否在后台运行等参数:

#!/bin/sh# do this./provision-everything --async --instance-count 400# and notINSTANCE_COUNT=400 ASYNC=true ./provision-everything

技巧5:打印对系统执行的所有操作

脚本通常会对系统执行有状态的更改。不过,由于我们不知道用户何时会向发送SIGINT,也不知道脚本错误何时可能导致脚本意外终止,因此很有必要将正在做的事情打印在终端上,这样用户就可以在不去查看脚本的情况下回溯这些步骤:

#!/bin/shprintf -- 'Downloading required document to ./downloaded... ';wget -o ./downloaded https://some.site.com/downloaded;printf -- 'Moving ./downloaded to /opt/downloaded...';mv ./downloaded /opt/;printf -- 'Creating symlink to /opt/downloaded...';ln -s /opt/downloaded /usr/bin/downloaded;

技巧6:在必要时提供--silent选项

有些脚本是为了将其输出传给其他脚本。虽说脚本都应该能够单独运行,不过有时候也有必要让它们把输出结果传给另一个脚本。可以利用stty -echo来实现--silent标记:

#!/bin/shif [ ${#@} -ne 0 ] && [ "${@#"--silent"}" = "" ]; then stty -echo;fi;# ...# before point of intended output:stty +echo && printf -- 'intended output\n';# silence it again till end of scriptstty -echo;# ...stty +echo;exit 0;

技巧7:重新开启显示

在使用stty -echo关闭脚本显示之后,如果发生致命错误,脚本将终止,而且不会恢复终端输出,这样对用户来说是没有意义的。可以使用trap来捕捉SIGINT和其他操作系统级别的信号,然后使用stty echo打开终端显示:

#!/bin/sherror_handle() { stty echo;}if [ ${#@} -ne 0 ] && [ "${@#"--silent"}" = "" ]; then stty -echo; trap error_handle INT; trap error_handle TERM; trap error_handle KILL; trap error_handle EXIT;fi;# ...

技巧8:用动画方式显示进度

有些命令需要运行很长时间,并非所有脚本都提供了进度条。在用户等待异步任务完成时,可以通过一些方式告诉他们脚本仍在运行。比如在while循环中打印一些信息:

#!/bin/shprintf -- 'Performing asynchronous action..';./trigger-action;DONE=0;while [ $DONE -eq 0 ]; do ./async-checker; if [ "$?" = "0" ]; then DONE=1; fi; printf -- '.'; sleep 1;done;printf -- ' DONE!\n';

技巧9:用颜色编码输出

在脚本中调用其他二进制文件或脚本时,对它们的输出进行颜色编码,这样就可以知道哪个输出来自哪个脚本或二进制文件。这样我们就不需要在满屏的黑白输出文本中查找想要的输出结果。

理想情况下,脚本应该输出白色(默认的,前台进程),子进程应该使用灰色(通常不需要,除非出现错误),使用绿色表示成功,红色表示失败,黄色表示警告。

#!/bin/shprintf -- 'doing something... \n';printf -- '\033[37m someone else's output \033[0m\n';printf -- '\033[32m SUCCESS: yay \033[0m\n';printf -- '\033[33m WARNING: hmm \033[0m\n';printf -- '\033[31m ERROR: fubar \033[0m\n';

技巧10:出现错误立即退出脚本

set -e表示从当前位置开始,如果出现任何错误都将触发EXIT。相反,set +e表示不管出现任何错误继续执行脚本。

如果脚本是有状态的(每个后续步骤都依赖前一个步骤),那么请使用set -e,在脚本出现错误时立即退出脚本。如果要求所有命令都要执行完(很少会这样),那么就使用set +e。

#!/bin/shset +e;./script-1;./script-2; # does not depend on ./script-1./script-3; # does not depend on ./script-2set -e;./script-4;./script-5; # depends on success of ./script-4# ...

技巧11:自己执行清理工作

大多数脚本在出现错误时不会执行清理工作,能够做好这方面工作的脚本实属罕见,但这样做其实很有用,还可以省下不少时间。前面已经给出过示例,让stty恢复正常,并借助trap命令来执行清理工作:

#!/bin/shhandle_exit_code() { ERROR_CODE="$?"; printf -- "an error occurred. cleaning up now... "; # ... cleanup code ... printf -- "DONE.\nExiting with error code ${ERROR_CODE}.\n"; exit ${ERROR_CODE};}trap "handle_exit_code" EXIT;# ... actual script...

技巧12: 在退出时使用不同的错误码

在绝大多数 shell 脚本中,exit 0 表示执行成功,exit 1 表示发生错误。对错误与错误码进行一对一的映射,这样有助于脚本调试。

#!/bin/sh# ...if [ "$?" != "0" ]; then printf -- 'X happened. Exiting with status code 1.\n'; exit 1;fi;# ...if [ "$?" != "0" ]; then printf -- 'Y happened. Exiting with status code 2.\n'; exit 2;fi;
  • 这样做有另一个额外的好处,就是其他脚本在调用你的脚本时,可以根据错误码来判断发生了什么错误

技巧13:在结束是打印一个新行

如果你有在遵循脚本的最佳实践,那么可能会使用printf代替echo(它在不同系统中的行为有所差别)。问题是printf在命令结束后不会自动添加一个新行。

#!/bin/sh# ... your awesome script ...printf -- '\n';exit 0;