blog.robur.coop

The Robur cooperative blog.
Back to index

DNSvizor performance engineering

2025-07-24

TL;DR: We evaluated and improved the performance of DNSvizor. We are continuing our work on DNSvizor (repository).

Performance considerations

It is not easy to measure the performance of DNSvizor, especially since we don't want our benchmarks to mostly include waiting for replies of authoritative nameservers on the Internet. A realistic scenario is that a bunch of clients request different domain names, sometimes hitting the block list, and also the DHCP part of DNSvizor should respond quickly.

From this perspective, DNSvizor includes a shared cache, where e.g. if some client asked to resolve example.com, the nameservers for com are cached, so another client (or the same one) asking for anotherexample.com will use the cached entry. DNSvizor as well takes into account that DNS is fault-tolerant, and some authoritative nameservers are not reachable all the time. Whenever a query to an authoritative nameserver is sent out, up to three different nameservers are queried in parallel. There is a tradeoff between causing a lot of traffic for a single DNS request (which may be used by attackers to facilitate amplification attacks) and minimizing response time to a client DNS request.

In this article, we will go into detail of some bottlenecks we encountered, and how we fixed them. Please keep in mind, that in OCaml the overall performance is tightly related to the memory usage: if we allocate less memory, the garbage collector will have to do less work.

Block lists

If we use a large lists of domains to block, and update that often, a lot of memory is used. One reason is that the block list is saved as a zone in a trie, which is used in our authoritative DNS server implementation as well. For the authoritative server, we implemented a cache of several versions of the trie to support incremental zone transfer. Now, for DNSvizor and block lists, that is not useful. We made the number of versions configurable and set it to 0 in DNSvizor. This reduced the memory usage by a factor of 5 after updating the block list 5 times (this is the amount of versions we kept previously).

Another big usage of memory originated from the compare function of the trie, which compared the domain names. The comparison, as mandated by DNS, is case-insensitive. This used to be done in every compare, requiring lower-case strings to be allocated. We changed the implementation to use a canonical domain-name, so we don't have to lower-case each domain name when comparing, but we once compute the lower-case version before inserting it. Thus, only lower-case domain names are in the trie. The reduction in the trie is from 650MB allocated down to 223MB, so nearly a factor of 3.

In developing the block list feature we started out with a simple approach: fetch the block list (into memory); then use a simple parser to get the domain names to block. This approach worked, but has issues: some block lists are huge (e.g. >6 MB for the block list we evaluated against), and the naïve parser worked by splitting the huge string into lines and so on. So we reworked the parser to work in a streaming fashion reducing the peak memory during block list fetching and parsing.

The block list we used for evaluating this is https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts (the default list used in pi-hole).

Overall performance

We used the statistical memory profiler that the OCaml runtime ships. Within our NGI pointer project, we developed memtrace-mirage to get the data out of a unikernel using a TCP socket. Such a trace can be visualized using memtrace-viewer.

Since our goal is to use a hot cache, we requested thousands of times the same domain-name, and looked at the memory profiling output (shown below):

DNSvizor memtrace

There are various occurences of calls to Metrics, and Logs - which records metrics and emits log messages. After we demoted a log message to debug level and only enable metrics we need, the picture changes drastically:

DNSvizor memtrace

Here, we don't see any single function that allocates a lot of memory (> 5%).

What we learned is that for performance engineering, it is necessary to have an idea what the real-world scenario will be, and also that the tooling such as the statistical memory profiler, are very useful to discover bottlenecks.

Conclusion

DNSvizor provides DNS resolution and DHCP service for your network with a web interface - now with enhanced performance. It already exists :). Please report issues you encounter and questions you may have. Also, if you use dnsmasq, please show us your configuration.

If you're interested in MirageOS and using it in your domain, don't hesitate to reach out to us (via eMail: team@robur.coop) - we're keen to deploy MirageOS and find more domains where it is useful.

Our work is only partially funded, we cross-fund our work by commercial contracts and public (EU) funding. We are part of a non-profit company, you can make a (in the EU tax-deductible) donation (select "DONATION robur" in the dropdown menu), or sponsor us via the GitHub sponsor button.