]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Naive sampling in the ring buffers
authorRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 5 Jan 2026 14:57:00 +0000 (15:57 +0100)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Fri, 23 Jan 2026 12:04:30 +0000 (13:04 +0100)
Signed-off-by: Remi Gacogne <remi.gacogne@powerdns.com>
pdns/dnsdistdist/dnsdist-configuration.hh
pdns/dnsdistdist/dnsdist-lua.cc
pdns/dnsdistdist/dnsdist-rings.cc
pdns/dnsdistdist/dnsdist-rings.hh
pdns/dnsdistdist/dnsdist-settings-definitions.yml
pdns/dnsdistdist/dnsdist.cc
pdns/dnsdistdist/docs/reference/config.rst
pdns/dnsdistdist/test-dnsdist-lua-ffi.cc
pdns/dnsdistdist/test-dnsdistdynblocks_hh.cc
pdns/dnsdistdist/test-dnsdistrings_cc.cc

index eefdcf308f53197d0dd95713ab30faea9b26cb22..91629b16dccaaf2bf437e5aaffe5153ee946c20d 100644 (file)
@@ -97,6 +97,7 @@ struct ImmutableConfiguration
   size_t d_ringsCapacity{10000};
   size_t d_ringsNumberOfShards{10};
   size_t d_ringsNbLockTries{5};
+  size_t d_ringsSamplingRate{0};
   uint32_t d_socketUDPSendBuffer{0};
   uint32_t d_socketUDPRecvBuffer{0};
   uint32_t d_hashPerturbation{0};
index 606c6e16635b3efea0a97f175180d6b8b5fe0db7..2903a95c6566d5d3b94581f63557206353ab64ac 100644 (file)
@@ -2052,6 +2052,9 @@ static void setupLuaConfig(LuaContext& luaCtx, bool client, bool configCheck)
         if (options.count("recordResponses") > 0) {
           config.d_ringsRecordResponses = boost::get<bool>(options.at("recordResponses"));
         }
+        if (options.count("samplingRate") > 0) {
+          config.d_ringsSamplingRate = boost::get<uint64_t>(options.at("samplingRate"));
+        }
       });
     }
     catch (const std::exception& exp) {
index a25f84cf8697ad2b02879325ed9c9e12dff464de..5dc98d7a9e846b781b5d9b41c98ef874a9f08965 100644 (file)
 
 #include "dnsdist-rings.hh"
 
-void Rings::init(size_t capacity, size_t numberOfShards, size_t nbLockRetries, bool recordQueries, bool recordResponses)
+void Rings::init(const RingsConfiguration& config)
 {
   if (d_initialized.exchange(true)) {
     throw std::runtime_error("Rings::init() should only be called once");
   }
 
-  d_capacity = capacity;
-  d_numberOfShards = numberOfShards;
-  d_nbLockTries = nbLockRetries;
-  d_recordQueries = recordQueries;
-  d_recordResponses = recordResponses;
+  d_capacity = config.capacity;
+  d_numberOfShards = config.numberOfShards;
+  d_nbLockTries = config.nbLockTries;
+  d_samplingRate = config.samplingRate;
+  d_recordQueries = config.recordQueries;
+  d_recordResponses = config.recordResponses;
   if (d_numberOfShards <= 1) {
     d_nbLockTries = 0;
   }
@@ -200,3 +201,12 @@ bool Rings::Response::isACacheHit() const
   }
   return hit;
 }
+
+bool Rings::shouldSkipDueToSampling()
+{
+  if (d_samplingRate == 0) {
+    return false;
+  }
+  auto counter = d_samplingCounter++;
+  return (counter % d_samplingRate) == 0;
+}
index 1aa68f89a4868790f5d9b7088dd6932a80652bd0..7cdee74f599f95bba75957cba7d65e02a8178734 100644 (file)
@@ -76,8 +76,18 @@ struct Rings
   std::unordered_map<int, vector<boost::variant<string, double>>> getTopBandwidth(unsigned int numentries);
   size_t numDistinctRequestors();
 
+  struct RingsConfiguration
+  {
+    size_t capacity{0};
+    size_t numberOfShards{1};
+    size_t nbLockTries{5};
+    size_t samplingRate{0};
+    bool recordQueries{true};
+    bool recordResponses{true};
+  };
+
   /* This function should only be called at configuration time before any query or response has been inserted */
-  void init(size_t capacity, size_t numberOfShards, size_t nbLockRetries = 5, bool recordQueries = true, bool recordResponses = true);
+  void init(const RingsConfiguration& config);
 
   size_t getNumberOfShards() const
   {
@@ -96,6 +106,9 @@ struct Rings
 
   void insertQuery(const struct timespec& when, const ComboAddress& requestor, const DNSName& name, uint16_t qtype, uint16_t size, const struct dnsheader& dh, dnsdist::Protocol protocol)
   {
+    if (shouldSkipDueToSampling()) {
+      return;
+    }
     auto ourName = DNSName(name);
 #if defined(DNSDIST_RINGS_WITH_MACADDRESS)
     dnsdist::MacAddress macaddress;
@@ -149,6 +162,9 @@ struct Rings
 
   void insertResponse(const struct timespec& when, const ComboAddress& requestor, const DNSName& name, uint16_t qtype, unsigned int usec, unsigned int size, const struct dnsheader& dh, const ComboAddress& backend, dnsdist::Protocol protocol)
   {
+    if (shouldSkipDueToSampling()) {
+      return;
+    }
     auto ourName = DNSName(name);
     for (size_t idx = 0; idx < d_nbLockTries; idx++) {
       auto& shard = getOneShard();
@@ -221,6 +237,11 @@ struct Rings
     return d_recordResponses;
   }
 
+  size_t getSamplingRate() const
+  {
+    return d_samplingRate;
+  }
+
   std::vector<std::unique_ptr<Shard>> d_shards;
   pdns::stat_t d_blockingQueryInserts{0};
   pdns::stat_t d_blockingResponseInserts{0};
@@ -264,6 +285,8 @@ private:
     return wasFull;
   }
 
+  bool shouldSkipDueToSampling();
+
   static constexpr bool s_keepLockingStats{false};
 
   std::atomic<size_t> d_nbQueryEntries{0};
@@ -274,6 +297,8 @@ private:
   size_t d_capacity{10000};
   size_t d_numberOfShards{10};
   size_t d_nbLockTries{5};
+  size_t d_samplingRate{0};
+  std::atomic<size_t> d_samplingCounter{0};
   bool d_recordQueries{true};
   bool d_recordResponses{true};
 };
index fc65b7775150b901d850dfd28cc593c5a090b7a1..0a85f823492c543969fe1e3e745e3b313ecbf748 100644 (file)
@@ -754,6 +754,13 @@ ring_buffers:
       lua-name: "setRingBuffersOptions"
       internal-field-name: "d_ringsRecordResponses"
       runtime-configurable: false
+    - name: "sampling_rate"
+      type: "u64"
+      default: 0
+      description: "Set a sampling rate ``S`` so that only 1 out of ``S`` queries and responses are inserted into the rings, to keep a longer history without consuming too much memory while also being able to process it quickly. Default is 0 which means there is no sampling and all entries are inserted."
+      lua-name: "setRingBuffersOptions"
+      internal-field-name: "d_ringsSamplingRate"
+      runtime-configurable: false
 
 incoming_tls_certificate_key_pair:
   description: "A pair of TLS certificate and key, with an optional associated password"
index f2092264d86e79a33be407cf0b4ad7f677761506..5113124904a1e87f516163259e71d26db190f84f 100644 (file)
@@ -3785,8 +3785,16 @@ int main(int argc, char** argv)
 
     {
       const auto& config = dnsdist::configuration::getImmutableConfiguration();
-      g_rings.init(config.d_ringsCapacity, config.d_ringsNumberOfShards, config.d_ringsNbLockTries, config.d_ringsRecordQueries, config.d_ringsRecordResponses);
-    }
+      Rings::RingsConfiguration ringsConfig {
+        .capacity = config.d_ringsCapacity,
+        .numberOfShards = config.d_ringsNumberOfShards,
+        .nbLockTries = config.d_ringsNbLockTries,
+        .samplingRate = config.d_ringsSamplingRate,
+        .recordQueries = config.d_ringsRecordQueries,
+        .recordResponses = config.d_ringsRecordResponses,
+      };
+      g_rings.init(ringsConfig);
+     }
 
     for (const auto& frontend : dnsdist::getFrontends()) {
       setUpLocalBind(*frontend, setupLogger);
index b2bf9e5ebd032cbf874b8668eed42a2163dcf871..ea81e1fb24b59e1e3f2b303792bce662bbd46fea 100644 (file)
@@ -625,6 +625,9 @@ Ringbuffers
 
   .. versionadded:: 1.8.0
 
+  .. versionchanged:: 2.1.0
+    ``samplingRate`` option added.
+
   Set the rings buffers configuration
 
   :param table options: A table with key: value pairs with options.
@@ -632,6 +635,7 @@ Ringbuffers
   Options:
 
   * ``lockRetries``: int - Set the number of shards to attempt to lock without blocking before giving up and simply blocking while waiting for the next shard to be available. Default to 5 if there is more than one shard, 0 otherwise
+  * ``samplingRate``: int - Set a sampling rate ``S`` so that only 1 out of ``S`` queries and responses are inserted into the rings, to keep a longer history without consuming too much memory while also being able to process it quickly. Default is 0 which means there is no sampling and all entries are inserted
   * ``recordQueries``: boolean - Whether to record queries in the ring buffers. Default is true. Note that :func:`grepq`, several top* commands (:func:`topClients`, :func:`topQueries`, ...) and the :doc:`Dynamic Blocks <../guides/dynblocks>` require this to be enabled.
   * ``recordResponses``: boolean - Whether to record responses in the ring buffers. Default is true. Note that :func:`grepq`, several top* commands (:func:`topResponses`, :func:`topSlow`, ...) and the :doc:`Dynamic Blocks <../guides/dynblocks>` require this to be enabled.
 
index b7f110a40e5ead84c91d696e4316e6adcbdf0056..6ddbe9232b88feead6ea885d6338c332709b92dc 100644 (file)
@@ -838,7 +838,11 @@ BOOST_AUTO_TEST_CASE(test_RingBuffers)
   gettime(&now);
 
   g_rings.reset();
-  g_rings.init(10000, 10);
+  Rings::RingsConfiguration config {
+    .capacity = 10000U,
+    .numberOfShards = 10U,
+  };
+  g_rings.init(config);
   BOOST_CHECK_EQUAL(g_rings.getNumberOfQueryEntries(), 0U);
 
   g_rings.insertQuery(now, requestor1, qname, qtype, size, dh, protocol);
index 296bc9a81b25340b3cb45a1b2ba125e158edc9e2..42123b5ea631a2689b169f56f47652283fe85f32 100644 (file)
@@ -24,7 +24,11 @@ struct TestFixture
   TestFixture()
   {
     g_rings.reset();
-    g_rings.init(10000, 10);
+    Rings::RingsConfiguration config {
+      .capacity = 10000U,
+      .numberOfShards = 10U,
+    };
+    g_rings.init(config);
   }
   ~TestFixture()
   {
@@ -428,7 +432,10 @@ BOOST_FIXTURE_TEST_CASE(test_DynBlockRulesGroup_QueryRate_responses, TestFixture
 
   /* 100k entries, one shard */
   g_rings.reset();
-  g_rings.init(1000000, 1);
+  Rings::RingsConfiguration config {
+    .capacity = 1000000U,
+  };
+  g_rings.init(config);
 
   size_t numberOfSeconds = 10;
   size_t blockDuration = 60;
@@ -1228,7 +1235,10 @@ BOOST_FIXTURE_TEST_CASE(test_DynBlockRulesMetricsCache_GetTopN, TestFixture) {
 
   g_rings.reset();
   /* 10M entries, only one shard */
-  g_rings.init(10000000, 1);
+  Rings::RingsConfiguration config {
+    .capacity = 10000000U,
+  };
+  g_rings.init(config);
 
   {
     DynBlockRulesGroup dbrg;
index 4dc7ae0dab84dcc86093ab12f0cf94ee95ff2b98..9695aa68cc162ca77b0b8cac7b2ed2389b2cfa85 100644 (file)
@@ -50,7 +50,12 @@ static bool checkResponse(const Rings::Response& entry, const DNSName& qname, ui
 static void test_ring(size_t maxEntries, size_t numberOfShards, size_t nbLockTries)
 {
   Rings rings;
-  rings.init(maxEntries, numberOfShards, nbLockTries);
+  Rings::RingsConfiguration config {
+    .capacity = maxEntries,
+    .numberOfShards = numberOfShards,
+    .nbLockTries = nbLockTries,
+  };
+  rings.init(config);
   size_t entriesPerShard = maxEntries / numberOfShards;
 
   BOOST_CHECK_EQUAL(rings.getNumberOfShards(), numberOfShards);
@@ -223,7 +228,12 @@ BOOST_AUTO_TEST_CASE(test_Rings_Threaded) {
   dnsdist::Protocol outgoingProtocol = dnsdist::Protocol::DoUDP;
 
   Rings rings;
-  rings.init(numberOfEntries, numberOfShards, lockAttempts, true);
+  Rings::RingsConfiguration config {
+    .capacity = numberOfEntries,
+    .numberOfShards = numberOfShards,
+    .nbLockTries = lockAttempts,
+  };
+  rings.init(config);
 #if defined(DNSDIST_RINGS_WITH_MACADDRESS)
   Rings::Query query({requestor, qname, now, dh, size, qtype, protocol, dnsdist::MacAddress(), false});
 #else