Browse Source

Expose more session control knobs to the operator

patch-2
Neil Booth 6 years ago
parent
commit
91576490c1
  1. 39
      docs/environment.rst
  2. 4
      electrumx/server/env.py
  3. 5
      electrumx/server/session.py
  4. 10
      tests/server/test_env.py

39
docs/environment.rst

@ -248,12 +248,15 @@ raise them.
.. envvar:: COST_SOFT_LIMIT .. envvar:: COST_SOFT_LIMIT
.. envvar:: COST_HARD_LIMIT .. envvar:: COST_HARD_LIMIT
.. envvar:: REQUEST_SLEEP
.. envvar:: INITIAL_CONCURRENT
Session cost soft and hard limits as integers. The default values are :const:`500` All values are integers. :envar:`COST_SOFT_LIMIT` defaults to :const:`1,000`,
and :const:`10,000` respectively. :envvar:`COST_HARD_LIMIT` to :const:`10,000`, :envvar:`REQUEST_SLEEP` to :const:`2,500`
milliseconds, and :envvar:`INITIAL_CONCURRENT` to :const:`10` concurrent requests.
The server prices each request made to it based upon an estimate of the resources needed The server prices each request made to it based upon an estimate of the resources needed
to process it . Factors include whether the request uses bitcoind, how much bandwidth to process it. Factors include whether the request uses bitcoind, how much bandwidth
it uses, and how hard it hits the databases. it uses, and how hard it hits the databases.
To set a base for the units, a :func:`blockchain.scripthash.subscribe` subscription to To set a base for the units, a :func:`blockchain.scripthash.subscribe` subscription to
@ -261,14 +264,28 @@ raise them.
considering the bandwidth consumed. :func:`server.ping` is costed at :const:`0.1`. considering the bandwidth consumed. :func:`server.ping` is costed at :const:`0.1`.
As the total cost of a session goes over the soft limit, its requests start to be As the total cost of a session goes over the soft limit, its requests start to be
throttled in two ways. First, each request sleeps a little before being handled. throttled in two ways. First, the number of requests for that session that the server
Second, the number of requests that the server will handle concurrently reduces. Both will process concurrently is reduced. Second, each request starts to sleep a little
effects increase as the hard limit is approached, at which point the session is before being handled.
disconnected.
Before throttling starts, the server will process up to :envvar:`INITIAL_CONCURRENT`
So non-abusive sessions can continue to be served, a session's cost gradually decays requests concurrently without sleeping. As the session cost ranges from
over time. Subscriptions have an ongoing servicing cost, so the decay is slower as the :envar:`COST_SOFT_LIMIT` to :envvar:`COST_HARD_LIMIT`, concurrency drops linearly to
number of subscriptions increases. zero and each request's sleep time increases linearly up to :envvar:`REQUEST_SLEEP`
milliseconds. Once the hard limit is reached, the session is disconnected.
In order that non-abusive sessions can continue to be served, a session's cost gradually
decays over time. Subscriptions have an ongoing servicing cost, so the decay is slower
as the number of subscriptions increases.
If a session disconnects, ElectrumX continues to associate its cost with its IP address,
so if it immediately reconnects it will re-acquire its previous cost allocation.
A server operator should experiment with different values according to server loads. It
is not necessarily true that e.g. having a low soft limit, decreasing concurrency and
increasing sleep will help handling heavy loads, as it will also increase the backlog of
requests the server has to manage in memory. It will also give a much worse experience
for genuine connections.
.. envvar:: BANDWIDTH_UNIT_COST .. envvar:: BANDWIDTH_UNIT_COST

4
electrumx/server/env.py

@ -68,9 +68,11 @@ class Env(EnvBase):
# Server limits to help prevent DoS # Server limits to help prevent DoS
self.max_send = self.integer('MAX_SEND', self.coin.DEFAULT_MAX_SEND) self.max_send = self.integer('MAX_SEND', self.coin.DEFAULT_MAX_SEND)
self.max_sessions = self.sane_max_sessions() self.max_sessions = self.sane_max_sessions()
self.cost_soft_limit = self.integer('COST_SOFT_LIMIT', 500) self.cost_soft_limit = self.integer('COST_SOFT_LIMIT', 1000)
self.cost_hard_limit = self.integer('COST_HARD_LIMIT', 10000) self.cost_hard_limit = self.integer('COST_HARD_LIMIT', 10000)
self.bw_unit_cost = self.integer('BANDWIDTH_UNIT_COST', 5000) self.bw_unit_cost = self.integer('BANDWIDTH_UNIT_COST', 5000)
self.initial_concurrent = self.integer('INITIAL_CONCURRENT', 10)
self.request_sleep = self.integer('REQUEST_SLEEP', 2500)
self.session_timeout = self.integer('SESSION_TIMEOUT', 600) self.session_timeout = self.integer('SESSION_TIMEOUT', 600)
self.drop_client = self.custom("DROP_CLIENT", None, re.compile) self.drop_client = self.custom("DROP_CLIENT", None, re.compile)
self.blacklist_url = self.default('BLACKLIST_URL', self.coin.BLACKLIST_URL) self.blacklist_url = self.default('BLACKLIST_URL', self.coin.BLACKLIST_URL)

5
electrumx/server/session.py

@ -484,12 +484,17 @@ class SessionManager(object):
session_class.cost_hard_limit = self.env.cost_hard_limit session_class.cost_hard_limit = self.env.cost_hard_limit
session_class.cost_decay_per_sec = session_class.cost_hard_limit / 10000 session_class.cost_decay_per_sec = session_class.cost_hard_limit / 10000
session_class.bw_cost_per_byte = 1.0 / self.env.bw_unit_cost session_class.bw_cost_per_byte = 1.0 / self.env.bw_unit_cost
session_class.cost_sleep = self.env.request_sleep / 1000
session_class.initial_concurrent = self.env.initial_concurrent
self.logger.info(f'max session count: {self.env.max_sessions:,d}') self.logger.info(f'max session count: {self.env.max_sessions:,d}')
self.logger.info(f'session timeout: {self.env.session_timeout:,d} seconds') self.logger.info(f'session timeout: {self.env.session_timeout:,d} seconds')
self.logger.info(f'session cost hard limit {self.env.cost_hard_limit:,d}') self.logger.info(f'session cost hard limit {self.env.cost_hard_limit:,d}')
self.logger.info(f'session cost soft limit {self.env.cost_soft_limit:,d}') self.logger.info(f'session cost soft limit {self.env.cost_soft_limit:,d}')
self.logger.info(f'bandwidth unit cost {self.env.bw_unit_cost:,d}') self.logger.info(f'bandwidth unit cost {self.env.bw_unit_cost:,d}')
self.logger.info(f'request sleep {self.env.request_sleep:,d}ms')
self.logger.info(f'initial concurrent {self.env.initial_concurrent:,d}')
self.logger.info(f'max response size {self.env.max_send:,d} bytes') self.logger.info(f'max response size {self.env.max_send:,d} bytes')
if self.env.drop_client is not None: if self.env.drop_client is not None:
self.logger.info('drop clients matching: {}' self.logger.info('drop clients matching: {}'

10
tests/server/test_env.py

@ -201,7 +201,15 @@ def test_COST_HARD_LIMIT():
def test_COST_SOFT_LIMIT(): def test_COST_SOFT_LIMIT():
assert_integer('COST_SOFT_LIMIT', 'cost_soft_limit', 500) assert_integer('COST_SOFT_LIMIT', 'cost_soft_limit', 1000)
def test_INITIAL_CONCURRENT():
assert_integer('INITIAL_CONCURRENT', 'initial_concurrent', 10)
def test_REQUEST_SLEEP():
assert_integer('REQUEST_SLEEP', 'request_sleep', 2500)
def test_BANDWIDTH_UNIT_COST(): def test_BANDWIDTH_UNIT_COST():

Loading…
Cancel
Save