Advisor: README and blog, and also tests for DBBenchRunner, DatabaseOptions (#4201)

Summary:
This pull request adds a README file and a blog post for the Advisor tool. It also adds the missing tests for some Optimizer modules. Some comments are added to the classes being tested for improved readability.
Pull Request resolved: https://github.com/facebook/rocksdb/pull/4201

Reviewed By: maysamyabandeh

Differential Revision: D9125311

Pulled By: poojam23

fbshipit-source-id: aefcf2f06eaa05490cc2834ef5aa6e21f0d1dc55
main
Pooja Malik 6 years ago committed by Facebook Github Bot
parent f8f6983f89
commit 892a156267
  1. 58
      docs/_posts/2018-08-01-rocksdb-tuning-advisor.markdown
  2. 96
      tools/advisor/README.md
  3. 141
      tools/advisor/advisor/db_bench_runner.py
  4. 93
      tools/advisor/advisor/db_options_parser.py
  5. 101
      tools/advisor/advisor/db_stats_fetcher.py
  6. 89
      tools/advisor/advisor/rule_parser_example.py
  7. 20
      tools/advisor/test/input_files/OPTIONS-000005
  8. 3
      tools/advisor/test/input_files/log_stats_parser_keys_ts
  9. 2
      tools/advisor/test/input_files/test_rules.ini
  10. 147
      tools/advisor/test/test_db_bench_runner.py
  11. 5
      tools/advisor/test/test_db_log_parser.py
  12. 216
      tools/advisor/test/test_db_options_parser.py
  13. 126
      tools/advisor/test/test_db_stats_fetcher.py
  14. 10
      tools/advisor/test/test_rule_parser.py

@ -0,0 +1,58 @@
---
title: Rocksdb Tuning Advisor
layout: post
author: poojam23
category: blog
---
The performance of Rocksdb is contingent on its tuning. However, because
of the complexity of its underlying technology and a large number of
configurable parameters, a good configuration is sometimes hard to obtain. The aim of
the python command-line tool, Rocksdb Advisor, is to automate the process of
suggesting improvements in the configuration based on advice from Rocksdb
experts.
### Overview
Experts share their wisdom as rules comprising of conditions and suggestions in the INI format (refer
[rules.ini](https://github.com/facebook/rocksdb/blob/master/tools/advisor/advisor/rules.ini)).
Users provide the Rocksdb configuration that they want to improve upon (as the
familiar Rocksdb OPTIONS file —
[example](https://github.com/facebook/rocksdb/blob/master/examples/rocksdb_option_file_example.ini))
and the path of the file which contains Rocksdb logs and statistics.
The [Advisor](https://github.com/facebook/rocksdb/blob/master/tools/advisor/advisor/rule_parser_example.py)
creates appropriate DataSource objects (for Rocksdb
[logs](https://github.com/facebook/rocksdb/blob/master/tools/advisor/advisor/db_log_parser.py),
[options](https://github.com/facebook/rocksdb/blob/master/tools/advisor/advisor/db_options_parser.py),
[statistics](https://github.com/facebook/rocksdb/blob/master/tools/advisor/advisor/db_stats_fetcher.py) etc.)
and provides them to the [Rules Engine](https://github.com/facebook/rocksdb/blob/master/tools/advisor/advisor/rule_parser.py).
The Rules uses rules from experts to parse data-sources and trigger appropriate rules.
The Advisor's output gives information about which rules were triggered,
why they were triggered and what each of them suggests. Each suggestion
provided by a triggered rule advises some action on a Rocksdb
configuration option, for example, increase CFOptions.write_buffer_size,
set bloom_bits to 2 etc.
### Usage
An example command to run the tool:
```shell
cd rocksdb/tools/advisor
python3 -m advisor.rule_parser_example --rules_spec=advisor/rules.ini --rocksdb_options=test/input_files/OPTIONS-000005 --log_files_path_prefix=test/input_files/LOG-0 --stats_dump_period_sec=20
```
Sample output where a Rocksdb log-based rule has been triggered :
```shell
Rule: stall-too-many-memtables
LogCondition: stall-too-many-memtables regex: Stopping writes because we have \d+ immutable memtables \(waiting for flush\), max_write_buffer_number is set to \d+
Suggestion: inc-bg-flush option : DBOptions.max_background_flushes action : increase suggested_values : ['2']
Suggestion: inc-write-buffer option : CFOptions.max_write_buffer_number action : increase
scope: col_fam:
{'default'}
```
### Read more
For more information, refer to [advisor](https://github.com/facebook/rocksdb/tree/master/tools/advisor/README.md).

@ -0,0 +1,96 @@
# Rocksdb Tuning Advisor
## Motivation
The performance of Rocksdb is contingent on its tuning. However,
because of the complexity of its underlying technology and a large number of
configurable parameters, a good configuration is sometimes hard to obtain. The aim of
the python command-line tool, Rocksdb Advisor, is to automate the process of
suggesting improvements in the configuration based on advice from Rocksdb
experts.
## Overview
Experts share their wisdom as rules comprising of conditions and suggestions in the INI format (refer
[rules.ini](https://github.com/facebook/rocksdb/blob/master/tools/advisor/advisor/rules.ini)).
Users provide the Rocksdb configuration that they want to improve upon (as the
familiar Rocksdb OPTIONS file —
[example](https://github.com/facebook/rocksdb/blob/master/examples/rocksdb_option_file_example.ini))
and the path of the file which contains Rocksdb logs and statistics.
The [Advisor](https://github.com/facebook/rocksdb/blob/master/tools/advisor/advisor/rule_parser_example.py)
creates appropriate DataSource objects (for Rocksdb
[logs](https://github.com/facebook/rocksdb/blob/master/tools/advisor/advisor/db_log_parser.py),
[options](https://github.com/facebook/rocksdb/blob/master/tools/advisor/advisor/db_options_parser.py),
[statistics](https://github.com/facebook/rocksdb/blob/master/tools/advisor/advisor/db_stats_fetcher.py) etc.)
and provides them to the [Rules Engine](https://github.com/facebook/rocksdb/blob/master/tools/advisor/advisor/rule_parser.py).
The Rules uses rules from experts to parse data-sources and trigger appropriate rules.
The Advisor's output gives information about which rules were triggered,
why they were triggered and what each of them suggests. Each suggestion
provided by a triggered rule advises some action on a Rocksdb
configuration option, for example, increase CFOptions.write_buffer_size,
set bloom_bits to 2 etc.
## Usage
### Prerequisites
The tool needs the following to run:
* python3
### Running the tool
An example command to run the tool:
```shell
cd rocksdb/tools/advisor
python3 -m advisor.rule_parser_example --rules_spec=advisor/rules.ini --rocksdb_options=test/input_files/OPTIONS-000005 --log_files_path_prefix=test/input_files/LOG-0 --stats_dump_period_sec=20
```
### Command-line arguments
Most important amongst all the input that the Advisor needs, are the rules
spec and starting Rocksdb configuration. The configuration is provided as the
familiar Rocksdb Options file (refer [example](https://github.com/facebook/rocksdb/blob/master/examples/rocksdb_option_file_example.ini)).
The Rules spec is written in the INI format (more details in
[rules.ini](https://github.com/facebook/rocksdb/blob/master/tools/advisor/advisor/rules.ini)).
In brief, a Rule is made of conditions and is triggered when all its
constituent conditions are triggered. When triggered, a Rule suggests changes
(increase/decrease/set to a suggested value) to certain Rocksdb options that
aim to improve Rocksdb performance. Every Condition has a 'source' i.e.
the data source that would be checked for triggering that condition.
For example, a log Condition (with 'source=LOG') is triggered if a particular
'regex' is found in the Rocksdb LOG files. As of now the Rules Engine
supports 3 types of Conditions (and consequently data-sources):
LOG, OPTIONS, TIME_SERIES. The TIME_SERIES data can be sourced from the
Rocksdb [statistics](https://github.com/facebook/rocksdb/blob/master/include/rocksdb/statistics.h)
or [perf context](https://github.com/facebook/rocksdb/blob/master/include/rocksdb/perf_context.h).
For more information about the remaining command-line arguments, run:
```shell
cd rocksdb/tools/advisor
python3 -m advisor.rule_parser_example --help
```
### Sample output
Here, a Rocksdb log-based rule has been triggered:
```shell
Rule: stall-too-many-memtables
LogCondition: stall-too-many-memtables regex: Stopping writes because we have \d+ immutable memtables \(waiting for flush\), max_write_buffer_number is set to \d+
Suggestion: inc-bg-flush option : DBOptions.max_background_flushes action : increase suggested_values : ['2']
Suggestion: inc-write-buffer option : CFOptions.max_write_buffer_number action : increase
scope: col_fam:
{'default'}
```
## Running the tests
Tests for the code have been added to the
[test/](https://github.com/facebook/rocksdb/tree/master/tools/advisor/test)
directory. For example, to run the unit tests for db_log_parser.py:
```shell
cd rocksdb/tools/advisor
python3 -m unittest -v test.test_db_log_parser
```

@ -1,11 +1,8 @@
from advisor.bench_runner import BenchmarkRunner from advisor.bench_runner import BenchmarkRunner
from advisor.db_log_parser import DataSource, DatabaseLogs, NO_COL_FAMILY from advisor.db_log_parser import DataSource, DatabaseLogs, NO_COL_FAMILY
from advisor.db_options_parser import DatabaseOptions
from advisor.db_stats_fetcher import ( from advisor.db_stats_fetcher import (
LogStatsParser, OdsStatsFetcher, DatabasePerfContext LogStatsParser, OdsStatsFetcher, DatabasePerfContext
) )
import os
import re
import shutil import shutil
import subprocess import subprocess
import time import time
@ -30,6 +27,8 @@ class DBBenchRunner(BenchmarkRunner):
@staticmethod @staticmethod
def get_opt_args_str(misc_options_dict): def get_opt_args_str(misc_options_dict):
# given a dictionary of options and their values, return a string
# that can be appended as command-line arguments
optional_args_str = "" optional_args_str = ""
for option_name, option_value in misc_options_dict.items(): for option_name, option_value in misc_options_dict.items():
if option_value: if option_value:
@ -43,12 +42,10 @@ class DBBenchRunner(BenchmarkRunner):
self.db_bench_binary = positional_args[0] self.db_bench_binary = positional_args[0]
self.benchmark = positional_args[1] self.benchmark = positional_args[1]
self.db_bench_args = None self.db_bench_args = None
# TODO(poojam23): move to unittest with method get_available_workloads
self.supported_benchmarks = None
if len(positional_args) > 2: if len(positional_args) > 2:
# options list with each option given as "<option>=<value>" # options list with each option given as "<option>=<value>"
self.db_bench_args = positional_args[2:] self.db_bench_args = positional_args[2:]
# save ods_args if provided # save ods_args, if provided
self.ods_args = ods_args self.ods_args = ods_args
def _parse_output(self, get_perf_context=False): def _parse_output(self, get_perf_context=False):
@ -67,21 +64,29 @@ class DBBenchRunner(BenchmarkRunner):
with open(self.OUTPUT_FILE, 'r') as fp: with open(self.OUTPUT_FILE, 'r') as fp:
for line in fp: for line in fp:
if line.startswith(self.benchmark): if line.startswith(self.benchmark):
print(line) # print output of db_bench run # line from sample output:
# readwhilewriting : 16.582 micros/op 60305 ops/sec; \
# 4.2 MB/s (3433828 of 5427999 found)\n
print(line) # print output of the benchmark run
token_list = line.strip().split() token_list = line.strip().split()
for ix, token in enumerate(token_list): for ix, token in enumerate(token_list):
if token.startswith(self.THROUGHPUT): if token.startswith(self.THROUGHPUT):
# in above example, throughput = 60305 ops/sec
output[self.THROUGHPUT] = ( output[self.THROUGHPUT] = (
float(token_list[ix - 1]) float(token_list[ix - 1])
) )
break break
elif line.startswith(self.PERF_CON): elif get_perf_context and line.startswith(self.PERF_CON):
# the following lines in the output contain perf context
# statistics (refer example above)
perf_context_begins = True perf_context_begins = True
elif get_perf_context and perf_context_begins: elif get_perf_context and perf_context_begins:
# Sample perf_context output: # Sample perf_context output:
# user_key_comparison_count = 500, block_cache_hit_count =\ # user_key_comparison_count = 500, block_cache_hit_count =\
# 468, block_read_count = 580, block_read_byte = 445, ... # 468, block_read_count = 580, block_read_byte = 445, ...
token_list = line.strip().split(',') token_list = line.strip().split(',')
# token_list = ['user_key_comparison_count = 500',
# 'block_cache_hit_count = 468','block_read_count = 580'...
perf_context = { perf_context = {
tk.split('=')[0].strip(): tk.split('=')[1].strip() tk.split('=')[0].strip(): tk.split('=')[1].strip()
for tk in token_list for tk in token_list
@ -99,6 +104,8 @@ class DBBenchRunner(BenchmarkRunner):
output[self.PERF_CON] = perf_context_ts output[self.PERF_CON] = perf_context_ts
perf_context_begins = False perf_context_begins = False
elif line.startswith(self.DB_PATH): elif line.startswith(self.DB_PATH):
# line from sample output:
# DB path: [/tmp/rocksdbtest-155919/dbbench]\n
output[self.DB_PATH] = ( output[self.DB_PATH] = (
line.split('[')[1].split(']')[0] line.split('[')[1].split(']')[0]
) )
@ -111,8 +118,10 @@ class DBBenchRunner(BenchmarkRunner):
stats_freq_sec = None stats_freq_sec = None
logs_file_prefix = None logs_file_prefix = None
# fetch the options # fetch frequency at which the stats are dumped in the Rocksdb logs
dump_period = 'DBOptions.stats_dump_period_sec' dump_period = 'DBOptions.stats_dump_period_sec'
# fetch the directory, if specified, in which the Rocksdb logs are
# dumped, by default logs are dumped in same location as database
log_dir = 'DBOptions.db_log_dir' log_dir = 'DBOptions.db_log_dir'
log_options = db_options.get_options([dump_period, log_dir]) log_options = db_options.get_options([dump_period, log_dir])
if dump_period in log_options: if dump_period in log_options:
@ -153,6 +162,7 @@ class DBBenchRunner(BenchmarkRunner):
shutil.rmtree(db_path, ignore_errors=True) shutil.rmtree(db_path, ignore_errors=True)
except OSError as e: except OSError as e:
print('Error: rmdir ' + e.filename + ' ' + e.strerror) print('Error: rmdir ' + e.filename + ' ' + e.strerror)
# setup database with a million keys using the fillrandom benchmark
command = "%s --benchmarks=fillrandom --db=%s --num=1000000" % ( command = "%s --benchmarks=fillrandom --db=%s --num=1000000" % (
self.db_bench_binary, db_path self.db_bench_binary, db_path
) )
@ -164,15 +174,16 @@ class DBBenchRunner(BenchmarkRunner):
command = "%s --benchmarks=%s --statistics --perf_level=3 --db=%s" % ( command = "%s --benchmarks=%s --statistics --perf_level=3 --db=%s" % (
self.db_bench_binary, self.benchmark, db_path self.db_bench_binary, self.benchmark, db_path
) )
# fetch the command-line arguments string for providing Rocksdb options
args_str = self._get_options_command_line_args_str(curr_options) args_str = self._get_options_command_line_args_str(curr_options)
# handle the command-line args passed in the constructor # handle the command-line args passed in the constructor, these
# arguments are specific to db_bench
for cmd_line_arg in self.db_bench_args: for cmd_line_arg in self.db_bench_args:
args_str += (" --" + cmd_line_arg) args_str += (" --" + cmd_line_arg)
command += args_str command += args_str
return command return command
def _run_command(self, command): def _run_command(self, command):
# run db_bench and return the
out_file = open(self.OUTPUT_FILE, "w+") out_file = open(self.OUTPUT_FILE, "w+")
err_file = open(self.ERROR_FILE, "w+") err_file = open(self.ERROR_FILE, "w+")
print('executing... - ' + command) print('executing... - ' + command)
@ -181,18 +192,21 @@ class DBBenchRunner(BenchmarkRunner):
err_file.close() err_file.close()
def run_experiment(self, db_options, db_path): def run_experiment(self, db_options, db_path):
# type: (List[str], str) -> str # setup the Rocksdb database before running experiment
self._setup_db_before_experiment(db_options, db_path) self._setup_db_before_experiment(db_options, db_path)
# get the command to run the experiment
command = self._build_experiment_command(db_options, db_path) command = self._build_experiment_command(db_options, db_path)
# run experiment
self._run_command(command) self._run_command(command)
# parse the db_bench experiment output
parsed_output = self._parse_output(get_perf_context=True) parsed_output = self._parse_output(get_perf_context=True)
# Create the LOGS object # get the log files path prefix and frequency at which Rocksdb stats
# get the log options from the OPTIONS file # are dumped in the logs
logs_file_prefix, stats_freq_sec = self.get_log_options( logs_file_prefix, stats_freq_sec = self.get_log_options(
db_options, parsed_output[self.DB_PATH] db_options, parsed_output[self.DB_PATH]
) )
# create the Rocksbd LOGS object
db_logs = DatabaseLogs( db_logs = DatabaseLogs(
logs_file_prefix, db_options.get_column_families() logs_file_prefix, db_options.get_column_families()
) )
@ -202,6 +216,7 @@ class DBBenchRunner(BenchmarkRunner):
db_perf_context = DatabasePerfContext( db_perf_context = DatabasePerfContext(
parsed_output[self.PERF_CON], 0, False parsed_output[self.PERF_CON], 0, False
) )
# create the data-sources dictionary
data_sources = { data_sources = {
DataSource.Type.DB_OPTIONS: [db_options], DataSource.Type.DB_OPTIONS: [db_options],
DataSource.Type.LOG: [db_logs], DataSource.Type.LOG: [db_logs],
@ -214,99 +229,5 @@ class DBBenchRunner(BenchmarkRunner):
self.ods_args['entity'], self.ods_args['entity'],
self.ods_args['key_prefix'] self.ods_args['key_prefix']
)) ))
# return the experiment's data-sources and throughput
return data_sources, parsed_output[self.THROUGHPUT] return data_sources, parsed_output[self.THROUGHPUT]
# TODO: this method is for testing, shift it out to unit-tests when ready
def get_available_workloads(self):
if not self.supported_benchmarks:
self.supported_benchmarks = []
command = '%s --help' % self.db_bench_binary
self._run_command(command)
with open(self.OUTPUT_FILE, 'r') as fp:
start = False
for line in fp:
if re.search('available benchmarks', line, re.IGNORECASE):
start = True
continue
elif start:
if re.search('meta operations', line, re.IGNORECASE):
break
benchmark_info = line.strip()
if benchmark_info:
token_list = benchmark_info.split()
if len(token_list) > 2 and token_list[1] == '--':
self.supported_benchmarks.append(token_list[0])
else:
continue
self.supported_benchmarks = sorted(self.supported_benchmarks)
return self.supported_benchmarks
# TODO: remove this method, used only for testing
def main():
pos_args = [
'/home/poojamalik/workspace/rocksdb/db_bench',
'readwhilewriting',
'use_existing_db=true',
'duration=10'
]
db_bench_helper = DBBenchRunner(pos_args)
# populate benchmarks with the available ones in the db_bench tool
benchmarks = db_bench_helper.get_available_workloads()
print(benchmarks)
print()
options_file = (
'/home/poojamalik/workspace/rocksdb/tools/advisor/temp/' +
'OPTIONS_temp.tmp'
)
misc_options = ["rate_limiter_bytes_per_sec=1024000", "bloom_bits=2"]
db_options = DatabaseOptions(options_file, misc_options)
data_sources, _ = db_bench_helper.run_experiment(db_options)
print(data_sources[DataSource.Type.DB_OPTIONS][0].options_dict)
print()
print(data_sources[DataSource.Type.LOG][0].logs_path_prefix)
if os.path.isfile(data_sources[DataSource.Type.LOG][0].logs_path_prefix):
print('log file exists!')
else:
print('error: log file does not exist!')
print(data_sources[DataSource.Type.LOG][0].column_families)
print()
print(data_sources[DataSource.Type.TIME_SERIES][0].logs_file_prefix)
if (
os.path.isfile(
data_sources[DataSource.Type.TIME_SERIES][0].logs_file_prefix
)
):
print('log file exists!')
else:
print('error: log file does not exist!')
print(data_sources[DataSource.Type.TIME_SERIES][0].stats_freq_sec)
print(data_sources[DataSource.Type.TIME_SERIES][1].keys_ts)
db_options = DatabaseOptions(options_file, None)
data_sources, _ = db_bench_helper.run_experiment(db_options)
print(data_sources[DataSource.Type.DB_OPTIONS][0].options_dict)
print()
print(data_sources[DataSource.Type.LOG][0].logs_path_prefix)
if os.path.isfile(data_sources[DataSource.Type.LOG][0].logs_path_prefix):
print('log file exists!')
else:
print('error: log file does not exist!')
print(data_sources[DataSource.Type.LOG][0].column_families)
print()
print(data_sources[DataSource.Type.TIME_SERIES][0].logs_file_prefix)
if (
os.path.isfile(
data_sources[DataSource.Type.TIME_SERIES][0].logs_file_prefix
)
):
print('log file exists!')
else:
print('error: log file does not exist!')
print(data_sources[DataSource.Type.TIME_SERIES][0].stats_freq_sec)
print(data_sources[DataSource.Type.TIME_SERIES][1].keys_ts)
print(data_sources[DataSource.Type.TIME_SERIES][1].stats_freq_sec)
if __name__ == "__main__":
main()

@ -6,7 +6,6 @@
import copy import copy
from advisor.db_log_parser import DataSource, NO_COL_FAMILY from advisor.db_log_parser import DataSource, NO_COL_FAMILY
from advisor.ini_parser import IniParser from advisor.ini_parser import IniParser
from advisor.rule_parser import Condition, OptionCondition
import os import os
@ -28,10 +27,12 @@ class OptionsSpecParser(IniParser):
@staticmethod @staticmethod
def get_section_name(line): def get_section_name(line):
# example: get_section_name('[CFOptions "default"]')
token_list = line.strip()[1:-1].split('"') token_list = line.strip()[1:-1].split('"')
# token_list = ['CFOptions', 'default', '']
if len(token_list) < 3: if len(token_list) < 3:
return None return None
return token_list[1] return token_list[1] # return 'default'
@staticmethod @staticmethod
def get_section_str(section_type, section_name): def get_section_str(section_type, section_name):
@ -73,6 +74,9 @@ class DatabaseOptions(DataSource):
@staticmethod @staticmethod
def is_misc_option(option_name): def is_misc_option(option_name):
# these are miscellaneous options that are not yet supported by the
# Rocksdb options file, hence they are not prefixed with any section
# name
return '.' not in option_name return '.' not in option_name
@staticmethod @staticmethod
@ -352,88 +356,3 @@ class DatabaseOptions(DataSource):
# field # field
if col_fam_options_dict: if col_fam_options_dict:
cond.set_trigger(col_fam_options_dict) cond.set_trigger(col_fam_options_dict)
# TODO(poojam23): remove these methods once the unit tests for this class are
# in place
def main():
options_file = 'temp/OPTIONS_default.tmp'
misc_options = ["misc_opt1=10", "misc_opt2=100", "misc_opt3=1000"]
db_options = DatabaseOptions(options_file, misc_options)
print(db_options.get_column_families())
get_op = db_options.get_options([
'DBOptions.db_log_dir',
'DBOptions.is_fd_close_on_exec',
'CFOptions.memtable_prefix_bloom_size_ratio',
'TableOptions.BlockBasedTable.verify_compression',
'misc_opt1',
'misc_opt3'
])
print(get_op)
get_op['DBOptions.db_log_dir'][NO_COL_FAMILY] = 'some_random_path'
get_op['CFOptions.memtable_prefix_bloom_size_ratio']['default'] = 2.31
get_op['TableOptions.BlockBasedTable.verify_compression']['default'] = 4.4
get_op['misc_opt2'] = {}
get_op['misc_opt2'][NO_COL_FAMILY] = 2
db_options.update_options(get_op)
print('options updated in ' + db_options.generate_options_config(123))
print('misc options ' + repr(db_options.get_misc_options()))
options_file = 'temp/OPTIONS_123.tmp'
db_options = DatabaseOptions(options_file, misc_options)
# only CFOptions
cond1 = Condition('opt-cond-1')
cond1 = OptionCondition.create(cond1)
cond1.set_parameter(
'options', [
'CFOptions.level0_file_num_compaction_trigger',
'CFOptions.write_buffer_size',
'CFOptions.max_bytes_for_level_base'
]
)
cond1.set_parameter(
'evaluate',
'int(options[0])*int(options[1])-int(options[2])>=0'
)
# only DBOptions
cond2 = Condition('opt-cond-2')
cond2 = OptionCondition.create(cond2)
cond2.set_parameter(
'options', [
'DBOptions.max_file_opening_threads',
'DBOptions.table_cache_numshardbits',
'misc_opt2',
'misc_opt3'
]
)
cond2_expr = (
'(int(options[0])*int(options[2]))-' +
'((4*int(options[1])*int(options[3]))/10)==0'
)
cond2.set_parameter('evaluate', cond2_expr)
# mix of CFOptions and DBOptions
cond3 = Condition('opt-cond-3')
cond3 = OptionCondition.create(cond3)
cond3.set_parameter(
'options', [
'DBOptions.max_background_jobs', # 2
'DBOptions.write_thread_slow_yield_usec', # 3
'CFOptions.num_levels', # 7
'misc_opt1' # 10
]
)
cond3_expr = (
'(int(options[3])*int(options[2]))-' +
'(int(options[1])*int(options[0]))==64'
)
cond3.set_parameter('evaluate', cond3_expr)
db_options.check_and_trigger_conditions([cond1, cond2, cond3])
print(cond1.get_trigger()) # {'col-fam-B': ['4', '10', '10']}
print(cond2.get_trigger()) # {'DB_WIDE': ['16', '4']}
# {'col-fam-B': ['2', '3', '10'], 'col-fam-A': ['2', '3', '7']}
print(cond3.get_trigger())
if __name__ == "__main__":
main()

@ -5,7 +5,6 @@
from advisor.db_log_parser import Log from advisor.db_log_parser import Log
from advisor.db_timeseries_parser import TimeSeriesData, NO_ENTITY from advisor.db_timeseries_parser import TimeSeriesData, NO_ENTITY
from advisor.rule_parser import Condition, TimeSeriesCondition
import copy import copy
import glob import glob
import re import re
@ -123,7 +122,7 @@ class LogStatsParser(TimeSeriesData):
class DatabasePerfContext(TimeSeriesData): class DatabasePerfContext(TimeSeriesData):
# TODO(poojam23): check if any benchrunner provides PerfContext sampled at # TODO(poojam23): check if any benchrunner provides PerfContext sampled at
# regular intervals # regular intervals
def __init__(self, perf_context_ts, stats_freq_sec=0, cumulative=True): def __init__(self, perf_context_ts, stats_freq_sec, cumulative):
''' '''
perf_context_ts is expected to be in the following format: perf_context_ts is expected to be in the following format:
Dict[metric, Dict[timestamp, value]], where for Dict[metric, Dict[timestamp, value]], where for
@ -321,101 +320,3 @@ class OdsStatsFetcher(TimeSeriesData):
with open(self.OUTPUT_FILE, 'r') as fp: with open(self.OUTPUT_FILE, 'r') as fp:
url = fp.readline() url = fp.readline()
return url return url
# TODO(poojam23): remove these blocks once the unittests for LogStatsParser are
# in place
def main():
# populating the statistics
log_stats = LogStatsParser('temp/db_stats_fetcher_main_LOG.tmp', 20)
print(log_stats.type)
print(log_stats.keys_ts)
print(log_stats.logs_file_prefix)
print(log_stats.stats_freq_sec)
print(log_stats.duration_sec)
statistics = [
'rocksdb.number.rate_limiter.drains.count',
'rocksdb.number.block.decompressed.count',
'rocksdb.db.get.micros.p50',
'rocksdb.manifest.file.sync.micros.p99',
'rocksdb.db.get.micros.p99'
]
log_stats.fetch_timeseries(statistics)
print()
print(log_stats.keys_ts)
# aggregated statistics
print()
print(log_stats.fetch_aggregated_values(
NO_ENTITY, statistics, TimeSeriesData.AggregationOperator.latest
))
print(log_stats.fetch_aggregated_values(
NO_ENTITY, statistics, TimeSeriesData.AggregationOperator.oldest
))
print(log_stats.fetch_aggregated_values(
NO_ENTITY, statistics, TimeSeriesData.AggregationOperator.max
))
print(log_stats.fetch_aggregated_values(
NO_ENTITY, statistics, TimeSeriesData.AggregationOperator.min
))
print(log_stats.fetch_aggregated_values(
NO_ENTITY, statistics, TimeSeriesData.AggregationOperator.avg
))
# condition 'evaluate_expression' that evaluates to true
cond1 = Condition('cond-1')
cond1 = TimeSeriesCondition.create(cond1)
cond1.set_parameter('keys', statistics)
cond1.set_parameter('behavior', 'evaluate_expression')
cond1.set_parameter('evaluate', 'keys[3]-keys[2]>=0')
cond1.set_parameter('aggregation_op', 'avg')
# condition 'evaluate_expression' that evaluates to false
cond2 = Condition('cond-2')
cond2 = TimeSeriesCondition.create(cond2)
cond2.set_parameter('keys', statistics)
cond2.set_parameter('behavior', 'evaluate_expression')
cond2.set_parameter('evaluate', '((keys[1]-(2*keys[0]))/100)<3000')
cond2.set_parameter('aggregation_op', 'latest')
# condition 'evaluate_expression' that evaluates to true; no aggregation_op
cond3 = Condition('cond-3')
cond3 = TimeSeriesCondition.create(cond3)
cond3.set_parameter('keys', [statistics[2], statistics[3]])
cond3.set_parameter('behavior', 'evaluate_expression')
cond3.set_parameter('evaluate', '(keys[1]/keys[0])>23')
# check remaining methods
conditions = [cond1, cond2, cond3]
print()
print(log_stats.get_keys_from_conditions(conditions))
log_stats.check_and_trigger_conditions(conditions)
print()
print(cond1.get_trigger())
print(cond2.get_trigger())
print(cond3.get_trigger())
# TODO(poojam23): shift this code to the unit tests for DatabasePerfContext
def check_perf_context_code():
string = (
" user_key_comparison_count = 675903942, " +
"block_cache_hit_count = 830086, " +
"get_from_output_files_time = 85088293818, " +
"seek_on_memtable_time = 0,"
)
token_list = string.split(',')
perf_context = {
token.split('=')[0].strip(): int(token.split('=')[1].strip())
for token in token_list
if token
}
timestamp = int(time.time())
perf_ts = {}
for key in perf_context:
perf_ts[key] = {}
start_val = perf_context[key]
for ix in range(5):
perf_ts[key][timestamp+(ix*10)] = start_val + (2 * ix)
db_perf_context = DatabasePerfContext(perf_ts, 10, True)
print(db_perf_context.keys_ts)
if __name__ == '__main__':
main()
check_perf_context_code()

@ -0,0 +1,89 @@
# Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
# This source code is licensed under both the GPLv2 (found in the
# COPYING file in the root directory) and Apache 2.0 License
# (found in the LICENSE.Apache file in the root directory).
from advisor.rule_parser import RulesSpec
from advisor.db_log_parser import DatabaseLogs, DataSource
from advisor.db_options_parser import DatabaseOptions
from advisor.db_stats_fetcher import LogStatsParser, OdsStatsFetcher
import argparse
def main(args):
# initialise the RulesSpec parser
rule_spec_parser = RulesSpec(args.rules_spec)
rule_spec_parser.load_rules_from_spec()
rule_spec_parser.perform_section_checks()
# initialize the DatabaseOptions object
db_options = DatabaseOptions(args.rocksdb_options)
# Create DatabaseLogs object
db_logs = DatabaseLogs(
args.log_files_path_prefix, db_options.get_column_families()
)
# Create the Log STATS object
db_log_stats = LogStatsParser(
args.log_files_path_prefix, args.stats_dump_period_sec
)
data_sources = {
DataSource.Type.DB_OPTIONS: [db_options],
DataSource.Type.LOG: [db_logs],
DataSource.Type.TIME_SERIES: [db_log_stats]
}
if args.ods_client:
data_sources[DataSource.Type.TIME_SERIES].append(OdsStatsFetcher(
args.ods_client,
args.ods_entity,
args.ods_tstart,
args.ods_tend,
args.ods_key_prefix
))
triggered_rules = rule_spec_parser.get_triggered_rules(
data_sources, db_options.get_column_families()
)
rule_spec_parser.print_rules(triggered_rules)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Use this script to get\
suggestions for improving Rocksdb performance.')
parser.add_argument(
'--rules_spec', required=True, type=str,
help='path of the file containing the expert-specified Rules'
)
parser.add_argument(
'--rocksdb_options', required=True, type=str,
help='path of the starting Rocksdb OPTIONS file'
)
parser.add_argument(
'--log_files_path_prefix', required=True, type=str,
help='path prefix of the Rocksdb LOG files'
)
parser.add_argument(
'--stats_dump_period_sec', required=True, type=int,
help='the frequency (in seconds) at which STATISTICS are printed to ' +
'the Rocksdb LOG file'
)
# ODS arguments
parser.add_argument(
'--ods_client', type=str, help='the ODS client binary'
)
parser.add_argument(
'--ods_entity', type=str,
help='the servers for which the ODS stats need to be fetched'
)
parser.add_argument(
'--ods_key_prefix', type=str,
help='the prefix that needs to be attached to the keys of time ' +
'series to be fetched from ODS'
)
parser.add_argument(
'--ods_tstart', type=int,
help='start time of timeseries to be fetched from ODS'
)
parser.add_argument(
'--ods_tend', type=int,
help='end time of timeseries to be fetched from ODS'
)
args = parser.parse_args()
main(args)

@ -12,7 +12,8 @@
manual_wal_flush=false manual_wal_flush=false
allow_ingest_behind=false allow_ingest_behind=false
db_write_buffer_size=0 db_write_buffer_size=0
db_log_dir=
random_access_max_buffer_size=1048576
[CFOptions "default"] [CFOptions "default"]
ttl=0 ttl=0
@ -29,3 +30,20 @@
[TableOptions/BlockBasedTable "default"] [TableOptions/BlockBasedTable "default"]
block_align=false block_align=false
index_type=kBinarySearch index_type=kBinarySearch
[CFOptions "col_fam_A"]
ttl=0
max_bytes_for_level_base=268435456
max_bytes_for_level_multiplier=10.000000
level0_file_num_compaction_trigger=5
level0_stop_writes_trigger=36
write_buffer_size=1024000
min_write_buffer_number_to_merge=1
num_levels=5
compaction_filter_factory=nullptr
compaction_style=kCompactionStyleLevel
[TableOptions/BlockBasedTable "col_fam_A"]
block_align=true
block_restart_interval=16
index_type=kBinarySearch

@ -0,0 +1,3 @@
rocksdb.number.block.decompressed.count: 1530896335 88.0, 1530896361 788338.0, 1530896387 1539256.0, 1530896414 2255696.0, 1530896440 3009325.0, 1530896466 3767183.0, 1530896492 4529775.0, 1530896518 5297809.0, 1530896545 6033802.0, 1530896570 6794129.0
rocksdb.db.get.micros.p50: 1530896335 295.5, 1530896361 16.561841, 1530896387 16.20677, 1530896414 16.31508, 1530896440 16.346602, 1530896466 16.284669, 1530896492 16.16005, 1530896518 16.069096, 1530896545 16.028746, 1530896570 15.9638
rocksdb.manifest.file.sync.micros.p99: 1530896335 649.0, 1530896361 835.0, 1530896387 1435.0, 1530896414 9938.0, 1530896440 9938.0, 1530896466 9938.0, 1530896492 9938.0, 1530896518 1882.0, 1530896545 1837.0, 1530896570 1792.0

@ -32,7 +32,7 @@ regex=Stalling writes because of estimated pending compaction bytes \d+
[Condition "options-1-false"] [Condition "options-1-false"]
source=OPTIONS source=OPTIONS
options=CFOptions.level0_file_num_compaction_trigger:CFOptions.write_buffer_size:random_access_max_buffer_size options=CFOptions.level0_file_num_compaction_trigger:CFOptions.write_buffer_size:DBOptions.random_access_max_buffer_size
evaluate=int(options[0])*int(options[1])-int(options[2])<0 # should evaluate to a boolean evaluate=int(options[0])*int(options[1])-int(options[2])<0 # should evaluate to a boolean
[Suggestion "inc-bg-flush"] [Suggestion "inc-bg-flush"]

@ -0,0 +1,147 @@
# Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
# This source code is licensed under both the GPLv2 (found in the
# COPYING file in the root directory) and Apache 2.0 License
# (found in the LICENSE.Apache file in the root directory).
from advisor.db_bench_runner import DBBenchRunner
from advisor.db_log_parser import NO_COL_FAMILY, DataSource
from advisor.db_options_parser import DatabaseOptions
import os
import unittest
class TestDBBenchRunnerMethods(unittest.TestCase):
def setUp(self):
self.pos_args = [
'./../../db_bench',
'overwrite',
'use_existing_db=true',
'duration=10'
]
self.bench_runner = DBBenchRunner(self.pos_args)
this_path = os.path.abspath(os.path.dirname(__file__))
options_path = os.path.join(this_path, 'input_files/OPTIONS-000005')
self.db_options = DatabaseOptions(options_path)
def test_setup(self):
self.assertEqual(self.bench_runner.db_bench_binary, self.pos_args[0])
self.assertEqual(self.bench_runner.benchmark, self.pos_args[1])
self.assertSetEqual(
set(self.bench_runner.db_bench_args), set(self.pos_args[2:])
)
def test_get_info_log_file_name(self):
log_file_name = DBBenchRunner.get_info_log_file_name(
None, 'random_path'
)
self.assertEqual(log_file_name, 'LOG')
log_file_name = DBBenchRunner.get_info_log_file_name(
'/dev/shm/', '/tmp/rocksdbtest-155919/dbbench/'
)
self.assertEqual(log_file_name, 'tmp_rocksdbtest-155919_dbbench_LOG')
def test_get_opt_args_str(self):
misc_opt_dict = {'bloom_bits': 2, 'empty_opt': None, 'rate_limiter': 3}
optional_args_str = DBBenchRunner.get_opt_args_str(misc_opt_dict)
self.assertEqual(optional_args_str, ' --bloom_bits=2 --rate_limiter=3')
def test_get_log_options(self):
db_path = '/tmp/rocksdb-155919/dbbench'
# when db_log_dir is present in the db_options
update_dict = {
'DBOptions.db_log_dir': {NO_COL_FAMILY: '/dev/shm'},
'DBOptions.stats_dump_period_sec': {NO_COL_FAMILY: '20'}
}
self.db_options.update_options(update_dict)
log_file_prefix, stats_freq = self.bench_runner.get_log_options(
self.db_options, db_path
)
self.assertEqual(
log_file_prefix, '/dev/shm/tmp_rocksdb-155919_dbbench_LOG'
)
self.assertEqual(stats_freq, 20)
update_dict = {
'DBOptions.db_log_dir': {NO_COL_FAMILY: None},
'DBOptions.stats_dump_period_sec': {NO_COL_FAMILY: '30'}
}
self.db_options.update_options(update_dict)
log_file_prefix, stats_freq = self.bench_runner.get_log_options(
self.db_options, db_path
)
self.assertEqual(log_file_prefix, '/tmp/rocksdb-155919/dbbench/LOG')
self.assertEqual(stats_freq, 30)
def test_build_experiment_command(self):
# add some misc_options to db_options
update_dict = {
'bloom_bits': {NO_COL_FAMILY: 2},
'rate_limiter_bytes_per_sec': {NO_COL_FAMILY: 128000000}
}
self.db_options.update_options(update_dict)
db_path = '/dev/shm'
experiment_command = self.bench_runner._build_experiment_command(
self.db_options, db_path
)
opt_args_str = DBBenchRunner.get_opt_args_str(
self.db_options.get_misc_options()
)
opt_args_str += (
' --options_file=' +
self.db_options.generate_options_config('12345')
)
for arg in self.pos_args[2:]:
opt_args_str += (' --' + arg)
expected_command = (
self.pos_args[0] + ' --benchmarks=' + self.pos_args[1] +
' --statistics --perf_level=3 --db=' + db_path + opt_args_str
)
self.assertEqual(experiment_command, expected_command)
class TestDBBenchRunner(unittest.TestCase):
def setUp(self):
# Note: the db_bench binary should be present in the rocksdb/ directory
self.pos_args = [
'./../../db_bench',
'overwrite',
'use_existing_db=true',
'duration=20'
]
self.bench_runner = DBBenchRunner(self.pos_args)
this_path = os.path.abspath(os.path.dirname(__file__))
options_path = os.path.join(this_path, 'input_files/OPTIONS-000005')
self.db_options = DatabaseOptions(options_path)
def test_experiment_output(self):
update_dict = {'bloom_bits': {NO_COL_FAMILY: 2}}
self.db_options.update_options(update_dict)
db_path = '/dev/shm'
data_sources, throughput = self.bench_runner.run_experiment(
self.db_options, db_path
)
self.assertEqual(
data_sources[DataSource.Type.DB_OPTIONS][0].type,
DataSource.Type.DB_OPTIONS
)
self.assertEqual(
data_sources[DataSource.Type.LOG][0].type,
DataSource.Type.LOG
)
self.assertEqual(len(data_sources[DataSource.Type.TIME_SERIES]), 2)
self.assertEqual(
data_sources[DataSource.Type.TIME_SERIES][0].type,
DataSource.Type.TIME_SERIES
)
self.assertEqual(
data_sources[DataSource.Type.TIME_SERIES][1].type,
DataSource.Type.TIME_SERIES
)
self.assertEqual(
data_sources[DataSource.Type.TIME_SERIES][1].stats_freq_sec, 0
)
if __name__ == '__main__':
unittest.main()

@ -1,3 +1,8 @@
# Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
# This source code is licensed under both the GPLv2 (found in the
# COPYING file in the root directory) and Apache 2.0 License
# (found in the LICENSE.Apache file in the root directory).
from advisor.db_log_parser import DatabaseLogs, Log, NO_COL_FAMILY from advisor.db_log_parser import DatabaseLogs, Log, NO_COL_FAMILY
from advisor.rule_parser import Condition, LogCondition from advisor.rule_parser import Condition, LogCondition
import os import os

@ -0,0 +1,216 @@
# Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
# This source code is licensed under both the GPLv2 (found in the
# COPYING file in the root directory) and Apache 2.0 License
# (found in the LICENSE.Apache file in the root directory).
from advisor.db_log_parser import NO_COL_FAMILY
from advisor.db_options_parser import DatabaseOptions
from advisor.rule_parser import Condition, OptionCondition
import os
import unittest
class TestDatabaseOptions(unittest.TestCase):
def setUp(self):
self.this_path = os.path.abspath(os.path.dirname(__file__))
self.og_options = os.path.join(
self.this_path, 'input_files/OPTIONS-000005'
)
misc_options = [
'bloom_bits = 4', 'rate_limiter_bytes_per_sec = 1024000'
]
# create the options object
self.db_options = DatabaseOptions(self.og_options, misc_options)
# perform clean-up before running tests
self.generated_options = os.path.join(
self.this_path, '../temp/OPTIONS_testing.tmp'
)
if os.path.isfile(self.generated_options):
os.remove(self.generated_options)
def test_get_options_diff(self):
old_opt = {
'DBOptions.stats_dump_freq_sec': {NO_COL_FAMILY: '20'},
'CFOptions.write_buffer_size': {
'default': '1024000',
'col_fam_A': '128000',
'col_fam_B': '128000000'
},
'DBOptions.use_fsync': {NO_COL_FAMILY: 'true'},
'DBOptions.max_log_file_size': {NO_COL_FAMILY: '128000000'}
}
new_opt = {
'bloom_bits': {NO_COL_FAMILY: '4'},
'CFOptions.write_buffer_size': {
'default': '128000000',
'col_fam_A': '128000',
'col_fam_C': '128000000'
},
'DBOptions.use_fsync': {NO_COL_FAMILY: 'true'},
'DBOptions.max_log_file_size': {NO_COL_FAMILY: '0'}
}
diff = DatabaseOptions.get_options_diff(old_opt, new_opt)
expected_diff = {
'DBOptions.stats_dump_freq_sec': {NO_COL_FAMILY: ('20', None)},
'bloom_bits': {NO_COL_FAMILY: (None, '4')},
'CFOptions.write_buffer_size': {
'default': ('1024000', '128000000'),
'col_fam_B': ('128000000', None),
'col_fam_C': (None, '128000000')
},
'DBOptions.max_log_file_size': {NO_COL_FAMILY: ('128000000', '0')}
}
self.assertDictEqual(diff, expected_diff)
def test_is_misc_option(self):
self.assertTrue(DatabaseOptions.is_misc_option('bloom_bits'))
self.assertFalse(
DatabaseOptions.is_misc_option('DBOptions.stats_dump_freq_sec')
)
def test_set_up(self):
options = self.db_options.get_all_options()
self.assertEqual(22, len(options.keys()))
expected_misc_options = {
'bloom_bits': '4', 'rate_limiter_bytes_per_sec': '1024000'
}
self.assertDictEqual(
expected_misc_options, self.db_options.get_misc_options()
)
self.assertListEqual(
['default', 'col_fam_A'], self.db_options.get_column_families()
)
def test_get_options(self):
opt_to_get = [
'DBOptions.manual_wal_flush', 'DBOptions.db_write_buffer_size',
'bloom_bits', 'CFOptions.compaction_filter_factory',
'CFOptions.num_levels', 'rate_limiter_bytes_per_sec',
'TableOptions.BlockBasedTable.block_align', 'random_option'
]
options = self.db_options.get_options(opt_to_get)
expected_options = {
'DBOptions.manual_wal_flush': {NO_COL_FAMILY: 'false'},
'DBOptions.db_write_buffer_size': {NO_COL_FAMILY: '0'},
'bloom_bits': {NO_COL_FAMILY: '4'},
'CFOptions.compaction_filter_factory': {
'default': 'nullptr', 'col_fam_A': 'nullptr'
},
'CFOptions.num_levels': {'default': '7', 'col_fam_A': '5'},
'rate_limiter_bytes_per_sec': {NO_COL_FAMILY: '1024000'},
'TableOptions.BlockBasedTable.block_align': {
'default': 'false', 'col_fam_A': 'true'
}
}
self.assertDictEqual(expected_options, options)
def test_update_options(self):
# add new, update old, set old
# before updating
expected_old_opts = {
'DBOptions.db_log_dir': {NO_COL_FAMILY: None},
'DBOptions.manual_wal_flush': {NO_COL_FAMILY: 'false'},
'bloom_bits': {NO_COL_FAMILY: '4'},
'CFOptions.num_levels': {'default': '7', 'col_fam_A': '5'},
'TableOptions.BlockBasedTable.block_restart_interval': {
'col_fam_A': '16'
}
}
get_opts = list(expected_old_opts.keys())
options = self.db_options.get_options(get_opts)
self.assertEqual(expected_old_opts, options)
# after updating options
update_opts = {
'DBOptions.db_log_dir': {NO_COL_FAMILY: '/dev/shm'},
'DBOptions.manual_wal_flush': {NO_COL_FAMILY: 'true'},
'bloom_bits': {NO_COL_FAMILY: '2'},
'CFOptions.num_levels': {'col_fam_A': '7'},
'TableOptions.BlockBasedTable.block_restart_interval': {
'default': '32'
},
'random_misc_option': {NO_COL_FAMILY: 'something'}
}
self.db_options.update_options(update_opts)
update_opts['CFOptions.num_levels']['default'] = '7'
update_opts['TableOptions.BlockBasedTable.block_restart_interval'] = {
'default': '32', 'col_fam_A': '16'
}
get_opts.append('random_misc_option')
options = self.db_options.get_options(get_opts)
self.assertDictEqual(update_opts, options)
expected_misc_options = {
'bloom_bits': '2',
'rate_limiter_bytes_per_sec': '1024000',
'random_misc_option': 'something'
}
self.assertDictEqual(
expected_misc_options, self.db_options.get_misc_options()
)
def test_generate_options_config(self):
# make sure file does not exist from before
self.assertFalse(os.path.isfile(self.generated_options))
self.db_options.generate_options_config('testing')
self.assertTrue(os.path.isfile(self.generated_options))
def test_check_and_trigger_conditions(self):
# options only from CFOptions
# setup the OptionCondition objects to check and trigger
update_dict = {
'CFOptions.level0_file_num_compaction_trigger': {'col_fam_A': '4'},
'CFOptions.max_bytes_for_level_base': {'col_fam_A': '10'}
}
self.db_options.update_options(update_dict)
cond1 = Condition('opt-cond-1')
cond1 = OptionCondition.create(cond1)
cond1.set_parameter(
'options', [
'CFOptions.level0_file_num_compaction_trigger',
'TableOptions.BlockBasedTable.block_restart_interval',
'CFOptions.max_bytes_for_level_base'
]
)
cond1.set_parameter(
'evaluate',
'int(options[0])*int(options[1])-int(options[2])>=0'
)
# only DBOptions
cond2 = Condition('opt-cond-2')
cond2 = OptionCondition.create(cond2)
cond2.set_parameter(
'options', [
'DBOptions.db_write_buffer_size',
'bloom_bits',
'rate_limiter_bytes_per_sec'
]
)
cond2.set_parameter(
'evaluate',
'(int(options[2]) * int(options[1]) * int(options[0]))==0'
)
# mix of CFOptions and DBOptions
cond3 = Condition('opt-cond-3')
cond3 = OptionCondition.create(cond3)
cond3.set_parameter(
'options', [
'DBOptions.db_write_buffer_size', # 0
'CFOptions.num_levels', # 5, 7
'bloom_bits' # 4
]
)
cond3.set_parameter(
'evaluate', 'int(options[2])*int(options[0])+int(options[1])>6'
)
self.db_options.check_and_trigger_conditions([cond1, cond2, cond3])
cond1_trigger = {'col_fam_A': ['4', '16', '10']}
self.assertDictEqual(cond1_trigger, cond1.get_trigger())
cond2_trigger = {NO_COL_FAMILY: ['0', '4', '1024000']}
self.assertDictEqual(cond2_trigger, cond2.get_trigger())
cond3_trigger = {'default': ['0', '7', '4']}
self.assertDictEqual(cond3_trigger, cond3.get_trigger())
if __name__ == '__main__':
unittest.main()

@ -0,0 +1,126 @@
# Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
# This source code is licensed under both the GPLv2 (found in the
# COPYING file in the root directory) and Apache 2.0 License
# (found in the LICENSE.Apache file in the root directory).
from advisor.db_stats_fetcher import LogStatsParser, DatabasePerfContext
from advisor.db_timeseries_parser import NO_ENTITY
from advisor.rule_parser import Condition, TimeSeriesCondition
import os
import time
import unittest
from unittest.mock import MagicMock
class TestLogStatsParser(unittest.TestCase):
def setUp(self):
this_path = os.path.abspath(os.path.dirname(__file__))
stats_file = os.path.join(
this_path, 'input_files/log_stats_parser_keys_ts'
)
# populate the keys_ts dictionary of LogStatsParser
self.stats_dict = {NO_ENTITY: {}}
with open(stats_file, 'r') as fp:
for line in fp:
stat_name = line.split(':')[0].strip()
self.stats_dict[NO_ENTITY][stat_name] = {}
token_list = line.split(':')[1].strip().split(',')
for token in token_list:
timestamp = int(token.split()[0])
value = float(token.split()[1])
self.stats_dict[NO_ENTITY][stat_name][timestamp] = value
self.log_stats_parser = LogStatsParser('dummy_log_file', 20)
self.log_stats_parser.keys_ts = self.stats_dict
def test_check_and_trigger_conditions_bursty(self):
# mock fetch_timeseries() because 'keys_ts' has been pre-populated
self.log_stats_parser.fetch_timeseries = MagicMock()
# condition: bursty
cond1 = Condition('cond-1')
cond1 = TimeSeriesCondition.create(cond1)
cond1.set_parameter('keys', 'rocksdb.db.get.micros.p50')
cond1.set_parameter('behavior', 'bursty')
cond1.set_parameter('window_sec', 40)
cond1.set_parameter('rate_threshold', 0)
self.log_stats_parser.check_and_trigger_conditions([cond1])
expected_cond_trigger = {
NO_ENTITY: {1530896440: 0.9767546362322214}
}
self.assertDictEqual(expected_cond_trigger, cond1.get_trigger())
# ensure that fetch_timeseries() was called once
self.log_stats_parser.fetch_timeseries.assert_called_once()
def test_check_and_trigger_conditions_eval_agg(self):
# mock fetch_timeseries() because 'keys_ts' has been pre-populated
self.log_stats_parser.fetch_timeseries = MagicMock()
# condition: evaluate_expression
cond1 = Condition('cond-1')
cond1 = TimeSeriesCondition.create(cond1)
cond1.set_parameter('keys', 'rocksdb.db.get.micros.p50')
cond1.set_parameter('behavior', 'evaluate_expression')
keys = [
'rocksdb.manifest.file.sync.micros.p99',
'rocksdb.db.get.micros.p50'
]
cond1.set_parameter('keys', keys)
cond1.set_parameter('aggregation_op', 'latest')
# condition evaluates to FALSE
cond1.set_parameter('evaluate', 'keys[0]-(keys[1]*100)>200')
self.log_stats_parser.check_and_trigger_conditions([cond1])
expected_cond_trigger = {NO_ENTITY: [1792.0, 15.9638]}
self.assertIsNone(cond1.get_trigger())
# condition evaluates to TRUE
cond1.set_parameter('evaluate', 'keys[0]-(keys[1]*100)<200')
self.log_stats_parser.check_and_trigger_conditions([cond1])
expected_cond_trigger = {NO_ENTITY: [1792.0, 15.9638]}
self.assertDictEqual(expected_cond_trigger, cond1.get_trigger())
# ensure that fetch_timeseries() was called
self.log_stats_parser.fetch_timeseries.assert_called()
def test_check_and_trigger_conditions_eval(self):
# mock fetch_timeseries() because 'keys_ts' has been pre-populated
self.log_stats_parser.fetch_timeseries = MagicMock()
# condition: evaluate_expression
cond1 = Condition('cond-1')
cond1 = TimeSeriesCondition.create(cond1)
cond1.set_parameter('keys', 'rocksdb.db.get.micros.p50')
cond1.set_parameter('behavior', 'evaluate_expression')
keys = [
'rocksdb.manifest.file.sync.micros.p99',
'rocksdb.db.get.micros.p50'
]
cond1.set_parameter('keys', keys)
cond1.set_parameter('evaluate', 'keys[0]-(keys[1]*100)>500')
self.log_stats_parser.check_and_trigger_conditions([cond1])
expected_trigger = {NO_ENTITY: {
1530896414: [9938.0, 16.31508],
1530896440: [9938.0, 16.346602],
1530896466: [9938.0, 16.284669],
1530896492: [9938.0, 16.16005]
}}
self.assertDictEqual(expected_trigger, cond1.get_trigger())
self.log_stats_parser.fetch_timeseries.assert_called_once()
class TestDatabasePerfContext(unittest.TestCase):
def test_unaccumulate_metrics(self):
perf_dict = {
"user_key_comparison_count": 675903942,
"block_cache_hit_count": 830086,
}
timestamp = int(time.time())
perf_ts = {}
for key in perf_dict:
perf_ts[key] = {}
start_val = perf_dict[key]
for ix in range(5):
perf_ts[key][timestamp+(ix*10)] = start_val + (2 * ix * ix)
db_perf_context = DatabasePerfContext(perf_ts, 10, True)
timestamps = [timestamp+(ix*10) for ix in range(1, 5, 1)]
values = [val for val in range(2, 15, 4)]
inner_dict = {timestamps[ix]: values[ix] for ix in range(4)}
expected_keys_ts = {NO_ENTITY: {
'user_key_comparison_count': inner_dict,
'block_cache_hit_count': inner_dict
}}
self.assertDictEqual(expected_keys_ts, db_perf_context.keys_ts)

@ -52,7 +52,10 @@ class TestAllRulesTriggered(unittest.TestCase):
db_options_parser = DatabaseOptions(options_path) db_options_parser = DatabaseOptions(options_path)
self.column_families = db_options_parser.get_column_families() self.column_families = db_options_parser.get_column_families()
db_logs_parser = DatabaseLogs(log_path, self.column_families) db_logs_parser = DatabaseLogs(log_path, self.column_families)
self.data_sources = [db_options_parser, db_logs_parser] self.data_sources = {
DataSource.Type.DB_OPTIONS: [db_options_parser],
DataSource.Type.LOG: [db_logs_parser]
}
def test_triggered_conditions(self): def test_triggered_conditions(self):
conditions_dict = self.db_rules.get_conditions_dict() conditions_dict = self.db_rules.get_conditions_dict()
@ -106,7 +109,10 @@ class TestConditionsConjunctions(unittest.TestCase):
db_options_parser = DatabaseOptions(options_path) db_options_parser = DatabaseOptions(options_path)
self.column_families = db_options_parser.get_column_families() self.column_families = db_options_parser.get_column_families()
db_logs_parser = DatabaseLogs(log_path, self.column_families) db_logs_parser = DatabaseLogs(log_path, self.column_families)
self.data_sources = [db_options_parser, db_logs_parser] self.data_sources = {
DataSource.Type.DB_OPTIONS: [db_options_parser],
DataSource.Type.LOG: [db_logs_parser]
}
def test_condition_conjunctions(self): def test_condition_conjunctions(self):
conditions_dict = self.db_rules.get_conditions_dict() conditions_dict = self.db_rules.get_conditions_dict()

Loading…
Cancel
Save