diff options
Diffstat (limited to 'src/mongo/tools/loadgenerator.cpp')
| -rw-r--r-- | src/mongo/tools/loadgenerator.cpp | 418 |
1 files changed, 418 insertions, 0 deletions
diff --git a/src/mongo/tools/loadgenerator.cpp b/src/mongo/tools/loadgenerator.cpp new file mode 100644 index 00000000000..1fb10a1e3e0 --- /dev/null +++ b/src/mongo/tools/loadgenerator.cpp @@ -0,0 +1,418 @@ +/** @file loadgenerator.cpp */ + +/** + * Copyright (C) 2012 10gen Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +/* + * LoadGenerator drives a certain number (# threads) of simultaneous operation into a + * specified number of databases as quickly as it can at a mongo instance, + * continuously, for some number of seconds. + * + */ + +/* + * For internal reference: + * Each document generated by the docgenerator.cpp is 176 bytes long + * Number of documents per instance size : + * small (500 MB) : 2978905 docs spread over 5 dbs (each db is 100 MB) Docs Per DB : 595781 + * medium (5 GB) : 30504030 docs spread over 5 dbs (each db is 1 GB) Docs Per DB : 6100806 + * large (25 GB) : 152520145 docs evenly spread over 5 dbs (each db is 5 GB) Docs Per DB : 30504029 + * vlarge (100 GB) : 621172954 docs evenly spread over 10 dbs (each db is 10 GB) Docs Per DB : 61008058 + * + */ + +#include <map> +#include <string> + +#include <boost/program_options.hpp> +#include <boost/scoped_ptr.hpp> + +#include "mongo/util/assert_util.h" +#include "mongo/scripting/bench.h" +#include "mongo/client/dbclientinterface.h" +#include "mongo/tools/docgenerator.h" +#include "mongo/util/map_util.h" +#include "mongo/util/md5.hpp" +#include "mongo/util/mongoutils/str.h" + +using namespace std; + +using namespace mongo; + +namespace po = boost::program_options; + +namespace { + +struct LoadGeneratorOptions { + LoadGeneratorOptions() : + hostname("localhost"), + type("query"), + instanceSize( "large" ), + numdbs( 5 ), + resultNS( "" ), + numOps( 60000 ), + durationSeconds( 60 ), + parallelThreads( 32 ), + trials( 5 ), + docsPerDB( 0 ) + { } + + string hostname; + string type; + string instanceSize; + int numdbs; + string resultNS; + int numOps; + double durationSeconds; + int parallelThreads; + int trials; + unsigned long long docsPerDB; +}; + + +struct OperationStats { + OperationStats( const unsigned long long numEvents, const unsigned long long totalTimeMicros, + const long long opcounter ) : + numEvents( numEvents ), + totalTimeMicros( totalTimeMicros ), + opcounter( opcounter ) { } + OperationStats() { } + + unsigned long long numEvents; + unsigned long long totalTimeMicros; + long long opcounter; +}; + +// ------Globals and typedefs---------- +LoadGeneratorOptions globalLoadGenOption; +typedef std::map<std::string, OperationStats> OpStatsMap; + +double randomBetweenRange(const int& min, const int& max) { + return rand() % (max - min) + min; +} + +mongo::DBClientBase *getDBConnection() { + string errmsg; + mongo::ConnectionString connectionString = mongo::ConnectionString::parse( + globalLoadGenOption.hostname, errmsg ); + mongo::fassert( 16265, connectionString.isValid() ); + mongo::DBClientBase *connection = connectionString.connect( errmsg ); + mongo::fassert( 16266, connection != NULL ); + return connection; +} + +void dropNS(const string ns) { + boost::scoped_ptr<mongo::DBClientBase> connection( getDBConnection() ); + connection->dropCollection( ns ); +} + +void dropDB(const string db) { + boost::scoped_ptr<mongo::DBClientBase> connection( getDBConnection() ); + connection->dropDatabase( db ); +} + +void writeToNS(const string ns, const mongo::BSONObj bs) { + boost::scoped_ptr<mongo::DBClientBase> connection( getDBConnection() ); + connection->insert( ns, bs ); + mongo::fassert( 16267, connection->getLastError().empty() ); +} + +// find the number of documents in a namespace +void numDocsInNS(const string ns) { + boost::scoped_ptr<mongo::DBClientBase> connection( getDBConnection() ); + globalLoadGenOption.docsPerDB = connection->count( ns ); +} + + +mongo::BSONArray generateInsertOps() { + + //create a document config object + BSONObj args = BSONObjBuilder() + .append( "blob", "MongoDB is an open source document-oriented database system." ) + .append( "md5seed", "newyork" ) + .append( "counterUp", 0 ) + .append( "counterDown", numeric_limits<long long>::max() ).obj(); + + scoped_ptr<DocumentGenerator> docGen( DocumentGenerator::makeDocumentGenerator(args) ); + mongo::BSONArrayBuilder insertOps; + for (int i = 0; i < globalLoadGenOption.numOps; ++i) { + + //insert into databases + string insertNS = mongoutils::str::stream() << globalLoadGenOption.instanceSize << "DB" << + i % globalLoadGenOption.numdbs << "I.sampledata"; + BSONObj doc = docGen->createDocument(); + insertOps.append( BSON( "ns" << insertNS << + "op" << "insert" << + "doc" << doc << + "safe" << true ) ); + } + return insertOps.arr(); +} + + +mongo::BSONArray generateFindOneOps() { + + mongo::BSONArrayBuilder queryOps; + + // query a namespace and find the number of docs in that ns. All benchmark namespaces should have + // the same number of docs. + string queryNS = mongoutils::str::stream() << globalLoadGenOption.instanceSize + << "DB0.sampledata"; + numDocsInNS( queryNS ); + + // Now fill the queryOps array. The findOne query operations will be evenly distributed + // across all databases. Thus it tries to find a random document from db1, then db2, + // and so on and so forth. + for (int i = 0; i < globalLoadGenOption.numOps; ++i) { + + queryNS = mongoutils::str::stream() << globalLoadGenOption.instanceSize + << "DB" + << i % globalLoadGenOption.numdbs + << ".sampledata"; + + // select a random document among all the documents + unsigned long long centerQueryKey = + ( randomBetweenRange(0, 100 ) * globalLoadGenOption.docsPerDB ) / 100; + + // cast to long long from unsigned long long as BSON didn't have the overloaded method + mongo::BSONObj query = + BSON( "counterUp" << static_cast<long long>( floor(centerQueryKey) ) ); + + queryOps.append( BSON( "ns" << queryNS << + "op" << "findOne" << + "query" << query) ); + } + + return queryOps.arr(); +} + + +mongo::BenchRunConfig *createBenchRunConfig() { + + BSONArray ops; + + if ( globalLoadGenOption.type == "findOne" ) + ops = generateFindOneOps(); + else if ( globalLoadGenOption.type == "insert" ) + ops = generateInsertOps(); + + return mongo::BenchRunConfig::createFromBson( + BSON( "ops" << ops << + "parallel" << globalLoadGenOption.parallelThreads << + "seconds" << globalLoadGenOption.durationSeconds << + "host"<< globalLoadGenOption.hostname ) ); +} + +/* + * The stats object from benchRun has two sub-objects : findOneCounter and opcounters + * This function creates a single map data structure to store all the info. + */ + +void collectAllStats( const BenchRunStats& stats, OpStatsMap& allStats ) { + + allStats.clear(); + allStats.insert( std::make_pair("findOne", + OperationStats(stats.findOneCounter.getNumEvents(), + stats.findOneCounter.getTotalTimeMicros(), + mapFindWithDefault<std::string, long long> + (stats.opcounters, "query", 0) + )) ); + allStats.insert( std::make_pair("insert", + OperationStats(stats.insertCounter.getNumEvents(), + stats.insertCounter.getTotalTimeMicros(), + mapFindWithDefault<std::string, long long> + (stats.opcounters, "insert", 0) + )) ); + +} + +// add the result of this trial to the trials array +BSONObj makeTrialDocument( const OpStatsMap& allStats ) { + + BSONObjBuilder outerBuilder; + for (OpStatsMap::const_iterator it = allStats.begin(); it != allStats.end(); ++it) { + + unsigned long long numEvents = it->second.numEvents; + unsigned long long totalTimeMicros = it->second.totalTimeMicros; + + BSONObjBuilder innerDocBuilder; + innerDocBuilder.append("numEvents", static_cast<long long>(numEvents)); + innerDocBuilder.append("totalTimeMicros", static_cast<long long>(totalTimeMicros)); + + if (numEvents) + innerDocBuilder.append("latencyMicros", static_cast<double>(totalTimeMicros/numEvents)); + + outerBuilder.append(it->first, innerDocBuilder.obj()); + } + return outerBuilder.obj(); +} + +BSONObj buildInformation() { + boost::scoped_ptr<mongo::DBClientBase> connection( getDBConnection() ); + BSONObj info; + connection->simpleCommand("admin", &info, "buildinfo"); + return info; +} + +BSONObj createResultDoc(const BSONArray& trialsArray) { + + return BSON( "name" << globalLoadGenOption.type << + "config" << BSON ( "hostname" << globalLoadGenOption.hostname << + "instanceSize" << globalLoadGenOption.instanceSize << + "durationSeconds" << globalLoadGenOption.durationSeconds << + "parallelThreads" << globalLoadGenOption.parallelThreads << + "numOps" << globalLoadGenOption.numOps << + "Date" << 10 << + "buildInfo" << buildInformation() + ) << + "trials" << trialsArray ); +} + +void runTest() { + stringstream oss; + BSONArrayBuilder trialsBuilder; + + // drop any previous dbs with the same name + for (int j=0; j < globalLoadGenOption.numdbs; ++j) { + string insertTestDB = mongoutils::str::stream() << globalLoadGenOption.instanceSize << + "DB" << j <<"I" ; + dropDB(insertTestDB); + } + + for (int i = 0; i<globalLoadGenOption.trials; ++i) { + + BenchRunner runner( createBenchRunConfig() ); + runner.start(); + sleepmillis( 1000 * globalLoadGenOption.durationSeconds ); + runner.stop(); + BenchRunStats stats; + runner.populateStats(&stats); + + // collate all the stats (and save it in a local allstats map + std::map<std::string, OperationStats> allStats; + collectAllStats(stats, allStats); + + trialsBuilder.append(makeTrialDocument(allStats)); + + // print for now -- this is temporary and will be removed + oss << allStats.find("insert")->second.totalTimeMicros / allStats.find("insert")->second.numEvents << + " " << + allStats.find("insert")->second.opcounter / globalLoadGenOption.durationSeconds << + " "; + + //clean up the newly created dbs for next trial + for (int j=0; j < globalLoadGenOption.numdbs; ++j) { + string insertTestDB = mongoutils::str::stream() << globalLoadGenOption.instanceSize << + "DB" << j <<"I" ; + dropDB(insertTestDB); + } + } + // Write the experiment document to the result NS. If the user did not pass a resultNS cmdline + // parameter then we won't write the results to the database. + // This is useful in cases where we just want to drive a constant load from a client and are + // not really interested in the statistics from it and so don't really care to save the stats + // to a db. + + string resultNS = globalLoadGenOption.resultNS; + if( !resultNS.empty() ) { + BSONObj resultDoc = createResultDoc(trialsBuilder.arr()); + writeToNS( resultNS, resultDoc ); + } + + // temp line -- will be removed + cout << oss.str() << endl; +} + + +int parseCmdLineOptions(int argc, char **argv) { + + try { + + po::options_description general_options("General options"); + + general_options.add_options() + ("help", "produce help message") + ("hostname,H", po::value<string>() , "ip address of the host where mongod is running" ) + ("type", po::value<string>() , "findOne/insert" ) + ("instanceSize,I", po::value<string>(), "DB type (small/medium/large/vlarge)" ) + ("numdbs", po::value<int>(), " number of databases in this instance" ) + ("trials", po::value<int>(), "number of trials") + ("durationSeconds,D", po::value<double>(), "how long should each trial run") + ("parallelThreads,P",po::value<int>(), "number of threads") + ("numOps", po::value<int>(), "number of ops per thread") + ("resultNS", po::value<string>(), "result NS where you would like to save the results." + "If this parameter is empty results will not be written") + ; + + po::variables_map params; + po::store(po::parse_command_line(argc, argv, general_options), params); + po::notify(params); + + // Parse the values if supplied by the user. No data sanity check is performed + // here so meaningless values (for eg. passing --numdbs 0) can crash the program. + // TODO: Perform data sanity check + + if(params.count("help")) { + cout << general_options << "\n"; + return 1; + } + if (params.count("hostname")) { + globalLoadGenOption.hostname = params["hostname"].as<string>(); + } + if (params.count("type")) { + globalLoadGenOption.type = params["type"].as<string>(); + } + if (params.count("instanceSize")) { + globalLoadGenOption.instanceSize = params["instanceSize"].as<string>(); + } + if (params.count("numdbs")) { + globalLoadGenOption.numdbs = params["numdbs"].as<int>(); + } + if (params.count("trials")) { + globalLoadGenOption.trials = params["trials"].as<int>(); + } + if (params.count("durationSeconds")) { + globalLoadGenOption.durationSeconds = params["durationSeconds"].as<double>(); + } + if (params.count("parallelThreads")) { + globalLoadGenOption.parallelThreads = params["parallelThreads"].as<int>(); + } + if (params.count("numOps")) { + globalLoadGenOption.numOps = params["numOps"].as<int>(); + } + if (params.count("resultNS")) { + globalLoadGenOption.resultNS = params["resultNS"].as<string>(); + } + } + catch(exception& e) { + cerr << "error: " << e.what() << "\n"; + return 1; + } + return 0; +} + +} // namespace + + +int main(int argc, char **argv) { + + if( parseCmdLineOptions(argc, argv) ) + return 1; + + runTest(); + return 0; +} + |
