同样是困难 ,同样是问题 ,同样是不幸 ,对于有些人来说是苦难 ,而对于有些人来说却是磨难。
苦难 与磨难区别在于,是否是你主观意志选择的。 苦难是无差别的打击。 磨难是你努力时候的必经之路。
尼采说:那些杀不死你的,终将使你变得更强大
好了,如果你看到这,说明你也是一个时不时爱装小叉的人。咱们这不搞这么高大上的格调啊,咱就是纯纯地想装个×了。
好了,请系好安全带,下面就开始我的表演了。
老规矩,直接上菜
1.问题是个啥问题?
如下,是研发人员与脱敏组件维护者之间的一段对话。【关于脱敏组件的背景内容,可以参考Guava在日志脱敏场景下的奇遇 】
一般,当别人使用了我们的组件的时候出现了问题,一般先怀疑对方是不是使用姿势不对 。如果姿势正常,且用户也没啥特别的骚操作 ,那么就只能遇事不决,量子力学了【实际上,一般没啥怀疑对象的时候,就只能央求用户直接开gitlab权限了】。
我们知道,一般我们查问题,如果能复现问题,那么离解决问题也就不远了。 但如果复现不了问题,并且线上出现问题还不是必现的问题,那么你就知道了,这TM有点不妙啊
不过,从上面的对话中我们大概可以总结出以下几点
- 1.用户使用方式大概率没啥问题(不然就会都脱敏失败)
- 2.本地无法复现,线上固定一台机器脱敏正常,另外一台机器脱敏异常(还好不是随机正常,随机异常)
2.简单聊一下背景
为了防止敏感信息打印到日志中,我们用Java
语言开发了一个脱敏组件,这样用户在打印日志的时候就可以实现敏感信息的脱敏了。
对于用户来讲,大致分为两步来使用这个脱敏组件
1.配置脱敏规则
配置脱敏规则大概分为两种:
- 使用注解(在DTO
Field
上标注) - 使用配置文件(配置字段名称对应的字段脱敏规则)
这里我们重点介绍一下配置文件 的脱敏规则配置方式,因为本次问题用户是使用了配置文件的方式来实现脱敏的
如下,大致是在用户项目的classpath
下配置一个data_mask.properties
文件来配置脱敏规则
具体的脱敏规则如下:
拿第一行简单举例来说明脱敏规则:
字段名称为email,或者是email_addr的都是Email类型的字段,都需要按照Email的脱敏规则来脱敏
具体效果,我们接着看
2.调用脱敏工具类来脱敏
假如有这样一个DTO
Java
public class Person {
// id是非敏感字段,不需要脱敏
private Long id;
private String nickName;
private String mobile;
private String idCardAccount;
// setter and gettr ...
}
我们调用脱敏组件来脱敏日志
Java
log.info("个人信息:{}", DataMask.toJSONString(person));
打印出来的效果如下:
json
个人信息:{"id":1,"nickName":"**忌","mobile":"177******90","idCardAccount":"146*********49"}
3.深度分析问题
了解好了大概背后,我们继续分析问题。现在的情况是:一台机器脱敏都正常 ,一台机器脱敏都异常。
这说明跟用户的DTO是什么类型无关,大概率是因为一台机器加载到了脱敏规则,另一台机器没加载到脱敏规则。
还好我们在加载脱敏规则的时候打印了日志,那实际上就比较好办了,直接让用户去搜索应用启动时候加载规则的logger
对象对应的所有日志就可以了
如下:从日志中,确实证实了我们的猜想:一台机器加载到了脱敏规则,一台机器没有加载到脱敏规则
问:那为什么一台机器加载到了脱敏规则,一台机器没有加载到脱敏规则呢?
答:会不会是因为一台机器打包的时候丢失了classpath
下的data_mask.properties
资源文件,另外一台没有丢失呢?
再问:那如果一台丢失了classpath下的data_mask.properties
资源文件,那其他资源文件丢失了吗?
再答:显然classptah下的其他资源文件(如application.yml
)没有丢失,不然应用启动应该就会有问题
再问:会不会都丢失了,application.yml
实际上在SpringBoot启动的时候用的是外置的配置文件呢?
思考到这,你大概能明确一点:大概率可能不是因为缺失 了classpath
下的data_mask.properties
资源文件(因为打包后只有一个jar包文件,然后生成了同一个docker镜像,启动了不同的容器。当然也可以拉取到镜像,然后查看资源文件来验证)
到这,发现,data_mask.properties资源文件缺失这个猜想应该是不可能了
那还能是什么原因呢?猜是猜不到的,还得回到代码本身。
还真是,不看不知道,一看就是惊喜啊:
加载资源文件的
ClassLoader
用的是线程下上文ClassLoader
如果用的是线程下上文ClassLoader
来加载classpath下的资源文件的话,那么确实是有可能加载不到的,因为 线程下上文ClassLoader
是可以由用户自己指定的。
那这个就比较好验证了,全局搜索用户的代码仓库中调用java.lang.Thread#setContextClassLoader
的地方就可以了
结果如下:
到这,应该有掌声。
请给我3秒钟装个×,不,不够给我5秒钟吧,算了,还是3秒钟吧。
1
2
3
看到这,看来真的是铁粉了。给你也鼓个掌吧
好的,我们继续装×,不,不,不,我们继续分析问题
是因为用户把
SystemClassLoader
作为 线程下上文ClassLoader
,然后脱敏组件使用SystemClassLoader
去加载classpath
下的data_mask.properties
资源文件的,所以没有加载到data_mask.properties
资源文件。
至于为什么SystemClassLoader
加载不到SpringBoot
项目下classptah
下的资源文件,大家有兴趣可以去了解一下SpringBoot
的启动过程以及对应的SpringBoot
的类加载器LaunchedURLClassLoader
4.用原因去解释现象
问题出现时,我们发现两个奇怪的现象:
- 1.本地没有复现脱敏不成功的场景
- 2.线上一台机器脱敏都正常,另一台机器脱敏都异常
当我们找到了问题的根因 后,需要使用根因来解释现象来反向验证根因是否是真的根因。
1.本地没有复现脱敏不成功的场景
本地是直接调用脱敏组件脱敏的,如下:
java
DataMask.toJSONString(person)
这个时候的classloader
实际上还是SystemClassLoader
,但是却把本地项目的路径
作为classpath
传到了启动命令中: 大概如下:
shell
-classpath :/xxx/xxx/your-project/xxx-service/target/classes:
这样本地就可以加载到本地的资源文件了
2.线上一台机器脱敏都正常,另一台机器脱敏都异常
- 正常的那台机器使用的是
SpringBoot
的ClassLoader
(请求先到达的是线程不是CustomizeForkJoinWorkerThreadFactory
new 出来的线程) - 异常的那台机器使用的是
SystemClassLoader
(请求先到达的是线程就是CustomizeForkJoinWorkerThreadFactory
new 出来的线程)
这个其实可以通过日志中的thread
来验证
5.如何解决
既然是因为请求先后顺讯导致使用的ClassLoader
不一样,那么其实只需要在请求到达之前,确保使用一下脱敏组件 ,这样就能保证脱敏组件 加载资源的ClassLoader就是SpringBoot的ClassLoader 就可以了。
大概如下
java
@SpringBootApplication
public class Application {
public static void main(String[] args) {
DataMask.toJSONString(new Person())
SpringApplication.run(Application.class, args);
}
}
当然,在脱敏组件这一侧,也是可以不要使用线程上下文ClassLoader
来加载资源文件的,就固定使用加载DataMask
类的类加载器来加载就可以了
6.番外篇
上面的问题解决后一段时间,又来了个差不多的问题,大致对话内容如下:
本来以为问题非常简单,结果却啪啪打脸。以为一样,却有优点不一样。
然后就开始排查问题,发现现象几乎一模一样:昨天正常的那台机器,加载到了脱敏规则资源文件data_mask.properties
,今天异常的两台机器都没有加载到data_mask.properties
资源文件。
然后我让研发把加载不到脱敏规则日志的那个线程对象看了下,如下:使用的是ForkJoinPool
线程池
然后又看了ForkJoinPool
源代码(如果看代码,觉得累,直接看最后一张图片就可以了)
然后搜索了一下用户代码,发现用了并行流(默认会使用ForkJoinPool
线程池)
还以为有啥新奇的,原来还是老问题啊
到这里原因就比较明确了
由于用户使用了并行流,背后默认会使用
ForkJoinPool
线程池,由于默认情况下,ForkJoinPool
线程池默认会把SystemClassLoader
作为线程池上下文ClassLoader
。所以一旦用户先请求的是使用并行流的接口,那么就会先使用SystemClassLoader
来加载data_mask.properties
资源文件,进而加载不到。如果新请求的不是使用并行流的接口,那么就是用SpringBoot的ClassLoader加载data_mask.properties
资源文件,就可以加载到脱敏规则。
7.一点点闲聊
- 不要轻易使用线程上下文ClassLoader 去加载
classpath
下的资源文件 - 生产级别的配置文件最好外置化 ,不要使用
ClassLoader
去加载,因为不同ClassLoader
会加载出来的结果不一样 - 没有思路的时候,审视代码就是唯一的思路
- 作为组件负责人,当一时半会定位不出问题的时候,不要浪费研发人员的宝贵时间来辅助你定位问题,直接使用对方代码调试即可
- 所有难不倒你的问题,最终都会使你变得更加强大