github.com/mrgossett/heapster@v0.18.2/store/statstore/stat_store.go (about)

     1  // Copyright 2015 Google Inc. All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package statstore
    16  
    17  import (
    18  	"container/list"
    19  	"fmt"
    20  	"math"
    21  	"sort"
    22  	"sync"
    23  	"time"
    24  )
    25  
    26  // StatStore is an in-memory rolling timeseries window that allows the extraction of -
    27  // segments of the stored timeseries and derived stats.
    28  // StatStore only retains information about the latest TimePoints that are within its
    29  // specified duration.
    30  
    31  // The tradeoff between space and precision can be configured through the epsilon and resolution -
    32  // parameters.
    33  // @epsilon: the acceptable error margin, in absolute value, such as 1024 bytes.
    34  // @resolution: the desired resolution of the StatStore, such as 2 * time.Minute
    35  
    36  // Compression in the StatStore is performed by storing values of consecutive time resolutions -
    37  // that differ less than epsilon in the same "bucket", represented by the tpBucket struct.
    38  
    39  // For example, a timeseries may be represented in the following manner.
    40  // Each line parallel to the x-axis represents a single tpBucket.
    41  // This example leads to 4 tpBuckets being stored in the StatStore, even though the length of the
    42  // window is 7 resolutions.
    43  //
    44  // Legend:
    45  //	ε : epsilon
    46  //	δ : resolution
    47  //      W : window length
    48  //
    49  //        value ^
    50  //		|
    51  //		|
    52  //	     4ε |      	 ----
    53  //	     	|	 |  |
    54  //	     2ε |--------|  |		 |----|
    55  //	      ε	|	    |------------|
    56  //		|_____________________________|______>
    57  //					       W   time
    58  //		|--------|--|------------|----|
    59  //		   2δ     δ       3δ        δ
    60  
    61  // StatStore assumes that values are inserted in a chronologically ascending order through the -
    62  // Put method. If a TimePoint with a past Timestamp is inserted, it is ignored.
    63  // The last one resolution's worth of Timepoints are held in a putState structure, as -
    64  // we are not confident of that resolution's average until values from the next resolution have -
    65  // arrived. Due to this assumption, the data extraction methods of the StatStore ignore values -
    66  // currently in the putState struct.
    67  
    68  func NewStatStore(epsilon uint64, resolution time.Duration, windowDuration uint, supportedPercentiles []float64) *StatStore {
    69  	return &StatStore{
    70  		buffer:               list.New(),
    71  		epsilon:              epsilon,
    72  		resolution:           resolution,
    73  		windowDuration:       windowDuration,
    74  		supportedPercentiles: supportedPercentiles,
    75  	}
    76  }
    77  
    78  // TimePoint is a single point of a timeseries, representing a time-value pair.
    79  type TimePoint struct {
    80  	Timestamp time.Time
    81  	Value     uint64
    82  }
    83  
    84  // StatStore is a TimeStore-like object that does not implement the TimeStore interface.
    85  type StatStore struct {
    86  	// start is the start of the represented time window.
    87  	start time.Time
    88  
    89  	// buffer is a list of tpBucket that is sequenced in a time-descending order, meaning that
    90  	// Front points to the latest tpBucket and Back to the oldest one.
    91  	buffer *list.List
    92  
    93  	// A RWMutex guards all operations on the StatStore.
    94  	sync.RWMutex
    95  
    96  	// epsilon is the acceptable error difference for the storage of TimePoints.
    97  	// Increasing epsilon decreases memory usage of the StatStore, at the cost of precision.
    98  	// The precision of max is not affected by epsilon.
    99  	epsilon uint64
   100  
   101  	// resolution is the standardized duration between points in the StatStore.
   102  	// the Get operation returns TimePoints at every multiple of resolution,
   103  	// even if TimePoints have not been Put for all such times.
   104  	resolution time.Duration
   105  
   106  	// windowDuration is the maximum number of time resolutions that is stored in the StatStore
   107  	// e.g. windowDuration 60, with a resolution of time.Minute represents an 1-hour window
   108  	windowDuration uint
   109  
   110  	// tpCount is the number of TimePoints that are represented in the StatStore.
   111  	// If tpCount is equal to windowDuration, then the StatStore window is considered full.
   112  	tpCount uint
   113  
   114  	// lastPut maintains the state of values inserted within the last resolution.
   115  	// When values of a later resolution are added to the StatStore, lastPut is flushed to
   116  	// the last tpBucket, if its average is within epsilon. Otherwise, a new bucket is -
   117  	// created.
   118  	lastPut putState
   119  
   120  	// suportedPercentiles is a slice of values from (0,1) that represents the percentiles
   121  	// that are calculated by the StatStore.
   122  	supportedPercentiles []float64
   123  
   124  	// validCache is true if lastPut has not been flushed since the calculation of the -
   125  	// cached derived stats.
   126  	validCache bool
   127  
   128  	// cachedAverage, cachedMax and cachedPercentiles are the cached derived stats that -
   129  	// are exposed by the StatStore. They are calculated upon the first request of any -
   130  	// derived stat, and invalidated when lastPut is flushed into the StatStore
   131  	cachedAverage     uint64
   132  	cachedMax         uint64
   133  	cachedPercentiles []uint64
   134  }
   135  
   136  // tpBucket is a bucket that represents a set of consecutive TimePoints with different
   137  // timestamps whose values differ less than epsilon.
   138  // tpBucket essentially represents a time window with a constant value.
   139  type tpBucket struct {
   140  	// count is the number of TimePoints represented in the tpBucket.
   141  	count uint
   142  
   143  	// value is the approximate value of all in the tpBucket, +- epsilon.
   144  	value uint64
   145  
   146  	// max is the maximum value of all TimePoints that have been used to generate the tpBucket.
   147  	max uint64
   148  
   149  	// maxIdx is the number of resolutions after the start time where the max value is located.
   150  	maxIdx uint
   151  }
   152  
   153  // putState is a structure that maintains context of the values in the resolution that is currently
   154  // being inserted.
   155  // Assumes that Puts are performed in a time-ascending order.
   156  type putState struct {
   157  	actualCount uint
   158  	average     float64
   159  	max         uint64
   160  	stamp       time.Time
   161  }
   162  
   163  // IsEmpty returns true if the StatStore is empty
   164  func (ss *StatStore) IsEmpty() bool {
   165  	if ss.buffer.Front() == nil {
   166  		return true
   167  	}
   168  	return false
   169  }
   170  
   171  // MaxSize returns the total duration of data that can be stored in the StatStore.
   172  func (ss *StatStore) MaxSize() time.Duration {
   173  	return time.Duration(ss.windowDuration) * ss.resolution
   174  }
   175  
   176  func (ss *StatStore) Put(tp TimePoint) error {
   177  	ss.Lock()
   178  	defer ss.Unlock()
   179  
   180  	// Flatten timestamp to the last multiple of resolution
   181  	ts := tp.Timestamp.Truncate(ss.resolution)
   182  
   183  	lastPutTime := ss.lastPut.stamp
   184  
   185  	// Handle the case where the buffer and lastPut are both empty
   186  	if lastPutTime.Equal(time.Time{}) {
   187  		ss.resetLastPut(ts, tp.Value)
   188  		return nil
   189  	}
   190  
   191  	if ts.Before(lastPutTime) {
   192  		// Ignore TimePoints with Timestamps in the past
   193  		return fmt.Errorf("the provided timepoint has a timestamp in the past")
   194  	}
   195  
   196  	if ts.Equal(lastPutTime) {
   197  		// update lastPut with the new TimePoint
   198  		newVal := tp.Value
   199  		if newVal > ss.lastPut.max {
   200  			ss.lastPut.max = newVal
   201  		}
   202  		oldAvg := ss.lastPut.average
   203  		n := float64(ss.lastPut.actualCount)
   204  		ss.lastPut.average = (float64(newVal) + (n * oldAvg)) / (n + 1)
   205  		ss.lastPut.actualCount++
   206  		return nil
   207  	}
   208  
   209  	ss.flush(ts, tp.Value)
   210  	return nil
   211  }
   212  
   213  // resetLastPut initializes the lastPut field of the StatStore, given a time and a value.
   214  func (ss *StatStore) resetLastPut(timestamp time.Time, value uint64) {
   215  	ss.lastPut.stamp = timestamp
   216  	ss.lastPut.actualCount = 1
   217  	ss.lastPut.average = float64(value)
   218  	ss.lastPut.max = value
   219  }
   220  
   221  // newBucket appends a new bucket to the StatStore, using the values of lastPut.
   222  // newBucket should be always called BEFORE resetting lastPut.
   223  // newBuckets are created by rounding up the lastPut average to the closest epsilon.
   224  // numRes represents the number of resolutions from the newest TimePoint to the lastPut.
   225  // numRes resolutions will be represented in the newly created bucket.
   226  func (ss *StatStore) newBucket(numRes uint) {
   227  	// Calculate the value of the new bucket based on the average of lastPut.
   228  	newVal := (uint64(ss.lastPut.average) / ss.epsilon) * ss.epsilon
   229  	if (uint64(ss.lastPut.average) % ss.epsilon) != 0 {
   230  		newVal += ss.epsilon
   231  	}
   232  	newEntry := tpBucket{
   233  		count:  numRes,
   234  		value:  newVal,
   235  		max:    ss.lastPut.max,
   236  		maxIdx: 0,
   237  	}
   238  	ss.buffer.PushFront(newEntry)
   239  	ss.tpCount += numRes
   240  
   241  	// If this was the first bucket, update ss.start
   242  	if ss.start.Equal(time.Time{}) {
   243  		ss.start = ss.lastPut.stamp
   244  	}
   245  }
   246  
   247  // flush causes the lastPut struct to be flushed to the StatStore list.
   248  func (ss *StatStore) flush(ts time.Time, val uint64) {
   249  	// The new point is in the future, lastPut needs to be flushed to the StatStore.
   250  	ss.validCache = false
   251  
   252  	// Determine how many resolutions in the future the new point is at.
   253  	// The StatStore always represents values up until 1 resolution from lastPut.
   254  	// If the TimePoint is more than one resolutions in the future, the last bucket is -
   255  	// extended to be exactly one resolution behind the new lastPut timestamp.
   256  	numRes := uint(0)
   257  	curr := ts
   258  	for curr.After(ss.lastPut.stamp) {
   259  		curr = curr.Add(-ss.resolution)
   260  		numRes++
   261  	}
   262  
   263  	// Create a new bucket if the buffer is empty
   264  	if ss.IsEmpty() {
   265  		ss.newBucket(numRes)
   266  		ss.resetLastPut(ts, val)
   267  		for ss.tpCount > ss.windowDuration {
   268  			ss.rewind()
   269  		}
   270  		return
   271  	}
   272  
   273  	lastElem := ss.buffer.Front()
   274  	lastEntry := lastElem.Value.(tpBucket)
   275  	lastAvg := ss.lastPut.average
   276  
   277  	// Place lastPut in the latest bucket if the difference from its average
   278  	// is less than epsilon
   279  	if uint64(math.Abs(float64(lastEntry.value)-lastAvg)) < ss.epsilon {
   280  		lastEntry.count += numRes
   281  		ss.tpCount += numRes
   282  		if ss.lastPut.max > lastEntry.max {
   283  			lastEntry.max = ss.lastPut.max
   284  			lastEntry.maxIdx = lastEntry.count - 1
   285  		}
   286  
   287  		// update in list
   288  		lastElem.Value = lastEntry
   289  	} else {
   290  		// Create a new bucket
   291  		ss.newBucket(numRes)
   292  	}
   293  
   294  	// Delete the earliest represented TimePoints if the window is full
   295  	for ss.tpCount > ss.windowDuration {
   296  		ss.rewind()
   297  	}
   298  
   299  	ss.resetLastPut(ts, val)
   300  }
   301  
   302  // rewind deletes the oldest one resolution of data in the StatStore.
   303  func (ss *StatStore) rewind() {
   304  	firstElem := ss.buffer.Back()
   305  	firstEntry := firstElem.Value.(tpBucket)
   306  	// Decrement number of TimePoints in the earliest tpBucket
   307  	firstEntry.count--
   308  	// Decrement total number of TimePoints in the StatStore
   309  	ss.tpCount--
   310  
   311  	// Update the max
   312  	if firstEntry.maxIdx == 0 {
   313  		// The Max value was just removed, lose precision for other maxes in this bucket
   314  		firstEntry.max = firstEntry.value
   315  		firstEntry.maxIdx = firstEntry.count - 1
   316  	} else {
   317  		firstEntry.maxIdx--
   318  	}
   319  
   320  	if firstEntry.count == 0 {
   321  		// Delete the entry if no TimePoints are represented any more
   322  		ss.buffer.Remove(firstElem)
   323  	} else {
   324  		firstElem.Value = firstEntry
   325  	}
   326  
   327  	// Update the start time of the StatStore
   328  	ss.start = ss.start.Add(ss.resolution)
   329  }
   330  
   331  // Get generates a []TimePoint from the appropriate tpEntries.
   332  // Get receives a start and end time as parameters.
   333  // If start or end are equal to time.Time{}, then we consider no such bound.
   334  func (ss *StatStore) Get(start, end time.Time) []TimePoint {
   335  	ss.RLock()
   336  	defer ss.RUnlock()
   337  
   338  	var result []TimePoint
   339  
   340  	if start.After(end) && end.After(time.Time{}) {
   341  		return result
   342  	}
   343  
   344  	// Generate a TimePoint for the lastPut, if within range
   345  	low := start.Equal(time.Time{}) || start.Before(ss.lastPut.stamp)
   346  	hi := end.Equal(time.Time{}) || !end.Before(ss.lastPut.stamp)
   347  	if ss.lastPut.actualCount > 0 && low && hi {
   348  		newTP := TimePoint{
   349  			Timestamp: ss.lastPut.stamp,
   350  			Value:     uint64(ss.lastPut.max), // expose the max to avoid conflicts when viewing derived stats
   351  		}
   352  		result = append(result, newTP)
   353  	}
   354  
   355  	if ss.IsEmpty() {
   356  		return result
   357  	}
   358  
   359  	// Generate TimePoints from the buckets in the buffer
   360  	skipped := 0
   361  	for elem := ss.buffer.Front(); elem != nil; elem = elem.Next() {
   362  		entry := elem.Value.(tpBucket)
   363  
   364  		// calculate the start time of the entry
   365  		offset := int(ss.tpCount) - skipped - int(entry.count)
   366  		entryStart := ss.start.Add(time.Duration(offset) * ss.resolution)
   367  
   368  		// ignore tpEntries later than the requested end time
   369  		if end.After(time.Time{}) && entryStart.After(end) {
   370  			skipped += int(entry.count)
   371  			continue
   372  		}
   373  
   374  		// break if we have reached a tpBucket with no values before or equal to
   375  		// the start time.
   376  		if !entryStart.Add(time.Duration(entry.count-1) * ss.resolution).After(start) {
   377  			break
   378  		}
   379  
   380  		// generate as many TimePoints as required from this bucket
   381  		newSkip := 0
   382  		for curr := 1; curr <= int(entry.count); curr++ {
   383  			offset = int(ss.tpCount) - skipped - curr
   384  			newStamp := ss.start.Add(time.Duration(offset) * ss.resolution)
   385  			if end.After(time.Time{}) && newStamp.After(end) {
   386  				continue
   387  			}
   388  
   389  			if newStamp.Before(start) {
   390  				break
   391  			}
   392  
   393  			// this TimePoint is within (start, end), generate it
   394  			newSkip++
   395  			newTP := TimePoint{
   396  				Timestamp: newStamp,
   397  				Value:     entry.value,
   398  			}
   399  			result = append(result, newTP)
   400  		}
   401  		skipped += newSkip
   402  	}
   403  
   404  	return result
   405  }
   406  
   407  // Last returns the latest TimePoint, representing the average value of lastPut.
   408  // Last also returns the max value of all Puts represented in lastPut.
   409  // Last returns an error if no Put operations have been performed on the StatStore.
   410  func (ss *StatStore) Last() (TimePoint, uint64, error) {
   411  	ss.RLock()
   412  	defer ss.RUnlock()
   413  
   414  	if ss.lastPut.stamp.Equal(time.Time{}) {
   415  		return TimePoint{}, uint64(0), fmt.Errorf("the StatStore is empty")
   416  	}
   417  
   418  	tp := TimePoint{
   419  		Timestamp: ss.lastPut.stamp,
   420  		Value:     uint64(ss.lastPut.average),
   421  	}
   422  
   423  	return tp, ss.lastPut.max, nil
   424  }
   425  
   426  // fillCache caches the average, max and percentiles of the StatStore.
   427  // Assumes a write lock is taken by the caller.
   428  func (ss *StatStore) fillCache() {
   429  	// Calculate the average and max, flatten values into a slice
   430  	sum := uint64(0)
   431  	curMax := ss.lastPut.max
   432  	vals := []float64{}
   433  	for elem := ss.buffer.Front(); elem != nil; elem = elem.Next() {
   434  		entry := elem.Value.(tpBucket)
   435  
   436  		// Calculate the weighted sum of all tpBuckets
   437  		sum += uint64(entry.count) * entry.value
   438  
   439  		// Compare the bucket value with the current max
   440  		if entry.value > curMax {
   441  			curMax = entry.value
   442  		}
   443  
   444  		// Create a slice of values to generate percentiles
   445  		for i := uint(0); i < entry.count; i++ {
   446  			vals = append(vals, float64(entry.value))
   447  		}
   448  	}
   449  	ss.cachedAverage = sum / uint64(ss.tpCount)
   450  	ss.cachedMax = curMax
   451  
   452  	// Calculate all supported percentiles
   453  	sort.Float64s(vals)
   454  	ss.cachedPercentiles = []uint64{}
   455  	for _, spc := range ss.supportedPercentiles {
   456  		pcIdx := int(math.Trunc(spc * float64(ss.tpCount)))
   457  		ss.cachedPercentiles = append(ss.cachedPercentiles, uint64(vals[pcIdx]))
   458  	}
   459  
   460  	ss.validCache = true
   461  }
   462  
   463  // Average returns a weighted average across all buckets, using the count of -
   464  // resolutions at each bucket as the weight.
   465  func (ss *StatStore) Average() (uint64, error) {
   466  	ss.Lock()
   467  	defer ss.Unlock()
   468  
   469  	if ss.IsEmpty() {
   470  		return uint64(0), fmt.Errorf("the StatStore is empty")
   471  	}
   472  
   473  	if !ss.validCache {
   474  		ss.fillCache()
   475  	}
   476  	return ss.cachedAverage, nil
   477  }
   478  
   479  // Max returns the maximum element currently in the StatStore.
   480  // Max does NOT consider the case where the maximum is in the last one minute.
   481  func (ss *StatStore) Max() (uint64, error) {
   482  	ss.Lock()
   483  	defer ss.Unlock()
   484  
   485  	if ss.IsEmpty() {
   486  		return uint64(0), fmt.Errorf("the StatStore is empty")
   487  	}
   488  
   489  	if !ss.validCache {
   490  		ss.fillCache()
   491  	}
   492  	return ss.cachedMax, nil
   493  }
   494  
   495  // Percentile returns the requested percentile from the StatStore.
   496  func (ss *StatStore) Percentile(p float64) (uint64, error) {
   497  	ss.Lock()
   498  	defer ss.Unlock()
   499  
   500  	if ss.IsEmpty() {
   501  		return uint64(0), fmt.Errorf("the StatStore is empty")
   502  	}
   503  
   504  	// Check if the specific percentile is supported
   505  	found := false
   506  	idx := 0
   507  	for i, spc := range ss.supportedPercentiles {
   508  		if p == spc {
   509  			found = true
   510  			idx = i
   511  			break
   512  		}
   513  	}
   514  
   515  	if !found {
   516  		return uint64(0), fmt.Errorf("the requested percentile is not supported")
   517  	}
   518  
   519  	if !ss.validCache {
   520  		ss.fillCache()
   521  	}
   522  
   523  	return ss.cachedPercentiles[idx], nil
   524  }