Monday, September 13, 2010

MySQL versus MongoDB - fetch by secondary index

This continues the silly benchmark series and compares performance from concurrent clients that fetch by secondary key. The previous post compared fetch by primary key. The test setup was the same as before. Clients were run on a 16-core x86 server. The servers (mongod, mysqld) were run alone on 8-core and 16-core x86 servers. The tests were run for servers that were 1ms and 200us apart according to ping. The database server saturates earlier when the client is only 200us away. That is to be expected.

InnoDB tables are clusters on the primary key and a query that fetches all columns by PK only has to read data from one leaf block of the PK index. When all columns are fetched by secondary key then the secondary index leaf node and PK index leaf node must be read. As all data was cached for this test that does not make a big difference. Were data not cached the extra IO used to read the PK index leaf node would be significant.

This displays throughput on the 16-core server. MongoDB saturates earlier on the 16-core server than on the 8-core server. From vmstat output this appears to be mutex contention on the server but I cannot provide more details on where that occurs as the mongod binary I downloaded has been stripped.
This displays throughput on the 8-core server. Peak QPS for MongoDB is much better than on the 16-core server.
This displays response time for the 16 core server.
The test was repeated using a hosts that were 200us apart according to ping. The database host was an 8-core server in this test. The peak QPS is similar to the previous tests but the servers saturate with fewer concurrent clients. The results are here and have been updated to include results for MongoDB 1.6.2 and 1.7.0.

Source code to setup the MySQL table:

def setup_mysql(host, db, user, password, engine, rows):
  filterwarnings( 'ignore', category = MySQLdb.Warning )
  conn = connect_mysql(host, db, user, password)
  conn.autocommit(True)
  cursor = conn.cursor()
  cursor.execute('drop table if exists bm')
  cursor.execute('create table bm (id int primary key, sid int, k int, c char(120), pad char(60), key sidx(sid)) engine=%s' % engine)

  vals = []
  for x in xrange(0, rows):
    sx = str(x)
    lsx = len(sx)
    row = '(%d, %d, %d, "%s", "%s")' % (x, x, x, sx+'x'*(120-lsx), sx+'y'*(60-lsx))
    vals.append(row)

    if len(vals) == 1000:
      r = cursor.execute('insert into bm values %s' % ','.join(vals))
      vals = []
      print '... row %d, result %s' % (x, r)

  if vals:
    r = cursor.execute('insert into bm values %s' % ','.join(vals))
    vals = []
    print '... row %d, result %s' % (x, r)


Source code to query the MySQL table:


def query_mysql(host, db, user, password, pipe_to_parent, requests_per, rows, check, testname, worst_n, id):
  conn = connect_mysql(host, db, user, password)
  conn.autocommit(True)
  cursor = conn.cursor()

  gets = 0
  stats = SummaryStats(worst_n)

  while True:
    for loop in xrange(0, requests_per):

      target = random.randrange(0, rows)

      s = time.time()
      cursor.execute('select id, k, c, pad from bm where sid = %d' % target)
      sel_rows = cursor.fetchall()
      stats.update(s)

      if len(sel_rows) != 1:
        print 'No rows for %d' % target
        assert False
      if sel_rows[0][0] != target:
        print 'id is %s and should be %s' % (sel_rows[0][0], target)
        assert False

      gets += 1

Source code to setup the MongoDB collection:


def setup_mongo(host, port, dbname, rows):
  conn = pymongo.Connection(host, port)
  conn.drop_database(dbname)
  db = conn[dbname]

  for x in xrange(0, rows):
    sx = str(x)
    lsx = len(sx)
    db.c.save({'_id':x, 'sid':x, 'k':x, 'c':sx+'x'*(120 - lsx), 'pad':sx+'y'*(60 - lsx)})
    if x % 1000 == 0:
      print '... row %d' % x

  db.c.create_index('sid')

Source code to query the MongoDB table:

def query_mongo(host, port, pipe_to_parent, requests_per, dbname, rows, check, testname, worst_n, id):
  conn = pymongo.Connection(host, port)
  db = conn[dbname]

  gets = 0
  stats = SummaryStats(worst_n)

  while True:
    for loop in xrange(0, requests_per):

      target = random.randrange(0, rows)

      s = time.time()
      o = db.c.find_one({'sid': target})
      stats.update(s)

      assert o['_id'] == target
      if check:
        assert o['k'] == target
        sx = str(o['_id'])
        lsx = len(sx)
        assert o['c'] == sx+'x'*(120-lsx)
        assert o['pad'] == sx+'y'*(60-lsx)

      gets += 1

13 comments:

  1. "The database server saturates earlier when the client is only 200ms away".

    Should that be s/ms/us/ ?

    ReplyDelete
  2. I assume the MySQL results are obtained with binlog disabled, no?

    Could you do a benchmark comparing MySQL & MongoDB (& perhaps Postgresql) with replication on? An Achilles heel of MySQL is that performance tends to take a massive drop with binlog turned on (and sync_binlog=1).

    But binlog is needed for an HA setup. So I wonder how MySQL, MongoDB, Postgresql stack up against each others in an HA settng.

    ReplyDelete
  3. The binlog is irrelevant for this test. This test is read-only.

    ReplyDelete
  4. Hi!

    Is there anyway I could get access to this machine and try the same setup with Drizzle? Possibly with more connections then what you are using in your tests?

    Thanks!

    -Brian

    ReplyDelete
  5. Brian - you can't but I can. What code should I use (branch, revno)?

    ReplyDelete
  6. Hi!

    Just pull trunk, or even the latest version. Our sysbench normally shows us on par or slower for low number of connections (which we spend no time optimizing for). If you crack it up past 128 till about 1024 I would be curious to see what you find.

    You should be able to just use the mysql client connector. If you run into any issues just IM me.

    Thanks,
    -Brian

    ReplyDelete
  7. Also, I am curious how *uniform* the response times and throughputs are for each. If you graph the QPS over time in 1-second intervals, are the lines smooth or jerky? How does the stddev of response times look? (hint: tcprstat + gnuplot is the bomb for this.)

    ReplyDelete
  8. I will get the vmstat output as soon as I enhance my python to display avg and p98 response time per interval.

    ReplyDelete
  9. No vmstat yet, but this is an example of a pileup found by http://poormansprofiler.org in mongod 1.7.0. This one should be easy to fix. Alas scons won't build mongod on my centos 5.2 server and after 1 hour I gave up on figure out why it could not find libstdc++

    Anyhow, here it the thread stack where there is too much mutex contention:
    __lll_mutex_lock_wait,_L_mutex_lock_107,pthread_mutex_lock,mongo::Top::record,mongo::CurOp::leave,mongo::Client::Context::~Context,mongo::runQuery,mongo::Database::closeDatabase,mongo::assembleResponse

    ReplyDelete
  10. For a non-stripped version: http://fastdl.mongodb.org/linux/mongodb-linux-x86_64-debugsymbols-1.7.0.tgz

    There is a lot performance tuning left to do on mongo, help is always welcome :)

    If you post to mongodb-user or mongodb-dev I'm sure we can help get the compilation issue fixed. Don't forget to look at config.log

    ReplyDelete
  11. I was able to run PMP and report on one source of mutex contention using the public downloads so symbols are there. The result is listed above and I sent it to Roge. The contention listed above should be easy to fix and that might make a big difference.

    I suppose I will ask for help on one of your mailing lists to figure out why the scons build fails for me. Modern software libraries and not so modern Linux distributions are not a great match. I am also unable to build Drizzle there.

    ReplyDelete

 
Creative Commons License
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.