Skip to content

writer: log once per flush window when LowLatencyBuffer overflows - #133

Open
akonradi wants to merge 1 commit into
Netflix:mainfrom
akonradi:log-dropped-metric-sample-on-overflow
Open

akonradi wants to merge 1 commit into
Netflix:mainfrom
akonradi:log-dropped-metric-sample-on-overflow

Conversation

@akonradi

Copy link
Copy Markdown

When reserveChunkForLine fails to reserve, if it is the first time that happens in a flush window, emit the name of the metric that was dropped as a hint for identifying problematic metrics. While it's not guaranteed that the logged metric name is itself the problematic one, if the same name is observing multiple times, that would be more suggestive.

When reserveChunkForLine fails to reserve, if it is the first time that
happens in a flush window, emit the name of the metric that was dropped
as a hint for identifying problematic metrics. While it's not guaranteed
that the logged metric name is itself the problematic one, if the same
name is observing multiple times, that would be more suggestive.
defer buffer.mu.Unlock()

// If there is no data to flush from the shard, then skip socket writes
if buffer.chunkIndex == 0 && len(buffer.data[0]) == 0 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This early return skips reporting and resetting overflows/overflowBytes/droppedSampleId. If the only thing that landed in a shard this window was an oversized line (the lineLen > chunkSize drop path), nothing is logged and no overflow metric is emitted, and the state carries over. In a later window with real drops, the log reports the merged count as "this window" and names the stale meter id from the earlier window. Since recordOverflow only samples when overflows == 0, the stale id is never replaced. Oversized lines are probably the main pathological case this is meant to diagnose. The overflow reporting and reset should happen before the early return.

if id, ok := meterIdKey(prefix); ok {
b.droppedSampleId = id
} else {
b.droppedSampleId = prefix

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If meterIdKey fails, the raw prefix is stored and later logged verbatim with no length limit and no escaping. A malformed line containing \n can split or forge log records, and a malformed line over 60KB (the oversized path) would be logged in full every window. Suggest truncating to a bounded length and formatting with %q.

func (b *bufferShard) recordOverflow(prefix string, totalWriteLength int) {
if b.overflows == 0 {
if id, ok := meterIdKey(prefix); ok {
b.droppedSampleId = id

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a substring of the caller's string, so it keeps the whole original allocation alive until the shard next reports (possibly indefinitely given the early-return issue in flushBufferShard). For a multi-MB oversized line that could be significant. strings.Clone of a truncated id would avoid it; it only happens once per window so the cost is negligible.


// Log a single overflow for each window.
if totalOverflows > 0 {
llb.logger.Infof("LowLatencyBuffer overflow in bufferSet=%s: dropped %d lines (%d bytes) this window; sample dropped meter id: %s", bufferSet, totalOverflows, totalOverflowBytes, droppeSampleId)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logs at Info on every flush window with no rate limit. Overflow is usually sustained (buffer too small for the app's volume), so with the default 5s interval that is ~17k lines/day per process. The overflows metric already signals the condition. Consider limiting to at most once per minute or similar.

}
if !b.reserveChunkForLine(len(prefix) + len(value)) {
// overflows (drops) are counted in reserveChunkForLine, for metric reporting
b.recordOverflow(prefix, len(prefix)+len(value))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Behavior change for the overflowBytes metric: previously the drop path in reserveChunkForLine that advanced chunkIndex counted totalWriteLength (including the +1 separator); now it counts len(prefix)+len(value). Small, but it silently shifts the metric, and the new test encodes the new semantics. Intentional?

llb.writer.WriteString(fmt.Sprintf("d:spectator-go.lowLatencyBuffer.overflowBytes,bufferSet=%s:%d", bufferSet, buffer.overflowBytes))
overflows, overflowBytes, sampleId := buffer.overflows, buffer.overflowBytes, buffer.droppedSampleId
if overflows > 0 {
llb.writer.WriteString(fmt.Sprintf("c:spectator-go.lowLatencyBuffer.overflows,bufferSet=%s:%d", bufferSet, overflows))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: swapAndFlush now has window-wide totals, but the overflow metrics are still written once per shard, up to 128 separate writes per window on a 64-CPU host during sustained overflow. Emitting the two aggregate lines once from swapAndFlush would reduce that to 2.

// "<type>[,<extra>]:<id>:<value>" line; the id is the substring between the
// first and last ':'. Returns ok=false for malformed lines (fewer than two ':'
// characters).
func meterIdKey(line string) (string, bool) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this repeats the first/last-colon parsing in orderSensitiveMeterIdKey and meter.idFromLinePrefix, with slightly different guards (first < 0 vs first <= 0). orderSensitiveMeterIdKey could check the type symbol and then delegate to this.

var bytesWritten int
var bytesWritten, totalOverflows int
var totalOverflowBytes int64
var droppeSampleId string

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: typo droppeSampleId, should be droppedSampleId.

}

type flushBufferResult struct {
bytesWritten,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: bytesWritten, on its own line reads as if it has a separate type; bytesWritten, overflows int or separate declarations would be clearer.

}
}

func TestLowLatencyBuffer_OverflowLogsOncePerFlushWindow(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only covers a single window. Worth adding a second swapAndFlush cycle with a different dropped id to verify the sample and counters reset, plus a case where the only line in a shard is an oversized one. Those would catch the early-return issue in flushBufferShard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants