Code change:
We should introduce a buffer (similar to what we have in the webhook output backend) to the log file output backend. Audit events should be queued on a relatively large buffer chanel, and an output go routine should grab events off the queue and write them to disk. If an audit event is output when the channel is full, it should be dropped (this should be counted by a prometheus metric, and an e2e test should ensure that the count is below some threshold). This behavior should be optional (configurable) /cc is tracking the regression
Naturally os will cache , these cached data will be written to disk either by fsync or pdflush periodically. What do you think is the most time-costing procedure in audit?
The problem is not with the time consumed. The problem is that currently write is performed synchronously with processing the request, which slows the request down, no matter how well caching works
The problem is that currently write is performed synchronously with processing the request Sure synchronously write does cost time serializing. Introduce buffer mechanism can really reduce api request latency time.
It doesn't matter that much if it happens asynchronously Indeed
Yes, this is a good point to improve performance.
Now I totally comprehend this. I am pleased to implement this buffer.
I'll be happy to review that
audit log is important sensitive information which we should try to make sure it reaches disk at all cost. Does this effort compromises that in favor of optimizing for latency ?
I'll make sure it's configurable and if the security is critical, you can setup synchronous write in your deployment. Having said that, it's critical to be able to accommodate the case where having 100% calls written does not compensate for increased latencies. Does it make sense?
thanks my preference would be to keep the default as no buffering when making it configurable. I didnt understand how does one setup "synchronous write in your deployment" and how that will affect audit logs reaching disk. My worry is mainly around a crash where the buffered audit logs dont reach the disk and are lost
if use buffer, audit events lost is inevitable when crash. As like webhook backend, still exist this risk. But to improve apiserver qps, something should comprise. By the way, we still reserve the synchronous write mode, which is configurable.
To make it simple, when offered trade-off, people are more likely to give up audit logs reliability than performance. Makes sense?
i am not against this change. i think what would be best is to make the default as no buffering and enable buffering through a parameter. Also for Enterprises, the trade off is not correct, they would prefer to NOT give up audit logs
I understand what you're suggesting and I don't agree :) Switching to buffering will for sure increase the latency, but will only increase the probability of losing logs, which will never be zero. I'm pretty sure we need to have buffered mode by default. However, since this argument is about making a trade-off and since neither of us represents quorum, let's ask more people for their opinion What do you think?
The default is slightly less important here since audit logging as a whole is not enabled by default. If we do turn it on by default, it becomes more important. That said, my hunch is that we should default to the mode with lower performance impact, but I don't have strong feelings either way.
As long as there are options to turn it on of off (the synchronous write) I'm fine with either default. Still, I'd like to see due diligence towards ensuring low lost rate when using buffered write, and sufficient documentation around caveats of using one or the other. Audit, from my personal experience, is crucial for majority of enterprises and they won't tolerate not even single lost event.
That is not possible, there will always be a situation when an audit event can be lost: sudden power loss, disk/network failure, random bit flip, etc. The questions is whether 10^(-6) is low enough loss probability, or we need to get 10^(-14) by sacrificing performance (arbitrary numbers, just to illustrate my point). Note, that reduced performance can increase the number of timeouts and retries, increasing load and leading to failures and even more lost events.
I think this is an important bug that we're not testing audit logging at scale
Marking it as urgent, since it's blocking
If split into 3 prs, I must do it one by one . Now first one is up .
SGTM Status of this issue: ETA: 1/12/2017 Risks: Complicated fix required
has been reviewed for almost 2 months and has not done. If I raise 3 prs to replace it, I am still worried whether all of them can be merged. They act as a whole.
Now I'm going to devote my time to it, we'll do it
OK.
In progress, reviewed first PR, waiting for applying changes ETA: Dec 01
Per Exception discussion, this portion of the feature is being removed from the milestone. As such, changing the priority. /priority important-soon /remove-priority critical-urgent
[MILESTONENOTIFIER] Milestone Removed From Issue Important: Code freeze is in effect and only issues with may remain in the v1.9 milestone. <details<summaryHelp</summary<ul<li<a href=""Additional instructions</a</li<li<a href=""Commands for setting labels</a</li</ul</details

Related discussion:
text: <ins> /* Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package buffered import ( "fmt" "sync" "time" "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/wait" auditinternal "k8s.io/apiserver/pkg/apis/audit" "k8s.io/apiserver/pkg/audit" "k8s.io/client-go/util/flowcontrol" ) // The plugin name reported in error metrics. const pluginName = "buffered" const ( // Default configuration values for ModeBatch. defaultBatchBufferSize = 10000            // Buffer up to 10000 events before starting discarding. defaultBatchMaxSize    = 400              // Only send up to 400 events at a time. defaultBatchMaxWait    = 30 * time.Second // Send events at least twice a minute. defaultBatchThrottleQPS   = 10 // Limit the send rate by 10 QPS. defaultBatchThrottleBurst = 15 // Allow up to 15 QPS burst. ) // BatchConfig represents batching delegate audit backend configuration. type BatchConfig struct { // BufferSize defines a size of the buffering queue. BufferSize int // MaxBatchSize defines maximum size of a batch. MaxBatchSize int // MaxBatchWait indicates the maximum interval between two batches. MaxBatchWait time.Duration // ThrottleEnable defines whether throttling will be applied to the batching process. ThrottleEnable bool // ThrottleQPS defines the allowed rate of batches per second sent to the delegate backend. ThrottleQPS float32 // ThrottleBurst defines the maximum rate of batches per second sent to the delegate backend in case // the capacity defined by ThrottleQPS was not utilized. ThrottleBurst int } // NewDefaultBatchConfig returns new Config objects populated by default values. func NewDefaultBatchConfig() BatchConfig { return BatchConfig{ BufferSize:   defaultBatchBufferSize, MaxBatchSize: defaultBatchMaxSize, MaxBatchWait: defaultBatchMaxWait, ThrottleEnable: true, ThrottleQPS:    defaultBatchThrottleQPS, ThrottleBurst:  defaultBatchThrottleBurst, } } type bufferedBackend struct { // The delegate backend that actually exports events. delegateBackend audit.Backend // Channel to buffer events before sending to the delegate backend. buffer chan *auditinternal.Event // Maximum number of events in a batch sent to the delegate backend. maxBatchSize int // Amount of time to wait after sending a batch to the delegate backend before sending another one. // // Receiving maxBatchSize events will always trigger sending a batch, regardless of the amount of time passed. maxBatchWait time.Duration // Channel to signal that the batching routine has processed all remaining events and exited. // Once `shutdownCh` is closed no new events will be sent to the delegate backend. shutdownCh chan struct{} // WaitGroup to control the concurrency of sending batches to the delegate backend. // Worker routine calls Add before sending a batch and // then spawns a routine that calls Done after batch was processed by the delegate backend. // This WaitGroup is used to wait for all sending routines to finish before shutting down audit backend. wg sync.WaitGroup // Limits the number of batches sent to the delegate backend per second. throttle flowcontrol.RateLimiter } var _ audit.Backend = &bufferedBackend{} // NewBackend returns a buffered audit backend that wraps delegate backend. func NewBackend(delegate audit.Backend, config BatchConfig) audit.Backend { var throttle flowcontrol.RateLimiter if config.ThrottleEnable { throttle = flowcontrol.NewTokenBucketRateLimiter(config.ThrottleQPS, config.ThrottleBurst) } return &bufferedBackend{ delegateBackend: delegate, buffer:          make(chan *auditinternal.Event, config.BufferSize), maxBatchSize:    config.MaxBatchSize, maxBatchWait:    config.MaxBatchWait, shutdownCh:      make(chan struct{}), wg:              sync.WaitGroup{}, throttle:        throttle, } } func (b *bufferedBackend) Run(stopCh <-chan struct{}) error { go func() { // Signal that the working routine has exited. defer close(b.shutdownCh) b.processIncomingEvents(stopCh) // Handle the events that were received after the last buffer // scraping and before this line. Since the buffer is closed, no new // events will come through. allEventsProcessed := false timer := make(chan time.Time) for !allEventsProcessed { allEventsProcessed = func() bool { // Recover from any panic in order to try to process all remaining events. // Note, that in case of a panic, the return value will be false and // the loop execution will continue. defer runtime.HandleCrash() events := b.collectEvents(timer, wait.NeverStop) b.processEvents(events) return len(events) == 0 }() } }() return b.delegateBackend.Run(stopCh) } // Shutdown blocks until stopCh passed to the Run method is closed and all // events added prior to that moment are batched and sent to the delegate backend. func (b *bufferedBackend) Shutdown() { // Wait until the routine spawned in Run method exits. <-b.shutdownCh // Wait until all sending routines exit. b.wg.Wait() b.delegateBackend.Shutdown() } // processIncomingEvents runs a loop that collects events from the buffer. When // b.stopCh is closed, processIncomingEvents stops and closes the buffer. func (b *bufferedBackend) processIncomingEvents(stopCh <-chan struct{}) { defer close(b.buffer) t := time.NewTimer(b.maxBatchWait) defer t.Stop() for { func() { // Recover from any panics caused by this function so a panic in the // goroutine can't bring down the main routine. defer runtime.HandleCrash() t.Reset(b.maxBatchWait) b.processEvents(b.collectEvents(t.C, stopCh)) }() select { case <-stopCh: return default: } } } // collectEvents attempts to collect some number of events in a batch. // // The following things can cause collectEvents to stop and return the list // of events: // //   * Maximum number of events for a batch. //   * Timer has passed. //   * Buffer channel is closed and empty. //   * stopCh is closed. func (b *bufferedBackend) collectEvents(timer <-chan time.Time, stopCh <-chan struct{}) []*auditinternal.Event { var events []*auditinternal.Event L: for i := 0; i < b.maxBatchSize; i++ { select { case ev, ok := <-b.buffer: // Buffer channel was closed and no new events will follow. if !ok { break L } events = append(events, ev) case <-timer: // Timer has expired. Send currently accumulated batch. break L case <-stopCh: // Backend has been stopped. Send currently accumulated batch. break L } } return events } // processEvents process the batch events in a goroutine using delegateBackend's ProcessEvents. func (b *bufferedBackend) processEvents(events []*auditinternal.Event) { if len(events) == 0 { return } // TODO(audit): Should control the number of active goroutines // if one goroutine takes 5 seconds to finish, the number of goroutines can be 5 * defaultBatchThrottleQPS if b.throttle != nil { b.throttle.Accept() } b.wg.Add(1) go func() { defer b.wg.Done() defer runtime.HandleCrash() // Execute the real processing in a goroutine to keep it from blocking. // This lets the batching routine continue draining the queue immediately. b.delegateBackend.ProcessEvents(events...) }() } func (b *bufferedBackend) ProcessEvents(ev ...*auditinternal.Event) { // The following mechanism is in place to support the situation when audit // events are still coming after the backend was stopped. var sendErr error var evIndex int // If the delegateBackend was shutdown and the buffer channel was closed, an // attempt to add an event to it will result in panic that we should // recover from. defer func() { if err := recover(); err != nil { sendErr = fmt.Errorf("panic when processing events: %v", err) } if sendErr != nil { audit.HandlePluginError(pluginName, sendErr, ev[evIndex:]...) } }() for i, e := range ev { evIndex = i // Per the audit.Backend interface these events are reused after being // sent to the Sink. Deep copy and send the copy to the queue. event := e.DeepCopy() select { case b.buffer <- event: default: sendErr = fmt.Errorf("audit buffer queue blocked") return } } } </ins>