做微服务重构或者依赖升级的时候,我最怕的事情不是代码编译不过,而是“上线没问题但行为悄悄变了”。接口还是那个接口,字段类型、边界条件、异常消息却可能被人为调整过,这种回归问题靠人眼很难发现。这次在Windows上部署了一次Diffy做差异测试,把新旧版本服务同时挂在一个代理后面,用真实请求对比响应差异,整个过程踩了一些坑,也把原理和参数理解得更透彻了。这篇文章就把完整流程写出来,从环境准备到启动脚本,再到常见问题的排查思路,希望对准备在Windows上做类似验证的同学有帮助。
Diffy是一个开源的差异测试工具,核心思路很直接:让同一个请求同时打到老版本和新版本,然后比较返回结果。它特别适合“行为是否发生变化”这类回归验证。整个部署依赖Java环境,Windows本身没有特殊限制,但官方脚本默认面向Linux和macOS,所以我们需要用命令直接启动,额外处理端口、防火墙这些细节。文章里我会尽量把每个步骤说清楚,也会解释为什么这样做。
1. 这个工具到底是干嘛的
1.1 先搞清楚“差异测试”解决什么问题
传统接口回归测试通常是提前准备一组用例,跑完后对比预期结果。但真实世界里请求参数是千变万化的,覆盖率永远有限,尤其是老服务重构、第三方SDK升级、数据库迁移这种场景,你很难确定“所有行为都保持一致”。差异测试的思路反过来了,它不预设预期值,而是把同一个请求同时发到新老两个版本上,直接比较两边返回是否一致。只要有差异,就说明行为已经变了,至于这个变化是不是有意的,由人再去判断。
Diffy就是专门做这件事的工具。它作为一个代理层,接收外部发来的请求,然后转给三个后端实例做对比。它不关心你的业务逻辑,只关心HTTP响应、异常、响应体内容是否对得上。这里面有一个很关键的设计:参与对比的“老版本”有两个实例,而不是一个。原因很简单,很多服务本身就有不确定性,比如接口返回里带时间戳、随机数、TraceID,如果拿一个老版本和一个新版本比,即使完全没改动,也会因为随机字段的不同而产生一堆误报。有两个老版本实例时,Diffy可以先看两个老版本之间是否不一致,如果它们自己都不一致,那这个差异就被判定为“噪音”,不在最终结果里当成问题。
1.2 三个实例的对比逻辑
在Diffy的配置里有三个角色:master.primary、master.secondary和candidate。前两个通常指向稳定生产版本的两个副本,candidate指向待验证的新版本。每次请求进来,Diffy会以一定策略选择目的版本组合,然后比较三份响应。
比较的时候最终会产生三类结果:一类是“相同”,指候选版本和两个基础版本都没有明显差异;一类是“差异”,指两个基础版本一致,但候选版本和它们不同,这就是需要人重点关注的回归点;还有一类是“错误”,一般指请求异常、超时、后端不可用等。整个过程中Diffy会把请求和响应都记录下来,方便之后在Web界面里逐个查看。
我举个实际例子帮助理解。假设你的老接口返回{"name":"demo","ts":1690000000},由于ts是个动态时间戳,两个老版本自身返回的ts就不一样。Diffy会把这部分差异过滤掉。但如果新版本把字段名从name改成了userName,两个老版本返回都是name,只有候选版本返回userName,Diffy就会把这个差异标记出来。这样你就不用浪费精力去处理那些因为随机因素造成的假告警,直接盯着有效差异看就行。
1.3 适合用在哪,不适合用在哪
就我的实际经验而言,Diffy最合适的场景有三类:
- 接口重构或内部实现替换,需要验证新旧行为完全一致。
- 第三方依赖、基础组件升级,担心暗改行为。
- 数据迁移或缓存引入后,确认逻辑没有出现偏差。
它不适合做性能压测,因为一次原始请求会被放大成三次后端请求。也不适合对强一致要求非常高的场景,因为Diffy本身是采样对比,不一定覆盖全部流量。它更偏向“开发阶段的验证工具”,而不是生产环境的实时监控。理解了这个边界,你后面部署配置参数时就不会有过高预期。
2. Windows部署前的准备
2.1 Windows上部署的麻烦点
Diffy官方发布包里的启动脚本是shell脚本,默认给Linux和macOS用的,Windows下直接运行会报错。这不是说Diffy不支持Windows,而是它默认没提供Windows的批处理脚本。解决办法也简单:不用官方脚本,用java -jar直接启动核心jar包,所有配置通过命令行参数传进去。你要具备的基础只有两点:会打开命令提示符或PowerShell,知道JAVA_HOME怎么配。
另一个麻烦点是Windows的防火墙。默认情况下,Java进程监听端口后,外部设备不一定能访问,你需要手动放行。如果只是本机浏览器访问,一般没问题,但如果要把Diffy地址给同事联调,就必须处理防火墙规则,不然会白白折腾很久。
2.2 装好JDK并确认版本
Diffy是基于JVM的Scala应用,对JDK版本比较敏感。我的实测经验是JDK 8最稳妥。它的底层依赖是在JDK 8时代构建的,用JDK 11或者17启动,有可能遇到模块系统限制或者反射访问报错。虽然在某些版本下高版本也能跑起来,但没必要在环境上给自己挖坑。
安装步骤不复杂,下载JDK 8安装包,完成安装后在系统环境变量里增加JAVA_HOME,指向安装目录,再把%JAVA_HOME%\bin加到Path变量里。打开新的命令提示符,执行java -version,看到类似java version "1.8.x"的输出,就说明环境没问题。
注意,如果电脑上已经有多个JDK,一定要确认当前命令窗口用的是哪个版本。可以在CMD里执行
where java,看看解析到的路径是不是你期望的JDK目录。Windows不会自动帮你切换,先后顺序和Path配置会直接影响运行结果。
2.3 拿到Diffy包:下载还是自己编译
Diffy官方在开源代码托管平台上有发布构建产物,如果你只是想在Windows上快速用起来,直接下载编译好的压缩包就好。解压后你会发现目录里有bin、lib等文件夹。bin里主要是给Linux和macOS的脚本,Windows下不用管它。lib里放着Diffy的server jar包,我们后面命令直接引用这个jar。
如果你想自己从源码构建,需要先装好sbt和Scala工具链,命令大致是sbt assembly。但在Windows上构建Scala项目偶尔会遇到编码、权限、依赖下载慢的问题,而且构建产物和官方下载的其实没有本质区别。所以我的建议是:不是想改源码的话,别自己编译,浪费时间。
下载后建议把压缩包解压到一个路径不包含中文和空格的目录,比如C:\diffy。避免后面命令里出现路径解析的问题,省心很多。
2.4 准备演示用的三个本地服务
在正式接业务服务之前,最好先准备三个本地HTTP服务,把Diffy跑通一次,理解界面上的概念后再接真实项目。最方便的方式是用Python内置的HTTP服务。Windows上如果已安装Python 3,可以省掉写代码的过程,直接创建目录和文件。
我建议建一个演示目录,结构大致如下:
C:\diffy-demo ├── master1 │ └── data.json ├── master2 │ └── data.json └── candidate └── data.json三个目录里的data.json故意写不同的内容。master1和master2作为老版本,内容应该基本一致,但可以包含一个不同的随机字段,用来演示噪音过滤;candidate作为新版本,可以故意改掉一个业务字段。这样启动之后,Diffy就能呈现出“有效差异”和“忽略差异”两种效果,非常直观。
3. 一步一步在Windows上启动Diffy
3.1 启动三个版本服务
打开三个独立的命令提示符窗口,分别执行三条命令。第一条:
python -m http.server 8882 --directory C:\diffy-demo\master1第二条:
python -m http.server 8883 --directory C:\diffy-demo\master2第三条:
python -m http.server 8881 --directory C:\diffy-demo\candidate这里我把master1放在8882端口,master2放在8883端口,candidate放在8881端口。先启动它们,是为了让Diffy启动时就能连到后端,避免后面排查时区分不清“Diffy没起来”和“后端没起来”。
如果Python版本比较老,没有--directory参数,可以先cd到对应目录,再执行python -m SimpleHTTPServer 8882。三种服务启动后,分别用浏览器访问一下这几个端口,能正常看到目录列表或data.json内容即可。
3.2 编写启动脚本
直接使用Diffy的jar包启动时,命令比较长,而且参数多。我建议在C:\diffy目录下新建一个start_diffy.bat文件,内容如下:
@echo off set JAVA_HOME=C:\Program Files\Java\jdk1.8.0_202 set PATH=%JAVA_HOME%\bin;%PATH% cd /d C:\diffy java -Xms256m -Xmx512m -jar C:\diffy\lib\diffy-server.jar ^ -candidate=localhost:8881 ^ -master.primary=localhost:8882 ^ -master.secondary=localhost:8883 ^ -service.protocol=http ^ -serviceName=diffy-demo ^ -proxy.port=8880 ^ -admin.port=8884命令里^是Windows CMD里的换行符号,注意它后面不能有空格,否则会被当成普通字符。如果你的JAVA_HOME设置已经配到了系统环境变量里,脚本前两行可以直接去掉。
有一点需要特别说明,
diffy-server.jar的具体文件名以你实际解压后的lib目录为准,可能带有版本号或其他前缀。写脚本前先看一眼,直接把文件名替换成真实名称。
3.3 第一次启动并观察日志
双击或者执行start_diffy.bat后,控制台会滚动输出日志。正常情况下会出现端口监听、配置加载之类的信息。由于Diffy本身日志量比较大,我第一次跑的时候看到一堆INFO日志还以为报错了,其实只要进程不退出、没有明显堆栈异常,就是正常的。
启动完成后可以测试两个端口:
- 8880:Diffy代理端口,也提供Web界面,浏览器访问
http://localhost:8880应该能看到界面。 - 8884:管理端口,访问
http://localhost:8884/admin之类的路径可以看到一些运行状态信息。
如果页面直接打不开,先用netstat -ano | findstr :8880确认端口是否被监听。找不到监听项,说明Java进程没起来,回看日志找具体错误。
3.4 用Web界面看差异
现在开始完整请求。打开浏览器访问http://localhost:8880/data.json,意思是让Diffy代理转发一次对data.json的请求。执行几次之后,回到Diffy首页,就能看到请求列表和差异状态。
在这个demo里,master1和master2的data.json内容可以故意保持大体一致,只让某个字段值不同;candidate里把另一个核心字段改成不同值。Diffy会先判定master1和master2之间的差异是“非确定性噪音”,然后只把candidate和它们的有效差异展示出来。你可以在界面上展开请求,看到三个版本的响应详情,也可以筛选只看“差异”类型。
这一步跑通,基本上就说明Diffy在Windows上已经部署成功。后面接真实服务时,只需要把candidate和master指向实际地址,路径按你自己的接口路径访问。
4. 关键参数和运行原理
4.1 参数逐个拆解
配置Diffy的一大堆参数里,最核心的是下面几个:
| 参数 | 作用 | 示例 |
|---|---|---|
| candidate | 待验证的新版本服务地址 | localhost:8881 |
| master.primary | 基准服务主实例地址 | localhost:8882 |
| master.secondary | 基准服务次实例地址 | localhost:8883 |
| service.protocol | 后端服务协议 | http |
| serviceName | Diffy内部的服务标记 | diffy-demo |
| proxy.port | Diffy对外代理和Web界面端口 | 8880 |
| admin.port | 管理端口,用于查看运行状态 | 8884 |
其中candidate、master.primary、master.secondary这三个是必须配置的。serviceName可以按项目名自己定义,作用是在Diffy内部区分不同服务,也会在日志和界面上展示。proxy.port是外界访问Diffy的唯一入口,所有测试请求都走这个端口。
有同学可能会问,为什么同时有proxy.port和admin.port,不能用一个端口?因为Diffy把“转发代理”和“管理信息”两个职责分开了。proxy端口给业务方使用,admin端口给运维和调试人员使用,互不干扰。Windows防火墙放行的时候,两个端口最好都放行。
4.2 一次请求在Diffy内部怎么走
当你的Windows上的服务A发起一个请求到http://localhost:8880/data.json时,Diffy并不是简单地把请求原样转发给三个后端。它会按照内部策略,把请求组合发送。常规组合是:一个请求发给candidate,一个请求发给master.primary,一个请求发给master.secondary。然后根据三个响应来判断分类。
如果master.primary和master.secondary返回的响应本身不一致,说明这两个老版本之间存在不确定因素,Diffy会把这个差异标记出来,但不会当作候选版本的问题。如果master两个实例返回一致,而candidate返回不一样,这才算真正值得关注的差异。
这种设计本质上是给“基准”这一侧也加了对照实验,用两份老版本来捕获老版本自身的不稳定输出。真实生产服务里经常有随机数、时间戳、动态请求ID,没有这一步,差异测试基本没法用,因为会满屏都是假阳性。
4.3 差异结果是怎么算出来的
Diffy比较的不只是HTTP状态码,还会看响应头、响应体、异常信息等。当然了,它默认不会对响应体的每个字节都做严格一致性校验,而会更偏重内容层面的差异。这个需要在实际使用中多积累经验。
比如响应体里有一段动态生成的Token,每次请求都会变。如果两个master返回的Token不一样,Diffy会认为这是噪音。但如果候选版本比老版本多了一个字段,两个master都没有该字段,Diffy就会给出一个有效差异。
从使用角度来说,只要看到界面里“Difference”类型的记录,就说明候选版本和基准版本的行为有出入。至于出入可不可以接受,需要你自己进入详情页,看完整响应体,甚至对比线上日志再做判断。Diffy只负责“找出不同”,不负责“判断对错”。
5. 常见问题排查实录
5.1 端口起不来怎么办
最典型的问题是8880端口被占用,或者候选版本端口没启动。排查分两步:
先用netstat -ano | findstr :8880看端口占用PID,再通过tasklist | findstr 老PID确认是哪个进程占用的。如果是Diffy自己占的,但页面打不开,可能是进程状态不对。如果被其他程序占了,可以改proxy.port,或者结束占用进程。
taskkill /PID 1234 /F这个命令在管理员权限下执行比较可靠。分不清占用进程是否重要时,建议不要乱杀进程,直接换一个端口更安全。
5.2 版本不兼容的报错
最常见的错误之一是UnsupportedClassVersionError,这通常是因为你用了JDK 17或更高版本,而Diffy的部分类库是旧版本构建的,字节码版本不被支持。解决办法是切回JDK 8,然后确认Java命令确实指向JDK 8。
另一种错误是类似NoSuchMethodError或ClassNotFoundException,多发生在自己源码构建、但构建环境依赖没对齐的时候。直接改用官方下载包,通常能绕开。
5.3 外部访问不了
本机能访问,但别的电脑通过http://你的IP:8880访问不了,百分之九十是防火墙拦截。Windows防火墙默认会阻止Java进程的入站请求。可以手动加一条入站规则,允许TCP端口8880和8884。
netsh advfirewall firewall add rule name="Diffy Allow" dir=in action=allow protocol=TCP localport=8880,8884这条命令需要在管理员权限的CMD里执行。如果公司网络或虚拟机网络还有更上层防火墙,那就不归Windows管了,需要联系网络负责人。
5.4 界面一直No data
Web界面能打开,但请求之后一直是No data,说明Diffy并没有收到任何请求,或者收到了但没被完整记录。优先检查:
- 发起请求时是不是用对了端口,有没有直接访问了三个后端端口。
- 三个后端是否都启动,Diffy是否在启动日志里成功连上它们。
- 请求路径是否在服务上真实存在,比如服务端只有/data.json,你却请求了根路径/。
我调试的时候犯过一个错,三个Python HTTP服务都开了,但启动Diffy时参数里的localhost写法不对,结果Diffy没法连接master。把参数改成完整localhost:8882后就好了。
5.5 更多避坑
Diffy会把一次请求放大到三次后端请求,所以如果你在Windows上用本地服务做验证,尽量避免在Diffy后端的服务里写很多日志或者做耗时操作,否则整个流程会感觉非常卡。
然后,Windows的命令行窗口默认编码经常是GBK,Diffy日志里如果包含中文内容,可能显示乱码。可以在CMD里先执行chcp 65001切到UTF-8,或者在PowerShell里启动脚本。这个不影响Diffy功能,但排查问题时能减少干扰。
最后一个容易被忽略的点:路径空格。Java命令在Windows解析时,如果jar包路径在C:\Program Files下面,命令行参数里可能需要用引号包住。最简单的办法是解压到C:\diffy这类路径下,省掉引号转义的折腾。
结尾就聊几句
把Diffy在Windows上跑通之后,你会发现它真正的作用不是部署本身,而是帮你把“版本行为是否一致”这件抽象事变成了可视化、可对比、可筛选的具体证据。我更推荐的做法是先在本地用模拟服务把Diffy的界面和差异分类看明白,再接入真实项目。原因是真服务的响应体更复杂,一旦出现海量差异,如果没有提前建立对Diffy输出形式的直觉,会很快迷失在数据里。Windows环境完全足够作为开发调试环境,生产环境想长期跑再去考虑Linux容器化。最后提醒一件事,接手别人的部署脚本时,先确认JDK版本和端口参数,这两个地方踩坑概率最高,也是我自己花时间最多的地方。