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 }