Add detailed scanner trace output and notifications (#16668)

This commit is contained in:
Klaus Post 2023-02-21 18:33:33 +01:00 committed by GitHub
parent 8a08861dd9
commit fd6622458b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 153 additions and 26 deletions

View File

@ -1142,6 +1142,17 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
UserAgent: r.UserAgent(), UserAgent: r.UserAgent(),
Host: handlers.GetSourceIP(r), Host: handlers.GetSourceIP(r),
}) })
if objInfo.NumVersions > dataScannerExcessiveVersionsThreshold {
defer sendEvent(eventArgs{
EventName: event.ObjectManyVersions,
BucketName: objInfo.Bucket,
Object: objInfo,
ReqParams: extractReqParams(r),
RespElements: extractRespElements(w),
UserAgent: r.UserAgent(),
Host: handlers.GetSourceIP(r),
})
}
if redirectURL != nil { // success_action_redirect is valid and set. if redirectURL != nil { // success_action_redirect is valid and set.
v := redirectURL.Query() v := redirectURL.Query()

View File

@ -69,9 +69,9 @@ const (
// log scanner action. // log scanner action.
// Use for s > scannerMetricStartTrace // Use for s > scannerMetricStartTrace
func (p *scannerMetrics) log(s scannerMetric, paths ...string) func() { func (p *scannerMetrics) log(s scannerMetric, paths ...string) func(custom map[string]string) {
startTime := time.Now() startTime := time.Now()
return func() { return func(custom map[string]string) {
duration := time.Since(startTime) duration := time.Since(startTime)
atomic.AddUint64(&p.operations[s], 1) atomic.AddUint64(&p.operations[s], 1)
@ -80,7 +80,7 @@ func (p *scannerMetrics) log(s scannerMetric, paths ...string) func() {
} }
if s > scannerMetricStartTrace && globalTrace.NumSubscribers(madmin.TraceScanner) > 0 { if s > scannerMetricStartTrace && globalTrace.NumSubscribers(madmin.TraceScanner) > 0 {
globalTrace.Publish(scannerTrace(s, startTime, duration, strings.Join(paths, " "))) globalTrace.Publish(scannerTrace(s, startTime, duration, strings.Join(paths, " "), custom))
} }
} }
} }

View File

@ -29,6 +29,7 @@ import (
"math/rand" "math/rand"
"os" "os"
"path" "path"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -59,6 +60,9 @@ const (
healDeleteDangling = true healDeleteDangling = true
healFolderIncludeProb = 32 // Include a clean folder one in n cycles. healFolderIncludeProb = 32 // Include a clean folder one in n cycles.
healObjectSelectProb = 512 // Overall probability of a file being scanned; one in n. healObjectSelectProb = 512 // Overall probability of a file being scanned; one in n.
dataScannerExcessiveVersionsThreshold = 1000 // Issue a warning when a single object has more versions than this
dataScannerExcessiveFoldersThreshold = 50000 // Issue a warning when a folder has more subfolders than this in a *set*
) )
var ( var (
@ -207,7 +211,11 @@ func runDataScanner(ctx context.Context, objAPI ObjectLayer) {
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
err = objAPI.NSScanner(ctx, bf, results, uint32(cycleInfo.current), scanMode) err = objAPI.NSScanner(ctx, bf, results, uint32(cycleInfo.current), scanMode)
logger.LogIf(ctx, err) logger.LogIf(ctx, err)
stopFn() res := map[string]string{"cycle": fmt.Sprint(cycleInfo.current)}
if err != nil {
res["error"] = err.Error()
}
stopFn(res)
if err == nil { if err == nil {
// Store new cycle... // Store new cycle...
cycleInfo.next++ cycleInfo.next++
@ -566,6 +574,19 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
len(existingFolders)+len(newFolders) >= dataScannerCompactAtFolders || len(existingFolders)+len(newFolders) >= dataScannerCompactAtFolders ||
len(existingFolders)+len(newFolders) >= dataScannerForceCompactAtFolders len(existingFolders)+len(newFolders) >= dataScannerForceCompactAtFolders
if len(existingFolders)+len(newFolders) > dataScannerExcessiveFoldersThreshold {
// Notify object accessed via a GET request.
sendEvent(eventArgs{
EventName: event.PrefixManyFolders,
BucketName: f.root,
Object: ObjectInfo{
Name: strings.TrimSuffix(folder.name, "/") + "/",
Size: int64(len(existingFolders) + len(newFolders)),
},
UserAgent: "scanner",
Host: globalMinioHost,
})
}
if !into.Compacted && shouldCompact { if !into.Compacted && shouldCompact {
into.Compacted = true into.Compacted = true
newFolders = append(newFolders, existingFolders...) newFolders = append(newFolders, existingFolders...)
@ -632,7 +653,8 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
f.updateCurrentPath(folder.name) f.updateCurrentPath(folder.name)
stopFn := globalScannerMetrics.log(scannerMetricScanFolder, f.root, folder.name) stopFn := globalScannerMetrics.log(scannerMetricScanFolder, f.root, folder.name)
scanFolder(folder) scanFolder(folder)
stopFn() stopFn(map[string]string{"type": "new"})
// Add new folders if this is new and we don't have existing. // Add new folders if this is new and we don't have existing.
if !into.Compacted { if !into.Compacted {
parent := f.updateCache.find(thisHash.Key()) parent := f.updateCache.find(thisHash.Key())
@ -661,9 +683,9 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
} }
} }
f.updateCurrentPath(folder.name) f.updateCurrentPath(folder.name)
stopFn := globalScannerMetrics.log(scannerMetricScanFolder, f.root, folder.name, "EXISTING") stopFn := globalScannerMetrics.log(scannerMetricScanFolder, f.root, folder.name)
scanFolder(folder) scanFolder(folder)
stopFn() stopFn(map[string]string{"type": "existing"})
} }
// Scan for healing // Scan for healing
@ -803,7 +825,7 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
this := cachedFolder{name: k, parent: &thisHash, objectHealProbDiv: 1} this := cachedFolder{name: k, parent: &thisHash, objectHealProbDiv: 1}
stopFn := globalScannerMetrics.log(scannerMetricScanFolder, f.root, this.name, "HEALED") stopFn := globalScannerMetrics.log(scannerMetricScanFolder, f.root, this.name, "HEALED")
scanFolder(this) scanFolder(this)
stopFn() stopFn(map[string]string{"type": "healed"})
} }
} }
break break
@ -817,9 +839,6 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
flat.Compacted = true flat.Compacted = true
var compact bool var compact bool
if flat.Objects < dataScannerCompactLeastObject { if flat.Objects < dataScannerCompactLeastObject {
if flat.Objects > 1 {
globalScannerMetrics.log(scannerMetricCompactFolder, folder.name)
}
compact = true compact = true
} else { } else {
// Compact if we only have objects as children... // Compact if we only have objects as children...
@ -832,13 +851,20 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
} }
} }
} }
if compact {
globalScannerMetrics.log(scannerMetricCompactFolder, folder.name)
}
} }
if compact { if compact {
stop := globalScannerMetrics.log(scannerMetricCompactFolder, folder.name)
f.newCache.deleteRecursive(thisHash) f.newCache.deleteRecursive(thisHash)
f.newCache.replaceHashed(thisHash, folder.parent, *flat) f.newCache.replaceHashed(thisHash, folder.parent, *flat)
total := map[string]string{
"objects": fmt.Sprint(flat.Objects),
"size": fmt.Sprint(flat.Size),
}
if flat.Versions > 0 {
total["versions"] = fmt.Sprint(flat.Versions)
}
stop(total)
} }
} }
@ -1082,8 +1108,27 @@ func (i *scannerItem) applyVersionActions(ctx context.Context, o ObjectLayer, fi
} }
} }
} }
fivs, err := i.applyNewerNoncurrentVersionLimit(ctx, o, fivs)
if err != nil {
return fivs, err
}
return i.applyNewerNoncurrentVersionLimit(ctx, o, fivs) // Check if we have many versions after applyNewerNoncurrentVersionLimit.
if len(fivs) > dataScannerExcessiveVersionsThreshold {
// Notify object accessed via a GET request.
sendEvent(eventArgs{
EventName: event.ObjectManyVersions,
BucketName: i.bucket,
Object: ObjectInfo{
Name: i.objectPath(),
},
UserAgent: "scanner",
Host: globalMinioHost,
RespElements: map[string]string{"x-minio-versions": strconv.Itoa(len(fivs))},
})
}
return fivs, nil
} }
// applyActions will apply lifecycle checks on to a scanned item. // applyActions will apply lifecycle checks on to a scanned item.

View File

@ -66,7 +66,7 @@ func (fi FileInfo) DataShardFixed() bool {
return fi.Metadata[reservedMetadataPrefixLowerDataShardFix] == "true" return fi.Metadata[reservedMetadataPrefixLowerDataShardFix] == "true"
} }
// Heals a bucket if it doesn't exist on one of the disks, additionally // HealBucket heals a bucket if it doesn't exist on one of the disks, additionally
// also heals the missing entries for bucket metadata files // also heals the missing entries for bucket metadata files
// `policy.json, notification.xml, listeners.json`. // `policy.json, notification.xml, listeners.json`.
func (er erasureObjects) HealBucket(ctx context.Context, bucket string, opts madmin.HealOpts) ( func (er erasureObjects) HealBucket(ctx context.Context, bucket string, opts madmin.HealOpts) (
@ -1062,7 +1062,12 @@ func healTrace(funcName healingMetric, startTime time.Time, bucket, object strin
Path: pathJoin(bucket, decodeDirObject(object)), Path: pathJoin(bucket, decodeDirObject(object)),
} }
if opts != nil { if opts != nil {
tr.Message = fmt.Sprintf("dry:%v, rm:%v, recreate:%v mode:%v", opts.DryRun, opts.Remove, opts.Recreate, opts.ScanMode) tr.Custom = map[string]string{
"dry": fmt.Sprint(opts.DryRun),
"remove": fmt.Sprint(opts.Remove),
"recreate": fmt.Sprint(opts.Recreate),
"mode": fmt.Sprint(opts.ScanMode),
}
} }
if err != nil { if err != nil {
tr.Error = err.Error() tr.Error = err.Error()

View File

@ -1918,7 +1918,7 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
writeSuccessResponseHeadersOnly(w) writeSuccessResponseHeadersOnly(w)
// Notify object created event. // Notify object created event.
sendEvent(eventArgs{ evt := eventArgs{
EventName: event.ObjectCreatedPut, EventName: event.ObjectCreatedPut,
BucketName: bucket, BucketName: bucket,
Object: objInfo, Object: objInfo,
@ -1926,7 +1926,12 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
RespElements: extractRespElements(w), RespElements: extractRespElements(w),
UserAgent: r.UserAgent(), UserAgent: r.UserAgent(),
Host: handlers.GetSourceIP(r), Host: handlers.GetSourceIP(r),
}) }
sendEvent(evt)
if objInfo.NumVersions > dataScannerExcessiveVersionsThreshold {
evt.EventName = event.ObjectManyVersions
sendEvent(evt)
}
// Remove the transitioned object whose object version is being overwritten. // Remove the transitioned object whose object version is being overwritten.
if !globalTierConfigMgr.Empty() { if !globalTierConfigMgr.Empty() {

View File

@ -1019,7 +1019,7 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
writeSuccessResponseXML(w, encodedSuccessResponse) writeSuccessResponseXML(w, encodedSuccessResponse)
// Notify object created event. // Notify object created event.
sendEvent(eventArgs{ evt := eventArgs{
EventName: event.ObjectCreatedCompleteMultipartUpload, EventName: event.ObjectCreatedCompleteMultipartUpload,
BucketName: bucket, BucketName: bucket,
Object: objInfo, Object: objInfo,
@ -1027,7 +1027,13 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
RespElements: extractRespElements(w), RespElements: extractRespElements(w),
UserAgent: r.UserAgent(), UserAgent: r.UserAgent(),
Host: handlers.GetSourceIP(r), Host: handlers.GetSourceIP(r),
}) }
sendEvent(evt)
if objInfo.NumVersions > dataScannerExcessiveVersionsThreshold {
evt.EventName = event.ObjectManyVersions
sendEvent(evt)
}
// Remove the transitioned object whose object version is being overwritten. // Remove the transitioned object whose object version is being overwritten.
if !globalTierConfigMgr.Empty() { if !globalTierConfigMgr.Empty() {

View File

@ -555,7 +555,7 @@ func storageTrace(s storageMetric, startTime time.Time, duration time.Duration,
} }
} }
func scannerTrace(s scannerMetric, startTime time.Time, duration time.Duration, path string) madmin.TraceInfo { func scannerTrace(s scannerMetric, startTime time.Time, duration time.Duration, path string, custom map[string]string) madmin.TraceInfo {
return madmin.TraceInfo{ return madmin.TraceInfo{
TraceType: madmin.TraceScanner, TraceType: madmin.TraceScanner,
Time: startTime, Time: startTime,
@ -563,6 +563,7 @@ func scannerTrace(s scannerMetric, startTime time.Time, duration time.Duration,
FuncName: "scanner." + s.String(), FuncName: "scanner." + s.String(),
Duration: duration, Duration: duration,
Path: path, Path: path,
Custom: custom,
} }
} }

View File

@ -492,11 +492,15 @@ func (s *xlStorage) NSScanner(ctx context.Context, cache dataUsageCache, updates
return sizeSummary{}, errSkipFile return sizeSummary{}, errSkipFile
} }
stopFn := globalScannerMetrics.log(scannerMetricScanObject, s.diskPath, pathJoin(item.bucket, item.objectPath())) stopFn := globalScannerMetrics.log(scannerMetricScanObject, s.diskPath, pathJoin(item.bucket, item.objectPath()))
defer stopFn() res := make(map[string]string, 8)
defer func() {
stopFn(res)
}()
doneSz := globalScannerMetrics.timeSize(scannerMetricReadMetadata) doneSz := globalScannerMetrics.timeSize(scannerMetricReadMetadata)
buf, err := s.readMetadata(ctx, item.Path) buf, err := s.readMetadata(ctx, item.Path)
doneSz(len(buf)) doneSz(len(buf))
res["metasize"] = fmt.Sprint(len(buf))
if err != nil { if err != nil {
if intDataUpdateTracker.debug { if intDataUpdateTracker.debug {
console.Debugf(color.Green("scannerBucket:")+" object path missing: %v: %w\n", item.Path, err) console.Debugf(color.Green("scannerBucket:")+" object path missing: %v: %w\n", item.Path, err)
@ -515,6 +519,7 @@ func (s *xlStorage) NSScanner(ctx context.Context, cache dataUsageCache, updates
} }
return sizeSummary{}, errSkipFile return sizeSummary{}, errSkipFile
} }
sizeS := sizeSummary{} sizeS := sizeSummary{}
var noTiers bool var noTiers bool
if noTiers = globalTierConfigMgr.Empty(); !noTiers { if noTiers = globalTierConfigMgr.Empty(); !noTiers {
@ -560,12 +565,45 @@ func (s *xlStorage) NSScanner(ctx context.Context, cache dataUsageCache, updates
} }
// apply tier sweep action on free versions // apply tier sweep action on free versions
if len(fivs.FreeVersions) > 0 {
res["free-versions"] = fmt.Sprint(len(fivs.FreeVersions))
}
for _, freeVersion := range fivs.FreeVersions { for _, freeVersion := range fivs.FreeVersions {
oi := freeVersion.ToObjectInfo(item.bucket, item.objectPath(), versioned) oi := freeVersion.ToObjectInfo(item.bucket, item.objectPath(), versioned)
done = globalScannerMetrics.time(scannerMetricTierObjSweep) done = globalScannerMetrics.time(scannerMetricTierObjSweep)
item.applyTierObjSweep(ctx, objAPI, oi) item.applyTierObjSweep(ctx, objAPI, oi)
done() done()
} }
// These are rather expensive. Skip if nobody listens.
if globalTrace.NumSubscribers(madmin.TraceScanner) > 0 {
if sizeS.versions > 0 {
res["versions"] = fmt.Sprint()
}
res["size"] = fmt.Sprint(sizeS.totalSize)
if len(sizeS.tiers) > 0 {
for name, tier := range sizeS.tiers {
res["size-"+name] = fmt.Sprint(tier.TotalSize)
res["versions-"+name] = fmt.Sprint(tier.NumVersions)
}
}
if sizeS.failedCount > 0 {
res["repl-failed"] = fmt.Sprintf("%d versions, %d bytes", sizeS.failedCount, sizeS.failedSize)
}
if sizeS.pendingCount > 0 {
res["repl-pending"] = fmt.Sprintf("%d versions, %d bytes", sizeS.pendingCount, sizeS.pendingSize)
}
for tgt, st := range sizeS.replTargetStats {
res["repl-size-"+tgt] = fmt.Sprint(st.replicatedSize)
if st.failedCount > 0 {
res["repl-failed-"+tgt] = fmt.Sprintf("%d versions, %d bytes", st.failedCount, st.failedSize)
}
if st.pendingCount > 0 {
res["repl-pending-"+tgt] = fmt.Sprintf("%d versions, %d bytes", st.pendingCount, st.pendingSize)
}
}
}
return sizeS, nil return sizeS, nil
}, scanMode) }, scanMode)
if err != nil { if err != nil {

2
go.mod
View File

@ -48,7 +48,7 @@ require (
github.com/minio/dperf v0.4.2 github.com/minio/dperf v0.4.2
github.com/minio/highwayhash v1.0.2 github.com/minio/highwayhash v1.0.2
github.com/minio/kes-go v0.1.0 github.com/minio/kes-go v0.1.0
github.com/minio/madmin-go/v2 v2.0.12 github.com/minio/madmin-go/v2 v2.0.13-0.20230220143547-e6641ef0b8d5
github.com/minio/minio-go/v7 v7.0.48 github.com/minio/minio-go/v7 v7.0.48
github.com/minio/mux v1.9.0 github.com/minio/mux v1.9.0
github.com/minio/pkg v1.6.1 github.com/minio/pkg v1.6.1

4
go.sum
View File

@ -776,8 +776,8 @@ github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLT
github.com/minio/kes-go v0.1.0 h1:h201DyOYP5sTqajkxFGxmXz/kPbT8HQNX1uh3Yx2PFc= github.com/minio/kes-go v0.1.0 h1:h201DyOYP5sTqajkxFGxmXz/kPbT8HQNX1uh3Yx2PFc=
github.com/minio/kes-go v0.1.0/go.mod h1:VorHLaIYis9/MxAHAtXN4d8PUMNKhIxTIlvFt0hBOEo= github.com/minio/kes-go v0.1.0/go.mod h1:VorHLaIYis9/MxAHAtXN4d8PUMNKhIxTIlvFt0hBOEo=
github.com/minio/madmin-go v1.6.6/go.mod h1:ATvkBOLiP3av4D++2v1UEHC/QzsGtgXD5kYvvRYzdKs= github.com/minio/madmin-go v1.6.6/go.mod h1:ATvkBOLiP3av4D++2v1UEHC/QzsGtgXD5kYvvRYzdKs=
github.com/minio/madmin-go/v2 v2.0.12 h1:3PKFAhiZOMjeShxhtY10INw1kDrY/2VwH55FAat3hM4= github.com/minio/madmin-go/v2 v2.0.13-0.20230220143547-e6641ef0b8d5 h1:IZa7nXFFDdOigg2wufK51C6oTB5q9c6LCV969u3L0YY=
github.com/minio/madmin-go/v2 v2.0.12/go.mod h1:5aFi/VLWBHC2DEFfGIlUmAeJhaF4ZAjuYpEWZFU14Zw= github.com/minio/madmin-go/v2 v2.0.13-0.20230220143547-e6641ef0b8d5/go.mod h1:5aFi/VLWBHC2DEFfGIlUmAeJhaF4ZAjuYpEWZFU14Zw=
github.com/minio/mc v0.0.0-20230216192011-54e2edd1be94 h1:umgaS1OMD7qHLca/GH/eoox8RL44tQenBp34ZV1aWkU= github.com/minio/mc v0.0.0-20230216192011-54e2edd1be94 h1:umgaS1OMD7qHLca/GH/eoox8RL44tQenBp34ZV1aWkU=
github.com/minio/mc v0.0.0-20230216192011-54e2edd1be94/go.mod h1:4tBWo9BLlIUajqXqo5HdxxpQAdXktlOLSVvHaHFNYjE= github.com/minio/mc v0.0.0-20230216192011-54e2edd1be94/go.mod h1:4tBWo9BLlIUajqXqo5HdxxpQAdXktlOLSVvHaHFNYjE=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=

View File

@ -57,6 +57,8 @@ const (
ObjectRestoreCompleted ObjectRestoreCompleted
ObjectTransitionFailed ObjectTransitionFailed
ObjectTransitionComplete ObjectTransitionComplete
ObjectManyVersions
PrefixManyFolders
objectSingleTypesEnd objectSingleTypesEnd
// Start Compound types that require expansion: // Start Compound types that require expansion:
@ -67,6 +69,7 @@ const (
ObjectReplicationAll ObjectReplicationAll
ObjectRestoreAll ObjectRestoreAll
ObjectTransitionAll ObjectTransitionAll
ObjectScannerAll
Everything Everything
) )
@ -113,6 +116,11 @@ func (name Name) Expand() []Name {
ObjectTransitionFailed, ObjectTransitionFailed,
ObjectTransitionComplete, ObjectTransitionComplete,
} }
case ObjectScannerAll:
return []Name{
ObjectManyVersions,
PrefixManyFolders,
}
case Everything: case Everything:
res := make([]Name, objectSingleTypesEnd-1) res := make([]Name, objectSingleTypesEnd-1)
for i := range res { for i := range res {
@ -202,6 +210,10 @@ func (name Name) String() string {
return "s3:ObjectTransition:Failed" return "s3:ObjectTransition:Failed"
case ObjectTransitionComplete: case ObjectTransitionComplete:
return "s3:ObjectTransition:Complete" return "s3:ObjectTransition:Complete"
case ObjectManyVersions:
return "s3:Scanner:ManyVersions"
case PrefixManyFolders:
return "s3:Scanner:BigPrefix"
} }
return "" return ""
@ -314,6 +326,10 @@ func ParseName(s string) (Name, error) {
return ObjectTransitionComplete, nil return ObjectTransitionComplete, nil
case "s3:ObjectTransition:*": case "s3:ObjectTransition:*":
return ObjectTransitionAll, nil return ObjectTransitionAll, nil
case "s3:Scanner:ManyVersions":
return ObjectManyVersions, nil
case "s3:Scanner:BigPrefix":
return PrefixManyFolders, nil
default: default:
return 0, &ErrInvalidEventName{s} return 0, &ErrInvalidEventName{s}
} }