Showing posts with label mongodb. Show all posts
Showing posts with label mongodb. Show all posts

Monday, 7 September 2020

The Case for MongoDB single sharded cluster
(or why PSA/PSS is suboptimal)

Recently I have bumped into a few clients who have hit issues with MongoDB, due to growth. Without fail these have been down to a couple of issues, but centred on the set up of Primary/Secondary/Arbiter (PSA) or Primary/Secondary/Secondary (PSS), and then trying to squeeze performance out of that setup.

Now, I understand that these were, almost exclusively,  set up by front end devs (nothing wrong with that), but the issue lies with just assuming that MongoDb is a Relational database. We all feel warm and cuddly about a Relational DB, its Master /Slave (Primary/Secondary) approach, and the fact that you write to a master (primary), and read from a slave (secondary). So Mongo gets set up in exactly the same way, and all is wonderful… up until we have squeezed that last ounce of performance out of it, and things still dont work well. 

“Isn’t MongoDb one of the fastest most efficient Dbs on the Market? So why is my site dying?” 

Lets look at why PSA is not a great idea

First off.. if you are using MongoDb on a site that you know for certain will never, in the lifetime of you , your kids and grandkids, require more than one copy of the data, on a single machine.. please ignore this part of the post, and carry on setting up as PSA… or PSS. 

Ok.. so lets look at a typical scenario. It has to be said, that most issues surround the usage of memory/cache by wiredTiger (WT), Mongodbs default storage engine. WT is the main reason why MongoDb is fast…. blindingly fast.. if used properly.

Sample setup of PSS/PSA
WT will use the max of 50% of memory – 1Gb or 256Mb memory whichever is bigger. What it does with this is:
1. Load all indexes that it can into memory
2. Assign memory to connections. You should never have to have more that 5k connections. If you do, something is wrong with your
application.
3. Load as much of recently used data into memory as it can, with what is left over.

The remaining memory, less whatever the OS is taking up, will be used by MongoDb for dealing with query results (aggregation/sorting/etc) , and communicating to your application (this is important).

So lets assume you have gone large on this, and have a 128 Gb memory box. WT will take 63.5 Gb. This means that all your indexes will need to take up 63.5Gb as a maximum, preferably less. Any extra space will be used as data space from the most recently used data. MongoDB will itself use the remaining 63.5Gb.

You now check your usage, and see that indexes are taking no more than 23Gb. Thats great.. plenty of room for expansion, plenty of room for data... all is going swimmingly well. Phew!

Then one day, it all starts slowing up. The Primary keeps swapping out, and needs restarting, queries that were fine are now hitting the slow logs, and all manner of weird stuff is occurring. No real reason for this. You know data has increased, but you have done no physical changes to the setup, COLLSCANS are not happening, IDXSCANS are not happening. So what has changed?

The chances are that your indexes have increased, and the amount of data has increased as well (obviously) , and essentially nothing actually ‘fits’ anymore. This leads to WT having to swap out old data, then also swap out indexes that are least useful, in order to reread those that are used. This can be confirmed by looking at the stats, or, if you are using a valid logging system (Atlas, Datadog
etc etc) .. that shows this.

So what happens now?

In no particular order, I have seen the following, and none of these provide a permanent fix for the situation...

Add to the cache
More memory can be taken from the OS, and added to the actual memory used by the mongod. This enables the mongod (data) unit to use more memory, but reduces the amount available to the Mongo unit for sorts, and pipelines.
It is a valid ’fix’, but will only be temporary until the data increases some more... when we are back to square one.

Increase the machine size
If you are in the cloud, this is relatively simple, if not, it involves provisioning new hardware, either on premise or through your hosting service, in which case, it is not a ‘quick fix’. It also buys you time only, at extra cost.

Neither actually solve the issue of a growing business. Sure, you now have extra memory, and probably extra disk space, but you have not actually solved the issue for the long term.

Split the data into different databases
This comes form the mistaken idea that some client data being accessed more than others will reduce the index usage, and data usage, allowing your more important data to stay in memory, reducing the actual memory usage.
What this actually does is slow the loading of indexes and data, by not loading into cache until the database is used. However, typically, all your databases will be used at some point by the system (if they aren't... why are they there?) .
Next, what this does is increase your filehandle usage at the OS level. Each index is opened and read... and the filehandle is kept open so that the refreshed data can be read in quicker, when changes are made. So for every index there is an open file. 

At the OS level this can be bad also.
Lets say you have, to keep it simple, one database, with 10 collections, and 15 clients. Each collection has 8 indexes. So for this one database you have 80 index files open. You then split that to one database per client. Now we have 15 versions of the above, so 80*15 or 1200 index files open.
This is a small example. However, if you look at this Mongo Jira ticket I found, you will see that some people struggle because they have, for example, 280 Databases, 47,891 Collections and 270,078 Indexes. 
Generally there are 65000 max file handles allowed.. so you can see the issue. With 1 database this user would have, 1 database, 171 collections and 965 indexes, or similar. A whole different story.

Split the collections into multiple collections
This is similar to the effect of splitting into multiple databases. I have seen this on more than one occasion with stats type data. Due to issues with the database, the collections were split into ‘daily’ or ‘weekly’ collections. 
All this does is increase the complexity, multiply the number of indexes used by 7, 14.. whatever, and leave you with a collection management issue, with no real gain.
Further, this leads to having monthly / yearly aggregation collections, as its no longer possible to use a single collection to get this data. So the storage and index usage increases further, which is exactly what we are trying to reduce.

Multiple Clusters
Finally, because all of the above have failed to lead to a stable system, the decision is made to have multiple clusters, with sets of data completely separated from each other.

This solves the issue, as essentially the data is being split completely, similar to each cluster representing all the data on it as though it was the total amount of data.

However, this is also an issue elsewhere. What you now need to do is execute queries against the correct cluster for that dataset, miss out on the ability to have a central source for all your companies data, and a need to query more than one cluster to get a single result across the company. 

Think of a simple question like:
How many clients do we have? 
What was the client with the highest actvity last week?

Add to this, that the pattern this leads to is having multiple clusters, all doing the same thing, but with added maintenance and complexity for your sysadmins to deal with... multiple backups.. all manner of costs for no particular gain.

Ok, so how do we solve this from the get go?
What we should do is set MongoDb up as a single sharded cluster. This involves the following units, and please bare with me.

Mongod :  Data/Shard nodes.
Arbiter   : At least one arbiter.
Mongos  : Query Nodes... these are called by the application, and call the data nodes, and config servers, directly... see below.
Config server(s) : Ideally a cluster of 3 for larger systems, if small you could get away with 1 or 2... but what happens if they die?

MongoDB single sharded cluster
Arbiters
These do what they always do, keep the primaries and secondaries in order, and help Mongodb work out which shard server should be a primary.

Config Servers
These hold metadata, and the distribution of the shards across the shard servers .. your current data nodes.

Shard/Data nodes
These hold your actual data. For a PSA / PSS setup, you will have one set of these, which can also be replicated.

Mongos nodes
These issue the queries. They take information from the config servers, send the ‘data gathering’ part of the query to the relevant shard nodes, and deal with any aggregations, sorting and extra work locally to the Mongos.
Importantly, the Mongos can reside on the same machine as your application/web servers, if they are big enough. No data on these, only memory requirements.

Extra work required.
Each collection will need a ‘shard’ key, which should be as unique as possible, and NOT the primary key. Further, the major use indexes should have the shard key as the first field in the index... it just makes life easier for data retrieval.

What does this achieve?

Separation of Concerns

From the outset, we have split the workload on the data node(s) from performing data retrieval and aggregation, to just performing the data retrieval, with any very basic aggregation and/or sorting for data on the relevant node.

The Mongos nodes perform any aggregations/sorting locally to itself , once it has received all the data from the data node(s).
This means that the data nodes are now limited to just data retrieval, allowing more memory for this task. 
Further it  means that you have a choice. If the application slows up due to more users, or more aggregations, then you can simply add more load balanced mongos units.
If your data is now the cause of any issues, you simply add more shard nodes, splitting the data down again. This is ‘horizontal sharding’ at its best. 

Separation of Concerns and Expand your Cluster
Expand your Cluster where it is needed
As your data grows, and the system starts to become overused, you simply add more shard/data nodes. 

For each new shard node, 
Mongodb will automatically split your data across the new nodes. So, if it is in a single shard (PSA) mode, adding a new data node will halve the data on the current node (effectively doubling the available memory), more than likely giving you the space you need, without increasing the individual node size.

Remember, if you have your shard servers as a replica set, you will need to add a server per member, so if you have a Primary and a Secondary, you will need to add 2+ new servers, so the new shards work as Primary and Secondary also.

If your user base grows, or more time is taken with aggregations, you add more load balanced Mongos units, as mentioned above.

This setup actually solves all the issues mentioned in the first part of this blog. If you have split your databases by client, then you should really put them back into one, with the client field as a part of each collection.

You wanted Speed?
Well, now you can have it. The Primaries can all now be relatively smaller units, and can be set up as ‘In Memory’ databases. 
This depends on the total size of your data and indexes, which should now be a lot smaller.
Having done this with a 15-20 node cluster in the past, this made a huge difference to performance. Ensuring that all your data and indexes.. per node... fit into the nodes memory on the Primaries, increased the speed of throughput on writes. We were also able to add ‘important quick queries’ back to the primaries, to make them even faster.

Sunday, 15 June 2014

An adventure with an ELK .. or how to replace Splunk

ELK - Elasticsearch/Logstash/Kibana

Well heres a thing. You are reading the ramblings of someone who has kept away from the realms of FB, Twitter, and Blogs in General. The dog has been prolific, but I have kept away for no good reason, except I didn't see the point.

Finally I get it. I really get it. I came across a product stack that would really really help, I tried installing , but found the docs missing in the areas that were really really necessary. They just didn't result in what I needed.. a working product.

So, reader, let me explain the scenario (and for those that just need instructions on how to install the ELK stack.. just scroll on down.. I won't be offended.. I am first an foremost a developer who can't RTFM):

I have a client (nameless for the time being, due to business reasons). It is a startup. I have built a largish database setup as it uses data.. lots of it. Strange and magical data. It gets queried in strange ways, it will only get stranger and bigger. They were using MongoDB, but it had been set up by someone who didn't see the beauty of this product. So it is now set up in a fully extensible way, on a number of machines, as 3 shards of 3 replicas each shard. Thats actually on 7 machines, but we can easily expand out to thousands.

The issue? Once up and working we had an issue. MongoDB comes with monitoring software (mms) its cool.. it works... but it misses the salient points of how the databases are split, what the queries are, the time it takes to execute queries... and importantly... how the heck do I visualise this?

I had been using Splunk for a couple of years. Its good... it gives you data, not necessarily live kickass data, but it does what it says on the tin.... and costs a few k dollars/sterling/Euros (Eeek... euros?? what a dumb name that is.. but I digress) per year to run.. per server... not good.
I trawled the web, I looked at various options, even juggling around with some code I wrote for another client in the MySql world... but nothing there in the short term (did I mention it is a startup with the need for speed?).

Queries by Shard (Fig 1)
I stumbled on Kibana... it mentioned logstash... I had briefly used this a year or so ago in the Mysql world.. but it failed. Now, though, its owned by the good folk at Elasticsearch. We are already using ES, the graphs looked great, the data would fit straight in (json logs -> json ES -> json reading Kibana). Whooppeee! Issue solved. I mean who wouldn't want an live output like this:


Query throughput by server (Fig 2)
Well.. not quite. To say I had a few major hiccups with the docs is an understatement, but with help from the amazing  Steve Mayzak of Elasticsearch, I finally got it working for us. As a result I was asked to document the process... so here we are.

Log Enquiry (Fig 3)

Briefly then:
1. Apologies for those that follow the Seattle boys.. I am a Linux Open Source  kindda guy. For those that like their fruit cidery, this will probably work with a  few tweaks, as Steve followed the right type of OS.
2. Our setup, as I mentioned, involves 7 machines running MongoDB, plus 3 running Elasticsearch, spread over 2 data centres. We also have multiple points of entry to both systems, so we have failover in case of a whole data centre being taken out.
3. (2) Does not preclude the single use machine, it just has relevance to those who try to understand some of the manoevres here.

On with the show:
You need to plan a bit first:
1. Which Machine(s) are you going to put ES on? You should have, even with a single node ES unit, a seperate 'sentinel' (no data) access unit. You will need master/slave/ shards at some point.. plan for it now. You do all your connections through the sentinel, it worries about getting the data for you. Make life easy for yourself. (If you are already a Mongo user, its the same as a mongos unit).
2. Which machine are you going to put Kibana (the graph end) on?
3. You will need Java. ES uses Lucene which uses Java. You will need Logstash, which uses Java. Essentially... you need Java. None of the programming/script building you do is Java, just the tools use it.

Elasticsearch (ES):
1. Install java. You may have to replace the version number..
        sh>sudo apt-get install openjdk-7-jre
2. Repeat on all systems using ES, logstash, and probably Kibana for good
    measure.
3. Check http://www.elasticsearch.org/download for the latest version of ES
4. sh>wget https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-[versionnumber].deb (current 1.2.1)
5. sudo dpkg -i elasticsearch-[versionnumber].deb
6. sudo service elasticsearch start
Thats gets you a node running. If you are building a cluster, you will need to change some bits in teh /etc/elasticsearch.yaml file. If you are not you will also need to change some items. The basic list is here... there is plenty of well documented sections on this elsehere.. so I wont bore you.
The essential elements are here:
            cluster.name: thisCluster
            node.name: thisNode
            node.data: true            (false if the sentinel)
            node.master: true        (always true, so it can be voted a master if the
                                                  current master dies)
            index.number_of_shards: 5 (the default)
            index.number_of_replicas: 2 (or however many masters+slaves you have)
            the paths for data/configs/ and logs
            network.bind_host: thisIP
            network.publish_host: thisIP (the Network address other nodes will use
                                                 to talk to this one).
            http.port: 9200               (default)
            transport.tcp.port:9300 (default)
            discovery.zen.minimum_master_nodes: 1
            discovery.zen.ping.multicast.enabled: false (forces Unicast)
7. Be careful. If installing a cluster, it wont work if one node is a different
    version of ES.
8. If you need an answer as regards replicas the ES way... can I suggest looking here
9. sh> sudo service elasticsearch restart  (with your .yaml changes) 
10. Make sure you have ES set up on each node you want to run it, and also any sentinel machines.

LogStash - The bit that reads the mongo logs and dumps to ES

1. Install Java... you have installed java haven't you?
2. Make sure you have a java tmp folder, that is fully writable/readable
     sh>mkdir /home/[user]/javatmp
3. Set the following environment valriables:
    sh>JAVA_HOME="/usr/lib/jvm/java-7-openjdk-amd64/"
    sh>source /etc/environment
    sh>_JAVA_OPTIONS=-Djava.io.tmpdir=/home/[user]/javatmp
4. Set them in the profile options (debian ubuntu... others roll as per your
    version)
    sudo nano /etc/profile.d/logstash.sh
       export JAVA_HOME=/usr/lib/jvm/java-7-openjdk-amd64/
       export _JAVA_OPTIONS=-Djava.io.tmpdir=/home/[user]/javatmp
    sh> sudo chmod 0755 /etc/profile.d/logstash.sh
5. From youor home directory
        sudo wget https://download.elasticsearch.org/logstash/logstash/logstash-1.4.1.tar.gz
6. tar -xvf logstash-1.4.1.tar.gz
7. Edit/Create mongoqry.conf - this bit is important as it tells ES/Lucene how to handle your data, which in turn will help with Kibana using it correctly.
       input {
        file {
          discover_interval => 10
          add_field => { host => "your_host"}
          add_field => { shard => "your_shard"}
          path => ["/var/log/mongodb/your_mongo_log.log"]
          start_position => "beginning"
          tags => ["your_host", "mongo","other_tags"]
          type => "your_host_log_type"          <-- This helps in Kibana to identify these entries
      }
   }
   filter {
     grok { pattern => ["(?m)%{GREEDYDATA} \[conn%{NUMBER:mongoConnection}\] %{WORD:mongoCommand} %{NOTSPACE:mongoDatabase} %{WORD}: \{ %{GREEDYDATA:mongoStatement} \} %{GREEDYDATA} %{NUMBER:mongoElapsedTime:int}ms"] }
    grok { pattern => [" cursorid:%{NUMBER:mongoCursorId}"] }
    grok { pattern => [" ntoreturn:%{NUMBER:mongoNumberToReturn:int}"] }
    grok { pattern => [" ntoskip:%{NUMBER:mongoNumberToSkip:int}"] }
    grok { pattern => [" nscanned:%{NUMBER:mongoNumberScanned:int}"] }
    grok { pattern => [" scanAndOrder:%{NUMBER:mongoScanAndOrder:int}"] }
    grok { pattern => [" idhack:%{NUMBER:mongoIdHack:int}"] }
    grok { pattern => [" nmoved:%{NUMBER:mongoNumberMoved:int}"] }
    grok { pattern => [" nupdated:%{NUMBER:mongoNumberUpdated:int}"] }
    grok { pattern => [" keyUpdates:%{NUMBER:mongoKeyUpdates:int}"] }
    grok { pattern => [" numYields: %{NUMBER:mongoNumYields:int}"] }
    grok { pattern => [" locks\(micros\) r:%{NUMBER:mongoReadLocks:int}"] }
    grok { pattern => [" locks\(micros\) w:%{NUMBER:mongoWriteLocks:int}"] }
    grok { pattern => [" nreturned:%{NUMBER:mongoNumberReturned:int}"] }
    grok { pattern => [" reslen:%{NUMBER:mongoResultLength:int}"] }
  }
  output {
    elasticsearch {
      index => "logstash"
      host => "your_es_sentinel"
      protocol => "http"
      bind_port => 9200
      manage_template => true
   }
   stdout {codec=> json  }
 } 

This will output to ES as well as to screen. At this point I would like to give thanks to the people at techblog.holidaycheck.com for the filter command, which I unashamedly lifted from their git repo. Thanks guys, saved a ton of work.

8. Repeat on all other mongo units, changing the inputs to teh server of your choice, but keep the outputs.

9. Logstash even has teh decency to stay with a rolled file. So if you swap your main log file every hour or so, it will stay with the original filename, acting similarly to 'tail -f ' so yoou miss nothing.. good old logstash.

10 Finally, start her up. You may want to do this inside a 'screen' so you can leave it running:
     sh>sudo bin/logstash -f mongoqry.conf
                            or to Daemonise
     sh>sudo bin/logstash -f mongoqry.conf &

If you get  This: (LoadError) Could not load FFI Provider: (NotImplementedError) FFI not available: null
      The /javatmp is not executable... make sure it is executeable
   Can also be seen if the timestamp filter is out of date... maybe


Right.. still with me? Well done. Finally... yes indeedy.. finally you need Kibana and a webserver, if you don't have one on the machine kibana will be installed on.

Kibana - The Logging output and pretty stuff
1. On your chosen machine for Kibana, install a web server (nginx/apache ..  
    whatever).
2. Ensure your webserer can listen on the port you want to connect to (443 for
    https is advisable).
3. Ensure the servers doc folder is pointing to where kibana will be.
4. Install Kibana
    sh> wget https://download.elasticsearch.org/kibana/kibana/kibana-3.1.0.tar.gz
    sh> tar -xvf kibana-3.1.0.tar.gz
5. In the kibana default directory will be a file called config.js. In this the  
    following will need changing:
      elasticsearch: Make sure the port number is :9200.
                             If your sentinel is on the same machine, you will be Ok,
                                 otherwise you will need to change the destination of ES as
                                 well.
      kibana-index: A name for your kibana index. Your kibana dashboards also
                            stored in ES... which is kindda useful.
6. Navigate to your host...and you should have the Kibana welcome screen.

Done! From here you can build panels, graphs, tables, get your log files in... all kinds of cool stuff... but you should be up and running at this point..... let me know if you aren't.

A couple of notes:
1. Mongodb produces a lot of logs. Putting a ttl (Time to Live) on  the record of 24 hrs, allows ES to automatically drop old records. Make this a sane number for your throughput. In other areas I have either expanded that to 2-3 days, or reduced to 6 hours, and added aggregation.

I am sure there are others.... please feel free to add at will. Hope this has been useful, enjoy the insights it will give you to your data.