1. /*
    2. * Copyright (c) 2017, MegaEase
    3. * All rights reserved.
    4. *
    5. * Licensed under the Apache License, Version 2.0 (the "License");
    6. * you may not use this file except in compliance with the License.
    7. * You may obtain a copy of the License at
    8. *
    9. * http://www.apache.org/licenses/LICENSE-2.0
    10. *
    11. * Unless required by applicable law or agreed to in writing, software
    12. * distributed under the License is distributed on an "AS IS" BASIS,
    13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14. * See the License for the specific language governing permissions and
    15. * limitations under the License.
    16. */
    17. package sampler
    18. import (
    19. "sync/atomic"
    20. "time"
    21. )
    22. type (
    23. // DurationSampler is the sampler for sampling duration.
    24. DurationSampler struct {
    25. count uint64
    26. durations []uint32
    27. }
    28. // DurationSegment defines resolution for a duration segment
    29. DurationSegment struct {
    30. resolution time.Duration
    31. slots int
    32. }
    33. )
    34. var segments = []DurationSegment{
    35. {time.Millisecond, 500}, // < 500ms
    36. {time.Millisecond * 2, 250}, // < 1s
    37. {time.Millisecond * 4, 250}, // < 2s
    38. {time.Millisecond * 8, 125}, // < 3s
    39. {time.Millisecond * 16, 125}, // < 5s
    40. {time.Millisecond * 32, 125}, // < 9s
    41. {time.Millisecond * 64, 125}, // < 17s
    42. {time.Millisecond * 128, 125}, // < 33s
    43. {time.Millisecond * 256, 125}, // < 65s
    44. {time.Millisecond * 512, 125}, // < 129s
    45. {time.Millisecond * 1024, 125}, // < 257s
    46. }
    47. // NewDurationSampler creates a DurationSampler.
    48. func NewDurationSampler() *DurationSampler {
    49. slots := 1
    50. for _, s := range segments {
    51. slots += s.slots
    52. }
    53. return &DurationSampler{
    54. durations: make([]uint32, slots),
    55. }
    56. }
    57. // Update updates the sample. This function could be called concurrently,
    58. // but should not be called concurrently with Percentiles.
    59. func (ds *DurationSampler) Update(d time.Duration) {
    60. idx := 0
    61. for _, s := range segments {
    62. bound := s.resolution * time.Duration(s.slots)
    63. if d < bound-s.resolution/2 {
    64. idx += int((d + s.resolution/2) / s.resolution)
    65. break
    66. }
    67. d -= bound
    68. idx += s.slots
    69. }
    70. atomic.AddUint64(&ds.count, 1)
    71. atomic.AddUint32(&ds.durations[idx], 1)
    72. }
    73. // Reset reset the DurationSampler to initial state
    74. func (ds *DurationSampler) Reset() {
    75. for i := 0; i < len(ds.durations); i++ {
    76. ds.durations[i] = 0
    77. }
    78. ds.count = 0
    79. }
    80. // Percentiles returns 7 metrics by order:
    81. // P25, P50, P75, P95, P98, P99, P999
    82. func (ds *DurationSampler) Percentiles() []float64 {
    83. percentiles := []float64{0.25, 0.5, 0.75, 0.95, 0.98, 0.99, 0.999}
    84. result := make([]float64, len(percentiles))
    85. count, total := uint64(0), float64(ds.count)
    86. di, pi := 0, 0
    87. base := time.Duration(0)
    88. for _, s := range segments {
    89. for i := 0; i < s.slots; i++ {
    90. count += uint64(ds.durations[di])
    91. di++
    92. p := float64(count) / total
    93. for p >= percentiles[pi] {
    94. d := base + s.resolution*time.Duration(i)
    95. result[pi] = float64(d / time.Millisecond)
    96. pi++
    97. if pi == len(percentiles) {
    98. return result
    99. }
    100. }
    101. }
    102. base += s.resolution * time.Duration(s.slots)
    103. }
    104. for pi < len(percentiles) {
    105. result[pi] = 9999999
    106. pi++
    107. }
    108. return result
    109. }

    https://github.com/megaease/easegress/blob/main/pkg/util/sampler/sampler.go

    1. /*
    2. * Copyright (c) 2017, MegaEase
    3. * All rights reserved.
    4. *
    5. * Licensed under the Apache License, Version 2.0 (the "License");
    6. * you may not use this file except in compliance with the License.
    7. * You may obtain a copy of the License at
    8. *
    9. * http://www.apache.org/licenses/LICENSE-2.0
    10. *
    11. * Unless required by applicable law or agreed to in writing, software
    12. * distributed under the License is distributed on an "AS IS" BASIS,
    13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14. * See the License for the specific language governing permissions and
    15. * limitations under the License.
    16. */
    17. package httpstat
    18. import (
    19. "math"
    20. "sync"
    21. "sync/atomic"
    22. "time"
    23. metrics "github.com/rcrowley/go-metrics"
    24. "github.com/megaease/easegress/pkg/util/codecounter"
    25. "github.com/megaease/easegress/pkg/util/sampler"
    26. )
    27. type (
    28. // HTTPStat is the statistics tool for HTTP traffic.
    29. HTTPStat struct {
    30. mutex sync.RWMutex
    31. count uint64
    32. rate1 metrics.EWMA
    33. rate5 metrics.EWMA
    34. rate15 metrics.EWMA
    35. errCount uint64
    36. errRate1 metrics.EWMA
    37. errRate5 metrics.EWMA
    38. errRate15 metrics.EWMA
    39. m1ErrPercent float64
    40. m5ErrPercent float64
    41. m15ErrPercent float64
    42. total uint64
    43. min uint64
    44. max uint64
    45. durationSampler *sampler.DurationSampler
    46. reqSize uint64
    47. respSize uint64
    48. cc *codecounter.HTTPStatusCodeCounter
    49. }
    50. // Metric is the package of statistics at once.
    51. Metric struct {
    52. StatusCode int
    53. Duration time.Duration
    54. ReqSize uint64
    55. RespSize uint64
    56. }
    57. // Status contains all status generated by HTTPStat.
    58. Status struct {
    59. Count uint64 `yaml:"count"`
    60. M1 float64 `yaml:"m1"`
    61. M5 float64 `yaml:"m5"`
    62. M15 float64 `yaml:"m15"`
    63. ErrCount uint64 `yaml:"errCount"`
    64. M1Err float64 `yaml:"m1Err"`
    65. M5Err float64 `yaml:"m5Err"`
    66. M15Err float64 `yaml:"m15Err"`
    67. M1ErrPercent float64 `yaml:"m1ErrPercent"`
    68. M5ErrPercent float64 `yaml:"m5ErrPercent"`
    69. M15ErrPercent float64 `yaml:"m15ErrPercent"`
    70. Min uint64 `yaml:"min"`
    71. Max uint64 `yaml:"max"`
    72. Mean uint64 `yaml:"mean"`
    73. P25 float64 `yaml:"p25"`
    74. P50 float64 `yaml:"p50"`
    75. P75 float64 `yaml:"p75"`
    76. P95 float64 `yaml:"p95"`
    77. P98 float64 `yaml:"p98"`
    78. P99 float64 `yaml:"p99"`
    79. P999 float64 `yaml:"p999"`
    80. ReqSize uint64 `yaml:"reqSize"`
    81. RespSize uint64 `yaml:"respSize"`
    82. Codes map[int]uint64 `yaml:"codes"`
    83. }
    84. )
    85. func (m *Metric) isErr() bool {
    86. return m.StatusCode >= 400
    87. }
    88. // New creates an HTTPStat.
    89. func New() *HTTPStat {
    90. hs := &HTTPStat{
    91. rate1: metrics.NewEWMA1(),
    92. rate5: metrics.NewEWMA5(),
    93. rate15: metrics.NewEWMA15(),
    94. errRate1: metrics.NewEWMA1(),
    95. errRate5: metrics.NewEWMA5(),
    96. errRate15: metrics.NewEWMA15(),
    97. min: math.MaxUint64,
    98. durationSampler: sampler.NewDurationSampler(),
    99. cc: codecounter.New(),
    100. }
    101. return hs
    102. }
    103. // Stat stats the ctx.
    104. func (hs *HTTPStat) Stat(m *Metric) {
    105. // Note: although this is a data update operation, we are using the RLock here,
    106. // which means goroutines can execute this function concurrently, and contentions
    107. // are handled by the atomic operations for each item.
    108. //
    109. // This lock is only a mutex for the 'Status' function below.
    110. hs.mutex.RLock()
    111. defer hs.mutex.RUnlock()
    112. atomic.AddUint64(&hs.count, 1)
    113. hs.rate1.Update(1)
    114. hs.rate5.Update(1)
    115. hs.rate15.Update(1)
    116. if m.isErr() {
    117. atomic.AddUint64(&hs.errCount, 1)
    118. hs.errRate1.Update(1)
    119. hs.errRate5.Update(1)
    120. hs.errRate15.Update(1)
    121. }
    122. duration := uint64(m.Duration.Milliseconds())
    123. atomic.AddUint64(&hs.total, duration)
    124. for {
    125. min := atomic.LoadUint64(&hs.min)
    126. if duration >= min {
    127. break
    128. }
    129. if atomic.CompareAndSwapUint64(&hs.min, min, duration) {
    130. break
    131. }
    132. }
    133. for {
    134. max := atomic.LoadUint64(&hs.max)
    135. if duration <= max {
    136. break
    137. }
    138. if atomic.CompareAndSwapUint64(&hs.max, max, duration) {
    139. break
    140. }
    141. }
    142. hs.durationSampler.Update(m.Duration)
    143. atomic.AddUint64(&hs.reqSize, m.ReqSize)
    144. atomic.AddUint64(&hs.respSize, m.RespSize)
    145. hs.cc.Count(m.StatusCode)
    146. }
    147. // Status returns HTTPStat Status, It assumes it is called every five seconds.
    148. // https://github.com/rcrowley/go-metrics/blob/3113b8401b8a98917cde58f8bbd42a1b1c03b1fd/ewma.go#L98-L99
    149. func (hs *HTTPStat) Status() *Status {
    150. hs.mutex.Lock()
    151. defer hs.mutex.Unlock()
    152. hs.rate1.Tick()
    153. hs.rate5.Tick()
    154. hs.rate15.Tick()
    155. hs.errRate1.Tick()
    156. hs.errRate5.Tick()
    157. hs.errRate15.Tick()
    158. m1, m5, m15 := hs.rate1.Rate(), hs.rate5.Rate(), hs.rate15.Rate()
    159. m1Err, m5Err, m15Err := hs.errRate1.Rate(), hs.errRate5.Rate(), hs.errRate15.Rate()
    160. m1ErrPercent, m5ErrPercent, m15ErrPercent := 0.0, 0.0, 0.0
    161. if m1 > 0 {
    162. m1ErrPercent = m1Err / m1
    163. }
    164. if m5 > 0 {
    165. m1ErrPercent = m5Err / m5
    166. }
    167. if m15 > 0 {
    168. m1ErrPercent = m15Err / m15
    169. }
    170. percentiles := hs.durationSampler.Percentiles()
    171. hs.durationSampler.Reset()
    172. codes := hs.cc.Codes()
    173. hs.cc.Reset()
    174. mean, min := uint64(0), uint64(0)
    175. if hs.count > 0 {
    176. mean = hs.total / hs.count
    177. min = hs.min
    178. }
    179. status := &Status{
    180. Count: hs.count,
    181. M1: m1,
    182. M5: m5,
    183. M15: m15,
    184. ErrCount: hs.errCount,
    185. M1Err: m1Err,
    186. M5Err: m5Err,
    187. M15Err: m15Err,
    188. M1ErrPercent: m1ErrPercent,
    189. M5ErrPercent: m5ErrPercent,
    190. M15ErrPercent: m15ErrPercent,
    191. Min: min,
    192. Mean: mean,
    193. Max: hs.max,
    194. P25: percentiles[0],
    195. P50: percentiles[1],
    196. P75: percentiles[2],
    197. P95: percentiles[3],
    198. P98: percentiles[4],
    199. P99: percentiles[5],
    200. P999: percentiles[6],
    201. ReqSize: hs.reqSize,
    202. RespSize: hs.respSize,
    203. Codes: codes,
    204. }
    205. return status
    206. }

    https://github.com/megaease/easegress/blob/main/pkg/util/httpstat/httpstat.go