]> git.proxmox.com Git - ceph.git/blame - ceph/src/tools/rbd/action/Perf.cc
import ceph quincy 17.2.4
[ceph.git] / ceph / src / tools / rbd / action / Perf.cc
CommitLineData
11fdf7f2
TL
1// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
2// vim: ts=8 sw=2 smarttab
3
4#include "tools/rbd/ArgumentTypes.h"
5#include "tools/rbd/Shell.h"
6#include "tools/rbd/Utils.h"
7#include "include/stringify.h"
8#include "common/ceph_context.h"
9#include "common/ceph_json.h"
10#include "common/errno.h"
11#include "common/Formatter.h"
12#include "common/TextTable.h"
13#include "global/global_context.h"
f67539c2 14#ifdef HAVE_CURSES
11fdf7f2 15#include <ncurses.h>
f67539c2 16#endif
11fdf7f2
TL
17#include <stdio.h>
18#include <unistd.h>
19#include <sys/time.h>
20#include <sys/types.h>
21#include <iostream>
22#include <vector>
23#include <boost/algorithm/string.hpp>
24#include <boost/assign.hpp>
25#include <boost/bimap.hpp>
26#include <boost/program_options.hpp>
9f95a23c 27#include "json_spirit/json_spirit.h"
11fdf7f2
TL
28
29namespace rbd {
30namespace action {
31namespace perf {
32
33namespace at = argument_types;
34namespace po = boost::program_options;
35
36namespace {
37
38enum class StatDescriptor {
39 WRITE_OPS = 0,
40 READ_OPS,
41 WRITE_BYTES,
42 READ_BYTES,
43 WRITE_LATENCY,
44 READ_LATENCY
45};
46
47typedef boost::bimap<StatDescriptor, std::string> StatDescriptors;
48
49static const StatDescriptors STAT_DESCRIPTORS =
50 boost::assign::list_of<StatDescriptors::relation>
51 (StatDescriptor::WRITE_OPS, "write_ops")
52 (StatDescriptor::READ_OPS, "read_ops")
53 (StatDescriptor::WRITE_BYTES, "write_bytes")
54 (StatDescriptor::READ_BYTES, "read_bytes")
55 (StatDescriptor::WRITE_LATENCY, "write_latency")
56 (StatDescriptor::READ_LATENCY, "read_latency");
57
58std::ostream& operator<<(std::ostream& os, const StatDescriptor& val) {
59 auto it = STAT_DESCRIPTORS.left.find(val);
60 if (it == STAT_DESCRIPTORS.left.end()) {
61 os << "unknown (" << static_cast<int>(val) << ")";
62 } else {
63 os << it->second;
64 }
65 return os;
66}
67
68void validate(boost::any& v, const std::vector<std::string>& values,
69 StatDescriptor *target_type, int) {
70 po::validators::check_first_occurrence(v);
71 std::string s = po::validators::get_single_string(values);
72 boost::replace_all(s, "_", " ");
73 boost::replace_all(s, "-", "_");
74
75 auto it = STAT_DESCRIPTORS.right.find(s);
76 if (it == STAT_DESCRIPTORS.right.end()) {
77 throw po::validation_error(po::validation_error::invalid_option_value);
78 }
79 v = boost::any(it->second);
80}
81
82struct ImageStat {
83 ImageStat(const std::string& pool_name, const std::string& pool_namespace,
84 const std::string& image_name)
85 : pool_name(pool_name), pool_namespace(pool_namespace),
86 image_name(image_name) {
87 stats.resize(STAT_DESCRIPTORS.size());
88 }
89
90 std::string pool_name;
91 std::string pool_namespace;
92 std::string image_name;
93 std::vector<double> stats;
94};
95
96typedef std::vector<ImageStat> ImageStats;
97
98typedef std::pair<std::string, std::string> SpecPair;
99
100std::string format_pool_spec(const std::string& pool,
101 const std::string& pool_namespace) {
102 std::string pool_spec{pool};
103 if (!pool_namespace.empty()) {
104 pool_spec += "/" + pool_namespace;
105 }
106 return pool_spec;
107}
108
109int query_iostats(librados::Rados& rados, const std::string& pool_spec,
110 StatDescriptor sort_by, ImageStats* image_stats,
111 std::ostream& err_os) {
112 auto sort_by_str = STAT_DESCRIPTORS.left.find(sort_by)->second;
113
114 std::string cmd = R"(
115 {
116 "prefix": "rbd perf image stats",
117 "pool_spec": ")" + pool_spec + R"(",
118 "sort_by": ")" + sort_by_str + R"(",
119 "format": "json"
120 }")";
121
122 bufferlist in_bl;
123 bufferlist out_bl;
124 std::string outs;
125 int r = rados.mgr_command(cmd, in_bl, &out_bl, &outs);
126 if (r == -EOPNOTSUPP) {
127 err_os << "rbd: 'rbd_support' mgr module is not enabled."
128 << std::endl << std::endl
129 << "Use 'ceph mgr module enable rbd_support' to enable."
130 << std::endl;
131 return r;
132 } else if (r < 0) {
133 err_os << "rbd: mgr command failed: " << cpp_strerror(r);
134 if (!outs.empty()) {
135 err_os << ": " << outs;
136 }
137 err_os << std::endl;
138 return r;
139 }
140
141 json_spirit::mValue json_root;
142 if (!json_spirit::read(out_bl.to_str(), json_root)) {
143 err_os << "rbd: error parsing perf stats" << std::endl;
144 return -EINVAL;
145 }
146
147 image_stats->clear();
148 try {
149 auto& root = json_root.get_obj();
150
151 // map JSON stat descriptor order to our internal order
152 std::map<uint32_t, uint32_t> json_to_internal_stats;
153 auto& json_stat_descriptors = root["stat_descriptors"].get_array();
154 for (size_t idx = 0; idx < json_stat_descriptors.size(); ++idx) {
155 auto it = STAT_DESCRIPTORS.right.find(
156 json_stat_descriptors[idx].get_str());
157 if (it == STAT_DESCRIPTORS.right.end()) {
158 continue;
159 }
160 json_to_internal_stats[idx] = static_cast<uint32_t>(it->second);
161 }
162
163 // cache a mapping from pool descriptors back to pool-specs
164 std::map<std::string, SpecPair> json_to_internal_pools;
165 auto& pool_descriptors = root["pool_descriptors"].get_obj();
166 for (auto& pool : pool_descriptors) {
167 auto& pool_spec = pool.second.get_str();
168 auto pos = pool_spec.rfind("/");
169
170 SpecPair pair{pool_spec.substr(0, pos), ""};
171 if (pos != std::string::npos) {
172 pair.second = pool_spec.substr(pos + 1);
173 }
174
175 json_to_internal_pools[pool.first] = pair;
176 }
177
178 auto& stats = root["stats"].get_array();
179 for (auto& stat : stats) {
180 auto& stat_obj = stat.get_obj();
181 if (!stat_obj.empty()) {
182 auto& image_spec = stat_obj.begin()->first;
183
184 auto pos = image_spec.find("/");
185 SpecPair pair{image_spec.substr(0, pos), ""};
186 if (pos != std::string::npos) {
187 pair.second = image_spec.substr(pos + 1);
188 }
189
190 const auto pool_it = json_to_internal_pools.find(pair.first);
191 if (pool_it == json_to_internal_pools.end()) {
192 continue;
193 }
194
195 image_stats->emplace_back(
196 pool_it->second.first, pool_it->second.second, pair.second);
197
198 auto& image_stat = image_stats->back();
199 auto& data = stat_obj.begin()->second.get_array();
200 for (auto& indexes : json_to_internal_stats) {
201 image_stat.stats[indexes.second] = data[indexes.first].get_real();
202 }
203 }
204 }
205 } catch (std::runtime_error &e) {
206 err_os << "rbd: error parsing perf stats: " << e.what() << std::endl;
207 return -EINVAL;
208 }
209
210 return 0;
211}
212
213void format_stat(StatDescriptor stat_descriptor, double stat,
214 std::ostream& os) {
215 switch (stat_descriptor) {
216 case StatDescriptor::WRITE_OPS:
217 case StatDescriptor::READ_OPS:
218 os << si_u_t(stat) << "/s";
219 break;
220 case StatDescriptor::WRITE_BYTES:
221 case StatDescriptor::READ_BYTES:
222 os << byte_u_t(stat) << "/s";
223 break;
224 case StatDescriptor::WRITE_LATENCY:
225 case StatDescriptor::READ_LATENCY:
226 os << std::fixed << std::setprecision(2);
227 if (stat >= 1000000000) {
228 os << (stat / 1000000000) << " s";
229 } else if (stat >= 1000000) {
230 os << (stat / 1000000) << " ms";
231 } else if (stat >= 1000) {
232 os << (stat / 1000) << " us";
233 } else {
234 os << stat << " ns";
235 }
236 break;
237 default:
238 ceph_assert(false);
239 break;
240 }
241}
242
243} // anonymous namespace
244
245namespace iostat {
246
247struct Iterations {};
248
249void validate(boost::any& v, const std::vector<std::string>& values,
250 Iterations *target_type, int) {
251 po::validators::check_first_occurrence(v);
252 auto& s = po::validators::get_single_string(values);
253
254 try {
255 auto iterations = boost::lexical_cast<uint32_t>(s);
256 if (iterations > 0) {
257 v = boost::any(iterations);
258 return;
259 }
260 } catch (const boost::bad_lexical_cast &) {
261 }
262 throw po::validation_error(po::validation_error::invalid_option_value);
263}
264
265void format(const ImageStats& image_stats, Formatter* f, bool global_search) {
266 TextTable tbl;
267 if (f) {
268 f->open_array_section("images");
269 } else {
270 tbl.define_column("NAME", TextTable::LEFT, TextTable::LEFT);
271 for (auto& stat : STAT_DESCRIPTORS.left) {
272 std::string title;
273 switch (stat.first) {
274 case StatDescriptor::WRITE_OPS:
275 title = "WR ";
276 break;
277 case StatDescriptor::READ_OPS:
278 title = "RD ";
279 break;
280 case StatDescriptor::WRITE_BYTES:
281 title = "WR_BYTES ";
282 break;
283 case StatDescriptor::READ_BYTES:
284 title = "RD_BYTES ";
285 break;
286 case StatDescriptor::WRITE_LATENCY:
287 title = "WR_LAT ";
288 break;
289 case StatDescriptor::READ_LATENCY:
290 title = "RD_LAT ";
291 break;
292 default:
293 ceph_assert(false);
294 break;
295 }
296 tbl.define_column(title, TextTable::RIGHT, TextTable::RIGHT);
297 }
298 }
299
300 for (auto& image_stat : image_stats) {
301 if (f) {
302 f->open_object_section("image");
303 f->dump_string("pool", image_stat.pool_name);
304 f->dump_string("pool_namespace", image_stat.pool_namespace);
305 f->dump_string("image", image_stat.image_name);
306 for (auto& pair : STAT_DESCRIPTORS.left) {
307 f->dump_float(pair.second.c_str(),
308 image_stat.stats[static_cast<size_t>(pair.first)]);
309 }
310 f->close_section();
311 } else {
312 std::string name;
313 if (global_search) {
314 name += image_stat.pool_name + "/";
315 if (!image_stat.pool_namespace.empty()) {
316 name += image_stat.pool_namespace + "/";
317 }
318 }
319 name += image_stat.image_name;
320
321 tbl << name;
322 for (auto& pair : STAT_DESCRIPTORS.left) {
323 std::stringstream str;
324 format_stat(pair.first,
325 image_stat.stats[static_cast<size_t>(pair.first)], str);
326 str << ' ';
327 tbl << str.str();
328 }
329 tbl << TextTable::endrow;
330 }
331 }
332
333 if (f) {
334 f->close_section();
335 f->flush(std::cout);
336 } else {
337 std::cout << tbl << std::endl;
338 }
339}
340
341} // namespace iostat
342
f67539c2 343#ifdef HAVE_CURSES
11fdf7f2
TL
344namespace iotop {
345
346class MainWindow {
347public:
348 MainWindow(librados::Rados& rados, const std::string& pool_spec)
349 : m_rados(rados), m_pool_spec(pool_spec) {
350 initscr();
351 curs_set(0);
352 cbreak();
353 noecho();
354 keypad(stdscr, TRUE);
355 nodelay(stdscr, TRUE);
356
357 init_columns();
358 }
359
360 int run() {
361 redraw();
362
363 int r = 0;
364 std::stringstream err_str;
365 while (true) {
366 r = query_iostats(m_rados, m_pool_spec, m_sort_by, &m_image_stats,
367 err_str);
368 if (r < 0) {
369 break;
370 return r;
371 }
372
373 redraw();
374 wait_for_key_or_delay();
375
376 int ch = getch();
377 if (ch == 'q' || ch == 'Q') {
378 break;
379 } else if (ch == '<' || ch == KEY_LEFT) {
380 auto it = STAT_DESCRIPTORS.left.find(m_sort_by);
381 if (it != STAT_DESCRIPTORS.left.begin()) {
382 m_sort_by = (--it)->first;
383 }
384 } else if (ch == '>' || ch == KEY_RIGHT) {
385 auto it = STAT_DESCRIPTORS.left.find(m_sort_by);
386 if (it != STAT_DESCRIPTORS.left.end() &&
387 ++it != STAT_DESCRIPTORS.left.end()) {
388 m_sort_by = it->first;
389 }
390 }
391 }
392
393 endwin();
394
395 if (r < 0) {
396 std::cerr << err_str.str() << std::endl;
397 }
398 return r;
399 }
400
401private:
402 static const size_t STAT_COLUMN_WIDTH = 12;
403
404 librados::Rados& m_rados;
405 std::string m_pool_spec;
406
407 ImageStats m_image_stats;
408 StatDescriptor m_sort_by = StatDescriptor::WRITE_OPS;
409
410 bool m_pending_win_opened = false;
411 WINDOW* m_pending_win = nullptr;
412
413 int m_height = 1;
414 int m_width = 1;
415
416 std::map<StatDescriptor, std::string> m_columns;
417
418 void init_columns() {
419 m_columns.clear();
420 for (auto& pair : STAT_DESCRIPTORS.left) {
421 std::string title;
422 switch (pair.first) {
423 case StatDescriptor::WRITE_OPS:
424 title = "WRITES OPS";
425 break;
426 case StatDescriptor::READ_OPS:
427 title = "READS OPS";
428 break;
429 case StatDescriptor::WRITE_BYTES:
430 title = "WRITE BYTES";
431 break;
432 case StatDescriptor::READ_BYTES:
433 title = "READ BYTES";
434 break;
435 case StatDescriptor::WRITE_LATENCY:
436 title = "WRITE LAT";
437 break;
438 case StatDescriptor::READ_LATENCY:
439 title = "READ LAT";
440 break;
441 default:
442 ceph_assert(false);
443 break;
444 }
445 m_columns[pair.first] = (title);
446 }
447 }
448
449 void redraw() {
450 getmaxyx(stdscr, m_height, m_width);
451
452 redraw_main_window();
453 redraw_pending_window();
454
455 doupdate();
456 }
457
458 void redraw_main_window() {
459 werase(stdscr);
460 mvhline(0, 0, ' ' | A_REVERSE, m_width);
461
462 // print header for all metrics
463 int remaining_cols = m_width;
464 std::stringstream str;
465 for (auto& pair : m_columns) {
466 int attr = A_REVERSE;
467 std::string title;
468 if (pair.first == m_sort_by) {
469 title += '>';
470 attr |= A_BOLD;
471 } else {
472 title += ' ';
473 }
474 title += pair.second;
475
476 str.str("");
477 str << std::right << std::setfill(' ')
478 << std::setw(STAT_COLUMN_WIDTH)
479 << title << ' ';
480
481 attrset(attr);
482 addstr(str.str().c_str());
483 remaining_cols -= title.size();
484 }
485
486 attrset(A_REVERSE);
487 addstr("IMAGE");
488 attrset(A_NORMAL);
489
490 // print each image (one per line)
491 int row = 1;
492 int remaining_lines = m_height - 1;
493 for (auto& image_stat : m_image_stats) {
494 if (remaining_lines <= 0) {
495 break;
496 }
497 --remaining_lines;
498
499 move(row++, 0);
500 for (auto& pair : m_columns) {
501 str.str("");
502 format_stat(pair.first,
503 image_stat.stats[static_cast<size_t>(pair.first)], str);
504 auto value = str.str().substr(0, STAT_COLUMN_WIDTH);
505
506 str.str("");
507 str << std::right << std::setfill(' ')
508 << std::setw(STAT_COLUMN_WIDTH)
509 << value << ' ';
510 addstr(str.str().c_str());
511 }
512
513 std::string image;
514 if (m_pool_spec.empty()) {
515 image = format_pool_spec(image_stat.pool_name,
516 image_stat.pool_namespace) + "/";
517 }
518 image += image_stat.image_name;
519 addstr(image.substr(0, remaining_cols).c_str());
520 }
521
522 wnoutrefresh(stdscr);
523 }
524
525 void redraw_pending_window() {
526 // draw a "please by patient" window while waiting
527 const char* msg = "Waiting for initial stats";
528 int height = 5;
529 int width = strlen(msg) + 4;;
530 int starty = (m_height - height) / 2;
531 int startx = (m_width - width) / 2;
532
533 if (m_image_stats.empty() && !m_pending_win_opened) {
534 m_pending_win_opened = true;
535 m_pending_win = newwin(height, width, starty, startx);
536 }
537
538 if (m_pending_win != nullptr) {
539 if (m_image_stats.empty()) {
540 box(m_pending_win, 0 , 0);
541 mvwaddstr(m_pending_win, 2, 2, msg);
542 wnoutrefresh(m_pending_win);
543 } else {
544 delwin(m_pending_win);
545 m_pending_win = nullptr;
546 }
547 }
548 }
549
550 void wait_for_key_or_delay() {
551 fd_set fds;
552 FD_ZERO(&fds);
553 FD_SET(STDIN_FILENO, &fds);
554
555 // no point to refreshing faster than the stats period
556 struct timeval tval;
557 tval.tv_sec = std::min<uint32_t>(
558 10, g_conf().get_val<int64_t>("mgr_stats_period"));
559 tval.tv_usec = 0;
560
561 select(STDIN_FILENO + 1, &fds, NULL, NULL, &tval);
562 }
563};
564
565} // namespace iotop
f67539c2 566#endif // HAVE_CURSES
11fdf7f2
TL
567
568
569void get_arguments_iostat(po::options_description *positional,
570 po::options_description *options) {
571 at::add_pool_options(positional, options, true);
572 options->add_options()
573 ("iterations", po::value<iostat::Iterations>(),
574 "iterations of metric collection [> 0]")
575 ("sort-by", po::value<StatDescriptor>()->default_value(StatDescriptor::WRITE_OPS),
576 "sort-by IO metric "
577 "(write-ops, read-ops, write-bytes, read-bytes, write-latency, read-latency) "
578 "[default: write-ops]");
579 at::add_format_options(options);
580}
581
582int execute_iostat(const po::variables_map &vm,
583 const std::vector<std::string> &ceph_global_init_args) {
584 std::string pool;
585 std::string pool_namespace;
586 size_t arg_index = 0;
2a845540 587 int r = utils::get_pool_and_namespace_names(vm, false, &pool,
11fdf7f2
TL
588 &pool_namespace, &arg_index);
589 if (r < 0) {
590 return r;
591 }
592
593 uint32_t iterations = 0;
594 if (vm.count("iterations")) {
595 iterations = vm["iterations"].as<uint32_t>();
596 }
597 auto sort_by = vm["sort-by"].as<StatDescriptor>();
598
599 at::Format::Formatter formatter;
600 r = utils::get_formatter(vm, &formatter);
601 if (r < 0) {
602 return r;
603 }
604
605 auto f = formatter.get();
606 if (iterations > 1 && f != nullptr) {
607 std::cerr << "rbd: specifing iterations is not valid with formatted output"
608 << std::endl;
609 return -EINVAL;
610 }
611
612 librados::Rados rados;
613 r = utils::init_rados(&rados);
614 if (r < 0) {
615 return r;
616 }
617
618 r = rados.wait_for_latest_osdmap();
619 if (r < 0) {
620 std::cerr << "rbd: failed to retrieve OSD map" << std::endl;
621 return r;
622 }
623
2a845540
TL
624 if (!pool_namespace.empty()) {
625 // default empty pool name only if namespace is specified to allow
626 // for an empty pool_spec (-> GLOBAL_POOL_KEY)
627 utils::normalize_pool_name(&pool);
628 }
11fdf7f2
TL
629 std::string pool_spec = format_pool_spec(pool, pool_namespace);
630
631 // no point to refreshing faster than the stats period
632 auto delay = std::min<uint32_t>(10, g_conf().get_val<int64_t>("mgr_stats_period"));
633
634 ImageStats image_stats;
635 uint32_t count = 0;
636 bool printed_notice = false;
637 while (count++ < iterations || iterations == 0) {
638 r = query_iostats(rados, pool_spec, sort_by, &image_stats, std::cerr);
639 if (r < 0) {
640 return r;
641 }
642
643 if (count == 1 && image_stats.empty()) {
644 count = 0;
645 if (!printed_notice) {
646 std::cerr << "rbd: waiting for initial image stats"
647 << std::endl << std::endl;;
648 printed_notice = true;
649 }
650 } else {
651 iostat::format(image_stats, f, pool_spec.empty());
652 if (f != nullptr) {
653 break;
654 }
655 }
656
657 sleep(delay);
658 }
659
660 return 0;
661}
662
f67539c2 663#ifdef HAVE_CURSES
11fdf7f2
TL
664void get_arguments_iotop(po::options_description *positional,
665 po::options_description *options) {
666 at::add_pool_options(positional, options, true);
667}
668
669int execute_iotop(const po::variables_map &vm,
670 const std::vector<std::string> &ceph_global_init_args) {
671 std::string pool;
672 std::string pool_namespace;
673 size_t arg_index = 0;
2a845540 674 int r = utils::get_pool_and_namespace_names(vm, false, &pool,
11fdf7f2
TL
675 &pool_namespace, &arg_index);
676 if (r < 0) {
677 return r;
678 }
679
680 librados::Rados rados;
681 r = utils::init_rados(&rados);
682 if (r < 0) {
683 return r;
684 }
685
686 r = rados.wait_for_latest_osdmap();
687 if (r < 0) {
688 std::cerr << "rbd: failed to retrieve OSD map" << std::endl;
689 return r;
690 }
691
2a845540
TL
692 if (!pool_namespace.empty()) {
693 // default empty pool name only if namespace is specified to allow
694 // for an empty pool_spec (-> GLOBAL_POOL_KEY)
695 utils::normalize_pool_name(&pool);
696 }
11fdf7f2
TL
697 iotop::MainWindow mainWindow(rados, format_pool_spec(pool, pool_namespace));
698 r = mainWindow.run();
699 if (r < 0) {
700 return r;
701 }
702
703 return 0;
704}
705
11fdf7f2
TL
706Shell::Action top_action(
707 {"perf", "image", "iotop"}, {}, "Display a top-like IO monitor.", "",
708 &get_arguments_iotop, &execute_iotop);
709
f67539c2
TL
710#endif // HAVE_CURSES
711
712Shell::Action stat_action(
713 {"perf", "image", "iostat"}, {}, "Display image IO statistics.", "",
714 &get_arguments_iostat, &execute_iostat);
11fdf7f2
TL
715} // namespace perf
716} // namespace action
717} // namespace rbd