]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
Allow the LMDB domains table to be split, with the timestamps separate.
authorMiod Vallat <miod.vallat@powerdns.com>
Thu, 15 Jan 2026 09:19:59 +0000 (10:19 +0100)
committerMiod Vallat <miod.vallat@powerdns.com>
Wed, 18 Mar 2026 08:00:30 +0000 (09:00 +0100)
Signed-off-by: Miod Vallat <miod.vallat@powerdns.com>
docs/backends/lmdb.rst
modules/lmdbbackend/lmdbbackend.cc
modules/lmdbbackend/lmdbbackend.hh

index 524afd2359c5b772a14d1cd5e4ff7f1f8efeffc3..811ad1bd316a602e4ed961a592347e1ffa51c673 100644 (file)
@@ -171,6 +171,18 @@ to be sent upon startup, unless a ``flush`` command is sent using
 :doc:`pdns_control <../manpages/pdns_control.1>` before stopping the
 PowerDNS Authoritative Server.
 
+``lmdb-split-domains-table``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+  .. versionadded:: 5.1.0
+
+-  Boolean
+-  Default: no
+
+Split the domains table in two, with the last notification timestamp and
+last freshness check timestamp in a separate table.
+This lowers the I/O bandwidth requirements on setups with many zones.
+
 ``lmdb-lightning-stream``
 ^^^^^^^^^^^^^^^^^^^^^^^^^
 
index a43105427099cea85a4670dbd5109482aac55bb2..b50fa47a52c9e83be107ef3034137c6d62f63990 100644 (file)
@@ -60,6 +60,7 @@ constexpr unsigned int SCHEMAVERSION{6};
 BOOST_CLASS_VERSION(LMDBBackend::KeyDataDB, 1)
 BOOST_CLASS_VERSION(ZoneName, 1)
 BOOST_CLASS_VERSION(DomainInfo, 2)
+BOOST_CLASS_VERSION(LMDBBackend::TransientDomainInfo, 0)
 
 static bool s_first = true;
 static uint32_t s_shards = 0;
@@ -739,6 +740,7 @@ LMDBBackend::LMDBBackend(const std::string& suffix)
   }
 
   d_write_notification_update = mustDo("write-notification-update");
+  d_split_domains_table = mustDo("split-domains-table");
 
   if (mustDo("lightning-stream")) {
     d_random_ids = true;
@@ -879,12 +881,16 @@ LMDBBackend::~LMDBBackend()
 
 void LMDBBackend::openAllTheDatabases()
 {
-  d_tdomains = std::make_shared<tdomains_t>(getMDBEnv(getArg("filename").c_str(), MDB_NOSUBDIR | MDB_NORDAHEAD | d_asyncFlag, 0600, d_mapsize_main), "domains_v5");
+  auto filename = getArg("filename");
+  d_tdomains = std::make_shared<tdomains_t>(getMDBEnv(filename.c_str(), MDB_NOSUBDIR | MDB_NORDAHEAD | d_asyncFlag, 0600, d_mapsize_main), "domains_v5");
   d_tmeta = std::make_shared<tmeta_t>(d_tdomains->getEnv(), "metadata_v5");
   d_tkdb = std::make_shared<tkdb_t>(d_tdomains->getEnv(), "keydata_v5");
   d_ttsig = std::make_shared<ttsig_t>(d_tdomains->getEnv(), "tsig_v5");
   d_tnetworks = d_tdomains->getEnv()->openDB("networks_v6", MDB_CREATE);
   d_tviews = d_tdomains->getEnv()->openDB("views_v6", MDB_CREATE);
+  if (d_split_domains_table) {
+    d_tdomains_extra = std::make_shared<tdomain_extra_t>(d_tdomains->getEnv(), "domains_extra_v6");
+  }
 }
 
 unsigned int LMDBBackend::getCapabilities()
@@ -1009,6 +1015,20 @@ namespace serialization
     }
   }
 
+  template <class Archive>
+  void save(Archive& ar, const LMDBBackend::TransientDomainInfo& g, const unsigned int /* version */)
+  {
+    ar & g.last_check;
+    ar & g.notified_serial;
+  }
+
+  template <class Archive>
+  void load(Archive& ar, LMDBBackend::TransientDomainInfo& g, const unsigned int /* version */)
+  {
+    ar & g.last_check;
+    ar & g.notified_serial;
+  }
+
   template <class Archive>
   void serialize(Archive& ar, LMDBBackend::DomainMeta& g, const unsigned int /* version */)
   {
@@ -1048,6 +1068,7 @@ BOOST_SERIALIZATION_SPLIT_FREE(DNSName);
 BOOST_SERIALIZATION_SPLIT_FREE(ZoneName);
 BOOST_SERIALIZATION_SPLIT_FREE(LMDBBackend::KeyDataDB);
 BOOST_SERIALIZATION_SPLIT_FREE(DomainInfo);
+BOOST_SERIALIZATION_SPLIT_FREE(LMDBBackend::TransientDomainInfo);
 BOOST_IS_BITWISE_SERIALIZABLE(ComboAddress);
 
 // Resource records are serialized in the following format:
@@ -1280,22 +1301,52 @@ bool LMDBBackend::findDomain(domainid_t domainid, DomainInfo& info) const
 
 void LMDBBackend::consolidateDomainInfo(DomainInfo& info) const
 {
-  // Update the DomainInfo values if we have cached data in memory.
+  TransientDomainInfo tdi;
+  bool valid{false};
+
+  // Get data from the cache if we don't keep the database up to date.
   if (!d_write_notification_update) {
     auto container = s_transient_domain_info.read_lock();
-    TransientDomainInfo tdi;
     if (container->get(info.id, tdi)) {
-      info.notified_serial = tdi.notified_serial;
-      info.last_check = tdi.last_check;
+      valid = true;
     }
   }
+
+  // If the DomainInfo table is split, get the TransientDomainInfo part
+  // from the extra table.
+  if (!valid && d_split_domains_table) {
+    auto rotxn = d_tdomains_extra->getROTransaction();
+    if (rotxn.get(info.id, tdi)) {
+      valid = true;
+    }
+  }
+
+  if (valid) {
+    info.notified_serial = tdi.notified_serial;
+    info.last_check = tdi.last_check;
+  }
+}
+
+void LMDBBackend::writeTransientDomainInfo(const DomainInfo& info)
+{
+  // If the DomainInfo table is split, write the TransientDomainInfo part
+  // to the extra table.
+  if (d_split_domains_table) {
+    TransientDomainInfo tdi;
+    tdi.notified_serial = info.notified_serial;
+    tdi.last_check = info.last_check;
+    auto txn = d_tdomains_extra->getRWTransaction();
+    txn.put(tdi, info.id);
+    txn.commit();
+  }
 }
 
 void LMDBBackend::writeDomainInfo(const DomainInfo& info)
 {
+  // Update the in-memory cache if we don't keep the database up to date.
   if (!d_write_notification_update) {
-    auto container = s_transient_domain_info.write_lock();
     TransientDomainInfo tdi;
+    auto container = s_transient_domain_info.write_lock();
     if (container->get(info.id, tdi)) {
       // Only remove the in-memory value if it has not been modified since the
       // DomainInfo data was set up.
@@ -1303,10 +1354,13 @@ void LMDBBackend::writeDomainInfo(const DomainInfo& info)
         container->remove(info.id);
       }
     }
+    return;
   }
+
   auto txn = d_tdomains->getRWTransaction();
   txn.put(info, info.id);
   txn.commit();
+  writeTransientDomainInfo(info);
 }
 
 /* Here's the complicated story. Other backends have just one transaction, which is either
@@ -2221,6 +2275,27 @@ bool LMDBBackend::genChangeDomain(domainid_t id, const std::function<void(Domain
   return true;
 }
 
+// Similar to the above, but callback will only change the TransientDomainInfo
+// fields.
+bool LMDBBackend::genChangeTransientDomain(domainid_t id, const std::function<void(DomainInfo&)>& func) // NOLINTNEXT(readability-identifier-length)
+{
+  DomainInfo info;
+  if (!findDomain(id, info)) {
+    return false;
+  }
+  consolidateDomainInfo(info);
+  func(info);
+  if (!d_write_notification_update) {
+    // This won't write anything but update the in-memory cache
+    writeDomainInfo(info);
+  }
+  else {
+    // No need to write the complete DomainInfo in this case
+    writeTransientDomainInfo(info);
+  }
+  return true;
+}
+
 bool LMDBBackend::setKind(const ZoneName& domain, const DomainInfo::DomainKind kind)
 {
   return genChangeDomain(domain, [kind](DomainInfo& di) {
@@ -2259,6 +2334,7 @@ bool LMDBBackend::createDomain(const ZoneName& domain, const DomainInfo::DomainK
 
     txn.put(info, 0, d_random_ids, domain.hash());
     txn.commit();
+    writeTransientDomainInfo(info);
   }
 
   return true;
@@ -2374,24 +2450,24 @@ void LMDBBackend::setFresh(domainid_t domain_id)
 
 void LMDBBackend::setLastCheckTime(domainid_t domain_id, time_t last_check)
 {
-  if (d_write_notification_update) {
-    genChangeDomain(domain_id, [last_check](DomainInfo& info) {
-      info.last_check = last_check;
-    });
+  if (!d_write_notification_update) {
+    DomainInfo info;
+    if (findDomain(domain_id, info)) {
+      auto container = s_transient_domain_info.write_lock();
+      TransientDomainInfo tdi;
+      if (!container->get(info.id, tdi)) {
+        // No data yet, initialize from DomainInfo
+        tdi.notified_serial = info.notified_serial;
+      }
+      tdi.last_check = last_check;
+      container->update(info.id, tdi);
+    }
     return;
   }
 
-  DomainInfo info;
-  if (findDomain(domain_id, info)) {
-    auto container = s_transient_domain_info.write_lock();
-    TransientDomainInfo tdi;
-    if (!container->get(info.id, tdi)) {
-      // No data yet, initialize from DomainInfo
-      tdi.notified_serial = info.notified_serial;
-    }
-    tdi.last_check = last_check;
-    container->update(info.id, tdi);
-  }
+  genChangeTransientDomain(domain_id, [last_check](DomainInfo& info) {
+    info.last_check = last_check;
+  });
 }
 
 void LMDBBackend::getUpdatedPrimaries(vector<DomainInfo>& updatedDomains, std::unordered_set<DNSName>& catalogs, CatalogHashMap& catalogHashes)
@@ -2424,24 +2500,24 @@ void LMDBBackend::getUpdatedPrimaries(vector<DomainInfo>& updatedDomains, std::u
 
 void LMDBBackend::setNotified(domainid_t domain_id, uint32_t serial)
 {
-  if (d_write_notification_update) {
-    genChangeDomain(domain_id, [serial](DomainInfo& info) {
-      info.notified_serial = serial;
-    });
+  if (!d_write_notification_update) {
+    DomainInfo info;
+    if (findDomain(domain_id, info)) {
+      auto container = s_transient_domain_info.write_lock();
+      TransientDomainInfo tdi;
+      if (!container->get(info.id, tdi)) {
+        // No data yet, initialize from DomainInfo
+        tdi.last_check = info.last_check;
+      }
+      tdi.notified_serial = serial;
+      container->update(info.id, tdi);
+    }
     return;
   }
 
-  DomainInfo info;
-  if (findDomain(domain_id, info)) {
-    auto container = s_transient_domain_info.write_lock();
-    TransientDomainInfo tdi;
-    if (!container->get(info.id, tdi)) {
-      // No data yet, initialize from DomainInfo
-      tdi.last_check = info.last_check;
-    }
-    tdi.notified_serial = serial;
-    container->update(info.id, tdi);
-  }
+  genChangeTransientDomain(domain_id, [serial](DomainInfo& info) {
+    info.notified_serial = serial;
+  });
 }
 
 class getCatalogMembersReturnFalseException : std::runtime_error
@@ -3506,9 +3582,15 @@ void LMDBBackend::flush()
       if (findDomain(domid, info)) {
         info.notified_serial = tdi.notified_serial;
         info.last_check = tdi.last_check;
-        auto txn = d_tdomains->getRWTransaction();
-        txn.put(info, info.id);
-        txn.commit();
+        // If the DomainInfo table is split, only update the extra table.
+        if (d_split_domains_table) {
+          writeTransientDomainInfo(info);
+        }
+        else {
+          auto txn = d_tdomains->getRWTransaction();
+          txn.put(info, info.id);
+          txn.commit();
+        }
       }
       else {
         // Domain has been removed. This should not happen because deletion
@@ -3539,6 +3621,7 @@ public:
     declare(suffix, "shards-map-size", "shard LMDB map size in megabytes, zero to use the same size as main", "0");
     declare(suffix, "flag-deleted", "Flag entries on deletion instead of deleting them", "no");
     declare(suffix, "write-notification-update", "Update domain table upon notification", "yes");
+    declare(suffix, "split-domains-table", "Use a split domain table to reduce I/O load after XFR notifications", "no");
     declare(suffix, "lightning-stream", "Run in Lightning Stream compatible mode", "no");
   }
   DNSBackend* make(const string& suffix = "") override
index 9aa1a9c132d81d4835032cf3d664223cb7931990..ad8103907a099c4072f9f67f0e6d1f5cecc81bde 100644 (file)
@@ -265,6 +265,14 @@ public:
     bool active{true};
     bool published{true};
   };
+  // Transient DomainInfo data, not necessarily synchronized with the
+  // database.
+  // All the fields exist with the exact same types in DomainInfo.
+  struct TransientDomainInfo
+  {
+    time_t last_check{};
+    uint32_t notified_serial{};
+  };
   class LMDBResourceRecord : public DNSResourceRecord
   {
   public:
@@ -294,6 +302,8 @@ private:
                    index_on<TSIGKey, DNSName, &TSIGKey::name>>
     ttsig_t;
 
+  using tdomain_extra_t = TypedDBI<TransientDomainInfo, nullindex_t>;
+
   int d_asyncFlag;
 
   struct RecordsDB
@@ -327,6 +337,7 @@ private:
   shared_ptr<ttsig_t> d_ttsig;
   MDBDbi d_tnetworks;
   MDBDbi d_tviews;
+  shared_ptr<tdomain_extra_t> d_tdomains_extra; // may be unset if no split domain data
 
   shared_ptr<RecordsROTransaction> d_rotxn; // for lookup and list
   shared_ptr<RecordsRWTransaction> d_rwtxn; // for feedrecord within begin/aborttransaction
@@ -336,12 +347,14 @@ private:
   std::shared_ptr<RecordsROTransaction> getRecordsROTransaction(domainid_t id, const std::shared_ptr<LMDBBackend::RecordsRWTransaction>& rwtxn = nullptr);
   bool genChangeDomain(const ZoneName& domain, const std::function<void(DomainInfo&)>& func);
   bool genChangeDomain(domainid_t id, const std::function<void(DomainInfo&)>& func);
+  bool genChangeTransientDomain(domainid_t id, const std::function<void(DomainInfo&)>& func);
   static void deleteDomainRecords(RecordsRWTransaction& txn, const std::string& match, QType qtype = QType::ANY);
 
   bool findDomain(const ZoneName& domain, DomainInfo& info) const;
   bool findDomain(domainid_t domainid, DomainInfo& info) const;
   void consolidateDomainInfo(DomainInfo& info) const;
   void writeDomainInfo(const DomainInfo& info);
+  void writeTransientDomainInfo(const DomainInfo& info);
 
   void setLastCheckTime(domainid_t domain_id, time_t last_check);
 
@@ -362,14 +375,7 @@ private:
 
   string directBackendCmd_list(std::vector<string>& argv);
 
-  // Transient DomainInfo data, not necessarily synchronized with the
-  // database.
-  struct TransientDomainInfo
-  {
-    time_t last_check{};
-    uint32_t notified_serial{};
-  };
-  // Cache of DomainInfo notified_serial values
+  // Cache of TransientDomainInfo
   class TransientDomainInfoCache : public boost::noncopyable
   {
   public:
@@ -423,6 +429,7 @@ private:
   bool d_handle_dups;
   bool d_views;
   bool d_write_notification_update;
+  bool d_split_domains_table;
   DTime d_dtime; // used only for logging
   uint64_t d_mapsize_main;
   uint64_t d_mapsize_shards;