github.com/mrgossett/heapster@v0.18.2/store/daystore/day_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 daystore
    16  
    17  import (
    18  	"fmt"
    19  	"math"
    20  	"sort"
    21  	"sync"
    22  	"time"
    23  
    24  	statstore "k8s.io/heapster/store/statstore"
    25  
    26  	"k8s.io/heapster/third_party/window"
    27  )
    28  
    29  // DayStore is an in-memory window of derived stats for each hour of a day.
    30  // DayStore holds 24 hours of derived stats, plus an hour-long StatStore that holds -
    31  // historical data on the current hour.
    32  // DayStore can calculate the Average, Max and 95th Percentile over the past 24 hours.
    33  // The DayStore needs to be populated in a chronological order.
    34  
    35  // Note on how derived stats are extracted:
    36  // If the DayStore holds less than 1 hour of data, then the average, max and 95th are -
    37  // calculated only from the the Hour store.
    38  //
    39  // If the DayStore holds more than 1 hour of data, then the average and 95th are calculated -
    40  // only from the 24 past hourly stats that have been collected.
    41  // Assuming a resolution of 1 minute, this behavior may ignore up to 59 minutes of the latest data.
    42  // This behavior is required to avoid calculating less than a full day's data.
    43  // In order to correctly capture spikes, the Max takes into account the latest data, causing the -
    44  // max to reflect values in the past [24 hours, 25 hours)
    45  type DayStore struct {
    46  	// a RWMutex guards all operations on the underlying window and cached values.
    47  	sync.RWMutex
    48  
    49  	// Hour is a StatStore with data from the last one hour
    50  	Hour *statstore.StatStore
    51  
    52  	window *window.MovingWindow
    53  
    54  	// size is the number of items currently stored in the window
    55  	size int
    56  
    57  	// lastFlush is the time when the previous hourly stats were flushed into the DayStore
    58  	lastFlush time.Time
    59  
    60  	// validAvgPct marks whether the cached average and percentiles are correct.
    61  	validAvgPct bool
    62  	// validMax marks whether the cached max value is correct.
    63  	validMax bool
    64  
    65  	cachedAverage     uint64
    66  	cachedMax         uint64
    67  	cachedNinetyFifth uint64
    68  }
    69  
    70  // hourEntry is the set of derived stats that are maintained per hour.
    71  type hourEntry struct {
    72  	average     uint64
    73  	max         uint64
    74  	ninetyFifth uint64
    75  }
    76  
    77  // NewDayStore is a DayStore constructor.
    78  // The recommended minimum resolution is at least one minute.
    79  func NewDayStore(epsilon uint64, resolution time.Duration) *DayStore {
    80  	// Calculate how many resolutions correspond to an hour
    81  	hourNS := time.Hour.Nanoseconds()
    82  	resNS := resolution.Nanoseconds()
    83  	intervals := uint(hourNS / resNS)
    84  
    85  	if hourNS%resNS != 0 {
    86  		intervals++
    87  	}
    88  
    89  	return &DayStore{
    90  		window: window.New(24, 1),
    91  		Hour:   statstore.NewStatStore(epsilon, resolution, intervals, []float64{0.95}),
    92  	}
    93  }
    94  
    95  // Put stores a TimePoint into the Hour StatStore, while checking whether it -
    96  // is time to flush the last hour's stats in the window.
    97  // Put operations need to be performed in a chronological (time-ascending) order
    98  func (ds *DayStore) Put(tp statstore.TimePoint) error {
    99  	ds.Lock()
   100  	defer ds.Unlock()
   101  
   102  	err := ds.Hour.Put(tp)
   103  	if err != nil {
   104  		return err
   105  	}
   106  
   107  	if tp.Value > ds.cachedMax {
   108  		ds.validMax = false
   109  	}
   110  
   111  	// Check if this is the first TimePoint ever, in which case flush in one hour from now.
   112  	if ds.lastFlush.Equal(time.Time{}) {
   113  		ds.lastFlush = tp.Timestamp
   114  		return nil
   115  	}
   116  
   117  	// The new TimePoint is not newer by at least one hour since the last flush
   118  	if tp.Timestamp.Add(-time.Hour).Before(ds.lastFlush) {
   119  		return nil
   120  	}
   121  
   122  	// create an hourEntry for the existing hour
   123  	ds.validAvgPct = false
   124  	avg, _ := ds.Hour.Average()
   125  	max, _ := ds.Hour.Max()
   126  	nf, _ := ds.Hour.Percentile(0.95)
   127  	newEntry := hourEntry{
   128  		average:     avg,
   129  		max:         max,
   130  		ninetyFifth: nf,
   131  	}
   132  
   133  	// check if the TimePoint is multiple hours in the future
   134  	// insert the hourEntry the appropriate amount of hours
   135  	distance := tp.Timestamp.Sub(ds.lastFlush)
   136  	nextflush := tp.Timestamp
   137  	for distance.Nanoseconds() >= time.Hour.Nanoseconds() {
   138  		ds.lastFlush = nextflush
   139  		nextflush = ds.lastFlush.Add(time.Hour)
   140  		if ds.size < 24 {
   141  			ds.size += 1
   142  		}
   143  		ds.window.PushBack(newEntry)
   144  		distance = time.Time{}.Add(distance).Add(-time.Hour).Sub(time.Time{})
   145  	}
   146  	return nil
   147  }
   148  
   149  // fillMax caches the max of the DayStore.
   150  func (ds *DayStore) fillMax() {
   151  	// generate a slice of the window
   152  	day := ds.window.Slice()
   153  
   154  	// calculate th max of the hourly maxes
   155  	curMax, _ := ds.Hour.Max()
   156  	for _, elem := range day {
   157  		he := elem.(hourEntry)
   158  		if he.max > curMax {
   159  			curMax = he.max
   160  		}
   161  	}
   162  	ds.cachedMax = curMax
   163  	ds.validMax = true
   164  }
   165  
   166  // fillAvgPct caches the average, 95th percentile of the DayStore.
   167  func (ds *DayStore) fillAvgPct() {
   168  	ds.validAvgPct = true
   169  
   170  	// If no past Hourly data has been flushed to the window,
   171  	// return the average and 95th percentile of the past hour.
   172  	if ds.size == 0 {
   173  		ds.cachedAverage, _ = ds.Hour.Average()
   174  		ds.cachedNinetyFifth, _ = ds.Hour.Percentile(0.95)
   175  		return
   176  	}
   177  	// Otherwise, ignore the past one hour and use the window values
   178  
   179  	// generate a slice of the window
   180  	day := ds.window.Slice()
   181  
   182  	// calculate the average value of the hourly averages
   183  	// also create a sortable slice of float64
   184  	var sum uint64
   185  	var nf []float64
   186  	for _, elem := range day {
   187  		he := elem.(hourEntry)
   188  		sum += he.average
   189  		nf = append(nf, float64(he.ninetyFifth))
   190  	}
   191  	ds.cachedAverage = sum / uint64(ds.size)
   192  
   193  	// sort and calculate the 95th percentile
   194  	sort.Float64s(nf)
   195  	pcIdx := int(math.Trunc(0.95 * float64(ds.size+1)))
   196  	if pcIdx >= len(nf) {
   197  		pcIdx = len(nf) - 1
   198  	}
   199  	ds.cachedNinetyFifth = uint64(nf[pcIdx])
   200  }
   201  
   202  // Average returns the average value of the hourly averages in the past day.
   203  func (ds *DayStore) Average() (uint64, error) {
   204  	ds.Lock()
   205  	defer ds.Unlock()
   206  
   207  	if ds.Hour.IsEmpty() {
   208  		return uint64(0), fmt.Errorf("the DayStore is empty")
   209  	}
   210  
   211  	if !ds.validAvgPct {
   212  		ds.fillAvgPct()
   213  	}
   214  
   215  	return ds.cachedAverage, nil
   216  }
   217  
   218  // Max returns the maximum value of the hourly maxes in the past day.
   219  func (ds *DayStore) Max() (uint64, error) {
   220  	ds.Lock()
   221  	defer ds.Unlock()
   222  
   223  	if ds.Hour.IsEmpty() {
   224  		return uint64(0), fmt.Errorf("the DayStore is empty")
   225  	}
   226  
   227  	if !ds.validMax {
   228  		ds.fillMax()
   229  	}
   230  
   231  	return ds.cachedMax, nil
   232  }
   233  
   234  // NinetyFifth returns the 95th percentile of the hourly 95th percentiles in the past day.
   235  func (ds *DayStore) NinetyFifth() (uint64, error) {
   236  	ds.Lock()
   237  	defer ds.Unlock()
   238  
   239  	if ds.Hour.IsEmpty() {
   240  		return uint64(0), fmt.Errorf("the DayStore is empty")
   241  	}
   242  
   243  	if !ds.validAvgPct {
   244  		ds.fillAvgPct()
   245  	}
   246  
   247  	return ds.cachedNinetyFifth, nil
   248  }