]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Replace linked lists with the hashtables to hold fetch contexts
authorOndřej Surý <ondrej@isc.org>
Wed, 15 Jan 2025 12:02:20 +0000 (13:02 +0100)
committerOndřej Surý <ondrej@isc.org>
Wed, 22 Jan 2025 14:06:04 +0000 (15:06 +0100)
When the recursive-clients value is too large, the linked lists holding
the fetch contexts can also grow large and since the algorithm to merge
outgoing queries is quadratic, named can get slow.

Replace the linked list with hashtable for faster lookups.  This also
allows us to reduce the number of tasks (buckets) in the resolver.

bin/named/server.c
lib/dns/client.c
lib/dns/resolver.c

index 8ce865f2c7b8fe6e7674f1c89a50f97bc7ae4078..bd3f5a8dca46ead16f119a394bca5933a6dd2178 100644 (file)
 #define SIZE_AS_PERCENT ((size_t)-2)
 #endif /* ifndef SIZE_AS_PERCENT */
 
-#define RESOLVER_NTASKS 523
-
 /* RFC7828 defines timeout as 16-bit value specified in units of 100
  * milliseconds, so the maximum and minimum advertised and keepalive
  * timeouts are capped by the data type (it's ~109 minutes)
@@ -503,6 +501,104 @@ static isc_result_t
 nzf_append(dns_view_t *view, const cfg_obj_t *zconfig);
 #endif /* ifdef HAVE_LMDB */
 
+static const uint32_t primes[] = {
+       2,    3,    5,    7,    11,   13,   17,   19,   23,   29,   31,   37,
+       41,   43,   47,   53,   59,   61,   67,   71,   73,   79,   83,   89,
+       97,   101,  103,  107,  109,  113,  127,  131,  137,  139,  149,  151,
+       157,  163,  167,  173,  179,  181,  191,  193,  197,  199,  211,  223,
+       227,  229,  233,  239,  241,  251,  257,  263,  269,  271,  277,  281,
+       283,  293,  307,  311,  313,  317,  331,  337,  347,  349,  353,  359,
+       367,  373,  379,  383,  389,  397,  401,  409,  419,  421,  431,  433,
+       439,  443,  449,  457,  461,  463,  467,  479,  487,  491,  499,  503,
+       509,  521,  523,  541,  547,  557,  563,  569,  571,  577,  587,  593,
+       599,  601,  607,  613,  617,  619,  631,  641,  643,  647,  653,  659,
+       661,  673,  677,  683,  691,  701,  709,  719,  727,  733,  739,  743,
+       751,  757,  761,  769,  773,  787,  797,  809,  811,  821,  823,  827,
+       829,  839,  853,  857,  859,  863,  877,  881,  883,  887,  907,  911,
+       919,  929,  937,  941,  947,  953,  967,  971,  977,  983,  991,  997,
+       1009, 1013, 1019, 1021, 1031, 1033, 1039, 1049, 1051, 1061, 1063, 1069,
+       1087, 1091, 1093, 1097, 1103, 1109, 1117, 1123, 1129, 1151, 1153, 1163,
+       1171, 1181, 1187, 1193, 1201, 1213, 1217, 1223, 1229, 1231, 1237, 1249,
+       1259, 1277, 1279, 1283, 1289, 1291, 1297, 1301, 1303, 1307, 1319, 1321,
+       1327, 1361, 1367, 1373, 1381, 1399, 1409, 1423, 1427, 1429, 1433, 1439,
+       1447, 1451, 1453, 1459, 1471, 1481, 1483, 1487, 1489, 1493, 1499, 1511,
+       1523, 1531, 1543, 1549, 1553, 1559, 1567, 1571, 1579, 1583, 1597, 1601,
+       1607, 1609, 1613, 1619, 1621, 1627, 1637, 1657, 1663, 1667, 1669, 1693,
+       1697, 1699, 1709, 1721, 1723, 1733, 1741, 1747, 1753, 1759, 1777, 1783,
+       1787, 1789, 1801, 1811, 1823, 1831, 1847, 1861, 1867, 1871, 1873, 1877,
+       1879, 1889, 1901, 1907, 1913, 1931, 1933, 1949, 1951, 1973, 1979, 1987,
+       1993, 1997, 1999, 2003, 2011, 2017, 2027, 2029, 2039, 2053, 2063, 2069,
+       2081, 2083, 2087, 2089, 2099, 2111, 2113, 2129, 2131, 2137, 2141, 2143,
+       2153, 2161, 2179, 2203, 2207, 2213, 2221, 2237, 2239, 2243, 2251, 2267,
+       2269, 2273, 2281, 2287, 2293, 2297, 2309, 2311, 2333, 2339, 2341, 2347,
+       2351, 2357, 2371, 2377, 2381, 2383, 2389, 2393, 2399, 2411, 2417, 2423,
+       2437, 2441, 2447, 2459, 2467, 2473, 2477, 2503, 2521, 2531, 2539, 2543,
+       2549, 2551, 2557, 2579, 2591, 2593, 2609, 2617, 2621, 2633, 2647, 2657,
+       2659, 2663, 2671, 2677, 2683, 2687, 2689, 2693, 2699, 2707, 2711, 2713,
+       2719, 2729, 2731, 2741, 2749, 2753, 2767, 2777, 2789, 2791, 2797, 2801,
+       2803, 2819, 2833, 2837, 2843, 2851, 2857, 2861, 2879, 2887, 2897, 2903,
+       2909, 2917, 2927, 2939, 2953, 2957, 2963, 2969, 2971, 2999, 3001, 3011,
+       3019, 3023, 3037, 3041, 3049, 3061, 3067, 3079, 3083, 3089, 3109, 3119,
+       3121, 3137, 3163, 3167, 3169, 3181, 3187, 3191, 3203, 3209, 3217, 3221,
+       3229, 3251, 3253, 3257, 3259, 3271, 3299, 3301, 3307, 3313, 3319, 3323,
+       3329, 3331, 3343, 3347, 3359, 3361, 3371, 3373, 3389, 3391, 3407, 3413,
+       3433, 3449, 3457, 3461, 3463, 3467, 3469, 3491, 3499, 3511, 3517, 3527,
+       3529, 3533, 3539, 3541, 3547, 3557, 3559, 3571, 3581, 3583, 3593, 3607,
+       3613, 3617, 3623, 3631, 3637, 3643, 3659, 3671, 3673, 3677, 3691, 3697,
+       3701, 3709, 3719, 3727, 3733, 3739, 3761, 3767, 3769, 3779, 3793, 3797,
+       3803, 3821, 3823, 3833, 3847, 3851, 3853, 3863, 3877, 3881, 3889, 3907,
+       3911, 3917, 3919, 3923, 3929, 3931, 3943, 3947, 3967, 3989, 4001, 4003,
+       4007, 4013, 4019, 4021, 4027, 4049, 4051, 4057, 4073, 4079, 4091, 4093,
+       4099, 4111, 4127, 4129, 4133, 4139, 4153, 4157, 4159, 4177, 4201, 4211,
+       4217, 4219, 4229, 4231, 4241, 4243, 4253, 4259, 4261, 4271, 4273, 4283,
+       4289, 4297, 4327, 4337, 4339, 4349, 4357, 4363, 4373, 4391, 4397, 4409,
+       4421, 4423, 4441, 4447, 4451, 4457, 4463, 4481, 4483, 4493, 4507, 4513,
+       4517, 4519, 4523, 4547, 4549, 4561, 4567, 4583, 4591, 4597, 4603, 4621,
+       4637, 4639, 4643, 4649, 4651, 4657, 4663, 4673, 4679, 4691, 4703, 4721,
+       4723, 4729, 4733, 4751, 4759, 4783, 4787, 4789, 4793, 4799, 4801, 4813,
+       4817, 4831, 4861, 4871, 4877, 4889, 4903, 4909, 4919, 4931, 4933, 4937,
+       4943, 4951, 4957, 4967, 4969, 4973, 4987, 4993, 4999, 5003, 5009, 5011,
+       5021, 5023, 5039, 5051, 5059, 5077, 5081, 5087, 5099, 5101, 5107, 5113,
+       5119, 5147, 5153, 5167, 5171, 5179, 5189, 5197, 5209, 5227, 5231, 5233,
+       5237, 5261, 5273, 5279, 5281, 5297, 5303, 5309, 5323, 5333, 5347, 5351,
+       5381, 5387, 5393, 5399, 5407, 5413, 5417, 5419, 5431, 5437, 5441, 5443,
+       5449, 5471, 5477, 5479, 5483, 5501, 5503, 5507, 5519, 5521, 5527, 5531,
+       5557, 5563, 5569, 5573, 5581, 5591, 5623, 5639, 5641, 5647, 5651, 5653,
+       5657, 5659, 5669, 5683, 5689, 5693, 5701, 5711, 5717, 5737, 5741, 5743,
+       5749, 5779, 5783, 5791, 5801, 5807, 5813, 5821, 5827, 5839, 5843, 5849,
+       5851, 5857, 5861, 5867, 5869, 5879, 5881, 5897, 5903, 5923, 5927, 5939,
+       5953, 5981, 5987, 6007, 6011, 6029, 6037, 6043, 6047, 6053, 6067, 6073,
+       6079, 6089, 6091, 6101, 6113, 6121, 6131, 6133, 6143, 6151, 6163, 6173,
+       6197, 6199, 6203, 6211, 6217, 6221, 6229, 6247, 6257, 6263, 6269, 6271,
+       6277, 6287, 6299, 6301, 6311, 6317, 6323, 6329, 6337, 6343, 6353, 6359,
+       6361, 6367, 6373, 6379, 6389, 6397, 6421, 6427, 6449, 6451, 6469, 6473,
+       6481, 6491, 6521, 6529, 6547, 6551, 6553, 6563, 6569, 6571, 6577, 6581,
+       6599, 6607, 6619, 6637, 6653, 6659, 6661, 6673, 6679, 6689, 6691, 6701,
+       6703, 6709, 6719, 6733, 6737, 6761, 6763, 6779, 6781, 6791, 6793, 6803,
+       6823, 6827, 6829, 6833, 6841, 6857, 6863, 6869, 6871, 6883, 6899, 6907,
+       6911, 6917, 6947, 6949, 6959, 6961, 6967, 6971, 6977, 6983, 6991, 6997,
+       7001, 7013, 7019, 7027, 7039, 7043, 7057, 7069, 7079, 7103, 7109, 7121,
+       7127, 7129, 7151, 7159, 7177, 7187, 7193, 7207, 7211, 7213, 7219, 7229,
+       7237, 7243, 7247, 7253, 7283, 7297, 7307, 7309, 7321, 7331, 7333, 7349,
+       7351, 7369, 7393, 7411, 7417, 7433, 7451, 7457, 7459, 7477, 7481, 7487,
+       7489, 7499, 7507, 7517, 7523, 7529, 7537, 7541, 7547, 7549, 7559, 7561,
+       7573, 7577, 7583, 7589, 7591, 7603, 7607, 7621, 7639, 7643, 7649, 7669,
+       7673, 7681, 7687, 7691, 7699, 7703, 7717, 7723, 7727, 7741, 7753, 7757,
+       7759, 7789, 7793, 7817, 7823, 7829, 7841, 7853, 7867, 7873, 7877, 7879,
+       7883, 7901, 7907, 7919,
+};
+
+static uint32_t
+closest_prime(uint32_t n) {
+       for (size_t i = 0; i < ARRAY_SIZE(primes); i++) {
+               if (primes[i] >= n) {
+                       return primes[i];
+               }
+       }
+
+       return primes[ARRAY_SIZE(primes) - 1];
+}
+
 /*%
  * Configure a single view ACL at '*aclp'.  Get its configuration from
  * 'vconfig' (for per-view configuration) and maybe from 'config'
@@ -4221,7 +4317,7 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, cfg_obj_t *config,
        named_cache_t *nsc;
        bool zero_no_soattl;
        dns_acl_t *clients = NULL, *mapped = NULL, *excluded = NULL;
-       unsigned int query_timeout, ndisp;
+       unsigned int query_timeout, ndisp, ntasks;
        bool old_rpz_ok = false;
        dns_dyndbctx_t *dctx = NULL;
        unsigned int resolver_param;
@@ -4880,10 +4976,11 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, cfg_obj_t *config,
        }
        dns_view_setresquerystats(view, resquerystats);
 
+       ntasks = closest_prime(2 * named_g_cpus);
        ndisp = 4 * ISC_MIN(named_g_udpdisp, MAX_UDP_DISPATCH);
-       CHECK(dns_view_createresolver(view, named_g_taskmgr, RESOLVER_NTASKS,
-                                     ndisp, named_g_netmgr, named_g_timermgr,
-                                     resopts, named_g_dispatchmgr, dispatch4,
+       CHECK(dns_view_createresolver(view, named_g_taskmgr, ntasks, ndisp,
+                                     named_g_netmgr, named_g_timermgr, resopts,
+                                     named_g_dispatchmgr, dispatch4,
                                      dispatch6));
 
        /*
index ed6bf0e0cbd1f3a9edfe43ff055f0aaea683b410..bde6ec61a3ea74f84770a9d94af06e6dccfd545c 100644 (file)
@@ -60,8 +60,6 @@
 #define UCTX_MAGIC    ISC_MAGIC('U', 'c', 't', 'x')
 #define UCTX_VALID(c) ISC_MAGIC_VALID(c, UCTX_MAGIC)
 
-#define RESOLVER_NTASKS 523
-
 #define CHECK(r)                             \
        do {                                 \
                result = (r);                \
@@ -328,9 +326,8 @@ dns_client_create(isc_mem_t *mctx, isc_appctx_t *actx, isc_taskmgr_t *taskmgr,
        isc_refcount_init(&client->references, 1);
 
        /* Create the default view for class IN */
-       result = createview(mctx, dns_rdataclass_in, taskmgr, RESOLVER_NTASKS,
-                           nm, timermgr, client->dispatchmgr, dispatchv4,
-                           dispatchv6, &view);
+       result = createview(mctx, dns_rdataclass_in, taskmgr, 1, nm, timermgr,
+                           client->dispatchmgr, dispatchv4, dispatchv6, &view);
        if (result != ISC_R_SUCCESS) {
                goto cleanup_references;
        }
index e612eb7bb3e96b17e6cfa0fc13b699d0d2ee8770..2178a17b5e9be5d288a24029ee9101a90e9b3b76 100644 (file)
@@ -19,6 +19,8 @@
 
 #include <isc/atomic.h>
 #include <isc/counter.h>
+#include <isc/hash.h>
+#include <isc/ht.h>
 #include <isc/log.h>
 #include <isc/print.h>
 #include <isc/random.h>
@@ -327,6 +329,7 @@ struct fetchctx {
        unsigned int options;
        unsigned int bucketnum;
        unsigned int dbucketnum;
+       bool hashed;
        char *info;
        isc_mem_t *mctx;
        isc_stdtime_t now;
@@ -341,7 +344,6 @@ struct fetchctx {
        bool cloned;
        bool spilled;
        isc_event_t control_event;
-       ISC_LINK(struct fetchctx) link;
        ISC_LIST(dns_fetchevent_t) events;
 
        /*% Locked by task event serialization. */
@@ -514,10 +516,14 @@ struct dns_fetch {
 #define DNS_FETCH_MAGIC               ISC_MAGIC('F', 't', 'c', 'h')
 #define DNS_FETCH_VALID(fetch) ISC_MAGIC_VALID(fetch, DNS_FETCH_MAGIC)
 
+#define DNS_FCTX_KEYSIZE \
+       (DNS_NAME_MAXWIRE + sizeof(dns_rdatatype_t) + sizeof(unsigned int))
+
 typedef struct fctxbucket {
        isc_task_t *task;
        isc_mutex_t lock;
-       ISC_LIST(fetchctx_t) fctxs;
+       isc_refcount_t nfctxs;
+       isc_ht_t *fctxs;
        atomic_bool exiting;
 } fctxbucket_t;
 
@@ -529,12 +535,11 @@ struct fctxcount {
        uint32_t allowed;
        uint32_t dropped;
        isc_stdtime_t logged;
-       ISC_LINK(fctxcount_t) link;
 };
 
 typedef struct zonebucket {
        isc_mutex_t lock;
-       ISC_LIST(fctxcount_t) list;
+       isc_ht_t *counters;
 } zonebucket_t;
 
 typedef struct alternate {
@@ -603,9 +608,6 @@ struct dns_resolver {
 
        /* Locked by primelock. */
        dns_fetch_t *primefetch;
-
-       /* Atomic. */
-       atomic_uint_fast32_t nfctx;
 };
 
 #define RES_MAGIC          ISC_MAGIC('R', 'e', 's', '!')
@@ -1623,6 +1625,25 @@ fcount_logspill(fetchctx_t *fctx, fctxcount_t *counter, bool final) {
        counter->logged = now;
 }
 
+static void
+fcount_makekey(const dns_name_t *name, uint8_t *key, size_t *keysizep) {
+       REQUIRE(*keysizep >= name->length);
+
+       size_t keysize = 0;
+       isc_buffer_t buffer;
+       dns_name_t downname;
+       isc_result_t result;
+
+       DNS_NAME_INIT(&downname, NULL);
+       isc_buffer_init(&buffer, key, *keysizep);
+
+       result = dns_name_downcase(name, &downname, &buffer);
+       RUNTIME_CHECK(result == ISC_R_SUCCESS);
+       keysize += isc_buffer_usedlength(&buffer);
+
+       *keysizep = keysize;
+}
+
 static isc_result_t
 fcount_incr(fetchctx_t *fctx, bool force) {
        isc_result_t result = ISC_R_SUCCESS;
@@ -1630,6 +1651,9 @@ fcount_incr(fetchctx_t *fctx, bool force) {
        fctxcount_t *counter = NULL;
        uint32_t hashval;
        uint32_t dbucketnum;
+       uint8_t key[DNS_NAME_MAXWIRE];
+       size_t keysize = sizeof(key);
+       uint_fast32_t spill;
 
        REQUIRE(fctx != NULL);
        REQUIRE(fctx->res != NULL);
@@ -1641,15 +1665,14 @@ fcount_incr(fetchctx_t *fctx, bool force) {
        dbucket = &fctx->res->dbuckets[dbucketnum];
 
        LOCK(&dbucket->lock);
-       for (counter = ISC_LIST_HEAD(dbucket->list); counter != NULL;
-            counter = ISC_LIST_NEXT(counter, link))
-       {
-               if (dns_name_equal(counter->domain, fctx->domain)) {
-                       break;
-               }
-       }
 
-       if (counter == NULL) {
+       fcount_makekey(fctx->domain, key, &keysize);
+
+       result = isc_ht_find(dbucket->counters, key, keysize,
+                            (void **)&counter);
+
+       switch (result) {
+       case ISC_R_NOTFOUND:
                counter = isc_mem_get(fctx->res->mctx, sizeof(*counter));
                *counter = (fctxcount_t){
                        .count = 1,
@@ -1657,11 +1680,11 @@ fcount_incr(fetchctx_t *fctx, bool force) {
                };
 
                counter->domain = dns_fixedname_initname(&counter->dfname);
-               ISC_LINK_INIT(counter, link);
                dns_name_copy(fctx->domain, counter->domain);
-               ISC_LIST_APPEND(dbucket->list, counter, link);
-       } else {
-               uint_fast32_t spill = atomic_load_acquire(&fctx->res->zspill);
+               result = isc_ht_add(dbucket->counters, key, keysize, counter);
+               break;
+       case ISC_R_SUCCESS:
+               spill = atomic_load_acquire(&fctx->res->zspill);
                if (!force && spill != 0 && counter->count >= spill) {
                        counter->dropped++;
                        fcount_logspill(fctx, counter, false);
@@ -1670,6 +1693,9 @@ fcount_incr(fetchctx_t *fctx, bool force) {
                        counter->count++;
                        counter->allowed++;
                }
+               break;
+       default:
+               UNREACHABLE();
        }
        UNLOCK(&dbucket->lock);
 
@@ -1684,6 +1710,9 @@ static void
 fcount_decr(fetchctx_t *fctx) {
        zonebucket_t *dbucket = NULL;
        fctxcount_t *counter = NULL;
+       uint8_t key[DNS_NAME_MAXWIRE];
+       size_t keysize = sizeof(key);
+       isc_result_t result;
 
        REQUIRE(fctx != NULL);
 
@@ -1694,24 +1723,22 @@ fcount_decr(fetchctx_t *fctx) {
        dbucket = &fctx->res->dbuckets[fctx->dbucketnum];
 
        LOCK(&dbucket->lock);
-       for (counter = ISC_LIST_HEAD(dbucket->list); counter != NULL;
-            counter = ISC_LIST_NEXT(counter, link))
-       {
-               if (dns_name_equal(counter->domain, fctx->domain)) {
-                       break;
-               }
-       }
 
-       if (counter != NULL) {
-               INSIST(counter->count != 0);
-               counter->count--;
-               fctx->dbucketnum = RES_NOBUCKET;
+       fcount_makekey(fctx->domain, key, &keysize);
 
-               if (counter->count == 0) {
-                       fcount_logspill(fctx, counter, true);
-                       ISC_LIST_UNLINK(dbucket->list, counter, link);
-                       isc_mem_put(fctx->res->mctx, counter, sizeof(*counter));
-               }
+       result = isc_ht_find(dbucket->counters, key, keysize,
+                            (void **)&counter);
+       RUNTIME_CHECK(result == ISC_R_SUCCESS);
+
+       INSIST(counter->count != 0);
+       counter->count--;
+       fctx->dbucketnum = RES_NOBUCKET;
+
+       if (counter->count == 0) {
+               fcount_logspill(fctx, counter, true);
+               result = isc_ht_delete(dbucket->counters, key, keysize);
+               RUNTIME_CHECK(result == ISC_R_SUCCESS);
+               isc_mem_put(fctx->res->mctx, counter, sizeof(*counter));
        }
 
        UNLOCK(&dbucket->lock);
@@ -1830,6 +1857,73 @@ fctx_sendevents(fetchctx_t *fctx, isc_result_t result, int line) {
        }
 }
 
+static void
+fctx_makekey(const dns_name_t *name, dns_rdatatype_t type, unsigned int options,
+            uint8_t *key, size_t *keysizep) {
+       REQUIRE(*keysizep >= name->length + sizeof(type) + sizeof(options));
+
+       size_t keysize = 0;
+       isc_buffer_t buffer;
+       dns_name_t downname;
+       isc_result_t result;
+
+       DNS_NAME_INIT(&downname, NULL);
+       isc_buffer_init(&buffer, key, *keysizep);
+
+       result = dns_name_downcase(name, &downname, &buffer);
+       RUNTIME_CHECK(result == ISC_R_SUCCESS);
+       keysize += isc_buffer_usedlength(&buffer);
+
+       memmove(&key[keysize], &type, sizeof(type));
+       keysize += sizeof(type);
+
+       memmove(&key[keysize], &options, sizeof(options));
+       keysize += sizeof(options);
+
+       *keysizep = keysize;
+}
+
+static void
+fctx_unhash(fetchctx_t *fctx) {
+       dns_resolver_t *res = fctx->res;
+       unsigned int bucketnum = fctx->bucketnum;
+       uint8_t key[DNS_FCTX_KEYSIZE] = { 0 };
+       size_t keysize = sizeof(key);
+       isc_result_t result;
+
+       if (!fctx->hashed) {
+               return;
+       }
+
+       fctx_makekey(fctx->name, fctx->type, fctx->options, key, &keysize);
+
+       result = isc_ht_delete(res->buckets[bucketnum].fctxs, key, keysize);
+       RUNTIME_CHECK(result == ISC_R_SUCCESS);
+
+       fctx->hashed = false;
+}
+
+static void
+fctx_hash(fetchctx_t *fctx) {
+       dns_resolver_t *res = fctx->res;
+       unsigned int bucketnum = fctx->bucketnum;
+       uint8_t key[DNS_FCTX_KEYSIZE] = { 0 };
+       size_t keysize = sizeof(key);
+       isc_result_t result;
+       bool unshared = ((fctx->options & DNS_FETCHOPT_UNSHARED) != 0);
+
+       if (unshared) {
+               return;
+       }
+
+       fctx_makekey(fctx->name, fctx->type, fctx->options, key, &keysize);
+
+       result = isc_ht_add(res->buckets[bucketnum].fctxs, key, keysize, fctx);
+       RUNTIME_CHECK(result == ISC_R_SUCCESS);
+
+       fctx->hashed = true;
+}
+
 static void
 fctx__done_detach(fetchctx_t **fctxp, isc_result_t result, const char *file,
                  unsigned int line, const char *func) {
@@ -1857,6 +1951,7 @@ fctx__done_detach(fetchctx_t **fctxp, isc_result_t result, const char *file,
        LOCK(&res->buckets[fctx->bucketnum].lock);
        INSIST(fctx->state != fetchstate_done);
        fctx->state = fetchstate_done;
+       fctx_unhash(fctx);
        UNLOCK(&res->buckets[fctx->bucketnum].lock);
 
        if (result == ISC_R_SUCCESS) {
@@ -2089,6 +2184,10 @@ resquery_timeout(resquery_t *query) {
                event->result = ISC_R_TIMEDOUT;
                isc_task_sendanddetach(&sender, ISC_EVENT_PTR(&event));
        }
+
+       if (ISC_LIST_EMPTY(fctx->events)) {
+               fctx_unhash(fctx);
+       }
        UNLOCK(&fctx->res->buckets[fctx->bucketnum].lock);
 
        /*
@@ -4481,7 +4580,6 @@ fctx_destroy(fetchctx_t *fctx, bool exiting) {
        struct tried *tried = NULL;
        unsigned int bucketnum;
        bool bucket_empty = false;
-       uint_fast32_t nfctx;
 
        REQUIRE(VALID_FCTX(fctx));
        REQUIRE(ISC_LIST_EMPTY(fctx->events));
@@ -4501,18 +4599,19 @@ fctx_destroy(fetchctx_t *fctx, bool exiting) {
        LOCK(&res->buckets[bucketnum].lock);
        REQUIRE(fctx->state != fetchstate_active);
 
-       ISC_LIST_UNLINK(res->buckets[bucketnum].fctxs, fctx, link);
-
-       nfctx = atomic_fetch_sub_release(&res->nfctx, 1);
-       INSIST(nfctx > 0);
+       fctx_unhash(fctx);
 
-       dec_stats(res, dns_resstatscounter_nfetch);
+       INSIST(res->buckets[bucketnum].nfctxs > 0);
+       res->buckets[bucketnum].nfctxs--;
 
        if (atomic_load_acquire(&res->buckets[bucketnum].exiting) &&
-           ISC_LIST_EMPTY(res->buckets[bucketnum].fctxs))
+           res->buckets[bucketnum].nfctxs == 0)
        {
                bucket_empty = true;
        }
+
+       dec_stats(res, dns_resstatscounter_nfetch);
+
        UNLOCK(&res->buckets[bucketnum].lock);
 
        if (bucket_empty && exiting &&
@@ -4654,6 +4753,7 @@ fctx_doshutdown(isc_task_t *task, isc_event_t *event) {
 
        if (fctx->state == fetchstate_active) {
                fctx->state = fetchstate_done;
+               fctx_unhash(fctx);
 
                fctx_sendevents(fctx, ISC_R_CANCELED, __LINE__);
 
@@ -4828,7 +4928,6 @@ fctx_create(dns_resolver_t *res, isc_task_t *task, const dns_name_t *name,
        isc_interval_t interval;
        unsigned int findoptions = 0;
        char buf[DNS_NAME_FORMATSIZE + DNS_RDATATYPE_FORMATSIZE + 1];
-       uint_fast32_t nfctx;
        size_t p;
 
        /*
@@ -5110,7 +5209,6 @@ fctx_create(dns_resolver_t *res, isc_task_t *task, const dns_name_t *name,
        isc_mem_attach(res->mctx, &fctx->mctx);
 
        ISC_LIST_INIT(fctx->events);
-       ISC_LINK_INIT(fctx, link);
        fctx->magic = FCTX_MAGIC;
 
        /*
@@ -5124,10 +5222,9 @@ fctx_create(dns_resolver_t *res, isc_task_t *task, const dns_name_t *name,
                fctx_minimize_qname(fctx);
        }
 
-       ISC_LIST_APPEND(res->buckets[bucketnum].fctxs, fctx, link);
+       fctx_hash(fctx);
 
-       nfctx = atomic_fetch_add_relaxed(&res->nfctx, 1);
-       INSIST(nfctx < UINT32_MAX);
+       res->buckets[bucketnum].nfctxs++;
 
        inc_stats(res, dns_resstatscounter_nfetch);
 
@@ -5334,6 +5431,7 @@ clone_results(fetchctx_t *fctx) {
         * Caller must be holding the appropriate lock.
         */
 
+       fctx_unhash(fctx);
        fctx->cloned = true;
 
        for (event = ISC_LIST_HEAD(fctx->events); event != NULL;
@@ -10356,12 +10454,12 @@ destroy(dns_resolver_t *res) {
 
        RTRACE("destroy");
 
-       REQUIRE(atomic_load_acquire(&res->nfctx) == 0);
-
        isc_mutex_destroy(&res->primelock);
        isc_mutex_destroy(&res->lock);
        for (i = 0; i < res->nbuckets; i++) {
-               INSIST(ISC_LIST_EMPTY(res->buckets[i].fctxs));
+               INSIST(res->buckets[i].nfctxs == 0);
+               INSIST(isc_ht_count(res->buckets[i].fctxs) == 0);
+               isc_ht_destroy(&res->buckets[i].fctxs);
                isc_task_shutdown(res->buckets[i].task);
                isc_task_detach(&res->buckets[i].task);
                isc_mutex_destroy(&res->buckets[i].lock);
@@ -10369,7 +10467,8 @@ destroy(dns_resolver_t *res) {
        isc_mem_put(res->mctx, res->buckets,
                    res->nbuckets * sizeof(fctxbucket_t));
        for (i = 0; i < HASHSIZE(res->dhashbits); i++) {
-               INSIST(ISC_LIST_EMPTY(res->dbuckets[i].list));
+               INSIST(isc_ht_count(res->dbuckets[i].counters) == 0);
+               isc_ht_destroy(&res->dbuckets[i].counters);
                isc_mutex_destroy(&res->dbuckets[i].lock);
        }
        isc_mem_put(res->mctx, res->dbuckets,
@@ -10500,7 +10599,6 @@ dns_resolver_create(dns_view_t *view, isc_taskmgr_t *taskmgr,
        atomic_init(&res->exiting, false);
        atomic_init(&res->priming, false);
        atomic_init(&res->zspill, 0);
-       atomic_init(&res->nfctx, 0);
        ISC_LIST_INIT(res->whenshutdown);
        ISC_LIST_INIT(res->alternates);
 
@@ -10537,7 +10635,8 @@ dns_resolver_create(dns_view_t *view, isc_taskmgr_t *taskmgr,
                snprintf(name, sizeof(name), "res%" PRIu32, i);
                isc_task_setname(res->buckets[i].task, name, res);
 
-               ISC_LIST_INIT(res->buckets[i].fctxs);
+               isc_ht_init(&res->buckets[i].fctxs, res->mctx, 8,
+                           ISC_HT_CASE_SENSITIVE);
                atomic_init(&res->buckets[i].exiting, false);
        }
 
@@ -10545,8 +10644,10 @@ dns_resolver_create(dns_view_t *view, isc_taskmgr_t *taskmgr,
                                    HASHSIZE(res->dhashbits) *
                                            sizeof(res->dbuckets[0]));
        for (size_t i = 0; i < HASHSIZE(res->dhashbits); i++) {
-               res->dbuckets[i] = (zonebucket_t){ .list = { 0 } };
-               ISC_LIST_INIT(res->dbuckets[i].list);
+               res->dbuckets[i] = (zonebucket_t){ 0 };
+               isc_ht_init(&res->dbuckets[i].counters, res->mctx, 8,
+                           ISC_HT_CASE_SENSITIVE);
+
                isc_mutex_init(&res->dbuckets[i].lock);
        }
 
@@ -10774,33 +10875,45 @@ dns_resolver_whenshutdown(dns_resolver_t *res, isc_task_t *task,
 void
 dns_resolver_shutdown(dns_resolver_t *res) {
        unsigned int i;
-       fetchctx_t *fctx;
        isc_result_t result;
-       bool is_false = false;
        bool is_done = false;
 
        REQUIRE(VALID_RESOLVER(res));
 
        RTRACE("shutdown");
 
-       if (atomic_compare_exchange_strong(&res->exiting, &is_false, true)) {
+       if (atomic_compare_exchange_strong(&res->exiting, &(bool){ false },
+                                          true))
+       {
                RTRACE("exiting");
 
                for (i = 0; i < res->nbuckets; i++) {
                        LOCK(&res->buckets[i].lock);
-                       for (fctx = ISC_LIST_HEAD(res->buckets[i].fctxs);
-                            fctx != NULL; fctx = ISC_LIST_NEXT(fctx, link))
+                       isc_ht_iter_t *it = NULL;
+
+                       isc_ht_iter_create(res->buckets[i].fctxs, &it);
+
+                       for (result = isc_ht_iter_first(it);
+                            result == ISC_R_SUCCESS;
+                            result = isc_ht_iter_next(it))
                        {
+                               fetchctx_t *fctx = NULL;
+                               isc_ht_iter_current(it, (void **)&fctx);
                                fctx_shutdown(fctx);
                        }
+
+                       isc_ht_iter_destroy(&it);
+
                        atomic_store(&res->buckets[i].exiting, true);
-                       if (ISC_LIST_EMPTY(res->buckets[i].fctxs)) {
+
+                       if (res->buckets[i].nfctxs == 0) {
                                if (isc_refcount_decrement(
                                            &res->activebuckets) == 1)
                                {
                                        is_done = true;
                                }
                        }
+
                        UNLOCK(&res->buckets[i].lock);
                }
                if (is_done) {
@@ -10831,24 +10944,6 @@ dns_resolver_detach(dns_resolver_t **resp) {
        }
 }
 
-static bool
-fctx_match(fetchctx_t *fctx, const dns_name_t *name, dns_rdatatype_t type,
-          unsigned int options) {
-       /*
-        * Don't match fetch contexts that are shutting down.
-        */
-       if (fctx->cloned || fctx->state == fetchstate_done ||
-           ISC_LIST_EMPTY(fctx->events))
-       {
-               return false;
-       }
-
-       if (fctx->type != type || fctx->options != options) {
-               return false;
-       }
-       return dns_name_equal(fctx->name, name);
-}
-
 static void
 log_fetch(const dns_name_t *name, dns_rdatatype_t type) {
        char namebuf[DNS_NAME_FORMATSIZE];
@@ -11039,13 +11134,13 @@ dns_resolver_createfetch(dns_resolver_t *res, const dns_name_t *name,
        }
 
        if ((options & DNS_FETCHOPT_UNSHARED) == 0) {
-               for (fctx = ISC_LIST_HEAD(res->buckets[bucketnum].fctxs);
-                    fctx != NULL; fctx = ISC_LIST_NEXT(fctx, link))
-               {
-                       if (fctx_match(fctx, name, type, options)) {
-                               break;
-                       }
-               }
+               uint8_t key[DNS_FCTX_KEYSIZE];
+               size_t keysize = sizeof(key);
+
+               fctx_makekey(name, type, options, key, &keysize);
+
+               (void)isc_ht_find(res->buckets[bucketnum].fctxs, key, keysize,
+                                 (void **)&fctx);
        }
 
        /*
@@ -11192,6 +11287,7 @@ dns_resolver_cancelfetch(dns_fetch_t *fetch) {
                        }
                }
        }
+
        /*
         * The "trystale" event must be sent before the "fetchdone" event,
         * because the latter clears the "recursing" query attribute, which is
@@ -11810,17 +11906,24 @@ dns_resolver_dumpfetches(dns_resolver_t *resolver, isc_statsformat_t format,
        REQUIRE(format == isc_statsformat_file);
 
        for (size_t i = 0; i < HASHSIZE(resolver->dhashbits); i++) {
-               fctxcount_t *fc;
                LOCK(&resolver->dbuckets[i].lock);
-               for (fc = ISC_LIST_HEAD(resolver->dbuckets[i].list); fc != NULL;
-                    fc = ISC_LIST_NEXT(fc, link))
+               isc_ht_iter_t *it = NULL;
+               isc_result_t result;
+
+               isc_ht_iter_create(resolver->dbuckets[i].counters, &it);
+               for (result = isc_ht_iter_first(it); result == ISC_R_SUCCESS;
+                    result = isc_ht_iter_next(it))
                {
-                       dns_name_print(fc->domain, fp);
+                       fctxcount_t *counter = NULL;
+                       isc_ht_iter_current(it, (void **)&counter);
+                       dns_name_print(counter->domain, fp);
                        fprintf(fp,
                                ": %u active (%u spilled, %u "
                                "allowed)\n",
-                               fc->count, fc->dropped, fc->allowed);
+                               counter->count, counter->dropped,
+                               counter->allowed);
                }
+               isc_ht_iter_destroy(&it);
                UNLOCK(&resolver->dbuckets[i].lock);
        }
 }