扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
awk是一种处理文本文件的语言,是一个强大的文本分析公具。
awk处理文本和数据的方式:逐行读入文本,寻找匹配特定模式的行,然后进行操作。
功能很强大,所以有很多用处。这里我主要关注下面这样的场景:
逐行读入文本,按规则匹配特定的行,以空格为默认分隔符将每行切片,输出其中特定的某个切片(切开的部分可以进行各种分析处理,这里就是要输出其中以段):
$ cat /etc/hosts
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
$ awk '/local/ {print $1}' /etc/hosts
127.0.0.1
::1
$
这种方法很适合用来做zabbix的自定义key的监控。比如从free命令中,提取出内存的使用量:
$ free
total used free shared buff/cache available
Mem: 1855432 320688 1238808 10612 295936 1495432
Swap: 2093052 0 2093052
$ free | awk '/^Mem:/ {print $3}'
320688
$
grep命令
同样的效果,也可以通过grep命令来把需要的行过滤出来,然后还得借助cut命令来进行列切割。
但是使用awk的话就一步搞定了。
内置变量先列出来,后面会用到其中一些。
awk内置变量:
上面这些变量,有些是直接来使用的。比如$1,$NF,后面的例子中会用到,也比较好理解。
还有些是用来改变awk行为的,需要对变量进行设置,这个需要会为变量赋值,有多种方式可以实现。
比如FS,是用来指定分隔符的,默认的分隔符是空白符,但是可以指定。这就需要自己定义FS的值。不过分隔符还提供了一个 -F 选项来定义。所以也可以在命令行选项中设置。
但是其他一些变量需要指定,但又没有提供别的方法的话,就只能用过为变量赋值来实现了。
分隔符和为变量赋值的方式在后面会展开,为变量赋值参考自定义变量的内容。
默认awk是以空白符来做分隔的。使用 -F 选项可以自定义分隔符:
$ grep -e "^root" /etc/passwd
root:x:0:0:root:/root:/bin/bash
$ awk -F: '/^root/ {print $1,$NF}' /etc/passwd
root /bin/bash
$
这里将分隔符指定为冒号。
多分隔符
默认的也是多分隔符的情况,空格、制表符等都会被识别。自己要指定多个分隔符,则是用中括号把需要识别的分隔符都括起来:
$ echo "a-b_c=d-E_F=G" | awk -F[-_=] '{print $1,$2,$3,$4,$5,$6,$7}'
a b c d E F G
$
过滤连续的分隔符
-F 选项也是支持正则表达式的,中括号就是正则表达式字符集合的意思。但是如果这时遇到连续的分隔符,就会有问题。下面使用逗号和空格作为分隔符,并且每次都连续出现:
$ echo "a,,b c" | awk -F'[ ,]' '{print $1"-"$2"-"$3}'
a--b
$
正则表达式中匹配一次或多次,使用加号后,就可以了:
$ echo "a,,b c" | awk -F'[ ,]+' '{print $1"-"$2"-"$3}'
a-b-c
$
特殊字符分隔符
特殊字符应该就是这些: $、^、*、(、)、[、]、?、.、|
单独作为分隔符并没有问题:
$ echo '1a$1b$1c' | awk -F'$' '{print $1"-"$2"-"$3}'
1a-1b-1c
$
如果指定多个字符作为一个整体作为一个分隔符,就会有问题,需要转义。比如这里要将 $1 作为分隔符:
$ echo '1a$1b$1c' | awk -F'$1' '{print $1"-"$2"-"$3}'
1a$1b$1c--
$ echo '1a$1b$1c' | awk -F'\\$1' '{print $1"-"$2"-"$3}'
1a-b-c
$
再来个多个特殊字符组合的:
$ echo 'a$|b$|c' | awk -F'\\$\\|' '{print $1"-"$2"-"$3}'
a-b-c
$
默认分隔符
默认就是空白符作为分隔符,并且能够识别连续的空白符。默认分隔符就是下面的这个正则表达式:
FS="[[:space:]+]"
看上面的内置变量,FS和-F选项是等价的。
除了用print,还可以用printf做格式化输出。这里就给出一个例子,关于printf格式化输出,需要的话再去参考下C语言的printf的功能把。
一般都用print输出:
$ awk -F: '{print "filename:" FILENAME ",linenumber:" NR ",columns:" NF ",linecontent:"$0}' /etc/passwd
filename:/etc/passwd,linenumber:1,columns:7,linecontent:root:x:0:0:root:/root:/bin/bash
filename:/etc/passwd,linenumber:2,columns:7,linecontent:bin:x:1:1:bin:/bin:/sbin/nologin
filename:/etc/passwd,linenumber:3,columns:7,linecontent:daemon:x:2:2:daemon:/sbin:/sbin/nologin
对比下用printf格式化输出后的效果:
$ awk -F: '{printf ("filename:%10s, linenumber:%3s,column:%3s,content:%3f\n",FILENAME,NR,NF,$0)}' /etc/passwd
filename:/etc/passwd, linenumber: 1,column: 7,content:0.000000
filename:/etc/passwd, linenumber: 2,column: 7,content:0.000000
filename:/etc/passwd, linenumber: 3,column: 7,content:0.000000
通常,对于每个输入行,awk 都会执行一次脚本代码块。
有时,需要在 awk 开始处理输入文件中的文本之前执行初始化代码。这就需要定义一个 BEGIN 块。
另外,还有一个 END 块,用于执行最终计算或打印应该出现在输出流结尾的摘要信息。
在BEGIN块中定义内置变量
这里在BEGIN块中定义了两个内置变量:
$ echo "a,,b c" | awk 'BEGIN{FS="[ ,]+";OFS="-"}{print $1,$2,$3}'
a-b-c
$
FS是分隔符,OFS是输出字段分隔符。在之前的例子中,不用BEGIN块也是能实现这个效果的。
这里修改的是一个内置变量,但是方法是针对变量的,包括自定义变量。具体参考下一章“awk自定义变量”。
这里主要挑BEGIN块举例用法。END块可以实现计算统计输出的功能,暂时用不上,略过。
除了内置变量,也可以定义自定义变量并使用。这部分内容对于灵活的配置非常有用,而且如果自己写,也会遇到一些坑。
这里变量的赋值在发生在BEGIN块执行之后的
直接写在后面:
$ echo | awk '{print key1,key2}' key1=v1 key2=V2
v1 V2
$
这种用法在BEGIN块中是识别不了变量的。BEGIN块的执行在这些变量定义之前。不过还有其他的方法可以用。
另外,这里使用管道作为标准输入。如果是从文件输入的话,文件路径在写最后。
这里变量额赋值是在BEGIN块执行的时候
在BEGIN块中可以对内置变量赋值,同样的也可以为自定义变量赋值
$ echo | awk 'BEGIN{key1="v1";key2="value2";OFS="_"}{print key1,key2}'
v1_value2
$
这里变量的赋值是在BEGIN块执行之前
这个方法在发生在BEGIN块执行之前的:
$ echo | awk -v key1=V1 -v key2=value2 '{print key1,key2}'
V1 value2
$
如果是多个变量,则使用 -v 多次。
如果把上面两个方法合起来:
$ echo | awk -v key1=v1 -v key2=v2 'BEGIN{print "BEGIN: "key1,key2}{print "ACTION: "key1,key2}' key1=VALUE1 key2=VALUE2
BEGIN: v1 v2
ACTION: VALUE1 VALUE2
$
先是 -v 进行赋值,然后BEGIN块执行。之后是最后的变量赋值,如果有同名的就替换值,之后再逐行执行。打印出来的就是之后改变的值。
最好的方法在后一小节。这里的方法也是可行的,但是可读性不好。
写这段是为了理解一下命令参数解析的过程,以及一些特殊情况的处理。
要直接打印环境变量是这样的:
$ echo | awk '{print "'"$PATH"'"}'
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
$
这里的显示不明显,两边都是一对双引号套一个单引号,看图
解释说明
先用一个简单点的环境变量来举例:
$ echo $USER
root
$
这个没有什么空格换特殊字符,这样可以去掉最里面的一对双引号:
$ echo | awk '{print "'$USER'"}'
root
$
这里成对出现了2对单引号,所以就被分成了这样两个部分:awk '{print
和 '"}'
。awk对2个单引号内的命令起作用。
剩下的就是 $USER
了,这个最早就被 shell 给处理替换了。
在变量本身被shell处理完之后,如果有空格之类的,有会被认为不是一个部分。这里就再用双引号把环境变量的值包起来,将值作为整体的一个域。
我的理解
最外层的引号是用来界定字符边界的,但是只要是连续的就被系统认为是一串(一个域)。可以用多对引号把多个字符串引起来,但是每对引号之间不要出现分隔符。这样,最后解析交给命令处理的还是一个整体的字符串(一个域)。
下面是用echo命令的演示:
$ echo 'abc''def'
abcdef
$ echo 'abc'$USER'def'
abcrootdef
$ echo 'abc '$USER' def'
abc root def
$
加上for循环再演示一次:
$ HELLO='Hello World !'
$ for i in $HELLO; do echo $i;done
Hello
World
!
$ for i in 'BEFOR'$HELLO'AFTER'; do echo $i;done
BEFORHello
World
!AFTER
$ for i in 'BEFOR'"$HELLO"'AFTER'; do echo $i;done
BEFORHello World !AFTER
$
最外层的引号仅仅是界定边界的,用多对引号但是所有内容都相连,也被认为是一个域。
虽然有多对引号,但是所有内容都是相连的,没有分隔符,最后交给命令处理的还是一个域。
这样做的好处就是,用了单引号,但是把需要shell解析的部分放到了单引号的外面,这样shell还是可以正常解析。
为了保证环境变量解析完之后依然是一个域,需要用双引号引起来。
再来就是awk中接着print的双引号了。awk中的引号不是界定边界的而是区分是变量还是字符串的。没有双引号的话表示这个内容是变量,用双引号引起来表示里面的内容是字符串,直接打印。
其他写法
下面两种写法也能实现同样的效果,帮助理解吧:
$ echo | awk "{print \"$PATH\"}"
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
$ echo } awk \{print\""$PATH"\"\}
} awk {print"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin"}
$
awk命令还是尽量用单引号引起来,防止shell对其中内容进行解释。就是第一种办法就最好的。
最开始的3种方法,有2种是在引号外完成变量定义的,这样就不会对shell进行干扰:
$ echo | awk '{print path}' path="$PATH"
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
$ echo | awk -v path="$PATH" '{print path}'
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
$
先是用命令行的方式把环境变量赋值给自定义变量,这个操作在引号外。然后再引号里面直接用自定义变量就好了。
如果是在BEGIN块中要这么做,就参考上以小节的做法。
从free命令同获取当前内存使用数值:
$ free | awk '/^Mem:/ {print $3}'
335840
$
这里用的是正则匹配。不过awk还有其他的一些语法,可以做到更加精确的匹配,
限制第一个字段值来匹配:
$ free | awk '$1 == "Mem:" {print $3}'
335744
$
限制要第几行的数据:
$ free | awk 'NR == 2 {print $3}'
335796
$
awk 也提供了 if, else, while 等这些条件语句,不过似乎用不了那么深,举一个if的例子。
同样是限制第几行,这里通过if语句来判断:
$ free | awk '{if(NR == 2) print $3}'
335740
$
~是匹配正则表达式的运算符。另外,~!是不匹配正则表达式的运算符。
匹配第一个字段:
$ free | awk '$1 ~ /Mem/ {print $3}'
335844
$
关于正则还有一个内置变量是 IGNORECASE,如果设置为1,可以忽略大小写:
$ free | awk '$1 ~ "mem" {print $3}' IGNORECASE=1
335708
$
为变量赋值的方法之前讲过了,有好几种方式。
这个很高端的样子,就贴在最后了:
$ seq 9 | sed 'H;g' | awk -v RS='' '{for(i=1;i<=NF;i++)printf("%dx%d=%d%s", i, NR, i*NR, i==NR?"\n":"\t")}'
1x1=1
1x2=2 2x2=4
1x3=3 2x3=6 3x3=9
1x4=4 2x4=8 3x4=12 4x4=16
1x5=5 2x5=10 3x5=15 4x5=20 5x5=25
1x6=6 2x6=12 3x6=18 4x6=24 5x6=30 6x6=36
1x7=7 2x7=14 3x7=21 4x7=28 5x7=35 6x7=42 7x7=49
1x8=8 2x8=16 3x8=24 4x8=32 5x8=40 6x8=48 7x8=56 8x8=64
1x9=9 2x9=18 3x9=27 4x9=36 5x9=45 6x9=54 7x9=63 8x9=72 9x9=81
$
用户自定义参数可以通过Zabbix agent执行非Zabbix原生的agent监控项。只要你有办法能通过命令获取到要监控的指标。
可以直接在配置文件 zabbix_agentd.conf 中定义 UserParameter。
### Option: HostnameItem
# Item used for generating Hostname if it is undefined. Ignored if Hostname is defined.
# Does not support UserParameters or aliases.
#
# Mandatory: no
# Default:
# HostnameItem=system.hostname
Include配置
虽然直接写在这下面就可以了,不过配置文件还有一个Include的配置:
### Option: Include
# You may include individual files or all files in a directory in the configuration file.
# Installing Zabbix will create include directory in /usr/local/etc, unless modified during the compile time.
#
# Mandatory: no
# Default:
# Include=
Include=/etc/zabbix/zabbix_agentd.d/*.conf
# Include=/usr/local/etc/zabbix_agentd.userparams.conf
# Include=/usr/local/etc/zabbix_agentd.conf.d/
# Include=/usr/local/etc/zabbix_agentd.conf.d/*.conf
建议把这些配置分下类,创建独立的 zabbix_agentd.d/*.conf 文件,方便管理。
自定义参数的语法如下:
UserParameter=,
key,就是监控项用的key。必须全局唯一。
命名要求:只能使用字母、数字、下划线、中横杠、点号。即 0-9a-zA-Z_-.
这些字符。
比如下面的文件中定义了3个通过free命令获取值的监控项:
$ cat /etc/zabbix/zabbix_agentd.d/os.conf
UserParameter=os.memory.total, free -m | awk '$1=="Mem:" {print $2}'
UserParameter=os.memory.used, free -m | awk '$1=="Mem:" {print $3}'
UserParameter=os.memory.free, free -m | awk '$1=="Mem:" {print $4}'
具体一步步如何实现的,参考下一小节。
自定义参数,一步步实现的操作过程。
第一步:写一个命令或脚本
能够成功的在命令行中把值打印出来:
$ free -m | awk '$1=="Mem:" {print $3}'
569
$
由于zabbix是使用zabbix账号执行的,有些命令有可能zabbix无权限。所以可以加上sudo指定zabbix用户执行再验证一下:
$ sudo -u zabbix free -m | awk '$1=="Mem:" {print $3}'
571
$
第二步:添加到配置文件中
UserParameter=os.memory.used, free -m | awk '$1=="Mem:" {print $3}'
第三步:测试key
使用 zabbix_agentd 并且用 -t 选项指定key来进行测试:
$ zabbix_agentd -t os.memory.used
os.memory.used [t|571]
$
测试成功说明写的没问题
第四步:重启agent
要重启agent才能使新的配置文件生效:
$ systemctl restart zabbix-agent
$
之后就可以去Web添加监控项了。
可以为key设置参数,这样一个设置可以应对多个监控项。
语法如下:
UserParameter=key[*],command
这里的星号表示可以带任意数量的参数,并且似乎也只有这一种用法,没有指定参数数量的写法。
在command中使用参数
在command中使用位置引用$1......$9,来引用key中相应的参数。
另外$0表示命令本身。
关于$符号
由于$1.....$9有了特殊的意义,在awk中的$1也会被zabbix先替换掉。这时应该使用$$1。
zabbix仅仅只替换位置参数,对于单独的$符号或者其他组合(比如$NF),zabbix不会处理。
zabbix仅仅只在使用了key[*],这样指定了key是带参数的时候才会进行位置参数替换的处理。所以之前的示例使用$1没有问题。
修改为带参数的key
现在把之前的示例改成一种更灵活的设置方式:
UserParameter=os.free[*], free -m | awk '$$1~NAME {print $$(COLUMN+1)}' IGNORECASE=1 NAME="$1" COLUMN=$2
测试效果如下:
$ zabbix_agentd -t os.free[mem,2]
os.free[mem,2] [t|570]
$
一些自定义参数的示例:
UserParameter=Nginx.active[*], /usr/bin/curl -s "http://$1:$2/status" | awk '/^Active/ {print $NF}'
UserParameter=Nginx.reading[*], /usr/bin/curl -s "http://$1:$2/status" | grep 'Reading' | cut -d" " -f2
UserParameter=Nginx.writing[*], /usr/bin/curl -s "http://$1:$2/status" | grep 'Writing' | cut -d" " -f4
UserParameter=Nginx.waiting[*], /usr/bin/curl -s "http://$1:$2/status" | grep 'Waiting' | cut -d" " -f6
UserParameter=Nginx.accepted[*], /usr/bin/curl -s "http://$1:$2/status" | awk '/^([ \t]+[0-9]+){3}/ {print $$1}'
UserParameter=Nginx.handled[*], /usr/bin/curl -s "http://$1:$2/status" | awk '/^([ \t]+[0-9]+){3}/ {print $$2}'
UserParameter=Nginx.requests[*], /usr/bin/curl -s "http://$1:$2/status" | awk '/^([ \t]+[0-9]+){3}/ {print $$3}'
UserParameter=os.free[*], free | awk '$$1~NAME {print $$(COLUMN+1)}' IGNORECASE=1 NAME="$1" COLUMN=$2
UserParameter=Mysql.dml[*] -h$1 -u$2 -p$3 -e 'SHOW GLOBAL STATUS' | awk '/^Com_$4\>/ {print $$2}'
正则表达式 词尾锚定
在调试mysql的时候,遇到一些问题。正则表达式匹配不够精确,有多个值:
$ mysql -e 'SHOW GLOBAL STATUS' | awk '/^Com_select/ {print $0}'
Com_select 67679
$ mysql -e 'SHOW GLOBAL STATUS' | awk '/^Com_update/ {print $0}'
Com_update 1098
Com_update_multi 0
$ mysql -e 'SHOW GLOBAL STATUS' | awk '/^Com_delete/ {print $0}'
Com_delete 678
Com_delete_multi 0
$ mysql -e 'SHOW GLOBAL STATUS' | awk '/^Com_insert/ {print $0}'
Com_insert 38494
Com_insert_select 0
$
这里是加了词尾锚定:
$ mysql -e 'SHOW GLOBAL STATUS' | awk '/^Com_delete\>/ {print $0}'
Com_delete 708
$
词尾锚定是 \>,顺便词首就是 \<。
其实也没那么复杂,还有很多办法:
$ mysql -e 'SHOW GLOBAL STATUS' | awk '/^Com_delete[" "\t]/ {print $0}'
Com_delete 712
$ mysql -e 'SHOW GLOBAL STATUS' | awk '/^Com_delete[^_]/ {print $0}'
Com_delete 715
$ mysql -e 'SHOW GLOBAL STATUS' | awk '$1 == "Com_delete" {print $0}'
Com_delete 717
$
这个是zabbix内置key,也能够实现同样的功能,那么到底用哪个好?
内置key system.run
这是一个内置key:
system.run[command,]
在主机上指定的命令的执行。返回命令执行结果的文本值。如果指定NOWAIT的模式,这将返回执行命令的结果1。
默认agent不支持,安全隐患还是很大的。需要agent端开启RemoteCommand,允许远程执行命令。
优劣比较
通过这个也能实现自定义监控功能,而且不用去agent上定义UserParameter。直接在web就能完成全部操作。这个可能是好处。
用UserParameter,如果agent多,需要每一台agent上都去设置UserParameter,这就很烦。不过还有自动化运维工具可以解决批量更新、操作文件的问题。
另外有需要云服务器可以了解下创新互联scvps.cn,海内外云服务器15元起步,三天无理由+7*72小时售后在线,公司持有idc许可证,提供“云服务器、裸金属服务器、高防服务器、香港服务器、美国服务器、虚拟主机、免备案服务器”等云主机租用服务以及企业上云的综合解决方案,具有“安全稳定、简单易用、服务可用性高、性价比高”等特点与优势,专为企业上云打造定制,能够满足用户丰富、多元化的应用场景需求。
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流