-
Notifications
You must be signed in to change notification settings - Fork 764
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
Enable audit to write cache to disk to reduce memory #1634
Conversation
Codecov Report
@@ Coverage Diff @@
## master #1634 +/- ##
==========================================
- Coverage 52.81% 52.14% -0.67%
==========================================
Files 98 98
Lines 8591 8693 +102
==========================================
- Hits 4537 4533 -4
- Misses 3695 3800 +105
- Partials 359 360 +1
Flags with carried forward coverage won't be shown. Click here to find out more.
Continue to review full report at Codecov.
|
pkg/audit/manager.go
Outdated
} | ||
resp, err := am.opa.Review(ctx, augmentedObj) | ||
// for each item, write object to a file along with the namespace | ||
fileName := fmt.Sprintf("%s_%d", objNamespace, index) |
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.
What purpose does embedding the namespace in the filename serve?
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.
From few lines above, since we are already calling the api server to get the namespace of the object to see if we can skip it to avoid having to create the file if namespace is excluded, we should persist the namespace as part of the filename so we do not have to make another api call later when loading the obj from disk.
objNamespace := objList.Items[index].GetNamespace()
isExcludedNamespace, err := am.skipExcludedNamespace(&objList.Items[index])
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.
Two problems with this benefit:
- We need to get the full namespace object so that we can evaluate namespace selectors, not the name
- We are already caching the namespace in nsCache
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.
From few lines below, the namespace saved as part of the file name is used to lookup the full namespace object from nsCache:
objNs := strings.Split(fileName, "_")[0]
ns := corev1.Namespace{}
if objNs != "" {
ns, err = nsCache.Get(ctx, am.client, objNs)
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.
nit: Can we encapsulate the encoding and decoding logic for these filenames into helper functions, making this naming convention more explicit?
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.
I'm removing the namespace from the filename but prefixing with kind to avoid delays in os.RemoveAll.
@@ -403,13 +414,71 @@ func (am *Manager) auditResources( | |||
} | |||
} | |||
} | |||
// loop thru each subDir in output dir to get files | |||
for i := 0; i < folderCount; i++ { |
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.
Actually, it looks like we are auditing after we've looped through all GVKs, we'd definitely need to write out objects to separate directories by GVK in order to avoid clobbering.
One note about how we could be gentler on the API server...
If we perform the audit for a given kind before grabbing the next kind from the API server, we even out the load on the API server somewhat.
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.
Per above comment, I dont think we are clobbering it.
If we perform the audit for a given kind before grabbing the next kind from the API server, we even out the load on the API server somewhat.
Can this optimization be a follow up PR given that we would need to do few rounds of load tests?
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.
Yeah, you're not clobbering it.
One other benefit of interleaving audits and lists... you can wipe the disk after each audit, lowering the maximum amount of disk space used.
Why would we need load tests?
In any case, we can defer, but the reduction in disk space seems valuable (akin to how much memory we got back when we switched to auditing on a per-kind basis)
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.
latest commit audits right after cache to disk for a given kind
pkg/audit/manager.go
Outdated
} | ||
for _, f := range files { | ||
fileName := f.Name() | ||
objNs := strings.Split(fileName, "_")[0] |
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.
If this is why we are embedding the namespace in the filename, could we retrieve the namespace after we've called am.readUnstructured()
and get it from the object itself?
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.
Per above comment, we are already making the api call to get namespace to test ns exclusion prior to saving the file. It makes more sense to me to save it as part of the file at that point instead.
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.
Except we are calling nsCache.Get()
here anyway? Per above, we'd need to get the whole namespace object so we can evaluate the namespace selector.
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.
done
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.
mostly nits
pkg/audit/manager.go
Outdated
} | ||
resp, err := am.opa.Review(ctx, augmentedObj) | ||
// for each item, write object to a file along with the namespace | ||
fileName := fmt.Sprintf("%s_%d", objNamespace, index) |
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.
Two problems with this benefit:
- We need to get the full namespace object so that we can evaluate namespace selectors, not the name
- We are already caching the namespace in nsCache
@@ -403,13 +414,71 @@ func (am *Manager) auditResources( | |||
} | |||
} | |||
} | |||
// loop thru each subDir in output dir to get files | |||
for i := 0; i < folderCount; i++ { |
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.
Yeah, you're not clobbering it.
One other benefit of interleaving audits and lists... you can wipe the disk after each audit, lowering the maximum amount of disk space used.
Why would we need load tests?
In any case, we can defer, but the reduction in disk space seems valuable (akin to how much memory we got back when we switched to auditing on a per-kind basis)
pkg/audit/manager.go
Outdated
} | ||
for _, f := range files { | ||
fileName := f.Name() | ||
objNs := strings.Split(fileName, "_")[0] |
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.
Except we are calling nsCache.Get()
here anyway? Per above, we'd need to get the whole namespace object so we can evaluate the namespace selector.
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.
@ritazh overall looks good! I had a few questions and nits, nothing blocking. LMKWYT.
I think a cool future enhancement might be to process the reviews concurrently to filling the cache - this might improve overall audit time for larger clusters.
pkg/audit/manager.go
Outdated
@@ -239,6 +243,15 @@ func (am *Manager) auditResources( | |||
totalViolationsPerConstraint map[util.KindVersionResource]int64, | |||
totalViolationsPerEnforcementAction map[util.EnforcementAction]int64, | |||
timestamp string) error { | |||
// delete all from cache dir before starting audit | |||
dir, err := os.ReadDir(*outputDir) |
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.
- Do we know how many files will be in this directory? Is it better to page through the items using
File.Readdirnames(n)
instead of making assumptions, or do we know this will be manageable? - Is there any validation we want to do to make sure it's actually our cache directory, and not
/
or some other unfortunate mistake?
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.
Do we know how many files will be in this directory?
latest commit adds paging to get files
Is there any validation we want to do to make sure it's actually our cache directory
we pass in apiCacheDir
to these functions.
apiCacheDir = flag.String("api-cache-dir", defaultApiCacheDir, "The directory where
pkg/audit/manager.go
Outdated
} | ||
resp, err := am.opa.Review(ctx, augmentedObj) | ||
// for each item, write object to a file along with the namespace | ||
fileName := fmt.Sprintf("%s_%d", objNamespace, index) |
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.
nit: Can we encapsulate the encoding and decoding logic for these filenames into helper functions, making this naming convention more explicit?
Signed-off-by: Rita Zhang <rita.z.zhang@gmail.com>
Signed-off-by: Rita Zhang <rita.z.zhang@gmail.com>
Signed-off-by: Rita Zhang <rita.z.zhang@gmail.com>
7a8cb30
to
0b074c4
Compare
@@ -12,7 +12,7 @@ enableDeleteOperations: false | |||
enableExternalData: false | |||
mutatingWebhookFailurePolicy: Ignore | |||
mutatingWebhookTimeoutSeconds: 3 | |||
auditChunkSize: 0 | |||
auditChunkSize: 500 |
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.
NOTE: updating default auditChunkSize
value
@@ -43,16 +46,18 @@ const ( | |||
msgSize = 256 | |||
defaultAuditInterval = 60 | |||
defaultConstraintViolationsLimit = 20 | |||
defaultListLimit = 0 | |||
defaultListLimit = 500 |
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.
NOTE: updated default to 500
and flag usage. Will open a separate PR to update doc after the next release is out.
0b074c4
to
77acf31
Compare
Signed-off-by: Rita Zhang <rita.z.zhang@gmail.com>
77acf31
to
8fd8a51
Compare
@maxsmythe @shomron I have addressed you comments, PTAL. |
defer dir.Close() | ||
for { | ||
names, err := dir.Readdirnames(batchSize) | ||
if err == io.EOF || len(names) == 0 { |
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.
I think we ignore non-EOF errors here. Should we bubble those back up to the caller?
pkg/audit/manager.go
Outdated
Namespace: &ns, | ||
} | ||
resp, err := am.opa.Review(ctx, augmentedObj) | ||
// for each item, write object to a file along with the namespace |
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.
Is this comment still accurate? Does it just mean that the namespace is stored within the file payload?
@@ -335,6 +347,14 @@ func (am *Manager) auditResources( | |||
for gv, gvKinds := range clusterAPIResources { | |||
kindsLoop: | |||
for kind := range gvKinds { | |||
// delete all existing folders from cache dir before starting next kind | |||
err := am.removeAllFromDir(*apiCacheDir, int(*auditChunkSize)) |
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.
Why did this move into the loop? Are we trying to reduce peak disk usage?
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.
I think so?
Could be useful if there are a lot of large lists of kinds.
|
||
func (am *Manager) reviewObjects(ctx context.Context, kind string, folderCount int, nsCache *nsCache, |
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.
Can we add a comment somewhere with the cache directory structure? Otherwise we need to infer this from the code.
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 was commented earlier when we create the sub folders.
// for each batch, create a parent folder
// prefix kind to avoid delays in removeall
subPath = fmt.Sprintf("%s_%d", kind, folderCount)
parentDir := path.Join(*apiCacheDir, subPath)
I will add the same comment in this func as well.
subDir := fmt.Sprintf("%s_%d", kind, i) | ||
pDir := path.Join(*apiCacheDir, subDir) | ||
|
||
files, err := am.getFilesFromDir(pDir, int(*auditChunkSize)) |
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.
While this pages files from the OS, it still aggregates the list in memory before processing. This should be fine unless we expect the list to be very large. Otherwise we could pass in a processing function to be called per page.
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 maybe walk the directory and call audit on every file we walk into?
walk()
is probably preferable, but I don't have an issue with this as-is, since the list of file names is probably much smaller than the list of actual 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.
I think the list of file names is pretty small compare to everything else (actual resources) we are caching. We can further optimize this if we see a dramatic improvement later.
@@ -335,6 +347,14 @@ func (am *Manager) auditResources( | |||
for gv, gvKinds := range clusterAPIResources { | |||
kindsLoop: | |||
for kind := range gvKinds { | |||
// delete all existing folders from cache dir before starting next kind | |||
err := am.removeAllFromDir(*apiCacheDir, int(*auditChunkSize)) |
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.
I think so?
Could be useful if there are a lot of large lists of kinds.
subDir := fmt.Sprintf("%s_%d", kind, i) | ||
pDir := path.Join(*apiCacheDir, subDir) | ||
|
||
files, err := am.getFilesFromDir(pDir, int(*auditChunkSize)) |
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 maybe walk the directory and call audit on every file we walk into?
walk()
is probably preferable, but I don't have an issue with this as-is, since the list of file names is probably much smaller than the list of actual objects.
Signed-off-by: Rita Zhang <rita.z.zhang@gmail.com>
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
QQ, I might be misreading but it looks like this only excludes from writing to disk objects in namespaces that are excluded by ProcessExcluder, which would just be the GK config-level excludedNamespaces? Could the map of matchedKinds also include a field for matchedNamespaces so as to also exclude those that don't match the namespaces defined in the constraint? And potentially use that info to winnow down the List api call? |
In theory, but that would presume that:
A second point: performance "optimizations" that lower resource coverage are less optimizations and more an acknowledgement of the limits of how far a system can scale. If I can only handle 1000 resources, and remove some from scope, the system can still only handle 1000 resources and I will still have a problem if I scale beyond that size WRT resources that are in scope. As such, performance improvements, graceful degradation, and removal of upper bounds (e.g. via chunking) should be the main focus, with culling more useful for triage and avoiding unnecessary processing time (as opposed to being an operational necessity). |
Thanks for the reply, that makes a lot of sense! I was looking at a case where a cluster had hundreds of thousands of a certain resource type, but none in the default namespace, and were applying the "shouldn't use the default namespace" template with "default" as the only included namespace on the constraint. Obviously a very niche case but it just spurred me to think about possible triages and their feasibility. Totally understand if it's not as worthwhile as actual optimizations! |
Signed-off-by: Rita Zhang rita.z.zhang@gmail.com
What this PR does / why we need it:
Improve audit memory footprint by writing audit cache to disk
Which issue(s) this PR fixes (optional, using
fixes #<issue number>(, fixes #<issue_number>, ...)
format, will close the issue(s) when the PR gets merged):Fixes #163
Fixes #1088
Fixes #1279
Partial #1405
Special notes for your reviewer: