-
Notifications
You must be signed in to change notification settings - Fork 296
XAPI throttling proof of concept #6778
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature/throttling
Are you sure you want to change the base?
XAPI throttling proof of concept #6778
Conversation
The token bucket library implements the token bucket algorithm, to be used for rate-limiting. This commit implements basic token buckets, which contain tokens that are refilled over time according to their refill parameter, up to a maximum determined by the burst parameter. Tokens can be consumed in a thread-safe way - consuming returns false when there are not enough tokens available, and true when the operation was successful. Signed-off-by: Christian Pardillo Laursen <christian.pardillolaursen@citrix.com>
Signed-off-by: Christian Pardillo Laursen <christian.pardillolaursen@citrix.com>
Bucket tables map client identifiers to their token buckets, and are the main data structure for rate limiting. Signed-off-by: Christian Pardillo Laursen <christian.pardillolaursen@citrix.com>
To be replaced with a proper datamodel. Bucket tables are used for mapping requests to their respective token bucket so that they can be rate limited. Signed-off-by: Christian Pardillo Laursen <christian.pardillolaursen@citrix.com>
Signed-off-by: Christian Pardillo Laursen <christian.pardillolaursen@citrix.com>
Signed-off-by: Christian Pardillo Laursen <christian.pardillolaursen@citrix.com>
Signed-off-by: Christian Pardillo Laursen <christian.pardillolaursen@citrix.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a bit tough to understand how the datastructure does rate-limitting. I had to find documentation online to undestand it. As such, I would appreciate a more in-depth explanation at the header of the mli file. For example, it would be good to understand when is the "refill" timestamp changed, and explain that some of the methods are only meant to be used for testing.
| (** Create token bucket with given parameters and supplied inital timestamp | ||
| @param timestamp Initial timestamp | ||
| @param burst_size Maximum number of tokens that can fit in the bucket | ||
| @param fill_rate Number of tokens added to the bucket per second |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fill_rate has an implicit unit (Hz), I think that it can be worth representing the fill rate as the amount of time it takes the bucket from being empty to becoming full. This is a timespan, and we can use a known datatype with explicit unit here: Mtime.span.
How some operations would be changed:
let peek_with_delta time_delta tb =
let fill_time = Mtime.Span.to_float_ns tb.fill_time in
let time_delta = Mtime.Span.to_float_ns time_delta in
min tb.burst_size (tb.tokens +. (time_delta /. fill_time)
let delay_until_available_with_delta delta tb amount =
let current_tokens = peek_with_delta delta tb in
let required_tokens = max 0. (amount -. current_tokens) in
required_tokens *. (Mtime.Span.to_float_ns tb.fill_time)
|> Float.to_int64
|> Mtime.Span.of_unit64_ns There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can see the appeal of adding units to the fill_rate but I'd rather keep the burst size decoupled from fill rate, and I think it's more intuitive as tokens per second than seconds per token.
| burst_size: float | ||
| ; fill_rate: float | ||
| ; mutable tokens: float | ||
| ; mutable last_refill: Mtime.span |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any reason this cannot be a counter? They return the time difference directly: https://ocaml.org/p/mtime/latest/doc/mtime.clock/Mtime_clock/index.html#counters
This would mean that the peek functions would turn into:
let peek_with_delta time_delta tb =
let time_delta_seconds = Mtime.Span.to_float_ns time_delta *. 1e-9 in
min tb.burst_size (tb.tokens +. (time_delta_seconds *. tb.fill_rate))
let peek tb = peek_with_delta (Mtime_clock.count tb.last_refill) tbThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keeping a counter does simplify things a bit, but if I understand correctly it forces us to make two system time calls when consuming tokens - one to obtain the difference from the counter, and another to produce a new counter. It probably won't make much of a difference, I can try profiling.
Signed-off-by: Christian Pardillo Laursen <christian.pardillolaursen@citrix.com>
|
Added some documentation to the token bucket module to explain the rate limit application. |
Zero or negative rate limits can cause issues in the behaviour of rate limiting. In particular, zero fill rate leads to a division by zero in time calculations. Rather than account for this, we forbid the creation of token buckets with a bad fill rate by returning None. Signed-off-by: Christian Pardillo Laursen <christian.pardillolaursen@citrix.com>
b9098c1 to
dd10c1c
Compare
Signed-off-by: Christian Pardillo Laursen <christian.pardillolaursen@citrix.com>
dd10c1c to
a540ca7
Compare
Make token bucket type abstract to hide Hashtbl.t Use `replace` rather than `add` for adding a new bucket Signed-off-by: Christian Pardillo Laursen <christian.pardillolaursen@citrix.com>
| * GNU Lesser General Public License for more details. | ||
| *) | ||
|
|
||
| type t = (string, Token_bucket.t) Hashtbl.t |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest adding a mutex here, so that we can use this from XAPI's API server (unless we're really sure only 1 thread would use this at a time).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is only set up once on startup and never changed then we'd like need a kind of immutable hashtable, perhaps a bool to say that from now on modifications to the hashtbl are not allowed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The plan is to allow changes via the CLI, but we should probably protect this just in case.
| () | ||
| else | ||
| let wait_time = Token_bucket.delay_until_available bucket amount in | ||
| Thread.delay wait_time ; try_consume () |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But if we add a mutex we'll need to release it here. Perhaps as a start use the mutex just to protect the data structure?
Also if the hashtbl changes rarely we could also use a Map in an Atomic.t
Proof of concept for XAPI throttling.
This allows users to specify the user-agent rate limited clients in xapi.conf, which then consume from a token bucket whenever a request is made, and have to wait for it to refill if they exceed its capacity.