2024年7月

本文全面探索PromQL,从基础语法到高级操作,详细介绍了数据聚合、时间序列分析及内置函数应用,旨在提升用户构建复杂监控策略和性能分析的能力。

关注【TechLeadCloud】,分享互联网架构、云服务技术的全维度知识。作者拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师,项目管理专业人士,上亿营收AI产品研发负责人

file

一、PromQL简介

Prometheus Query Language (PromQL) 是一个专为Prometheus监控系统设计的强大查询语言,它允许用户对收集的时间序列数据进行高效、灵活的查询和分析。PromQL的设计哲学在于提供简洁而强大的语法,以支持复杂的数据检索和实时监控场景。本章节旨在为读者提供PromQL的背景知识、设计原则以及它与Prometheus的关系。

1.1 Prometheus和PromQL的关系

Prometheus是一个开源的系统监控和警报工具包,广泛用于云原生环境中。它通过收集和存储时间序列数据,支持实时监控和警报。PromQL作为Prometheus的核心组件,允许用户通过强大的查询语言对这些数据进行检索和分析。无论是简单的数据查看还是复杂的性能分析,PromQL都能够提供必要的工具来满足用户的需求。

1.2 PromQL的设计哲学

PromQL的设计哲学围绕着几个关键点:灵活性、表现力和性能。它旨在提供足够的灵活性,以支持从简单到复杂的各种查询需求,同时保持查询表达式的简洁性。此外,PromQL经过优化以支持高效的数据处理和检索,这对于实时监控系统来说至关重要。

灵活性和表现力

PromQL支持广泛的操作符、函数和聚合方法,使用户能够编写精确的查询来检索所需的数据。用户可以通过标签选择器来过滤时间序列,或者使用聚合操作来汇总数据。这种灵活性和表现力使PromQL成为一个强大的工具,适用于各种监控和分析场景。

性能

Prometheus和PromQL都设计有优秀的性能特性,可以快速处理大量的时间序列数据。PromQL的查询优化器能够有效地减少查询的计算资源消耗,保证即使在数据量巨大的情况下也能保持良好的查询响应时间。

二、PromQL基础

file

PromQL(Prometheus Query Language)是一个专为Prometheus设计的强大查询语言,它为用户提供了一种高效且灵活的方式来查询和分析时间序列数据。本章节将深入探讨PromQL的基础知识,包括数据类型、核心语法、以及如何构建基本的查询表达式。通过具体的示例和详细的解释,我们将帮助读者掌握PromQL的基本使用方法,为进一步的学习和应用打下坚实的基础。

2.1 数据类型和结构

PromQL操作的核心数据单元是时间序列,时间序列是由时间戳和对应值组成的序列。在PromQL中,主要操作以下几种数据类型:

即时向量(Instant Vector)

即时向量是一个时间点上的一组时间序列,每个时间序列具有一个唯一的标签集合和一个数值。它通常用于表示某一瞬间的系统状态。

示例:

假设我们有一个监控系统的CPU使用率的时间序列,其查询表达式可能如下:

cpu_usage{host="server01"}

该查询返回“server01”主机上最新的CPU使用率数据。

区间向量(Range Vector)

区间向量是在一段时间范围内的一组时间序列,它可以用来分析时间序列的变化趋势或计算时间序列的移动平均等。

示例:

要查询过去5分钟内“server01”主机的CPU使用率数据:

cpu_usage{host="server01"}[5m]

标量(Scalar)

标量是一个简单的数值类型,它不带有时间戳,通常用于数学计算或与时间序列数据的比较。

示例:

假设我们想要将“server01”主机的CPU使用率与一个固定阈值进行比较:

cpu_usage{host="server01"} > 80

这里“80”就是一个标量值。

字符串(String)

字符串类型在PromQL中用得较少,主要用于标签值的展示。

2.2 核心语法

PromQL的核心语法包括标签选择器、操作符、内置函数等,下面我们将一一介绍。

标签选择器

标签选择器允许用户根据标签过滤时间序列,标签由键值对组成。用户可以根据需要选择一个或多个标签进行过滤。

示例:

查询标签为
{job="prometheus", instance="localhost:9090"}
的所有时间序列:

{job="prometheus", instance="localhost:9090"}

操作符

PromQL支持多种操作符,包括算术操作符、比较操作符和逻辑操作符,用于对数据进行计算和比较。

算术操作符示例:

cpu_usage{host="server01"} + 10

这个查询会将“server01”主机的CPU使用率每个值增加10。

比较操作符示例:

cpu_usage{host="server01"} > 80

这个查询会返回所有CPU使用率大于80%的数据点。

内置函数

PromQL提供了一系列内置函数,用于数据聚合、数据处理等。

聚合函数示例:

sum(cpu_usage{job="prometheus"}) by (instance)

这个查询会按照
instance
标签对
cpu_usage
进行求和。

数据处理函数示例:

rate(http_requests_total{job="api-server"}[5m])

这个查询会计算每个
instance
在过去5分钟内每秒的HTTP请求增长率。

2.3 构建基本的查询表达式

实例

查询

假设我们要监控名为"api-server"的服务的HTTP请求延迟,我们可以使用以下查询:

histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{job="api-server"}[5m])) by (le))

这个查询使用了
histogram_quantile
函数来计算在过去5分钟内,所有"api-server"服务中95%的请求所观察到的最大延迟。

综合应用

考虑到一个更复杂的场景,我们不仅想要监控服务的延迟,还想要根据不同的HTTP方法(如GET、POST)分别监控。这时,我们可以构建如下查询:

sum by (method)(rate(http_request_duration_seconds_count{job="api-server"}[5m]))

这个查询将按照HTTP方法分类,计算过去5分钟内每种方法的请求频率。

通过这些示例,我们可以看到,PromQL的查询表达式非常灵活而强大,它能够帮助用户从不同角度和维度对监控数据进行深入分析。掌握PromQL的基础知识和使用方法,对于有效地利用Prometheus进行系统监控和性能分析至关重要。随着对PromQL更深入的学习和实践,用户将能够构建更加复杂和精细的监控策略,以适应不断变化的监控需求。

三、PromQL高级操作

随着对Prometheus和PromQL的深入了解,用户会发现其强大功能不仅限于基本的数据查询和简单计算。PromQL的高级操作包括复杂的数据聚合、时间序列选择器的高级用法、以及各种内置函数的灵活应用,这些都是进行深入监控分析和故障排查的强大工具。本章节将通过详细的示例和解释,探讨PromQL的高级操作功能。

3.1 聚合运算

聚合运算是PromQL中最强大的特性之一,它允许用户对一组时间序列进行统一处理,从而得出单一的结果。这对于理解整体趋势和性能瓶颈尤为重要。

sum - 求和

求和是最常用的聚合操作之一,可以用来计算多个时间序列的总和。

示例:

sum(http_requests_total{job="api-server"}) by (method)

这个查询会按照HTTP方法(如GET、POST)对所有
api-server
服务的请求总数进行求和。

avg - 平均值

计算一组时间序列的平均值,通常用来理解系统的平均表现。

示例:

avg(cpu_usage{environment="production"}) by (instance)

这个查询会计算生产环境中每个实例的CPU平均使用率。

max/min - 最大值/最小值

找出一组时间序列中的最大值或最小值,用于监控系统的极限表现。

示例:

max(memory_usage{job="database"}) by (instance)

这个查询将返回每个数据库实例的最大内存使用量。

3.2 时间序列选择器的高级用法

时间序列选择器不仅可以选择特定的时间范围,还可以用来执行更复杂的查询,比如滑动窗口平均或预测。

offset - 时间偏移

offset
允许用户查询过去某个时间点的数据,对于比较历史数据非常有用。

示例:

http_requests_total{job="api-server"} offset 1w

这个查询返回一周前
api-server
服务的HTTP请求总数。

rate - 变化率

rate
函数计算时间序列在给定时间范围内的平均变化率,适用于计算增长或下降趋势。

示例:

rate(http_requests_total{job="api-server"}[5m])

这个查询计算过去5分钟内
api-server
服务每秒的请求增长率。

3.3 函数和运算符的灵活应用

PromQL提供了多种函数和运算符,支持复杂的数据处理和分析。

predict_linear - 线性预测

predict_linear
函数用于预测时间序列在未来一段时间内的值,基于线性回归模型。

示例:

predict_linear(disk_space_usage{job="database"}[1h], 4 * 3600)

这个查询预测4小时后数据库的磁盘空间使用情况。

histogram_quantile - 直方图分位数

histogram_quantile
函数用于从直方图数据中计算分位数值,适用于性能监控中的响应时间分析。

示例:

histogram_quantile(0.9, rate(http_request_duration_seconds_bucket{job="api-server"}[10m]))

这个查询计算过去10分钟内,
api-server
服务90%的请求响应时间。

3.4 实战案例分析

动态警报设置

使用PromQL的高级功能可以灵活设置动态警报,根据系统的实时表

现动态调整警报阈值。

示例:

avg by (job)(rate(http_requests_total{status="500"}[5m])) > 5 * avg by (job)(rate(http_requests_total[1h]))

这个警报规则意味着,如果5分钟内500错误的平均增长率超过过去1小时平均增长率的5倍,则触发警报。

性能瓶颈分析

通过聚合运算和函数,可以有效地分析系统的性能瓶颈。

示例:

topk(3, avg by (instance)(rate(cpu_usage{job="web-server"}[5m])))

这个查询找出CPU使用率平均增长最快的前3个web-server实例,帮助定位性能瓶颈。

通过这些高级操作和应用示例,我们可以看到PromQL不仅支持强大的数据查询和处理能力,而且还提供了灵活的监控和分析工具。掌握这些高级特性将帮助用户更深入地理解和优化他们的监控系统,从而提高系统的稳定性和性能。随着对PromQL更进一步的学习和实践,用户将能够发现更多高级技巧,以应对各种复杂的监控场景。

关注【TechLeadCloud】,分享互联网架构、云服务技术的全维度知识。作者拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师,项目管理专业人士,上亿营收AI产品研发负责人
如有帮助,请多关注
TeahLead KrisChang,10+年的互联网和人工智能从业经验,10年+技术和业务团队管理经验,同济软件工程本科,复旦工程管理硕士,阿里云认证云服务资深架构师,上亿营收AI产品业务负责人。

一句注释引发的思考

接到一个有鸡毛信般的紧急需求(当然,002的需求向来是如此紧急的):大屏展示原来只有二个品牌数据,现增加到三个品牌的数据。一句话的需求,且没有业务逻辑变更,我认为可以迅雷不及掩耳之势,2小时收拾干净交差。当我满腔激情的定位的核心逻辑部分时,这样一句注释(见下图),让我顿时思绪天马行空:

这个作者经历了什么样一个撕心裂肺的过程?但是可以肯点的是这一定是一个有想法的作者,不由得心中肃然起敬。

这段代码经历了多少次的蹂躏,才会让作者的心潮有如此波澜?

抑或,这到底提出了怎么样一个需求,让作者需要通过这样的注释来宣泄心中怨气?

注释图

巴拉开代码修改记录,作者已经去别的地方高就了,要不是留了这些代码,实在想不起有这样的一个同事存在过;代码提交记录比较整洁,大部分代码是在5月29号提交,5月30大概是修复bug提交了小部分代码。如此看来,代码没有经历过什么苦难,这里的需求仅仅是每个品牌的门店按订单数量排序(如下图),想想怎么也闹出什么大动静... 再细读作者留下的代码,只能说作者给自己设置了难度系数(这说法太含蓄了),稍微有一点改动,便是牵一发而动全身,于是留了这样一个不太成熟且不太有价值的注释,想起了最近在读的一本书,Bob大叔的《代码整洁之道》,颇有一点不成熟的感触。

如何衡量代码质量

《代码整洁之道》一书开篇第一句话就是一个著名的论断:衡量代码质量的唯一有效标准:WTF/min。 一看到WTF这样的简称就不明觉厉,一顿搜索居然没有找到“标准”的解释,更觉得高深莫测。 也是在后来一个偶然的时间看到原来是 What-The-Fuck 的缩写,看重看这张经典的图,一下子恍然顿悟:原来如此,就该如此。

原本学得这是一本好书,甚至觉得大部分程序都应该去阅读这类的书籍一遍(比如 马丁·福勒出品的《重构-改善既有代码的设计》)。 看到这样的论述,更觉此书接地气,仿佛与大师拉近了距离了。回想起过往种种,以及最近修改历史代码时的反应,没什么比WTF更有表达力了。

再回到开篇讲的注释,当时的作者必然也是有类似的反应,只是他的吐嘲对象是他自己,或者他只会认为这是需求的而不是代码的问题。所以 WTF/min作为衡量代码质量的唯一有效标准,还得加一个定语:优秀的Coder喊出的WTF次数,才是真正的标准。 至于如何为优秀的代码,在《代码整洁之道》,《重构-改善既有代码的设计》等经典书籍里都有详细的描述:

  • 从变量命名,到注释,到方法,到类,到模块都有非常详细的规范;
  • 从抽象到边界,到依赖也有完整的方法论;
  • 从SOLID设计原则到,组件相关的 CRP,CCP,CRP等原则都有从理论到落地的解说。

坦白讲,读完这些书后,我感觉自己原来写的代码,很多都缺乏这样的思考,也有不少代码相当丑陋,甚至觉得:在code这件事让,只能算得上初窥门径。于是越时读书,越觉得自己无知。最近读技术书籍的间歇,顺便翻读了几本名人传记:奥本海默为了让人觉得他是天才,总是"偷偷"的读书学习;特斯拉甚至在病危之际也是一心读书;让我们有在剑手的钱学森利用所有空闲时间阅读方能一年完成硕士学位,并获得航空和数学双博士,成本最年轻的终生教授...(省略好多荣誉)。他们无一不是通过自己努力读书,思考,实践终成我等学习的楷模。最近招聘的经历中,大部分应聘的人几乎不看技术书籍的了,也是让我捉摸不透。

读技术书籍没用了吗

最近两月一直忙于面试,沟通了没有100个也有80个候选人,大部分人都没有了读书习惯,更不用说技术书籍了,倒是有部分说觉得blog比书籍有用多了。有些说工作太忙,有得说没有用....他们中有的在传统公司朝9晚5,有的在互联网企业996,有从小企业过来的,也有从阿里,快手过来的... 其中一个人的话,让我记忆非常深刻,最后环节,我问他现在还读书不,他说为了进阿里,他非常努力了2年,把《深入理解java虚拟机》读了2遍,《高性能mysql》,《深入理解kafka》... 都仔细阅读了,后来进了阿里,觉得这些书都没有没啥用了,也不在阅读其他技术书籍了,最后我不知道应该说啥,毕竟我没去过阿里,于是结束了面试。就我自己而言,早些年面试也是被问得体无完肤,也有过这样的心态阅读了大量类似 《深入理解java虚拟机》,《MySQL技术内幕:InnoDB存储引擎》,《RocketMQ技术内幕:RocketMQ架构设计与实现原理》,《从Paxos到Zookeeper:分布式一致性原理与实践》等书籍,确实也让自己在后来的面试中可以从容面对。但越是阅读,越是感觉自己不知道的东西越多,越是想要通过阅读来充实自己。于是又开始阅读系统设计,架构一类的书籍。时至今日,读到《代码整洁之道》时,依然觉得即使在做了10年的编码这件事上,不懂的依然有非常之多(哈哈,也许是悟性不够)。 从前面的名人,到我身边认识的人,大凡优秀的人的,独有阅读的习惯,并且有大量阅读自己专业相关的书籍。

最近和媳妇探讨读书这件事儿,说我周末在家除了溜娃就是刷新闻,现如今的新闻包括热搜又大部分是没有“营养”的,也聊到目下短视频盛行,下到3岁,上到70,地铁上,公园里,城市里,老家里... 到处都是,当然也包括我们自己的爸妈,谈及此,心中不觉升起一股名族忧心(哈哈,操心有点多了)。于是我放下了新闻,当然了周5晚上,等娃睡着后,我们买些宵夜,找一部金典电影还是保持着。 一段时间后,发现一个周阅读10几个小时好像也挺正常的,阅读成生活的一部分。也没有了过去那种读了多久了,要休息下的,看看新闻,看看电视的想法了。现在想来,读书也好,新闻,短视频也罢,本质且没啥差异,内心富足就好。

再回到前面的面试,也不知道,我在面试过程中,把读书这一块看得如此重,是否合适,但是我相信:喜欢阅读技术书籍的人,应该都不会太差。

回到前面的代码

回家开篇的注释问题,想和大家一直分享下代码重构过程,如果不幸被作者看到,希望不要介怀,就如Bob大叔所讲,每个程序员都应该接专业眼光的检视(哈哈也许我也不是那么专业)。 需求比较简单,就是两个品牌下的门店根据订单数排序。 现在的需求是增加了第三个品牌,门店信息有品牌属性。

如果作者阅读了Clean Code ,他就会明白代码走向整洁的4原则:

  • 运行所有测试;
  • 不可重复;
  • 表达了程序员的意图;
  • 尽可能减少类和方法的数量。

就会把排序算法抽离出来,与业务逻辑分离,避免大量重复;

如果他深刻喊出了 Don't Repeat YourSelf, 就不会有么多 ConsultationOrderRank 对象的出事化,甚至不会单独处理有数据与没数据的情况。

如果作者阅读了Clean Architecture,他就会明白要面向抽象,而不是具体去编程,

他就会面向品牌这个概念去编程,而不是面向具体的品牌1,品牌2去实现。

//需求变更,改的像辣鸡。
if(CollectionUtils.isEmpty(orderList)) {
List
<CfgStore> allStoreList =cfgStoreService.getStoresBLAndBabyBL();
List
<CfgStore> bellaList = allStoreList.stream().filter(st ->{return st.getType() == 0;
}).sorted(Comparator.comparingInt(CfgStore::getStoreId)).collect(Collectors.toList());

ArrayList
<ConsultationOrderRank> ballaResult = new ArrayList<>();int bellaIndex = 0;for(CfgStore store : bellaList) {
ConsultationOrderRank consultationOrderRank
= newConsultationOrderRank();
consultationOrderRank.setStoreName(store.getNameAlias());
consultationOrderRank.setStoreId(store.getStoreId());
consultationOrderRank.setOrderNum(
0);
consultationOrderRank.setSort(bellaIndex);
ballaResult.add(consultationOrderRank);
bellaIndex
++;
}
List
<ConsultationOrderRank> blRankResult =ballaResult.stream()
.sorted(Comparator.comparing(ConsultationOrderRank::getSort)).collect(Collectors.toList());

List
<CfgStore> babyBellaList = storeList.stream().filter(st ->{return st.getType() == 1;
}).sorted(Comparator.comparingInt(CfgStore::getStoreId)).collect(Collectors.toList());

ArrayList
<ConsultationOrderRank> babyBallaResult = new ArrayList<>();int babyIndex = 0;for(CfgStore store : babyBellaList) {
ConsultationOrderRank consultationOrderRank
= newConsultationOrderRank();
consultationOrderRank.setStoreName(store.getNameAlias());
consultationOrderRank.setStoreId(store.getStoreId());
consultationOrderRank.setOrderNum(
0);
consultationOrderRank.setSort(babyIndex);
babyBallaResult.add(consultationOrderRank);
babyIndex
++;
}

List
<ConsultationOrderRank> babyRankResult =babyBallaResult.stream()
.sorted(Comparator.comparing(ConsultationOrderRank::getSort))
.collect(Collectors.toList());
Order order
=Order.builder().consultationOrderRankStBellaList(blRankResult)
.consultationOrderRankBabyBellaList(babyRankResult).build();
returnorder;
}

List
<CfgStore> others = storeList.stream().filter(store ->{return !Arrays.stream(storeIdArr).collect(Collectors.toList()).contains(store.getStoreId());
}).collect(Collectors.toList());

Map
<Integer, CfgStore> storeMap = storeList.stream().collect(Collectors.toMap(CfgStore::getStoreId, store ->{returnstore;
}));
//品牌1门店ID List<Integer> blIdList = storeList.stream().filter(st -> st.getType().equals(0))
.map(CfgStore::getStoreId).collect(Collectors.toList());
//品牌2门店ID List<Integer> babyblIdList = storeList.stream().filter(st -> st.getType().equals(1))
.map(CfgStore::getStoreId).collect(Collectors.toList());
//品牌2分组数据 Map<Integer, List<HeOrder>> babyblMap =orderList.stream()
.filter(order
->babyblIdList.contains(order.getStoreId()))
.collect(Collectors.groupingBy(HeOrder::getStoreId));
//品牌2分组数据 Map<Integer, List<HeOrder>> blMap =orderList.stream()
.filter(order
->blIdList.contains(order.getStoreId()))
.collect(Collectors.groupingBy(HeOrder::getStoreId));
//品牌1排行数据 List<ConsultationOrderRank> bellaList = new ArrayList<>();//品牌2排行数据 List<ConsultationOrderRank> babyBellaList = new ArrayList<>();//品牌1 for (Entry<Integer, List<HeOrder>>entry : babyblMap.entrySet()) {
CfgStore cfgStore
=storeMap.get(entry.getKey());
String storeName
=cfgStore.getNameAlias();if(Strings.isNotBlank(storeName)) {
List
<HeOrder> orderNum =entry.getValue();
ConsultationOrderRank consultationOrderRank
= newConsultationOrderRank();
consultationOrderRank.setStoreName(storeName);
consultationOrderRank.setStoreId(entry.getKey());
consultationOrderRank.setOrderNum(orderNum
== null ? 0: orderNum.size());
babyBellaList.add(consultationOrderRank);
}
}

List
<CfgStore> otherbabyBlList = others.stream().filter(store ->{return store.getType() == 1;
}).collect(Collectors.toList());
for(CfgStore store : otherbabyBlList) {
ConsultationOrderRank consultationOrderRank
= newConsultationOrderRank();
consultationOrderRank.setStoreName(store.getNameAlias());
consultationOrderRank.setStoreId(store.getStoreId());
consultationOrderRank.setOrderNum(
0);
babyBellaList.add(consultationOrderRank);
}
//品牌2 for (Entry<Integer, List<HeOrder>>entry : blMap.entrySet()) {
CfgStore cfgStore
=storeMap.get(entry.getKey());
String storeName
=cfgStore.getNameAlias();if(Strings.isNotBlank(storeName)) {
List
<HeOrder> orderNum =entry.getValue();
ConsultationOrderRank consultationOrderRank
= newConsultationOrderRank();
consultationOrderRank.setStoreName(storeName);
consultationOrderRank.setStoreId(entry.getKey());
consultationOrderRank.setOrderNum(orderNum
== null ? 0: orderNum.size());
bellaList.add(consultationOrderRank);
}
}

List
<CfgStore> otherBellaList = others.stream().filter(store ->{return store.getType() == 0;
}).collect(Collectors.toList());
for(CfgStore store : otherBellaList) {
ConsultationOrderRank consultationOrderRank
= newConsultationOrderRank();
consultationOrderRank.setStoreName(store.getNameAlias());
consultationOrderRank.setStoreId(store.getStoreId());
consultationOrderRank.setOrderNum(
0);
bellaList.add(consultationOrderRank);
}
//品牌1排序 List<ConsultationOrderRank> blRank =bellaList.stream().sorted(Comparator.comparing(ConsultationOrderRank::getOrderNum).reversed())
.collect(Collectors.toList());
int blSort = 0;int blIndexCounter = 0;for (int i = 0; i < blRank.size(); i++) {//订单=0, 订单值不同, 递增 boolean flag = blRank.get(i).getOrderNum() == 0 || (i != 0 && blRank.get(i).getOrderNum() != blRank.get(i - 1).getOrderNum());if(flag) {
blSort
= blSort + blIndexCounter + 1;
blIndexCounter
= 0;
}
else{if (i != 0) {
blIndexCounter
++;
}
}
blRank.get(i).setSort(blSort);
}
//品牌2排序 List<ConsultationOrderRank> babyBlRank =babyBellaList.stream().sorted(Comparator.comparing(ConsultationOrderRank::getOrderNum).reversed())
.collect(Collectors.toList());
int babySort = 0;int babyIndexCounter = 0;for (int i = 0; i < babyBlRank.size(); i++) {//订单=0, 订单值不同, 递增 boolean flag = babyBlRank.get(i).getOrderNum() == 0 || (i != 0 && babyBlRank.get(i).getOrderNum() != babyBlRank.get(i - 1).getOrderNum());if(flag) {
babySort
= babySort + babyIndexCounter + 1;
babyIndexCounter
= 0;
}
else{if (i != 0) {
babyIndexCounter
++;
}
}
babyBlRank.get(i).setSort(babySort);
}

我相信作者如果经常阅读技术书籍,写出的代码应该是这样的。

        //统计每个品牌每个门店订单数量
        for(Integer brandType : brandTypeList){
Map
<Integer, Long> theBrandStoreOrderCount = orderList.stream().filter(order -> brandType.longValue() ==order.getBrandType()).collect(Collectors.groupingBy(HeOrder::getStoreId, Collectors.counting()));
List
<CfgStore> brandStores = storeList.stream().filter(store ->store.getType().equals(brandType)).collect(Collectors.toList());

List
<ConsultationOrderRank> storeOrderRank = new ArrayList<>();

brandStores.forEach(store
->{
Long orderCount
= 0L;if(theBrandStoreOrderCount.containsKey(store.getStoreId())){
orderCount
=theBrandStoreOrderCount.get(store.getStoreId());
}
ConsultationOrderRank storeOrder
=ConsultationOrderRank.builder()
.storeId(store.getStoreId())
.orderNum(orderCount.intValue())
.storeName(store.getStoreName())
.sort(
0).build();
storeOrderRank.add(storeOrder);
});
List
<ConsultationOrderRank> sortedStoreRank =storeOrderRank.stream().sorted(Comparator.comparing(ConsultationOrderRank::getOrderNum).reversed()).collect(Collectors.toList());

setSortWithSameRankNum(sortedStoreRank);

BrandOrderStatistic statistic
=BrandOrderStatistic.builder()
.name(CfgStoreEnum.getValueByCode(brandType))
.storeOrderRank(sortedStoreRank)
.brandType(brandType).build();
brandOrderStatistics.add(statistic);
}

如此这般,我们当时践行了编码里的童子军规:当你离开营地时候,要让它比你来的时候更整洁干净。

成为一名优秀的程序员!

deepin系统介绍

deepin(原名Linux Deepin)致力于为全球用户提供美观易用,安全可靠的 Linux发行版。deepin项目于2008年发起,并在2009年发布了以 linux deepin为名称的第一个版本。2014年4月更名为 deepin,在中国常被称为“深度操作系统”。

截止到 2023 年,深度操作系统全球下载量超过 9000 万次,提供超过 33 种不同的语言版本,以及遍布六大洲的 140 多个镜像站点的下载服务。在
全球开源操作系统排行榜
上是排名最高的中国操作系统产品。

image-20240704102737696

在国产操作系统中我个人最喜欢也最看好deepin,因此本文以deepin系统为例,其他linux系统也差不多。

开始实践

避坑

Avalonia开发桌面项目项目结构一般如下所示:

image-20240704102959543

如果直接继续操作,复制文件到deepin系统上的时候是这样子的:

image-20240704103119929

.Desktop文件是Linux系统中应用程序启动和管理的重要组成部分,它使得用户可以方便地在不同的桌面环境中启动和管理应用程序。同时,.Desktop文件也可以用于定义快捷方式、脚本和其他类型的程序。

貌似被当成了.Desktop文件,无法运行起来,直接改名也不行。

如果你的项目结构也如上图所示,修改项目名称为不以.Desktop结尾:

image-20240704103814304

命名空间也顺带改一下:

image-20240704103849727

右键,点击发布:

image-20240704103932189

选文件夹:

image-20240704104011175

选文件夹:

image-20240704104032691

默认位置:

image-20240704104054973

点击显示所有设置:

image-20240704104123221

进行设置:

image-20240704104210856

这里选择独立部署模型,选择目标运行时,这里是linux-x64,选择生成单个文件。

点击发布:

image-20240704104310426

发布结果如下所示:

image-20240704104359526

生成位置中的文件如下所示:

image-20240704104540234

将linux-x64文件夹复制到deepin系统中,如下所示:

image-20240704104702975

image-20240704104735636

双击运行,点击运行:

image-20240704104824223

成功运行:

image-20240704105016761

将昨天写的小工具也按照上面的方式,在deepin系统上运行,效果如下所示:

一、介绍
这是我的《Advanced .Net Debugging》这个系列的第十篇文章。这篇文章的内容是原书的第三部分的【高级主题】的第八章【事后调试】。前面几篇文章,我们介绍了很多工具,可以帮助大家找出问题的所在。但是,有一类问题我们是没办法使用这些工具来解决的,那就是已经发布的程序。在程序发布后,总是会出现一些问题,并且这些问题出现的时机是不确定的,大多数出现在用户在使用软件的过程中。想要解决这样的问题,我们当然会想到远程调试,有时候可以,有时候是不可以的,当然,不可以的理由会有很多,比如:安全的原因等类似的。
如果使用软件的客户拒绝对出现问题的软件进行实时调试,并且本地又无法重现问题,那我们该怎么办呢?当然是有办法的,那就是【事后调试】。
【事后调试】的步骤:
1)、触发故障的发生。
2)、抓取系统在发生故障时的状态快照(根据不同的故障类型,在某些情况下还需要抓取故障发生前后的状态快照)。
3)、将快照发送给工程师作进一步的分析。
在这篇文章中,我们将讨论抓取快照的各种不同方式,不同类型的转储信息以及如何分析它们。
当然,高级调试会涉及很多方面的内容,你对 .NET 基础知识掌握越全面、细节越底层,调试成功的几率越大,当我们遇到各种奇葩问题的时候才不会手足无措。
如果在没有说明的情况下,所有代码的测试环境都是 Net 8.0,如果有变动,我会在项目章节里进行说明。好了,废话不多说,开始我们今天的调试工作。

调试环境我需要进行说明,以防大家不清楚,具体情况我已经罗列出来。
操作系统:Windows Professional 10
调试工具:Windbg Preview(Debugger Client:1.2306.1401.0,Debugger engine:10.0.25877.1004)和 NTSD(10.0.22621.2428 AMD64)
下载地址:可以去Microsoft Store 去下载
开发工具:Microsoft Visual Studio Community 2022 (64 位) - Current版本 17.8.3
Net 版本:.Net 8.0
CoreCLR源码:
源码下载


在此说明:我使用了两种调试工具,第一种:Windbg Preivew,图形界面,使用方便,操作顺手,不用担心干扰;第二种是:NTSD,是命令行形式的调试器,在命令使用上和 Windbg 没有任何区别,之所以增加了它的调试过程,不过是我的个人爱好,想多了解一些,看看他们有什么区别,为了学习而使用的。如果在工作中,我推荐使用 Windbg Preview,更好用,更方便,也不会出现奇怪问题(我在使用 NTSD 调试断点的时候,不能断住,提示内存不可读,Windbg preview 就没有任何问题)。
如果大家想了解调试过程,二选一即可,当然,我推荐查看【Windbg Preview 调试】。

二、目录结构
为了让大家看的更清楚,也为了自己方便查找,我做了一个目录结构,可以直观的查看文章的布局、内容,可以有针对性查看。

2.1、
转储文件基本知识

2.1.1、
通过调试器来生成转储文件

A、
基础知识

B、
眼见为实

1)、
NTSD 调试

2)、
Windbg Preview 调试

2.1.2、
通过 ADPlus 生成转储文件

A、
基础知识

B、
眼见为实

1)、
崩溃模式

2.1.3、
转储文件的调试

2.1.4、
数据访问层

2.1.5、
转储文件分析:未处理的 .NET 异常

1)、
NTSD 调试

2)、
Windbg Preview 调试

2.2、
Windows 错误报告


三、调试源码
废话不多说,本节是调试的源码部分,没有代码,当然就谈不上测试了,调试必须有载体。

1、ExampleCore_8_01


1 namespaceExampleCore_8_012 {3     internal classProgram4 {5         static void Main(string[] args)6 {7             Program program = newProgram();8 program.Run();9 }10 
11         public voidRun()12 {13             Console.WriteLine("Press any key to start");14 Console.ReadKey();15             ProcessData(null);16 }17 
18         public void ProcessData(string?data)19 {20             if (data == null)21 {22                 throw new ArgumentNullException("Argument is Null");23 }24             string s = "Hello:" +data;25 }26 }27 }

View Code


四、基础知识
在这一段内容中,有的小节可能会包含两个部分,分别是 A 和 B,也有可能只包含 A,如果只包含 A 部分,A 字母会省略。A 是【基础知识】,讲解必要的知识点,B 是【眼见为实】,通过调试证明讲解的知识点。

4.1、转储文件的基本知识

转储文件是进程状态的外在表示。生成转储文件的目的:在不需要对出问题的计算机进行实时访问的情况下,就可以对程序故障进行分析。有了转储文件,调试人员就可以使用调试器的事后调试功能来分析故障。
共有两种类型的转储文件:
1)、
完全转储文件(Full Dump)

2)、
微型转储文件(Mini Dump)

在完全转储文件中包含了进程的整个内存空间、可执行影响、句柄表和调试器需要使用的其他信息。当使用完全转储文件时不能指定所要收集的数据量。但是,我们可以使用调试器将完全转储文件转换为微型转储文件。
微型转储文件的内容是可变的,并且能根据使用的转储文件生成器进行定制。在微型转储文件中可以只包含某个线程的信息,也可以包含被转储进程的详细信息。需要注意的是,在最大的微型转储文件中包含的内容要多于在完全转储文件中包含的内容。因此,我们这里只介绍微型转储文件的结构。

以下是能够生成转储文件的工具列表:
1)、
Windows Debuggers(Windows 调试器)
:Windows 调试器可以生成各种不同大小的转储文件,并且能够完全控制转储文件的生成过程。
2)、
ADPlus
:ADPlus 是 Windows 调试工具集中的一个。它的作用相当于一个进程监视器,每当发生崩溃或者挂起时,都能生成转储文件。此外,它还能将崩溃事件通知给用户。
3)、
Windows 错误报告
:Windows错误报告是Microsoft提供的一种服务,用户通过这种服务注册到一个实时的错误报告站点。每当用户的某个应用程序发生错误时,都会将一个错误报告从发生崩溃的机器发送到Windows错误报告站点。然后,在进行事后分析时可以从WER服务中提取崩溃信息(包括转储文件)。

以上都是书里介绍的内容,由于书写的比较早,到现在还有很多其他工具可以生成转储文件,比如:任务管理器,Windbg 调试器,Process Explorer,PCHunter 等,使用起来也很方便,网上学习资料很多,我就不多说了。
接下来,我们就针对这三种工具分别介绍如何生成转储文件。

4.1.1、通过调试器生成转储文件

A、基础知识

如果我们想使用调试器生成 DUMP 文件,可以使用【.dump】命令,【.dump /m】表示调试器将生成一个微型转储文件。当然【.dump】命令还有其他的选项,如下:
a、生成一个完整的微型转储文件,启动所有选项。在这个文件中将包含完整的内存数据、句柄信息、模块信息、基本的内存信息和线程信息等。相当于使用/mfFhut。
f、生成一个微型转储文件,其中包含进程内所有可访问和已提交的内存页。
F、生成一个微型转储文件,其中包含调试器在重构整个虚拟内存地址空间时需要的所有基本内存信息。
h、生成一个微型转储文件,其中包含句柄信息。
u、生成一个微型转储文件,其中包含未卸载模块的信息。注意,这个选项只能在Windows Server 2003上使用。
t、生成一个微型转储文件,其中包含线程时间的信息。在线程时间信息中包括创建时间,以及在用户态和内核态中执行的时间。
i、生成一个微型转储文件,其中包含辅助内存信息。辅助内存是指由栈指针或者后台存储使用的内存(及其周围的一小块内存)。
p、生成一个微型转储文件,其中包含进程环境块和线程环境块。
w、生成一个微型转储文件,其中包含所有已提交的读-写私有内存页。
d、生成一个微型转储文件,其中包含映像中的所有数据段。
c、生成一个微型转储文件,其中包含映像中的所有代码段。
r、生成一个微型转储文件,适合于在需要保护隐私的情况中使用。这个选项将删除在重建栈时不需要的任何信息(将这些信息替换为0,包括局部变量)。
R、生成一个微型转储文件,适合在需要保护隐私的情况下使用。这个选项将从微型转储文件中删除完整的模块路径,因此将确保用户目录结构的隐私性。
举个例子
:我们在调试器中执行【.dump /ma /u F:\MyDump.dmp】命令,可以在F盘看到生成 dmp 文件到MyDump_1c90_2024-06-27_15-13-24-466_3660.dmp文件。 .dump命令参数比较多,常用的组合就是/ma,/m 表示生成minidump,/a 表示dmp包含所有信息,/u 参数就是上面说的附加时间和PID信息到文件名。

1 0:000> .dump /ma /u F:\Test\TestDump\MyDump.dmp2 Creating F:\Test\TestDump\MyDump_1c90_2024-06-27_15-13-24-466_3660.dmp -mini user dump3 Dump successfully written

当我们在生成一个转储文件的时候,有一个经验法则,在转储文件中包含的状态越多,在进行事后调试的时候就能获取更多的信息。当然,最大的限制因素就是转储文件的大小。有时候你会发现无法从一个高安全的服务器上获取很大的转储文件,而只能对一个删除了某些敏感信息之后的转储文件进行分析。
如果我们使用命令行调试器,当我们需要加载 dump 文件时,必须使用【ntsd -z dumpFileName】命令才可以,我使用的是【ntsd】调试器。dumpFileName:必须包含 dump 文件的完整路径和后缀名。
通过调试器生成转储文件的一个难点就是调试器必须在合适的时候被附加到故障进程上。一般来说,还好,但是对于崩溃情况是不定时的,是没有规律的,这样就很容易错失附加调试器的机会。当我们的程序发生崩溃的时候
Windows 可以自动的生成转储文件就好了,这种机制是有的。我们可以使用“
事后调试器设置(Postmortem Debugger Setup)
”。我们可以使用以下命令来修改事后调试器:
windbg -I、cdb -iae、ntsd -iae、drwtsn32 -i
效果如图:

想要执行以上命令,直接打开【cmd】命令行工具,输入命令【windbg -I】、【cdb -iae】、【ntsd -iae】、【drwtsn32 -i】就可以了。
修改注册表的值:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug
效果如图:

这个结果是我执行【cdb -iae】命令的结果。命令窗口如图:


我们可以直接运行我们的实例程序,控制台程序一旦抛出异常,立刻就能打开我们注册调试器。
1)先演示执行【Windbg -I】命令后的效果。
双击我们的控制台程序,看到“
Press any key to start
”字样,然后点击回车键,就会打开指定的调试器。
效果如图:

2)、先演示执行【cdb -iae】命令后的效果。
双击我们的控制台程序,看到“
Press any key to start
”字样,然后点击回车键,就会打开指定的调试器。
效果如图:

转储文件生成注意事项:

Windows Vista 中修改了错误报告技术在本地机器上保存转储文件的方式。在 Windows Vista 之前,Dr.Watson
默认将生成的转储文件保存在本地机器上。这些转储文件可以由任何一个想要调试转储文件的用户访问。在 Windows Vista 中去掉了
Dr.Watson,而是引入了一种更为可靠和稳定的错误报告机制。其中的修改之一就是,生成的转储文件(在默认情况下)不会被保存到本地机器上。要改变这种默认行为,可以将注册表
ForceQueue 设置为1,这将使所有转储文件在上传到 Microsoft 之前就在本地机器上排队。ForceQueue
注册键值位于以下注册路径:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error
Reporting
在注册键值 ForceQueue 被设置为1后,生成的所有转储文件都将被保存到以下位置:
对于在系统上下文中运行或者被提升到系统上下文中运行的进程:
%ALLUSERSPROFILE %\Microsoft\Windows\WER\[ReportQueue|ReportArchive]
对于所有其他的进程:
%LOCALAPPDATA%\Microsoft\Windows\WER\[ReportQueue|ReportArchive]

当调试非托管代码程序时,会用到 AeDebug

键值。然而,对于托管代码调试,可以使用【DbgManagedDebugger】和【DbgJITDebugLaunchSetting】这两个键值控制器调试。该键值位置:计算机\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework,效果如图:

1)

DbgManagedDebugger
:该注册键值会指定当遇到未处理的异常时应该启动哪一个调试器。当遇到未处理的异常时,该注册键值指定的调试器并不会立即调用,而是显示一个消息框,由用户选择是调试程序还是结束程序。在事后调试中,一个很常见的问题就是,如何在程序发生故障的时候自动生成转储文件。
我们可以执行以下命令【ntsd
-pv -p %ld -c ",dump /u /ma <path to dump file>; .kill;
qd】,该命令的意思是,当一个故障发生时,启动【ntsd】调试器,并且执行【.dump】命令生成一个转储文件,然后退出调试回话。

2)、DbgJITDebugLaunchSetting
:该注册键值表示发生未处理异常时的行为。如果这个值被设置为 0,那么将显示一个消息框,并由用户选择对故障采用何种处理方式。请注意,只有在交互进程的情况下才会显示这个消息框,而对于其他进程(例如服务)则是直接结束。
如果 DbgJITDebugLaunchSetting 被设置 1,那么程序会直接结束,并返回一个栈转储。
如果 DbgJITDebugLaunchSetting 被设置 2,那么将立刻启动在 DbgManagedDebugger 中指定的调试器,而不会显示消息框。
如果 DbgJITDebugLaunchSetting 被设置 16,对于交互式进程会显示前面的消息框,而对于非交互式进程则会直接启动在 DbgManagedDebugger 中指定的调试器。

B、眼见为实

调试源码:ExampleCore_8_01
调试任务:通过 Windows 调试器生成转储文件。
1)、NTSD 调试

编译项目,打开【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_8_01\bin\Debug\net8.0\ExampleCore_8_01.exe】打开调试器。
进入调试器,我们直接使用【g】命令,运行调试器,直到看到调试器输入“
Press any key to start
”字样,我们按回车键,让调试器继续执行,发现现在的调试器已经中断执行了。
我们可以执行【!clrstack】命令,查看一下当前的线程调用栈。

1 0:000> !clrstack2 OS Thread Id: 0x1764 (0)3 Child SP               IP Call Site4 000000132477E8A8 00007ff87ecbcf19 [HelperMethodFrame: 000000132477e8a8]5 000000132477E9A0 00007FFF41F21AFB ExampleCore_8_01.Program.ProcessData(System.String)6 000000132477EA00 00007FFF41F21A46 ExampleCore_8_01.Program.Run()7 000000132477EA50 00007FFF41F21988 ExampleCore_8_01.Program.Main(System.String[])8 0:000>

接下来,我们使用【
.dump /mf F:\Test\TestDump\08dumpfile2.dmp】命令,生成转储文件,保存目录在
F:\Test\TestDump 下,文件名称是
08dumpfile2.dmp。说明一下,文件名为什么加一个 2,因为我已经生成一个文件了,文件名必须不同,否则会有错误。

1 0:000> .dump /mf F:\Test\TestDump\08dumpfile.dmp2 Unable to create file 'F:\Test\TestDump\08dumpfile.dmp' -Win32 error 0n803     "文件存在。"

文件名修改后,继续执行,看到“
Dump successfully written
”字样就说明成功了。

1 0:000> .dump /mf F:\Test\TestDump\08dumpfile2.dmp2 Creating F:\Test\TestDump\08dumpfile2.dmp -mini user dump3 Dump successfully written

我们需要再打开一个【NTSD】调试器,执行【NTSD -z
F:\Test\TestDump\08dumpfile2.dmp
】命令,加载我们刚刚生成的 DUMP 文件。

1 ** Visual Studio 2022 Developer Command Prompt v17.9.6
2 ** Copyright (c) 2022Microsoft Corporation3 **********************************************************************
4 
5 D:\Program Files\Microsoft Visual Studio\2022\Community>NTSD -z F:\Test\TestDump\08dumpfile2.dmp

调试器显示如下:

1 Microsoft (R) Windows Debugger Version 10.0.22621.2428AMD642 Copyright (c) Microsoft Corporation. All rights reserved.3 
4 
5 Loading Dump File [F:\Test\TestDump\08dumpfile2.dmp]6 User Mini Dump File with Full Memory: Only application data is available7 
8 Symbol search path is: srv*
9 Executable search path is:10 Windows 10 Version 19045 MP (4procs) Free x6411 Product: WinNt, suite: SingleUserTS12 Edition build lab: 19041.1.amd64fre.vb_release.191206-1406
13 Machine Name:14 Debug session time: Wed Jun 26 17:02:22.000 2024 (UTC + 8:00)15 System Uptime: 0 days 6:57:46.621
16 Process Uptime: 0 days 0:05:15.000
17 ..................................18 This dump file has an exception of interest stored in it.19 The stored exception information can be accessed via .ecxr.20 (4fa8.1764): CLR exception - code e0434352 (first/second chance not available)21 For analysis of this file, run !analyze -v22 KERNELBASE!RaiseException+0x69:23 00007ff8`7ecbcf19 0f1f440000      nop     dword ptr [rax+rax]

第一部分红色标注的说明加载 DUMP 文件的信息,第二部分红色标注的就是故障原因(CLR 异常)。
接下来,我们就可以使用各种命令调试我们的程序了。

2)、Windbg Preview 调试

编译项目,打开【Windbg Preview】调试器,依次点击【文件】----【Launch executable】,加载我们的项目文件 ExampleCore_8_01.exe,进入到调试器中。
直接【g】命令运行调试器,直到我们的控制台程序输入“
Press any key to start
”字样,我们在控制台程序中,按任何键继续执行。此时,调试器会中断执行,因为抛出了异常。
我们可以使用【!clrstack】命令查看一下调用栈的情况。

1 0:000> !clrstack2 OS Thread Id: 0x3758 (0)3 Child SP               IP Call Site4 000000DB2357E988 00007ff87ecbcf19 [HelperMethodFrame: 000000db2357e988]5 000000DB2357EA80 00007fff3fb21afb ExampleCore_8_01.Program.ProcessData(System.String) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_8_01\Program.cs @ 22]6 000000DB2357EAE0 00007fff3fb21a46 ExampleCore_8_01.Program.Run() [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_8_01\Program.cs @ 15]7 000000DB2357EB30 00007fff3fb21988 ExampleCore_8_01.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_8_01\Program.cs @ 8]

说明我们的程序执行到 ProcessData 方法的第 22 行出现了问题,因为我们抛出了异常。红色标注的
22
就是源代码中发生问题的行号。

接下来,我们使用【
.dump /mf F:\Test\TestDump\08dumpfile.dmp
】命令,生成转储文件,保存目录在
F:\Test\TestDump 下,文件名称是
08dumpfile.dmp

1 0:000> .dump /mf F:\Test\TestDump\08dumpfile.dmp2 Creating F:\Test\TestDump\08dumpfile.dmp -mini user dump3 Dump successfully written

当我们看到了“
Dump successfully written
”字样时,说明 Dump 文件写成功了,在指定目录就能看到该文件。
我们有了 DUMP 文件,我们再打开一个【Windbg Preview】调试器加载 DUMP 文件就可以了。依次点击【文件】----【Open dump file】,在右侧通过浏览按钮选择我们的 Dump 文件,点击【open】按钮,就可以了。

1 ************* Preparing the environment for Debugger Extensions Gallery repositories **************
2 ExtensionRepository : Implicit3    UseExperimentalFeatureForNugetShare : false
4    AllowNugetExeUpdate : false
5    NonInteractiveNuget : true
6    AllowNugetMSCredentialProviderInstall : false
7    AllowParallelInitializationOfLocalRepositories : true
8 
9    EnableRedirectToV8JsProvider : false
10 
11    --Configuring repositories12       ----> Repository : LocalInstalled, Enabled: true
13       ----> Repository : UserExtensions, Enabled: true
14 
15 >>>>>>>>>>>>> Preparing the environment for Debugger Extensions Gallery repositories completed, duration 0.000seconds16 
17 ************* Waiting for Debugger Extensions Gallery to Initialize **************
18 
19 >>>>>>>>>>>>> Waiting for Debugger Extensions Gallery to Initialize completed, duration 0.032seconds20    ----> Repository : UserExtensions, Enabled: true, Packages count: 0
21    ----> Repository : LocalInstalled, Enabled: true, Packages count: 41
22 
23 Microsoft (R) Windows Debugger Version 10.0.27553.1004AMD6424 Copyright (c) Microsoft Corporation. All rights reserved.25 
26 
27 Loading Dump File [F:\Test\TestDump\08dumpfile.dmp]28 User Mini Dump File with Full Memory: Only application data is available29 
30 
31 ************* Path validation summary **************
32 Response                         Time (ms)     Location33 Deferred                                       srv*
34 Symbol search path is: srv*
35 Executable search path is:36 Windows 10 Version 19045 MP (4procs) Free x6437 Product: WinNt, suite: SingleUserTS38 Edition build lab: 19041.1.amd64fre.vb_release.191206-1406
39 Debug session time: Wed Jun 26 13:41:21.000 2024 (UTC + 8:00)40 System Uptime: 0 days 3:36:45.501
41 Process Uptime: 0 days 0:16:15.000
42 .......................................................43 This dump file has an exception of interest stored in it.44 The stored exception information can be accessed via .ecxr.45 (43e0.3758): CLR exception - code e0434352 (first/second chance not available)46 For analysis of this file, run !analyze -v47 KERNELBASE!RaiseException+0x69:48 00007ff8`7ecbcf19 0f1f440000      nop     dword ptr [rax+rax]

第一部分红色标注的说明加载 DUMP 文件的信息,第二部分红色标注的就是故障原因(CLR 异常)。
接下来,我们就可以使用各种命令调试我们的程序了。


4.1.2、通过 ADPlus 生成转储文件

A、基础知识

ADPlus 工具能够监测和自动化一个或者多个故障进程的转储文件生成过程,并且能够在发生崩溃时通知用户或者计算机。ADPlus 工具位于 windbg 安装目录,最早叫 adplus.vbs,以VBScript脚本提供,最新版改成了adplus.exe。我们可以使用【cmd】命令打开命令行工具,直接输入 adplus 就可以运行,效果如下:

1 C:\Users\Administrator>adplus2 Starting ADPlus3 ********************************************************
4 *                                                      *
5 * ADPLus Flash V 7.01.007 08/11/2011                   *
6 *                                                      *
7 *   For ADPlus documentation see ADPlus.doc            *
8 *   New command line options:                          *
9 *     -pmn <procname> - process monitor                *
10 *        waits for a process to start                  *
11 *     -po <procname> - optional process                *
12 *        won't fail if this process isn't running      *
13 *     -mss <LocalCachePath>                            *
14 *        Sets Microsoft's symbol server                *
15 *     -r <quantity> <interval in seconds>              *
16 *        Runs -hang multiple times                     *
17 *                                                      *
18 *   ADPlusManager - an additional tool to facilitate   *
19 *   the use of ADPlus in distributed environments like *
20 *   computer clusters.                                 *
21 *   Learn about ADPlusManager in ADPlus.doc            *
22 *                                                      *
23 ********************************************************
24 
25 
26 ADPlus Version 7.01.007 08/11/2011
27 
28 ====================
29 |  ADPlus Usage    |
30 ====================
31 Command line syntax options32         ADPlus -?   or 'ADPlus -help
33             Displays thisinformation.34 
35         ADPlus -HelpConfig36             Displays the built-in key-words and the defaultbehavior settings37 38 
39         ADPlus <runmode> -o <OutputDirectory>[options]40 Run Modes:41             -Crash    Runs ADPlus inCrash mode42             -Hang     Runs ADPlus inHang mode43 
44 Selecting processes to attach45         -p <PID>Defines a Process ID to be attached46         -pn <ProcessName>Defines a process name to be attached47         -po <ProcessName>Defines an optional process name to be attached48         -pmn <ProcessName>Defines a process name to be monitored49                 ADPlus will keep monitoring if a process with thisname starts50 and attach51         -sc <spawning command>Defines the application and parameters to be52                    started inthe debugger53                    The -sc switch, ifused, must be the last one54         -iis All iis related processes will be attached55 like inetinfo, dllhost,mtx, etc.56 
57 Symbol Path Options58 -y <symbol path>Defines the symbol path to be used59 -yp <symbol path to add>Defines an additional symbol path60 -mss <local cache>Adds Microsoft Symbol Server to the symbol path61 
62 Memory Dump Options63 -FullOnFirst   Sets ADPlus to create full dumps on first chance exceptions64 -MiniOnSecond  Sets ADPlus to create mini dumps on second chance exceptions65 -NoDumpOnFirst Sets ADPlus to not create any dumps on first chance exceptions66 -NoDumpOnSecond  Sets ADPlus to not create any dumps on second chance exceptions67 -do Dump Only - changes defaultbehavior to not include additional info, just a dump68 
69 Miscellaneous Options70 -c <config file name>Defines a configuration file to be used71 -o <output directory>   Defines the directory wherelogs and dumps are72 to be placed.73 -r <quantity> <interval in seconds> for multiple attachments inhang mode74 -dbg <debugger>  Allows you to selectthe debugger to be used75                     cdb, windbg or ntsd  (default iscdb)76 -dp    Debuggers path77 -gs    only generates the script file78 
79 -ce <custom exception code>Defines a custom exception to be monitored80                  -ce 0x80501001
81 
82 -bp <breakpoint parameters>Sets a breakpoint83       Syntax: -bp address;optional_additional_parameters84                    -bp MyModule!MyClass::MyMethod85                    -bp MyModule!MyClass::MyMethod;MiniDump86 
87 -CTCF  Creates a full dump on CTL+C, and quits88 -CTCFB  Creates a full dump on CTL+C, and breaks into the debugger89 -CTCV  No special action on CTL+C, just breaks in foruser interaction90 -lcq  sets the last script command to Q (quit)91 -lcg  sets the last script command to G (go)92 -lcgn  sets the last script command to GN (go not handled)93 -lcqd  sets the last script command to QD (quit and detach)94 -lcv  sets the last script command to void (no command; waits foruser input)95 -q2  sets the return action forsecond chance exceptions to Q (quit)96 -g2  sets the return action forsecond chance exceptions to GN (go not handled)97 
98 
99 -quiet    No dialog boxes will be displayed (no more required)100 -notify <destination>Will send a message to the destination101 
102 
103 Examples:104   ADPlus -hang -iis -o c:\dumps105 Produces memory dumps of IIS and all106       MTS/COM+packages currently running.107 
108   ADPlus -crash -p 1896 -o c:\dumps -mss c:\symbols109       Attaches the debugger to process with PID 1896
110       and monitors it for1st and 2nd chance access violations and uses111       Microsoft's public symbol server with c:\symbols as a local cache
112 
113 -------------------------------------------------------------------------------
114 
115 HELP and Documentation116 
117 For more detailed information on how to use and config ADPlus please see118   the debugger's help file (debugger.chm) under Extra Tools
119     However, be aware that this is a newversion of ADPlus and debugger.chm120 may take some time to be updated121   Check for ADPlus.doc in the debuggers'folder
122 -------------------------------------------------------------------------------
123 Current log content124 
125 ADPlus Engine Version: 7.01.007 08/11/2011
126 Command line arguments used were:

ADPlus.exe 不仅可以在程序崩溃时手动运行来生成dmp文件,也可以在崩溃之前就运行它,当程序崩溃时它会自动生成dmp文件;甚至可以在程序没有运行之前就先运行adplus,当程序崩溃时它会自动生成dmp文件。

ADPlus 可以在以下两种模式下运行:
1)、挂起模式(Hang Mode):用于分析出现挂起现象的进程(例如:程序不执行或者 100% 的 CPU 使用率)。ADPlus 必须在进程挂起之后启动。
2)、崩溃模式(Crash Mode):用于分析出现崩溃行为的进程。ADPlus 必须在进程崩溃之后启动。

ADPlus 可以控制生成转储文件的类型,共有四个命令行开关控制这种行为:
1)、-FullOnFirst:将 ADPlus 设置为在首次出现异常时创建完整转储文件。
2)、-MiniOnSecond:将 ADPlus 设置为在第二次出现异常时创建微型转储文件。
3)、-NoDumpOnFirst:将 ADPlus 设置为在首次出现异常时不创建任何微型转储文件。
4)、-NoDumpOnSecond:将 ADPlus 设置为在第二次出现异常时不创建任何微型转储文件。

ADPlus 还为用户提供了一种功能强大的方式来配置信息收集的频率以及在何种条件下收集信息,尤其是为调试器提供了一个脚本前端。这样做不过是采用了一种对用户更友好的方式来执行调试器命令,并将它们的执行过程自动化。

如果我们想了解 ADPlus 命令的使用,可以使用【adplus -?】命令查看该命令的使用方法和各种参数。

B、眼见为实

调试源码:ExampleCore_8_01
调试任务:通过 ADPlus 生成转储文件。
1)、崩溃模式

首先,我们先编译我们的项目,打开项目的可以执行程序文件,也就是 EXE 文件,直接双击执行。我们的程序输出“
Press any key to start
”字样。
在继续之前,我们打开【Visual Studio 2022 Developer Command Prompt v17.9.6】或者【cmd】命令行工具都可以,执行【adplus -pn ExampleCore_8_01.exe -crash -o F:\Test\TestDump】命令,-crash 将 ADPlus 设置为崩溃模式,-pn 告诉 ADPlus 要监控的进程名称,该参数的好处是它能够监视由 name 指定的进程的任意数量实例。-o 表示文件存储的目录地址。
执行效果如图:

当执行完成后,ADPlus 会把结果日志文件保存到我们设置的目录。效果如图:

它会新建一个目录
20240627_152459_Crash_Mode
,在该目录下存放日志文件。当我们的进程发生关闭事件时,ADPlus 将生成一个完全转储文件,我的文件名是:FULLDUMP_SecondChance_clr_NET_CLR_ExampleCore_8_01.exe__3c04_2024-06-27_16-15-43-200_2c28.dmp,效果如图:

文件名太长,截图没有显示全部。我们看到在 F:\Test\TestDump\20240627_152459_Crash_Mode 目录下,有一个文件 DebuggerScript.txt,其中包含了在 ADPlus 会话中使用的所有调试器命令。


4.1.3、转储文件的调试

因为转储文件只是进程状态的一个静态快照,因此,我们不能在代码上设置断点以及单步调试。最好把转储文件看成是一种手动调试,在使用转储文件时,仍然可以使用大多数的调试器命令。
在准备调试转储文件之前,需要先获取两个关键信息:符号文件和数据访问层(Data Access Layer,DAC)。由于在转储文件中不包含任何符号信息,因此当分析转储文件时,符号文件是非常重要的。这里的数据访问层(DAC)指的是 CLR 数据访问层,SOS 将通过这个信息来提供在调试会话中需要的数据。

4.1.4、数据访问层

先说明一下:对于所有版本的.NET Framework,DAC 的文件名 mscordacwks.dll,SOS 调试扩展的文件名 sos.dll,在 Net 跨平台版本名称改了,名称是
mscordaccore.d


我先说说在 Net Framework 环境下的情况。在非托管调试环境中,许多信息都可以通过观察内存来收集,而在托管代码中,SOS 依靠 CLR 来提供我们的调试输出以及结果。为了使 SOS 能够正确解析传递给它的原始数据,SOS 将调用 CLR (即执行CLR代码)来辅助执行这个过程。CLR 中负责实现这个功能的组件就是数据访问层,它包含在 mscordacwks.dll 中。现在,随着 CLR 被不断地增强,底层的 DAC 同样随各个版本(包含补丁)的不同而变化。通过查看机器上每个.NET版本的安装文件夹可以很容易地验证这一点。例如,在我的机器上,
mscordacwks.dll 位于以下文件夹中:C:\Windows\Microsoft.NET\Framework\v4.0.30319
。效果如图:

64 位的目录:C:\Windows\Microsoft.NET\Framework64\v4.0.30319,
效果如图:


由于调试器在其操作期间需要用到这个组件,因此要知道调试器这个文件的位置。在实时调试过程通常不需要关心这个问题,因为 SOS 能够从当前被调试的 CLR 所在位置上找到这个文件。在事后调试(或者转储文件)中,在程序中使用的 CLR 版本可能与转储文件所在机器上的 CLR 版本不同。再次重申,SOS 调试器扩展将调用 mscordacwks.dll 中的函数,这个动态库将执行 CLR 代码,因此为调试器指定正确的版本是非常重要的。
由于 CLR 版本的正确与否对于调试是否成功至关重要,因此,微软公布了 mscordacwks.dll 大部分符号,放在 Microsoft 共有符号服务器上。只要将调试器的符号路径指向共有符号服务器(使用 symfix 或者其他相关的命令),那么调试器就能找到这个文件。但是,有时候需要我们显示告诉 SOS 扩展命令在什么位置上查找该文件。这些情况:当文件不在公共符号服务器上或者文件没有安装在与生成转储文件的机器上的同一个路径下。此时,我们就可以使用【.cordll】命令来控制 mscordacwks.dll 加载的方式,并在处理版本不匹配问题时节约大量的时间。
接下来,我们看看【.cordll】命令的开关:
1)、-l  在默认加载路径中搜索 DLL并加载调试模块。
2)、-u  从内存中卸载调试模块。
3)、-e  启用 CLR 调试。
4)、-d  禁用 CLR 调试
5)、-D  禁用 CLR 调试并卸载调试模块。
6)、-N  重新加载调试模块。
7)、-lp  指定调试模块的目录。
8)、-se  启用使用短名字版本的调试模块,mscordacwks.dll。
9)、-sd  禁用使用短名字的调试模块,mscordacwks.dll。如果指定了这个开关,那么调试模块要以以下格式
加载:mscordacwks_<spec>.dll,其中<spec>的形式为<architecture>_<architecture ><file version>,而<Architecture >可以是x86 或者amd64。
10)、-ve  启用 verbose 模式。当处理不匹配问题时,verbose 模式是非常有用的,因为它能给出调试器如何加载调试模块的信息
11)、-vd  禁用 verbose 模式。

接下来,我们开始讲讲在 Net 5.0 和以上版本的情况,包括 NET 6.0、NET 7.0、NET 8.0,以后还会有更新的版本,都包含在内。
在 NET 跨平台版本中,名称和文件存放位置都发生了很大的变化,现在的文件名称是:
mscordaccore.dll
。当然这个文件也有两个版本,一个是 x86 架构的,一个是 64 架构的。我们安装几个版本的 .NET runtime,就会有几个版本的 DAC 文件与之匹配。我们可以使用【cmd】命令,打开命令行工具,然后输入【dotnet --info】命令,查看 dotnet 具体详情。

1 C:\Users\Administrator>dotnet --info2 .NET SDK:3  Version:           8.0.204
4 Commit:            c338c7548c5  Workload version:  8.0.200-manifests.7d36c14f6 
7 运行时环境:8 OS Name:     Windows9  OS Version:  10.0.19045
10 OS Platform: Windows11  RID:         win-x6412  Base Path:   C:\Program Files\dotnet\sdk\8.0.204\13 
14 已安装 .NET 工作负载:15 没有要显示的已安装工作负载。16 
17 Host:18   Version:      8.0.4
19 Architecture: x6420 Commit:       2d7eea252921 
22 .NET SDKs installed:23   6.0.402[C:\Program Files\dotnet\sdk]24   7.0.302[C:\Program Files\dotnet\sdk]25   8.0.201[C:\Program Files\dotnet\sdk]26   8.0.204[C:\Program Files\dotnet\sdk]27 
28 .NET runtimes installed:29   Microsoft.AspNetCore.App 3.1.32[C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]30   Microsoft.AspNetCore.App 6.0.10[C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]31   Microsoft.AspNetCore.App 6.0.29[C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]32   Microsoft.AspNetCore.App 7.0.5[C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]33   Microsoft.AspNetCore.App 7.0.18[C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]34   Microsoft.AspNetCore.App 8.0.2[C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]35   Microsoft.AspNetCore.App 8.0.4[C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]36   Microsoft.NETCore.App 3.1.32[C:\Program Files\dotnet\shared\Microsoft.NETCore.App]37   Microsoft.NETCore.App 6.0.10[C:\Program Files\dotnet\shared\Microsoft.NETCore.App]38   Microsoft.NETCore.App 6.0.14[C:\Program Files\dotnet\shared\Microsoft.NETCore.App]39   Microsoft.NETCore.App 6.0.29[C:\Program Files\dotnet\shared\Microsoft.NETCore.App]40   Microsoft.NETCore.App 7.0.5[C:\Program Files\dotnet\shared\Microsoft.NETCore.App]41   Microsoft.NETCore.App 7.0.18[C:\Program Files\dotnet\shared\Microsoft.NETCore.App]42   Microsoft.NETCore.App 8.0.2[C:\Program Files\dotnet\shared\Microsoft.NETCore.App]43   Microsoft.NETCore.App 8.0.4[C:\Program Files\dotnet\shared\Microsoft.NETCore.App]44   Microsoft.WindowsDesktop.App 3.1.32[C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]45   Microsoft.WindowsDesktop.App 6.0.10[C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]46   Microsoft.WindowsDesktop.App 6.0.29[C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]47   Microsoft.WindowsDesktop.App 7.0.5[C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]48   Microsoft.WindowsDesktop.App 7.0.18[C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]49   Microsoft.WindowsDesktop.App 8.0.2[C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]50   Microsoft.WindowsDesktop.App 8.0.4[C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]51 
52 Other architectures found:53 x86   [C:\Program Files (x86)\dotnet]54 registered at [HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x86\InstallLocation]55 
56 Environment variables:57   Not set
58 
59 global.json file:60 Not found61 
62 Learn more:63   https://aka.ms/dotnet/info
64 
65 Download .NET:66   https://aka.ms/dotnet/download

这个命令的输出内容确实很多,加粗标注的就是【Runtime(运行时)】的版本,我们也可以使用【dotnet --list-runtimes】命令,只查看运行时。

1 C:\Users\Administrator>dotnet --list-runtimes2 Microsoft.AspNetCore.App 3.1.32[C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]3 Microsoft.AspNetCore.App 6.0.10[C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]4 Microsoft.AspNetCore.App 6.0.29[C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]5 Microsoft.AspNetCore.App 7.0.5[C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]6 Microsoft.AspNetCore.App 7.0.18[C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]7 Microsoft.AspNetCore.App 8.0.2[C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]8 Microsoft.AspNetCore.App 8.0.4[C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]9 Microsoft.NETCore.App 3.1.32[C:\Program Files\dotnet\shared\Microsoft.NETCore.App]10 Microsoft.NETCore.App 6.0.10[C:\Program Files\dotnet\shared\Microsoft.NETCore.App]11 Microsoft.NETCore.App 6.0.14[C:\Program Files\dotnet\shared\Microsoft.NETCore.App]12 Microsoft.NETCore.App 6.0.29[C:\Program Files\dotnet\shared\Microsoft.NETCore.App]13 Microsoft.NETCore.App 7.0.5[C:\Program Files\dotnet\shared\Microsoft.NETCore.App]14 Microsoft.NETCore.App 7.0.18[C:\Program Files\dotnet\shared\Microsoft.NETCore.App]15 Microsoft.NETCore.App 8.0.2[C:\Program Files\dotnet\shared\Microsoft.NETCore.App]16 Microsoft.NETCore.App 8.0.4[C:\Program Files\dotnet\shared\Microsoft.NETCore.App]17 Microsoft.WindowsDesktop.App 3.1.32[C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]18 Microsoft.WindowsDesktop.App 6.0.10[C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]19 Microsoft.WindowsDesktop.App 6.0.29[C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]20 Microsoft.WindowsDesktop.App 7.0.5[C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]21 Microsoft.WindowsDesktop.App 7.0.18[C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]22 Microsoft.WindowsDesktop.App 8.0.2[C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]23 Microsoft.WindowsDesktop.App 8.0.4 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

这些【运行时(Runtimes)】的目录是【C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App】,说明是 64位架构的。说明一下,在这个目录下是找不到 mscordaccore.dll 文件的,该文件的目录是:C:\Program Files\dotnet\shared\Microsoft.NETCore.App,桌面程序(WindowsDesktop)和Asp.Net 程序(AspNetCore)共用这个。效果如图:

随便打开一个文件夹下,都能找到 mscordaccore.dll 和 coreclr.dll 文件,我打开 8.0.4 文件夹,效果如图:

以上是 64 位架构的,接下来,我们看看 x86 架构的,该文件的地址目录是:C:\Program Files (x86)\dotnet\shared\Microsoft.NETCore.App。
效果如图:

在这里,我同样打开最新版本的文件夹,该文件夹名称是 8.0.4,效果如图:


如何知道是否需要使用【.cordll】命令?如果存在版本不匹配的
mscordaccore.dll
(在 Net Framework 版本里提示的是 mscordacwks.dll) ,那么 SOS 调试扩展将输出以下错误信息,在跨平台版本里
mscordaccore.dll
和 coreclr.dll 是一一对应的,在 .Net Framework 版本里 mscordacwks.dll 和 mscorwks.dll 是一一对应的。
说明一下,我已经加载了一个 DUMP 文件,只是执行了一个【!t】命令,就会输出一下内容:

1 0:000> !t2 Failed to load data access module, 0x80004002
3 Verify that 1) you have a recent build of the debugger (10.0.18317.1001or newer)4             2) the file mscordaccore.dll that matches your version of coreclr.dll is
5                 inthe version directory or on the symbol path6             3) or, ifyou are debugging a dump file, verify that the file7                 mscordaccore_<arch>_<arch>_<version>.dll ison your symbol path.8             4) you are debugging on a platform and architecture that supports this
9 the dump file. For example, an ARM dump file must be debugged10 on an X86 or an ARM machine; an AMD64 dump file must be11 debugged on an AMD64 machine.12 
13 You can run the command '!setclrpath <directory>'to control the load path of mscordaccore.dll.14 
15 Or you can also run the debugger command .cordll to control the debugger's
16 load of mscordaccore.dll. .cordll -ve -u -l will doa verbose reload.17 If that succeeds, the SOS command should work on retry.18 
19 If you are debugging a minidump, you need to make sure that your executable20 path is pointing to coreclr.dll aswell.21 
22 For more information see https://go.microsoft.com/fwlink/?linkid=2135652              

我们仔细看看这篇建议,
第一建议很简单
,就是要确保我们的调试器是最新版本。
第二条建议
是要确保
mscordaccore.dll
(原著中是 mscordacwks.dll,也就是 NET FRAMEWORK 版本)的版本和所加载的
coreclr.dll
(原著中的是 mscorwks.dll,也就是 NET FRAMEWORK 版本)是相匹配的。
第三条建议
是如果你调试的 DUMP 文件,要确保
mscordaccore_<arch>_<arch>_<version>.dll
文件位于符号路径中。这个名字其实就是使用了 -sd 命令开关启用长名字,长名字只是将这个 DLL 文件对应的架构以及构建编号添加到 DLL 的名字中。然后,就可以更新符号路径,指向这个 DLL,并执行【.cordll】命令来重新加载 mscordaccore.dll。
例如:如果在生成转储文件时使用的是 mscordaccore.dll 的版本为 1.1.1.0,架构为 x86,那么就可以将 mscordaccore.dll 重命名为 mscordaccore_x86_x86_1.1.1.0.dll,并将调试器的符号路径指向这个重命名的位置,接着使用【.cordll】命令来重新加载调试模块。效果如图:

第四条建议
确保运行调试器的所在的架构与生成转储文件的架构相同。由于调试器要执行 DAC 中的代码,因此,用于调试转储文件的调试器的架构信息与创建转储文件时使用的调试器架构信息要完全一样。
输出的最后一行,要求可执行路径指向
mscordaccore.dll
。可执行路径可以在调试器中通过【.exepath】命令来控制。如果要添加可执行路径,可以使用【!exepath +】命令。
如果无法找到在生成转储文件时使用的 DLL 正确版本,最简单的方法就是,要求生成转储文件的人员把相应的
mscordaccore.dll
文件发给你,在收到文件后,在按照之前给出的策略来加载它。在成功加载之后,SOS 调试器扩展会充分发挥其功能。

我们可以使用【Windbg Preview】调试器,输入【.cordll】命令,可以加载
mscordaccore.dll

1 0:000>.cordll2 CLR DLL status: Loaded DLL C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\mscordaccore.dll               


4.1.5、转储文件分析:未处理的 .NET 异常。

调试源码:根据 ExampleCore_8_01 生成的 DUMP 文件。
调试任务:使用调试器分析 DUMP 文件。
1)、NTSD 调试

打开【Visual Studio 2022 Developer Command Prompt v17.9.6】,输入命令【NTSD -z F:\Test\TestDump\08dumpfile.dmp】打开调试器。
我们成功打开调试器,并且调试器已经中断执行,因为出现了异常。

1 Microsoft (R) Windows Debugger Version 10.0.22621.2428AMD642 Copyright (c) Microsoft Corporation. All rights reserved.3 
4 
5 Loading Dump File [F:\Test\TestDump\08dumpfile.dmp]--成功加载我们的 DUMP 文件6 User Mini Dump File with Full Memory: Only application data isavailable7 
8 Symbol search path is: srv*
9 Executable search path is:10 Windows 10 Version 19045 MP (4procs) Free x6411 Product: WinNt, suite: SingleUserTS12 Edition build lab: 19041.1.amd64fre.vb_release.191206-1406
13 Machine Name:14 Debug session time: Wed Jun 26 13:41:21.000 2024 (UTC + 8:00)15 System Uptime: 0 days 3:36:45.501
16 Process Uptime: 0 days 0:16:15.000
17 ...................................18 This dump file has an exception of interest stored init.19 The stored exception information can be accessed via .ecxr.20 (43e0.3758): CLR exception - code e0434352 (first/second chance not available)发生了异常21 For analysis of this file, run !analyze -v22 KERNELBASE!RaiseException+0x69:23 00007ff8`7ecbcf19 0f1f440000      nop     dword ptr [rax+rax]

以上信息说明,这个 DUMP 是由于引发了一个 CLR 异常而生成的。接着,我们查看异常的详细信息,包括传递的参数,使用【kb】命令。

1 0:000>kb2 RetAddr               : Args to Child                                                           : Call Site3 00007fff`9f5eb3a3     : 000001e5`f8809ce8 000000db`2357e7d0 00007fff`3fbe1080 00007fff`3fbd5e00 : KERNELBASE!RaiseException+0x69(抛出异常)
4 00007fff`9f5ead49     : 00000000`00000004 00000000`00000001 000000db`2357ec18 000000db`2357edb8 : coreclr!RaiseTheExceptionInternalOnly+0x26b
5 00007fff`3fb21afb     :000001e5`f8809ce800000226`8aad0a38 000001e5`f44cb1e8 00000226`8aae0000 : coreclr!IL_Throw
6 00007fff`3fb21a46     : 000001e5`f8809630 00000000`00000000 000001e5`f44af4d0 000001e5`f8809c40 : 0x00007fff`3fb21afb7 00007fff`3fb21988     : 000001e5`f8809630 000001e5`f8809648 000000db`2357f1b8 000000db`2357eda9 : 0x00007fff`3fb21a468 00007fff`9f68b8d3     : 000001e5`f8808e98 000000db`2357f1b8 000000db`2357f1b8 000000db`2357eda9 : 0x00007fff`3fb219889 00007fff`9f5c0b19     : 00000000`00000000 00000000`00000130 000000db`2357edb8 00007fff`9f540232 : coreclr!CallDescrWorkerInternal+0x83
10 (Inline Function)     : --------`-------- --------`-------- --------`-------- --------`-------- : coreclr!CallDescrWorkerWithHandler+0x56
11 00007fff`9f5bd730     : 000000db`2357ee38 00000000`00000000 00000000`00000048 00007fff`9f62d046 : coreclr!MethodDescCallSite::CallTargetWorker+0x2a1
12 (Inline Function)     : --------`-------- --------`-------- --------`-------- --------`-------- : coreclr!MethodDescCallSite::Call+0xb
13 00007fff`9f5e2fc6     : 000001e5`f8808e98 000001e5`f8808e98 00000000`00000000 000000db`2357f1b8 : coreclr!RunMainInternal+0x11c
14 00007fff`9f5e32fb     : 000001e5`f44af4d0 000001e5`00000000 000001e5`f44af4d0 00000000`00000000 : coreclr!RunMain+0xd2
15 00007fff`9f539141     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000130 : coreclr!Assembly::ExecuteMainMethod+0x1bf
16 00007fff`9f64e8b8     : 00000000`00000001 000000db`2357f301 000000db`2357f3e0 00007fff`9fa223ea : coreclr!CorHost2::ExecuteAssembly+0x281
17 00007fff`9fa42b76     : 000001e5`f4481250 000001e5`f4481030 00000000`00000000 000001e5`f4481030 : coreclr!coreclr_execute_assembly+0xd8
18 (Inline Function)     : --------`-------- --------`-------- --------`-------- --------`-------- : hostpolicy!coreclr_t::execute_assembly+0x2a
19 00007fff`9fa42e5c     : 000001e5`f446de98 000000db`2357f609 00007fff`9fa7ca10 000001e5`f446de98 : hostpolicy!run_app_for_context+0x596
20 00007fff`9fa4379a     : 00000000`00000000 000001e5`f446de90 000001e5`f446de90 00000000`00000000 : hostpolicy!run_app+0x3c
21 00007fff`9fa9b5c9     : 000001e5`f447e468 000001e5`f447e350 00000000`00000000 000000db`2357f709 : hostpolicy!corehost_main+0x15a
22 00007fff`9fa9e066     : 000001e5`f447ddc0 000000db`2357fa90 00000000`00000000 00000000`00000000 : hostfxr!execute_app+0x2e9
23 00007fff`9faa02ec     : 00007fff`9fad25f8 000001e5`f447c530 000000db`2357f9d0 000000db`2357f980 : hostfxr!`anonymous namespace'::read_config_and_execute+0xa6
24 00007fff`9fa9e644     : 000000db`2357fa90 000000db`2357fab0 000000db`2357fa01 000001e5`f447c9a0 : hostfxr!fx_muxer_t::handle_exec_host_command+0x16c
25 00007fff`9fa985a0     : 000000db`2357fab0 000001e5`f447fdf0 00000000`00000001 000001e5`f4460000 : hostfxr!fx_muxer_t::execute+0x494
26 *** WARNING: Unable to verify checksum forExampleCore_8_01.exe27 00007ff7`ae24f998     : 00007ff8`7f40f4e8 00007fff`9fa99b10 000000db`2357fc50 000001e5`f447bf30 : hostfxr!hostfxr_main_startupinfo+0xa0
28 00007ff7`ae24fda6     : 00007ff7`ae25b6c0 00000000`00000007 000001e5`f446de90 00000000`0000005e : ExampleCore_8_01_exe!exe_start+0x878
29 00007ff7`ae2512e8     : 00000000`00000000 00000000`00000000 000001e5`f446de90 00000000`00000000 : ExampleCore_8_01_exe!wmain+0x146
30 (Inline Function)     : --------`-------- --------`-------- --------`-------- --------`-------- : ExampleCore_8_01_exe!invoke_main+0x22
31 00007ff8`801d7344     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ExampleCore_8_01_exe!__scrt_common_main_seh+0x10c
32 00007ff8`816026b1     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : kernel32!BaseThreadInitThunk+0x14
33 00000000`00000000     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x21

我们看堆栈知道最后抛出异常,红色标注的地址就是异常对象的具体地址。我们可以使用【!do 000001e5`f8809ce8】命令或者【!DumpObj 000001e5`f8809ce8】,这个命令输入的内容太多。

1 0:000> !do000001e5`f8809ce82 Name:        System.ArgumentNullException(空引用异常,就是我们抛出的)3 MethodTable: 00007fff3fc22cb04 EEClass:     00007fff3fbef2105 Tracked Type: false
6 Size:        136(0x88) bytes7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll8 Fields:9 MT    Field   Offset                 Type VT     Attr            Value Name10 00007fff3fc77688  4000264        8 ...ection.MethodBase  0 instance 0000000000000000_exceptionMethod11 00007fff3fb0ec08  4000265       10        System.String  0instance 000001e5f880d328 _message12 00007fff3fba3060  4000266       18 ...tions.IDictionary  0 instance 0000000000000000_data13 00007fff3fba0820  4000267       20     System.Exception  0 instance 0000000000000000_innerException14 00007fff3fb0ec08  4000268       28        System.String  0 instance 0000000000000000_helpURL15 00007fff3fc09d98  4000269       30        System.Byte[]  0instance 000001e5f880d4c8 _stackTrace16 00007fff3fc09d98  400026a       38        System.Byte[]  0 instance 0000000000000000_watsonBuckets17 00007fff3fb0ec08  400026b       40        System.String  0 instance 0000000000000000_stackTraceString18 00007fff3fb0ec08  400026c       48        System.String  0 instance 0000000000000000_remoteStackTraceString19 00007fff3fa5c4d8  400026d       50      System.Object[]  0 instance 0000000000000000_dynamicMethods20 00007fff3fb0ec08  400026e       58        System.String  0 instance 0000000000000000_source21 00007fff3fb08b78  400026f       60       System.UIntPtr  1instance 00007FFF3FB21AFA _ipForWatsonBuckets22 00007fff3fb070a0  4000270       68        System.IntPtr  1 instance 0000000000000000_xptrs23 00007fff3fa91188  4000271       70         System.Int32  1 instance       -532462766_xcode24 00007fff3fa91188  4000272       74         System.Int32  1 instance      -2147467261_HResult25 00007fff3fb0ec08  4000383       78        System.String  0 instance 000002268aad0a38 _paramName

我们也可以使用【!pe】命令,这个命令输出就很简洁了。

1 0:000> !pe 000001e5`f8809ce82 WARNING: SOS needs to be upgraded for thisversion of the runtime. Some commands may not work correctly.3 For more information see https://go.microsoft.com/fwlink/?linkid=2135652
4 
5 Exception object: 000001e5f8809ce86 Exception type:   System.ArgumentNullException7 Message:          Value cannot be null.8 InnerException:   <none>
9 StackTrace (generated):10 SP               IP               Function11     000000DB2357EA80 00007FFF3FB21AFB ExampleCore_8_01!ExampleCore_8_01.Program.ProcessData(System.String)+0x8b
12     000000DB2357EAE0 00007FFF3FB21A46 ExampleCore_8_01!ExampleCore_8_01.Program.Run()+0x46
13     000000DB2357EB30 00007FFF3FB21988 ExampleCore_8_01!ExampleCore_8_01.Program.Main(System.String[])+0x58
14 
15 StackTraceString: <none>
16 HResult: 80004003

在这里看的就很清楚了,异常类型,错误码和调用堆栈都是一目了然。


2)、Windbg Preview 调试

我们打开【Windbg Preview】调试器,依次点击【文件】----【Open dump file】,在右侧选择我们的 Dump 文件,点击【open】按钮,就打开了调试器。
由于我们调试的是 Dump 文件,所以调试刚开始的输出也是不一样的。

1 ************* Preparing the environment for Debugger Extensions Gallery repositories **************
2 ExtensionRepository : Implicit3    UseExperimentalFeatureForNugetShare : false
4    AllowNugetExeUpdate : false
5    NonInteractiveNuget : true
6    AllowNugetMSCredentialProviderInstall : false
7    AllowParallelInitializationOfLocalRepositories : true
8 
9    EnableRedirectToV8JsProvider : false
10 
11    --Configuring repositories12       ----> Repository : LocalInstalled, Enabled: true
13       ----> Repository : UserExtensions, Enabled: true
14 
15 >>>>>>>>>>>>> Preparing the environment for Debugger Extensions Gallery repositories completed, duration 0.000seconds16 
17 ************* Waiting for Debugger Extensions Gallery to Initialize **************
18 
19 >>>>>>>>>>>>> Waiting for Debugger Extensions Gallery to Initialize completed, duration 0.031seconds20    ----> Repository : UserExtensions, Enabled: true, Packages count: 0
21    ----> Repository : LocalInstalled, Enabled: true, Packages count: 41
22 
23 Microsoft (R) Windows Debugger Version 10.0.27553.1004AMD6424 Copyright (c) Microsoft Corporation. All rights reserved.25 
26 
27 Loading Dump File [F:\Test\TestDump\08dumpfile.dmp]28 User Mini Dump File with Full Memory: Only application data isavailable29 
30 
31 ************* Path validation summary **************
32 Response                         Time (ms)     Location33 Deferred                                       srv*
34 Symbol search path is: srv*
35 Executable search path is:36 Windows 10 Version 19045 MP (4procs) Free x6437 Product: WinNt, suite: SingleUserTS38 Edition build lab: 19041.1.amd64fre.vb_release.191206-1406
39 Debug session time: Wed Jun 26 13:41:21.000 2024 (UTC + 8:00)40 System Uptime: 0 days 3:36:45.501
41 Process Uptime: 0 days 0:16:15.000
42 .......................................................43 This dump file has an exception of interest stored in it.44 The stored exception information can be accessed via .ecxr.45 (43e0.3758): CLR exception - code e0434352 (first/second chance not available)46 For analysis of this file, run !analyze -v47 KERNELBASE!RaiseException+0x69:48 00007ff8`7ecbcf19 0f1f440000      nop     dword ptr [rax+rax]

红色标注的都说明了加载了我们的 Dump 文件,并且由于发生了一个 CLR 异常生成的转储文件。
我们可以使用【!clrstack】命令看看托管调用堆栈,这里有用的信息不多。

1 0:000> !clrstack2 OS Thread Id: 0x3758 (0)3 Child SP               IP Call Site4 000000DB2357E988 00007ff87ecbcf19 [HelperMethodFrame: 000000db2357e988]5 000000DB2357EA80 00007fff3fb21afb ExampleCore_8_01.Program.ProcessData(System.String) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_8_01\Program.cs @ 22]6 000000DB2357EAE0 00007fff3fb21a46 ExampleCore_8_01.Program.Run() [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_8_01\Program.cs @ 15]7 000000DB2357EB30 00007fff3fb21988 ExampleCore_8_01.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_8_01\Program.cs @ 8]

我们可以使用【kb】命令查看非托管调用栈,包括参数。

1 0:000>kb2 *** WARNING: Unable to verify checksum forExampleCore_8_01.exe3 # RetAddr               : Args to Child                                                           : Call Site4 00 00007fff`9f5eb3a3     : 000001e5`f8809ce8 000000db`2357e7d0 00007fff`3fbe1080 00007fff`3fbd5e00 : KERNELBASE!RaiseException+0x69
5 01 00007fff`9f5ead49     : 00000000`00000004 00000000`00000001 000000db`2357ec18 000000db`2357edb8 : coreclr!RaiseTheExceptionInternalOnly+0x26b [D:\a\_work\1\s\src\coreclr\vm\excep.cpp @ 2795]6 02 00007fff`3fb21afb     : 000001e5`f8809ce8 00000226`8aad0a38 000001e5`f44cb1e8 00000226`8aae0000 : coreclr!IL_Throw+0xb9 [D:\a\_work\1\s\src\coreclr\vm\jithelpers.cpp @ 4247]7 03 00007fff`3fb21a46     : 000001e5`f8809630 00000000`00000000 000001e5`f44af4d0 000001e5`f8809c40 : ExampleCore_8_01!ExampleCore_8_01.Program.ProcessData+0x8b [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_8_01\Program.cs @ 24]8 04 00007fff`3fb21988     : 000001e5`f8809630 000001e5`f8809648 000000db`2357f1b8 000000db`2357eda9 : ExampleCore_8_01!ExampleCore_8_01.Program.Run+0x46 [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_8_01\Program.cs @ 15]9 05 00007fff`9f68b8d3     : 000001e5`f8808e98 000000db`2357f1b8 000000db`2357f1b8 000000db`2357eda9 : ExampleCore_8_01!ExampleCore_8_01.Program.Main+0x58 [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_8_01\Program.cs @ 8]10 06 00007fff`9f5c0b19     : 00000000`00000000 00000000`00000130 000000db`2357edb8 00007fff`9f540232 : coreclr!CallDescrWorkerInternal+0x83 [D:\a\_work\1\s\src\coreclr\vm\amd64\CallDescrWorkerAMD64.asm @ 100]11 07 (Inline Function)     : --------`-------- --------`-------- --------`-------- --------`-------- : coreclr!CallDescrWorkerWithHandler+0x56 [D:\a\_work\1\s\src\coreclr\vm\callhelpers.cpp @ 67]12 08 00007fff`9f5bd730     : 000000db`2357ee38 00000000`00000000 00000000`00000048 00007fff`9f62d046 : coreclr!MethodDescCallSite::CallTargetWorker+0x2a1 [D:\a\_work\1\s\src\coreclr\vm\callhelpers.cpp @ 570]13 09 (Inline Function)     : --------`-------- --------`-------- --------`-------- --------`-------- : coreclr!MethodDescCallSite::Call+0xb [D:\a\_work\1\s\src\coreclr\vm\callhelpers.h @ 458]14 0a 00007fff`9f5e2fc6     : 000001e5`f8808e98 000001e5`f8808e98 00000000`00000000 000000db`2357f1b8 : coreclr!RunMainInternal+0x11c [D:\a\_work\1\s\src\coreclr\vm\assembly.cpp @ 1304]15 0b 00007fff`9f5e32fb     : 000001e5`f44af4d0 000001e5`00000000 000001e5`f44af4d0 00000000`00000000 : coreclr!RunMain+0xd2 [D:\a\_work\1\s\src\coreclr\vm\assembly.cpp @ 1375]16 0c 00007fff`9f539141     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000130 : coreclr!Assembly::ExecuteMainMethod+0x1bf [D:\a\_work\1\s\src\coreclr\vm\assembly.cpp @ 1504]17 0d 00007fff`9f64e8b8     : 00000000`00000001 000000db`2357f301 000000db`2357f3e0 00007fff`9fa223ea : coreclr!CorHost2::ExecuteAssembly+0x281 [D:\a\_work\1\s\src\coreclr\vm\corhost.cpp @ 349]18 0e 00007fff`9fa42b76     : 000001e5`f4481250 000001e5`f4481030 00000000`00000000 000001e5`f4481030 : coreclr!coreclr_execute_assembly+0xd8 [D:\a\_work\1\s\src\coreclr\dlls\mscoree\exports.cpp @ 504]19 0f (Inline Function)     : --------`-------- --------`-------- --------`-------- --------`-------- : hostpolicy!coreclr_t::execute_assembly+0x2a [D:\a\_work\1\s\src\native\corehost\hostpolicy\coreclr.cpp @ 109]20 10 00007fff`9fa42e5c     : 000001e5`f446de98 000000db`2357f609 00007fff`9fa7ca10 000001e5`f446de98 : hostpolicy!run_app_for_context+0x596 [D:\a\_work\1\s\src\native\corehost\hostpolicy\hostpolicy.cpp @ 256]21 11 00007fff`9fa4379a     : 00000000`00000000 000001e5`f446de90 000001e5`f446de90 00000000`00000000 : hostpolicy!run_app+0x3c [D:\a\_work\1\s\src\native\corehost\hostpolicy\hostpolicy.cpp @ 285]22 12 00007fff`9fa9b5c9     : 000001e5`f447e468 000001e5`f447e350 00000000`00000000 000000db`2357f709 : hostpolicy!corehost_main+0x15a [D:\a\_work\1\s\src\native\corehost\hostpolicy\hostpolicy.cpp @ 426]23 13 00007fff`9fa9e066     : 000001e5`f447ddc0 000000db`2357fa90 00000000`00000000 00000000`00000000 : hostfxr!execute_app+0x2e9 [D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp @ 145]24 14 00007fff`9faa02ec     : 00007fff`9fad25f8 000001e5`f447c530 000000db`2357f9d0 000000db`2357f980 : hostfxr!`anonymous namespace'::read_config_and_execute+0xa6 [D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp @ 532]
25 15 00007fff`9fa9e644     : 000000db`2357fa90 000000db`2357fab0 000000db`2357fa01 000001e5`f447c9a0 : hostfxr!fx_muxer_t::handle_exec_host_command+0x16c [D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp @ 1007]26 16 00007fff`9fa985a0     : 000000db`2357fab0 000001e5`f447fdf0 00000000`00000001 000001e5`f4460000 : hostfxr!fx_muxer_t::execute+0x494 [D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp @ 578]27 17 00007ff7`ae24f998     : 00007ff8`7f40f4e8 00007fff`9fa99b10 000000db`2357fc50 000001e5`f447bf30 : hostfxr!hostfxr_main_startupinfo+0xa0 [D:\a\_work\1\s\src\native\corehost\fxr\hostfxr.cpp @ 62]28 18 00007ff7`ae24fda6     : 00007ff7`ae25b6c0 00000000`00000007 000001e5`f446de90 00000000`0000005e : ExampleCore_8_01_exe!exe_start+0x878 [D:\a\_work\1\s\src\native\corehost\corehost.cpp @ 240]29 19 00007ff7`ae2512e8     : 00000000`00000000 00000000`00000000 000001e5`f446de90 00000000`00000000 : ExampleCore_8_01_exe!wmain+0x146 [D:\a\_work\1\s\src\native\corehost\corehost.cpp @ 311]30 1a (Inline Function)     : --------`-------- --------`-------- --------`-------- --------`-------- : ExampleCore_8_01_exe!invoke_main+0x22 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 90]31 1b 00007ff8`801d7344     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ExampleCore_8_01_exe!__scrt_common_main_seh+0x10c [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288]32 1c 00007ff8`816026b1     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : kernel32!BaseThreadInitThunk+0x14
33 1d 00000000`00000000     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x21

000001e5`f8809ce8
这个参数就是异常实例的地址,我们可以使用【!do
000001e5`f8809ce8
】命令查看,但是使用【!pe
000001e5`f8809ce8
】命令会更好。

1 0:000> !pe 000001e5`f8809ce82 Exception object: 000001e5f8809ce83 Exception type:   System.ArgumentNullException4 Message:          Value cannot be null.5 InnerException:   <none>
6 StackTrace (generated):7 SP               IP               Function8     000000DB2357EA80 00007FFF3FB21AFB ExampleCore_8_01!ExampleCore_8_01.Program.ProcessData(System.String)+0x8b
9     000000DB2357EAE0 00007FFF3FB21A46 ExampleCore_8_01!ExampleCore_8_01.Program.Run()+0x46
10     000000DB2357EB30 00007FFF3FB21988 ExampleCore_8_01!ExampleCore_8_01.Program.Main(System.String[])+0x58
11 
12 StackTraceString: <none>
13 HResult: 80004003

这里的信息就很清楚了,异常类型,调用堆栈一目了然。


4.2、Windows 错误报告

这一节的内容挺不好写的,因为这里面的内容发生了很大的变化,如果照着原文写,很多内容会过时的,所以,这节就简写了,我会给出微软最先有关文章的连接,大家可以去学习。
Windows 错误报告(Windows Error Reporting,WER)是一种聚合故障数据的服务,使得 Microsoft 和独立软件供应商(Independent Software Vendor,ISV)可以很容易的访问与他们程序相关的故障数据。
我们先上一个图,来说明一下 WER 服务的操作流程。

假设在世界的某个地方,有一台计算机正在运行由 ADND 企业开发的一个程序(就是图中的 X 进程)。假设这个程序崩溃了,并且用户看到了 Dr.Watson 界面并询问是否将错误报告发送给 Microsoft。用户选择了发送,并且错误报告将通过安全通道(HTTPS)发送给 WER 服务。然后,WER 将收到错误报告进行分门别类并保存。要使用这些错误报告,来之 ADND 企业的用户需要查询 WER 服务,找出与其程序相关的崩溃并且获得报告的错误信息。如果 ADND 得到了这些错误报告,那么就可以修正这个问题,并且提供一个回应,这样下一次当用户遇到了想通的崩溃情况时,Dr. Watson 将给出相应的回应。这个回应可以是一个补丁或者是其他一些帮助信息。
WER 服务是一种功能非常强大的机制,提供了对错误报告的聚合功能,ISV 可以查询这些信息来改进程序。此外,ISV 可以提供问题的回应,并且这个回应会集成到 WER 反馈循环中,从而可以使用户很容易得到回应。
要想使用 WER 服务,必须向 Windows 错误报告服务注册,剩下的内容就是如何注册账号和使用了,内容太老就不多说了。

在文章的最后,如果大家想了解 WER 最新的使用方法,我把 Microsoft 官网文章地址贴出来,大家可以自行学习。
地址如下:
https://learn.microsoft.com/zh-cn/windows/win32/wer/windows-error-reporting

五、总结
这篇文章的终于写完了,这篇文章的内容相对来说,不是很多。写完一篇,就说明进步了一点点。Net 高级调试这条路,也刚刚起步,还有很多要学的地方。皇天不负有心人,努力,不辜负自己,我相信付出就有回报,再者说,学习的过程,有时候,虽然很痛苦,但是,学有所成,学有所懂,这个开心的感觉还是不可言喻的。不忘初心,继续努力。做自己喜欢做的,开心就好。

作者:vivo 互联网存储研发团队 - Shang Yongxing

本文介绍了当前DTS应用中,MySQL数据同步使用到的数据一致性校验工具,并对它的实现思路进行分享。

一、背景

在 MySQL 的使用过程中,经常会因为如集群拆分、数据传输、数据聚合等原因产生流动和数据复制。而在通常的数据复制过程中,因为涉及到目标的写入不可控、服务应用的未知问题、人为导致的异常缺陷等,很难保证复制产生的数据与源完全一致。除了通过完善流程与服务应用的能力和可靠性来保障数据一致性外,也需要提供快速有效的数据校验机制,便于发现存在异常的数据位置,服务于后续可能的自动重试或人工修订。

而具体到我们目前使用的数据传输服务DTS(MySQL部分),需要考虑的点:

  • 端对端从源集群到目标集群的外部数据校验

  • 内部数据校验机制,确保同步的数据正确可靠

二、选型参考

数据一致性校验,即对DTS的数据同步任务在目标产生(复制)的表数据,与在源库的原始数据进行对比,并给出对比结果。若存在不一致的情况则给出具体不一致的数据块,方便用户快速对不一致数据进行处理。它的基本原则是作为独立一个环节,既不能影响同步本身,也不能影响业务数据库的正常使用。

为了达成数据一致性的校验,需要具备以下的能力:

  • 校验的范围应当包括库表对象的结构、实际行数据已经其它被任务定义包含在内的内容(索引、视图、存储过程等)。

  • 校验应当在保证较小地侵入影响数据库的同时,尽快完成涉及数据的对比检查。

  • 校验应当具备精确定位不一致数据块的位置的能力,用于支持后续进行的数据修订。

三、端对端的数据一致性校验

3.1 现有问题

在数据传输的场景中,相关的数据大概率分散在无关的不同实例上,这种情况下想要对两端的数据进行对比分析,比较包括结构、索引、列数据等维度,当然可以通过最直接的逐行逐列地遍历各个表方式,这种方式最直观且可以最精确地对比相关的数据,但显然如果数据总量或数据列的规模较大时,这种逐行对比的方式会存在下列问题:

  • 执行耗时长

  • 结果时效性差,增量场景几乎无法使用

一般来说如果无法接受“全量”性质的扫描带来的时间开销,可以将“全量”转换为“部分”的验证,本质上一些数据同步场景后执行的点检就是数据的部分校验,这种方式完成速度快,可用于关键数据的快速验证,但这种形式也存在明显的问题:

  • 数据集覆盖度难以平衡

  • 如果确实存在一些不一致的情况,可能会被遗漏

针对这些问题,也许可以采取一些额外的校验保障,例如数据集大小(行数等)的校验,一定程度上可以降低数据不完整的错判风险。

那么回到我们的实际使用场景中,为了解决数据可靠性的问题,我们调研了几种比较主流的MySQL数据校验方案:

图片

pt本身更常被运维用于检查集群内主从的表数据是否一致,这显然与数据传输的场景存在较大的出入,不符合我们的实际需求。

px在实现上更满足数据传输场景的需求,同时包括上述两者在内,一些公用云的MySQL类型数据库的数据一致性校验也是采用类似部分采用数据进行比对的形式:

  • 表的数据分块

- 不分支持动态分块大小来调整负载

  • 分块级的数据特征计算→简化对比规模

这个思路在数据遍历的完整性和效率方面均做了一定的取舍,相当于是一种中间状态。

而为了达成满足一些定制场景以及提高使用效率的目的,我们最终是采用了px-table-checksum的实现思路来完成的数据校验实现,接下来以一个具体的表结构来介绍进行数据对比的思路。

sysbench在MySQL的压测场景产生的一种表结构如下:

图片

前文我们已经提到虽然直接使用全表逐行对比会存在时间消耗较大的问题,但如果业务属性上源和目标的表发生变化的频率很低(定时更新类),这种校验也是可以发挥作用的:

SELECT id, k, c, pad FROM sbtest1 limit n;

基于这样简易的抽取逻辑进行逐行的比对是可以做到完全校验的,代价就是时间上的开销非常大。

3.2 实现思路

而我们将要采取的分块获取数据的方式则是在这一基础上进行了优化,我们逐步介绍逻辑,首先这里假定使用的分块大小为10行–chunk_size=10

1.数据分块

mysql> SELECT * FROM sbtest1 FORCE INDEX(`PRIMARY`) WHERE ((`id`>= 1) AND  ((`id`) <= 10));
 
+----+---------+-------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------------+
| id | k       | c                                                                                                                       | pad                                                         |
+----+---------+-------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------------+
|  1 | 3230682 | 68487932199-96439406143-93774651418-41631865787-96406072701-20604855487-25459966574-28203206787-41238978918-19503783441 | 22195207048-70116052123-74140395089-76317954521-98694025897 |
|  2 |  556124 | 13241531885-45658403807-79170748828-69419634012-13605813761-77983377181-01582588137-21344716829-87370944992-02457486289 | 28733802923-10548894641-11867531929-71265603657-36546888392 |
|  3 | 2757236 | 16516882386-05118562259-88939752278-62800788230-55205008755-06868633780-74894238647-69655573455-70526404237-73358617781 | 73198647949-50059256035-48039302709-77824424754-93913530645 |
|  4 | 3080419 | 88936868384-35413314949-47572298747-62301572168-04725458949-84024294746-95505588496-92367527122-22018143923-07447340456 | 35540797267-28848817162-69859656941-29402637497-25804052865 |
|  5 | 2755665 | 38000028170-70584813101-64973736504-76149487237-52945047102-11275974719-79041047383-53171259040-17278926045-71359842623 | 24429914423-05032864825-55698585282-50062977513-27378309065 |
|  6 | 1689856 | 90918476202-02089391467-64272595615-72064149272-80467152282-18848936545-61767310237-38205570677-59195835610-06087350040 | 37783104634-08719671341-44662007841-24831185436-08450447859 |
|  7 |  367648 | 69873895168-42508713642-77344499431-18964268934-99713628807-43846750254-87716358839-40367934805-98231362293-37861509854 | 65003009667-83421336486-43798350655-86517975104-79705317753 |
|  8 | 4069722 | 03426487304-27156530652-16106764306-84175870374-36434920674-38029783924-53173822921-96186178437-58319716571-95077711704 | 31784578367-14387657451-27946335198-02419089416-67782425795 |
|  9 | 4608666 | 81689156752-44921640552-35987563480-16691191991-27936686268-18588338593-16235034269-90308874838-52095870672-98075954786 | 03144707666-87793208474-21823431822-18751222077-39980824756 |
| 10 | 2975029 | 03392914016-90098596959-72565142257-56206208928-54469213163-80095083408-91183949560-45926629535-07758798231-14358688386 | 44959141897-52907315042-08586003451-12076203782-52848887604 |
+----+---------+-------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------------+

2.按列聚合

mysql> SELECT CONCAT_WS('#', `id`, `k`, CRC32(`c`), CRC32(`pad`), CONCAT(ISNULL(`pad`))) AS CRC FROM `sbtest1` FORCE INDEX(`PRIMARY`) WHERE ((`id`>= 1) AND  ((`id`) <= 10));
 
+-----------------------------------+
| CRC                               |
+-----------------------------------+
| 1#3230682#4041434652#3764881225#0 |
| 2#556124#847118261#777674597#0    |
| 3#2757236#2890838841#3354864368#0 |
| 4#3080419#2053219065#3018733667#0 |
| 5#2755665#4230533455#266494007#0  |
| 6#1689856#2940387143#1608825719#0 |
| 7#367648#2894429300#3186127078#0  |
| 8#4069722#1825802258#3718534773#0 |
| 9#4608666#1487055134#1908388285#0 |
| 10#2975029#1272074468#264227369#0 |
+-----------------------------------+

3. 按行聚合

在前一步的基础上,可以再计算一次当前聚合列的CRC32值使长度减少(因为按列聚合时使用的group_concat可能会存在长度的限制,这也是需要关注的问题)

mysql> SELECT CRC32(CONCAT_WS('#', `id`, `k`, CRC32(`c`), CRC32(`pad`), CONCAT(ISNULL(`pad`)))) AS CRC FROM `sbtest1` FORCE INDEX(`PRIMARY`) WHERE ((`id`>= 1) AND  ((`id`) <= 10));
  
+------------+
| CRC        |
+------------+
|  501470676 |
| 3723711314 |
| 4091031521 |
|  571991173 |
| 3184804606 |
| 1525903855 |
| 3331492255 |
|  105586567 |
| 3803559186 |
| 3193672787 |
+------------+  
 
mysql> SELECT GROUP_CONCAT(CRC32(CONCAT_WS('#', `id`, `k`, CRC32(`c`), CRC32(`pad`), CONCAT(ISNULL(`pad`))))) AS CRC FROM `sbtest1` FORCE INDEX(`PRIMARY`) WHERE ((`id`>= 1) AND  ((`id`) <= 10));
 
+------------------------------------------------------------------------------------------------------------+
| CRC                                                                                                        |
+------------------------------------------------------------------------------------------------------------+
| 501470676,3723711314,4091031521,571991173,3184804606,1525903855,3331492255,105586567,3803559186,3193672787 |
+------------------------------------------------------------------------------------------------------------+

4.特征计算

到这一部分为止,我们可以将这种类型的CRC值作为这10行4列数据块的一种“特征”,用它来代表这部分数据,可以进一步进行压缩来提高比对效率,可选md5或继续CRC32等计算方式。

mysql> SELECT CRC32(GROUP_CONCAT(CRC32(CONCAT_WS('#', `id`, `k`, CRC32(`c`), CRC32(`pad`), CONCAT(ISNULL(`pad`)))))) AS CRC FROM `sbtest1` FORCE INDEX(`PRIMARY`) WHERE ((`id`>= 1) AND  ((`id`) <= 10));
+------------+
| CRC        |
+------------+
| 3337375759 |
+------------+
 
mysql> SELECT md5(GROUP_CONCAT(CRC32(CONCAT_WS('#', `id`, `k`, CRC32(`c`), CRC32(`pad`), CONCAT(ISNULL(`pad`)))))) AS md5 FROM `sbtest1` FORCE INDEX(`PRIMARY`) WHERE ((`id`>= 1) AND  ((`id`) <= 10));
+----------------------------------+
| md5                              |
+----------------------------------+
| 6b2fb38d05fee0733382f2e4d6dc2f91 |
+----------------------------------+

实际使用中使用更大的数据块来进行映射,能加快校验的速度:

  • 数据块越大,特征的精度越低,但匹配校验的速度越快,对源和目标的负载越大。

  • 数据快越小,特征的精度越高,但匹配校验的速度越慢,对源和目标的负载越小。

相对应的,以上是在源计算对应块的特征,在目标以同样的形式计算可以得到一个“类似”的结果,通过对它进行比对,可以判断两块数据的特征是否一致。  但这同样也有一些
问题

  • 概率上存在特征值相同但数据存在差异的情况,无法避免。

  • 比对的块依赖主键ID,不允许在目标存在主键覆盖的情况(源的数据因主键冲突被跳过)。

若存在数据不一致(块之间的CRC32值不一致),此时可以基于当前chunk的上下边界(upper/lower bound)进行进一步切分,通过精确的数据对比来定位到不一致的行。

图片

数据最终一致性

前文中的例子更偏向于一个单纯的全量数据抽取场景,如果目标的数据存在一定程度上的变化时,如何对应处理呢?

  • 差异块的重复校验

我们需要解决两个问题:尽可能在机制上确保数据不会出错,若存在异常或无法处理的情况应当以中断同步为优先选择;同时需要在端对端数据校验的基础上覆盖增量的数据同步场景。

数据校验任务并不是持续运行的场景,它应当是在低峰期、同步延迟1秒内或在业务要求的情况下发生的行为,那么基于数据同步的最终一致性特征,当增量场景下校验出某些块存在差异时:

例如:

两侧的chunk[1001-2000]存在差异,那么需要按照精细拆分地形式定位到具体的差异行, 一般基于binlog的延迟在0秒(1秒内)时,行的差异数量是有限的,可以对其进行全部的记录或抽样记录。

在一定的时间间隔后重新校验上一次记录中的差异行,判断是否最终达成了一致;

可能存在特殊的场景,只频繁更新某一行,导致校验一直无法判断两端一致。

图片

四、总结与思考

经过一定时间的线上实际应用,这种方案确实可以解决绝大部分(99%以上)的校验需求,不论是纯粹的全量还是涉及到增量的场景,都可以完成定点形式的数据一致性校验能力,但总的来说,它也存在可以优化改进的点:

  1. 本质上,块的特征计算值(MD5或CRC32)一致,还是存在内容实际不一致的可能性,虽然这部分概率很低,需要在当前基础上寻找更精确可靠的采样计算方式。

  2. 目前提供的一致性校验方案,只能支持同构型的数据库间,例如本文介绍的mysql->mysql(pg,tidb等),DTS支持的其它数据场景(redis->redis/kv)也是类似的情况,对于异构数据(例如订阅),暂时就没有比较好的方案可以做端对端的校验,需要使用方抽检部分或核心数据。