diff options
Diffstat (limited to 'src/tools/rbd/action/Perf.cc')
-rw-r--r-- | src/tools/rbd/action/Perf.cc | 717 |
1 files changed, 717 insertions, 0 deletions
diff --git a/src/tools/rbd/action/Perf.cc b/src/tools/rbd/action/Perf.cc new file mode 100644 index 000000000..b39beac91 --- /dev/null +++ b/src/tools/rbd/action/Perf.cc @@ -0,0 +1,717 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab + +#include "tools/rbd/ArgumentTypes.h" +#include "tools/rbd/Shell.h" +#include "tools/rbd/Utils.h" +#include "include/stringify.h" +#include "common/ceph_context.h" +#include "common/ceph_json.h" +#include "common/errno.h" +#include "common/Formatter.h" +#include "common/TextTable.h" +#include "global/global_context.h" +#ifdef HAVE_CURSES +#include <ncurses.h> +#endif +#include <stdio.h> +#include <unistd.h> +#include <sys/time.h> +#include <sys/types.h> +#include <iostream> +#include <vector> +#include <boost/algorithm/string.hpp> +#include <boost/assign.hpp> +#include <boost/bimap.hpp> +#include <boost/program_options.hpp> +#include "json_spirit/json_spirit.h" + +namespace rbd { +namespace action { +namespace perf { + +namespace at = argument_types; +namespace po = boost::program_options; + +namespace { + +enum class StatDescriptor { + WRITE_OPS = 0, + READ_OPS, + WRITE_BYTES, + READ_BYTES, + WRITE_LATENCY, + READ_LATENCY +}; + +typedef boost::bimap<StatDescriptor, std::string> StatDescriptors; + +static const StatDescriptors STAT_DESCRIPTORS = + boost::assign::list_of<StatDescriptors::relation> + (StatDescriptor::WRITE_OPS, "write_ops") + (StatDescriptor::READ_OPS, "read_ops") + (StatDescriptor::WRITE_BYTES, "write_bytes") + (StatDescriptor::READ_BYTES, "read_bytes") + (StatDescriptor::WRITE_LATENCY, "write_latency") + (StatDescriptor::READ_LATENCY, "read_latency"); + +std::ostream& operator<<(std::ostream& os, const StatDescriptor& val) { + auto it = STAT_DESCRIPTORS.left.find(val); + if (it == STAT_DESCRIPTORS.left.end()) { + os << "unknown (" << static_cast<int>(val) << ")"; + } else { + os << it->second; + } + return os; +} + +void validate(boost::any& v, const std::vector<std::string>& values, + StatDescriptor *target_type, int) { + po::validators::check_first_occurrence(v); + std::string s = po::validators::get_single_string(values); + boost::replace_all(s, "_", " "); + boost::replace_all(s, "-", "_"); + + auto it = STAT_DESCRIPTORS.right.find(s); + if (it == STAT_DESCRIPTORS.right.end()) { + throw po::validation_error(po::validation_error::invalid_option_value); + } + v = boost::any(it->second); +} + +struct ImageStat { + ImageStat(const std::string& pool_name, const std::string& pool_namespace, + const std::string& image_name) + : pool_name(pool_name), pool_namespace(pool_namespace), + image_name(image_name) { + stats.resize(STAT_DESCRIPTORS.size()); + } + + std::string pool_name; + std::string pool_namespace; + std::string image_name; + std::vector<double> stats; +}; + +typedef std::vector<ImageStat> ImageStats; + +typedef std::pair<std::string, std::string> SpecPair; + +std::string format_pool_spec(const std::string& pool, + const std::string& pool_namespace) { + std::string pool_spec{pool}; + if (!pool_namespace.empty()) { + pool_spec += "/" + pool_namespace; + } + return pool_spec; +} + +int query_iostats(librados::Rados& rados, const std::string& pool_spec, + StatDescriptor sort_by, ImageStats* image_stats, + std::ostream& err_os) { + auto sort_by_str = STAT_DESCRIPTORS.left.find(sort_by)->second; + + std::string cmd = R"( + { + "prefix": "rbd perf image stats", + "pool_spec": ")" + pool_spec + R"(", + "sort_by": ")" + sort_by_str + R"(", + "format": "json" + }")"; + + bufferlist in_bl; + bufferlist out_bl; + std::string outs; + int r = rados.mgr_command(cmd, in_bl, &out_bl, &outs); + if (r == -EOPNOTSUPP) { + err_os << "rbd: 'rbd_support' mgr module is not enabled." + << std::endl << std::endl + << "Use 'ceph mgr module enable rbd_support' to enable." + << std::endl; + return r; + } else if (r < 0) { + err_os << "rbd: mgr command failed: " << cpp_strerror(r); + if (!outs.empty()) { + err_os << ": " << outs; + } + err_os << std::endl; + return r; + } + + json_spirit::mValue json_root; + if (!json_spirit::read(out_bl.to_str(), json_root)) { + err_os << "rbd: error parsing perf stats" << std::endl; + return -EINVAL; + } + + image_stats->clear(); + try { + auto& root = json_root.get_obj(); + + // map JSON stat descriptor order to our internal order + std::map<uint32_t, uint32_t> json_to_internal_stats; + auto& json_stat_descriptors = root["stat_descriptors"].get_array(); + for (size_t idx = 0; idx < json_stat_descriptors.size(); ++idx) { + auto it = STAT_DESCRIPTORS.right.find( + json_stat_descriptors[idx].get_str()); + if (it == STAT_DESCRIPTORS.right.end()) { + continue; + } + json_to_internal_stats[idx] = static_cast<uint32_t>(it->second); + } + + // cache a mapping from pool descriptors back to pool-specs + std::map<std::string, SpecPair> json_to_internal_pools; + auto& pool_descriptors = root["pool_descriptors"].get_obj(); + for (auto& pool : pool_descriptors) { + auto& pool_spec = pool.second.get_str(); + auto pos = pool_spec.rfind("/"); + + SpecPair pair{pool_spec.substr(0, pos), ""}; + if (pos != std::string::npos) { + pair.second = pool_spec.substr(pos + 1); + } + + json_to_internal_pools[pool.first] = pair; + } + + auto& stats = root["stats"].get_array(); + for (auto& stat : stats) { + auto& stat_obj = stat.get_obj(); + if (!stat_obj.empty()) { + auto& image_spec = stat_obj.begin()->first; + + auto pos = image_spec.find("/"); + SpecPair pair{image_spec.substr(0, pos), ""}; + if (pos != std::string::npos) { + pair.second = image_spec.substr(pos + 1); + } + + const auto pool_it = json_to_internal_pools.find(pair.first); + if (pool_it == json_to_internal_pools.end()) { + continue; + } + + image_stats->emplace_back( + pool_it->second.first, pool_it->second.second, pair.second); + + auto& image_stat = image_stats->back(); + auto& data = stat_obj.begin()->second.get_array(); + for (auto& indexes : json_to_internal_stats) { + image_stat.stats[indexes.second] = data[indexes.first].get_real(); + } + } + } + } catch (std::runtime_error &e) { + err_os << "rbd: error parsing perf stats: " << e.what() << std::endl; + return -EINVAL; + } + + return 0; +} + +void format_stat(StatDescriptor stat_descriptor, double stat, + std::ostream& os) { + switch (stat_descriptor) { + case StatDescriptor::WRITE_OPS: + case StatDescriptor::READ_OPS: + os << si_u_t(stat) << "/s"; + break; + case StatDescriptor::WRITE_BYTES: + case StatDescriptor::READ_BYTES: + os << byte_u_t(stat) << "/s"; + break; + case StatDescriptor::WRITE_LATENCY: + case StatDescriptor::READ_LATENCY: + os << std::fixed << std::setprecision(2); + if (stat >= 1000000000) { + os << (stat / 1000000000) << " s"; + } else if (stat >= 1000000) { + os << (stat / 1000000) << " ms"; + } else if (stat >= 1000) { + os << (stat / 1000) << " us"; + } else { + os << stat << " ns"; + } + break; + default: + ceph_assert(false); + break; + } +} + +} // anonymous namespace + +namespace iostat { + +struct Iterations {}; + +void validate(boost::any& v, const std::vector<std::string>& values, + Iterations *target_type, int) { + po::validators::check_first_occurrence(v); + auto& s = po::validators::get_single_string(values); + + try { + auto iterations = boost::lexical_cast<uint32_t>(s); + if (iterations > 0) { + v = boost::any(iterations); + return; + } + } catch (const boost::bad_lexical_cast &) { + } + throw po::validation_error(po::validation_error::invalid_option_value); +} + +void format(const ImageStats& image_stats, Formatter* f, bool global_search) { + TextTable tbl; + if (f) { + f->open_array_section("images"); + } else { + tbl.define_column("NAME", TextTable::LEFT, TextTable::LEFT); + for (auto& stat : STAT_DESCRIPTORS.left) { + std::string title; + switch (stat.first) { + case StatDescriptor::WRITE_OPS: + title = "WR "; + break; + case StatDescriptor::READ_OPS: + title = "RD "; + break; + case StatDescriptor::WRITE_BYTES: + title = "WR_BYTES "; + break; + case StatDescriptor::READ_BYTES: + title = "RD_BYTES "; + break; + case StatDescriptor::WRITE_LATENCY: + title = "WR_LAT "; + break; + case StatDescriptor::READ_LATENCY: + title = "RD_LAT "; + break; + default: + ceph_assert(false); + break; + } + tbl.define_column(title, TextTable::RIGHT, TextTable::RIGHT); + } + } + + for (auto& image_stat : image_stats) { + if (f) { + f->open_object_section("image"); + f->dump_string("pool", image_stat.pool_name); + f->dump_string("pool_namespace", image_stat.pool_namespace); + f->dump_string("image", image_stat.image_name); + for (auto& pair : STAT_DESCRIPTORS.left) { + f->dump_float(pair.second.c_str(), + image_stat.stats[static_cast<size_t>(pair.first)]); + } + f->close_section(); + } else { + std::string name; + if (global_search) { + name += image_stat.pool_name + "/"; + if (!image_stat.pool_namespace.empty()) { + name += image_stat.pool_namespace + "/"; + } + } + name += image_stat.image_name; + + tbl << name; + for (auto& pair : STAT_DESCRIPTORS.left) { + std::stringstream str; + format_stat(pair.first, + image_stat.stats[static_cast<size_t>(pair.first)], str); + str << ' '; + tbl << str.str(); + } + tbl << TextTable::endrow; + } + } + + if (f) { + f->close_section(); + f->flush(std::cout); + } else { + std::cout << tbl << std::endl; + } +} + +} // namespace iostat + +#ifdef HAVE_CURSES +namespace iotop { + +class MainWindow { +public: + MainWindow(librados::Rados& rados, const std::string& pool_spec) + : m_rados(rados), m_pool_spec(pool_spec) { + initscr(); + curs_set(0); + cbreak(); + noecho(); + keypad(stdscr, TRUE); + nodelay(stdscr, TRUE); + + init_columns(); + } + + int run() { + redraw(); + + int r = 0; + std::stringstream err_str; + while (true) { + r = query_iostats(m_rados, m_pool_spec, m_sort_by, &m_image_stats, + err_str); + if (r < 0) { + break; + return r; + } + + redraw(); + wait_for_key_or_delay(); + + int ch = getch(); + if (ch == 'q' || ch == 'Q') { + break; + } else if (ch == '<' || ch == KEY_LEFT) { + auto it = STAT_DESCRIPTORS.left.find(m_sort_by); + if (it != STAT_DESCRIPTORS.left.begin()) { + m_sort_by = (--it)->first; + } + } else if (ch == '>' || ch == KEY_RIGHT) { + auto it = STAT_DESCRIPTORS.left.find(m_sort_by); + if (it != STAT_DESCRIPTORS.left.end() && + ++it != STAT_DESCRIPTORS.left.end()) { + m_sort_by = it->first; + } + } + } + + endwin(); + + if (r < 0) { + std::cerr << err_str.str() << std::endl; + } + return r; + } + +private: + static const size_t STAT_COLUMN_WIDTH = 12; + + librados::Rados& m_rados; + std::string m_pool_spec; + + ImageStats m_image_stats; + StatDescriptor m_sort_by = StatDescriptor::WRITE_OPS; + + bool m_pending_win_opened = false; + WINDOW* m_pending_win = nullptr; + + int m_height = 1; + int m_width = 1; + + std::map<StatDescriptor, std::string> m_columns; + + void init_columns() { + m_columns.clear(); + for (auto& pair : STAT_DESCRIPTORS.left) { + std::string title; + switch (pair.first) { + case StatDescriptor::WRITE_OPS: + title = "WRITES OPS"; + break; + case StatDescriptor::READ_OPS: + title = "READS OPS"; + break; + case StatDescriptor::WRITE_BYTES: + title = "WRITE BYTES"; + break; + case StatDescriptor::READ_BYTES: + title = "READ BYTES"; + break; + case StatDescriptor::WRITE_LATENCY: + title = "WRITE LAT"; + break; + case StatDescriptor::READ_LATENCY: + title = "READ LAT"; + break; + default: + ceph_assert(false); + break; + } + m_columns[pair.first] = (title); + } + } + + void redraw() { + getmaxyx(stdscr, m_height, m_width); + + redraw_main_window(); + redraw_pending_window(); + + doupdate(); + } + + void redraw_main_window() { + werase(stdscr); + mvhline(0, 0, ' ' | A_REVERSE, m_width); + + // print header for all metrics + int remaining_cols = m_width; + std::stringstream str; + for (auto& pair : m_columns) { + int attr = A_REVERSE; + std::string title; + if (pair.first == m_sort_by) { + title += '>'; + attr |= A_BOLD; + } else { + title += ' '; + } + title += pair.second; + + str.str(""); + str << std::right << std::setfill(' ') + << std::setw(STAT_COLUMN_WIDTH) + << title << ' '; + + attrset(attr); + addstr(str.str().c_str()); + remaining_cols -= title.size(); + } + + attrset(A_REVERSE); + addstr("IMAGE"); + attrset(A_NORMAL); + + // print each image (one per line) + int row = 1; + int remaining_lines = m_height - 1; + for (auto& image_stat : m_image_stats) { + if (remaining_lines <= 0) { + break; + } + --remaining_lines; + + move(row++, 0); + for (auto& pair : m_columns) { + str.str(""); + format_stat(pair.first, + image_stat.stats[static_cast<size_t>(pair.first)], str); + auto value = str.str().substr(0, STAT_COLUMN_WIDTH); + + str.str(""); + str << std::right << std::setfill(' ') + << std::setw(STAT_COLUMN_WIDTH) + << value << ' '; + addstr(str.str().c_str()); + } + + std::string image; + if (m_pool_spec.empty()) { + image = format_pool_spec(image_stat.pool_name, + image_stat.pool_namespace) + "/"; + } + image += image_stat.image_name; + addstr(image.substr(0, remaining_cols).c_str()); + } + + wnoutrefresh(stdscr); + } + + void redraw_pending_window() { + // draw a "please by patient" window while waiting + const char* msg = "Waiting for initial stats"; + int height = 5; + int width = strlen(msg) + 4;; + int starty = (m_height - height) / 2; + int startx = (m_width - width) / 2; + + if (m_image_stats.empty() && !m_pending_win_opened) { + m_pending_win_opened = true; + m_pending_win = newwin(height, width, starty, startx); + } + + if (m_pending_win != nullptr) { + if (m_image_stats.empty()) { + box(m_pending_win, 0 , 0); + mvwaddstr(m_pending_win, 2, 2, msg); + wnoutrefresh(m_pending_win); + } else { + delwin(m_pending_win); + m_pending_win = nullptr; + } + } + } + + void wait_for_key_or_delay() { + fd_set fds; + FD_ZERO(&fds); + FD_SET(STDIN_FILENO, &fds); + + // no point to refreshing faster than the stats period + struct timeval tval; + tval.tv_sec = std::min<uint32_t>( + 10, g_conf().get_val<int64_t>("mgr_stats_period")); + tval.tv_usec = 0; + + select(STDIN_FILENO + 1, &fds, NULL, NULL, &tval); + } +}; + +} // namespace iotop +#endif // HAVE_CURSES + + +void get_arguments_iostat(po::options_description *positional, + po::options_description *options) { + at::add_pool_options(positional, options, true); + options->add_options() + ("iterations", po::value<iostat::Iterations>(), + "iterations of metric collection [> 0]") + ("sort-by", po::value<StatDescriptor>()->default_value(StatDescriptor::WRITE_OPS), + "sort-by IO metric " + "(write-ops, read-ops, write-bytes, read-bytes, write-latency, read-latency) " + "[default: write-ops]"); + at::add_format_options(options); +} + +int execute_iostat(const po::variables_map &vm, + const std::vector<std::string> &ceph_global_init_args) { + std::string pool; + std::string pool_namespace; + size_t arg_index = 0; + int r = utils::get_pool_and_namespace_names(vm, false, &pool, + &pool_namespace, &arg_index); + if (r < 0) { + return r; + } + + uint32_t iterations = 0; + if (vm.count("iterations")) { + iterations = vm["iterations"].as<uint32_t>(); + } + auto sort_by = vm["sort-by"].as<StatDescriptor>(); + + at::Format::Formatter formatter; + r = utils::get_formatter(vm, &formatter); + if (r < 0) { + return r; + } + + auto f = formatter.get(); + if (iterations > 1 && f != nullptr) { + std::cerr << "rbd: specifing iterations is not valid with formatted output" + << std::endl; + return -EINVAL; + } + + librados::Rados rados; + r = utils::init_rados(&rados); + if (r < 0) { + return r; + } + + r = rados.wait_for_latest_osdmap(); + if (r < 0) { + std::cerr << "rbd: failed to retrieve OSD map" << std::endl; + return r; + } + + if (!pool_namespace.empty()) { + // default empty pool name only if namespace is specified to allow + // for an empty pool_spec (-> GLOBAL_POOL_KEY) + utils::normalize_pool_name(&pool); + } + std::string pool_spec = format_pool_spec(pool, pool_namespace); + + // no point to refreshing faster than the stats period + auto delay = std::min<uint32_t>(10, g_conf().get_val<int64_t>("mgr_stats_period")); + + ImageStats image_stats; + uint32_t count = 0; + bool printed_notice = false; + while (count++ < iterations || iterations == 0) { + r = query_iostats(rados, pool_spec, sort_by, &image_stats, std::cerr); + if (r < 0) { + return r; + } + + if (count == 1 && image_stats.empty()) { + count = 0; + if (!printed_notice) { + std::cerr << "rbd: waiting for initial image stats" + << std::endl << std::endl;; + printed_notice = true; + } + } else { + iostat::format(image_stats, f, pool_spec.empty()); + if (f != nullptr) { + break; + } + } + + sleep(delay); + } + + return 0; +} + +#ifdef HAVE_CURSES +void get_arguments_iotop(po::options_description *positional, + po::options_description *options) { + at::add_pool_options(positional, options, true); +} + +int execute_iotop(const po::variables_map &vm, + const std::vector<std::string> &ceph_global_init_args) { + std::string pool; + std::string pool_namespace; + size_t arg_index = 0; + int r = utils::get_pool_and_namespace_names(vm, false, &pool, + &pool_namespace, &arg_index); + if (r < 0) { + return r; + } + + librados::Rados rados; + r = utils::init_rados(&rados); + if (r < 0) { + return r; + } + + r = rados.wait_for_latest_osdmap(); + if (r < 0) { + std::cerr << "rbd: failed to retrieve OSD map" << std::endl; + return r; + } + + if (!pool_namespace.empty()) { + // default empty pool name only if namespace is specified to allow + // for an empty pool_spec (-> GLOBAL_POOL_KEY) + utils::normalize_pool_name(&pool); + } + iotop::MainWindow mainWindow(rados, format_pool_spec(pool, pool_namespace)); + r = mainWindow.run(); + if (r < 0) { + return r; + } + + return 0; +} + +Shell::Action top_action( + {"perf", "image", "iotop"}, {}, "Display a top-like IO monitor.", "", + &get_arguments_iotop, &execute_iotop); + +#endif // HAVE_CURSES + +Shell::Action stat_action( + {"perf", "image", "iostat"}, {}, "Display image IO statistics.", "", + &get_arguments_iostat, &execute_iostat); +} // namespace perf +} // namespace action +} // namespace rbd |