Add an "AccountingRule" feature to permit limiting bw usage by read+write

Patch from "chobe".  Closes ticket 961.
This commit is contained in:
Nick Mathewson 2014-09-23 08:34:22 -04:00
parent e6150c7fc0
commit 8527a29966
10 changed files with 161 additions and 30 deletions

View File

@ -1635,19 +1635,31 @@ is non-zero):
to 0 will disable the heartbeat. (Default: 6 hours)
[[AccountingMax]] **AccountingMax** __N__ **bytes**|**KBytes**|**MBytes**|**GBytes**|**KBits**|**MBits**|**GBits**|**TBytes**::
Never send more than the specified number of bytes in a given accounting
period, or receive more than that number in the period. For example, with
AccountingMax set to 1 GByte, a server could send 900 MBytes and
receive 800 MBytes and continue running. It will only hibernate once
one of the two reaches 1 GByte. When the number of bytes gets low,
Tor will stop accepting new connections and circuits. When the
number of bytes is exhausted, Tor will hibernate until some
time in the next accounting period. To prevent all servers from waking at
the same time, Tor will also wait until a random point in each period
before waking up. If you have bandwidth cost issues, enabling hibernation
is preferable to setting a low bandwidth, since it provides users with a
collection of fast servers that are up some of the time, which is more
useful than a set of slow servers that are always "available".
Limits the max number of bytes sent and received within a set time period
using a given calculation rule (see: AccountingStart, AccountingRule).
Useful if you need to stay under a specific bandwidth. By default, the
number used for calculation is the max of either the bytes sent or
received. For example, with AccountingMax set to 1 GByte, a server
could send 900 MBytes and receive 800 MBytes and continue running.
It will only hibernate once one of the two reaches 1 GByte. This can
be changed to use the sum of the both bytes received and sent by setting
the AccountingRule option to "sum" (total bandwidth in/out). When the
number of bytes remaining gets low, Tor will stop accepting new connections
and circuits. When the number of bytes is exhausted, Tor will hibernate
until some time in the next accounting period. To prevent all servers
from waking at the same time, Tor will also wait until a random point
in each period before waking up. If you have bandwidth cost issues,
enabling hibernation is preferable to setting a low bandwidth, since
it provides users with a collection of fast servers that are up some
of the time, which is more useful than a set of slow servers that are
always "available".
[[AccountingRule]] **AccountingRule** **sum**|**max**::
How we determine when our AccountingMax has been reached (when we
should hibernate) during a time interval. Set to "max" to calculate
using the higher of either the sent or received bytes (this is the
default functionality). Set to "sum" to calculate using the sent
plus received bytes.
[[AccountingStart]] **AccountingStart** **day**|**week**|**month** [__day__] __HH:MM__::
Specify how long accounting periods last. If **month** is given, each

View File

@ -126,6 +126,7 @@ static config_abbrev_t option_abbrevs_[] = {
*/
static config_var_t option_vars_[] = {
V(AccountingMax, MEMUNIT, "0 bytes"),
V(AccountingRule, STRING, "max"),
V(AccountingStart, STRING, NULL),
V(Address, STRING, NULL),
V(AllowDotExit, BOOL, "0"),
@ -3108,6 +3109,10 @@ options_validate(or_options_t *old_options, or_options_t *options,
"risky: they will all turn off at the same time, which may "
"alert observers that they are being run by the same party.");
}
if (options->AccountingRule &&
strcmp(options->AccountingRule, "sum") != 0 &&
strcmp(options->AccountingRule, "max") != 0)
REJECT("AccountingRule must be 'sum' or 'max'");
}
if (options->HTTPProxy) { /* parse it now */

View File

@ -410,6 +410,16 @@ configure_accounting(time_t now)
accounting_set_wakeup_time();
}
/** Return the relevant number of bytes sent/received this interval
* based on the set AccountingRule */
static uint64_t
get_accounting_bytes(void)
{
if (strcmp(get_options()->AccountingRule, "sum") == 0)
return n_bytes_read_in_interval+n_bytes_written_in_interval;
return MAX(n_bytes_read_in_interval, n_bytes_written_in_interval);
}
/** Set expected_bandwidth_usage based on how much we sent/received
* per minute last interval (if we were up for at least 30 minutes),
* or based on our declared bandwidth otherwise. */
@ -421,6 +431,11 @@ update_expected_bandwidth(void)
uint64_t max_configured = (options->RelayBandwidthRate > 0 ?
options->RelayBandwidthRate :
options->BandwidthRate) * 60;
/* max_configured is the larger of bytes read and bytes written
* If we are accounting based on sum, worst case is both are
* at max, doubling the expected sum of bandwidth */
if (strcmp(get_options()->AccountingRule, "sum") == 0)
max_configured *= 2;
#define MIN_TIME_FOR_MEASUREMENT (1800)
@ -439,8 +454,7 @@ update_expected_bandwidth(void)
* doesn't know to store soft-limit info. Just take rate at which
* we were reading/writing in the last interval as our expected rate.
*/
uint64_t used = MAX(n_bytes_written_in_interval,
n_bytes_read_in_interval);
uint64_t used = get_accounting_bytes();
expected = used / (n_seconds_active_in_interval / 60);
} else {
/* If we haven't gotten enough data last interval, set 'expected'
@ -715,8 +729,7 @@ hibernate_hard_limit_reached(void)
uint64_t hard_limit = get_options()->AccountingMax;
if (!hard_limit)
return 0;
return n_bytes_read_in_interval >= hard_limit
|| n_bytes_written_in_interval >= hard_limit;
return get_accounting_bytes() >= hard_limit;
}
/** Return true iff we have sent/received almost all the bytes we are willing
@ -747,8 +760,7 @@ hibernate_soft_limit_reached(void)
if (!soft_limit)
return 0;
return n_bytes_read_in_interval >= soft_limit
|| n_bytes_written_in_interval >= soft_limit;
return get_accounting_bytes() >= soft_limit;
}
/** Called when we get a SIGINT, or when bandwidth soft limit is
@ -772,8 +784,7 @@ hibernate_begin(hibernate_state_t new_state, time_t now)
hibernate_state == HIBERNATE_STATE_LIVE) {
soft_limit_hit_at = now;
n_seconds_to_hit_soft_limit = n_seconds_active_in_interval;
n_bytes_at_soft_limit = MAX(n_bytes_read_in_interval,
n_bytes_written_in_interval);
n_bytes_at_soft_limit = get_accounting_bytes();
}
/* close listeners. leave control listener(s). */
@ -1003,13 +1014,22 @@ getinfo_helper_accounting(control_connection_t *conn,
U64_PRINTF_ARG(n_bytes_written_in_interval));
} else if (!strcmp(question, "accounting/bytes-left")) {
uint64_t limit = get_options()->AccountingMax;
uint64_t read_left = 0, write_left = 0;
if (n_bytes_read_in_interval < limit)
read_left = limit - n_bytes_read_in_interval;
if (n_bytes_written_in_interval < limit)
write_left = limit - n_bytes_written_in_interval;
tor_asprintf(answer, U64_FORMAT" "U64_FORMAT,
U64_PRINTF_ARG(read_left), U64_PRINTF_ARG(write_left));
if (strcmp(get_options()->AccountingRule, "sum") == 0) {
uint64_t total_left = 0;
uint64_t total_bytes = get_accounting_bytes();
if (total_bytes < limit)
total_left = limit - total_bytes;
tor_asprintf(answer, U64_FORMAT" "U64_FORMAT,
U64_PRINTF_ARG(total_left), U64_PRINTF_ARG(total_left));
} else {
uint64_t read_left = 0, write_left = 0;
if (n_bytes_read_in_interval < limit)
read_left = limit - n_bytes_read_in_interval;
if (n_bytes_written_in_interval < limit)
write_left = limit - n_bytes_written_in_interval;
tor_asprintf(answer, U64_FORMAT" "U64_FORMAT,
U64_PRINTF_ARG(read_left), U64_PRINTF_ARG(write_left));
}
} else if (!strcmp(question, "accounting/interval-start")) {
*answer = tor_malloc(ISO_TIME_LEN+1);
format_iso_time(*answer, interval_start_time);

View File

@ -28,6 +28,7 @@ void consider_hibernation(time_t now);
int getinfo_helper_accounting(control_connection_t *conn,
const char *question, char **answer,
const char **errmsg);
uint64_t get_accounting_max_total(void);
#ifdef HIBERNATE_PRIVATE
/** Possible values of hibernate_state */

View File

@ -3775,6 +3775,10 @@ typedef struct {
uint64_t AccountingMax; /**< How many bytes do we allow per accounting
* interval before hibernation? 0 for "never
* hibernate." */
char *AccountingRule; /**< How do we determine when our AccountingMax
* has been reached?
* "max" for when in or out reaches AccountingMax
* "sum for when in plus out reaches AccountingMax */
/** Base64-encoded hash of accepted passwords for the control system. */
config_line_t *HashedControlPassword;

View File

@ -1080,6 +1080,7 @@ decide_to_advertise_dirport(const or_options_t *options, uint16_t dir_port)
* they're confused or to get statistics. */
int interval_length = accounting_get_interval_length();
uint32_t effective_bw = get_effective_bwrate(options);
uint64_t acc_bytes;
if (!interval_length) {
log_warn(LD_BUG, "An accounting interval is not allowed to be zero "
"seconds long. Raising to 1.");
@ -1090,8 +1091,12 @@ decide_to_advertise_dirport(const or_options_t *options, uint16_t dir_port)
"accounting interval length %d", effective_bw,
U64_PRINTF_ARG(options->AccountingMax),
interval_length);
acc_bytes = options->AccountingMax;
if (strcmp(options->AccountingRule, "sum") == 0)
acc_bytes /= 2;
if (effective_bw >=
options->AccountingMax / interval_length) {
acc_bytes / interval_length) {
new_choice = 0;
reason = "AccountingMax enabled";
}

View File

@ -145,10 +145,15 @@ log_accounting(const time_t now, const or_options_t *options)
or_state_t *state = get_or_state();
char *acc_rcvd = bytes_to_usage(state->AccountingBytesReadInInterval);
char *acc_sent = bytes_to_usage(state->AccountingBytesWrittenInInterval);
char *acc_max = bytes_to_usage(options->AccountingMax);
const char *acc_rule = options->AccountingRule;
uint64_t acc_bytes = options->AccountingMax;
char *acc_max;
time_t interval_end = accounting_get_end_time();
char end_buf[ISO_TIME_LEN + 1];
char *remaining = NULL;
if (strcmp(acc_rule, "sum") == 0)
acc_bytes *= 2;
acc_max = bytes_to_usage(acc_bytes);
format_local_iso_time(end_buf, interval_end);
remaining = secs_to_uptime(interval_end - now);

View File

@ -34,6 +34,7 @@ src_test_test_SOURCES = \
src/test/test_logging.c \
src/test/test_microdesc.c \
src/test/test_oom.c \
src/test/test_accounting.c \
src/test/test_options.c \
src/test/test_pt.c \
src/test/test_relaycell.c \

View File

@ -1305,6 +1305,7 @@ extern struct testcase_t hs_tests[];
extern struct testcase_t nodelist_tests[];
extern struct testcase_t routerkeys_tests[];
extern struct testcase_t oom_tests[];
extern struct testcase_t accounting_tests[];
extern struct testcase_t policy_tests[];
extern struct testcase_t status_tests[];
extern struct testcase_t routerset_tests[];
@ -1337,6 +1338,7 @@ static struct testgroup_t testgroups[] = {
{ "nodelist/", nodelist_tests },
{ "routerkeys/", routerkeys_tests },
{ "oom/", oom_tests },
{ "accounting/", accounting_tests },
{ "policy/" , policy_tests },
{ "status/" , status_tests },
{ "routerset/" , routerset_tests },

View File

@ -0,0 +1,76 @@
#include "or.h"
#include "test.h"
#define HIBERNATE_PRIVATE
#include "hibernate.h"
#include "config.h"
#define STATEFILE_PRIVATE
#include "statefile.h"
#define NS_MODULE accounting
#define NS_SUBMODULE limits
/*
* Test to make sure accounting triggers hibernation
* correctly with both sum or max rules set
*/
static or_state_t *or_state;
NS_DECL(or_state_t *, get_or_state, (void));
static or_state_t *
NS(get_or_state)(void)
{
return or_state;
}
static void
test_accounting_limits(void *arg)
{
or_options_t *options = get_options_mutable();
time_t fake_time = time(NULL);
(void) arg;
NS_MOCK(get_or_state);
or_state = or_state_new();
options->AccountingMax = 100;
options->AccountingRule = tor_strdup("max");
tor_assert(accounting_is_enabled(options));
configure_accounting(fake_time);
accounting_add_bytes(10, 0, 1);
fake_time += 1;
consider_hibernation(fake_time);
tor_assert(we_are_hibernating() == 0);
accounting_add_bytes(90, 0, 1);
fake_time += 1;
consider_hibernation(fake_time);
tor_assert(we_are_hibernating() == 1);
options->AccountingMax = 200;
options->AccountingRule = tor_strdup("sum");
accounting_add_bytes(0, 10, 1);
fake_time += 1;
consider_hibernation(fake_time);
tor_assert(we_are_hibernating() == 0);
accounting_add_bytes(0, 90, 1);
fake_time += 1;
consider_hibernation(fake_time);
tor_assert(we_are_hibernating() == 1);
goto done;
done:
NS_UNMOCK(get_or_state);
or_state_free(or_state);
}
#undef NS_SUBMODULE
struct testcase_t accounting_tests[] = {
{ "bwlimits", test_accounting_limits, TT_FORK, NULL, NULL },
END_OF_TESTCASES
};