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
Optimize NooBaa deletion paths #7262
Optimize NooBaa deletion paths #7262
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@utkarsh-pro. At first look, It looks good. There are few things we need to get more info on:
- How are the new queries behave with the indexes? We should run
EXPLAINon the new queries and ensure they hit the indexes correctly and do not require sorting. - Let's inspect other paths that go through the delete flows. mainly
delete_multiple_objects,delete_objectand lifecycle
9852ede
to
f0ed7ed
Compare
|
Folks, I have attached a |
d66eeb9
to
7390ca9
Compare
|
@guymguym if you feel like taking a look |
| @@ -71,7 +71,7 @@ async function background_worker() { | |||
| dbg.log0('LIFECYCLE READ BUCKETS configuration buckets:', system_store.data.buckets.map(e => e.name)); | |||
| for (const bucket of system_store.data.buckets) { | |||
| dbg.log0('LIFECYCLE READ BUCKETS configuration bucket name:', bucket.name, "rules", bucket.lifecycle_configuration_rules); | |||
| if (!bucket.lifecycle_configuration_rules || bucket.deleting) return; | |||
| if (!bucket.lifecycle_configuration_rules || bucket.deleting) continue; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like a bug fix, great catch 😀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm
| // This index is used for queries where we want to find all the chunks of a specific object | ||
| // which are already marked deleted. | ||
| fields: { | ||
| obj: 1, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@utkarsh-pro IIUC, this can also be achieved by removing the filter on deleted, Is that right? I wonder which approach is better, since adding indexes can affect writes performance. @guymguym any thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dannyzaken, I think partial indexes are faster for both reads and writes.
One major reason for using a partial index is to avoid indexing common values. Since a query searching for a common value (one that accounts for more than a few percent of all the table rows) will not use the index anyway, there is no point in keeping those rows in the index at all. This reduces the size of the index, which will speed up those queries that do use the index. It will also speed up many table update operations because the index does not need to be updated in all cases. Example 11.1 shows a possible application of this idea.
Taken from: https://www.postgresql.org/docs/current/indexes-partial.html
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or did I... misunderstood the comment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We discussed these new indexes on parts/multiparts and while there is an alternative to define the index only on the deleted field like in objectmds aggregate_by_delete_dates, this would mean that db_cleaner will cleanup parts and multiparts without specific relation to a deleted object, and this could be fine, but we don't have to do it now. The benefit is that the index will allow an index condition on the delete date, but the downside is that we will delete some parts and multiparts of objects and not all parts of the object, which might look a bit weird for other scanners (perhaps). In any case, this looks fine as we have to have some index for deletion, and this index will be limited by the amount of pending rows to deleted (the db_cleaner will keep it small).
7390ca9
to
8688995
Compare
Signed-off-by: Utkarsh Srivastava <srivastavautkarsh8097@gmail.com> improve DB Cleanup index hits Signed-off-by: Utkarsh Srivastava <srivastavautkarsh8097@gmail.com> optimize multipart deletions Signed-off-by: Utkarsh Srivastava <srivastavautkarsh8097@gmail.com>
Signed-off-by: Utkarsh Srivastava <srivastavautkarsh8097@gmail.com>
8688995
to
75de2be
Compare
| // find_objects will ensure that it does not return any object | ||
| // which is already marked for deletion and that's all we care | ||
| // about here. | ||
| const { objects } = await MDStore.instance().find_objects({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
better add a comment/hint that this uses the objectmd index with just bucket field and deleted condition -
noobaa-core/src/server/object_services/schemas/object_md_indexes.js
Lines 135 to 147 in d3091b6
| { | |
| // TODO index ??? count_objects_of_bucket() | |
| // TODO index ??? count_objects_per_bucket() | |
| fields: { | |
| bucket: 1, | |
| }, | |
| options: { | |
| unique: false, | |
| partialFilterExpression: { | |
| deleted: null, | |
| } | |
| } | |
| }, |
| // delete the objects | ||
| await MDStore.instance().remove_objects_and_unset_latest(objects); | ||
|
|
||
| const bucket_has_objects = await MDStore.instance().has_any_objects_for_bucket(bucket_id); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can remove this and just check the objects.length.
Both here and also in the previous function delete_multiple_objects_by_filter().
| count_total_objects() { | ||
| return this._objects.countDocuments({}); // maybe estimatedDocumentCount() | ||
| } | ||
|
|
||
| estimated_total_objects() { | ||
| return this._objects.estimatedDocumentCount(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe best remove the unused count_total_objects().
Also the comment is now definitely redundant (// maybe estimatedDocumentCount())
| @@ -51,7 +51,7 @@ async function background_worker() { | |||
| } | |||
|
|
|||
| async function clean_md_store(last_date_to_remove) { | |||
| const total_objects_count = await MDStore.instance().count_total_objects(); | |||
| const total_objects_count = await MDStore.instance().estimated_total_objects(); | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps we can check the estimate of deleted objects here instead of the total?
We have this index but we need to see if the database has an estimate per index -
noobaa-core/src/server/object_services/schemas/object_md_indexes.js
Lines 115 to 127 in d3091b6
| { | |
| // aggregate_objects_by_delete_dates() | |
| fields: { | |
| deleted: 1, | |
| }, | |
| options: { | |
| unique: false, | |
| name: "aggregate_by_delete_dates", | |
| partialFilterExpression: { | |
| deleted: { $exists: true } | |
| } | |
| } | |
| }, |
| async find_deleted_objects(max_delete_time, limit) { | ||
| const objects = await this._objects.find({ | ||
| deleted: { $lt: new Date(max_delete_time) }, | ||
| deleted: { | ||
| $lt: new Date(max_delete_time), | ||
| $exists: true // This forces the index to be used | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't use an index condition on the deleted date, and will continue scanning throught the index and filtering by it. This seems to be related to how this index is defined to use a to_ts() function. Here are the two different explains:
currently:
nbcore=> EXPLAIN SELECT * from objectmds WHERE data->'deleted'<'"2023-04-14T21:52:00.909Z"'::jsonb AND data ? 'deleted' LIMIT 1000;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------
Limit (cost=0.00..3.47 rows=1 width=430)
-> Seq Scan on objectmds (cost=0.00..3.47 rows=1 width=430)
Filter: ((data ? 'deleted'::text) AND ((data -> 'deleted'::text) < '"2023-04-14T21:52:00.909Z"'::jsonb))
(3 rows)
with to_ts():
nbcore=> EXPLAIN SELECT * from objectmds WHERE to_ts(data->>'deleted'::text) < to_ts('2023-04-14T21:52:00.909Z') AND data ? 'deleted' LIMIT 1000;
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------
Limit (cost=0.14..8.15 rows=1 width=430)
-> Index Scan using idx_btree_objectmds_aggregate_by_delete_dates on objectmds (cost=0.14..8.15 rows=1 width=430)
Index Cond: (to_ts((data ->> 'deleted'::text)) < '2023-04-14 21:52:00.909'::timestamp without time zone)
(3 rows)
| // This index is used for queries where we want to find all the chunks of a specific object | ||
| // which are already marked deleted. | ||
| fields: { | ||
| obj: 1, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We discussed these new indexes on parts/multiparts and while there is an alternative to define the index only on the deleted field like in objectmds aggregate_by_delete_dates, this would mean that db_cleaner will cleanup parts and multiparts without specific relation to a deleted object, and this could be fine, but we don't have to do it now. The benefit is that the index will allow an index condition on the delete date, but the downside is that we will delete some parts and multiparts of objects and not all parts of the object, which might look a bit weird for other scanners (perhaps). In any case, this looks fine as we have to have some index for deletion, and this index will be limited by the amount of pending rows to deleted (the db_cleaner will keep it small).
Explain the changes
This PR attempts to improve performance NooBaa delete paths by doing the following:
Analysis
analysis.md
Issues: Fixed #xxx / Gap #xxx
This was a customer issue where they noticed serious NooBaa degradation when a bucket with 3M objects was deleted.