Add week 8 materials

This commit is contained in:
osukhoroslov committed 2026-11-04 19:23:39 +03:00
1 parent a987b82834
commit 0bf71ed374
38 files changed
+2908

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

@@ -0,0 +1,30 @@
services:
kv1:
build:
context: ./storage
ports:
- 5001:5000
command: -n kv1
kv2:
build:
context: ./storage
ports:
- 5002:5000
command: -n kv2
kv3:
build:
context: ./storage
ports:
- 5003:5000
command: -n kv3
proxy:
build:
context: ./proxy
ports:
- 5005:5000
command: -c compose_config.yml
volumes:
- ./proxy/compose_config.yml:/app/compose_config.yml:ro
@@ -0,0 +1,9 @@
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
ENTRYPOINT [ "python", "main.py"]
Whitespace-only changes.
@@ -0,0 +1,8 @@
mode: sharding
nodes:
- name: node1
url: http://kv1:5000
- name: node2
url: http://kv2:5000
- name: node3
url: http://kv3:5000
@@ -0,0 +1,76 @@
import argparse
import requests
from strategies import (
Proxy, ShardingProxy,
ReplicatingProxy, ReplicatingQuorumProxy,
RAID3Proxy
)
import yaml
from flask import Flask, jsonify, request
app = Flask(__name__)
proxy: Proxy = None
@app.route('/put', methods=['POST'])
def put_handler():
key = request.args.get('key')
value = request.get_json().get('value')
if key is None or value is None:
return jsonify({'error': 'Please provide key and value.'}), 400
return proxy.put(key, value)
@app.route('/get', methods=['GET'])
def get_handler():
key = request.args.get('key')
if key is None:
return jsonify({'error': 'Please provide key.'}), 400
return proxy.get(key)
@app.route('/state', methods=['GET'])
def state_handler():
results = ""
for _, node in proxy.nodes.items():
url = f'{node["url"]}/state'
response = requests.get(url)
results += f'<h1 style="font-family: monospace;">Response from {url}</h1>{response.content.decode("utf-8")}<hr>'
return f"<html><body>{results}</body></html>", 200
def load_proxy_from_config(config_path):
with open(config_path, 'r') as file:
config = yaml.safe_load(file)
mode = config.get('mode', '')
nodes = {}
for node in config['nodes']:
name = node['name']
url = node['url']
nodes[name] = {'url': url}
if mode == 'sharding':
return ShardingProxy(nodes)
elif mode == 'replication':
return ReplicatingProxy(nodes)
elif mode == 'replication-quorum':
return ReplicatingQuorumProxy(nodes)
elif mode == 'replication-raid3':
return RAID3Proxy(nodes)
raise ValueError('Invalid mode, allowed values: sharding, replication, replication-quorum')
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Flask app acting as a proxy for key-value nodes')
parser.add_argument('-c', '--config', type=str, required=True, help='Path to the YAML config file')
parser.add_argument('-p', '--port', type=int, help='Port of the server')
args = parser.parse_args()
proxy = load_proxy_from_config(args.config)
app.run('0.0.0.0', args.port)
@@ -0,0 +1,3 @@
Flask==2.3.3
requests==2.31.0
pyyaml==6.0.1
@@ -0,0 +1,244 @@
from dataclasses import dataclass
import json
from collections import defaultdict
import math
from typing import Union
from flask import jsonify
import requests
from abc import ABC, abstractmethod
import base64
from requests.exceptions import ConnectionError
import hashlib
class Proxy(ABC):
def __init__(self, nodes):
self.nodes = nodes
@abstractmethod
def put(self, key, value):
pass
@abstractmethod
def get(self, key):
pass
class ShardingProxy(Proxy):
def __init__(self, nodes):
super().__init__(nodes)
def put(self, key, value):
node = self.get_node(key)
if node is None:
return {'error': 'No nodes available.'}, 500
url = f'{node["url"]}/put?key={key}'
response = requests.post(url, json={'value': value})
return response.content, response.status_code
def get(self, key):
node = self.get_node(key)
if node is None:
return {'error': 'No nodes available.'}, 500
url = f'{node["url"]}/get?key={key}'
response = requests.get(url)
return response.content, response.status_code
def get_node(self, key):
if len(self.nodes) == 0:
return None
hash_val = hashlib.sha256(key.encode()).hexdigest()
node_index = int(hash_val, 16) % len(self.nodes)
return self.nodes[list(self.nodes.keys())[node_index]]
class ReplicatingProxy(Proxy):
def __init__(self, nodes):
super().__init__(nodes)
def put(self, key, value):
responses = []
for _, node in self.nodes.items():
url = f'{node["url"]}/put?key={key}'
response = requests.post(url, json={'value': value})
if response.status_code == 200:
responses.append(response.content)
if len(responses) == 0:
return {'error': 'No nodes available.'}, 500
return responses[0], 200
def get(self, key):
non_ok_responses = dict()
for node_name, node in self.nodes.items():
url = f'{node["url"]}/get?key={key}'
response = requests.get(url)
if response.status_code == 200:
return response.content, 200
else:
non_ok_responses[node_name] = response.json()
return jsonify({'error': 'None of the nodes responded with 200.', 'responses': non_ok_responses}), 500
class ReplicatingQuorumProxy(Proxy):
def __init__(self, nodes):
super().__init__(nodes)
def put(self, key, value):
responses = []
for _, node in self.nodes.items():
url = f'{node["url"]}/put?key={key}'
response = requests.post(url, json={'value': value})
if response.status_code == 200:
responses.append(response.content)
if len(responses) == 0:
return {'error': 'No nodes available.'}, 500
if len(responses) > len(self.nodes) / 2:
return responses[0], 200
return jsonify({'error': 'Not enough responses for quorum write.', 'responses': responses}), 300
def get(self, key):
non_ok_responses = dict()
values = defaultdict(lambda: 0)
for node_name, node in self.nodes.items():
url = f'{node["url"]}/get?key={key}'
response = requests.get(url)
if response.status_code == 200:
values[response.content] += 1
else:
non_ok_responses[node_name] = response.json()
for value, cnt in values.items():
if cnt > len(self.nodes) / 2:
return value, 200
if len(values) > 0:
values_for_response = []
for value, cnt in values.items():
values_for_response.append({'value': json.loads(value)['value'], 'count': cnt})
return jsonify({'error': 'Got multiple values, none of them has enough responses for quorum', 'values': values_for_response}), 300
return jsonify({'error': 'None of the nodes responded with 200.', 'responses': non_ok_responses}), 500
@dataclass
class DataBlock:
index: Union[int, None]
data: bytes
def raid3_split(data_blocks: int, value: str) -> list[DataBlock]:
bytes_value = value.encode()
L = int(math.ceil(len(bytes_value) / data_blocks))
split: list[bytes] = [
bytes_value[i:i + L]
for i in range(0, len(bytes_value), L)
]
res = []
for i, block in enumerate(split):
data_bytes: bytes = block + (L - len(block)) * b'\x00'
res.append(DataBlock(i, data_bytes))
return res
def raid3_parity(blocks: list[DataBlock]) -> DataBlock:
parity = len(blocks[0].data) * b'\x00'
for block in blocks:
if len(block.data) != len(parity):
raise ValueError('All blocks must have the same length.')
parity = bytes(a ^ b for a, b in zip(parity, block.data))
return DataBlock(None, parity)
def raid3_join(blocks: list[DataBlock]) -> str:
return (b''.join(block.data for block in blocks[:-1])).rstrip(b'\x00').decode()
def raid3_recover(blocks: list[Union[DataBlock, None]], error_index: int) -> str:
if error_index == len(blocks) - 1:
return raid3_join(blocks)
recovered = None
for block in blocks:
if recovered is None and block is not None:
recovered = len(block.data) * b'\x00'
if block is None:
continue
recovered = bytes(a ^ b for a, b in zip(recovered, block.data))
blocks[error_index] = DataBlock(error_index, recovered)
return raid3_join(blocks)
class RAID3Proxy(Proxy):
def __init__(self, nodes):
if len(nodes) < 3:
raise ValueError('RAID3 requires at least 3 nodes.')
if len(nodes) > 255:
raise ValueError('This implementation does not support more than 255 nodes.')
super().__init__(nodes)
self._divisor = len(self.nodes) - 1
def put(self, key, value):
if len(value) < self._divisor:
return {'error': f'Too small body for RAID3 algorithm with {self._divisor} blocks.'}, 500
blocks = raid3_split(self._divisor, value)
parity = raid3_parity(blocks)
for node, block in zip(self.nodes.values(), blocks + [parity]):
value_dict = {
'index': block.index,
'value': base64.b64encode(block.data).decode('utf-8'),
}
url = f'{node["url"]}/put?key={key}'
response = requests.post(url, json={'value': json.dumps(value_dict)})
if response.status_code != 200:
return {'error': f'Error while writing to node {node["url"]}.'}, 500
return jsonify({'message': 'Key-value pair stored successfully on all nodes.'}), 200
def get(self, key):
blocks = []
errors = []
error_index = None
for node in self.nodes.values():
url = f'{node["url"]}/get?key={key}'
try:
response = requests.get(url)
if response.status_code == 200:
value_dict = json.loads(response.json()['value'])
blocks.append(DataBlock(value_dict['index'], base64.b64decode(value_dict['value'])))
else:
error_index = len(blocks)
blocks.append(None)
errors.append(response)
if len(errors) > 1:
return {'error': f'Got too many errors while reading key \'{key}\''}, 500
except ConnectionError as e:
error_index = len(blocks)
blocks.append(None)
errors.append(e)
if len(errors) > 1:
return {'error': f'Got too many errors while reading key \'{key}\''}, 500
if error_index is None:
value = raid3_join(blocks)
else:
value = raid3_recover(blocks, error_index)
return jsonify({'value': value}), 200
@@ -0,0 +1,20 @@
from .strategies import raid3_join, raid3_split
def test_raid3_split():
value = 'Hello, world!' # len = 13
blocks = raid3_split(2, value) # 2 data blocks of 7 bytes, 1 parity block
assert len(blocks) == 2
assert blocks[0].index == 0
assert blocks[0].data == b'Hello, '
assert blocks[1].index == 1
assert blocks[1].data == b'world!\x00'
def test_raid3_join_utf():
value = 'съешь ещё этих мягких французских булок'
blocks = raid3_split(4, value)
assert len(blocks) == 4
joined = raid3_join(blocks + [None])
assert joined == value
+124
View File
@@ -0,0 +1,124 @@
# Шардирование на примере простого key-value-хранилища
Этот пример показывает, как разделить данные между несколькими узлами. В стенде работают три простых
key-value-хранилища: каждое принимает ключ и значение и сохраняет их в памяти. Перед ними находится прокси
с единым API. Клиент обращается только к прокси, а тот определяет владельца ключа и пересылает запрос
нужному хранилищу.
В основном режиме прокси вычисляет `SHA-256(key) mod N` и направляет запись и чтение владельцу ключа.
В конце также кратко рассмотрен учебный режим репликации.
## API
- `POST /put?key=<key>` записывает значение из JSON-поля `value`.
- `GET /get?key=<key>` возвращает значение или `404`, если ключа нет на выбранном узле.
- `GET /state` показывает содержимое всех хранилищ.
## Запуск
Из корня репозитория:
```bash
cd materials/08-scaling/seminar/kv
docker compose up -d --build
docker compose ps
```
Прокси будет доступен на порту 5005. Хранилища будут доступны на портах 5001, 5002 и 5003.
## Запись и чтение
Записать одно значение через прокси:
```bash
curl -sS -X POST 'http://localhost:5005/put?key=key-0' \
-H 'Content-Type: application/json' \
-d '{"value":"value-0"}'
```
Прочитать его:
```bash
curl -sS 'http://localhost:5005/get?key=key-0'
```
Записать `N` значений с ключами от `key-0` до `key-(N-1)`:
```bash
N=20
for ((i = 0; i < N; i++)); do
curl -sS -X POST "http://localhost:5005/put?key=key-$i" \
-H 'Content-Type: application/json' \
-d "{\"value\":\"value-$i\"}"
echo
done
```
## Проверка распределения
Получить состояние всех узлов через прокси:
```bash
open 'http://localhost:5005/state'
```
Состояние отдельных хранилищ:
```bash
curl -sS 'http://localhost:5001/state'
curl -sS 'http://localhost:5002/state'
```
Для текущей конфигурации `key-0` находится на третьем узле:
```bash
curl -i 'http://localhost:5003/get?key=key-0' # 200
curl -i 'http://localhost:5002/get?key=key-0' # 404
```
## Изменение конфигурации
Состав узлов задаётся в `proxy/compose_config.yml`:
```yaml
mode: sharding
nodes:
- name: node1
url: http://kv1:5000
- name: node2
url: http://kv2:5000
- name: node3
url: http://kv3:5000
```
Конфигурация подключена к контейнеру как файл только для чтения. После её изменения достаточно
перезапустить прокси:
```bash
docker compose restart proxy
```
Хранилища при этом не перезапускаются, и данные в их памяти сохраняются.
## Режим репликации
В режиме `replication` прокси записывает каждое значение во все хранилища. При чтении он опрашивает
узлы по очереди и возвращает первый успешный ответ. В отличие от шардирования, данные не разделяются,
а копируются.
Чтобы включить этот режим, изменить первую строку в `proxy/compose_config.yml`:
```yaml
mode: replication
```
Затем перезапустить только прокси и повторить команды записи и чтения:
```bash
docker compose restart proxy
open 'http://localhost:5005/state'
```
После записи новый ключ должен появиться на всех трёх узлах. Это упрощённая учебная реализация:
она не гарантирует согласованность при частичных записях и не обрабатывает отказы как промышленная система.
@@ -0,0 +1,9 @@
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
ENTRYPOINT [ "python", "main.py"]
@@ -0,0 +1,64 @@
import argparse
from flask import Flask, jsonify, request
app = Flask(__name__)
data = {}
@app.route('/put', methods=['POST'])
def put_handler():
key = request.args.get('key')
value = request.get_json().get('value')
if key is None or value is None:
return jsonify({'error': 'Please provide key and value.'}), 400
data[key] = value
return jsonify({'message': 'Key-value pair stored successfully.'}), 200
@app.route('/get', methods=['GET'])
def get_handler():
key = request.args.get('key')
if key is None:
return jsonify({'error': 'Please provide key.'}), 400
value = data.get(key)
if value is None:
return jsonify({'error': 'Key not found.', 'key': key}), 404
return jsonify({'value': value}), 200
@app.route('/state', methods=['GET'])
def state_handler():
table = '<table>'
table += f'<tr><th colspan="2" style="font-size: 24px; font-weight: bold; font-family: monospace; padding-bottom: 10px;">Data on {app.name}</th></tr>'
table += '<tr><th style="font-weight: bold; text-align: left; font-family: monospace; padding-bottom: 15px;">Key</th><th style="font-family: monospace; text-align: left; padding-bottom: 15px; padding-left: 20px;">Value</th></tr>'
for key, value in data.items():
value = value[:64] + f'...[total of {len(value)} bytes]' if len(value) > 100 else value
table += f'<tr><td style="font-weight: bold; font-family: monospace;">{key}</td><td style="font-family: monospace; text-align: left; padding-left: 20px;">{value}</td></tr>'
table += '</table>'
return table
@app.route('/health', methods=['GET'])
def health_handler():
return f'Hi from {app.name}'
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Flask app with hostname as app name')
parser.add_argument('-n', '--hostname', type=str, help='Hostname of the server')
parser.add_argument('-p', '--port', type=int, help='Port of the server')
args = parser.parse_args()
if args.hostname:
app.name = args.hostname
app.run('0.0.0.0', args.port)
@@ -0,0 +1,3 @@
Flask==2.3.3
requests==2.31.0
pyyaml==6.0.1
@@ -0,0 +1,8 @@
target
**/target
**/__pycache__
**/.pytest_cache
*.pyc
Lab.ipynb
readme.md
media/
+725
View File
@@ -0,0 +1,725 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "cfg-if"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
[[package]]
name = "clap"
version = "3.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
dependencies = [
"atty",
"bitflags",
"clap_derive",
"clap_lex",
"indexmap 1.9.3",
"once_cell",
"strsim",
"termcolor",
"textwrap",
]
[[package]]
name = "clap_derive"
version = "3.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "clap_lex"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
dependencies = [
"os_str_bytes",
]
[[package]]
name = "colored"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.59.0",
]
[[package]]
name = "distsys-lb-lab"
version = "0.1.0"
dependencies = [
"clap",
"rand",
"rand_distr",
"rand_pcg",
"serde",
"serde_json",
"simcore",
]
[[package]]
name = "downcast-rs"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
name = "dyn-clone"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "erased-serde"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7"
dependencies = [
"serde",
"typeid",
]
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "futures-sink"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "futures-task"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-util"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "getrandom"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
]
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
]
[[package]]
name = "indexmap"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921"
dependencies = [
"equivalent",
"hashbrown 0.15.5",
]
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]]
name = "libm"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
[[package]]
name = "log"
version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "memchr"
version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
"libm",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "os_str_bytes"
version = "6.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1"
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn 1.0.109",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "rand_distr"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31"
dependencies = [
"num-traits",
"rand",
]
[[package]]
name = "rand_pcg"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59cad018caf63deb318e5a4586d99a24424a364f40f1e5778c29aca23f4fc73e"
dependencies = [
"rand_core",
]
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "serde_json"
version = "1.0.143"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
dependencies = [
"indexmap 2.11.1",
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "serde_type_name"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92acc4cd6ae20767c54d6cf1a075624e7f4d9e99d7ebc685398ff243144d8714"
dependencies = [
"serde",
]
[[package]]
name = "simcore"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a20dfec85e94e569fa5b810b2d992c68b361f31bfba471447427536488c9c292"
dependencies = [
"colored",
"downcast-rs",
"dyn-clone",
"erased-serde",
"futures",
"log",
"rand",
"rand_pcg",
"rustc-hash",
"serde",
"serde_json",
"serde_type_name",
]
[[package]]
name = "slab"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "textwrap"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
[[package]]
name = "typeid"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
[[package]]
name = "unicode-ident"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.0",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-link"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "zerocopy"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
]
@@ -0,0 +1,15 @@
[package]
name = "distsys-lb-lab"
version = "0.1.0"
license = "MIT OR Apache-2.0"
edition = "2021"
resolver = "3"
[dependencies]
clap = { version = "3.1.17", features = ["cargo", "derive"] }
rand = "0.8.5"
rand_distr = "0.4.3"
rand_pcg = "0.3.1"
serde = { version = "1.0.142", features = ["derive"] }
serde_json = "1.0.107"
simcore = "0.1.0"
@@ -0,0 +1,19 @@
# syntax=docker/dockerfile:1
FROM rust:1.97.1-bookworm@sha256:0e2bcaef56d041a486784e54104a81aebe0da44bd03019bd70bc0401e42e4a97 AS builder
RUN apt-get update && apt-get install -y build-essential python3-dev
WORKDIR /lab
COPY . .
RUN --mount=type=cache,id=distsys-course-cargo-registry,target=/usr/local/cargo/registry,sharing=locked \
--mount=type=cache,id=distsys-course-cargo-target-rust-1-97-1,target=/lab/target,sharing=locked \
cargo install --locked --path . --root /opt/course
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y python3-dev wget && rm -rf /var/lib/apt/lists/*
COPY --from=builder /opt/course/bin/distsys-lb-lab /usr/local/bin/distsys-lb-lab
WORKDIR /lab
ENTRYPOINT ["distsys-lb-lab"]
@@ -0,0 +1,569 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "9ba1cf58",
"metadata": {},
"source": [
"# Балансировщики нагрузки: лабораторная работа"
]
},
{
"cell_type": "markdown",
"id": "0d8f9518",
"metadata": {},
"source": [
"В этой лабораторной работе сравним алгоритмы балансировки по задержкам запросов. Будем менять нагрузку, число балансировщиков и актуальность сведений о серверах. Перед запуском выполните [инструкции подготовки](readme.md): Docker-образ нужно собрать из текущих исходников."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "7c48777d",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAqIAAAIGCAYAAACPjO0LAAA1dnRFWHRteGZpbGUAJTNDbXhmaWxlJTIwaG9zdCUzRCUyMmFwcC5kaWFncmFtcy5uZXQlMjIlMjBtb2RpZmllZCUzRCUyMjIwMjMtMTAtMzFUMTQlM0E1NSUzQTIzLjcxMFolMjIlMjBhZ2VudCUzRCUyMk1vemlsbGElMkY1LjAlMjAoWDExJTNCJTIwTGludXglMjB4ODZfNjQlM0IlMjBydiUzQTEwOS4wKSUyMEdlY2tvJTJGMjAxMDAxMDElMjBGaXJlZm94JTJGMTA5LjAlMjIlMjBldGFnJTNEJTIyNGVnTjE0dlhvbmpYamZvd0hBaW0lMjIlMjB2ZXJzaW9uJTNEJTIyMjEuNi44JTIyJTIwdHlwZSUzRCUyMmRldmljZSUyMiUzRSUwQSUyMCUyMCUzQ2RpYWdyYW0lMjBuYW1lJTNEJTIyUGFnZS0xJTIyJTIwaWQlM0QlMjJ3dW0wNGlsSUJDZ19XaXFjdmtjZiUyMiUzRSUwQSUyMCUyMCUyMCUyMCUzQ214R3JhcGhNb2RlbCUyMGR4JTNEJTIyMTQzNCUyMiUyMGR5JTNEJTIyNzA2JTIyJTIwZ3JpZCUzRCUyMjAlMjIlMjBncmlkU2l6ZSUzRCUyMjEwJTIyJTIwZ3VpZGVzJTNEJTIyMSUyMiUyMHRvb2x0aXBzJTNEJTIyMSUyMiUyMGNvbm5lY3QlM0QlMjIxJTIyJTIwYXJyb3dzJTNEJTIyMSUyMiUyMGZvbGQlM0QlMjIxJTIyJTIwcGFnZSUzRCUyMjElMjIlMjBwYWdlU2NhbGUlM0QlMjIxJTIyJTIwcGFnZVdpZHRoJTNEJTIyODUwJTIyJTIwcGFnZUhlaWdodCUzRCUyMjExMDAlMjIlMjBtYXRoJTNEJTIyMCUyMiUyMHNoYWRvdyUzRCUyMjAlMjIlM0UlMEElMjAlMjAlMjAlMjAlMjAlMjAlM0Nyb290JTNFJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTNDbXhDZWxsJTIwaWQlM0QlMjIwJTIyJTIwJTJGJTNFJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTNDbXhDZWxsJTIwaWQlM0QlMjIxJTIyJTIwcGFyZW50JTNEJTIyMCUyMiUyMCUyRiUzRSUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUzQ214Q2VsbCUyMGlkJTNEJTIyYVVXOVFVaW5zME5Cek82b3dMM1MtMTYlMjIlMjB2YWx1ZSUzRCUyMiUyMiUyMHN0eWxlJTNEJTIycm91bmRlZCUzRDElM0J3aGl0ZVNwYWNlJTNEd3JhcCUzQmh0bWwlM0QxJTNCZ2xhc3MlM0QwJTNCJTIyJTIwdmVydGV4JTNEJTIyMSUyMiUyMHBhcmVudCUzRCUyMjElMjIlM0UlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlM0NteEdlb21ldHJ5JTIweCUzRCUyMjEyMCUyMiUyMHklM0QlMjIyNjAlMjIlMjB3aWR0aCUzRCUyMjU2MCUyMiUyMGhlaWdodCUzRCUyMjEyMCUyMiUyMGFzJTNEJTIyZ2VvbWV0cnklMjIlMjAlMkYlM0UlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlM0MlMkZteENlbGwlM0UlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlM0NteENlbGwlMjBpZCUzRCUyMmFVVzlRVWluczBOQnpPNm93TDNTLTElMjIlMjB2YWx1ZSUzRCUyMnNlcnZlciUyMiUyMHN0eWxlJTNEJTIyc2hhcGUlM0RjeWxpbmRlcjMlM0J3aGl0ZVNwYWNlJTNEd3JhcCUzQmh0bWwlM0QxJTNCYm91bmRlZExibCUzRDElM0JiYWNrZ3JvdW5kT3V0bGluZSUzRDElM0JzaXplJTNEMTUlM0IlMjIlMjB2ZXJ0ZXglM0QlMjIxJTIyJTIwcGFyZW50JTNEJTIyMSUyMiUzRSUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUzQ214R2VvbWV0cnklMjB4JTNEJTIyMTMwJTIyJTIweSUzRCUyMjQwMCUyMiUyMHdpZHRoJTNEJTIyNjAlMjIlMjBoZWlnaHQlM0QlMjI4MCUyMiUyMGFzJTNEJTIyZ2VvbWV0cnklMjIlMjAlMkYlM0UlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlM0MlMkZteENlbGwlM0UlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlM0NteENlbGwlMjBpZCUzRCUyMmFVVzlRVWluczBOQnpPNm93TDNTLTIlMjIlMjB2YWx1ZSUzRCUyMnNlcnZlciUyMiUyMHN0eWxlJTNEJTIyc2hhcGUlM0RjeWxpbmRlcjMlM0J3aGl0ZVNwYWNlJTNEd3JhcCUzQmh0bWwlM0QxJTNCYm91bmRlZExibCUzRDElM0JiYWNrZ3JvdW5kT3V0bGluZSUzRDElM0JzaXplJTNEMTUlM0IlMjIlMjB2ZXJ0ZXglM0QlMjIxJTIyJTIwcGFyZW50JTNEJTIyMSUyMiUzRSUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUzQ214R2VvbWV0cnklMjB4JTNEJTIyMjEwJTIyJTIweSUzRCUyMjQwMCUyMiUyMHdpZHRoJTNEJTIyNjAlMjIlMjBoZWlnaHQlM0QlMjI4MCUyMiUyMGFzJTNEJTIyZ2VvbWV0cnklMjIlMjAlMkYlM0UlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlM0MlMkZteENlbGwlM0UlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlM0NteENlbGwlMjBpZCUzRCUyMmFVVzlRVWluczBOQnpPNm93TDNTLTMlMjIlMjB2YWx1ZSUzRCUyMnNlcnZlciUyMiUyMHN0eWxlJTNEJTIyc2hhcGUlM0RjeWxpbmRlcjMlM0J3aGl0ZVNwYWNlJTNEd3JhcCUzQmh0bWwlM0QxJTNCYm91bmRlZExibCUzRDElM0JiYWNrZ3JvdW5kT3V0bGluZSUzRDElM0JzaXplJTNEMTUlM0IlMjIlMjB2ZXJ0ZXglM0QlMjIxJTIyJTIwcGFyZW50JTNEJTIyMSUyMiUzRSUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUzQ214R2VvbWV0cnklMjB4JTNEJTIyMjkwJTIyJTIweSUzRCUyMjQwMCUyMiUyMHdpZHRoJTNEJTIyNjAlMjIlMjBoZWlnaHQlM0QlMjI4MCUyMiUyMGFzJTNEJTIyZ2VvbWV0cnklMjIlMjAlMkYlM0UlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlM0MlMkZteENlbGwlM0UlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlM0NteENlbGwlMjBpZCUzRCUyMmFVVzlRVWluczBOQnpPNm93TDNTLTQlMjIlMjB2YWx1ZSUzRCUyMnNlcnZlciUyMiUyMHN0eWxlJTNEJTIyc2hhcGUlM0RjeWxpbmRlcjMlM0J3aGl0ZVNwYWNlJTNEd3JhcCUzQmh0bWwlM0QxJTNCYm91bmRlZExibCUzRDElM0JiYWNrZ3JvdW5kT3V0bGluZSUzRDElM0JzaXplJTNEMTUlM0IlMjIlMjB2ZXJ0ZXglM0QlMjIxJTIyJTIwcGFyZW50JTNEJTIyMSUyMiUzRSUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUzQ214R2VvbWV0cnklMjB4JTNEJTIyMzcwJTIyJTIweSUzRCUyMjQwMCUyMiUyMHdpZHRoJTNEJTIyNjAlMjIlMjBoZWlnaHQlM0QlMjI4MCUyMiUyMGFzJTNEJTIyZ2VvbWV0cnklMjIlMjAlMkYlM0UlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlM0MlMkZteENlbGwlM0UlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlM0NteENlbGwlMjBpZCUzRCUyMmFVVzlRVWluczBOQnpPNm93TDNTLTUlMjIlMjB2YWx1ZSUzRCUyMnNlcnZlciUyMiUyMHN0eWxlJTNEJTIyc2hhcGUlM0RjeWxpbmRlcjMlM0J3aGl0ZVNwYWNlJTNEd3JhcCUzQmh0bWwlM0QxJTNCYm91bmRlZExibCUzRDElM0JiYWNrZ3JvdW5kT3V0bGluZSUzRDElM0JzaXplJTNEMTUlM0IlMjIlMjB2ZXJ0ZXglM0QlMjIxJTIyJTIwcGFyZW50JTNEJTIyMSUyMiUzRSUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUzQ214R2VvbWV0cnklMjB4JTNEJTIyNDUwJTIyJTIweSUzRCUyMjQwMCUyMiUyMHdpZHRoJTNEJTIyNjAlMjIlMjBoZWlnaHQlM0QlMjI4MCUyMiUyMGFzJTNEJTIyZ2VvbWV0cnklMjIlMjAlMkYlM0UlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlM0MlMkZteENlbGwlM0UlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlM0NteENlbGwlMjBpZCUzRCUyMmFVVzlRVWluczBOQnpPNm93TDNTLTYlMjIlMjB2YWx1ZSUzRCUyMnNlcnZlciUyMiUyMHN0eWxlJTNEJTIyc2hhcGUlM0RjeWxpbmRlcjMlM0J3aGl0ZVNwYWNlJTNEd3JhcCUzQmh0bWwlM0QxJTNCYm91bmRlZExibCUzRDElM0JiYWNrZ3JvdW5kT3V0bGlLine truncated
"text/plain": [
"<IPython.core.display.Image object>"
]
},
"execution_count": 1,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from IPython.display import Image\n",
"Image(filename='media/balancers.png') "
]
},
{
"cell_type": "markdown",
"id": "9cfcf69f",
"metadata": {},
"source": [
"Все балансировщики распределяют запросы по **одному общему набору серверов**. Это дискретно-событийная симуляция на Rust с библиотекой simcore: серверы и балансировщики моделируются обработчиками событий внутри одного процесса.\n",
"\n",
"- Запрос имеет числовой ID отправителя и длительность обслуживания. На медленном сервере длительность **умножается** на коэффициент замедления.\n",
"- Каждый сервер обслуживает запросы последовательно, остальные ждут в очереди.\n",
"- Входящий запрос попадает на случайный балансировщик. Состав серверов в течение эксперимента не меняется.\n",
"- Раз в `sync_interval` секунд балансировщик обновляет снимок числа незавершённых запросов на каждом сервере, включая обслуживаемый запрос. Между обновлениями снимок может устареть.\n",
"\n",
"`sync_interval` — интервал обновления сведений, а не время обработки запроса балансировщиком. Затраты на выбор сервера, сетевые задержки и стоимость проверок в этой модели не учитываются.\n",
"\n",
"На всех графиках по оси X отложено начало окна `t`, по оси Y — **p99 времени ответа** для запросов, пришедших в интервал `[t, t + 60)` секунд. Время ответа включает ожидание и обслуживание. Чтобы получить p99, длительности сортируют и берут элемент с номером `ceil(0.99 × n)` при нумерации с единицы. Например, для 100 запросов это 99-е значение, а не максимум. Ось Y логарифмическая.\n",
"\n",
"Симуляция создаёт запросы до конца последнего окна, затем дожидается завершения всех очередей. Поэтому даже долгие ответы учитываются полностью. Параметр `--time-limit` задаёт последнюю отметку на оси X, а не момент принудительного завершения запросов."
]
},
{
"cell_type": "markdown",
"id": "79e49154-fa8e-4bad-9377-b72b5350deb4",
"metadata": {},
"source": [
"## Вспомогательный код"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "ebd17f6c",
"metadata": {},
"outputs": [],
"source": [
"import json\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"import subprocess\n",
"\n",
"from pathlib import Path\n",
"\n",
"# Сначала соберите образ по инструкции в readme.md.\n",
"# Для локальной сборки и запуска через Cargo задайте False.\n",
"USE_DOCKER = True\n",
"DOCKER_IMAGE = 'distsys-load-balancing-lab:local'\n",
"\n",
"\n",
"def run_experiment(name, n_balancers=1, settings=None, balancers=None,\n",
" sync_interval=0.01, seed=123):\n",
" settings = list(settings or [])\n",
" if balancers is None:\n",
" balancers = ['Hash', 'Random', 'RoundRobin', 'LeastLoaded']\n",
" config_path = Path('.tmp-config.json')\n",
" result_path = Path('.tmp-result.txt')\n",
" configs = [{'balancer': balancer, 'n_balancers': n_balancers,\n",
" 'sync_interval': sync_interval} for balancer in balancers]\n",
" config_path.write_text(json.dumps(configs), encoding='utf-8')\n",
" args = ['--servers', '100', '--seed', str(seed), '--dump', result_path.name,\n",
" '--config', config_path.name, *settings]\n",
" try:\n",
" if USE_DOCKER:\n",
" command = ['docker', 'run', '--rm', '--mount',\n",
" f'type=bind,source={Path.cwd()},target=/lab',\n",
" DOCKER_IMAGE, *args]\n",
" else:\n",
" command = ['cargo', 'run', '--locked', '--release', '--', *args]\n",
" subprocess.run(command, check=True)\n",
" results = result_path.read_text(encoding='utf-8').splitlines()\n",
" finally:\n",
" config_path.unlink(missing_ok=True)\n",
" result_path.unlink(missing_ok=True)\n",
"\n",
" plt.figure(figsize=(12, 5))\n",
" plt.title(name)\n",
" plt.yscale('log')\n",
" plt.ylabel('p99 времени ответа, с (окно 60 с)')\n",
" plt.xlabel('Начало окна t, с')\n",
" for i in range(0, len(results), 3):\n",
" times = np.fromstring(results[i + 1], sep=',')\n",
" response_times = np.fromstring(results[i + 2], sep=',')\n",
" plt.plot(times, response_times, label=results[i])\n",
" plt.legend(loc='upper left')\n",
" plt.tight_layout()\n",
" plt.show()\n"
]
},
{
"cell_type": "markdown",
"id": "7bbba437",
"metadata": {},
"source": [
"## Эксперимент 1: централизованный балансировщик"
]
},
{
"cell_type": "markdown",
"id": "fcb15af2",
"metadata": {},
"source": [
"Начнём с четырёх алгоритмов:\n",
"\n",
"- **Hash** закрепляет отправителя за сервером. В симуляции это `sender_id % num_servers` — упрощённая модель детерминированного выбора по клиенту.\n",
"- **Random** каждый раз выбирает случайный сервер.\n",
"- **RoundRobin** перебирает серверы по кругу.\n",
"- **LeastLoaded** выбирает сервер с наименьшим числом незавершённых запросов по последнему снимку. Это не Least Connections: TCP-соединения здесь не моделируются.\n",
"\n",
"Сначала работает один балансировщик с обновлением снимков раз в **10 мс**. Сравниваем p99 времени ответа, определённый выше. Меньшее значение означает меньшую задержку на границе самых медленных 1% запросов."
]
},
{
"cell_type": "markdown",
"id": "bb9119ac",
"metadata": {},
"source": [
"Запустим эксперимент на 100 одинаковых серверах и 200 отправителях с сопоставимой интенсивностью запросов. При одинаковых параметрах нагрузки и `seed` все запуски используют одинаковый входной поток; генераторы выбора балансировщика и бэкенда отделены от генератора нагрузки."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "5cc16a68",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABaAAAAJYCAYAAACUxkEyAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAASdAAAEnQB3mYfeAABAABJREFUeJzs3QWUG1UXB/D/et3d3d0NKtQpUErxFil8aIHiTlsoUtyhWAvFpdSgTil1d3d33dp27Tv3JZN5k0yySTZZ/f/O2bPxTGzkvvvujUhNTU0FEREREREREREREVGIRYb6AYmIiIiIiIiIiIiIGIAmIiIiIiIiIiIiorBhBjQRERERERERERERhQUD0EREREREREREREQUFgxAExEREREREREREVFYMABNRERERERERERERGHBADQRERERERERERERhQUD0EREREREREREREQUFgxAExEREREREREREVFYMABNRERERERERERERGHBADQRERERERERERERhQUD0EREREREREREREQUFgxAExEREREREREREVFYMABNRERERERERERERGHBADQRERERERERERERhQUD0EREREREREREREQUFgxAExEREREREREREVFYMABNRERERERERERERGHBADQRERERERERERERhQUD0ERERESU4Xbt2oWIiAjccccdYX0eeY5OnTohN5HXK687O6tSpYr688e///6rXu+wYcOQXV8DERERUU7GADQRERERERERERERhQUD0EREREREREREREQUFgxAExEREREREREREVFYMABNRERERJlq06ZN6Nu3L4oVK4b8+fOjQ4cOmD59usftTp8+jbfeegtdunRBhQoVEBsbi5IlS+Lqq6/GwoUL/X6+AwcO4OWXX0b79u1RpkwZ9TjlypXDLbfcgg0bNvisVy2nb7rpJpQoUQJ58uRBixYtMHnyZK/P9csvv+CKK65Qr01uLzWBb775Zixbtszjtj/99BM6d+6MIkWKqNvWrVsXI0aMQEJCgu1j//zzz2jevDny5s2LUqVKYeDAgeq1BaJt27bq9Z87d85yeceOHdVrvuuuuyyXb9y4UV1+2223eXw2zz77LGrXrq2WvWjRoujRowdmzpzps2bzkiVLcOWVV6r3Ry6T99eXH3/8EXFxceq9Seu2YuvWrWpZy5cv7/qc5bxcnt7vhUhNTcXHH3+M+vXrq9ctzzN48GD1fhARERGRAwPQRERERJRpdu7cqYKgJ06cwL333ovrr78ey5cvR69evVTw1j34+fzzzyMyMlIFLR977DF069YN//zzDy6//HJMnTrVr+f877//8MYbb6hA73XXXYdHH30Ubdq0we+//45WrVph9erVtvfbvXu3ul4CnxLsvfHGG7Fu3Tpcc801mD17tkdgUgLWEqxes2YN+vXrp57nsssuw9y5cz2C1oMGDVKBzm3btqllevDBB1VQ9sUXX0TPnj2RlJRkuf17772nAtk7duxQAdU777wTa9euRbt27XDy5Ek/332o4HhiYqJaJsP58+exaNEidXrWrFmW2xvn5X6GU6dOqeeV97Rw4cIYMmSIeg0yKNC9e3eMGjXK9rnlenk/Ll68qF7/7bffroK+3rz55psYMGAAWrZsifnz56fZ4G/p0qVqgOD7779X93niiSfU5yzn5XK5Pr3fC3mtDz30kHrP77nnHvV5y/ewa9euuHTpks/lIyIiIso1UomIiIiIMtjOnTtTZVdU/p544gnLdUuXLk2Njo5OLVKkSOrp06ddl586dSr16NGjHo+1d+/e1LJly6bWqVPH4zp5/I4dO1ouO3z4cOqZM2c8brtq1arU/Pnzp/bs2dPrsg4bNsxy3dSpU9XlvXr1slw+atQodXnLli3VcuuSkpJSDxw44Do/evRoddtrr7029fz585bbDh06VF33/vvvW5YnJiYmtWjRouq0ITk5ObVfv36uZfXHrFmzPD4D4zV169ZN/d+2bZvrur59+6rL9uzZ47rsnnvuUZfJ/5SUFNflW7ZsSS1UqFBqbGysZTlnz57tWsbPP//cdrkqV66s/ozXNXjwYHV7eX0XLlyw3NZ4PHmvDLIc8n2Qy7///nvL7X/++Wd1ee3atdVjB/u9mD9/vnqc6tWrpx4/ftx1uSxfmzZt1HXGayAiIiLKzZgBTURERESZRjJmX3rpJctlkp166623qszaP//803JbKX3hTspx9O/fX5Xy2LNnT5rPKeUqChYs6HF548aNVXkPyWaWrGB3lStXxgsvvGC5TMpMVKpUSZWS0H300Ufqv2T/ynLroqKiULZsWdf5Dz74ANHR0fjmm29UOQ2dZEAXL14cP/zwg+syOS3LJ5m3ehawZIZLiRL57y/JXJbSEXqms5yW5Rk+fLjr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 truncated
"text/plain": [
"<Figure size 1200x500 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"run_experiment('balanced workload')"
]
},
{
"cell_type": "markdown",
"id": "475a053a",
"metadata": {},
"source": [
"Уменьшим число отправителей до 10, сохранив 100 серверов. Сколько серверов сможет использовать Hash при таком выборе? Нужны ли коллизии, чтобы нагрузка оказалась неравномерной?"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "738b357c",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABaAAAAJYCAYAAACUxkEyAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAASdAAAEnQB3mYfeAABAABJREFUeJzsnQeYFMXWhs/mBEvOGSQjIBlFQFQUUVEM1yyGK9ecMWfF34wJFQOY9eoVxYAgCiqiBBWVnHNc0u6yeXf+59Ruz1TPzsxOqI7zvc8z7DDT07G6uuqrU99J8Hg8HgIAAAAAAAAAAAAAAAAAFJOoeoUAAAAAAAAAAAAAAAAAAARoAAAAAAAAAAAAAAAAAIaBCGgAAAAAAAAAAAAAAAAAhgABGgAAAAAAAAAAAAAAAIAhQIAGAAAAAAAAAAAAAAAAYAgQoAEAAAAAAAAAAAAAAAAYAgRoAAAAAAAAAAAAAAAAAIYAARoAAAAAAAAAAAAAAACAIUCABgAAAAAAAAAAAAAAAGAIEKABAAAAAAAAAAAAAAAAGAIEaAAAAAAAAAAAAAAAAACGAAEaAAAAAAAAAAAAAAAAgCFAgAYAAAAAAAAAAAAAAABgCBCgAQAAAAAAAAAAAAAAABgCBGgAAAAAAAAAAAAAAAAAhgABGgAAAAAAAAAAAAAAAIAhQIAGAAAAAAAAAAAAAAAAYAgQoAEAAAAAAAAAAAAAAAAYAgRoAAAAAAAAAAAAAAAAAIYAARoAAAAAwCQ2bdpECQkJNG7cOEO3w9sYPnw4xRN8vHzcTqZt27biFQ7z5s0Tx/vggw+SU49B5oEHHqD09HTaunWrIfvldqZNmybKA/+1Ax6Ph3r16kXHHnus1bsCAAAAABsAARoAAAAAAABgGSw6P/XUU3TVVVdRq1atdN99+umndP311wshMzs7W4isF110UY3rXLBgAZ1yyilUv359ysjIoJ49e9KkSZOovLzcwCMBGnydHn74YZo/f764hgAAAACIbyBAAwAAAAAAACzjkUceoeLiYpowYUK17x599FF66aWXaOnSpdSiRYuw1vfFF1/Q0KFD6aeffqIzzzyTrrvuOiopKaGbb76ZzjvvPAOOAARizJgx1LVrV7rnnntERDQAAAAA4hcI0AAAAAAAAABLOHToEL3//vt0/PHHU8uWLat9/9xzz9GaNWsoNzeXXnnllRrXx8v9+9//pqSkJGFT8uabb4roahawBw8eLKJxP/roI4OOBvhz6aWXiuv3/fff4+QAAAAAcQwEaAAAAAAAC1i1ahWdccYZwiIgKyuLhgwZQrNnzw4o0LGANmLECCHQpaamUqNGjej000+nX3/9Nezt7dixQ0yJP+aYY6hp06ZiPc2bN6cLLriAVqxYEdKvmt9z5GjDhg2FT2+/fv3oq6++Crqtjz/+WAiKfGy8PHsCn3/++bRkyZJqy3744Yd03HHHUd26dcWyHDHJUa8cERsIFg/79u0rbBUaN25MF198sTi2SGAhko//8OHDus+HDRsmjvmKK67Qfb5y5Urx+SWXXFLt2tx1113UuXNnse/16tWjk046iebMmRPSs3nRokU0evRocX74Mz6/ofjggw8oLS1NnJualmXWrl0r9pUjhrXrzP/nz2MtFwxHs3JUcvfu3cVx83Y4ypjPR6Tw9S8oKKB//etfAb/nstGxY8ew/b1ZYN67d68or1xONXg/uVwx4QjZGjNmzBBluVmzZuIa8LnhcjJ58uRqy+7fv1+UB75OXD7r1KkjfhvovpY9m+fOnSs8zGvXri1sRrhscJkLxLp16+icc84RZY3rjaOPPpq+/vrrkMewbds2cX3at28vjqFBgwai/li8eHG1Zbl88n5xeeVyN3DgQKpVq5bO1zuSc6JFnPNAAAAAAADiFwjQAAAAAAAms3HjRiGCsmA1fvx4ISj9/vvvNGrUKCHeyrAQxVPYExMThTB1yy230Iknnkg//PCDsBn49ttvw9om2xH83//9nxB6zzrrLGFHMGjQICHYDRgwgP7666+Av9u8ebP4noVPFntZKFy2bJmYXs/Cmb8wyYI1i05///03jR07VmyH/Xt//vnnaqL15ZdfLoROFtV4n6699lohyt5333108sknU1lZWbVoWBayN2zYIATVyy67jP755x8hwh04cCDMs09CPCstLRX7pMEi6G+//Sbe+0drav/n32kcPHhQbJfPKQuNN910kzgGHhQYOXIkvfbaawG3zd/z+SgqKhLHzxGiLPoG48knnxSex/3796dffvmlxgR/LCqy8Pree++J39x2223iOvP/+XN/0TGacsHHyr7MfM7Zt5mvN5fDE044QVhdRIIm1vMAjAr4vmC4/PjD90tmZqbwhw42wCEzZcoUUc5ZiD/ttNPo1ltv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 truncated
"text/plain": [
"<Figure size 1200x500 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"run_experiment('balanced workload (10 senders)', settings=['--senders', '10'])"
]
},
{
"cell_type": "markdown",
"id": "d1b44c0b",
"metadata": {},
"source": [
"Теперь вернём 200 отправителей, но один из них будет посылать запросы в 50 раз чаще остальных. Проследите, куда попадёт его нагрузка при разных алгоритмах."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "ab3d360c",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABaAAAAJYCAYAAACUxkEyAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAASdAAAEnQB3mYfeAAA+7pJREFUeJzs3Qd0FOXXx/GbnpBCb6H3For0DqIoIIqiWFGx8hcVsaEoSlDsioqKiihgL7yIigVFQUEpYsMEpPfeEgiQnvfcZzKb3RTqbMrm+zlnz+7OzM7M7iabzW/u3McvKysrSwAAAAAAAAAAcJi/0ysEAAAAAAAAAIAAGgAAAAAAAADgNVRAAwAAAAAAAAC8ggAaAAAAAAAAAOAVBNAAAAAAAAAAAK8ggAYAAAAAAAAAeAUBNAAAAAAAAADAKwigAQAAAAAAAABeQQANAAAAAAAAAPAKAmgAAAAAAAAAgFcQQAMAAAAAAAAAvIIAGgAAAAAAAADgFQTQAAAAAAAAAACvIIAGAAAAAAAAAHgFATQAAAAAAAAAwCsIoAEAAAAAAAAAXkEADQAAAAAAAADwCgJoAAAAAAAAAIBXEEADAAAAAAAAALyCABoAAADFQu/evcXPz09Kg9jYWPNcFyxYICXVsGHDzHPYtGnTSS2vy+p7XJKfAwAAAE4dATQAAAAAAAAAwCsIoAEAAAAAAAAAXkEADQAAAAAAAADwCgJoAAAAeNWXX34p55xzjlSvXl1CQkIkOjpaevXqJZMnTz6px//0009StmxZ87i///7bNX3btm1yxx13SP369c16K1asKBdddJH8/vvvHo9/8803TZ/ft956y2P6tGnTzPQyZcpISkqKx7xOnTpJaGioHDt2zGP60qVL5bLLLpNq1apJcHCw1KpVS4YPHy47duzId9//+OMP6devn0RGRkpUVJSce+65snjxYjkVY8aMMfv5ww8/eEwfN26cmd6gQYM8j9H9q127tse0zMxMeeONN6RDhw4SEREh4eHh5vbrr79u5hXUs3nXrl1y8803S40aNSQgIECmT59+3P39559/zLL6fHPvc34SExPNc2zSpIl5zcuXLy/nn3++zJs3L8+yqamp8uqrr8qAAQOkTp065n2vUKGCeV2//fbbAreh6+rRo4d5zrr8xRdfLP/9998J9w0AAABnjgAaAAAAXjNlyhQZNGiQrFy5Ui688EK59957TXiowa4GwCfywQcfSP/+/U34rMFtmzZtzPQ///zT3NYQW4PLO++806z/l19+ke7du8s333zjWoeG3+rHH3/0WLd9X/fFPRTWQFSD4y5dukhYWJhr+jvvvCPdunUzQefZZ58to0aNkvbt28vUqVPN9ZYtWzzW/9tvv5nQU8NPfQ4almtoraGuBtkn60T7v2HDBo9B9OLi4mT37t2ux9muvfZaue2228w8DZRvvfVW2bt3r4wYMcLMy8+BAwekc+fOsmTJEhk8eLB5DlWrVi1wX3WfevbsKVlZWea96Nu373GfW0JCgnTt2lWefvppc5BBX9NLL73UvB/nnXeeOXiQe3/uuusuOXz4sFn3PffcYw46/PXXX+bnSt+L3GbOnGkC7eXLl8uQIUPMAYP9+/eb93fjxo3H3T8AAAA4IAsAAADwkrZt22YFBwdn7d69O8+8vXv3etzv1atXlvvX06effjrLz88vq3v37ln79+93TU9LS8tq0KBBVkhISNaCBQs81rF9+/as6OjorGrVqmUlJye7pteuXTurcuXKWZmZma5p1atXz+rTp0+Wv79/1tixY13TZ8+ebfbjsccec01bvXp1VlBQkNnutm3bPLY5b948s46LL77YNU2306RJE7MeXZ+7l156yUzXy/z580/4Gh49etQ81/bt27umHT582OxP3759zXqmTp2aZ/3vvvuua9qHH35opp111lnmsbakpKSsdu3amXkffPCBx3btfbz22mvNa57b9ddfb+Zv3LjR3H/vvffMPjVr1ixr06ZNeZbXZfU9dnfrrbea6Xrt/t6sWbMmKyoqyvzs2OtX+p5u3bo1z7oTEhKyWrRokVW+fHnzerm/ThUqVMgKDAzM+v333z0eM2rUKNdzdN8GAAAAnEUFNAAAALwqMDBQgoKC8kyvVKlSvstrOwittH3wwQflkksuMW0ctG2C7euvv5b169ebqmdt5eFOK6VHjx5t2ka4Vwz36dPHVPv++++/5r5WZO/cudO002jbtq3HsvZt9wpibVORlpYmL7/8smkv4U6X0yrcr776ylTm2tXPq1evNtXAWgHuTp9bfm0zCqJV2Fqtq1XfBw8eNNO0ulj3RyuGK1eufML91+ptpZXG2n7Dpi0pnnnmGXM7v+phrdh+/vnnzXt4PLre6667zrQu+fXXX017jBPR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 truncated
"text/plain": [
"<Figure size 1200x500 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"run_experiment('skewed workload', settings=['--preset', 'skewed'])"
]
},
{
"cell_type": "markdown",
"id": "34925ed0",
"metadata": {},
"source": [
"- Почему закрепление отправителя за сервером ухудшает задержки в этих двух случаях?\n",
"- Когда такая привязка полезна, например для локального кеша или состояния сессии?\n",
"- Чем равномерное распределение числа отправителей отличается от равномерного распределения нагрузки?"
]
},
{
"cell_type": "markdown",
"id": "1bccea52",
"metadata": {},
"source": [
"Пока все серверы были одинаковыми. Вернём равномерный поток от 200 отправителей и замедлим 5% серверов в пять раз."
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "0372fc52",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABaAAAAJYCAYAAACUxkEyAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAASdAAAEnQB3mYfeAABAABJREFUeJzs3Qd4FOXaBuBnZ7an95AAofemUkRQEEERO9iOFRv+9t4LYMeKysEjFvAcu9iwoKiAoiBFQKT3GiC9Z/v81/vFjQlJIMAGUp77uvbsZmZ25pvZTQ4+++77mQzDMEBEREREREREREREFGJaqHdIRERERERERERERMQAmoiIiIiIiIiIiIjqDCugiYiIiIiIiIiIiKhOMIAmIiIiIiIiIiIiojrBAJqIiIiIiIiIiIiI6gQDaCIiIiIiIiIiIiKqEwygiYiIiIiIiIiIiKhOMIAmIiIiIiIiIiIiojrBAJqIiIiIiIiIiIiI6gQDaCIiIiIiIiIiIiKqEwygiYiIiIiIiIiIiKhOMIAmIiIiIiIiIiIiojrBAJqIiIiIiIiIiIiI6gQDaCIiIiIiIiIiIiKqEwygiYiIiIiIiIiIiKhOMIAmIiIiIiIiIiIiojrBAJqIiIiIiIiIiIiI6gQDaCIiIiIiIiIiIiKqEwygiYiIiIiIiIiIiKhOMIAmIiKiRmHr1q0wmUwYPXp0nR5HjjF48GA0JXK+ct4NWatWrdStNubOnavOd9y4cWio51DR2LFjYbfbsWPHjsM61rRp09R1kft9zZo1CyeccAKio6PVNueee275uiVLlmDYsGGIj49X63r16nXQ50BNQ0lJCZKTk3HZZZcd7aEQERFRCJlDuTMiIiIiIqo/JHR+7rnnMGbMGLRo0aLOPvw555xzVPh89dVXIzIyEp06dVLrCgoKcMYZZ8DlcuHyyy9XIbQEjA2VfDhx8sknq1C/vn1A0Rg4nU488MADuOOOO3DbbbehT58+R3tIREREFAIMoImIiIiIGqnHH38cbrcb995772Hv67zzzsPxxx+PZs2aVVr+448/qoD5hRdewCWXXFJp3aJFi5CRkYEnn3wSDz744GGPgRq/66+/HuPHj8dDDz2kKuuJiIio4WMLDiIiIiKiRig/Px/vvfceTjnlFDRv3vyw9xcVFaUqm+W+ovT0dHWfkpJS5Tn7W0dUHWkXc9FFF6kPNjZs2MCLRERE1AgwgCYiIqJGZ+3ataoHbWxsLMLCwjBw4MBqK+kkoJP2BEOGDFEBndVqRUJCAs4++2wsWLCg1seTkO2xxx7DgAEDVHsB2Y8EblINunr16v32q5bHF198sWpNIMFL79698fXXX9d4rI8++kgFinJusr306f3Xv/6l+uzu64MPPlDtAqQ1gmzbuXNnPPHEE6oitjoffvghjjvuODgcDiQmJqqWCcEAsbb69++vzr+4uLjS8kGDBqlzvuaaayotX7NmjVp+xRVXVHlt5Kv4HTt2VGOPiYnBaaedpkKp/fVslopbafkg10eWyfXdn/fffx82m01dmwNtKyQQk7GmpqaWv87yc3VB2cG+L4RhGJg0aRK6du2qzluOc/PNN6vrcbDk9ZeeuhLmheJY+/aADl53aUch5L0mPwe3kfsrr7xSrbvqqqsqrQuS8T399NOqL7T8roaHh6v3kIz9UF/ng3nfB3u6Z2VlqTYlUt0t7we5JlOnTq20rfy+yn6FVOgGz0duMrYDmTdvHs466yz1t0aOIe8JqSiXfe0rVNdF/hbKdZDfZ5/PV+24brjhBrXtvn935LlyztK6Rd67SUlJ6r27bt26KvuQ7WQfmzdvxquvvooePXqovyPBfvnyXnvnnXdUn3D5Gyuvi+xXfqflb9q+5G+iPOftt98+4HUlIiKi+o8tOIiIiKhR2bJliwpqunfvrr7KvXv3bhVwnH766SpsrBjGSfgpX/M+6aSTVGgjIef27dsxY8YMzJw5E1999RWGDx9+wGP+8ssveOaZZ1Q4NWrUKBUWSSA5ffp0ta/ffvsNPXv2rPK8bdu2oW/fvmjTpo0Ke3NyctRYpZ+uBK3BsEtIGCMhnoQ4ElaPHDlSBTk7d+7EnDlzVFAr4XWQ9OKVAE3CLhmThFC///47HnnkEfz000/44YcfYDb/80/Bl156CXfeeafaTgJVuf/+++9VYLRvxev+SDgux5GwLXjtJEyTZUKOXVHwZ3leUF5engptJaSVHrC33367Cgg//vhjnHrqqXjttdfUa7sv+dBAQjv5wEHOX54jwVlNnn32Wdx///3qHOV1ktBufxYvXoyhQ4eisLBQfUjRpUsXFdK9++67+PLLL9VrVrFn7aG8L+RcX3nlFRWESiBqsVjUvhcuXAiPx7Pf89lXMKyX61Gdwz2WfPgh4bMEoD///LMKm4MTF0pwKuuWL1+u9inv6eDkg8F7eZ3lw59ly5bh2GOPVa9ZIBBQ7zsJOletWqWC44N5nQ/2fV/x/Sb7OP/881VQ/cknn6h9aZpWHqIHJ1aU30H5QKXiZKQHmhzyu+++U39jpD+2vHck7Jffd/kbNHny5PIQP9TXRY4nf/OmTJmi/qZJAF6RnKv8zZFwueLfOhmv/I3xer3qOe3atVN/az777DN888036m+OjG1f0rdZfvflXEeMGAFd19Vy+TsrY2vdujUuvPBC9TdF/jbL75Rc630/JJG/i/J+lNdLnkdEREQNnEFERETUCGzZssWQf9rI7e677660bvHixYbZbDaio6ON/Pz88uV5eXlGZmZmlX3t2LHDaNasmdGpU6cq62T/gwYNqrRs7969RkFBQZVtly9fboSFhRnDhw+vcazjxo2rtO67775Ty08//fRKy19//XW1vE+fPmrcFfl8PiM9Pb3856lTp6ptzzvvPKOkpKTStmPHjlXrJk6cWGk8FovFiImJUY+D/H6/MXLkyPKx1sZPP/1U5TUIntOwYcPU/caNG8vXnXvuuWrZ9u3by5eNGTNGLZP7QCBQvnz9+vVGZGSkYbVaK41zzpw55WP8z3/+U+240tLS1C14XjfffLPaXs6vtLS00rbB/cm1CpJxyPtBlr/77ruVtv/www/V8o4dO6p9H+r74rffflP7adu2rZGdnV2+XMZ3/PHHq3XBc6iNpKQkdb0qXsPDOVbwfSX31b2n5Lrtq6bniCuvvFKtmzBhQqXlMobTTjvNMJlMxrJly2r9Oh/s+14E93fNNdeo36OgVatWGbquG507dz7ge6M2gr9H8trva9+/QaG+LvPnz1frRo0aVWXdxx9/rNbdeeed5ctycnLU38q4uDh1HSr666+/1Hv3mGOOqXbMKSkpxubNm6scJzY21khNTTWKi4sPeP5BvXr1MjRNq/Z3iIiIiBoWBtBERETUKARD3aioqGoDi2BAMm3atFrt75ZbblHbb9u27YAB9P6cddZZhs1mMzweT5WxSsBXMfQKatmypQp/KurWrZt6ztKlSw94TAluJHDPzc2tsk6OJ/uWIDvoiSeeUPt+9NFHq2y/adMmFQLVNoCWkMxut1cKqO655x41nmAQJmG6kLBWgq727duXb+t2uw2n02mEh4dXCkaDHn74YbWP8ePHVwng5LxrEgygZXwSUMr28hpXDIz3FzL++uuvaln//v2r3f/AgQPV+p9//vmQ3xfXXnut2sfbb79d45hqG0DLdZTtK17big7lWKEMoLOyslTA27t372rHJ0GtPE/eO7V9nQ/2fS9kf/J+q/jBVNBJJ52k1hcWFoYsgF63bt1+t6uL6yI6dOigPrjZ93fqjDPOUM/9888/y5dJSC/LJk2aVO2+br/9drW+Yjgd/Pu6b8BfMYBu1aqV4XK5jNqSD2hkn2vWrKn1c4iIiKh+YgsOIiIialTka+ERERFVlsvX5eWr8/K19uBX6oW0QXj55ZfV19czMjJU64GKdu3ahZYtWx7wuPK19P/85z+qF7N89X3ffquyTFodVCStCIJfUa9IeqNW7EEt/ZRXrlypviZ/zDHH7Hcc0u7izz//VG06Jk6cWO020n9WvvoftHTpUnUvbQX2Je1BZDzSLqQ2pLertLSQr+hnZ2cjLi4Os2fPVq0ppDWKnIO0QpCWD3JcaTdQ8ev30l9WzkFaIlTXEkNaE0j7AXkd9yVf29+f0tJS1epDru2ECRNw7733oraC10iOXx1Z/uuvv6pxSUuXQ3lf7O91kLYK1b1XaiLXXkhbmf2dTyiOdSik9YLf7y/vXbwvaf0gKr5P9/c6H8r7Pqh9+/aqVcW+5H0vcnNzVfuUw3HppZeq9hX9+vVT73dpyyLv8X0nhwz1dQmSv3nSBkP6vN94441q2d69e1VbD/mbIj2bg4J/e+R6VjeG9evXl49B2tDUZgxy/tIbWraXFhzyvpO/B/tr7xP8/ZffESIiImrYGEATERFRoyIBZ3Vkwi9RcYK1zz//XPV8ldB02LBhaNu2rZrwS/q+Bvva1jRhX0USYEs/XQn7ZD8SWDudThUiffHFFyrIqW4/0p+2OtKjVnq+BklIK6Rv7IFIWCaFnZmZmdVOblad4DXZ37WrbQAtJOSV0FlCaHksoeyDDz5YHtRKb2IZY3X9n4Nj2TesDwouD16Tfce5P9K7WYJXCRtl8rODcSjjOtj3xf5eB3lPSLhaWzIBnHC5XPs9n1Ac61AEA3IJXOVWk6Kiolq9zofyvq/N76GQQPhwST9lmeTvhRdeUBPrvf7662q5TPopPY7l/VEX1yVI+rpLH2z5EC4YQL/33nvqA5GKH8hVHMMbb7yx33M6mDFIj3n5MEv6c0tfdLnJ9ZU+0XJNpMd0dR8YVXwvExERUcPFAJqIiIgaFanqq86ePXvUfcWKOwlkZOIxqU7t3Llzpe1lkjsJoA9EAhypEpTgRcLNfQPKipXMhyoYkEk19oEEz0+qGoNVrrV9jly7rl271njtaitYJSxBs4T5EqYHQ2ZZ98EHH6jwVQJoCWMrTrYYHEtLine truncated
"text/plain": [
"<Figure size 1200x500 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"run_experiment('balanced workload (different servers)', settings=['--different-servers'])"
]
},
{
"cell_type": "markdown",
"id": "9c8afb20",
"metadata": {},
"source": [
"- Почему алгоритмы, не учитывающие очереди, накапливают запросы на медленных серверах?\n",
"- Насколько LeastLoaded помогает в этой ситуации и как на результат влияет период обновления снимка?"
]
},
{
"cell_type": "markdown",
"id": "ce26e221",
"metadata": {},
"source": [
"## Эксперимент 2: независимые балансировщики"
]
},
{
"cell_type": "markdown",
"id": "ed6ad1a3",
"metadata": {},
"source": [
"Теперь несколько балансировщиков используют общий набор серверов. У каждого свой снимок состояния, который обновляется раз в **5 секунд**. В этой модели обновления происходят одновременно, поэтому все балансировщики видят одинаковые снимки. Запросы по-прежнему распределяются между балансировщиками случайно.\n",
"\n",
"По умолчанию у экземпляров Random и PowerOfK разные воспроизводимые последовательности случайных чисел, а у RoundRobin — случайные начальные позиции. Параметр `seed` позволяет воспроизвести эксперимент.\n",
"\n",
"По сравнению с первым экспериментом меняются и число балансировщиков, и период обновления. Сначала посмотрим на один балансировщик с периодом 5 секунд, чтобы отделить эти эффекты."
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "623b10ba",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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 truncated
"text/plain": [
"<Figure size 1200x500 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"run_experiment('balanced workload (1 balancer, updates every 5 s)', sync_interval=5)"
]
},
{
"cell_type": "markdown",
"id": "b2578704",
"metadata": {},
"source": [
"- Зачем распределять входящий поток между несколькими балансировщиками?\n",
"- Что изменилось при переходе от обновлений раз в 10 мс к обновлениям раз в 5 секунд при одном балансировщике?"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "a5f41eba",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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 truncated
"text/plain": [
"<Figure size 1200x500 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"run_experiment('balanced workload (5 balancers)', n_balancers=5, sync_interval=5)"
]
},
{
"cell_type": "markdown",
"id": "59fb2833",
"metadata": {},
"source": [
"- Почему LeastLoaded направляет много запросов на один сервер между обновлениями? Почему при одинаковых снимках и входном потоке его кривая не меняется с числом балансировщиков?\n",
"- Как изменяется p99 у Random и RoundRobin? Не ожидайте одинакового порядка кривых при любых параметрах нагрузки.\n"
]
},
{
"cell_type": "markdown",
"id": "17865058",
"metadata": {},
"source": [
"## Эксперимент 3: Power of Two Choices и Power of k Choices"
]
},
{
"cell_type": "markdown",
"id": "158f1f53",
"metadata": {},
"source": [
"С устаревшими снимками глобальный минимум может привлекать слишком много запросов от разных балансировщиков. Попробуем выбирать минимум только среди **двух случайных серверов** — Power of Two Choices. Более общий вариант с k кандидатами называется Power of k Choices.\n",
"\n",
"Это не гарантия улучшения для любой нагрузки: результат зависит от актуальности снимков, длительности запросов и различий между серверами. Проверим алгоритм на том же наборе из пяти балансировщиков. Подробнее о методе: [The Power of Two Choices in Randomized Load Balancing](http://www.eecs.harvard.edu/~michaelm/postscripts/handbook2001.pdf)."
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "98374a5a",
"metadata": {},
"outputs": [],
"source": [
"exp3_balancers = ['Hash', 'Random', 'RoundRobin', 'LeastLoaded', 'PowerOfK[k=2]']"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "64609ded",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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 truncated
"text/plain": [
"<Figure size 1200x500 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"run_experiment('power of two choices', n_balancers=5, sync_interval=5, balancers=exp3_balancers)"
]
},
{
"cell_type": "markdown",
"id": "44c62e12",
"metadata": {},
"source": [
"Увеличим число кандидатов k. Больший выбор повышает шанс найти свободный сервер, но при устаревших снимках может усилить концентрацию запросов на одних и тех же кандидатах. Какой эффект виден на этих графиках? Повторите эксперимент с другим периодом обновления или seed."
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "5f1c5da8",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABaAAAAJYCAYAAACUxkEyAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAASdAAAEnQB3mYfeAABAABJREFUeJzsnQW4VFX3xtfcDrq7VVBCRRETxERFEQML61NRPxS7OzD/2PFhYSAKYiJiIYqgYoPS3X253M75P2vfOWf2OXNy8szc98dzmZnTffZ+99rv8vn9fj8BAAAAAAAAAAAAAAAAAFEmLdoLBAAAAAAAAAAAAAAAAAAgQAMAAAAAAAAAAAAAAACIGYiABgAAAAAAAAAAAAAAABATIEADAAAAAAAAAAAAAAAAiAkQoAEAAAAAAAAAAAAAAADEBAjQAAAAAAAAAAAAAAAAAGICBGgAAAAAAAAAAAAAAAAAMQECNAAAAAAAAAAAAAAAAICYAAEaAAAAAAAAAAAAAAAAQEyAAA0AAAAAAAAAAAAAAAAgJkCABgAAAAAAAAAAAAAAABATIEADAAAAAAAAAAAAAAAAiAkQoAEAAAAAAAAAAAAAAADEBAjQAAAAAAAAAAAAAAAAAGICBGgAAAAAAAAAAAAAAAAAMQECNAAAAAAAAAAAAAAAAICYAAEaAAAAAAAAAAAAAAAAQEyAAA0AAAAAAAAAAAAAAAAgJkCABgAAAAAAAAAAAAAAABATIEADAAAAAACQpOzZs4euvfZa6tKlC2VkZJDP56O//vqLvMjEiRPF9vFnMq8DAAAAAAC4AwI0AAAAAAAAScott9xCzz33HPXp04duv/12uvfee6lNmzaW8wwePFiItLNnzw4ZV1JSQieddJIYf8IJJ1BxcXEMtx4AAAAAANQHMhK9AQAAAAAAAIDwmD59Ou2999702WefRXwId+zYQSeffDLNnz+fzj//fHrjjTcoMzMzqU7N6aefTgMHDqS2bdsmelMAAAAAAEAAREADAAAAAACQpGzatCkqYuvatWvpiCOOEOLzDTfcQG+//XbSic9M48aNqWfPnuITAAAAAAB4AwjQAAAAAAAg7qxZs0bYPFx88cW0ZMkSGj58ODVr1ozy8/OFEPrVV18ZzldRUUGPPvqosJzIy8ujRo0a0ZFHHklTpkzRTMfWEVlZWXT44YdrhpeVlVFOTo5YN4usMi+99JIY/vrrr2uG79q1S9hb9OrVi3Jzc4W4ecwxxxhuo+xBPHPmTGF3wdPzMCds3ryZ/vvf/wpPZ97+li1b0ogRI+j33383tNHw+/30/fffi+/8x8PdsnDhQjrssMNo2bJl9MQTT9D//d//Od5ehffff18cEz6HfHx5+88991z67bffDKf/7rvvxLY2bNhQnEOOvF68eHFEx8TOA3rDhg3CL3uvvfYS55G3dcCAAfTggw8aTjtmzBjq1q0bZWdnU/PmzenUU0+lX3/9NWTaoqIisYzevXuLfeF96t69O40cOdJwGwEAAAAA6huw4AAAAAAAAAlj9erVdOihhwpBefTo0UJsZDFz6NCh9O677woRT6GyslL4ErPgylGuLEqWlpbSBx98IKbj5Hvjxo0T0zZo0ECIi7/88osQCFkUZObOnStEbObbb7+lUaNGqcvn3wwLqXJkMAulLJiz0H3iiScKn2S2vuDv//vf/+jyyy8P2S/eJhageT+uvPJKsRwnx4LFd45qHjJkiBBw169fT1OnTqXPP/+cpk2bRqeccoqYloV73q7777+fOnfuLH4zLNK64YcffhDCKh/HN998U3M8nMAC+CWXXCLmbdGihRCGWSBmAZdF5n322YcOOuggzTx87D755BP12CxatIhmzJghxF3+zssJ55hYwUI4XzvcmHDUUUeJ7eR95vXdd999dPfdd6vT/vHHH3T88ceLaXkenpbtST7++GOxLR999JHwyVb2n6+DefPmiev4sssuE8kglf3na6Z///6ujikAAAAAQMrhBwAAAAAAIM6sXr3az0VR/rvppps043799Vd/RkaGv0mTJv7CwkJ1+Lhx48T0Q4cO9VdVVanDt27d6u/cubMYN3fuXHX43XffLYZNnz5dHXbbbbf509PT/UOGDPF36NBBHV5TU+Nv1qyZv1u3bpptGTRokN/n8/knT56sGV5QUODv16+fPycnx79lyxZ1+BtvvCHWyfN88cUXro7J8ccfL+Z96KGHNMN5n3ibefuKioo043h63kY38PQ839ixY8X25+fnu95Whf/9739iWQcffLB/9+7dmnHV1dX+TZs2hRwb3pdvvvlGMy2fFx732GOPRXRMlHXwp0JFRYW/S5cuYvikSZNC9mH9+vXqd76uunfv7s/OzvbPnj1bM93GjRv97dq187dp08ZfXl4u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 truncated
"text/plain": [
"<Figure size 1200x500 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"run_experiment('power of K choices', n_balancers=5, sync_interval=5, balancers=['PowerOfK[k=2]', 'PowerOfK[k=3]', 'PowerOfK[k=5]', 'PowerOfK[k=10]'])"
]
},
{
"cell_type": "markdown",
"id": "198d4745",
"metadata": {},
"source": [
"PowerOfK выбирает по периодически обновляемому снимку. Вариант **PowerOfKWithUpdates** сравнивает актуальные очереди только у выбранных k серверов перед каждым решением. В симуляции это прямое чтение состояния: стоимость и задержка таких проверок не моделируются. Сравните варианты и обсудите, какой дополнительный обмен потребовался бы в реальной системе."
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "ecb9a473",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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 truncated
"text/plain": [
"<Figure size 1200x500 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"run_experiment('do updates matter?', n_balancers=5, sync_interval=5, balancers=['PowerOfK[k=2]', 'PowerOfKWithUpdates[k=2]'])"
]
},
{
"cell_type": "markdown",
"id": "cffbb495",
"metadata": {},
"source": [
"Повторим сравнение при наличии медленных серверов. Сохраняется ли преимущество алгоритмов, учитывающих очередь? Как на результаты влияют выбранный seed и частота обновления состояния?"
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "ddcbc891",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABaAAAAJYCAYAAACUxkEyAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAASdAAAEnQB3mYfeAABAABJREFUeJzs3Qd4U+XbBvD7nOzuXfbeICDIcgCKIuBAUf9uRdx77wWK+rn3wgHuvXEPcIEMEZE9y+reM2nWdz1vSUk6oNCEtun9u66Y5JyTsxpLep8nz6t5vV4viIiIiIiIiIiIiIiCTA/2ComIiIiIiIiIiIiIGEATERERERERERERUciwApqIiIiIiIiIiIiIQoIBNBERERERERERERGFBANoIiIiIiIiIiIiIgoJBtBEREREREREREREFBIMoImIiIiIiIiIiIgoJBhAExEREREREREREVFIMIAmIiIiIiIiIiIiopBgAE1EREREREREREREIcEAmoiIiIiIiIiIiIhCggE0EREREREREREREYUEA2giIiIiIiIiIiIiCgkG0EREREREREREREQUEgygiYiIiIiIiIiIiCgkGEATERERERERERERUUgwgCYiIiIiIiIiIiKikGAATUREREREREREREQhwQCaiIiIiIiIiIiIiEKCATQRERERUQgUFxfjmmuuQZcuXWA0GqFpGpYvX94qz7Uc+9ixY1v8NoiIiIho3xn34zVERERERLQXt9xyC15++WUcf/zxOPfcc2EwGNCmTZs9vkbCapGWlsbzS0RERERhgQE0EREREVEIzJ07F7169cJXX33F83sArFmzBhERETzXRERERM0MW3AQEREREYVAeno62rZty3N7gPTp0wedOnXi+SYiIiJqZhhAExEREVEAaf8g/XSnTp2KtWvX4qSTTkJCQgIiIyNx+OGH44cffqjzjDkcDvzf//0fDjroIFWJGhMTgyOOOAIffvhhwHKlpaUwm8047LDDAqZXVFTAarWqbb/11lsB81588UU1/fXXXw+Ynp+fj9tvvx19+/aFzWZDbGwsxo0bV+c+zpkzR61D7r/77jvVL1iWl2kNkZGRgSuvvFK1yZD9T05OxpQpU/D3338HLCfrlXV6vV78+uuv6vHe+hPPnz9fLbN161Z1873G93MQ7dq1Q/v27Wu9tnPnzmq5+++/P2D6t99+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 truncated
"text/plain": [
"<Figure size 1200x500 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"run_experiment('power of two choices', n_balancers=5, sync_interval=5, balancers=exp3_balancers, settings=['--different-servers'])"
]
},
{
"cell_type": "markdown",
"id": "correlated-selectors-intro",
"metadata": {},
"source": [
"## Дополнительно: коррелированные выборы балансировщиков\n",
"\n",
"Этот эксперимент можно выполнить после основной части. Сравните его с запуском `balanced workload (5 balancers)` из эксперимента 2: число балансировщиков, поток запросов и период обновления снимков остаются прежними.\n",
"\n",
"Флаг `--correlated-selectors` задаёт всем балансировщикам одинаковый seed выбора сервера. Поэтому у экземпляров Random совпадают последовательности случайных чисел, а у RoundRobin — начальные позиции. Это отличается от общего параметра `seed`: без флага он воспроизводит эксперимент, но экземпляры получают разные последовательности выбора.\n"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "5e3a734f",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABaAAAAJYCAYAAACUxkEyAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAASdAAAEnQB3mYfeAABAABJREFUeJzs3Qd4U2UXwPFT2lL23ntvZAmIbESZIoJ7gVtRERcOVMCBgrj9cAsqbmWJCIiigCLgQPbee28otM33nPf2Njdp2qZt0qbp/+cTyWpyc3OT3Hve854T4XK5XAIAAAAAAAAAQIDlCfQDAgAAAAAAAABAABoAAAAAAAAAEDRkQAMAAAAAAAAAgoIANAAAAAAAAAAgKAhAAwAAAAAAAACCggA0AAAAAAAAACAoCEADAAAAAAAAAIKCADQAAAAAAAAAICgIQAMAAAAAAAAAgoIANAAAAAAAAAAgKAhAAwAAAAAAAACCggA0AAAAAAAAACAoCEADAAAAAAAAAIKCADQAAAAAAAAAICgIQAMAAAAAAAAAgoIANAAAAAAAAAAgKAhAAwAAAAAAAACCggA0AAAAAAAAACAoCEADAAAAAAAAAIKCADQAAECYGzhwoERERMiWLVskp/j111/NMo8YMSK7FyVHrr/sdPDgQSlRooQMGjQoaM+h24W+J7qdBMuECRPMc+i/8K1Pnz5Ss2ZNOXv2LKsIAACkiAA0AAAA/NKpUycTkEPw5eR1PXz4cDl9+rQ8+eSTPgO6KZ3eeeedbFtmZMwzzzwjmzdvljfeeINVCAAAUhSV8k0AAAAA4L9t27bJu+++KzfffLNUqFDB530uu+wyadq0abLrzz//fFZ1DqPvY/fu3eX55583Ge8FChTI7kUCAAAhiAA0AAAAgIDQ4HNcXJwpW5KSvn37pno7cpYBAwbIjz/+KJ9//rncdttt2b04AAAgBFGCAwAAIAWLFy+Wq6++WipWrCgxMTFSvnx5ueSSS+Trr79Odl+9rkOHDlK0aFHJnz+/NG7cWF544QWJjY1Ndt9q1aqZ07Fjx+TBBx8056Ojo5PqHad1u1qzZo0J4lWuXFny5s0rZcuWleuuu07Wrl3r9/upJRH69+8vNWrUMMtcpEgRadu2rUycONHjflr7WEsk/Pbbb+ays2yClopw2rFjh9x7773mMXWdlSxZ0tSJXbJkic9l2Lt3r9x6661m+XUZNKPy448/lvTSGrRaBqB58+ZSvHhxk4mp602zbefMmZPs/oFYf2rRokVyxRVXSLly5czj6OPdeeedsmvXLp/3P3TokAwbNkwaNWpkllG3lyZNmshjjz0mJ0+eTNe6/vvvv837V6ZMGbOuq1atarJQd+/enWId602bNsmbb74p5513nlnf9mO6XC6z3i+88EIpXbq05MuXz7yWbt26yVdffeXXutDHGD9+vPk7fZysosvdrFkz83p0Xdxyyy2yZ8+eZPfT9XX//feb9a01qvU11q5dWx566CE5fPiw3883d+5cueOOO6RBgwbmM6PPq+/nyJEj5cyZM6nWq/7222+lVatW5r3XZbjmmmtk586dGdpWvO/7+OOPS/369c3y6H0vuugimT17dqq1rWfOnGm2Ab2/s+TL/Pnz5dJLL5VKlSqZbUu37wsuuMC8Rm/6GdN1+eGHH/q9DgEAQO5CBjQAAIAP77//vtx9990SGRlpAqgaqNq3b5/89ddfMm7cOLnqqquS7vvEE0+YYHOpUqVMELNQoUImI1CvnzVrlgkCaXDSO2DapUsXEzjSoLYGsqpXr+7X7Ro06tevn5w7d84EiWrVqmUCv5MmTZIffvjBBMg0EJsWfX0NGzY0gXMNrmvzuBkzZsiNN95oArHPPvusuV+xYsVMXV8NWG3dutWct2mQ1/bPP/+YZdVl1sClLuOBAwdkypQp0q5dO5k8ebL07Nkz6f56mwYqNSiqt+tJg6d33XWXeZz00ADrF198YYJ1N910kwnCaRB4wYIFZn117do16b6BWn8fffSRCURqgE63EQ28rl+/Xj744AP5/vvv5c8//5QqVaok3V9r5Xbu3NmswxYtWpj1n5CQIOvWrZNXX33VvG5/1/X06dNN8FmDvhoA1+CzBljffvttmTp1qnndzu3JpgFYDS726tXLvBe6fSsNdOo2rH+j27YGJPW90IGDb775xgzEpGXlypXmbzSompqlS5fKa6+9ZoK1Orij60QDnRmh600/X7p8WgpCX7cGwTXYq4MDGkx3fqZ1G+zYsaPZHnTd6zp75ZVXzOdV71+4cOE0n3P06NFmAEO3XV2P+jp+//13E2jW59UBD3u9Oun3xrRp08y2osugz6fB/f/++8+sE92O0rOtFCxY0NxX76NBZB28aN++vVkPGqDWbUTPa1b67bff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 truncated
"text/plain": [
"<Figure size 1200x500 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"run_experiment('correlated selectors (5 balancers)', n_balancers=5, sync_interval=5,\n",
" settings=['--correlated-selectors'])"
]
},
{
"cell_type": "markdown",
"id": "correlated-selectors-questions",
"metadata": {},
"source": [
"- Как изменились p99 у Random и RoundRobin по сравнению с независимой инициализацией? Почему одинаковые последовательности могут создавать перекосы нагрузки?\n",
"- Почему результаты Hash и LeastLoaded не изменились? Используют ли они генератор случайных чисел при выборе сервера?\n",
"\n",
"В этом прогоне особенно заметен рост p99 у Random. Запросы поступают на балансировщики случайно, поэтому их последовательности продвигаются с разной скоростью: одинаковый seed не означает, что все балансировщики всегда выбирают один сервер одновременно.\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.7"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

@@ -0,0 +1,25 @@
# Балансировщики нагрузки: лабораторная работа
Откройте `Lab.ipynb` в JupyterLab или редакторе с поддержкой Jupyter и выполните ячейки по порядку. Нужны Python, JupyterLab, NumPy и Matplotlib:
```bash
python -m pip install jupyterlab numpy matplotlib
```
В основной части три раздела и 10 прогонов. В конце есть дополнительный эксперимент с одинаковыми seed у балансировщиков: его можно оставить для самостоятельного разбора.
Симулятор написан на Rust с библиотекой simcore. По умолчанию ноутбук запускает его в Docker. Перед первым запуском и после изменения Rust-кода соберите образ из этой папки:
```bash
docker build -t distsys-load-balancing-lab:local .
```
Для запуска без Docker установите Rust версии, указанной в `rust-toolchain.toml` в корне репозитория, и задайте `USE_DOCKER = False` в ноутбуке. Тогда `cargo run --locked --release` соберёт текущие исходники автоматически.
На графиках показан p99 времени ответа для запросов, пришедших в окно `[t, t + 60)` секунд. Время ответа включает ожидание в очереди и обслуживание. Смысл метрики и параметры экспериментов разобраны в ноутбуке. После изменения симулятора или параметров пересчитайте графики.
Проверка симулятора при установленном Rust:
```bash
cargo test --locked
```
@@ -0,0 +1,284 @@
use std::boxed::Box;
use std::cell::RefCell;
use std::rc::Rc;
use rand::prelude::*;
use rand_pcg::Pcg64;
use simcore::cast;
use simcore::context::SimulationContext;
use simcore::event::Event;
use simcore::handler::EventHandler;
use crate::events::{RequestArrivalEvent, SyncEvent};
use crate::host::Host;
#[derive(Copy, Clone)]
pub struct HostSnapshot {
pub id: usize,
pub requests: usize,
}
pub struct LoadBalancer {
state: Vec<HostSnapshot>,
hosts: Vec<Rc<RefCell<Host>>>,
selector: Box<dyn HostSelector>,
sync_interval: f64,
sync_until: f64,
ctx: Rc<RefCell<SimulationContext>>,
}
impl LoadBalancer {
pub fn new(
hosts: Vec<Rc<RefCell<Host>>>,
selector: Box<dyn HostSelector>,
sync_interval: f64,
sync_until: f64,
ctx: Rc<RefCell<SimulationContext>>,
) -> Self {
Self {
state: (0..hosts.len())
.map(|i| HostSnapshot { id: i, requests: 0 })
.collect(),
hosts,
selector,
sync_interval,
sync_until,
ctx,
}
}
}
impl EventHandler for LoadBalancer {
fn on(&mut self, event: Event) {
cast!(match event.data {
RequestArrivalEvent {
processing_time,
sender,
} => {
let host = self.selector.select(&self.state, &self.hosts, sender);
self.hosts[host.id]
.borrow_mut()
.on_new_request(processing_time, event.time);
}
SyncEvent {} => {
for (i, host) in self.hosts.iter().enumerate() {
self.state[i].requests = host.borrow().requests.len();
}
if event.time + self.sync_interval < self.sync_until {
self.ctx
.borrow_mut()
.emit_self(SyncEvent {}, self.sync_interval);
}
}
});
}
}
pub trait HostSelector {
fn select(
&mut self,
states: &[HostSnapshot],
hosts: &[Rc<RefCell<Host>>],
sender: usize,
) -> HostSnapshot;
}
pub struct RoundRobinSelector {
pub ptr: usize,
}
impl HostSelector for RoundRobinSelector {
fn select(
&mut self,
states: &[HostSnapshot],
hosts: &[Rc<RefCell<Host>>],
_sender: usize,
) -> HostSnapshot {
let id = self.ptr;
self.ptr += 1;
if self.ptr == hosts.len() {
self.ptr = 0;
}
states[id]
}
}
pub struct RandomSelector {
rng: Pcg64,
}
impl RandomSelector {
fn new(seed: u64) -> Self {
Self {
rng: Pcg64::seed_from_u64(seed),
}
}
}
impl HostSelector for RandomSelector {
fn select(
&mut self,
states: &[HostSnapshot],
_hosts: &[Rc<RefCell<Host>>],
_sender: usize,
) -> HostSnapshot {
*states.choose(&mut self.rng).unwrap()
}
}
pub struct LeastLoadedSelector {}
impl HostSelector for LeastLoadedSelector {
fn select(
&mut self,
states: &[HostSnapshot],
_hosts: &[Rc<RefCell<Host>>],
_sender: usize,
) -> HostSnapshot {
*states.iter().min_by_key(|s| s.requests).unwrap()
}
}
pub struct PowerOfKSelector {
k: usize,
rng: Pcg64,
}
impl PowerOfKSelector {
pub fn new(k: usize, seed: u64) -> Self {
Self {
k,
rng: Pcg64::seed_from_u64(seed),
}
}
}
impl HostSelector for PowerOfKSelector {
fn select(
&mut self,
states: &[HostSnapshot],
_hosts: &[Rc<RefCell<Host>>],
_sender: usize,
) -> HostSnapshot {
**states
.iter()
.choose_multiple(&mut self.rng, self.k)
.iter()
.min_by_key(|s| s.requests)
.unwrap()
}
}
pub struct PowerOfKSelectorWithUpdates {
k: usize,
rng: Pcg64,
}
impl PowerOfKSelectorWithUpdates {
pub fn new(k: usize, seed: u64) -> Self {
Self {
k,
rng: Pcg64::seed_from_u64(seed),
}
}
}
impl HostSelector for PowerOfKSelectorWithUpdates {
fn select(
&mut self,
states: &[HostSnapshot],
hosts: &[Rc<RefCell<Host>>],
_sender: usize,
) -> HostSnapshot {
**states
.iter()
.choose_multiple(&mut self.rng, self.k)
.iter()
.min_by_key(|s| hosts[s.id].borrow().requests.len())
.unwrap()
}
}
pub struct HashSelector {}
impl HostSelector for HashSelector {
fn select(
&mut self,
states: &[HostSnapshot],
_hosts: &[Rc<RefCell<Host>>],
sender: usize,
) -> HostSnapshot {
states[sender % states.len()]
}
}
pub fn create_selector_by_name(name: &str, seed: u64, num_hosts: usize) -> Box<dyn HostSelector> {
match name {
"RoundRobin" => Box::new(RoundRobinSelector {
ptr: Pcg64::seed_from_u64(seed).gen_range(0..num_hosts),
}),
"Random" => Box::new(RandomSelector::new(seed)),
"LeastLoaded" => Box::new(LeastLoadedSelector {}),
"Hash" => Box::new(HashSelector {}),
_ => {
if name.starts_with("PowerOfK[k=") && name.ends_with("]") {
let k = name[11..name.len() - 1].parse::<usize>().unwrap();
assert!(
(1..=num_hosts).contains(&k),
"k must be between 1 and the number of servers"
);
Box::new(PowerOfKSelector::new(k, seed))
} else if name.starts_with("PowerOfKWithUpdates[k=") && name.ends_with("]") {
let k = name[22..name.len() - 1].parse::<usize>().unwrap();
assert!(
(1..=num_hosts).contains(&k),
"k must be between 1 and the number of servers"
);
Box::new(PowerOfKSelectorWithUpdates::new(k, seed))
} else {
panic!("Unknown host selector: {name}");
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use simcore::simulation::Simulation;
fn choices(name: &str, seed: u64) -> Vec<usize> {
let mut sim = Simulation::new(0);
let log = Rc::new(RefCell::new(Vec::new()));
let hosts: Vec<_> = (0..100)
.map(|id| {
Rc::new(RefCell::new(Host::new(
id,
1.0,
log.clone(),
Rc::new(RefCell::new(sim.create_context(format!("host_{id}")))),
)))
})
.collect();
let states: Vec<_> = (0..hosts.len())
.map(|id| HostSnapshot { id, requests: 0 })
.collect();
let mut selector = create_selector_by_name(name, seed, hosts.len());
(0..200)
.map(|_| selector.select(&states, &hosts, 0).id)
.collect()
}
#[test]
fn selector_streams_are_reproducible_and_depend_on_seed() {
for name in [
"Random",
"RoundRobin",
"PowerOfK[k=2]",
"PowerOfKWithUpdates[k=2]",
] {
assert_eq!(choices(name, 111), choices(name, 111), "{name}");
assert_ne!(choices(name, 111), choices(name, 222), "{name}");
}
}
}
@@ -0,0 +1,8 @@
use serde::Deserialize;
#[derive(Deserialize)]
pub struct Config {
pub balancer: String,
pub n_balancers: u32,
pub sync_interval: f64,
}
@@ -0,0 +1,13 @@
use serde::Serialize;
#[derive(Clone, Serialize)]
pub struct SyncEvent {}
#[derive(Clone, Serialize)]
pub struct RequestArrivalEvent {
pub processing_time: f64,
pub sender: usize,
}
#[derive(Clone, Serialize)]
pub struct RequestEndEvent {}
@@ -0,0 +1,60 @@
use std::cell::RefCell;
use std::collections::VecDeque;
use std::rc::Rc;
use simcore::cast;
use simcore::context::SimulationContext;
use simcore::event::Event;
use simcore::handler::EventHandler;
use crate::events::RequestEndEvent;
use crate::log::EventLog;
pub struct Host {
#[allow(dead_code)]
pub id: usize,
pub requests: VecDeque<(f64, f64)>,
slowdown: f64,
log: Rc<RefCell<EventLog>>,
ctx: Rc<RefCell<SimulationContext>>,
}
impl Host {
pub fn new(
id: usize,
slowdown: f64,
log: Rc<RefCell<EventLog>>,
ctx: Rc<RefCell<SimulationContext>>,
) -> Self {
Self {
id,
requests: Default::default(),
slowdown,
log,
ctx,
}
}
pub fn on_new_request(&mut self, duration: f64, time: f64) {
self.requests.push_back((duration * self.slowdown, time));
if self.requests.len() == 1 {
self.ctx
.borrow_mut()
.emit_self(RequestEndEvent {}, duration * self.slowdown);
}
}
}
impl EventHandler for Host {
fn on(&mut self, event: Event) {
cast!(match event.data {
RequestEndEvent {} => {
let (_, old) = self.requests.pop_front().unwrap();
self.log.borrow_mut().push((event.time, old));
if let Some((delay, _)) = self.requests.front() {
self.ctx.borrow_mut().emit_self(RequestEndEvent {}, *delay);
}
}
});
}
}
@@ -0,0 +1 @@
pub type EventLog = Vec<(f64, f64)>;
@@ -0,0 +1,224 @@
use std::cell::RefCell;
use std::fs::File;
use std::io::prelude::*;
use std::io::BufWriter;
use std::path::Path;
use std::rc::Rc;
use clap::Parser;
use rand::prelude::*;
use rand_distr::Exp;
use rand_pcg::Pcg64;
use simcore::simulation::Simulation;
mod balancer;
mod config;
mod events;
mod host;
mod log;
mod metrics;
use crate::balancer::{create_selector_by_name, LoadBalancer};
use crate::config::Config;
use crate::events::{RequestArrivalEvent, SyncEvent};
use crate::host::Host;
use crate::log::EventLog;
use crate::metrics::{windowed_p99, WINDOW};
#[derive(Parser, Debug)]
#[clap(about, long_about = None)]
struct Args {
/// Results dump path.
#[clap(long)]
dump: String,
/// Configuration JSON file.
#[clap(long)]
config: String,
/// Number of servers.
#[clap(long, default_value = "100")]
servers: u32,
/// Last arrival time shown on the graph (seconds).
#[clap(long, default_value = "3600")]
time_limit: f64,
/// Number of senders generating requests.
#[clap(long, default_value = "200")]
senders: u32,
/// Load distribution preset (balanced/skewed).
#[clap(long, default_value = "balanced")]
preset: String,
/// Adds slower servers to simulate heterogenous cluster.
#[clap(long)]
different_servers: bool,
/// Random seed.
#[clap(long, default_value = "123")]
seed: u64,
/// Reproduce correlated selector streams for the teaching experiment.
#[clap(long)]
correlated_selectors: bool,
}
fn run_simulation(args: &Args, config: &Config) -> Vec<(f64, f64)> {
assert!(
args.servers > 0 && args.senders > 0,
"servers and senders must be positive"
);
assert!(
args.time_limit.is_finite() && args.time_limit > 0.0,
"time-limit must be finite and positive"
);
assert!(config.n_balancers > 0, "n_balancers must be positive");
assert!(
config.sync_interval.is_finite() && config.sync_interval > 0.0,
"sync_interval must be finite and positive"
);
// Generate all arrivals needed by the last complete measurement window.
let arrivals_end = args.time_limit + WINDOW;
let mut sim = Simulation::new(args.seed);
let ctx = sim.create_context("entry point");
let log = Rc::new(RefCell::new(EventLog::new()));
let mut hosts = Vec::with_capacity(args.servers as usize);
for i in 0..args.servers {
let slowdown = if args.different_servers && i < args.servers / 20 {
5.0
} else {
1.0
};
hosts.push(Rc::new(RefCell::new(Host::new(
i as usize,
slowdown,
log.clone(),
Rc::new(RefCell::new(sim.create_context(format!("host_{i}")))),
))));
sim.add_handler(format!("host_{i}"), hosts[i as usize].clone());
}
let mut balancers = Vec::with_capacity(config.n_balancers as usize);
// Keep selector randomness separate from the workload generator.
let mut selector_seeds = Pcg64::seed_from_u64(args.seed);
for i in 0..config.n_balancers {
let seed = if args.correlated_selectors {
111
} else {
selector_seeds.gen()
};
balancers.push(Rc::new(RefCell::new(LoadBalancer::new(
hosts.clone(),
create_selector_by_name(&config.balancer, seed, hosts.len()),
config.sync_interval,
arrivals_end,
Rc::new(RefCell::new(sim.create_context(format!("balancer_{i}")))),
))));
sim.add_handler(format!("balancer_{i}"), balancers[i as usize].clone());
ctx.emit(SyncEvent {}, sim.lookup_id(&format!("balancer_{i}")), 0.0);
}
let mut rng = Pcg64::seed_from_u64(args.seed);
let mut routing_rng = Pcg64::seed_from_u64(args.seed.wrapping_add(1));
for sender in 0..(args.senders as usize) {
let distr = Exp::new(if args.preset == "skewed" && sender == 0 {
50.0
} else {
1.0
})
.unwrap();
let mut t = 0f64;
loop {
t += distr.sample(&mut rng) * 5.;
if t >= arrivals_end {
break;
}
let processing_time = rng.gen_range(0.5..2.5);
ctx.emit(
RequestArrivalEvent {
processing_time,
sender,
},
sim.lookup_id(&format!(
"balancer_{}",
routing_rng.gen_range(0..balancers.len())
)),
t,
);
}
}
// Periodic updates stop at arrivals_end. Drain every queued request so that
// slow responses are measured in full, not truncated at a time cutoff.
sim.step_until_no_events();
let requests = log
.borrow()
.iter()
.map(|&(end, start)| (start, end - start))
.collect();
windowed_p99(requests, args.time_limit)
}
fn main() {
let args = Args::parse();
let configs: Vec<Config> =
serde_json::from_reader(File::open(Path::new(&args.config)).unwrap()).unwrap();
let mut out = BufWriter::new(File::create(&args.dump).unwrap());
for config in configs.iter() {
let result = run_simulation(&args, config);
out.write_all(config.balancer.as_bytes()).unwrap();
out.write_all(b"\n").unwrap();
out.write_all(
result
.iter()
.map(|x| format!("{:.3}", x.0))
.collect::<Vec<_>>()
.join(",")
.as_bytes(),
)
.unwrap();
out.write_all(b"\n").unwrap();
out.write_all(
result
.iter()
.map(|x| format!("{}", x.1))
.collect::<Vec<_>>()
.join(",")
.as_bytes(),
)
.unwrap();
out.write_all(b"\n").unwrap();
}
out.flush().unwrap();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn overloaded_server_finishes_requests_after_arrival_horizon() {
let args = Args::parse_from([
"lab",
"--dump",
"unused",
"--config",
"unused",
"--servers",
"1",
"--senders",
"100",
"--time-limit",
"1",
]);
let config = Config {
balancer: "Hash".into(),
n_balancers: 1,
sync_interval: 0.01,
};
let result = run_simulation(&args, &config);
assert!(!result.is_empty());
assert!(result.iter().all(|(t, _)| *t <= args.time_limit));
assert!(result[0].1 > args.time_limit + WINDOW);
assert_eq!(result, run_simulation(&args, &config));
}
}
@@ -0,0 +1,132 @@
use std::cmp::Ordering;
use std::collections::BTreeSet;
pub const WINDOW: f64 = 60.0;
#[derive(Copy, Clone, PartialEq)]
struct RequestTime {
time: f64,
id: usize,
}
impl Eq for RequestTime {}
impl Ord for RequestTime {
fn cmp(&self, other: &Self) -> Ordering {
self.time
.total_cmp(&other.time)
.then(self.id.cmp(&other.id))
}
}
impl PartialOrd for RequestTime {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
/// Nearest-rank p99 of response times for requests arriving in [t, t + WINDOW).
/// Emit one point per distinct arrival time t <= time_limit.
pub fn windowed_p99(mut requests: Vec<(f64, f64)>, time_limit: f64) -> Vec<(f64, f64)> {
requests.sort_by(|a, b| a.0.total_cmp(&b.0));
let mut result = Vec::new();
let mut items = BTreeSet::new();
let mut end = 0;
let mut start = 0;
while start < requests.len() && requests[start].0 <= time_limit {
let time = requests[start].0;
while end < requests.len() && requests[end].0 < time + WINDOW {
items.insert(RequestTime {
time: requests[end].1,
id: end,
});
end += 1;
}
// ceil(0.99 * n), computed exactly, without floating-point rounding.
let rank = items.len() - items.len() / 100;
let p99 = items.iter().rev().nth(items.len() - rank).unwrap().time;
result.push((time, p99));
// Equal response times remain distinct observations thanks to id.
// Remove the entire arrival-time group before advancing the window.
while start < requests.len() && requests[start].0 == time {
items.remove(&RequestTime {
time: requests[start].1,
id: start,
});
start += 1;
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn expires_old_requests_and_excludes_right_boundary() {
let requests = vec![
(0.0, 100.0),
(1.0, 10.0),
(61.0, 1.0),
(62.0, 1.0),
(123.0, 3.0),
];
assert_eq!(
windowed_p99(requests, 123.0),
vec![
(0.0, 100.0),
(1.0, 10.0),
(61.0, 1.0),
(62.0, 1.0),
(123.0, 3.0)
]
);
}
#[test]
fn uses_nearest_rank_and_counts_equal_response_times() {
let mut requests = vec![(0.0, 1.0); 99];
requests.push((0.0, 100.0));
assert_eq!(windowed_p99(requests, 0.0), vec![(0.0, 1.0)]);
let requests = (1..=100).map(|n| (0.0, n as f64)).collect();
assert_eq!(windowed_p99(requests, 0.0), vec![(0.0, 99.0)]);
}
#[test]
fn sorts_arrivals_and_limits_reported_times() {
assert_eq!(
windowed_p99(vec![(120.0, 8.0), (60.0, 2.0), (0.0, 4.0)], 60.0),
vec![(0.0, 4.0), (60.0, 2.0)]
);
assert!(windowed_p99(Vec::new(), 60.0).is_empty());
}
#[test]
fn matches_independent_sort_per_window_reference() {
let requests: Vec<_> = (0..400)
.map(|i| (((i * 37) % 241) as f64, ((i * 17) % 31 + 1) as f64))
.collect();
let mut times: Vec<_> = requests
.iter()
.map(|r| r.0)
.filter(|t| *t <= 180.0)
.collect();
times.sort_by(f64::total_cmp);
times.dedup();
let expected: Vec<_> = times
.into_iter()
.map(|t| {
let mut values: Vec<_> = requests
.iter()
.filter(|r| r.0 >= t && r.0 < t + WINDOW)
.map(|r| r.1)
.collect();
values.sort_by(f64::total_cmp);
let rank = (0.99 * values.len() as f64).ceil() as usize;
(t, values[rank - 1])
})
.collect();
assert_eq!(windowed_p99(requests, 180.0), expected);
}
}
@@ -0,0 +1,23 @@
services:
nginx:
image: nginx
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
ports:
- 8000:8000
server1:
image: hello
build:
context: hello-world
command: 8000 "Alice"
ports:
- 8001:8000
server2:
image: hello
build:
context: hello-world
command: 8000 "Bob"
ports:
- 8002:8000
@@ -0,0 +1,8 @@
FROM python:3.9-slim
RUN pip install Flask==2.3.3
WORKDIR /app
COPY . .
ENTRYPOINT ["python3", "main.py"]
@@ -0,0 +1,15 @@
from flask import Flask
import sys
app = Flask(__name__)
if len(sys.argv) < 3:
print('please call with arguments <port> <hostname>')
exit()
@app.route('/')
def index():
return f'Hello from {app.name}\n'
app.name = sys.argv[2]
app.run(host='0.0.0.0', port=int(sys.argv[1]))
@@ -0,0 +1,13 @@
upstream rr_backend {
server server2:8000;
server server1:8000;
}
server {
listen 8000;
server_name _;
location / {
proxy_pass http://rr_backend;
}
}
@@ -0,0 +1,16 @@
# Пример распределения нагрузки с помощью Nginx
В *hello-world/main.py* написан простейший hello-world сервер, который отвечает хостом, указанном при запуске сервера в формате `python3 main.py <HOST>`.
В *nginx.conf* приведен пример конфигурации Nginx, который распределяет нагрузку между двумя инстансами hello-world сервера. Nginx работает как HTTP-прокси: принимает запрос клиента, отправляет его бэкенду и возвращает ответ клиенту. Stateless-логика приложения при этом совместима с TCP-соединениями на обоих участках.
Другие стратегии балансировки: https://docs.nginx.com/nginx/admin-guide/load-balancer/http-load-balancer/
Запуск примера:
```bash
docker compose build
docker compose up
for i in {0..20}; do curl localhost:8000; done
```
+94
View File
@@ -0,0 +1,94 @@
# Балансировка трафика и шардирование
На семинаре разбираем, как распределять запросы между серверами и данные между узлами хранения, чтобы масштабировать сервисы при росте нагрузки и объёма данных. Сравнить алгоритмы балансировки можно в [лабораторной работе](./load-balancing-lab/), а запустить небольшие сервисы — в примерах [Nginx](./nginx-balancing/) и [key-value](./kv/).
## Балансировка трафика
При росте "читающей" нагрузки на сервис одно из простых решений — запустить несколько копий
сервиса и научиться распределять нагрузку между ними. Балансировщик принимает запрос или пакет и выбирает сервер для его обработки. На выбор могут влиять адрес клиента, загрузка и производительность
серверов, а также состояние уже установленных соединений.
При переходе к модели с несколькими копиями сервиса необходимо продумать,
нужно ли обмениваться информацией между данными копиями. Как правило, проще всего балансировать
нагрузку в случае реплицированного и/или stateless-сервиса.
Одним из вопросов при выборе балансировщика нагрузки является вопрос: на каком "уровне" вы хотите
балансировать нагрузку? L3/L4-балансировщики работают на третьем или четвёртом уровне модели OSI и распределяют
входящие TCP- и UDP-пакеты. На данном уровне мы не знаем о клиенте ничего, кроме его IP-адреса и порта,
и не смотрим на содержимое запросов, поэтому балансировка менее точная, но более легковесная.
L7-балансировщики оперируют на уровне протоколов приложений, например HTTP. Данные балансировщики более
гибкие и имеют больше информации о пользователе. Также достаточно много полезной информации можно
получить из заголовков HTTP-запроса (например, идентификатор пользователя). Это позволяет сделать
балансировку более точной, но балансировщик начинает выполнять больше задач, из-за чего требуется
больше ресурсов.
### Запросы и соединения
Stateless-приложение не требует хранить состояние клиентской сессии в конкретном экземпляре между запросами. Оно может работать поверх TCP: состояние транспортного соединения и состояние приложения — разные вещи. Например, HTTP-прокси может распределять независимые запросы между серверами, поддерживая собственные TCP-соединения с ними.
UDP также не означает отсутствие состояния: HTTP/3 работает поверх QUIC, который поддерживает соединения, используя UDP. См. [HTTP/3](https://www.rfc-editor.org/rfc/rfc9114.html) и [QUIC](https://www.rfc-editor.org/rfc/rfc9000.html).
При балансировке TCP-пакетов одного соединения они должны попадать на сервер, который знает состояние этого соединения. Для этого можно хранить таблицу соединений или детерминированно выбирать сервер по адресам и портам. Изменение набора серверов требует осторожности: оно может изменить выбор для уже открытых соединений. Короткий таймаут таблицы сам по себе эту задачу не решает.
DNS помогает выбрать адрес перед установлением соединения. Новый DNS-ответ не меняет удалённый адрес существующего TCP-соединения. При anycast один адрес объявляют из нескольких мест, а путь к нему выбирает сеть; изменение этого пути — отдельная причина попадания пакетов на другой балансировщик.
Ответ может возвращаться через прокси или идти напрямую от бэкенда клиенту — схема Direct Server Return/Response (DSR). Подход зависит от архитектуры. Например, в нашем [демо Nginx](./nginx-balancing/) ответы проходят через HTTP-прокси. Подробнее: [Google SRE: балансировка на входе](https://sre.google/sre-book/load-balancing-frontend/).
### Алгоритмы выбора сервера
| Алгоритм | Как выбирается сервер |
| --- | --- |
| **Round robin** | Серверы перебираются по кругу. |
| **Random** | Для каждого выбора берётся случайный сервер. Даже равномерный случайный выбор даёт колебания нагрузки. |
| **Least connections** | Выбирается сервер с наименьшим числом активных соединений. **LeastLoaded** в лабораторной работе использует другую метрику — число незавершённых запросов. |
| **Power of Two Choices** | Выбираются два случайных сервера, затем менее загруженный из них по выбранной метрике. Обобщение на k кандидатов называется **Power of k Choices**. |
| **IP hashing** | Сервер выбирается детерминированно по адресу клиента, чтобы его запросы попадали на один бэкенд при неизменном составе серверов. |
| **Consistent hashing** | Устойчивое отображение ключа на сервер уменьшает число переназначений при изменении состава серверов. Применяется и в балансировке, и в шардировании. |
Серверы могут иметь разные мощности; тогда в выборе учитывают веса. Число соединений или запросов не всегда точно отражает нагрузку: запросы различаются по стоимости, а сведения о состоянии успевают устареть. Несколько балансировщиков, видя один и тот же наименее загруженный сервер, могут одновременно направить на него много запросов.
Балансировщику также нужны сведения о доступности бэкендов. Активные проверки здоровья и наблюдение за ошибками обычных запросов позволяют временно исключать проблемные экземпляры. Например, [Keepalived](https://github.com/acassen/keepalived#readme) поддерживает проверки здоровья и VRRP для переключения виртуального адреса; его название не следует путать с TCP keepalive.
В Nginx, Envoy и HAProxy многие из этих алгоритмов уже реализованы. В [лабораторной работе](./load-balancing-lab/) мы сравним их на общем наборе серверов при разной нагрузке и частоте обновления сведений.
## Шардирование и репликация
Шардирование делит данные на части — **шарды**. Оно нужно, когда данные или нагрузка на их обработку уже не помещаются на одном сервере. **Репликация** создаёт несколько копий данных для доступности и надёжности. Эти приёмы можно сочетать: каждый логический шард хранить в нескольких репликах.
Шард, реплика и физический сервер не обязательно соответствуют друг другу один к одному. Один сервер может хранить несколько шардов и реплики разных частей данных. Есть и схемы, где набор узлов хранения выбирается отдельно для каждого ключа; тогда эти наборы могут пересекаться.
<p align="center"><img src="./content/sharding.png" width="100%" /></p>
Ключ шардирования позволяет детерминированно определить, где находятся нужные данные. В [демо kv](./kv/) можно отдельно сравнить шардирование и репликацию; сочетание обоих подходов в нём не реализовано.
### Остаток от деления
Простой вариант: `shard_id = hash(key) % num_shards`. При равномерном хеше ключи распределяются по шардам примерно равномерно, но при изменении числа шардов приходится перемещать большую часть данных. Популярность отдельных ключей при этом может быть очень разной: равное число ключей не означает равную нагрузку.
Пример реализации: [simple-sharding](./simple-sharding/).
### Consistent hash ring
Представим пространство хешей как кольцо. Хеш идентификатора узла задаёт его позицию на кольце, а хеш ключа — позицию данных. Для ключа выбираем ближайший узел по часовой стрелке. Можно договориться и об обратном направлении; все участники должны применять одно правило.
<p align="center"><img src="./content/ring_composition.png" width="100%" /></p>
*[Источник иллюстрации](https://medium.com/@kewei.train/deterministic-aperture-algorithm-%E7%AD%86%E8%A8%98-3a3bbb5e30e2)*
При добавлении узла к нему переходит часть ключей соседнего участка; при удалении его участок переходит следующему узлу. При равномерном хешировании и фиксированном коэффициенте репликации ожидаемый объём перемещений имеет порядок N / n, где N — объём данных, n — число узлов.
Одна случайная точка на физический сервер может давать заметный перекос размеров участков. Поэтому одному серверу назначают несколько **виртуальных узлов** — позиций на кольце. Сумма нескольких участков обычно распределена ровнее; число виртуальных узлов можно выбирать с учётом мощности сервера.
Если нужны r копий ключа, обходят кольцо и выбирают r разных физических узлов. Виртуальные позиции, принадлежащие уже выбранному серверу, пропускают: несколько копий на одной машине не защищают от её отказа. Пример такой схемы описан в [Dynamo, разделы 4.2–4.3](https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf).
## Полезные материалы
- [Оригинальная статья про балансер Твиттера](https://blog.twitter.com/engineering/en_us/topics/infrastructure/2019/daperture-load-balancer.html)
- [Описание алгоритмов балансировки в envoy proxy](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/load_balancers)
- [Load Balancing in the Datacenter](https://sre.google/sre-book/load-balancing-datacenter/)
- [Power of 2 choices](https://www.eecs.harvard.edu/~michaelm/postscripts/mythesis.pdf)
- [Test Driving “Power of Two Random Choices” Load Balancing](https://www.haproxy.com/blog/power-of-two-load-balancing/)
- [A Guide to Consistent Hashing](https://www.toptal.com/big-data/consistent-hashing)
- [Consistent hashing](http://michaelnielsen.org/blog/consistent-hashing/)
- [Consistent Hashing: Algorithmic Tradeoffs](https://dgryski.medium.com/consistent-hashing-algorithmic-tradeoffs-ef6b8e2fcae8)
- [Sharding with consistent hashing](https://blog.yugabyte.com/four-data-sharding-strategies-we-analyzed-in-building-a-distributed-sql-database/) и [cassandra partitioning (секция 5)](https://www.cs.cornell.edu/projects/ladis2009/papers/lakshman-ladis2009.pdf)
@@ -0,0 +1,35 @@
import requests
# put your shards for experiments here
shards = {
'server1': 'http://localhost:8001/',
'server2': 'http://localhost:8002/'
}
keys = [
'aasdjkahsdl',
'clkjhlkajhsd',
'basdjahlkj',
'dakjshd',
]
MAX_HASH = 1000000
def hashf(x: str):
# sum of ascii codes
return sum([ord(c) for c in x]) % MAX_HASH
def get_shard(k, shards):
i = hashf(k) % len(shards)
return list(shards)[i]
def call_by_key(key):
shard = get_shard(key, shards.keys())
resp = requests.get(shards[shard])
return resp.text
if __name__=='__main__':
for k in keys:
print(call_by_key(k))
@@ -0,0 +1,21 @@
# Пример шардирования
Обычно под шардированием понимают "распределение чего-либо по множеству узлов (*шардов*) детерминированно и постоянно для указанного ключа". Например, шардирование данных в key-value хранилище обычно происходит на основе ключа. Шардирование запросов между http серверами может происходить на основе имени пользователя или его id. Такое свойство обычно называют *ключом шардирования*.
В этом примере написан самый простой пример шардирования запросов: в словаре указываются url куда слать запрос и имена серверов. Ниже указан список ключей для проверки алгоритма шардирования. Для каждого ключа скрипт выбирает сервер и отправляет на него get запрос, после чего печатает результат в *stdout*.
Запуск:
```bash
# 1. Запустить какие-то сервера, которые будут принимать запрос. Например, [hello-world](../nginx-balancing/README.md) из примера с nginx или [key-value](../kv/README.md) из примера с шардированием ключей.
cd ../nginx-balancing
docker compose up
# 2. Поправить словарь shards, добавить туда свои url (если они отличны от уже указанных)
# 3. Запустить скрипт, выполняющий запросы
python3 client.py
# 4. Посмотреть на результаты, что для указанного списка ключей запросы были сделаны на разные сервера
```