15 KiB
Plan:
gossipsub performs at scale
This test plan evaluates the performance of gossipsub, the gossiping mesh routing protocol implemented by go-libp2p-pubsub.
Installation & Setup
We're using python to generate testground composition files, and we shell out to a few external commands, so there's some environment setup to do.
Requirements
Hardware
While no special hardware is needed to run the tests, running with a lot of test instances requires considerable CPU and RAM, and will likely exceed the capabilities of a single machine.
A large workstation with many CPU cores can reasonably run a few hundred instances using the testground
local:docker
runner, although the exact limit will require some trial and error. In early testing we were able to
run 500 containers on a 56 core Xeon W-3175X with 124 GiB of RAM, although it's possible we could run more
now that we've optimized things a bit.
It's useful to run with the local:docker
or local:exec
runners during test development, so long as you use
fairly small instance counts (~25 or so works fine on a 2018 13" MacBook Pro with 16 GB RAM).
When using the local:docker
runner, it's a good idea to periodically garbage collect the docker images created by
testground using docker system prune
to reclaim disk space.
To run larger tests like the ones in the saved configurations, you'll need a kubernetes cluster.
To create your own cluster, follow the directions in the testground/infra repo
to provision a cluster on AWS, and configure your tests to use the cluster:k8s
test runner.
The testground daemon process can be running on your local machine, as long as it has access to the k8s cluster. The machine running the daemon must have Docker installed, and the user account must have permission to use docker and have the correct AWS credentials to connect to the cluster. When running tests on k8s, the machine running the testground daemon doesn't need a ton of resources, but ideally it should have a fast internet connection to push the Docker images to the cluster.
Running the analysis notebooks benefits from multiple cores and consumes quite a bit of RAM, especially on the first run when it's converting data to pandas format. It's best to have at least 8 GB of RAM free when running the analysis notebook for the first time.
Also note that closing the browser tab containing a running Jupyter notebook does not stop the python kernel and reclaim
the memory used. It's best to select Close and Halt
from the Jupyter File
menu when you're done with the analysis
notebook instead of just closing the tab.
Testground
You'll need to have the testground binary built and accessible
on your $PATH
. Testground must be version 0.5.0 or newer.
After running testground list
for the first time, you should have a ~/testground
directory. You can change this
to another location by setting the TESTGROUND_HOME
environment variable.
Cloning this repo
The testground client will look for test plans in $TESTGROUND_HOME/plans
, so this repo should be cloned or
symlinked into there:
cd ~/testground/plans # if this dir doesn't exist, run 'testground list' once first to create it
git clone git@github.com:libp2p/test-plans libp2p-plans
Python
We need python 3.7 or later, ideally in a virtual environment. If you have python3 installed, you can create
a virtual environment named venv
in this repo and it will be ignored by git:
python3 -m venv venv
After creating the virtual environment, you need to "activate" it for each shell session:
# bash / zsh:
source ./venv/bin/activate
# fish:
source ./venv/bin/activate.fish
You'll also need to install the python packages used by the scripts:
pip install -r scripts/requirements.txt
External binaries
The run scripts rely on a few commands being present on the PATH
:
- the
testground
binary go
Running Tests
Running using the Runner Jupyter notebook
With the python virtualenv active, run
jupyter notebook
This will start a Jupyter notebook server and open a browser to the Jupyter file navigator.
In the Jupyter UI, navigate to the scripts
dir and open Runner.ipynb
.
This will open the runner notebook, which lets you configure the test parameters using a configuration UI.
You'll need to run all the cells to prepare the notebook UI using Cell menu > Run All
. You can reset
the notebook state using the Kernel Menu > Restart and Run All
command.
The cell at the bottom of the notebook has a "Run Test" button that will convert the configured parameters
to a composition file and start running the test. It will shell out to the testground
client binary,
so if you get an error about a missing executable, make sure testground
is on your PATH
and restart
the Jupyter server.
At the end of a successful test, there will be a new output/pubsub-test-$timestamp
directory (relative to
the scripts
dir) containing the composition file, the full test-output.tgz
file collected from testground,
and an analysis
directory.
The analysis
directory has relevant files that were extracted from the test-output.tgz
archive, along with a
new Jupyter notebook, Analysis.ipynb
. See below for more details about the analysis notebook.
If the test fails (testground
returns a non-zero exit code), the runner script will move the pubsub-test-$timestamp
dir to ./output/failed
.
The "Test Execution" section of the config UI will let you override the output path, for example if you want to give your test a meaningful name.
Targeting a specific version of go-libp2p-pubsub
The default configuration is to test against the current master
branch of go-libp2p-pubsub,
but you can change that in the Pubsub
panel of the configuration UI. You can enter the name
of a branch or tag, or the full SHA-1 hash of a specific commit.
Important: if you target a version before the Gossipsub v1.1 PR was merged, you must uncheck the "target hardening branch API" checkbox to avoid build failures due to missing methods.
Saved test configurations
You can save configuration snapshots to JSON files and load them again using the buttons at the bottom
of the configuration panel. The snapshots contain the state of all the configuration widgets, so can
only be used with the Runner notebook, not the command line run.py
script.
There are several saved configs in scripts/configs
that we've been using to evaluate different scenarios.
There are subdirectories inside of scripts/configs
corresponding to different testground
Runners, and
there are a few configurations for each runner with various node counts. For example, configs/local-exec/25-peers.json
will create a composition for the test using the exec:go
builder and local:exec
runner, with 25 pubsub peers,
while configs/local-docker/25-peers.json
will use the docker:go
and local:docker
runner.
The saved configs all expect to find the testground
daemon on a non-standard port (8080 instead of 8042).
If you're not running the daemon on port 8080, you can change the endpoint in the Testground
section of
the config UI, or tell the daemon to listen on 8080 by editing ~/testground/.env.toml
.
Running using the cli scripts
Inside the scripts
directory, the run.py
script will generate a composition and run it by shelling out to
testground
. If you just want it to generate the composition, you can skip the test run by passing the --dry-run
flag.
You can get the full usage by running ./run.py --help
.
To run a test with baseline parameters (as defined in scripts/templates/baseline/params/_base.toml
), run:
./run.py
By default, this will create a directory called ./output/pubsub-test-$timestamp
, which will have a composition.toml
file inside, as well as a template-params.toml
that contains the params used to generate the composition.
You can control the output location with the -o
and --name
flags, for example:
./run.py -o /tmp --name 'foo'
# creates directory at /tmp/pubsub-test-$timestamp-foo
Note that the params defined in scripts/templates/baseline/params/_base.toml
have very low instance counts and
are likely useless for real-world evaluation of gossipsub.
You can override individual template parameters using the -D
flag, for example, ./run.py -D T_RUN=5m
.
There's no exhaustive list of template parameters, so check the template at scripts/templates/baseline/template.toml.j2
to see what's defined.
Alternatively, you can create a new toml file containing the parameters you want to set, and it will override
any parameters defined in scripts/templates/baseline/params/_base.toml
By default, the run.py
script will extract the test data from the collected test output archive and copy the
analysis notebook to the analysis
subdirectory of the test output dir. If you want to skip this step,
you can pass the --skip-analysis
flag.
Analyzing Test Outputs
After running a test, there should be a directory full of test outputs, with an analysis
dir containing
an Analysis.ipynb
Jupyter notebook. If you're not already running the Jupyter server, start it with
jupyter notebook
, and use the Jupyter UI to navigate to the analysis notebook and open it.
Running all the cells in the analysis notebook will convert the extracted test data to
pandas DataFrame
s. This conversion takes a minute or two depending on the
size of the test and your hardware, but the results are cached to disk, so future runs should be pretty fast.
Once everything is loaded, you'll see some charts and tables, and there will be a new figures
directory inside the
analysis
dir containing the charts in a few image formats. There's also a figures.zip
with the same contents
for easier downloading / storage.
Running the analysis notebook from the command line
If you just want to generate the charts and don't care about interacting with the notebook, you can execute the analysis notebook using a cli script.
Change to the scripts
directory, then run
./analyze.py run_notebook ./output/pubsub-test-$timestamp
This will copy the latest analysis notebook template into the analysis
directory and execute the notebook, which
will generate the chart images.
This command is useful if you've made changes to the analysis notebook template and want to re-run it against a
bunch of existing test outputs. In that case, you can pass multiple paths to the run_notebook
subcommand:
./analyze.py run_notebook ./output/pubsub-test-*
# will run the latest notebook against everything in `./output
Storing & Fetching Test Outputs in S3
The scripts/sync_outputs.py
script is a wrapper around the rclone command that
helps backup test outputs to an s3 bucket, or fetch a previously stored output directory to the local filesystem.
The AWS credentials are pulled from the environment - see the AWS cli docs if you haven't already configured the aws cli to use your credentials. The configured user must have permission to access the bucket used to sync.
rclone
must be installed and on the $PATH
to use the sync_outputs.py
script.
By default, it uses the S3 bucket gossipsub-test-outputs
in eu-central-1
, but you can control this with the
--bucket
and --region
flags.
To backup all the test outputs in ./output
:
./sync_outputs.py store-all ./output
It will ignore the failed
subdirectory automatically, but if you want to ignore more, you can pass in a flag:
./sync_outputs.py store-all ./output --ignore some-dir-you-dont-want-to-store
Alternatively, you can selectively store one or more test outputs with the store
subcommand:
./sync_outputs.py store ./output/pubsub-test-20200409-152658 ./output/pubsub-test-20200409-152983 # etc...
You can also fetch test outputs from S3 to the local filesystem.
To fetch everything from the bucket into ./output
:
./sync_outputs.py fetch-all ./output
Or, to fetch one or more tests from the bucket instead of everything:
./sync_outputs.py fetch --dest=./output pubsub-test-20200409-152658
You can list all the top-level directories in the S3 bucket (so you know what to fetch) using the list
command:
./sync_outputs.py list
Code Overview
The test code all lives in the test
directory.
main.go
is the actual entry point, but it just calls into the "real" main function, RunSimulation
, which is defined
in run.go
.
params.go
contains the parameter parsing code. The parseParams
function will return a testParams
struct with
all test parameters.
The set of params provided to each test instance depends on which composition group they're in. The composition
template we're using defines two groups: publishers
, and lurkers
. The lurkers and publishers have
identical params with the exception of the boolean publisher
param, which controls whether they will publish messages
or just consume them.
After parsing the params, RunSimulation
will prepare the libp2p Host
s, do some network setup and then
call runPubsubNode
to begin the test.
discovery.go
contains a SyncDiscovery
component that uses the testground sync service to broadcast information about
the test peers (e.g. addreses, whether they're honest, etc) with every other peer. It uses this information to connect
nodes to each other in various topologies.
The honest node implementation is in node.go
, and there are also node_v10.go
and node_v11.go
files
that allow us to target the new gossipsub v1.1 API or the old v1.0 API by setting a build tag. If the v1.0 API
is used, the test will not produce any peer score information, since that was added in v1.1.
The tracer.go
file implements the pubsub.EventTracer
interface to capture pubsub events and produce test metrics.
Because the full tracer output is quite large (several GB for a few minutes of test execution with lots of nodes),
we aggregate the trace events at runtime and spit out a json file with aggregated metrics at the end of the test.
We also capture a filtered subset of the original traces, containing only Publish, Deliver, Graft, and Prune events.
At the end of the test, we run tracestat
on the filtered traces to calculate the latency distribution and get a summary of publish and deliver counts.