satellite/{metainfo,metabase}: optimize expired/zombie objects deletion

* optimize SQL for zombie objects deletion query by reducing some direct
selects to segments table
* set AOST for expired/zombie object deletion (was 0 since now)

https://github.com/storj/storj/issues/5881

Change-Id: I50482151d056a86fe0e31678a463f413d410759d
This commit is contained in:
Michal Niewrzal 2023-05-15 13:48:03 +02:00
parent aea3baf6a9
commit 61933bc6f0
5 changed files with 40 additions and 36 deletions

View File

@ -22,9 +22,9 @@ const (
// DeleteExpiredObjects contains all the information necessary to delete expired objects and segments.
type DeleteExpiredObjects struct {
ExpiredBefore time.Time
AsOfSystemTime time.Time
BatchSize int
ExpiredBefore time.Time
AsOfSystemInterval time.Duration
BatchSize int
}
// DeleteExpiredObjects deletes all objects that expired before expiredBefore.
@ -37,7 +37,7 @@ func (db *DB) DeleteExpiredObjects(ctx context.Context, opts DeleteExpiredObject
project_id, bucket_name, object_key, version, stream_id,
expires_at
FROM objects
` + db.impl.AsOfSystemTime(opts.AsOfSystemTime) + `
` + db.impl.AsOfSystemInterval(opts.AsOfSystemInterval) + `
WHERE
(project_id, bucket_name, object_key, version) > ($1, $2, $3, $4)
AND expires_at < $5
@ -95,10 +95,10 @@ func (db *DB) DeleteExpiredObjects(ctx context.Context, opts DeleteExpiredObject
// DeleteZombieObjects contains all the information necessary to delete zombie objects and segments.
type DeleteZombieObjects struct {
DeadlineBefore time.Time
InactiveDeadline time.Time
AsOfSystemTime time.Time
BatchSize int
DeadlineBefore time.Time
InactiveDeadline time.Time
AsOfSystemInterval time.Duration
BatchSize int
}
// DeleteZombieObjects deletes all objects that zombie deletion deadline passed.
@ -112,7 +112,7 @@ func (db *DB) DeleteZombieObjects(ctx context.Context, opts DeleteZombieObjects)
SELECT
project_id, bucket_name, object_key, version, stream_id
FROM objects
` + db.impl.AsOfSystemTime(opts.AsOfSystemTime) + `
` + db.impl.AsOfSystemInterval(opts.AsOfSystemInterval) + `
WHERE
(project_id, bucket_name, object_key, version) > ($1, $2, $3, $4)
AND status = ` + pendingStatus + `
@ -155,7 +155,7 @@ func (db *DB) DeleteZombieObjects(ctx context.Context, opts DeleteZombieObjects)
return ObjectStream{}, nil
}
err = db.deleteInactiveObjectsAndSegments(ctx, objects, opts.InactiveDeadline)
err = db.deleteInactiveObjectsAndSegments(ctx, objects, opts)
if err != nil {
db.log.Warn("delete from DB zombie objects", zap.Error(err))
return ObjectStream{}, nil
@ -237,7 +237,7 @@ func (db *DB) deleteObjectsAndSegments(ctx context.Context, objects []ObjectStre
return nil
}
func (db *DB) deleteInactiveObjectsAndSegments(ctx context.Context, objects []ObjectStream, inactiveDeadline time.Time) (err error) {
func (db *DB) deleteInactiveObjectsAndSegments(ctx context.Context, objects []ObjectStream, opts DeleteZombieObjects) (err error) {
defer mon.Task()(&ctx)(&err)
if len(objects) == 0 {
@ -248,24 +248,20 @@ func (db *DB) deleteInactiveObjectsAndSegments(ctx context.Context, objects []Ob
var batch pgx.Batch
for _, obj := range objects {
batch.Queue(`
WITH deleted_objects AS (
WITH check_segments AS (
SELECT 1 FROM segments WHERE stream_id = $5::BYTEA AND created_at > $6
), deleted_objects AS (
DELETE FROM objects
WHERE
(project_id, bucket_name, object_key, version) = ($1::BYTEA, $2::BYTEA, $3::BYTEA, $4) AND
stream_id = $5::BYTEA AND (
-- TODO figure out something more optimal
NOT EXISTS (SELECT stream_id FROM segments WHERE stream_id = $5::BYTEA)
OR
-- check that all segments where created before inactive time
NOT EXISTS (SELECT stream_id FROM segments WHERE stream_id = $5::BYTEA AND created_at > $6)
)
RETURNING version -- return anything
NOT EXISTS (SELECT 1 FROM check_segments)
RETURNING stream_id
)
DELETE FROM segments
`+db.impl.AsOfSystemInterval(opts.AsOfSystemInterval)+`
WHERE
segments.stream_id = $5::BYTEA AND
NOT EXISTS (SELECT stream_id FROM segments WHERE stream_id = $5::BYTEA AND created_at > $6)
`, obj.ProjectID, []byte(obj.BucketName), []byte(obj.ObjectKey), obj.Version, obj.StreamID, inactiveDeadline)
segments.stream_id IN (SELECT stream_id FROM deleted_objects)
`, obj.ProjectID, []byte(obj.BucketName), []byte(obj.ObjectKey), obj.Version, obj.StreamID, opts.InactiveDeadline)
}
results := conn.SendBatch(ctx, &batch)

View File

@ -23,10 +23,11 @@ var (
// Config contains configurable values for zombie object cleanup.
type Config struct {
Interval time.Duration `help:"the time between each attempt to go through the db and clean up zombie objects" releaseDefault:"12h" devDefault:"10s"`
Enabled bool `help:"set if zombie object cleanup is enabled or not" default:"true"`
ListLimit int `help:"how many objects to query in a batch" default:"100"`
InactiveFor time.Duration `help:"after what time object will be deleted if there where no new upload activity" default:"24h"`
Interval time.Duration `help:"the time between each attempt to go through the db and clean up zombie objects" releaseDefault:"12h" devDefault:"10s"`
Enabled bool `help:"set if zombie object cleanup is enabled or not" default:"true"`
ListLimit int `help:"how many objects to query in a batch" default:"100"`
InactiveFor time.Duration `help:"after what time object will be deleted if there where no new upload activity" default:"24h"`
AsOfSystemInterval time.Duration `help:"as of system interval" releaseDefault:"-5m" devDefault:"-1us" testDefault:"-1us"`
}
// Chore implements the zombie objects cleanup chore.
@ -80,8 +81,9 @@ func (chore *Chore) deleteZombieObjects(ctx context.Context) (err error) {
chore.log.Debug("deleting zombie objects")
return chore.metabase.DeleteZombieObjects(ctx, metabase.DeleteZombieObjects{
DeadlineBefore: chore.nowFn(),
InactiveDeadline: chore.nowFn().Add(-chore.config.InactiveFor),
BatchSize: chore.config.ListLimit,
DeadlineBefore: chore.nowFn(),
InactiveDeadline: chore.nowFn().Add(-chore.config.InactiveFor),
BatchSize: chore.config.ListLimit,
AsOfSystemInterval: chore.config.AsOfSystemInterval,
})
}

View File

@ -26,8 +26,8 @@ func TestZombieDeletion(t *testing.T) {
SatelliteCount: 1, StorageNodeCount: 1, UplinkCount: 1,
Reconfigure: testplanet.Reconfigure{
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
config.ZombieDeletion.Enabled = true
config.ZombieDeletion.Interval = 500 * time.Millisecond
config.ZombieDeletion.AsOfSystemInterval = 0
},
},
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
@ -53,6 +53,7 @@ func TestZombieDeletion(t *testing.T) {
require.NoError(t, err)
defer ctx.Check(project.Close)
// upload pending object with multipart upload but without segment
_, err = project.BeginUpload(ctx, "testbucket2", "zombie_object_multipart_no_segment", nil)
require.NoError(t, err)

View File

@ -23,9 +23,10 @@ var (
// Config contains configurable values for expired segment cleanup.
type Config struct {
Interval time.Duration `help:"the time between each attempt to go through the db and clean up expired segments" releaseDefault:"24h" devDefault:"10s" testDefault:"$TESTINTERVAL"`
Enabled bool `help:"set if expired segment cleanup is enabled or not" releaseDefault:"true" devDefault:"true"`
ListLimit int `help:"how many expired objects to query in a batch" default:"100"`
Interval time.Duration `help:"the time between each attempt to go through the db and clean up expired segments" releaseDefault:"24h" devDefault:"10s" testDefault:"$TESTINTERVAL"`
Enabled bool `help:"set if expired segment cleanup is enabled or not" releaseDefault:"true" devDefault:"true"`
ListLimit int `help:"how many expired objects to query in a batch" default:"100"`
AsOfSystemInterval time.Duration `help:"as of system interval" releaseDefault:"-5m" devDefault:"-1us" testDefault:"-1us" hidden:"true"`
}
// Chore implements the expired segment cleanup chore.
@ -81,8 +82,9 @@ func (chore *Chore) deleteExpiredObjects(ctx context.Context) (err error) {
// TODO log error instead of crashing core until we will be sure
// that queries for deleting expired objects are stable
err = chore.metabase.DeleteExpiredObjects(ctx, metabase.DeleteExpiredObjects{
ExpiredBefore: chore.nowFn(),
BatchSize: chore.config.ListLimit,
ExpiredBefore: chore.nowFn(),
BatchSize: chore.config.ListLimit,
AsOfSystemInterval: chore.config.AsOfSystemInterval,
})
if err != nil {
chore.log.Error("deleting expired objects failed", zap.Error(err))

View File

@ -1171,6 +1171,9 @@ server.private-address: 127.0.0.1:7778
# server address to check its version against
# version.server-address: https://version.storj.io
# as of system interval
# zombie-deletion.as-of-system-interval: -5m0s
# set if zombie object cleanup is enabled or not
# zombie-deletion.enabled: true