Skip to content
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

Token bucket refactor #46

Closed
wants to merge 19 commits into from

Conversation

Labels
None yet
Projects
None yet
2 participants
@nmathewson
Copy link
Contributor

@nmathewson nmathewson commented Apr 10, 2018

No description provided.

nmathewson added 9 commits Apr 10, 2018
This option was used for shadow testing previously, but is no longer
used for anything.  It interferes with refactoring our token buckets.
This differs from our previous token bucket abstraction in a few
ways:

  1) It is an abstraction, and not a collection of fields.
  2) It is meant to be used with monotonic timestamps, which should
     produce better results than calling gettimeofday over and over.
/* See LICENSE for licensing information */

/**
* \file address.c
Copy link
Contributor

@dgoulet-tor dgoulet-tor Apr 12, 2018

tocket_bucket.c might be better. Also, while at it, maybe the copyright should only be "2018 TPO".

Copy link
Contributor Author

@nmathewson nmathewson Apr 12, 2018

or even token_bucket.c . ;)
5ce7846 has a fix.

token_bucket_init(token_bucket_t *bucket,
uint32_t rate,
uint32_t burst,
uint32_t now_ts)
Copy link
Contributor

@dgoulet-tor dgoulet-tor Apr 12, 2018

Within the DoS subsystem, I did something extremely similar (of course bucketting...) but I went for uint64_t on purposes so we could address just all use cases in the future. Turned out that we could just have uint32_t instead since we use consensus param for some values which are bound to be int32_t.

You think we could go to 64 bit there or memory footprint is the idea of 32 bit?

Copy link
Contributor Author

@nmathewson nmathewson Apr 12, 2018

Hm. So, the existing code here assumes it's 32-bit.

Do we ever need to allow a burst of greater than 4GB? Nobody has actually asked for it so far. And since the API returns a size_t, it should be comparatively easy to make this bigger later on if we want it.

Copy link
Contributor

@dgoulet-tor dgoulet-tor Apr 13, 2018

Ok, after seeing how it is used in the code and tightly coupled with torrc options that are int32_t, this is fine.


/** Convert a rate in bytes per second to a rate in bytes per step */
static uint32_t
rate_per_sec_to_rate_per_step(uint32_t rate)
Copy link
Contributor

@dgoulet-tor dgoulet-tor Apr 12, 2018

I think we should inline this for the small tiny bit of performance we get. Token bucket like that are in fast path often.

Copy link
Contributor Author

@nmathewson nmathewson Apr 12, 2018

I think the compiler should be able to figure out that this function should be inlined on its own. (In my experiments, both gcc and clang inline it.)

Also, note that this function is only used when setting the rate -- not when actually using the buckets.

if (bucket->read_bucket > (int32_t)burst)
bucket->read_bucket = burst;
if (bucket->write_bucket > (int32_t)burst)
bucket->write_bucket = burst;
Copy link
Contributor

@dgoulet-tor dgoulet-tor Apr 12, 2018

Could we do a = MIN(bucket->read_bucket, burst) here instead? Would just make things a bit clearer in terms of code semantic I believe.

Copy link
Contributor Author

@nmathewson nmathewson Apr 12, 2018

c752a23 has a fix here.

const int32_t burst,
const uint32_t elapsed_steps)
{
const int was_empty = *bucketptr <= 0;
Copy link
Contributor

@dgoulet-tor dgoulet-tor Apr 12, 2018

Huge nitpick but for my brain to parse, having (*bucketptr <= 0) would be great.

Copy link
Contributor Author

@nmathewson nmathewson Apr 12, 2018

Done with 3deeec3

{
const int was_empty = *bucketptr <= 0;
/* The casts here prevent an underflow. */
const size_t gap = ((size_t)burst) - ((size_t)*bucketptr);
Copy link
Contributor

@dgoulet-tor dgoulet-tor Apr 12, 2018

bucketptr = -1 and burst = INT32_MAX we wned up with gap = INT32_MAX + 1 which means we can go above the maximum burst value for the gap. It is fine since we normalize it to the "burst" value after but I think its worth commenting that this can happen and it is ok.

Copy link
Contributor Author

@nmathewson nmathewson Apr 12, 2018

4fc779c is a comment rewrite for this.

token_bucket_refill(token_bucket_t *bucket,
uint32_t now_ts)
{
const uint32_t elapsed_ticks = (now_ts - bucket->last_refilled_at_ts);
Copy link
Contributor

@dgoulet-tor dgoulet-tor Apr 12, 2018

As the commit messages mentions it, it is meant to be use with monotonic time only so then now_ts will never be smaller than last_refilled_at_ts. Can we make sure of this?

We had issues in the scheduler with Windows not really having monotonic clock so we had to adjust to this reality (see kist_scheduler_schedule().

Copy link
Contributor Author

@nmathewson nmathewson Apr 12, 2018

I've done something similar here with 2c4eef1


static int
decrement_single_bucket(int32_t *bucketptr,
ssize_t n)
Copy link
Contributor

@dgoulet-tor dgoulet-tor Apr 12, 2018

Why ssize_t and not int32_t which is what the counters uses?

Copy link
Contributor Author

@nmathewson nmathewson Apr 12, 2018

So that the API is agnostic to the implementation details.

uint32_t rate;
int32_t burst;
int32_t read_bucket;
int32_t write_bucket;
Copy link
Contributor

@dgoulet-tor dgoulet-tor Apr 12, 2018

Is more of a design questions.

I would love to use that in the DoS subsystem which would mean removing bunch of codes. However, this token_bucket_t interfaces kind of bakes the fact that you have "read" and "write" thus a connection.

If that struct had only one bucket, we could use it in some other places in a more generic way. Which would mean we would have token_bucket_t read_bucket and the write one in the or_connection_t. Is that a major inconvenience? Because as it is, I can use that in DoS code but not really semantically.

Copy link
Contributor Author

@nmathewson nmathewson Apr 12, 2018

This is what I was asking you about earlier: if we make the change you suggest here, we have to include the rate, burst, and last_refilled_at fields in the or_connection_t twice.

I can try a refactoring to make this change, but I'd rather do it as a separate branch after this is merged, if that's okay with you.

There are also couple of issues with this implementation that make it a little tricky to reuse. Most notably, it assumes that it's working in units of bytes per second, but it converts them to "bytes per step". That conversion is fine for bandwidths, but the imprecesion might not be okay for all other units: if so, we'd have to configure it.

Copy link
Contributor

@dgoulet-tor dgoulet-tor Apr 13, 2018

Right, I think I realized what you meant when you asked when seeing this.

Hmmm... and it makes sense also with our current options since we only have a single bw rate and not a read bw rate and write bw rate.

If we could make this whole piece of work in a generic way to have token bucket for other things than "bytes per second", it would be really great so we don't have multiple bucket implementation.

I'm not that concerned about having duplicate values that is 4 bytes (rate), 4 bytes (burst) and 4 bytes (timetamp) which means 16 bytes extra. And scaled to 100k connections let say, it is overhead of ~2MB which for a relay is not that much compared to a mobile client.

An option then is to configure a "bucket param" object statically and attach it to any bucket you create with it. The DoS subsystem does not copy rate/burst to the bucket object and uses the global value each time instead. If we had a way to do this with a "param" object, it would also work out pretty well. Which also could reduce by 8 bytes (rate + burst) the token_bucket_t object and make it only have a pointer to a static object (+ 4 bytes). Anyway, we aren't saving much space here either way but what I'm more wanting here is a more generic API so it can be used elsewhere. I know that we'll have to go to hidden service circuit creation rate limitation at some point which a bucket system would be great to have.

src/or/main.c Outdated

stats_n_bytes_read += bytes_read;
stats_n_bytes_written += bytes_written;
if (accounting_is_enabled(options) && milliseconds_elapsed >= 0)
accounting_add_bytes(bytes_read, bytes_written, seconds_rolled_over);

if (milliseconds_elapsed > 0) {
connection_bucket_refill(milliseconds_elapsed, (time_t)now.tv_sec,
connection_bucket_refill((time_t)now.tv_sec,
Copy link
Contributor

@dgoulet-tor dgoulet-tor Apr 12, 2018

refill_callback() doesn't use monotonic time unless we do adjustments in the previous commit I pointed it out.

nmathewson added 9 commits Apr 12, 2018
Fix boilerplate on token_bucket.c header.
Improve comment in refill_single_bucket() to better describe C
underflow rules.
Actually, don't. Instead:

Expose a function that computes stamp units from msec.

(It turns out we can't just expose STAMP_TICKS_PER_SECOND, since
Apple doesn't have that.)
Increase TICKS_PER_STEP, to reduce rounding errors.
Stop using STAMP_TICKS_PER_SECOND: Apple doesn't have it.
Fix a copy-paste error in an earlier fixup commit
dgoulet-tor
Copy link
Contributor

dgoulet-tor commented on 2c4eef1 Apr 13, 2018

You sure about this 300 * 1000 ?

Isn't 5 minutes backward would be UINT32_MAX - 300 ? Lets consider this example:

now_ts = 100
last_refilled_as_ts = 400

Considering that now_ts jumped backwards of 300 seconds.

elapsed_ticks = 4294966995 or in other words UINT32_MAX - 300.

Also, 48 days in seconds is 4147200 which is not really 300 * 1000 so I'm puzzled here?

nmathewson
Copy link
Contributor

nmathewson commented on 2c4eef1 Apr 13, 2018

Ticks are about 1ms long. (They're somewhere between 0.5 and 2.0 ms, technically.)

if (bucket->write_bucket > (int32_t)burst)
bucket->write_bucket = burst;
bucket->read_bucket = MIN(bucket->read_bucket, (int32_t)burst);
bucket->read_bucket = MIN(bucket->write_bucket, (int32_t)burst);
}
Copy link
Contributor

@dgoulet-tor dgoulet-tor Apr 13, 2018

write_bucket for the second.

@nmathewson
Copy link
Contributor Author

@nmathewson nmathewson commented May 7, 2018

This was squashed and merged with b152d62

@nmathewson nmathewson closed this May 7, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment