| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170 |
- package doppel
-
- import (
- "math"
- "math/rand/v2"
- "time"
- )
-
- const (
- StatsBisectTimes = 70
- StatsLowK = 0.01
- StatsHighK = 10.0
-
- // do not calculate statistics if we have < than this number of durations
- MinDurationsToCalculate = 100
-
- // these values are taken from ok.ru. measured from moscow site.
- StatsDefaultK = 0.37846373895785335
- StatsDefaultLambda = 1.73177086015485
-
- // how many bytes should we drift
- DRSNoise = 100
- )
-
- // Stats is responsible for generating values that are distributed according
- // to some statistical distribution.
- //
- // It follows several ideas:
- // 1. Based on nginx and Cloudflare behaviour, even if server is eager
- // to send a lot, they all start with small TLS packets that are
- // approximately MTU-sized. After
- // 2. After ~40 TLS records, server considers TCP session as somewhat solid
- // and reliable and ramps up to 4096.
- // 3. After ~20 TLS records more it jumps to the max 16384 bytes and keep
- // this size as long as it can
- // 4. If there is no any byte within a connection for a longer time period,
- // this counter resets.
- //
- // This is called Dynamic TLS Record Sizing
- // - https://blog.cloudflare.com/optimizing-tls-over-tcp-to-reduce-latency/
- // - https://community.f5.com/kb/technicalarticles/boosting-tls-performance-with-dynamic-record-sizing-on-big-ip/280798
- // - https://www.igvita.com/2013/10/24/optimizing-tls-record-size-and-buffering-latency/
- //
- // And this optimized for the very first byte, so web browsers could start to
- // render as early as possible, showing user some preliminary results, optimizing
- // for perceived latency.
- //
- // Since this is very typical for the website, we also aim for that.
- //
- // Another important idea is how delays between TLS packets are distributed.
- // In case of sending huge heavy content with max sized record, delays have
- // lognormal distribution. But a nature of a typical website shows that
- // it eagers to deliver as fast as it can in a few very first records and
- // could possibly slow down later.
- //
- // This is perfectly described by Weibull distribution:
- // - https://en.wikipedia.org/wiki/Weibull_distribution
- // - https://ieeexplore.ieee.org/document/6662948
- // - https://www.researchgate.net/publication/224621285_Traffic_modelling_and_cost_optimization_for_transmitting_traffic_messages_over_a_hybrid_broadcast_and_cellular_network
- // - https://ir.uitm.edu.my/id/eprint/105386/1/105386.pdf
- //
- // In other word, a combination of Dynamic TLS Record Sizing hints us for
- // Weibull distribution.
- //
- // But we also have to keep in mind that DRS is not well spread yet. In most cases
- // users still rely on OpenSSL or webserver defaults. OpenSSL chunks with
- // biggest packet sizes, nginx relies on static setting that is 16k by default.
- // Thus, dynamic sizing has to be present but we cannot oblige users to use that.
- type Stats struct {
- sizeLastRequested time.Time
- sizeCounter int
-
- // https://en.wikipedia.org/wiki/Shape_parameter
- k float64
- // https://en.wikipedia.org/wiki/Scale_parameter
- lambda float64
-
- // Dynamic Record Sizing
- drs bool
- }
-
- func (d *Stats) Delay() time.Duration {
- // u ∈ (0, 1], avoids ln(0)
- u := 1.0 - rand.Float64()
-
- // X = λ·(-ln U)^(1/k)
- generated := d.lambda * math.Pow(-math.Log(u), 1.0/d.k)
-
- // generated is in milliseconds
- return time.Duration(generated * float64(time.Millisecond))
- }
-
- func (d *Stats) Size() int {
- if time.Since(d.sizeLastRequested) > TLSRecordSizeResetAfter {
- d.sizeCounter = 0
- }
-
- if !d.drs {
- return TLSRecordSizeMax
- }
-
- d.sizeLastRequested = time.Now()
- d.sizeCounter++
-
- switch {
- case d.sizeCounter <= TLSCounterAccelAfter:
- return TLSRecordSizeStart - rand.IntN(DRSNoise)
- case d.sizeCounter <= TLSCounterMaxAfter:
- return TLSRecordSizeAccel - rand.IntN(DRSNoise)
- }
-
- return TLSRecordSizeMax
- }
-
- func NewStats(durations []time.Duration, drs bool) *Stats {
- n := float64(len(durations))
-
- // in milliseconds
- durFloats := make([]float64, len(durations))
- for i, v := range durations {
- durFloats[i] = float64(v.Microseconds()) / 1000.0
- }
-
- // The bisection solves the standard Weibull MLE equation for shape
- // parameter k. There is no any good formula for doing that so we
- // approximate it by several bisections. The number of operations
- // is statically defined by a constant.
-
- sumLog := 0.0
- for _, v := range durFloats {
- sumLog += math.Log(v)
- }
-
- lowK := StatsLowK
- highK := StatsHighK
-
- for range StatsBisectTimes {
- midK := (lowK + highK) / 2.0
- sumXK := 0.0
- sumXKLog := 0.0
-
- for _, v := range durFloats {
- xk := math.Pow(v, midK)
- sumXK += xk
- sumXKLog += xk * math.Log(v)
- }
-
- if (1.0/midK)+(sumLog/n)-(sumXKLog/sumXK) > 0 {
- lowK = midK
- } else {
- highK = midK
- }
- }
-
- k := (lowK + highK) / 2
-
- sumXK := 0.0
- for _, v := range durFloats {
- sumXK += math.Pow(v, k)
- }
-
- // λ = (Σxᵢᵏ / n)^(1/k)
- lambda := math.Pow(sumXK/n, 1.0/k)
-
- return &Stats{
- k: k,
- lambda: lambda,
- drs: drs,
- }
- }
|