防痴呆设计




最近有点痴呆,因为解决了太多的痴呆问题,
服务框架实施面超来超广,已有50多个项目在使用,
每天都要去帮应用查问题,来来回回,
发现大部分都是配置错误,或者重复的文件或类,或者网络不通等,
所以准备在新版本中加入防痴呆设计,估且这么叫吧,
可能很简单,但对排错速度还是有点帮助,
希望能抛砖引玉,也希望大家多给力,想出更多的防范措施共享出来。

(1) 检查重复的jar包
最痴呆的问题,就是有多个版本的相同jar包,
会出现新版本的A类,调用了旧版本的B类,
而且和JVM加载顺序有关,问题带有偶然性,误导性,
遇到这种莫名其妙的问题,最头疼,
所以,第一条,先把它防住,
在每个jar包中挑一个一定会加载的类,加上重复类检查,
给个示例:
Java代码   收藏代码
  1. static {  
  2.     Duplicate.checkDuplicate(Xxx.class);  
  3. }  

检查重复工具类:
Java代码   收藏代码
  1. public final class Duplicate {  
  2.   
  3.     private Duplicate() {}  
  4.   
  5.     public static void checkDuplicate(Class cls) {  
  6.         checkDuplicate(cls.getName().replace('.''/') + ".class");  
  7.     }  
  8.   
  9.     public static void checkDuplicate(String path) {  
  10.         try {  
  11.             // 在ClassPath搜文件  
  12.             Enumeration urls = Thread.currentThread().getContextClassLoader().getResources(path);  
  13.             Set files = new HashSet();  
  14.             while (urls.hasMoreElements()) {  
  15.                 URL url = urls.nextElement();  
  16.                 if (url != null) {  
  17.                     String file = url.getFile();  
  18.                     if (file != null && file.length() > 0) {  
  19.                         files.add(file);  
  20.                     }  
  21.                 }  
  22.             }  
  23.             // 如果有多个,就表示重复  
  24.             if (files.size() > 1) {  
  25.                 logger.error("Duplicate class " + path + " in " + files.size() + " jar " + files);  
  26.             }  
  27.         } catch (Throwable e) { // 防御性容错  
  28.             logger.error(e.getMessage(), e);  
  29.         }  
  30.     }  
  31.   
  32. }  

(2) 检查重复的配置文件
配置文件加载错,也是经常碰到的问题,
用户通常会和你说:“我配置的很正确啊,不信我发给你看下,但就是报错”,
然后查一圈下来,原来他发过来的配置根本没加载,
平台很多产品都会在classpath下放一个约定的配置,
如果项目中有多个,通常会取JVM加载的第一个,
为了不被这么低级的问题折腾,
和上面的重复jar包一样,在配置加载的地方,加上:
Java代码   收藏代码
  1. Duplicate.checkDuplicate("xxx.properties");  

(3) 检查所有可选配置
必填配置估计大家都会检查,因为没有的话,根本没法运行,
但对一些可选参数,也应该做一些检查,
比如:服务框架允许通过注册中心关联服务消费者和服务提供者,
也允许直接配置服务提供者地址点对点直连,
这时候,注册中心地址是可选的,
但如果没有配点对点直连配置,注册中心地址就一定要配,
这时候也要做相应检查。

(4) 异常信息给出解决方案
在给应用排错时,最怕的就是那种只有简单的一句错误描述,啥信息都没有的异常信息,
比如上次碰到一个Failed to get session异常,
就这几个单词,啥都没有,哪个session出错? 什么原因Failed?
看了都快疯掉,因是线上环境不好调试,而且有些场景不是每次都能重现,
异常最基本要带有上下文信息,包括操作者,操作目标,原因等,
最好的异常信息,应给出解决方案,比如上面可以给出:
"从10.20.16.3到10.20.130.20:20880之间的网络不通,
请在10.20.16.3使用telnet 10.20.130.20 20880测试一下网络,
如果是跨机房调用,可能是防火墙阻挡,请联系SA开通访问权限"
等等,上面甚至可以根据IP段判断是不是跨机房。
另外一个例子,是spring-web的context加载,
如果在getBean时spring没有被启动,
spring会报一个错,错误信息写着:
请在web.xml中加入:<listener>...<init-param>...
多好的同学,看到错误的人复制一下就完事了,我们该学学,
可以把常见的错误故意犯一遍,看看错误信息能否自我搞定问题,
或者把平时支持应用时遇到的问题及解决办法都写到异常信息里。

(5) 日志信息包含环境信息
每次应用一出错,应用的开发或测试就会把出错信息发过来,询问原因,
这时候我都会问一大堆套话,
用的哪个版本呀?
是生产环境还是开发测试环境?
哪个注册中心呀?
哪个项目中的?
哪台机器呀?
哪个服务?
。。。
累啊,最主要的是,有些开发或测试人员根本分不清,
没办法,只好提供上门服务,浪费的时间可不是浮云,
所以,日志中最好把需要的环境信息一并打进去,
最好给日志输出做个包装,统一处理掉,免得忘了。
包装Logger接口如:
Java代码   收藏代码
  1. public void error(String msg, Throwable e) {  
  2.     delegate.error(msg + " on server " + InetAddress.getLocalHost() + " using version " + Version.getVersion(), e);  
  3. }  

获取版本号工具类:
Java代码   收藏代码
  1. public final class Version {  
  2.   
  3.     private Version() {}  
  4.   
  5.     private static final Logger logger = LoggerFactory.getLogger(Version.class);  
  6.   
  7.     private static final Pattern VERSION_PATTERN = Pattern.compile("([0-9][0-9\\.\\-]*)\\.jar");  
  8.   
  9.     private static final String VERSION = getVersion(Version.class"2.0.0");  
  10.   
  11.     public static String getVersion(){  
  12.         return VERSION;  
  13.     }  
  14.   
  15.     public static String getVersion(Class cls, String defaultVersion) {  
  16.         try {  
  17.             // 首先查找MANIFEST.MF规范中的版本号  
  18.             String version = cls.getPackage().getImplementationVersion();  
  19.             if (version == null || version.length() == 0) {  
  20.                 version = cls.getPackage().getSpecificationVersion();  
  21.             }  
  22.             if (version == null || version.length() == 0) {  
  23.                 // 如果MANIFEST.MF规范中没有版本号,基于jar包名获取版本号  
  24.                 String file = cls.getProtectionDomain().getCodeSource().getLocation().getFile();  
  25.                 if (file != null && file.length() > 0 && file.endsWith(".jar")) {  
  26.                     Matcher matcher = VERSION_PATTERN.matcher(file);  
  27.                     while (matcher.find() && matcher.groupCount() > 0) {  
  28.                         version = matcher.group(1);  
  29.                     }  
  30.                 }  
  31.             }  
  32.             // 返回版本号,如果为空返回缺省版本号  
  33.             return version == null || version.length() == 0 ? defaultVersion : version;  
  34.         } catch (Throwable e) { // 防御性容错  
  35.             // 忽略异常,返回缺省版本号  
  36.             logger.error(e.getMessage(), e);  
  37.             return defaultVersion;  
  38.         }  
  39.     }  
  40.   
  41. }  


(6) kill之前先dump
每次线上环境一出问题,大家就慌了,
通常最直接的办法回滚重启,以减少故障时间,
这样现场就被破坏了,要想事后查问题就麻烦了,
有些问题必须在线上的大压力下才会发生,
线下测试环境很难重现,
不太可能让开发或Appops在重启前,
先手工将出错现场所有数据备份一下,
所以最好在kill脚本之前调用dump,
进行自动备份,这样就不会有人为疏忽。
dump脚本示例:
Java代码   收藏代码
  1. JAVA_HOME=/usr/java  
  2. OUTPUT_HOME=~/output  
  3. DEPLOY_HOME=`dirname $0`  
  4. HOST_NAME=`hostname`  
  5.   
  6. DUMP_PIDS=`ps  --no-heading -C java -f --width 1000 | grep "$DEPLOY_HOME" |awk '{print $2}'`  
  7. if [ -z "$DUMP_PIDS" ]; then  
  8.     echo "The server $HOST_NAME is not started!"  
  9.     exit 1;  
  10. fi  
  11.   
  12. DUMP_ROOT=$OUTPUT_HOME/dump  
  13. if [ ! -d $DUMP_ROOT ]; then  
  14.     mkdir $DUMP_ROOT  
  15. fi  
  16.   
  17. DUMP_DATE=`date +%Y%m%d%H%M%S`  
  18. DUMP_DIR=$DUMP_ROOT/dump-$DUMP_DATE  
  19. if [ ! -d $DUMP_DIR ]; then  
  20.     mkdir $DUMP_DIR  
  21. fi  
  22.   
  23. echo -e "Dumping the server $HOST_NAME ...\c"  
  24. for PID in $DUMP_PIDS ; do  
  25.     $JAVA_HOME/bin/jstack $PID > $DUMP_DIR/jstack-$PID.dump 2>&1  
  26.     echo -e ".\c"  
  27.     $JAVA_HOME/bin/jinfo $PID > $DUMP_DIR/jinfo-$PID.dump 2>&1  
  28.     echo -e ".\c"  
  29.     $JAVA_HOME/bin/jstat -gcutil $PID > $DUMP_DIR/jstat-gcutil-$PID.dump 2>&1  
  30.     echo -e ".\c"  
  31.     $JAVA_HOME/bin/jstat -gccapacity $PID > $DUMP_DIR/jstat-gccapacity-$PID.dump 2>&1  
  32.     echo -e ".\c"  
  33.     $JAVA_HOME/bin/jmap $PID > $DUMP_DIR/jmap-$PID.dump 2>&1  
  34.     echo -e ".\c"  
  35.     $JAVA_HOME/bin/jmap -heap $PID > $DUMP_DIR/jmap-heap-$PID.dump 2>&1  
  36.     echo -e ".\c"  
  37.     $JAVA_HOME/bin/jmap -histo $PID > $DUMP_DIR/jmap-histo-$PID.dump 2>&1  
  38.     echo -e ".\c"  
  39.     if [ -r /usr/sbin/lsof ]; then  
  40.     /usr/sbin/lsof -p $PID > $DUMP_DIR/lsof-$PID.dump  
  41.     echo -e ".\c"  
  42.     fi  
  43. done  
  44. if [ -r /usr/bin/sar ]; then  
  45. /usr/bin/sar > $DUMP_DIR/sar.dump  
  46. echo -e ".\c"  
  47. fi  
  48. if [ -r /usr/bin/uptime ]; then  
  49. /usr/bin/uptime > $DUMP_DIR/uptime.dump  
  50. echo -e ".\c"  
  51. fi  
  52. if [ -r /usr/bin/free ]; then  
  53. /usr/bin/free -t > $DUMP_DIR/free.dump  
  54. echo -e ".\c"  
  55. fi  
  56. if [ -r /usr/bin/vmstat ]; then  
  57. /usr/bin/vmstat > $DUMP_DIR/vmstat.dump  
  58. echo -e ".\c"  
  59. fi  
  60. if [ -r /usr/bin/mpstat ]; then  
  61. /usr/bin/mpstat > $DUMP_DIR/mpstat.dump  
  62. echo -e ".\c"  
  63. fi  
  64. if [ -r /usr/bin/iostat ]; then  
  65. /usr/bin/iostat > $DUMP_DIR/iostat.dump  
  66. echo -e ".\c"  
  67. fi  
  68. if [ -r /bin/netstat ]; then  
  69. /bin/netstat > $DUMP_DIR/netstat.dump  
  70. echo -e ".\c"  
  71. fi  
  72. echo "OK!"  


------------------------
Dubbo设计分享系列:
负载均衡扩展接口重构
一些设计上的基本常识
谈谈泛化式扩展与组合式扩展
分享到:
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值