-
Notifications
You must be signed in to change notification settings - Fork 40.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve exploded structure experience for efficient deployments #38276
Comments
About performance - In my project a data exchange between agents conducted via zip files. In one case I had 1k+ zips inside other zip and preparing data for agent by unziping all archives had taken minutes. pom dependency <dependency>
<groupId>com.google.jimfs</groupId>
<artifactId>jimfs</artifactId>
<version>1.2</version>
</dependency> repo is here - https://github.com/google/jimfs |
@bclozel and I had a bit of brainstorming on this one and he raised a use case that could become problematic. The solution above ignores the Spring Boot launcher on purpose to make the startup as straightforward as possible. However, it currently does not allow to augment the classpath, which is something that can be problematic. I don't know if this is something we need to take into consideration, but the POC could be adapted accordingly. |
The PoC has been updated to provide the ability to include additional libraries. This is typically used by buildpacks, for instance:
This copies One decision could help move the POC into something that can be reviewed. If the above is working for buildpacks, perhaps this could become the default mode of operation for layer tools as well. In other words, merging The structure above reuse the concept of "layers" to nicely separate concerns, except it does not expand the structure of the far jar (i.e. Merging the two means we need to bring back the launcher, perhaps as an opt-in? Keeping things separate would be less disturbing. However, the argument that buildpacks would work with the new mechanism, and has to for optimal CDS performance, means two mode of operations that might not be desirable. There are also a lot of infrastructure in I am happy to improve the proposal based on what the team decides. |
I spent some time looking at this last week and we discussed it a bit yesterday. The high-level conclusion was that we need to combine support for layers and CDS in a single command. This is necessary as the former needs to know about the latter so that the runner jar can go in the most frequently changing layer. At the moment, I'm imagining a single command with two options – one for CDS and one for layers – that can each be used individually and that can also be used together. There's quite a lot to figure out in terms of how we get from where we are now to where we want to be in a non-breaking manner. We hope that we might be able to look at this in March but there are other, higher priority items that require our attention in the 3.3.0-M2 timeframe. |
Yes. I guess I should have dumped my thoughts somewhere as I reached the same conclusion. Considering the buildpacks use case, I see it as a continuation of what we've started when we introduce layertools back then. Buildpacks still need all the features that we've built so far, but with a mode of operation that allows to further tune the application before final packaging. Given the classpath cannot be extended further, a mode of operation could be that Then, we need to review With buildpacks being updated upfront to support the new infrastructure, we could imagine a mode of operation where the build plugins introduce a flag that defaults to this There are a ton of things that I overlooked, but I am happy to dig further based on the team's guidance when it gets to this. Thanks! |
We'd like to maintain the current model where the archive produced by the build plugins is the single source of truth. It can then be run as-is using |
I've integrated Stephane's work into a new When building your application, the resulting uber jar contains the new jarmode This provides four ways of extracting:
|
@mhalbritter is only the first extraction option ( |
Hello @mhalbritter !
Using that option, simple
While I have been able to CDS'ize my app
I was wondering if there was any reason you did not pick @sdeleuze unpacking layout, which is:
The difference being Sebastien created 2 jars: one with a manifest only ( At first, I thought it was kind of the same, but when I tried adding a jar to the classpath (such as spring-cloud-bindings), I found out that with @sdeleuze layout, I could add it to the classpath with:
skipping the But with your layout, I can't skip
Using this, I could obtain such a layout:
First, not sure why there's an empty folder named
I don't think this is usable as such, please let me know if I missed something
the layout I got was:
No surprises here, it just worked.
and then
it worked but... apparently very few classes got cached (I don't know the command you used to create a CDS caching report) from the training run output; and anyway, with the caching dsa file, I just went from 1.2 secs to 1.0 secs; I think the JarLauncher somehow hid the classes to CDS... Seems like the JarLauncher way is not usable efficiently with CDS....
I obtained this layout:
This one, same as layers no launcher, I could not use it:
Not sure how to proceed with this one. Those are the results of my experimentation.
Sorry for the long post post merge; I wish I had provided the feedback before you merged, but that was so much easier for me to test using published SNAPSHOTs ! Thank you |
I don't understand why you seem surprised. The whole point of this issue is to provide a CDS friendly unpack structure as the extract method we had before this issue wasn't and we can't get rid of it. This is the "expected" behavior. |
|
Thanks for giving it a try!
This isn't CDS friendly, is it? Because you're adding the whole If you want to add additional JARs to the classpath, I think you need to edit the manifest. But I also think @sdeleuze wanted to investigate how that's possible without editing the JAR, maybe with some clever |
The |
Yeah, that's expected. As soon as you include
See #38276 (comment). |
Soo, i played around with it. If you want to add additional libraries, this works with:
where Java then takes the This is CDS friendly, too:
|
Hello all! So there remains the simple
It works fine (CDS'ifying worked too); not sure why I could not make it work during my testing 🤷 |
After a bit of discussion we want to change the names of the produced artifacts a bit. The plan is: If you don't specify So given this:
you'll get If you specify a directory with So given this:
you'll get |
It now extracts the contents of the JAR in a folder named after the JAR without the extension. It now also checks if the folder is empty. There's a new --force option to skip those checks. The "runner.jar" is now named like the uber JAR from which the extraction has been started. See gh-38276
You meant:
Right? |
Yeah, you're right, that's a typo. I've edited the original post. |
ZipInputStream can't cope with some non-deflated entries, see https://bugs.openjdk.org/browse/JDK-8143613. JarFile works better, but it doesn't support creation time / access time. See gh-38276
Hello, I'm strugging with preparing multi stage dockerfile. It works fine when I don't use layers: FROM --platform=linux/arm64/v8 bellsoft/liberica-openjre-alpine-musl:21.0.3-10-cds AS builder
WORKDIR application
COPY build/libs/*.jar app.jar
RUN java -Djarmode=tools -jar app.jar extract --layers --destination out --application-filename app.jar
RUN cp -R ./out/dependencies/* ./
#When no loader and snapshot dependencies
#RUN cp -R ./out/spring-boot-loader/* ./
#RUN cp -R ./out/snapshot-dependencies/* ./
RUN cp -R ./out/application/* ./
RUN rm -rf ./out
RUN java -Xshare:dump #-XX:+UseZGC
RUN java \
-Dspring.context.exit=onRefresh \
-XX:ArchiveClassesAtExit=/application/app-cds.jsa \
-Xshare:on \
-jar app.jar
FROM --platform=linux/arm64/v8 bellsoft/liberica-openjre-alpine-musl:21.0.3-10-cds
WORKDIR application
COPY --from=builder /usr/lib/jvm/jre/lib/server/classes.jsa /usr/lib/jvm/jre/lib/server/classes.jsa
COPY --from=builder application .
ENTRYPOINT ["java", "-XX:SharedArchiveFile=app-cds.jsa", "-Xshare:on", "-jar", "app.jar"] However if I would like to pass layers separately: FROM --platform=linux/arm64/v8 bellsoft/liberica-openjre-alpine-musl:21.0.1-cds AS builder
WORKDIR application
COPY build/libs/*.jar app.jar
RUN java -Djarmode=tools -jar app.jar extract --layers --destination out --application-filename app.jar
RUN cp -R ./out/dependencies/* ./
#When no loader and snapshot dependencies
#RUN cp -R ./out/spring-boot-loader/* ./
#RUN cp -R ./out/snapshot-dependencies/* ./
RUN cp -R ./out/application/* ./
RUN rm -rf ./out
RUN java -Xshare:dump #-XX:+UseZGC
RUN java \
-Dspring.context.exit=onRefresh \
-XX:ArchiveClassesAtExit=/application/app-cds.jsa \
-Xshare:on \
-jar app.jar
FROM --platform=linux/arm64/v8 bellsoft/liberica-openjre-alpine-musl:21.0.1-cds
WORKDIR application
COPY --from=builder /usr/lib/jvm/jre/lib/server/classes.jsa /usr/lib/jvm/jre/lib/server/classes.jsa
COPY --from=builder application/lib ./lib
COPY --from=builder application/app.jar ./app.jar
COPY --from=builder application/app-cds.jsa /application/app-cds.jsa
ENTRYPOINT ["java", "-XX:SharedArchiveFile=app-cds.jsa", "-Xshare:on", "-jar", "app.jar"] I can see following error:
it seems that coping changes timestamps, can I avoid it somehow? The very same happens with Liberica JVM 21.0.3-10-cds |
@wyhasany please see moby/moby#17175. I don't think there's anything we can do about it in Spring Boot, unfortunately. |
@wilkinsona I think it is worth mentioning in docs, because using layered images could be confusing for users. I'm going to check behavior when the files are prepared outside of multistage docker. The other option is jib. |
Have you considered Paketo Buildpacks, the default Spring Boot container solution that supports CDS? |
Unfortunately buildpacks images are bigger from the ones manually created. |
well, I invite you to join the Paketo community: if you're interested in "small" images you'll learn about tiny builders, how to rebase on small run images, and if you're still unsatisfied, you'll learn about custom stack you could build with Chainguard, etc. See you there 👋 |
@wilkinsona Hmm... it seems to be not specific to the Docker: $ java -Djarmode=tools -jar spring-cold-startup-0.0.1-SNAPSHOT.jar extract --destination out --application-filename app.jar
$ cd out
$ tree -D
[May 13 14:16] .
├── [May 13 14:16] application
│ └── [May 13 14:16] app.jar
├── [May 13 14:16] dependencies
│ └── [May 13 14:16] lib
│ ├── [Apr 23 15:16] byte-buddy-1.14.13.jar
│ ├── [Mar 19 13:53] jackson-annotations-2.17.0.jar
│ ├── [Mar 19 13:53] jackson-core-2.17.0.jar
│ ├── [Mar 19 13:53] jackson-databind-2.17.0.jar
│ ├── [Apr 26 18:25] jackson-datatype-jdk8-2.17.0.jar
│ ├── [Apr 26 18:25] jackson-datatype-jsr310-2.17.0.jar
│ ├── [Apr 26 18:25] jackson-module-parameter-names-2.17.0.jar
│ ├── [May 13 11:34] jakarta.annotation-api-2.1.1.jar
│ ├── [Apr 23 15:16] jul-to-slf4j-2.0.13.jar
│ ├── [Apr 26 18:25] log4j-api-2.23.1.jar
│ ├── [Apr 26 18:25] log4j-to-slf4j-2.23.1.jar
│ ├── [Apr 23 10:25] logback-classic-1.5.6.jar
│ ├── [Apr 23 10:25] logback-core-1.5.6.jar
│ ├── [Apr 26 18:25] micrometer-commons-1.13.0-RC1.jar
│ ├── [Apr 26 18:25] micrometer-observation-1.13.0-RC1.jar
│ ├── [Apr 23 10:25] slf4j-api-2.0.13.jar
│ ├── [May 13 11:34] snakeyaml-2.2.jar
│ ├── [Apr 23 15:16] spring-aop-6.1.6.jar
│ ├── [Apr 23 15:16] spring-beans-6.1.6.jar
│ ├── [Apr 26 18:25] spring-boot-3.3.0-RC1.jar
│ ├── [Apr 26 18:25] spring-boot-autoconfigure-3.3.0-RC1.jar
│ ├── [Apr 23 15:16] spring-context-6.1.6.jar
│ ├── [Apr 16 12:10] spring-core-6.1.6.jar
│ ├── [Apr 23 15:16] spring-expression-6.1.6.jar
│ ├── [Apr 16 12:10] spring-jcl-6.1.6.jar
│ ├── [Apr 23 15:16] spring-web-6.1.6.jar
│ ├── [Apr 23 15:16] spring-webmvc-6.1.6.jar
│ ├── [Apr 23 15:16] tomcat-embed-core-10.1.20.jar
│ ├── [Apr 23 15:16] tomcat-embed-el-10.1.20.jar
│ └── [Apr 23 15:16] tomcat-embed-websocket-10.1.20.jar
├── [May 13 14:16] snapshot-dependencies
└── [May 13 14:16] spring-boot-loader
$ cp -R application/ .
$ cp -R dependencies/ .
$ ls -la
.rw-r--r-- 3.4k rowickim 15 May 16:09 app.jar
drwxr-xr-x - rowickim 13 May 14:16 application
drwxr-xr-x - rowickim 13 May 14:16 dependencies
drwxr-xr-x - rowickim 15 May 16:10 lib #date was changed after the copy, the very same goes for files underneath
$ ls -la lib
.rw-r--r-- 4.2M rowickim 15 May 16:10 byte-buddy-1.14.13.jar
.rw-r--r-- 78k rowickim 15 May 16:10 jackson-annotations-2.17.0.jar
.rw-r--r-- 582k rowickim 15 May 16:10 jackson-core-2.17.0.jar
.rw-r--r-- 1.6M rowickim 15 May 16:10 jackson-databind-2.17.0.jar
.rw-r--r-- 36k rowickim 15 May 16:10 jackson-datatype-jdk8-2.17.0.jar
.rw-r--r-- 132k rowickim 15 May 16:10 jackson-datatype-jsr310-2.17.0.jar
.rw-r--r-- 10k rowickim 15 May 16:10 jackson-module-parameter-names-2.17.0.jar
.rw-r--r-- 26k rowickim 15 May 16:10 jakarta.annotation-api-2.1.1.jar
.rw-r--r-- 6.3k rowickim 15 May 16:10 jul-to-slf4j-2.0.13.jar
.rw-r--r-- 343k rowickim 15 May 16:10 log4j-api-2.23.1.jar
#...
$ java -Dspring.context.exit=onRefresh -XX:ArchiveClassesAtExit=app-cds.jsa -Xshare:on -jar app.jar
$ rm -rf app.jar lib
$ cp -R application/ .
$ cp -R dependencies/ .
$ java -XX:SharedArchiveFile=app-cds.jsa -Xshare:on -jar app.jar
[0.123s][warning][cds] A jar file is not the one used while building the shared archive file: app.jar
[0.123s][warning][cds] A jar file is not the one used while building the shared archive file: app.jar
[0.123s][warning][cds] app.jar timestamp has changed.
[0.123s][warning][cds,dynamic] Unable to use shared archive. The top archive failed to load: app-cds.jsa
[0.124s][error ][cds ] An error has occurred while processing the shared archive file.
[0.124s][error ][cds ] Unable to map shared spaces
Error occurred during initialization of VM
Unable to use shared archive. To fix that we can just preserve metadata of files during copy: $ rm -rf app.jar lib
$ cp -pR application/ .
$ cp -pR dependencies/ .
[May 13 14:16] .
├── [May 15 16:14] app-cds.jsa
├── [May 13 14:16] app.jar
├── [May 13 14:16] application
│ └── [May 13 14:16] app.jar
├── [May 13 14:16] dependencies
│ └── [May 13 14:16] lib
│ ├── [Apr 23 15:16] byte-buddy-1.14.13.jar
│ ├── [Mar 19 13:53] jackson-annotations-2.17.0.jar
│ ├── [Mar 19 13:53] jackson-core-2.17.0.jar
│ ├── [Mar 19 13:53] jackson-databind-2.17.0.jar
│ ├── [Apr 26 18:25] jackson-datatype-jdk8-2.17.0.jar
│ ├── [Apr 26 18:25] jackson-datatype-jsr310-2.17.0.jar
│ ├── [Apr 26 18:25] jackson-module-parameter-names-2.17.0.jar
│ ├── [May 13 11:34] jakarta.annotation-api-2.1.1.jar
│ ├── [Apr 23 15:16] jul-to-slf4j-2.0.13.jar
│ ├── [Apr 26 18:25] log4j-api-2.23.1.jar
│ ├── [Apr 26 18:25] log4j-to-slf4j-2.23.1.jar
│ ├── [Apr 23 10:25] logback-classic-1.5.6.jar
│ ├── [Apr 23 10:25] logback-core-1.5.6.jar
│ ├── [Apr 26 18:25] micrometer-commons-1.13.0-RC1.jar
│ ├── [Apr 26 18:25] micrometer-observation-1.13.0-RC1.jar
│ ├── [Apr 23 10:25] slf4j-api-2.0.13.jar
│ ├── [May 13 11:34] snakeyaml-2.2.jar
│ ├── [Apr 23 15:16] spring-aop-6.1.6.jar
│ ├── [Apr 23 15:16] spring-beans-6.1.6.jar
│ ├── [Apr 26 18:25] spring-boot-3.3.0-RC1.jar
│ ├── [Apr 26 18:25] spring-boot-autoconfigure-3.3.0-RC1.jar
│ ├── [Apr 23 15:16] spring-context-6.1.6.jar
│ ├── [Apr 16 12:10] spring-core-6.1.6.jar
│ ├── [Apr 23 15:16] spring-expression-6.1.6.jar
│ ├── [Apr 16 12:10] spring-jcl-6.1.6.jar
│ ├── [Apr 23 15:16] spring-web-6.1.6.jar
│ ├── [Apr 23 15:16] spring-webmvc-6.1.6.jar
│ ├── [Apr 23 15:16] tomcat-embed-core-10.1.20.jar
│ ├── [Apr 23 15:16] tomcat-embed-el-10.1.20.jar
│ └── [Apr 23 15:16] tomcat-embed-websocket-10.1.20.jar
├── [May 13 14:16] lib
│ ├── [Apr 23 15:16] byte-buddy-1.14.13.jar
│ ├── [Mar 19 13:53] jackson-annotations-2.17.0.jar
│ ├── [Mar 19 13:53] jackson-core-2.17.0.jar
│ ├── [Mar 19 13:53] jackson-databind-2.17.0.jar
│ ├── [Apr 26 18:25] jackson-datatype-jdk8-2.17.0.jar
│ ├── [Apr 26 18:25] jackson-datatype-jsr310-2.17.0.jar
│ ├── [Apr 26 18:25] jackson-module-parameter-names-2.17.0.jar
│ ├── [May 13 11:34] jakarta.annotation-api-2.1.1.jar
│ ├── [Apr 23 15:16] jul-to-slf4j-2.0.13.jar
│ ├── [Apr 26 18:25] log4j-api-2.23.1.jar
│ ├── [Apr 26 18:25] log4j-to-slf4j-2.23.1.jar
│ ├── [Apr 23 10:25] logback-classic-1.5.6.jar
│ ├── [Apr 23 10:25] logback-core-1.5.6.jar
│ ├── [Apr 26 18:25] micrometer-commons-1.13.0-RC1.jar
│ ├── [Apr 26 18:25] micrometer-observation-1.13.0-RC1.jar
│ ├── [Apr 23 10:25] slf4j-api-2.0.13.jar
│ ├── [May 13 11:34] snakeyaml-2.2.jar
│ ├── [Apr 23 15:16] spring-aop-6.1.6.jar
│ ├── [Apr 23 15:16] spring-beans-6.1.6.jar
│ ├── [Apr 26 18:25] spring-boot-3.3.0-RC1.jar
│ ├── [Apr 26 18:25] spring-boot-autoconfigure-3.3.0-RC1.jar
│ ├── [Apr 23 15:16] spring-context-6.1.6.jar
│ ├── [Apr 16 12:10] spring-core-6.1.6.jar
│ ├── [Apr 23 15:16] spring-expression-6.1.6.jar
│ ├── [Apr 16 12:10] spring-jcl-6.1.6.jar
│ ├── [Apr 23 15:16] spring-web-6.1.6.jar
│ ├── [Apr 23 15:16] spring-webmvc-6.1.6.jar
│ ├── [Apr 23 15:16] tomcat-embed-core-10.1.20.jar
│ ├── [Apr 23 15:16] tomcat-embed-el-10.1.20.jar
│ └── [Apr 23 15:16] tomcat-embed-websocket-10.1.20.jar
├── [May 13 14:16] snapshot-dependencies
└── [May 13 14:16] spring-boot-loader
$ rm app-cds.jsa
$ java -Dspring.context.exit=onRefresh -XX:ArchiveClassesAtExit=app-cds.jsa -Xshare:on -jar app.jar
$ rm -rf app.jar lib
$ cp -pR application/ .
$ cp -pR dependencies/ .
$ java -XX:SharedArchiveFile=app-cds.jsa -Xshare:on -jar app.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.3.0-RC1)
2024-05-15T16:27:32.998+02:00 INFO 5878 --- [ main] t.p.cold.SpringColdStartupApplication : Starting SpringColdStartupApplication v0.0.1-SNAPSHOT using Java 21.0.2 with PID 5878 Let's try with Dockerfile: FROM --platform=linux/arm64/v8 bellsoft/liberica-openjre-alpine-musl:21.0.3-10-cds AS builder
WORKDIR application
COPY build/libs/*.jar app.jar
RUN java -Djarmode=tools -jar app.jar extract --layers --destination out --application-filename app.jar
RUN cp -pR ./out/dependencies/* ./
#RUN cp -pR ./out/spring-boot-loader/* ./
#RUN cp -pR ./out/snapshot-dependencies/* ./
RUN cp -pR ./out/application/* ./
RUN java -Xshare:dump #-XX:+UseZGC
RUN java \
-Dspring.context.exit=onRefresh \
-XX:ArchiveClassesAtExit=/application/app-cds.jsa \
-Xshare:on \
-jar app.jar
FROM --platform=linux/arm64/v8 bellsoft/liberica-openjre-alpine-musl:21.0.3-10-cds
WORKDIR application
COPY --from=builder /usr/lib/jvm/jre/lib/server/classes.jsa /usr/lib/jvm/jre/lib/server/classes.jsa
COPY --from=builder application/out/dependencies/ ./
COPY --from=builder application/out/spring-boot-loader/ ./
COPY --from=builder application/out/snapshot-dependencies/ ./
COPY --from=builder application/out/application/ ./
COPY --from=builder application/app-cds.jsa /application/app-cds.jsa
ENTRYPOINT ["java", "-XX:SharedArchiveFile=/usr/lib/jvm/jre/lib/server/classes.jsa:app-cds.jsa", "-Xshare:on", "-jar", "app.jar"] it works as expected using correct jars. Based on that this time the issue is not with the docker but missing |
Very interesting. Thank you, @wyhasany. Let me discuss it with the team to see if we think it would be best documented in Boot's docs, Framework's docs, or perhaps even in both. |
The reference guide has a section on unpacking the executable jar to get extra boost. It also has an additional hint for bypassing the bootlader with the impact on losing classpath ordering.
Our work on AppCDS (see spring-projects/spring-framework#31497) has shown that a predicable classpath has an impact on how effective the cache is going to be. Investigating a bit more, it looks doable to provide the above as a first class concept.
Here is a proposal that is hacking
layertools
with an additionalextract2
(sic) command: https://github.com/snicoll/spring-boot/tree/gh-38276Ignoring the fact that this does use layertools for convenience, you can execute the following on any repackaged archive:
This creates a directory with the following structure:
You can then run the app as follows (assuming you're in the same directory as the previous command):
The
run-app.jar
has the following characteristics:classpath.idx
, withapplication/my-app-1.0.0-SNAPSHOT.jar
being in frontmy-app-1.0.0-SNAPSHOT.jar
has some manifest entries of the original jar so thatpackage.getImplementationVersion()
continues to work.The work on this prototype has led to a number of questions:
Command
infrastructure and several utilities related to extracting/copying that are not specific to layers. If we want to go the same route with a different jar mode, a significant number of classes should be copied/reimplemented. Perhaps a single jar with a public API and sub-packages for layertools and this new mode could be an option?dependencies/BOOT-INF/lib
and a layer for the bootloader, we could use the same exact structure. Or perhaps the structure above could become a layer configuration (where all libs go todependencies
).run-app.jar
to have sensible File attributes. The command tries to respect the file attributes of files it extracts from the repackaged archive, butrun-jar.jar
is created on the spot.We have confirmed that with the prototype, AppCDS is effective (close to 95% classes loaded from the cache).
The text was updated successfully, but these errors were encountered: