来源:Qunar技术沙龙
江保贵
2011年4月加入去哪儿网,目前在技术中心大前端架构团队担任架构师,专注于移动端质量效率提升,监控体系设计搭建等工作。先后多次参与客户端框架改版设计、制定开发规范,设计开发移动端差分升级系统、移动端交互日志 & 性能采集系统、渠道快速打包自动发布系统等,长期关注大前端技术演变与性能提升。


首先,我们需要根据出问题的堆栈提取引起关键信息,排除内存值等干扰信息后进行聚合分类; 其次,根据 Android native、so 库、ios、ReactNative 等不同异常类型,对错误堆栈进行反混淆、符号明文化处理,让开发可以看到堆栈就能知道定位哪块代码; APM 系统与构建打包平台 MPortal(大前端打包发布平台)数据打通,获取每个 lib 库包名、so 资源列表、ios 页面列表、业务负责人等,精准划分业务线,获取报警联系人; 降噪:我们动态配置 hook/xposed 等关键字,过滤掉爬虫黑名单数据,减少因误报而造成的数据干扰;

应用运行上报的异常到达 APM 后,都会提取异常信息(lib库、异常类型、关键堆栈)进行聚合生成 BugId,判断该 BugID 之前30天内是否有过上报,如果没有则作为新增崩溃,过去一段时间影响用户数是否超过一定阈值(崩溃2个/5个、卡顿10个/25个),如果有则通过 QTalk(Qunar 内部即时通讯软件)、电话等进行报警,这样就做到防止微小异常扩大成大的故障;

粗粒度监控——Watcher:我们每个异常崩溃上报,都会调用 Watcher 监控系统进行打点,超过一定阈值就会报警,如下边这个是针对 Android 客户端 fastjson 库的异常监控指标图






支持热发布代码:针对 ReactNative 这类支持热发布的问题,往往通过动态下发资源包即可修复相关问题,更新比较及时快速;为此我们增加业务线积分机制,该类问题要求业务线24小时内进行处理,并标记解决状态,否则进行积分扣除,强化业务对开发质量的重视程度; 堆栈明确的原生代码:如 Java、So 等需要发版才能修复验证的问题,堆栈明确的问题,我们一般采用尝鲜用户优先灰度升级进行验证,灰度发布期间发现的问题修复后方可上线,灰度崩溃率超过千分之一就自动阻断发布; 没有明确堆栈、各个业务都有的共性问题:我们框架会立项专项治理,通过加强基础数据采集和监控、分析用户反馈和日志、分析交互轨迹进行溯源、添加保护逻辑等手段进行治理;
Fatal Exception: java.lang.IllegalArgumentException Expected URL scheme 'http' or 'https' but no colon was found okhttp3.HttpUrl$Builder.parse$okhttp (HttpUrl.kt:1260) okhttp3.HttpUrl$Companion.get (HttpUrl.kt:1633) okhttp3.Request$Builder.url (Request.kt:184) okhttp3.Cache$Entry.response (Cache.kt:641) okhttp3.Cache.get$okhttp (Cache.kt:183) okhttp3.internal.cache.CacheInterceptor.intercept (CacheInterceptor.kt:47) okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) okhttp3.internal.http.BridgeInterceptor.intercept (BridgeInterceptor.kt:83) okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.kt:76) okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) okhttp3.internal.connection.RealCall.getResponseWithInterceptorChain$okhttp(RealCall.kt:201) okhttp3.internal.connection.RealCall$AsyncCall.run (RealCall.kt:517) java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
okhttp3.Request$Builder.class 源码:
public Builder url(String url) { if (url == null) throw new NullPointerException("url == null"); if (url.regionMatches(true, 0, "ws:", 0, 3)) { url = "http:" + url.substring(3); } else if (url.regionMatches(true, 0, "wss:", 0, 4)) { url = "https:" + url.substring(4); } return url(HttpUrl.get(url)); } public Builder url(URL url) { if (url == null) throw new NullPointerException("url == null"); return url(HttpUrl.get(url.toString())); }public Builder url(String str) { String url = HttpUtils.checkNullUrl(str);//插入url是否为空检查 if (url == null) { throw new NullPointerException("url == null"); } if (url.regionMatches(true, 0, "ws:", 0, 3)) { url = "http:" + url.substring(3); } else if (url.regionMatches(true, 0, "wss:", 0, 4)) { url = "https:" + url.substring(4); } return url(HttpUrl.get(HttpUtils.checkUrl(url)));//插入url格式是否合法检查 } public Builder url(URL url) { URL url2 = HttpUtils.checkUrlURL(url);//插入url格式是否正确代码 if (url2 == null) { throw new NullPointerException("url == null"); } return url(HttpUrl.get(url2.toString())); }public class HttpUtils { public static String checkUrl(String url) { try { HttpUrl.get(url);//okhttp原有方法,对url进行格式校验 return url; } catch (Exception e) { recodeUrlErrorLog(url);//记录错误并上报监控 return "https://imgs.qunarzz.com/404.png";//返回一个标准格式的404 URL } } }
//bundle数据大的时候比较耗时,此段逻辑需要放子线程处理void recodeBundleSize(String activityName, Bundle bundle) { Bundle copyBundle = bundle.deepCopy(); int totalSize = getParcelSize(copyBundle); Log.d("BundleSize", activityName + " totalSize:" + totalSize); if (totalSize > 100 * 1024) {//如果bundle数据Size大于100KB进行下一步分析 for (String itemKey : copyBundle.keySet().toArray(new String[0])) { int itemSize = getParcelSize(bundle.get(itemKey)); Log.d("BundleSize", activityName + " itemSize:" + itemSize); } } } //获取bundle或者object数据大小方法 int getParcelSize(Object data) { Parcel deepData = Parcel.obtain(); try { deepData.writeValue(data); return deepData.dataPosition();// 单位是byte } finally { deepData.recycle(); } }




