addresses slow import/export performance by limiting persistence context size - #37926
Conversation
|
Also, @pedroigor |
|
I always thought that the import operation was mainly targeted for provisioning realms with the very basic data and anything beyond that would be better addressed by dumping/restoring the database. But reading the original it seems we introduced a fix that is degrading the performance of import, is that correct? I see the solution you are proposing but for me, it is adding additional complexity for use cases that I'm not sure we are supposed to support. It looks like we are trying to work around the problem that we now have the import running in the scope of a single (KC/HHM) session. Wouldn't be better to import user files in batch (e.g.: |
Import from multiple user files, yes.
I don't agree really agree with this viewpoint - the existing logic is inefficient for grouping together any number of user creations. I'd rather start with addressing that, then decide if splitting over multiple transactions makes sense.
Did you see the rationale for why this was set to true?
That's what this code change will effectively allow for - by removing the explicit and implicit flushes we'll allow hibernate to actually batch under the covers. |
|
Dumping/restoring, but also migrating from a legacy system? Import is also for this use case no? |
I think it does because a single transaction might eventually end up with lot of objects in the persistence context and that, I think, is the limiting factor here. At the same time, it would be nice to improve how batch processing from
Yes, I see. Do you mean what you commented here #34364 (comment)?
IIRC, most of the time (mainly for users) we are calling In summary, I think we agree/need:
The only thing I'm not sure is reviewing the explicit calls to |
2498aa0 to
f02a0f3
Compare
Thank you for the review @pedroigor and agreed on this point. To make things clearer / safer, I've refined the changes to be more explicit rather than to hide all the handling under the PersistenceExceptionConverter. I also stuck with using a thread local so that the batching can apply to all entitymanagers proxied by PersistenceExceptionConverter - but that does make the JpaConnectionProvider.batchMode method a bit odd because it's effectively static.
To keep things explicit it's now up to a caller to initiate a batching mode, then turn it off. Then to align with the previous import logic this is done around each directory file. Alternatively the notion of how many operations to batch could be handled by the PersistenceExceptionConverter like the migration logic. This change by itself gives us most of the performance benefit of using separate transactions. There are 2 further potential optimizations:
I left this out of the changes for now, but yes that would definitely be nice. |
|
Some small benchmarking efforts with running pg in podman and 2000 minimal users in a single file:
10000 users in a single file with batching and with setting queries to COMMIT took 4 s, so pretty linear time scaling. If we can agree on an implicit flush behavior (like the current logic for migration batching), then we wouldn't have to scope the batching behavior per file nor worry about the persistence context getting too large. |
|
Looked at #34364 (comment) - I don't see any large performance issue even with the current state of main as long as the users per file is under 1000, so there doesn't seem to be a regression with in general. However doing several thousand users per file starts to exhibit the same type of performance issues as importing a large number of users. |
|
I looked at the explicit flushes in the past, and IMHO the primary reason they are there is the fear what might happen if we remove them. Even if it has been added in the past to show DB constrains earlier and not only when the transaction is over, this is IMHO an antipattern as with concurrent requests there could still be conflicts discovered only at commit time. So +1 for for removing them step-by-step. This might surface some wrongly applied JPA logic especially when child entities are updated - we've been struck by #11666 in the past, which is AFAIK still a problem in Hibernate. Also +1 for the COMMIT setting when importing. When importing a list of users, IMHO it still makes sense to put each user file in its own transaction to avoid allocating too much memory and having a large persistence context. |
|
@shawkins Nice work! JFYI - not batched flushes are also an issue for default client scopes when the realm is created. So there might also be some possible improvement. |
I'll show what a possible higher-level control for this could look like in my next commit. I believe that something like this could eventually be used to allow callers to do error handling prior to the commit.
Thanks @mabartos that's good to know, I can certainly expand the scope of the batching as needed. |
This commit adds batching to export, so that the export of 10000 users to a single file or the realm file is taking around 14-18 s - so there is some asymetric inefficiency as import from a single file only takes about 4 seconds. Since #37990 has not been applied, these results are with running in a single transaction. The higher level batch controls provide flush / clear methods when running in batch mode. Usage of it is shown in two ways - for export it's just clearing the context around what could be transaction boundaries. For import it's flushing and clearing the context after a given number of users, rather than just letting the number of users in the file drive the decision. I'm open to tweaking this in anyway that others see fit. In the end we should be able to batch operations and limit the memory usage of the persistence context in a flexible way. Also by leaving the ExportImportSessionTask my intention is to allow a fallback to multi-transaction behavior, but to make that require an additional switch for import / export. |
d4740b7 to
8b0de41
Compare
|
Adding hold label not to merge before 26.2 release. |
70e2bd2 to
17e1417
Compare
|
Refactored to pull out unrelated changes, command level switches to use multiple transactions, and the KeycloakModelUtils handling of reusing the current session. Also switched the logic to create nested entity managers when batching. I don't believe there is any benefit to exposing to the user multi-txn controls - however I still left in the current structuring for multi-txn in case we need to quickly support that. @mabartos also added batching for the default client scopes - as you pointed out they are the next biggest issue with import performance. And it does are to aslo be hit when migration logic runs - along with poor query performance in the migrateRealm method. Those are the likely culprits behind #34662 |
|
@vmuzikar @ahus1 @pedroigor what needs to be done here to keep this moving forward? I believe the changes have been minimized to keep this lower risk, but I'm open to refining as needed. |
|
I plan to review on Monday. Additionally, I'd like to ask if we could get a review either from @pedroigor or @ahus1. After that, IMHO we'll be ready to merge. |
|
@shawkins - thank you for the PR. While I suppose this is good to be merged, see below for some thoughts about corner cases. Note that this is built on the assumption that importing a user doesn't modify any other existing entity that has been touched in a previous batch. Those entities are basically detached by the em.clear() method. Those now detached entities are still bound to the current session in UserCacheSession#managedUsers. Any subsequent changes to them will get lost. Operations like https://github.com/keycloak/keycloak/blob/9721d920feeed1718bda49ac59cb4fc80ae305a2/model/storage-services/src/main/java/org/keycloak/exportimport/util/ImportUtils.java#L73-L78 still seems to only add new entities, but don't modify existing ones. I'm good to proceed with this as long as we don't extend this to other areas of Keycloak where this assumption might not hold. While I am quite sure this will not break Keycloak code, I hope people didn't write extensions that would break this assumption. While the speed of the import is probably good, with a lot of users or other entities it will acquire a lot of entries in memory due to UserCacheSession#managedUsers and other similar patterns I suspect for other entities. I see that the recommendation to put users in files is now removed completely. Assuming that the files are still imported in separate transactions, and that this would keep the heap usage at bay, we might still want to keep that recommendation, with a higher number though. |
Which is an assumption that was already built into the multi-file/transaction case, so this is not a new concept. Here we're mostly just replacing the transaction with a manipulation of the EntityManager(s).
Since the EntityManagers class is in spi-private, they would have to knowingly abuse a private api.
With this change they are not.
Right with the old text gone it doesn't cover the possiblity of the import running out of memory. I add some language about using multiple files to protect the memory of the import operation. I did see that without any tweaks 200,000 bare-bones users were enough to run the import process out of memory. As for what the number should be:
|
I had more something like a very crazy user storage provider in mind or similar. Anyway, it should be fine.
So we are now limiting the import to the heap available to KC during import? If you think this is acceptable, it might be worth mentioning in the docs ("if you see an OOM during import due to a large number of imported entities, increase the heap available to Keycloak"). By running each file in a separate transaction, the limitation by the available heap can possibly be avoided. Given your tests, the new default number of users per file can be increased. Given you latest comment, I suppose it would be ok to increase it to 100_000 users then? |
This is not a new limitation. This has been a latent issue that was probably not addressed due to recommending multiple files. The old logic put the entire user contents from a single file (whether that was realm+users, or just a users file) into memory. That is still the case, even with these changes, for a file containg both the realm and the users. You probably noticed that I tried to improve a little upon multi-file import logic with some tweaks so that it could be more streaming, but that problem wasn't expressly my goal to address here - just the multi-transaction issue. So even with this pr we still do eventually hit an OOM at some number of users per file.
No, that is central point of these changes - multiple transactions are not needed. However the usage of multiple files still helps address some other limitation with our logic and how it is parsing / performing the import. With a little more effort this limitation could be removed. The changes here will allow me to address #38251 by making the logic more uniform between single and multi-file imports, then I believe I could tackle better streaming of user imports such that multiple files are no longer even needed. |
|
@shawkins - ok, let's then process with this one as it is a big improvement to many, and tackle limitations in future PRs. |
closes: keycloak#37991 Signed-off-by: Steve Hawkins <shawkins@redhat.com>
Signed-off-by: Steve Hawkins <shawkins@redhat.com>
Signed-off-by: Steve Hawkins <shawkins@redhat.com>
|
@ahus1 sounds good. Refined the language in the docs to reference memory limitations. |
| default boolean useExistingSession() { | ||
| return false; | ||
| } |
There was a problem hiding this comment.
This is essentially a breaking change in an SPI that is not strictly marked as private. However, it was used only in private KeycloakModelUtils, so I guess it's kinda grey area. Still, it'd be good to at least mention it in the upgrading guide.
There was a problem hiding this comment.
Correct, the only developers who would have been using this method were those exercising the private api. We talked about moving the Task classes to the private module as well, but that was dismissed as a follow-on to the change that introduced useExistingSession in the first place.
I can certainly add a note or leave this method, but deprecated, if you want.
There was a problem hiding this comment.
Nah, let's remove it, but I'd suggest just documenting it out of courtesy to users that shouldn't be using it. :)
There was a problem hiding this comment.
Added the doc note.
and expanding javadocs Signed-off-by: Steve Hawkins <shawkins@redhat.com>
closes: #37991
adds a batching mode for JPA operations that will remove flush / detach from the critical path of import. A very small sample of the potential improvement is covered in #37926 (comment)- removed the logic that conditionally exlucded flush / detach.These changes also address: