无论是系统运维,还是应用运维,均可分为“纯手工”——> “脚本化”——> “自动化”——>“智能化”几个阶段,其中自动化阶段,主要是将一些重复性人工操作和运维经验封装为程序或脚本,一方面避免重复性操作及风险,另一方面提高执行效率。在自动化运维的转变过程中,经常使用的可能就是shell脚本了,今天主要分享下shell脚本开发在运维工作中的一些经验总结。
小脚本有大智慧,别小看几十行代码,夹杂着系统设计、代码规范和操作经验等等细节,在建设自动化运维的工作中,还是很值得我们研究学习的,下面总结这些也都是源于各位脚本达人和我们在自身工作中“遇到的坑”、“摔过的跟头”和“排过的雷”,与大家共享。
<img src="https://pic4.zhimg.com/50/v2-e0bc540a32aa318ce19a51a6c5b0ca8c_hd.jpg" data-caption="" data-size="normal" data-rawwidth="1080" data-rawheight="249" class="origin_image zh-lightbox-thumb" width="1080" data-original="https://pic4.zhimg.com/v2-e0bc540a32aa318ce19a51a6c5b0ca8c_r.jpg"/>
这里主要介绍并参考我行已经形成的一些shell编写规范,编写时严格遵守这些规范,不仅使编写人受益,同时也能提高使用者的执行效率。
1)脚本开头部分应有脚本功能说明、参数使用说明、作者姓名、创建/修改日期、版本信息,格式为:
<img src="https://pic2.zhimg.com/50/v2-510225d5417b616844dd09fea57ae203_hd.jpg" data-caption="" data-size="normal" data-rawwidth="640" data-rawheight="246" class="origin_image zh-lightbox-thumb" width="640" data-original="https://pic2.zhimg.com/v2-510225d5417b616844dd09fea57ae203_r.jpg"/>
2)脚本编写时,注意格式对齐,如所有的循环或者判断语句前后的语句进行对齐,以及case的选取完全,如:
<img src="https://pic3.zhimg.com/50/v2-7a29583461b4029098786a1734c95058_hd.jpg" data-caption="" data-size="normal" data-rawwidth="640" data-rawheight="253" class="origin_image zh-lightbox-thumb" width="640" data-original="https://pic3.zhimg.com/v2-7a29583461b4029098786a1734c95058_r.jpg"/>
3)脚本开头执行时,执行如下命令,在执行过程中若遇到使用了未定义的变量或命令返回值为非零,将直接报错退出:
<img src="https://pic1.zhimg.com/50/v2-7c5c0f5749f31fd2e668bcb3312ff81e_hd.jpg" data-caption="" data-size="normal" data-rawwidth="857" data-rawheight="103" class="origin_image zh-lightbox-thumb" width="857" data-original="https://pic1.zhimg.com/v2-7c5c0f5749f31fd2e668bcb3312ff81e_r.jpg"/>
4)建议将命令行的每个参数放在单引号、双引号中,特别是rm、mv等可能对生产现有数据造成修改的操作,建议使用垃圾箱策略:rm操作转意为mv操作,制定文件保存目录,以防回退,并定期清理:
<img src="https://pic3.zhimg.com/50/v2-479548dd50b9752e46c92cdb2fe081b8_hd.jpg" data-caption="" data-size="normal" data-rawwidth="857" data-rawheight="88" class="origin_image zh-lightbox-thumb" width="857" data-original="https://pic3.zhimg.com/v2-479548dd50b9752e46c92cdb2fe081b8_r.jpg"/>
5)命令行中参数需要使用‘*’、‘?’通配符的,应依据最精确匹配原则,如能确定文件、目录名称的前缀、后缀、扩展名及其他可识别关键字的,须在参数中包含该信息,如能确定文件、目录的长度应使用‘?’通配符,不得使用‘*’,推荐的使用方式:
<img src="https://pic4.zhimg.com/50/v2-fe62c2e6ec0aa53cbdb3bcc88abdf3da_hd.jpg" data-caption="" data-size="normal" data-rawwidth="856" data-rawheight="39" class="origin_image zh-lightbox-thumb" width="856" data-original="https://pic4.zhimg.com/v2-fe62c2e6ec0aa53cbdb3bcc88abdf3da_r.jpg"/>
不推荐使用的方式:
<img src="https://pic4.zhimg.com/50/v2-b032aea5604e220b1501105a0b819ba8_hd.jpg" data-caption="" data-size="normal" data-rawwidth="857" data-rawheight="92" class="origin_image zh-lightbox-thumb" width="857" data-original="https://pic4.zhimg.com/v2-b032aea5604e220b1501105a0b819ba8_r.jpg"/>
禁止使用的方式:
<img src="https://pic3.zhimg.com/50/v2-bda77aac21144a307a80e772d735e858_hd.jpg" data-caption="" data-size="normal" data-rawwidth="857" data-rawheight="109" class="origin_image zh-lightbox-thumb" width="857" data-original="https://pic3.zhimg.com/v2-bda77aac21144a307a80e772d735e858_r.jpg"/>
6)给数值型变量的赋值后,需由手段保证变量的值为数值型,避免在后续的处理中出现异常:
<img src="https://pic3.zhimg.com/50/v2-c9df2a82befb42bb2b46f22d822ed2f9_hd.jpg" data-caption="" data-size="normal" data-rawwidth="850" data-rawheight="259" class="origin_image zh-lightbox-thumb" width="850" data-original="https://pic3.zhimg.com/v2-c9df2a82befb42bb2b46f22d822ed2f9_r.jpg"/>
7)在判断条件中使用的变量,必须包含在双引号中,如:
<img src="https://pic2.zhimg.com/50/v2-9088d8f4ad3fcc5d4e320040508ea360_hd.jpg" data-caption="" data-size="normal" data-rawwidth="850" data-rawheight="211" class="origin_image zh-lightbox-thumb" width="850" data-original="https://pic2.zhimg.com/v2-9088d8f4ad3fcc5d4e320040508ea360_r.jpg"/>
禁止使用的方式:
<img src="https://pic4.zhimg.com/50/v2-337ce61623499817f6516b3daca90445_hd.jpg" data-caption="" data-size="normal" data-rawwidth="850" data-rawheight="110" class="origin_image zh-lightbox-thumb" width="850" data-original="https://pic4.zhimg.com/v2-337ce61623499817f6516b3daca90445_r.jpg"/>
<img src="https://pic3.zhimg.com/50/v2-1d2ce85039fd701b5cebb6a347480bec_hd.jpg" data-caption="" data-size="normal" data-rawwidth="850" data-rawheight="109" class="origin_image zh-lightbox-thumb" width="850" data-original="https://pic3.zhimg.com/v2-1d2ce85039fd701b5cebb6a347480bec_r.jpg"/>
8)对文件进行打包备份时,必须使用相对路径进行打包,如:
<img src="https://pic2.zhimg.com/50/v2-e5af950769f93d803bbfb36dce78da38_hd.jpg" data-caption="" data-size="normal" data-rawwidth="922" data-rawheight="39" class="origin_image zh-lightbox-thumb" width="922" data-original="https://pic2.zhimg.com/v2-e5af950769f93d803bbfb36dce78da38_r.jpg"/>
严禁将全路径打入tar包, 如:
<img src="https://pic4.zhimg.com/50/v2-110fe4098545d45912d72ee4afedd8fd_hd.jpg" data-caption="" data-size="normal" data-rawwidth="922" data-rawheight="39" class="origin_image zh-lightbox-thumb" width="922" data-original="https://pic4.zhimg.com/v2-110fe4098545d45912d72ee4afedd8fd_r.jpg"/>
9)对于打包后还需进行压缩的文件,建议使用管道进行处理,如:
<img src="https://pic4.zhimg.com/50/v2-913a7e4dfdd8027ed0bc1e0236c4f56a_hd.jpg" data-caption="" data-size="normal" data-rawwidth="922" data-rawheight="61" class="origin_image zh-lightbox-thumb" width="922" data-original="https://pic4.zhimg.com/v2-913a7e4dfdd8027ed0bc1e0236c4f56a_r.jpg"/>
不建议两部分分开执行:
<img src="https://pic2.zhimg.com/50/v2-988db2f301e501239f4cc007f95e4a23_hd.jpg" data-caption="" data-size="normal" data-rawwidth="922" data-rawheight="74" class="origin_image zh-lightbox-thumb" width="922" data-original="https://pic2.zhimg.com/v2-988db2f301e501239f4cc007f95e4a23_r.jpg"/>
10)使用ps命令筛选进程时,如能确定进程所属用户,必须在参数中指定用户名称,如其输出作为kill命令的输入,则必须指定进程所属用户,如:
<img src="https://pic3.zhimg.com/50/v2-9b358f6a6613cf6cf8fa5108fe7ac559_hd.jpg" data-caption="" data-size="normal" data-rawwidth="922" data-rawheight="39" class="origin_image zh-lightbox-thumb" width="922" data-original="https://pic3.zhimg.com/v2-9b358f6a6613cf6cf8fa5108fe7ac559_r.jpg"/>
<img src="https://pic3.zhimg.com/50/v2-d451e90e3d2fc5d759f881abc162bbc0_hd.jpg" data-caption="" data-size="normal" data-rawwidth="1080" data-rawheight="250" class="origin_image zh-lightbox-thumb" width="1080" data-original="https://pic3.zhimg.com/v2-d451e90e3d2fc5d759f881abc162bbc0_r.jpg"/>
这里介绍的主要是日常shell编写中遇到比较隐蔽或看似简单,却难以发现的“坑”,编写中应尽量避免使用,使用更优的方法避免重蹈覆辙。
1)更新文件使用>不用cp
使用>修改和回退文件时,保留原文件的属组和权限,避免使用cp时权限属组被修改。
<img src="https://pic3.zhimg.com/50/v2-b13a26cf4553abe5340840174ffc1f56_hd.jpg" data-caption="" data-size="normal" data-rawwidth="870" data-rawheight="63" class="origin_image zh-lightbox-thumb" width="870" data-original="https://pic3.zhimg.com/v2-b13a26cf4553abe5340840174ffc1f56_r.jpg"/>
2)使用kill前确认
关键字用-w 精确匹配字段;
kill前后都保留现场, 两次ps -ef|grep -w 关键字|grep -v grep >>/tmp/kill_进程名_.backup;
删除前要校验,获取进程号是否唯一,避免多杀或误杀的情况。
<img src="https://pic1.zhimg.com/50/v2-8a665e08ad74781563e2887449615004_hd.jpg" data-caption="" data-size="normal" data-rawwidth="861" data-rawheight="184" class="origin_image zh-lightbox-thumb" width="861" data-original="https://pic1.zhimg.com/v2-8a665e08ad74781563e2887449615004_r.jpg"/>
3)使用rm前确认
删除前备份删除对象信息,避免使用变量,直接使用文件和目录名;
如果必须使用时,删除前,建议检查避免误删,删除目录和文件信息保留:
<img src="https://pic1.zhimg.com/50/v2-3ac6fab9c301afbed527611b9e15e658_hd.jpg" data-caption="" data-size="normal" data-rawwidth="861" data-rawheight="151" class="origin_image zh-lightbox-thumb" width="861" data-original="https://pic1.zhimg.com/v2-3ac6fab9c301afbed527611b9e15e658_r.jpg"/>
建议禁用find遍历根目录进行查找,同时删除前进行确认,避免多删或误删的情况。
4)For循环的坑
for循环的in条件按空格来区分,避免进入不正确或死循环。
<img src="https://pic1.zhimg.com/50/v2-e749fa9136ae9439f47855363738162c_hd.jpg" data-caption="" data-size="normal" data-rawwidth="861" data-rawheight="151" class="origin_image zh-lightbox-thumb" width="861" data-original="https://pic1.zhimg.com/v2-e749fa9136ae9439f47855363738162c_r.jpg"/>
5)while循环的禁忌
如果还想使用循环中的变量,不要while结合管道使用。
<img src="https://pic4.zhimg.com/50/v2-026b870cd39ceaa5b2d17d8e8ef6c27a_hd.jpg" data-caption="" data-size="normal" data-rawwidth="640" data-rawheight="372" class="origin_image zh-lightbox-thumb" width="640" data-original="https://pic4.zhimg.com/v2-026b870cd39ceaa5b2d17d8e8ef6c27a_r.jpg"/>
6)慎用cp
这句话基本上正确,但同样有空格分词的问题。所以应当用双引号:
<img src="https://pic2.zhimg.com/50/v2-d7edfb3e8db566ea3406403e3e0b1d87_hd.jpg" data-caption="" data-size="normal" data-rawwidth="885" data-rawheight="47" class="origin_image zh-lightbox-thumb" width="885" data-original="https://pic2.zhimg.com/v2-d7edfb3e8db566ea3406403e3e0b1d87_r.jpg"/>
但是如果凑巧文件名以 - 开头,这个文件名会被 cp 当作命令行选项来处理。
可以试试下面这个:
<img src="https://pic4.zhimg.com/50/v2-4cec69faa868d045de6cda7385203135_hd.jpg" data-caption="" data-size="normal" data-rawwidth="885" data-rawheight="47" class="origin_image zh-lightbox-thumb" width="885" data-original="https://pic4.zhimg.com/v2-4cec69faa868d045de6cda7385203135_r.jpg"/>
但也可能再碰上一个不支持 -- 选项的系统,所以最好用下面的方法:
<img src="https://pic1.zhimg.com/50/v2-3870d61348a8de6dc7940bf2629662fb_hd.jpg" data-caption="" data-size="normal" data-rawwidth="885" data-rawheight="47" class="origin_image zh-lightbox-thumb" width="885" data-original="https://pic1.zhimg.com/v2-3870d61348a8de6dc7940bf2629662fb_r.jpg"/>
7)慎用cd
避免使用cd到操作目录再操作的方式,可能导致进入目录失败,误删除,如:
<img src="https://pic2.zhimg.com/50/v2-c33f655e49675fd45ab77a1733107a37_hd.jpg" data-caption="" data-size="normal" data-rawwidth="885" data-rawheight="61" class="origin_image zh-lightbox-thumb" width="885" data-original="https://pic2.zhimg.com/v2-c33f655e49675fd45ab77a1733107a37_r.jpg"/>
建议如下:
<img src="https://pic4.zhimg.com/50/v2-3ec886f55765d11bfbabb5fd3c444e04_hd.jpg" data-caption="" data-size="normal" data-rawwidth="885" data-rawheight="61" class="origin_image zh-lightbox-thumb" width="885" data-original="https://pic4.zhimg.com/v2-3ec886f55765d11bfbabb5fd3c444e04_r.jpg"/>
8) 用[[ ]]代替[ ]
<img src="https://pic4.zhimg.com/50/v2-4a8e15cbd39e2c3cb8aa621fc3c4ad00_hd.jpg" data-caption="" data-size="normal" data-rawwidth="885" data-rawheight="47" class="origin_image zh-lightbox-thumb" width="885" data-original="https://pic4.zhimg.com/v2-4a8e15cbd39e2c3cb8aa621fc3c4ad00_r.jpg"/>
当$var为空时,上面的命令就变成了[ ="bar" ]
类似地,当$var包含空格时:
[ space words here = "var" ]两者都会出错。所以应当用双引号将变量括起来:
[ "$var" = var ] 几乎完美了。
但是,当$var以 - 开头时依然会有问题。在较新的bash中你可以用下面的方法来代替,[[ ]]关键字能正确处理空白、空格、带横线等问题。
<img src="https://pic4.zhimg.com/50/v2-0352efe306c588aa58e3c15a49f0b018_hd.jpg" data-caption="" data-size="normal" data-rawwidth="822" data-rawheight="62" class="origin_image zh-lightbox-thumb" width="822" data-original="https://pic4.zhimg.com/v2-0352efe306c588aa58e3c15a49f0b018_r.jpg"/>
另注意,[[适用于字符串,如果是数值,要用如:(( $var > 8 ))
9)管道操作中不要同时读写文件
<img src="https://pic3.zhimg.com/50/v2-376f530d5df5cc55657e0e0b501277c8_hd.jpg" data-caption="" data-size="normal" data-rawwidth="890" data-rawheight="47" class="origin_image zh-lightbox-thumb" width="890" data-original="https://pic3.zhimg.com/v2-376f530d5df5cc55657e0e0b501277c8_r.jpg"/>
你不能在同一条管道操作中同时读写一个文件。根据管道的实现方式,file要么被截断成0字节,要么会无限增长直到填满整个硬盘。如果想改变原文件的内容,只能先将输出写到临时文件中再用mv命令。
<img src="https://pic3.zhimg.com/50/v2-1542fd3a9cbca7d10bbf2bca0fa98305_hd.jpg" data-caption="" data-size="normal" data-rawwidth="890" data-rawheight="47" class="origin_image zh-lightbox-thumb" width="890" data-original="https://pic3.zhimg.com/v2-1542fd3a9cbca7d10bbf2bca0fa98305_r.jpg"/>
10)cd的易错问题
cd 有可能会出错,导致要执行的命令就会在你预想不到的目录里执行了。所以一定要记得判断cd的返回值。
<img src="https://pic1.zhimg.com/50/v2-39d461a02b5d932f4651b8ceb1996ea9_hd.jpg" data-caption="" data-size="normal" data-rawwidth="883" data-rawheight="47" class="origin_image zh-lightbox-thumb" width="883" data-original="https://pic1.zhimg.com/v2-39d461a02b5d932f4651b8ceb1996ea9_r.jpg"/>
如果你要根据cd的返回值执行多条命令,可以用 ||。
<img src="https://pic2.zhimg.com/50/v2-66ccef68c41f227f519059a627f54cb4_hd.jpg" data-caption="" data-size="normal" data-rawwidth="883" data-rawheight="47" class="origin_image zh-lightbox-thumb" width="883" data-original="https://pic2.zhimg.com/v2-66ccef68c41f227f519059a627f54cb4_r.jpg"/>
关于目录的一点题外话,假设你要在shell程序中频繁变换工作目录,如下面的代码:
<img src="https://pic2.zhimg.com/50/v2-cf8d812b81444b8f2a9697743247749c_hd.jpg" data-caption="" data-size="normal" data-rawwidth="890" data-rawheight="83" class="origin_image zh-lightbox-thumb" width="890" data-original="https://pic2.zhimg.com/v2-cf8d812b81444b8f2a9697743247749c_r.jpg"/>
不如这样写:
<img src="https://pic2.zhimg.com/50/v2-27e5c18b8466d9c0044fe1000e2f0af7_hd.jpg" data-caption="" data-size="normal" data-rawwidth="890" data-rawheight="123" class="origin_image zh-lightbox-thumb" width="890" data-original="https://pic2.zhimg.com/v2-27e5c18b8466d9c0044fe1000e2f0af7_r.jpg"/>
括号会强制启动一个子shell,这样在这个子shell中改变工作目录不会影响父shell(执行这个脚本的shell),就可以省掉cd - 的麻烦。
<img src="https://pic1.zhimg.com/50/v2-5c544f818522d8b3ddd91e8773471ff6_hd.jpg" data-caption="" data-size="normal" data-rawwidth="1080" data-rawheight="249" class="origin_image zh-lightbox-thumb" width="1080" data-original="https://pic1.zhimg.com/v2-5c544f818522d8b3ddd91e8773471ff6_r.jpg"/>
目前行里自动化工具越来越多,无论是应用的MAOP或系统的SMDB,自动化实现都还是日常运维脚本的调用,结合日常运维的一些经验,脚本中就更需要考虑周全和控制风险。这里介绍一些结合运维场景的脚本应用,希望规避以前犯过的错,重点在控制风险。
1) 支持交互式脚本的应用
很多脚本中需要进行交互,在规避风险的同时,需要通过自动化工具发布来支持交互,可以使用expect,示例如下:
<img src="https://pic1.zhimg.com/50/v2-d05779b0bc29662e91ae226e665f93c7_hd.jpg" data-caption="" data-size="normal" data-rawwidth="640" data-rawheight="943" class="origin_image zh-lightbox-thumb" width="640" data-original="https://pic1.zhimg.com/v2-d05779b0bc29662e91ae226e665f93c7_r.jpg"/>
也可以使用curl工具来替代简单的交互:
#FTP SFTP下载
curl-u ftpuser:ftppassword -O "sftp://ftp_ip:ftp_port/pathfile"
#FTP SFTP上传
curl-u ftpuser:ftppassword --ftp-create-dirs-T upfile "sftp://ftp_ip:ftp_port/filepath/upfile"
2)脚本规范执行和日志追溯
直接执行的脚本很危险,要提示用户如何使用脚本,并记录日志以便跟踪。
示例如下:
<img src="https://pic2.zhimg.com/50/v2-ed6a36d84fbe94d9bfa89d53999b30a6_hd.jpg" data-caption="" data-size="normal" data-rawwidth="640" data-rawheight="686" class="origin_image zh-lightbox-thumb" width="640" data-original="https://pic2.zhimg.com/v2-ed6a36d84fbe94d9bfa89d53999b30a6_r.jpg"/>
3)脚本的并发锁控制
避免多人同时执行或并发同时执行的异常问题,建议增加锁机制,示例如下:
<img src="https://pic2.zhimg.com/50/v2-873a1b3a7ef0eaca3eb49e0ee446bbd3_hd.jpg" data-caption="" data-size="normal" data-rawwidth="640" data-rawheight="921" class="origin_image zh-lightbox-thumb" width="640" data-original="https://pic2.zhimg.com/v2-873a1b3a7ef0eaca3eb49e0ee446bbd3_r.jpg"/>
4)控制脚本不退出的风险
周期频繁执行的脚本,需要防止脚本hang住不退出,导致后续脚本再次执行。
<img src="https://pic4.zhimg.com/50/v2-e3a715abdfba6a91011ca4f0d45757dc_hd.jpg" data-caption="" data-size="normal" data-rawwidth="640" data-rawheight="249" class="origin_image zh-lightbox-thumb" width="640" data-original="https://pic4.zhimg.com/v2-e3a715abdfba6a91011ca4f0d45757dc_r.jpg"/>
5)避免集中发布脚本造成的风险
使用ftp、sftp传输、下载文件,或者集中访问存储端口时,尽量增加发布对象散列,避免集中操作造成存储端口拥堵,跨防火墙流量超限报警等影响。
<img src="https://pic4.zhimg.com/50/v2-4adc7ea83ebc54c68dbc64a1e6d5842e_hd.jpg" data-caption="" data-size="normal" data-rawwidth="964" data-rawheight="216" class="origin_image zh-lightbox-thumb" width="964" data-original="https://pic4.zhimg.com/v2-4adc7ea83ebc54c68dbc64a1e6d5842e_r.jpg"/>
6)避免文件无限增长的风险
向一个文件中追加数据时,一定要设置阀值,必要时清空,避免文件无限增大:
<img src="https://pic3.zhimg.com/50/v2-acf429788d04dcbd0e6f1777f91637e9_hd.jpg" data-caption="" data-size="normal" data-rawwidth="640" data-rawheight="272" class="origin_image zh-lightbox-thumb" width="640" data-original="https://pic3.zhimg.com/v2-acf429788d04dcbd0e6f1777f91637e9_r.jpg"/>
目录增加清理过期文件策略,避免产生的文件越来越多,造成文件节点用尽:
<img src="https://pic2.zhimg.com/50/v2-e09302efe97ca337e3b52fbed441c2eb_hd.jpg" data-caption="" data-size="normal" data-rawwidth="964" data-rawheight="123" class="origin_image zh-lightbox-thumb" width="964" data-original="https://pic2.zhimg.com/v2-e09302efe97ca337e3b52fbed441c2eb_r.jpg"/>
目录中的文件过多,会报参数太长错误无法删除,建议放在循环中遍历删除:
<img src="https://pic1.zhimg.com/50/v2-cce24570cd146b1fb9467ae0c37990cb_hd.jpg" data-caption="" data-size="normal" data-rawwidth="927" data-rawheight="245" class="origin_image zh-lightbox-thumb" width="927" data-original="https://pic1.zhimg.com/v2-cce24570cd146b1fb9467ae0c37990cb_r.jpg"/>
总结:
鉴于以上脚本,我们可以从中汲取一些经验,规避一些风险:
通过增加日志记录输出和脚本执行的方法说明,并自动交互和传递参数,避免执行脚本的操作风险;利用文件锁机制和运维中一些规避风险的方法,使得脚本自动执行起来更便捷更安全。
1. 通过规范类脚本的定义,标准常量定义、清晰的注释、函数和变量大小写用法,细节中可以看出严谨,即使只有几行,也能体现出一名优秀脚本开发人员的素质。
2. 通过易错类脚本中的“坑”,使得 shell面向过程的编写更得心应手,让脚本规范的同时,逻辑也更严谨清晰,避免了错误,也提高了脚本的开发效率。
3. 通过运维场景的脚本应用,规避各种开发和执行过程中的风险,使得shell脚本不仅能支持自动化发布,更可以全面智能化的为运维服务。
本文转自公众号:LD 匠心独运维妙维效