2 * This file is part of PowerDNS or dnsdist.
3 * Copyright -- PowerDNS.COM B.V. and its contributors
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of version 2 of the GNU General Public License as
7 * published by the Free Software Foundation.
9 * In addition, for the avoidance of any doubt, permission is granted to
10 * link this program with OpenSSL and to (re)distribute the binaries
11 * produced as the result of such linking.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
33 #include <boost/program_options.hpp>
35 #include "dns_random.hh"
36 #include "dnsparser.hh"
37 #include "dnswriter.hh"
38 #include "dnsrecords.hh"
39 #include "ednsoptions.hh"
40 #include "ednssubnet.hh"
46 using std::unique_ptr
;
50 static std::atomic
<unsigned int> g_recvcounter
, g_recvbytes
;
51 static volatile bool g_done
;
53 namespace po
= boost::program_options
;
54 static po::variables_map g_vm
;
58 static void* recvThread(const vector
<std::unique_ptr
<Socket
>>* sockets
)
60 vector
<pollfd
> rfds
, fds
;
61 for(const auto& s
: *sockets
) {
63 pfd
.fd
= s
->getHandle();
72 vector
<struct mmsghdr
> buf(100);
74 cmsgbuf_aligned
*cbuf
= new cmsgbuf_aligned
;
75 fillMSGHdr(&m
.msg_hdr
, new struct iovec
, cbuf
, sizeof(*cbuf
), new char[1500], 1500, new ComboAddress("127.0.0.1"));
79 cmsgbuf_aligned
*cbuf
= new cmsgbuf_aligned
;
80 fillMSGHdr(&buf
, new struct iovec
, cbuf
, sizeof(*cbuf
), new char[1500], 1500, new ComboAddress("127.0.0.1"));
86 err
= poll(&fds
[0], fds
.size(), -1);
90 unixDie("Unable to poll for new UDP events");
93 for(auto &pfd
: fds
) {
94 if (pfd
.revents
& POLLIN
) {
96 if ((err
=recvmmsg(pfd
.fd
, &buf
[0], buf
.size(), MSG_WAITFORONE
, 0)) < 0 ) {
102 for(int n
=0; n
< err
; ++n
)
103 g_recvbytes
+= buf
[n
].msg_len
;
105 if ((err
= recvmsg(pfd
.fd
, &buf
, 0)) < 0) {
111 for (decltype(buf
.msg_iovlen
) i
= 0; i
< buf
.msg_iovlen
; i
++)
112 g_recvbytes
+= buf
.msg_iov
[i
].iov_len
;
120 static ComboAddress
getRandomAddressFromRange(const Netmask
& ecsRange
)
122 ComboAddress result
= ecsRange
.getMaskedNetwork();
123 uint8_t bits
= ecsRange
.getBits();
124 uint32_t mod
= 1 << (32 - bits
);
125 result
.sin4
.sin_addr
.s_addr
= result
.sin4
.sin_addr
.s_addr
+ ntohl(dns_random(mod
));
129 static void replaceEDNSClientSubnet(vector
<uint8_t>* packet
, const Netmask
& ecsRange
)
131 /* the last 4 bytes of the packet are the IPv4 address */
132 ComboAddress rnd
= getRandomAddressFromRange(ecsRange
);
133 uint32_t addr
= rnd
.sin4
.sin_addr
.s_addr
;
135 const auto packetSize
= packet
->size();
136 if (packetSize
< sizeof(addr
)) {
140 memcpy(&packet
->at(packetSize
- sizeof(addr
)), &addr
, sizeof(addr
));
143 static void sendPackets(const vector
<std::unique_ptr
<Socket
>>& sockets
, const vector
<vector
<uint8_t>* >& packets
, int qps
, ComboAddress dest
, const Netmask
& ecsRange
)
145 unsigned int burst
=100;
146 const auto nsecPerBurst
=1*(unsigned long)(burst
*1000000000.0/qps
);
147 struct timespec nsec
;
151 unsigned int nBursts
=0;
158 cmsgbuf_aligned cbuf
;
160 vector
<unique_ptr
<Unit
> > units
;
163 for(const auto& p
: packets
) {
168 if (!ecsRange
.empty()) {
169 replaceEDNSClientSubnet(p
, ecsRange
);
172 fillMSGHdr(&u
.msgh
, &u
.iov
, nullptr, 0, (char*)&(*p
)[0], p
->size(), &dest
);
173 if((ret
=sendmsg(sockets
[count
% sockets
.size()]->getHandle(),
181 // Calculate the time in nsec we need to sleep to the next burst.
182 // If this is negative, it means that we are not achieving the requested
183 // target rate, in which case we skip the sleep.
184 int toSleep
= nBursts
*nsecPerBurst
- 1000*dt
.udiffNoReset();
186 nsec
.tv_nsec
= toSleep
;
193 static void usage(po::options_description
&desc
) {
194 cerr
<<"Syntax: calidns [OPTIONS] QUERY_FILE DESTINATION INITIAL_QPS HITRATE"<<endl
;
199 New plan. Set cache hit percentage, which we achieve on a per second basis.
200 So we start with 10000 qps for example, and for 90% cache hit ratio means
201 we take 1000 unique queries and each send them 10 times.
203 We then move the 1000 unique queries to the 'known' pool.
205 For the next second, say 20000 qps, we know we are going to need 2000 new queries,
206 so we take 2000 from the unknown pool. Then we need 18000 cache hits. We can get 1000 from
207 the known pool, leaving us down 17000. Or, we have 3000 in total now and we need 2000. We simply
208 repeat the 3000 mix we have ~7 times. The 2000 can now go to the known pool too.
210 For the next second, say 30000 qps, we'll need 3000 cache misses, which we get from
211 the unknown pool. To this we add 3000 queries from the known pool. Next up we repeat this batch 5
214 In general the algorithm therefore is:
216 1) Calculate number of cache misses required, get them from the unknown pool
217 2) Move those to the known pool
218 3) Fill up to amount of queries we need with random picks from the known pool
222 int main(int argc
, char** argv
)
225 po::options_description
desc("Options");
227 ("help,h", "Show this helpful message")
228 ("version", "Show the version number")
229 ("ecs", po::value
<string
>(), "Add EDNS Client Subnet option to outgoing queries using random addresses from the specified range (IPv4 only)")
230 ("ecs-from-file", "Read IP or subnet values from the query file and add them as EDNS Client Subnet options to outgoing queries")
231 ("increment", po::value
<float>()->default_value(1.1), "Set the factor to increase the QPS load per run")
232 ("maximum-qps", po::value
<uint32_t>(), "Stop incrementing once this rate has been reached, to provide a stable load")
233 ("minimum-success-rate", po::value
<double>()->default_value(0), "Stop the test as soon as the success rate drops below this value, in percent")
234 ("plot-file", po::value
<string
>(), "Write results to the specific file")
235 ("quiet", "Whether to run quietly, outputting only the maximum QPS reached. This option is mostly useful when used with --minimum-success-rate")
236 ("want-recursion", "Set the Recursion Desired flag on queries");
237 po::options_description alloptions
;
238 po::options_description
hidden("hidden options");
240 ("query-file", po::value
<string
>(), "File with queries")
241 ("destination", po::value
<string
>(), "Destination address")
242 ("initial-qps", po::value
<uint32_t>(), "Initial number of queries per second")
243 ("hitrate", po::value
<double>(), "Aim this percent cache hitrate");
245 alloptions
.add(desc
).add(hidden
);
246 po::positional_options_description p
;
247 p
.add("query-file", 1);
248 p
.add("destination", 1);
249 p
.add("initial-qps", 1);
252 po::store(po::command_line_parser(argc
, argv
).options(alloptions
).positional(p
).run(), g_vm
);
255 if (g_vm
.count("help")) {
260 if (g_vm
.count("version")) {
261 cerr
<<"calidns "<<VERSION
<<endl
;
265 if (!(g_vm
.count("query-file") && g_vm
.count("destination") && g_vm
.count("initial-qps") && g_vm
.count("hitrate"))) {
270 float increment
= 1.1;
272 increment
= g_vm
["increment"].as
<float>();
277 bool wantRecursion
= g_vm
.count("want-recursion");
278 bool useECSFromFile
= g_vm
.count("ecs-from-file");
279 g_quiet
= g_vm
.count("quiet");
281 double hitrate
= g_vm
["hitrate"].as
<double>();
282 if (hitrate
> 100 || hitrate
< 0) {
283 cerr
<<"hitrate must be between 0 and 100, not "<<hitrate
<<endl
;
287 uint32_t qpsstart
= g_vm
["initial-qps"].as
<uint32_t>();
289 uint32_t maximumQps
= std::numeric_limits
<uint32_t>::max();
290 if (g_vm
.count("maximum-qps")) {
291 maximumQps
= g_vm
["maximum-qps"].as
<uint32_t>();
294 double minimumSuccessRate
= g_vm
["minimum-success-rate"].as
<double>();
295 if (minimumSuccessRate
> 100.0 || minimumSuccessRate
< 0.0) {
296 cerr
<<"Minimum success rate must be between 0 and 100, not "<<minimumSuccessRate
<<endl
;
301 if (g_vm
.count("ecs")) {
304 ecsRange
= Netmask(g_vm
["ecs"].as
<string
>());
305 if (!ecsRange
.empty()) {
307 if (!ecsRange
.isIPv4()) {
308 cerr
<<"Only IPv4 ranges are supported for ECS at the moment!"<<endl
;
313 cout
<<"Adding ECS option to outgoing queries with random addresses from the "<<ecsRange
.toString()<<" range"<<endl
;
317 catch (const NetmaskException
& e
) {
318 cerr
<<"Error while parsing the ECS netmask: "<<e
.reason
<<endl
;
323 struct sched_param param
;
324 param
.sched_priority
=99;
326 #if HAVE_SCHED_SETSCHEDULER
327 if(sched_setscheduler(0, SCHED_FIFO
, ¶m
) < 0) {
329 cerr
<<"Unable to set SCHED_FIFO: "<<stringerror()<<endl
;
334 ifstream
ifs(g_vm
["query-file"].as
<string
>());
337 vector
<std::shared_ptr
<vector
<uint8_t> > > unknown
, known
;
338 std::vector
<std::string
> fields
;
341 while(getline(ifs
, line
)) {
342 vector
<uint8_t> packet
;
343 DNSPacketWriter::optvect_t ednsOptions
;
345 if (line
.empty() || line
.at(0) == '#') {
350 stringtok(fields
, line
, "\t ");
351 if ((useECSFromFile
&& fields
.size() < 3) || fields
.size() < 2) {
352 cerr
<<"Skipping invalid line '"<<line
<<", it does not contain enough values"<<endl
;
356 const std::string
& qname
= fields
.at(0);
357 const std::string
& qtype
= fields
.at(1);
360 if (useECSFromFile
) {
361 subnet
= fields
.at(2);
364 DNSPacketWriter
pw(packet
, DNSName(qname
), DNSRecordContent::TypeToNumber(qtype
));
365 pw
.getHeader()->rd
=wantRecursion
;
366 pw
.getHeader()->id
=dns_random_uint16();
368 if(!subnet
.empty() || !ecsRange
.empty()) {
370 opt
.source
= Netmask(subnet
.empty() ? "0.0.0.0/32" : subnet
);
371 ednsOptions
.emplace_back(EDNSOptionCode::ECS
, makeEDNSSubnetOptsString(opt
));
374 if(!ednsOptions
.empty() || pw
.getHeader()->id
% 2) {
375 pw
.addOpt(1500, 0, EDNSOpts::DNSSECOK
, ednsOptions
);
378 unknown
.push_back(std::make_shared
<vector
<uint8_t>>(packet
));
381 shuffle(unknown
.begin(), unknown
.end(), pdns::dns_random_engine());
383 cout
<<"Generated "<<unknown
.size()<<" ready to use queries"<<endl
;
386 vector
<std::unique_ptr
<Socket
>> sockets
;
389 dest
= ComboAddress(g_vm
["destination"].as
<string
>(), 53);
391 catch (PDNSException
&e
) {
392 cerr
<<e
.reason
<<endl
;
395 for(int i
=0; i
< 24; ++i
) {
396 auto sock
= make_unique
<Socket
>(dest
.sin4
.sin_family
, SOCK_DGRAM
);
397 // sock->connect(dest);
399 setSocketSendBuffer(sock
->getHandle(), 2000000);
401 catch (const std::exception
& e
) {
403 cerr
<<e
.what()<<endl
;
407 setSocketReceiveBuffer(sock
->getHandle(), 2000000);
409 catch (const std::exception
& e
) {
411 cerr
<<e
.what()<<endl
;
415 sockets
.push_back(std::move(sock
));
417 new thread(recvThread
, &sockets
);
421 if (g_vm
.count("plot-file")) {
422 plot
.open(g_vm
["plot-file"].as
<string
>());
424 cerr
<<"Error opening "<<g_vm
["plot-file"].as
<string
>()<<" for writing: "<<stringerror()<<endl
;
429 double bestQPS
= 0.0;
430 double bestPerfectQPS
= 0.0;
432 for(qps
=qpsstart
;;) {
435 cout
<<"Aiming at "<<qps
<< "qps (RD="<<wantRecursion
<<") for "<<seconds
<<" seconds at cache hitrate "<<100.0*hitrate
<<"%";
437 unsigned int misses
=(1-hitrate
)*qps
*seconds
;
438 unsigned int total
=qps
*seconds
;
443 cout
<<", need "<<misses
<<" misses, "<<total
<<" queries, have "<<unknown
.size()<<" unknown left!"<<endl
;
446 if (misses
> unknown
.size()) {
447 cerr
<<"Not enough queries remaining (need at least "<<misses
<<" and got "<<unknown
.size()<<", please add more to the query file), exiting."<<endl
;
450 vector
<vector
<uint8_t>*> toSend
;
452 for(n
=0; n
< misses
; ++n
) {
453 auto ptr
=unknown
.back();
455 toSend
.push_back(ptr
.get());
456 known
.push_back(ptr
);
458 for(;n
< total
; ++n
) {
459 toSend
.push_back(known
[dns_random(known
.size())].get());
462 shuffle(toSend
.begin(), toSend
.end(), pdns::dns_random_engine());
463 g_recvcounter
.store(0);
468 sendPackets(sockets
, toSend
, qps
, dest
, ecsRange
);
470 const auto udiff
= dt
.udiffNoReset();
471 const auto realqps
=toSend
.size()/(udiff
/1000000.0);
473 cout
<<"Achieved "<<realqps
<<" qps over "<< udiff
/1000000.0<<" seconds"<<endl
;
477 const auto received
= g_recvcounter
.load();
478 const auto udiffReceived
= dt
.udiff();
479 const auto realReceivedQPS
= received
/(udiffReceived
/1000000.0);
480 double perc
=received
*100.0/toSend
.size();
482 cout
<<"Received "<<received
<<" packets over "<< udiffReceived
/1000000.0<<" seconds ("<<perc
<<"%, adjusted received rate "<<realReceivedQPS
<<" qps)"<<endl
;
486 plot
<<qps
<<" "<<realqps
<<" "<<perc
<<" "<<received
/(udiff
/1000000.0)<<" " << 8*g_recvbytes
.load()/(udiff
/1000000.0)<<endl
;
490 if (qps
< maximumQps
) {
497 if (minimumSuccessRate
> 0.0 && perc
< minimumSuccessRate
) {
502 cout
<<"The latest success rate ("<<perc
<<") dropped below the minimum success rate of "<<minimumSuccessRate
<<", stopping."<<endl
;
503 cout
<<"The final rate reached before failing was "<<bestQPS
<<" qps (best rate at 100% was "<<bestPerfectQPS
<<" qps)"<<endl
;
508 bestQPS
= std::max(bestQPS
, realReceivedQPS
);
510 bestPerfectQPS
= std::max(bestPerfectQPS
, realReceivedQPS
);
520 catch(std::exception
& e
)
522 cerr
<<"Fatal error: "<<e
.what()<<endl
;