如何将JAR文件转换为可通过rsync同步的JAR文件?

15
我有一个由Gradle Shadow插件生成的大型JAR文件。我经常需要通过网络发送这个大型JAR文件,因此,只发送文件的差异而不是约40 MB的数据对我来说很方便。rsync是实现此目的的好工具。然而,我的源代码的微小更改会导致最终的大型JAR文件发生较大变化,因此rsync没有发挥其最大作用。

我能否将大型JAR文件转换为rsync友好的JAR文件?

我的解决方案/解决方法的想法:

  • 将重点放在rsync上,并告诉它以某种方式使用压缩文件(我没有找到任何方法)。
  • 将不可同步的JAR文件转换为可同步的JAR文件。
  • 告诉Gradle Shadow生成可同步的JAR文件(目前不可能)。

可能相关的问题:


评论以防有人回答。我也需要知道。 - Y.Kaan Yılmaz
有没有将JAR文件解压缩并使用rsync发送,然后在远程机器上重新压缩的选项?这样,rsync应该能够具有低流量。 - Steffen Harbich
好的,这是一个选项。虽然我更喜欢在源机器上准备所有东西。我认为,这个解决方案也需要相当多的不必要的I/O磁盘操作。 - MartyIX
由于我正在使用大型的jar/war构建,这些构建默认情况下允许rsync通过节省传输而实现主要加速(正如您所称的rsyncable),因此我得出的印象是您实际上在构建过程中遇到了问题。您是否验证了“未更改”的文件上的元数据确实保持不变(例如,类的最后修改时间是上次更改后第一次构建的时间,而不仅仅是最后一次构建的时间)? - rpy
4个回答

4

有两种方法可以做到这一点,两种方法都涉及关闭压缩。首先使用Gradle,然后使用jar方法关闭它...

您可以使用Gradle来完成此操作(实际上该答案来自OP)

shadowJar {
    zip64 true
    entryCompression = org.gradle.api.tasks.bundling.ZipEntryCompression.STORED
    exclude 'META-INF/*.RSA', 'META-INF/*.SF','META-INF/*.DSA'
    manifest {
        attributes 'Main-Class': 'com.my.project.Main'
    }
}

使用

jar {
    manifest {
        attributes(
                'Main-Class': 'com.my.project.Main',
        )
    }
}

task fatJar(type: Jar) {
    manifest.from jar.manifest
    classifier = 'all'
    from {
        configurations.runtime.collect { it.isDirectory() ? it : zipTree(it) }
    } {
        exclude "META-INF/*.SF"
        exclude "META-INF/*.DSA"
        exclude "META-INF/*.RSA"
    }
    with jar
}

关键在于压缩已被关闭,即:
org.gradle.api.tasks.bundling.ZipEntryCompression.STORED

您可以在这里找到文档: https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/bundling/ZipEntryCompression.html#STORED 是的,您可以通过不压缩JAR文件,利用rsync的分块算法,从而在新存档中将速度提高约40%,在已经同步过的JAR存档上将速度提高超过200%。
我使用了以下命令来压缩一个包含大量class文件的目录...
jar cf0 uncompressed.jar .
jar cf  compressed.jar   .

这创建了以下两个 JAR 包...
-rw-r--r--  1 rsync jar    28331212 Apr 13 14:11 ./compressed.jar
-rw-r--r--  1 rsync jar    38746054 Apr 13 14:10 ./uncompressed.jar

请注意,未压缩的Jar大小约大10MB。
然后我使用以下命令对这些文件进行了rsync并计时。(注意,即使为压缩文件启用压缩也几乎没有影响,稍后我会解释) 压缩的Jar
time rsync -av -e ssh compressed.jar jar@rsync-server.org:/tmp/

building file list ... done
compressed.jar

sent 28334806 bytes  received 42 bytes  2982615.58 bytes/sec
total size is 28331212  speedup is 1.00

real  0m9.208s
user  0m0.248s
sys 0m0.483s

未压缩的Jar包

time rsync -avz -e ssh uncompressed.jar jar@rsync-server.org:/tmp/

building file list ... done
uncompressed.jar

sent 11751973 bytes  received 42 bytes  2136730.00 bytes/sec
total size is 38746054  speedup is 3.30

real  0m5.145s
user  0m1.444s
sys 0m0.219s

我们已经实现了近50%的加速。这至少可以加快rsync并且我们获得了很好的提升,但是如果进行了小的更改会如何呢?
我从目录中删除了一个大小为170字节的类文件,并重新创建了jar文件,现在它们的大小是这样的...
-rw-r--r--  1 rsycn jar  28330943 Apr 13 14:30 compressed.jar
-rw-r--r--  1 rsync jar  38745784 Apr 13 14:30 uncompressed.jar

现在的时间安排非常不同。 压缩的Jar文件
building file list ... done
compressed.jar

sent 12166657 bytes  received 31998 bytes  2217937.27 bytes/sec
total size is 28330943  speedup is 2.32

real  0m5.435s
user  0m0.378s
sys 0m0.335s

未压缩的Jar包

building file list ... done
uncompressed.jar

sent 220163 bytes  received 43624 bytes  175858.00 bytes/sec
total size is 38745784  speedup is 146.88

real  0m1.533s
user  0m0.363s
sys 0m0.047s

因为信息理论的原因,我们可以使用这种方法加速大型jar文件的rsync。当您压缩数据时,它实际上会将数据中的所有常见部分删除,也就是说,剩下的内容看起来非常像随机数据,最好的压缩程序会删除更多的此类信息。对数据进行微小更改,大多数压缩算法都会对数据输出产生巨大影响。
Zip算法有效地使rsync在服务器和客户端之间查找相同校验和变得更加困难,这意味着它需要传输更多的数据。当您解压缩时,您让rsync做它擅长的事情,发送较少的数据来同步两个文件。

2
据我所知,rsyncable gzip 是通过在每 8192 字节的压缩数据中重新设置哈夫曼树和填充到字节边界来工作的。这避免了对压缩的长距离副作用(如果它们至少是字节对齐的,则 rsync 会处理移位的数据块)。
从这个意义上讲,包含小文件(小于 8192 字节)的 jar 已经是可 rsync 的,因为每个文件都是单独压缩的。作为测试,您可以使用 jar 的 -0 选项(无压缩)检查它是否有助于 rsync,但我认为它不会有帮助。
要改进 rsyncability,您需要(至少):
- 确保文件以相同的顺序存储。 - 确保与未更改的文件关联的元数据也未更改,因为每个文件都有一个本地文件头。例如,最后修改时间对于 .class 文件是有问题的。
我不确定 jar 是否如此,但 zip 允许额外的字段,其中一些可能会阻止 rsync 匹配,例如 unix 扩展的最后访问时间。
编辑:我使用以下命令进行了一些测试:
FILENAME=SomeJar.jar

rm -rf tempdir
mkdir tempdir

unzip ${FILENAME} -d tempdir/

cd tempdir

# set the timestamp to 2000-01-01 00:00
find . -print0 | xargs --null touch -t 200001010000

# normalize file mode bits, maybe not necessary
chmod -R u=rwX,go=rX .

# sort and zip files, without extra
find . -type f -print | sort | zip ../${FILENAME}_normalized  -X -@

cd ..
rm -rf tempdir

当jar / zip文件中包含的第一个文件被删除时,rsync统计信息:

total: matches=1973  hash_hits=13362  false_alarms=0 data=357859
sent 365,918 bytes  received 12,919 bytes  252,558.00 bytes/sec
total size is 4,572,187  speedup is 12.07

当第一个文件被删除并且每个时间戳都被修改时:

total: matches=334  hash_hits=124326  false_alarms=4 data=3858763
sent 3,861,473 bytes  received 12,919 bytes  7,748,784.00 bytes/sec
total size is 4,572,187  speedup is 1.18

因此,存在显著的差异,但并不像我预期的那样大。

另外,更改文件模式似乎不会影响传输(可能是因为它存储在中央目录中?)


谢谢。我对rsyncable的工作原理有基本的了解。不幸的是,这并没有真正回答我的问题,因为您没有说出我如何实现您提出的建议。感谢您的意见。 - MartyIX
我能想到的最简单的解决方案是解包jar文件,更改时间戳,然后重新打包排序。这取决于您使用的操作系统,例如对于Linux,它将基于“unzip”,“touch”和“zip”。这并不难,但我觉得有点奇怪,没有已经做到这一点的构建工具。 - bwt
顺便说一下:我正在使用Linux。你能展示一下你的方法的一个工作示例吗? - MartyIX
我添加了测试结果。 - bwt

1

我在build.gradle中替换了原始的配置代码:

shadowJar {
    zip64 true
    entryCompression = org.gradle.api.tasks.bundling.ZipEntryCompression.STORED
    exclude 'META-INF/*.RSA', 'META-INF/*.SF','META-INF/*.DSA'
    manifest {
        attributes 'Main-Class': 'com.my.project.Main'
    }
}

使用

jar {
    manifest {
        attributes(
                'Main-Class': 'com.my.project.Main',
        )
    }
}

task fatJar(type: Jar) {
    manifest.from jar.manifest
    classifier = 'all'
    from {
        configurations.runtime.collect { it.isDirectory() ? it : zipTree(it) }
    } {
        exclude "META-INF/*.SF"
        exclude "META-INF/*.DSA"
        exclude "META-INF/*.RSA"
    }
    with jar
}

(使用此处发布的解决方案https://dev59.com/iW445IYBdhLWcg3wappH#31426413

最终的fatJar比Shadow插件为我生成的要大得多(即56 MB而不是35 MB)。但是,最终的jar似乎可以通过rsync同步(当我对源代码进行微小更改时,rsync只传输了非常少量的数据)。

请注意,我对Gradle的知识非常有限,这只是我的观察,可能还有进一步的改进空间。


它关闭了压缩,这就是我在答案中所做的。请参阅此处的文档... org.gradle.api.tasks.bundling.ZipEntryCompression.STORED https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/bundling/ZipEntryCompression.html#STORED - Harry
@Harry 如果你将我的答案(一个gradle的解决方案)整合到你的答案中(从我的角度来看,这样就是一个完整的答案),我很乐意授予你赏金,因为我总体上喜欢你的答案。 - MartyIX
我刚刚更新了它。这有帮助吗?如果你想对答案做更多的修改,请告诉我。另外,如果你对压缩感兴趣,可以看看Hutter奖 http://prize.hutter1.net/。当我发现它时,我花了几个月的时间研究压缩。 - Harry
看起来不错,谢谢!我会看一下,但不幸的是我没有几个月的空闲时间去投资。 :) - MartyIX

1

让我们往回走一步;如果你不创建大的Jar包,这个问题也就消失了。

因此,如果你将依赖的Jar包分开部署,而不是将它们打成一个单独的大的Jar包,那么你也解决了这个问题。

为此,假设你有:

  • /foo/yourapp.jar
  • /foo/lib/guava.jar
  • /foo/lib/h2.jar

然后,在 yourapp.jarMETA-INF/MANIFEST.MF 文件中添加以下条目:

Class-Path: lib/guava.jar lib/h2.jar

现在,您只需运行java -jar yourapp.jar即可正常工作,并获取依赖项。您现在可以使用rsync单独传输这些文件;yourapp.jar将更小,而您的依赖项jars通常不会发生变化,因此在rsync时也不会花费太多时间。我知道这并没有直接回答实际提出的问题,但我敢打赌,在90%以上的情况下,不使用fatjarring是适当的答案。注意:Ant、Maven、Guava等可以处理正确的清单条目。如果您的jar的意图不是运行它,而是例如Web Servlet容器的war,则这些容器有自己的规则来指定您的依赖项jar位于何处。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接