diff --git a/src/bees-context.cc b/src/bees-context.cc index 568d538..1e53659 100644 --- a/src/bees-context.cc +++ b/src/bees-context.cc @@ -178,15 +178,28 @@ BeesContext::BeesContext(shared_ptr parent) : } } +bool +BeesContext::is_root_ro(uint64_t root) +{ + return roots()->is_root_ro(root); +} + bool BeesContext::dedup(const BeesRangePair &brp) { // TOOLONG and NOTE can retroactively fill in the filename details, but LOG can't BEESNOTE("dedup " << brp); - brp.first.fd(shared_from_this()); brp.second.fd(shared_from_this()); + if (is_root_ro(brp.second.fid().root())) { + // BEESLOGDEBUG("WORKAROUND: dst subvol is read-only in " << name_fd(brp.second.fd())); + BEESCOUNT(dedup_workaround_btrfs_send); + return false; + } + + brp.first.fd(shared_from_this()); + BEESTOOLONG("dedup " << brp); BeesAddress first_addr(brp.first.fd(), brp.first.begin()); diff --git a/src/bees-roots.cc b/src/bees-roots.cc index 4d6bfe8..f536f9a 100644 --- a/src/bees-roots.cc +++ b/src/bees-roots.cc @@ -11,8 +11,6 @@ using namespace crucible; using namespace std; -BeesRoots::ScanMode BeesRoots::s_scan_mode = BeesRoots::SCAN_MODE_ZERO; - string format_time(time_t t) { @@ -67,10 +65,21 @@ void BeesRoots::set_scan_mode(ScanMode mode) { THROW_CHECK1(invalid_argument, mode, mode < SCAN_MODE_COUNT); - s_scan_mode = mode; + m_scan_mode = mode; BEESLOGINFO("Scan mode set to " << mode << " (" << scan_mode_ntoa(mode) << ")"); } +void +BeesRoots::set_workaround_btrfs_send(bool do_avoid) +{ + m_workaround_btrfs_send = do_avoid; + if (m_workaround_btrfs_send) { + BEESLOGINFO("WORKAROUND: btrfs send workaround enabled"); + } else { + BEESLOGINFO("btrfs send workaround disabled"); + } +} + string BeesRoots::crawl_state_filename() const { @@ -281,7 +290,7 @@ BeesRoots::crawl_roots() BEESLOGINFO("idle: crawl map is empty!"); } - switch (s_scan_mode) { + switch (m_scan_mode) { case SCAN_MODE_ZERO: { // Scan the same inode/offset tuple in each subvol (good for snapshots) @@ -366,6 +375,13 @@ BeesRoots::crawl_roots() return false; } +void +BeesRoots::clear_caches() +{ + m_ctx->fd_cache()->clear(); + m_root_ro_cache.clear(); +} + void BeesRoots::crawl_thread() { @@ -408,7 +424,7 @@ BeesRoots::crawl_thread() // Even open files are a problem if they're big enough. auto new_count = m_transid_re.count(); if (new_count != last_count) { - m_ctx->fd_cache()->clear(); + clear_caches(); } last_count = new_count; @@ -541,6 +557,12 @@ BeesRoots::BeesRoots(shared_ptr ctx) : m_writeback_thread("crawl_writeback"), m_task_running(false) { + + m_root_ro_cache.func([&](uint64_t root) -> bool { + return is_root_ro_nocache(root); + }); + m_root_ro_cache.max_size(BEES_ROOT_FD_CACHE_SIZE); + m_crawl_thread.exec([&]() { // Measure current transid before creating any crawlers catch_all([&]() { @@ -646,6 +668,7 @@ BeesRoots::open_root_nocache(uint64_t rootid) Stat st(rv); THROW_CHECK1(runtime_error, st.st_ino, st.st_ino == BTRFS_FIRST_FREE_OBJECTID); // BEESLOGDEBUG("open_root_nocache " << rootid << ": " << name_fd(rv)); + BEESCOUNT(root_ok); return rv; } @@ -667,6 +690,32 @@ BeesRoots::open_root(uint64_t rootid) return m_ctx->fd_cache()->open_root(m_ctx, rootid); } +bool +BeesRoots::is_root_ro_nocache(uint64_t root) +{ + Fd root_fd = open_root(root); + BEESTRACE("checking subvol flags on root " << root << " path " << name_fd(root_fd)); + + uint64_t flags = 0; + DIE_IF_NON_ZERO(ioctl(root_fd, BTRFS_IOC_SUBVOL_GETFLAGS, &flags)); + if (flags & BTRFS_SUBVOL_RDONLY) { + BEESLOGDEBUG("WORKAROUND: Avoiding RO subvol " << root); + BEESCOUNT(root_workaround_btrfs_send); + return true; + } + return false; +} + +bool +BeesRoots::is_root_ro(uint64_t root) +{ + // If we are not implementing the workaround there is no need for cache + if (!m_workaround_btrfs_send) { + return false; + } + + return m_root_ro_cache(root); +} uint64_t BeesRoots::next_root(uint64_t root) @@ -889,6 +938,20 @@ BeesCrawl::fetch_extents() return next_transid(); } + // Check for btrfs send workaround: don't scan RO roots at all, pretend + // they are just empty. We can't free any space there, and we + // don't have the necessary analysis logic to be able to use + // them as dedup src extents (yet). + // + // This will keep the max_transid up to date so if the root + // is ever switched back to read-write, it won't trigger big + // expensive in-kernel searches for ancient transids. + if (m_ctx->is_root_ro(old_state.m_root)) { + BEESLOGDEBUG("WORKAROUND: RO root " << old_state.m_root); + BEESCOUNT(root_workaround_btrfs_send); + return next_transid(); + } + BEESNOTE("crawling " << get_state_end()); Timer crawl_timer; diff --git a/src/bees.cc b/src/bees.cc index 467f15f..b267c73 100644 --- a/src/bees.cc +++ b/src/bees.cc @@ -35,6 +35,7 @@ int bees_log_level = 8; int do_cmd_help(char *argv[]) { + // 80col 01234567890123456789012345678901234567890123456789012345678901234567890123456789 cerr << "Usage: " << argv[0] << " [options] fs-root-path [fs-root-path-2...]\n" "Performs best-effort extent-same deduplication on btrfs.\n" "\n" @@ -42,25 +43,35 @@ do_cmd_help(char *argv[]) "Other directories will be rejected.\n" "\n" "Options:\n" - "\t-h, --help\t\tShow this help\n" - "\t-c, --thread-count\tWorker thread count (default CPU count * factor)\n" - "\t-C, --thread-factor\tWorker thread factor (default " << BEES_DEFAULT_THREAD_FACTOR << ")\n" - "\t-G, --thread-min\t\tMinimum worker thread count with load average target (default 0)\n" - "\t-g, --loadavg-target\t\tTarget load average for worker threads (default is no target)\n" - "\t-m, --scan-mode\t\tScanning mode (0..2, default 0)\n" - "\t-t, --timestamps\tShow timestamps in log output (default)\n" - "\t-T, --no-timestamps\tOmit timestamps in log output\n" - "\t-p, --absolute-paths\tShow absolute paths (default)\n" - "\t-P, --strip-paths\tStrip $CWD from beginning of all paths in the log\n" - "\t-v, --verbose\tSet maximum log level (0..8, default 8)\n" + " -h, --help Show this help\n" + "\n" + "Load management options:\n" + " -c, --thread-count Worker thread count (default CPU count * factor)\n" + " -C, --thread-factor Worker thread factor (default " << BEES_DEFAULT_THREAD_FACTOR << ")\n" + " -G, --thread-min Minimum worker thread count (default 0)\n" + " -g, --loadavg-target Target load average for worker threads (default none)\n" + "\n" + "Filesystem tree traversal options:\n" + " -m, --scan-mode Scanning mode (0..2, default 0)\n" + "\n" + "Workarounds:\n" + " -a, --workaround-btrfs-send Workaround for btrfs send\n" + "\n" + "Logging options:\n" + " -t, --timestamps Show timestamps in log output (default)\n" + " -T, --no-timestamps Omit timestamps in log output\n" + " -p, --absolute-paths Show absolute paths (default)\n" + " -P, --strip-paths Strip $CWD from beginning of all paths in the log\n" + " -v, --verbose Set maximum log level (0..8, default 8)\n" "\n" "Optional environment variables:\n" - "\tBEESHOME\tPath to hash table and configuration files\n" - "\t\t\t(default is .beeshome/ in the root of each filesystem).\n" + " BEESHOME Path to hash table and configuration files\n" + " (default is .beeshome/ in the root of each filesystem).\n" "\n" - "\tBEESSTATUS\tFile to write status to (tmpfs recommended, e.g. /run).\n" - "\t\t\tNo status is written if this variable is unset.\n" + " BEESSTATUS File to write status to (tmpfs recommended, e.g. /run).\n" + " No status is written if this variable is unset.\n" "\n" + // 80col 01234567890123456789012345678901234567890123456789012345678901234567890123456789 << endl; return 0; } @@ -657,26 +668,47 @@ bees_main(int argc, char *argv[]) unsigned thread_count = 0; unsigned thread_min = 0; double load_target = 0; + bool workaround_btrfs_send = false; + + // Configure getopt_long + static const struct option long_options[] = { + { "thread-factor", required_argument, NULL, 'C' }, + { "thread-min", required_argument, NULL, 'G' }, + { "strip-paths", no_argument, NULL, 'P' }, + { "no-timestamps", no_argument, NULL, 'T' }, + { "workaround-btrfs-send", no_argument, NULL, 'a' }, + { "thread-count", required_argument, NULL, 'c' }, + { "loadavg-target", required_argument, NULL, 'g' }, + { "help", no_argument, NULL, 'h' }, + { "scan-mode", required_argument, NULL, 'm' }, + { "absolute-paths", no_argument, NULL, 'p' }, + { "timestamps", no_argument, NULL, 't' }, + { "verbose", required_argument, NULL, 'v' }, + { 0, 0, 0, 0 }, + }; + + // Build getopt_long's short option list from the long_options table. + // While we're at it, make sure we didn't duplicate any options. + string getopt_list; + set option_vals; + for (const struct option *op = long_options; op->val; ++op) { + THROW_CHECK1(runtime_error, op->val, !option_vals.count(op->val)); + option_vals.insert(op->val); + if ((op->val & 0xff) != op->val) { + continue; + } + getopt_list += op->val; + if (op->has_arg == required_argument) { + getopt_list += ':'; + } + } // Parse options int c; while (1) { int option_index = 0; - static const struct option long_options[] = { - { "thread-factor", required_argument, NULL, 'C' }, - { "thread-min", required_argument, NULL, 'G' }, - { "strip-paths", no_argument, NULL, 'P' }, - { "no-timestamps", no_argument, NULL, 'T' }, - { "thread-count", required_argument, NULL, 'c' }, - { "loadavg-target", required_argument, NULL, 'g' }, - { "help", no_argument, NULL, 'h' }, - { "scan-mode", required_argument, NULL, 'm' }, - { "absolute-paths", no_argument, NULL, 'p' }, - { "timestamps", no_argument, NULL, 't' }, - { "verbose", required_argument, NULL, 'v' }, - }; - c = getopt_long(argc, argv, "C:G:PTc:hg:m:ptv:", long_options, &option_index); + c = getopt_long(argc, argv, getopt_list.c_str(), long_options, &option_index); if (-1 == c) { break; } @@ -695,6 +727,9 @@ bees_main(int argc, char *argv[]) case 'T': chatter_prefix_timestamp = false; break; + case 'a': + workaround_btrfs_send = true; + break; case 'c': thread_count = stoul(optarg); break; @@ -702,7 +737,7 @@ bees_main(int argc, char *argv[]) load_target = stod(optarg); break; case 'm': - BeesRoots::set_scan_mode(static_cast(stoul(optarg))); + bc->roots()->set_scan_mode(static_cast(stoul(optarg))); break; case 'p': crucible::set_relative_path(""); @@ -763,6 +798,9 @@ bees_main(int argc, char *argv[]) BEESLOGNOTICE("setting worker thread pool maximum size to " << thread_count); TaskMaster::set_thread_count(thread_count); + // Workaround for btrfs send + bc->roots()->set_workaround_btrfs_send(workaround_btrfs_send); + // Create a context and start crawlers bool did_subscription = false; while (optind < argc) { diff --git a/src/bees.h b/src/bees.h index 3b7b6e8..e34fc78 100644 --- a/src/bees.h +++ b/src/bees.h @@ -524,11 +524,14 @@ class BeesRoots : public enable_shared_from_this { size_t m_transid_factor = BEES_TRANSID_FACTOR; atomic m_task_running; Task m_crawl_task; + bool m_workaround_btrfs_send = false; + LRUCache m_root_ro_cache; void insert_new_crawl(); void insert_root(const BeesCrawlState &bcs); Fd open_root_nocache(uint64_t root); Fd open_root_ino_nocache(uint64_t root, uint64_t ino); + bool is_root_ro_nocache(uint64_t root); uint64_t transid_min(); uint64_t transid_max(); uint64_t transid_max_nocache(); @@ -545,6 +548,7 @@ class BeesRoots : public enable_shared_from_this { void current_state_set(const BeesCrawlState &bcs); RateEstimator& transid_re(); size_t crawl_batch(shared_ptr crawl); + void clear_caches(); friend class BeesFdCache; friend class BeesCrawl; @@ -554,6 +558,7 @@ public: Fd open_root(uint64_t root); Fd open_root_ino(uint64_t root, uint64_t ino); Fd open_root_ino(const BeesFileId &bfi) { return open_root_ino(bfi.root(), bfi.ino()); } + bool is_root_ro(uint64_t root); // TODO: think of better names for these. // or TODO: do extent-tree scans instead @@ -564,10 +569,11 @@ public: SCAN_MODE_COUNT, // must be last }; - static void set_scan_mode(ScanMode new_mode); + void set_scan_mode(ScanMode new_mode); + void set_workaround_btrfs_send(bool do_avoid); private: - static ScanMode s_scan_mode; + ScanMode m_scan_mode = SCAN_MODE_ZERO; static string scan_mode_ntoa(ScanMode new_mode); }; @@ -712,6 +718,7 @@ public: BeesFileRange scan_forward(const BeesFileRange &bfr); + bool is_root_ro(uint64_t root); BeesRangePair dup_extent(const BeesFileRange &src); bool dedup(const BeesRangePair &brp);