]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
LMDB-based very high performance backend 1152/head
authorMark Zealey <mark@markandruth.co.uk>
Fri, 14 Feb 2014 15:06:16 +0000 (17:06 +0200)
committerMark Zealey <mark@markandruth.co.uk>
Fri, 14 Feb 2014 16:39:03 +0000 (18:39 +0200)
12 files changed:
.travis.yml
configure.ac
m4/pdns_with_lmdb.m4 [new file with mode: 0644]
modules/Makefile.am
modules/lmdbbackend/Makefile.am [new file with mode: 0644]
modules/lmdbbackend/OBJECTFILES [new file with mode: 0644]
modules/lmdbbackend/OBJECTLIBS [new file with mode: 0644]
modules/lmdbbackend/dumpdb.pl [new file with mode: 0644]
modules/lmdbbackend/lmdb-example.pl [new file with mode: 0644]
modules/lmdbbackend/lmdbbackend.cc [new file with mode: 0644]
modules/lmdbbackend/lmdbbackend.hh [new file with mode: 0644]
pdns/docs/pdns.xml

index 02a3f465813b594e242be7bff12ce66c0a002ea3..cb541b611f1e93ff44ce2677fdb217584ef3060e 100644 (file)
@@ -7,6 +7,9 @@ before_script:
  - sudo rm /etc/apt/sources.list.d/travis_ci_zeromq3-source.list
  - sudo apt-get update
  - sudo apt-get install libboost-all-dev libtolua-dev bc libcdb-dev libnet-dns-perl unbound-host ldnsutils dnsutils bind9utils libtool libcdb-dev xmlto dblatex links asciidoc ruby-json ruby-sqlite3 rubygems libcurl4-openssl-dev ruby1.9.1 socat time libzmq1 libzmq-dev pkg-config daemontools authbind liblua5.1-posix1 libopendbx1-dev libopendbx1-sqlite3 python-virtualenv libldap2-dev
+ - sudo sh -c 'sed s/precise/trusty/g /etc/apt/sources.list > /etc/apt/sources.list.d/trusty.list'
+ - sudo apt-get update
+ - sudo apt-get install liblmdb0 liblmdb-dev lmdb-utils
  - sudo update-alternatives --set ruby /usr/bin/ruby1.9.1
  - sudo touch /etc/authbind/byport/53
  - sudo chmod 755 /etc/authbind/byport/53
@@ -20,7 +23,7 @@ before_script:
  - cd ../..
 script:
  - ./bootstrap
- - ./configure --with-modules='bind gmysql gpgsql gsqlite3 mydns tinydns remote random opendbx ldap' --enable-unit-tests --enable-remotebackend-http --enable-tools --enable-remotebackend-zeromq
+ - ./configure --with-modules='bind gmysql gpgsql gsqlite3 mydns tinydns remote random opendbx ldap lmdb' --enable-unit-tests --enable-remotebackend-http --enable-tools --enable-remotebackend-zeromq
  - make dist
  - make -j 4
  - make -j 4 check
index ac0640959e2860e9c466dcc78507aa5b680c091c..3433fd9c13a2687215510414ad9d2655df34d669 100644 (file)
@@ -240,6 +240,9 @@ for a in $modules $dynmodules; do
       PDNS_WITH_ORACLE
       needoracle=yes
       ;;
+    lmdb)
+      PDNS_WITH_LMDB
+      ;;
     mydns|gmysql|pdns)
       PDNS_WITH_MYSQL
       ;;
@@ -328,6 +331,7 @@ AC_CONFIG_FILES([
   modules/randombackend/Makefile
   modules/remotebackend/Makefile
   modules/tinydnsbackend/Makefile
+  modules/lmdbbackend/Makefile
 ])
 AC_OUTPUT
 
diff --git a/m4/pdns_with_lmdb.m4 b/m4/pdns_with_lmdb.m4
new file mode 100644 (file)
index 0000000..00156fd
--- /dev/null
@@ -0,0 +1,8 @@
+AC_DEFUN([PDNS_WITH_LMDB],[
+    AC_CHECK_HEADERS([lmdb.h], , [AC_MSG_ERROR([lmdb header (lmdb.h) not found])])
+    AC_SUBST([LIBLMDB])
+    AC_CHECK_LIB(
+        [lmdb], [mdb_env_create],
+        [AC_DEFINE([HAVE_LIBLMDB], 1, [Have -llmdb]) LIBLMDB="lmdb"]
+    )
+])
index 089b4c3b3cadbd06f13f368f2e9dd20895acc805..1a544a9fbd81fbe2590acc5d9968b845646b104b 100644 (file)
@@ -1,2 +1,2 @@
 SUBDIRS=@moduledirs@
-DIST_SUBDIRS=bindbackend db2backend geobackend gmysqlbackend goraclebackend gpgsqlbackend gsqlite3backend ldapbackend luabackend mydnsbackend opendbxbackend oraclebackend pipebackend tinydnsbackend remotebackend randombackend
+DIST_SUBDIRS=bindbackend db2backend geobackend gmysqlbackend goraclebackend gpgsqlbackend gsqlite3backend ldapbackend luabackend mydnsbackend opendbxbackend oraclebackend pipebackend tinydnsbackend remotebackend randombackend lmdbbackend
diff --git a/modules/lmdbbackend/Makefile.am b/modules/lmdbbackend/Makefile.am
new file mode 100644 (file)
index 0000000..2aac8f8
--- /dev/null
@@ -0,0 +1,8 @@
+AM_CPPFLAGS=@THREADFLAGS@ $(BOOST_CPPFLAGS)
+lib_LTLIBRARIES = liblmdbbackend.la
+
+EXTRA_DIST=OBJECTFILES OBJECTLIBS
+
+liblmdbbackend_la_SOURCES=lmdbbackend.cc lmdbbackend.hh
+liblmdbbackend_la_LDFLAGS=-module -avoid-version
+liblmdbbackend_la_LIBADD=-l@LIBLMDB@
diff --git a/modules/lmdbbackend/OBJECTFILES b/modules/lmdbbackend/OBJECTFILES
new file mode 100644 (file)
index 0000000..10d9763
--- /dev/null
@@ -0,0 +1 @@
+lmdbbackend.o
diff --git a/modules/lmdbbackend/OBJECTLIBS b/modules/lmdbbackend/OBJECTLIBS
new file mode 100644 (file)
index 0000000..0c0c430
--- /dev/null
@@ -0,0 +1 @@
+-l$(LIBLMDB)
diff --git a/modules/lmdbbackend/dumpdb.pl b/modules/lmdbbackend/dumpdb.pl
new file mode 100644 (file)
index 0000000..a682b6c
--- /dev/null
@@ -0,0 +1,36 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+use LMDB_File qw( :dbflags :envflags :cursor_op :writeflags );
+
+my ($path, $dbname, $searchkey) = @ARGV;
+die unless -d $path;
+
+my $env = LMDB::Env->new( $path, {
+    mapsize => 1024*1024*1024,
+    maxdbs => 3,
+    flags => MDB_RDONLY,
+});
+my $txn = LMDB::Txn->new( $env, MDB_RDONLY );
+my $db = $txn->OpenDB( $dbname, MDB_DUPSORT );
+my $c = $db->Cursor;
+my ($k, $v);
+if( $searchkey ) {
+    $c->get( $k = $searchkey, $v, MDB_SET_RANGE );
+} else {
+    $c->get( $k, $v, MDB_FIRST );
+}
+
+print "key: $k; value: $v\n";
+
+while(1) {
+    eval {
+        $c->get( $k, $v, MDB_NEXT );
+    };
+    if( $@ =~ /MDB_NOTFOUND/ ) {
+        exit;
+    }
+    die $@ if $@;
+    print "key: $k; value: $v\n";
+}
diff --git a/modules/lmdbbackend/lmdb-example.pl b/modules/lmdbbackend/lmdb-example.pl
new file mode 100644 (file)
index 0000000..15563bc
--- /dev/null
@@ -0,0 +1,63 @@
+#!/usr/bin/perl
+# An example script to generate files for the PowerDNS LMDB high performance
+# backend
+
+use LMDB_File 0.04 qw( :dbflags :envflags :cursor_op :writeflags );
+
+use strict;
+use warnings;
+
+my $HOME = "/var/tmp/lmdb";
+
+mkdir $HOME unless -d $HOME;
+my $env = LMDB::Env->new( $HOME, {
+    mapsize => 100*1024*1024*1024,
+    maxdbs => 3,
+});
+my $txn = LMDB::Txn->new( $env, 0 );
+my $dns_zone = $txn->OpenDB( 'zone', MDB_CREATE );
+
+my $zone = 'example.com';
+my $zone_id = 1;
+my $zone_ttl = 300;
+my $soa_entry = "ns.$zone. hostmaster.$zone. 2012021101 86400 7200 604800 86400";
+# XXX $zone length MUST be less than 500 bytes
+$dns_zone->put( scalar reverse(lc $zone), join("\t", $zone_id, $zone_ttl, $soa_entry) );
+
+my $dns_data = $txn->OpenDB( 'data', MDB_CREATE | MDB_DUPSORT );
+my $dns_extended_data = $txn->OpenDB( 'extended_data', MDB_CREATE );
+my @entries = (
+    # host type data
+    [ $zone, 'NS', "ns.$zone" ],
+    # MX/SRV put priority <space> data
+    [ $zone, 'MX', "10 mail.hotmail.com" ],
+    # No SOA records
+    [ "test.$zone", 'A', '1.2.3.4' ],
+    [ "text.$zone", 'TXT', "test\n123" ],
+    [ "longtext.$zone", 'TXT', "A" x 550 ],
+
+);
+
+my $extended_ref = 0;
+for my $row (@entries) {
+    my ($host, $type, $data) = @$row;
+
+    # Don't ever allow these characters as they break powerdns
+    $data =~ tr/"\\//d;
+
+    if( $type eq 'TXT' ) {
+        $data =~ s/([^ -~])/sprintf '\\%03d', ord $1/eg;
+    }
+
+    my $key = join( "\t", scalar reverse(lc $host), $type );  # XXX must be less than 500 bytes
+    my $val = join( "\t", $zone_id, $zone_ttl, $data);
+    if( length $val > 500 ) {
+        $dns_data->put( $key, "REF\t" . ++$extended_ref );
+        $dns_extended_data->put( $extended_ref, $val );
+        # Extended data record storage as DUPSORT can only store up to 500 bytes of data unfortunately
+    } else {
+        $dns_data->put( $key, $val );
+    }
+}
+
+$txn->commit;
diff --git a/modules/lmdbbackend/lmdbbackend.cc b/modules/lmdbbackend/lmdbbackend.cc
new file mode 100644 (file)
index 0000000..2e7a322
--- /dev/null
@@ -0,0 +1,370 @@
+/*
+ * LMDBBackend - a high performance LMDB based backend for PowerDNS written by
+ * Mark Zealey, 2013
+ *
+ * This was originally going to be a backend using BerkeleyDB 5 for high
+ * performance DNS over massive (millions of zones) databases. However,
+ * BerkeleyDB had a number of issues to do with locking, contention and
+ * corruption which made it unsuitable for use. Instead, we use LMDB to perform
+ * very fast lookups.
+ *
+ * See the documentation for more details, and lmdb-example.pl for an example
+ * script which generates a simple zone.
+ */
+
+#include <pdns/utility.hh>
+#include <pdns/dnsbackend.hh>
+#include <pdns/dns.hh>
+#include <pdns/dnspacket.hh>
+#include <pdns/pdnsexception.hh>
+#include <pdns/logger.hh>
+#include <signal.h>
+#include "lmdbbackend.hh"
+#include <pdns/arguments.hh>
+
+#if 0
+#define DEBUGLOG(msg) L<<Logger::Error<<msg
+#else
+#define DEBUGLOG(msg) do {} while(0)
+#endif
+
+LMDBBackend::LMDBBackend(const string &suffix)
+{
+    setArgPrefix("lmdb"+suffix);
+    open_db();
+}
+
+void LMDBBackend::open_db() {
+    L<<Logger::Error<<"Loading LMDB database " << getArg("datapath") << endl;
+
+    string path = getArg("datapath");
+    int rc;
+    int major, minor, patch;
+
+    string verstring( mdb_version( &major, &minor, &patch ) );
+    if( MDB_VERINT( major, minor, patch ) < MDB_VERINT( 0, 9, 8 ) )
+        throw PDNSException( "LMDB Library version too old (" + verstring + "). Needs to be 0.9.8 or greater" );
+
+    if( rc = mdb_env_create(&env) )
+        throw PDNSException("Couldn't open LMDB database " + path + ": mdb_env_create() returned " + mdb_strerror(rc));
+
+    if( rc = mdb_env_set_maxdbs( env, 3 ) )
+        throw PDNSException("Couldn't open LMDB database " + path + ": mdb_env_set_maxdbs() returned " + mdb_strerror(rc));
+
+    if( rc = mdb_env_open(env, path.c_str(), MDB_RDONLY, 0) )
+        throw PDNSException("Couldn't open LMDB database " + path + ": mdb_env_open() returned " + mdb_strerror(rc));
+
+    if( rc = mdb_txn_begin(env, NULL, MDB_RDONLY, &txn) )
+        throw PDNSException("Couldn't start LMDB txn " + path + ": mdb_txn_begin() returned " + mdb_strerror(rc));
+
+    if( rc = mdb_dbi_open(txn, "zone", 0, &zone_db) )
+        throw PDNSException("Couldn't open LMDB zone database " + path + ": mdb_dbi_open() returned " + mdb_strerror(rc));
+    if( rc = mdb_cursor_open(txn, zone_db, &zone_cursor) )
+        throw PDNSException("Couldn't open cursor on LMDB zone database " + path + ": mdb_cursor_open() returned " + mdb_strerror(rc));
+
+    if( rc = mdb_dbi_open(txn, "data", MDB_DUPSORT, &data_db) )
+        throw PDNSException("Couldn't open LMDB data database " + path + ": mdb_dbi_open() returned " + mdb_strerror(rc));
+    if( rc = mdb_cursor_open(txn, data_db, &data_cursor) )
+        throw PDNSException("Couldn't open cursor on LMDB data database " + path + ": mdb_cursor_open() returned " + mdb_strerror(rc));
+
+    if( rc = mdb_dbi_open(txn, "extended_data", 0, &data_extended_db) )
+        throw PDNSException("Couldn't open LMDB extended_data database " + path + ": mdb_dbi_open() returned " + mdb_strerror(rc));
+    if( rc = mdb_cursor_open(txn, data_extended_db, &data_extended_cursor) )
+        throw PDNSException("Couldn't open cursor on LMDB data_extended database " + path + ": mdb_cursor_open() returned " + mdb_strerror(rc));
+
+}
+
+void LMDBBackend::close_db() {
+    L<<Logger::Error<<"Closing LMDB database"<< endl;
+
+    mdb_cursor_close(data_cursor);
+    mdb_cursor_close(zone_cursor);
+    mdb_cursor_close(data_extended_cursor);
+    mdb_dbi_close(env, data_db);
+    mdb_dbi_close(env, zone_db);
+    mdb_dbi_close(env, data_extended_db);
+    mdb_txn_abort(txn);
+    mdb_env_close(env);
+}
+
+LMDBBackend::~LMDBBackend()
+{
+    close_db();
+}
+
+void LMDBBackend::reload() {
+    close_db();
+    open_db();
+}
+
+// Get the zone name and value of the requested zone (reversed) OR the entry
+// just before where it should have been
+bool LMDBBackend::getAuthZone( string &rev_zone )
+{
+    MDB_val key, data;
+    // XXX can do this just using char *
+
+    string orig = rev_zone;
+    key.mv_data = (char *)rev_zone.c_str();
+    key.mv_size = rev_zone.length();
+
+    // Release our transaction and cursors in order to get latest data
+    mdb_txn_reset( txn );
+    mdb_txn_renew( txn );
+    mdb_cursor_renew( txn, zone_cursor );
+    mdb_cursor_renew( txn, data_cursor );
+    mdb_cursor_renew( txn, data_extended_cursor );
+
+    // Find the nearest record, or the last record if none
+    if( mdb_cursor_get(zone_cursor, &key, &data, MDB_SET_RANGE) )
+        mdb_cursor_get(zone_cursor, &key, &data, MDB_LAST);
+
+    rev_zone.assign( (const char *)key.mv_data, key.mv_size );
+
+    DEBUGLOG("Auth key: " << rev_zone <<endl);
+
+    /* Only skip this bit if we got an exact hit on the SOA. otherwise we have
+     * to go back to the previous record */
+    if( orig.compare( rev_zone ) != 0 ) {
+        /* Skip back 1 entry to what should be a substring of what was searched
+         * for (or a totally different entry) */
+        if( mdb_cursor_get(zone_cursor, &key, &data, MDB_PREV) ) {
+            // At beginning of database; therefore didnt actually hit the
+            // record. Return false
+            return false;
+        }
+
+        rev_zone.assign( (const char *)key.mv_data, key.mv_size );
+    }
+
+    return true;
+}
+
+bool LMDBBackend::getAuthData( SOAData &soa, DNSPacket *p )
+{
+    MDB_val key, value;
+    if( mdb_cursor_get(zone_cursor, &key, &value, MDB_GET_CURRENT) )
+        return false;
+
+    string data( (const char *)value.mv_data, value.mv_size );
+    DEBUGLOG("Auth record data " << data<<endl);
+
+// XXX do this in C too
+    vector<string>parts;
+    stringtok(parts,data,"\t");
+
+    if(parts.size() != 3 )
+        throw PDNSException("Invalid record in zone table: " + data );
+
+    fillSOAData( parts[2], soa );
+
+    soa.domain_id = atoi( parts[0].c_str() );
+    soa.ttl = atoi( parts[1].c_str() );
+
+    soa.scopeMask = 0;
+    soa.db = this;
+
+    return true;
+}
+
+// Called to start an AXFR then ->get() is called. Return true if the domain exists
+bool LMDBBackend::list(const string &target, int zoneId, bool include_disabled) {
+    DEBUGLOG("list() requested for " <<target << endl);
+    d_first = true;
+    d_origdomain = target;
+    d_domain_id = zoneId;
+    d_curqtype = QType::AXFR;
+
+    // getSOA will have been called first to ensure the domain exists so if
+    // that's the case then there's no reason we can't AXFR it.
+
+    return true;
+}
+
+void LMDBBackend::lookup(const QType &type, const string &inQdomain, DNSPacket *p, int zoneId)
+{
+    DEBUGLOG("lookup: " <<inQdomain << " " << type.getName() << endl);
+
+    d_first = true;
+    d_origdomain = inQdomain;
+    d_curqtype = type;
+}
+
+inline bool LMDBBackend::get_finished()
+{
+    d_origdomain = "";
+
+    return false;
+}
+
+bool LMDBBackend::get(DNSResourceRecord &rr)
+{
+    MDB_val key, value;
+    bool is_axfr = (d_curqtype == QType::AXFR);
+    bool is_full_key = ( ! is_axfr && d_curqtype != QType::ANY );
+
+    DEBUGLOG("get : " <<d_origdomain<< endl);
+    if( !d_origdomain.length() )
+        return false;
+
+    DEBUGLOG("Starting Q " << d_first<< endl);
+
+    if( d_first ) {
+        d_first = false;
+
+        // Reverse the query string
+        string lowerq = toLower( d_origdomain );
+        d_querykey = string( lowerq.rbegin(), lowerq.rend() );
+        d_searchkey = d_querykey;
+
+        // For normal queries ensure that we are only trying to get the exact
+        // record and also try to specify the type too to make negatives a lot
+        // quicker
+        if( ! is_axfr ) {
+            d_searchkey += "\t";
+
+            // Search by query type too to easily exclude anything that doesn't
+            // belong to us
+            if( is_full_key )
+                d_searchkey += d_curqtype.getName();
+        }
+
+        key.mv_size = d_searchkey.length();
+        key.mv_data = (char *)d_searchkey.c_str();
+        if( mdb_cursor_get(data_cursor, &key, &value, is_full_key ? MDB_SET_KEY : MDB_SET_RANGE ) )
+            return get_finished();
+    } else {
+next_record:
+        key.mv_size = 0;
+        if( mdb_cursor_get(data_cursor, &key, &value, is_full_key ? MDB_NEXT_DUP : MDB_NEXT ) )
+            return get_finished();
+    }
+
+    // Some buggy versions of lmdb will do this. Should be caught in opendb above though.
+    if( key.mv_size == 0 ) {
+        DEBUGLOG("No key returned. Error" << endl);
+        return get_finished();
+    }
+
+    string cur_value((const char *)value.mv_data, value.mv_size);
+    string cur_key((const char *)key.mv_data, key.mv_size);
+
+    DEBUGLOG("querykey: " << d_querykey << "; cur_key: " <<cur_key<< "; cur_value: " << cur_value << endl);
+
+    vector<string> keyparts, valparts;
+
+    stringtok(keyparts,cur_key,"\t");
+    stringtok(valparts,cur_value,"\t");
+
+    if( valparts.size() == 2 && valparts[0] == "REF" ) {
+        MDB_val extended_key, extended_val;
+
+        // XXX parse into an int and have extended table as MDB_INTEGER to have
+        // a bit better performance/smaller space?
+        extended_key.mv_data = (char *)valparts[1].c_str();
+        extended_key.mv_size = valparts[1].length();
+
+        if( int rc = mdb_cursor_get( data_extended_cursor, &extended_key, &extended_val, MDB_SET_KEY ) )
+            throw PDNSException("Record " + cur_key + " references extended record " + cur_value + " but this doesn't exist: " + mdb_strerror( rc ));
+
+        cur_value.assign((const char *)extended_val.mv_data, extended_val.mv_size);
+        valparts.clear();
+        stringtok(valparts, cur_value, "\t");
+    }
+
+    if( keyparts.size() != 2 || valparts.size() != 3 )
+        throw PDNSException("Invalid record in record table: key: '" + cur_key + "'; value: "+ cur_value);
+
+    string compare_string = cur_key.substr(0, d_searchkey.length());
+    DEBUGLOG( "searchkey: " << d_searchkey << "; compare: " << compare_string << ";" << endl);
+
+    // If we're onto records not beginning with this search prefix, then we
+    // must be past the end
+    if( compare_string.compare( d_searchkey ) )
+        return get_finished();
+
+    int domain_id = atoi( valparts[0].c_str() );
+
+    // If we are doing an AXFR and the record fetched has been outside of our domain then end the transfer
+    if( is_axfr ) {
+        // Check it's not a subdomain ie belongs to this record
+        if( domain_id != d_domain_id )
+            goto next_record;
+
+        // If it's under the main domain then append the . to the comparison to
+        // ensure items outside our zone don't enter
+        if( keyparts[0].length() > d_querykey.length() ) {
+            string test = d_querykey;
+            test.append(".");
+
+            compare_string = cur_key.substr(0, d_querykey.length() + 1);
+
+            DEBUGLOG("test: " << test << "; compare: " << compare_string << ";" << endl);
+
+            if( test.compare( compare_string ) )
+                goto next_record;
+        }
+
+        // We need to maintain query casing so strip off domain (less dot) and append originial query
+        string sub = keyparts[0].substr( d_origdomain.length(), string::npos );
+        rr.qname = string( sub.rbegin(), sub.rend() ) + d_origdomain;
+    } else
+        rr.qname = d_origdomain; // use cached and original casing
+
+    DEBUGLOG("Found record: " <<cur_key << ": "<<valparts.size() << endl);
+
+    DEBUGLOG("pass! " << rr.qname << ";" << endl);
+    rr.qtype = keyparts[1];
+
+    /* Filter records to only match query type */
+    if( d_curqtype != QType::ANY && !is_axfr && rr.qtype != d_curqtype )
+        goto next_record;
+
+    DEBUGLOG("Correct record type" << endl);
+    rr.auth = 1;
+
+    rr.domain_id = domain_id;
+    rr.ttl = atoi( valparts[1].c_str() );
+
+    if( rr.qtype.getCode() != QType::MX && rr.qtype.getCode() != QType::SRV )
+        rr.content = valparts[2];
+    else {
+        // split out priority field
+        string::size_type pos = valparts[2].find_first_of(" ", 0);
+
+        rr.priority = atoi( valparts[2].substr(0, pos).c_str() );
+        rr.content = valparts[2].substr(pos+1, valparts[2].length());
+    }
+
+    return true;
+}
+
+class LMDBFactory : public BackendFactory
+{
+public:
+  LMDBFactory() : BackendFactory("lmdb") {}
+  void declareArguments(const string &suffix="")
+  {
+    declare(suffix,"datapath","Path to the directory containing the lmdb files","/etc/pdns/data");
+  }
+  DNSBackend *make(const string &suffix="")
+  {
+    return new LMDBBackend(suffix);
+  }
+};
+
+/* THIRD PART */
+
+class LMDBLoader
+{
+public:
+  LMDBLoader()
+  {
+    BackendMakers().report(new LMDBFactory);
+
+    L<<Logger::Info<<" [LMDBBackend] This is the LMDBBackend version ("__DATE__", "__TIME__") reporting"<<endl;
+  }
+};
+
+static LMDBLoader lmdbLoader;
+
diff --git a/modules/lmdbbackend/lmdbbackend.hh b/modules/lmdbbackend/lmdbbackend.hh
new file mode 100644 (file)
index 0000000..8a9bc56
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * LMDBBackend - a high performance LMDB based backend for PowerDNS written by
+ * Mark Zealey, 2013
+ */
+
+#include <lmdb.h>
+#include <pdns/dnsbackend.hh>
+
+class LMDBBackend : public DNSReversedBackend
+{
+private:
+
+    MDB_env *env;
+    MDB_dbi data_db, zone_db, data_extended_db;
+    MDB_txn *txn;
+    MDB_cursor *data_cursor, *zone_cursor, *data_extended_cursor;
+
+    // Domain that we are querying for in list()/lookup()/get(). In original case and direction.
+    string d_origdomain;
+
+    // Current QType being queried for
+    QType d_curqtype;
+
+    // Is this the first call to ::get() ?
+    bool d_first;
+
+    // Current domain ID being queried for
+    int d_domain_id;
+
+    // The reversed and lowercase key that we are querying in the database. Set after the first ::get() call.
+    string d_querykey;
+
+    // d_querykey with some additional bits potentially tacked on to make searching faster
+    string d_searchkey;
+
+    void open_db();
+    void close_db();
+    inline bool get_finished();
+
+public:
+    LMDBBackend(const string &suffix="");
+    ~LMDBBackend();
+    bool list(const string &target, int id, bool include_disabled=false);
+    void lookup(const QType &type, const string &qdomain, DNSPacket *p, int zoneId);
+    void reload();
+    bool get(DNSResourceRecord &rr);
+
+    bool getAuthZone( string &rev_zone );
+    bool getAuthData( SOAData &, DNSPacket *);
+};
index bf62b4485edeeaab9412ba0d50e6857ac6c19b7b..0d3b030bb42ec0cb413b90123f5d2296130aa046 100644 (file)
@@ -19579,6 +19579,180 @@ VALUES (:zoneid, :ip)
       </sect2>
     </sect1>
 
+    <sect1 id="lmdbbackend"><title>LMDB (high performance) backend</title>
+      <para>
+       <table>
+         <title>LMDB backend capabilities</title>
+         <tgroup cols="2">
+           <tbody>
+             <row><entry>Native</entry><entry>Yes</entry></row>
+             <row><entry>Master</entry><entry>No</entry></row>
+             <row><entry>Slave</entry><entry>No</entry></row>
+             <row><entry>Superslave</entry><entry>No</entry></row>
+             <row><entry>Autoserial</entry><entry>No</entry></row>
+             <row><entry>DNSSEC</entry><entry>No</entry></row>
+             <row><entry>Module name</entry><entry>lmdb</entry></row>
+             <row><entry>Launch</entry><entry>lmdb</entry></row>
+           </tbody>
+         </tgroup>
+       </table>
+      </para>
+      <para>
+      Based on the <ulink url="http://symas.com/mdb/">LMDB key-value
+      database</ulink>, the LMDB backend turns powerdns into a very high
+      performance and DDOS-resilient authoritative DNS server. Testing on a
+      32-core server shows the ability to answer up to 400,000 queries per second
+      with instant startup and real-time updates independent of database size.
+      </para>
+      <para>
+       <variablelist>
+         <varlistentry>
+           <term>lmdb-datapath=</term>
+           <listitem>
+             <para>
+            Location of the database to load
+             </para>
+           </listitem>
+         </varlistentry>
+       </variablelist>
+      </para>
+      <sect2>
+       <title>Operation</title>
+       <para>
+      Unlike other backends LMDB does not require any special configuration.
+      New or updated zones are available the next query after the update
+      transaction is committed. If the underlying database is removed or
+      recreated then the reload command should be sent through to powerdns to
+      get it to close and reopen the database.
+       </para>
+    </sect2>
+      <sect2><title>Database Format</title>
+       <para>
+        A full example script for generating a database can be found in
+        pdns/modules/lmdbbackend/lmdb-example.pl. Basically the database
+        environment is comprised of three databases to store the data:
+       </para>
+        <sect3><title>zone database</title>
+        <para>
+            Each key in the zone database is the reversed lower-cased name of
+            the zone without
+            leading or trailing dots (ie for example.com the key would be moc.elpmaxe).
+        </para>
+        <para>
+          Each value in the database must contain the following data (tab-separated):
+          <variablelist>
+            <varlistentry>
+              <term>Zone ID</term>
+              <listitem>
+                <para>The Zone's unique integer ID in ASCII (32-bit)</para>
+              </listitem>
+            </varlistentry>
+            <varlistentry>
+              <term>TTL</term>
+              <listitem>
+                <para>The TTL for the zone's SOA record</para>
+              </listitem>
+            </varlistentry>
+            <varlistentry>
+              <term>SOA data</term>
+              <listitem>
+                <para>space-separated SOA data eg
+                <screen>
+                ns.foo.com. hostmaster.foo.com. &lt;serial&gt; &lt;refresh&gt; &lt;retry&gt; &lt;expire&gt; &lt;minimum&gt;
+                </screen>
+                If refresh, retry, expire or minimum are not specified then the powerdns defaults will be used
+                </para>
+              </listitem>
+            </varlistentry>
+          </variablelist>
+        </para>
+        </sect3>
+        <sect3 id="lmdb-data"><title>data database</title>
+        <para>
+            This database is required to have been created with the MDB_DUPSORT flag enabled. It stores the records for each domain.
+            Each key must contain the following data (tab-separated):
+          <variablelist>
+            <varlistentry>
+              <term>Record name</term>
+              <listitem>
+                <para>The reversed lower-cased name of the record and zone without leading or trailing dots</para>
+              </listitem>
+            </varlistentry>
+            <varlistentry>
+              <term>Record type</term>
+              <listitem>
+                <para>The type of record A, NS, PTR etc. SOA is not allowed as it is automatically created from the zone database records.</para>
+              </listitem>
+            </varlistentry>
+          </variablelist>
+          </para>
+          <para>
+            The value for each entry must contain the following data
+            (tab-separated). If the length of this record is greater than the
+            LMDB limit of 510 bytes (for DUPSORT databases) an entry of "REF"
+            followed by the tab character and a unique 32-bit ASCII integer
+            which contains a reference into <xref linkend="lmdb-extended-data" />.
+          <variablelist>
+            <varlistentry>
+              <term>Zone ID</term>
+              <listitem>
+                <para>The Zone's unique integer ID in ASCII (32-bit)</para>
+              </listitem>
+            </varlistentry>
+            <varlistentry>
+              <term>TTL</term>
+              <listitem>
+                <para>The TTL for the SOA record</para>
+              </listitem>
+            </varlistentry>
+            <varlistentry>
+              <term>Record data</term>
+              <listitem>
+                <para>
+                The record's data entry. For MX/SRV records the
+                priority is the first field and space-separated from the rest
+                of the data. Care must be taken to escape the data
+                appropriately for PowerDNS. As in the Pipe backend " and \
+                characters are not allowed and any it is advised that any
+                characters outside of ASCII 32-126 are escaped using the \
+                character.
+                </para>
+              </listitem>
+            </varlistentry>
+          </variablelist>
+        </para>
+        </sect3>
+        <sect3 id="lmdb-extended-data"><title>extended_data database</title>
+        <para>
+        If the length of the value that you wish to insert into <xref
+        linkend="lmdb-data" /> is longer than 510 bytes you need to create the
+        REF entry as described above linked in to this table. The value is a
+        unique 32-bit integer value formatted in ASCII and the value is the
+        exact same format as it would have been in <xref linkend="lmdb-data" />
+        but can be however long you require.
+        </para>
+        </sect3>
+
+        <sect3><title>Example database structure</title>
+        <para>
+        (as output by the pdns/modules/lmdbbackend/lmdb-example.pl example script and shown by pdns/modules/lmdbbackend/dumpdb.pl)
+        <screen>
+        # perl dumpdb.pl /var/tmp/lmdb zone
+        key: moc.elpmaxe; value: 1      300     ns.example.com. hostmaster.example.com. 2012021101 86400 7200 604800 86400
+        # perl dumpdb.pl /var/tmp/lmdb data
+        key: moc.elpmaxe        MX; value: 1    300     10 mail.hotmail.com
+        key: moc.elpmaxe        NS; value: 1    300     ns.example.com
+        key: moc.elpmaxe.tset   A; value: 1     300     1.2.3.4
+        key: moc.elpmaxe.txet   TXT; value: 1   300     test\010123
+        key: moc.elpmaxe.txetgnol       TXT; value: REF 1
+        # perl dumpdb.pl /var/tmp/lmdb extended_data
+        key: 1; value: 1        300     AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+        </screen>
+        </para>
+        </sect3>
+      </sect2>
+    </sect1>
+
   <sect1 id="odbc">
     <title>ODBC backend</title>
       <para>