-
Notifications
You must be signed in to change notification settings - Fork 4.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Mongo dbstats #3228
Mongo dbstats #3228
Conversation
Jenkins standing by to test this. If you aren't a maintainer, you can ignore this comment. Someone with commit access, please review this and clear it for Jenkins to run. |
1 similar comment
Jenkins standing by to test this. If you aren't a maintainer, you can ignore this comment. Someone with commit access, please review this and clear it for Jenkins to run. |
func (m *MetricSet) Fetch() ([]common.MapStr, error) { | ||
|
||
// establish connection to mongo | ||
session, err := mgo.DialWithInfo(m.dialInfo) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This follows the pattern used by the status
metricset here, but I'm not sure it makes sense to open and close the mongo connection each time fetch() is called.
According to the [docs],(https://www.elastic.co/guide/en/beats/metricbeat/current/metricset-details.html#_timeout_connections_to_services) connections should be established in the New()
method and persist between fetches.
I'm going to begin implementing a pattern where the mongo session is persisted in each MetricSet
instance to avoid constantly opening and closing connections. I would add that work to this existing PR. Or, would you prefer it's handled under a separate submission?
Lastly, based on this implementation I'm more confident that the mongodb module is not connecting directly to the individual instances, but would instead connect to the cluster. This means that any cluster member may answer the query and result in data inconsistencies. This issue is a bit more involved, and I don't know enough about the platform to be sure, so I may need to jump on a Zoom call or something to talk further.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 on persisting the connection. It is probably best to extract the connection setup logic to the module level and just use it as a function.
About connecting to single instances or the cluster can be come tricky. We had a similar issue with Kafka. Our recommendation is to install metricbeat on each edge node (some exceptions exist here), as it gives you also additional metrics about the system etc. I'm don't know the details of the mongodb clustering but for Kafka we solved it the following way (generic description):
- Individual node stats are reported by each mongo instance
- Global stats about cluster / indices are only fetched from master
This requires that a metricbeat instance can detect if it is a master or not. Also I assume this must be dynamic, as master can change over time.
I assume dbStats response is the same for each node, so only the data from master should be fetched.
Happy to jump on a zoom call to go into more details.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Connecting to single instances
Tricky indeed. Fortunately, mgo (the mongo client) has a dialInfo.Direct
parameter. In mongobeat, I initiate a master connection to the cluster, and then create individual node connections to each node. You can see it here
I create the individual connections in the "controller" here
I've begun implementing something similar for the mongodb module, minus node discovery. This is implemented at the module level to share logic and prevent redundant connections.
Detecting master/primary
Easily implemented with mgo. You can specify read preference to always read from primary. If the primary changes, your reads will automatically be directed to the new primary.
dbStats response the same
Technically dbStats response (and serverStatus) are not the same for each node. Sometimes the differences are trivial, other times they're not. I'll cover the cases where they're not the same:
In the case of a replica set, the state of the secondaries lag behind the primary. Thus, seeing the stats for each node helps monitor replication lag and success/failure. This is especially helpful in the case of multi-datacenter replication, where larger discrepancies are usually present.
In the case of a sharded cluster, the complexity is compounded because nodes contain different databases and collections, and a shard can also be a replica set! =o
Conclusion
Given these considerations, I'm still having a hard time understanding what the best implementation for metricbeat would be. Given the current implementation - the node that is read from is implicit and can change from period to period. For example, you might connect to mongo on localhost
and expect that you're reading from that node. But under the hood, mgo is actually discovering the other nodes in the cluster and directing reads to any one of the member nodes. Thus, I think it's possible to receive unexpected and inconsistent results.
If we enforce that metricbeat only establishes a direct connection to a single mongo instance on localhost, then all of this complexity goes away and output is very predictable. Yay! But, it limits the flexibility of the system.
If we allow the agent to connect to mongo as a cluster, and permit configuration of multiple hosts, then I think there's a lot of different directions we could take.
Definitely a Zoom call will be helpful. I'm sure you can teach me a lot more about how metricbeat works under the hood, so I can understand the best direction.
I don't have your email, but can you contact me at [email protected] and we'll set up a time? Perhaps we could talk sometime Thursday afternoon (your time).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@scottcrespo Sorry, I was mostly offline the last two days, so only saw the update now.
I always try to go with the simplest solution first, which I agree is the one only connecting to localhost. Because of docker I would probably rephrase this to: The mongodb module gets only the stats from the node it connects to, as it does not necessarly always have to be localhost on docker.
Could you update the PR so that it always only connects to the defined host (and not the cluster)?
About zoom call: Lets see how far we can take it here in the PR as I'm rarely only in the next days because of holidays. But I'm happy to answer any questions about metricbeat in more detail here if needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No problem! I've been on vacation as well, but I have a few days to work on beats before taking some additional time off.
I'm in the process of updating the mongodb module to establish direct connections to the hosts listed in the config, and to persist those connections between calls. I should be able to update this PR within a few days!
… node from holding up the reporting
… fields get lost when calling eventMapping()
…additional fields to dbstats metricset
…connections Metricbeat mongodb persistent connections
Ready for your review. Please let me know what changes I might need to make! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for pushing this forward. Also finally back online again.
In general changes LGTM. I left some minor comments / question. The major logical change is that now Mongodb connects to a specific host instead of the cluster, correct?
@tsg Could you also have a look at the changes as you did the mongo implementation as far as I remember.
@@ -117,7 +117,7 @@ metricbeat.modules: | |||
|
|||
#------------------------------- MongoDB Module ------------------------------ | |||
#- module: mongodb | |||
#metricsets: ["status"] | |||
#metricsets: ["status", "dbstats"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably sort this alphabetically. But not sure if we are consistent in other modules with that ...
}, | ||
"mongodb":{ | ||
"dbstats":{ | ||
"example": "dbstats" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An example doc should be generated here with make generate-json
. But that can still be done after merging the PR.
- name: objects | ||
type: long | ||
|
||
- name: storage_size |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is the unit for all the _size
fields?
- name: file_size | ||
type: long | ||
|
||
- name: index_size |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is probably index_size.bytes
and then you can add format: bytes
to each block, so Kibana will automatically now that it is in bytes and can do some conversion.
"indexes": c.Int("indexes"), | ||
"index_size": c.Int("indexSize"), | ||
// mmapv1 only | ||
"ns_size_mb": c.Int("nsSizeMB"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this field be optional as based on the comment, I assume it does not always exist?
// Part of new is also setting up the configuration by processing additional | ||
// configuration entries if needed. | ||
func New(base mb.BaseMetricSet) (mb.MetricSet, error) { | ||
dialInfo, err := mgo.ParseURL(base.HostData().URI) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add an experimental flag here? We normally introduce new metricsets as experimental. This allows us to first get some real word feedback for it and still change the data structure if needed without having to wait for a major release. Have a look here: https://github.com/elastic/beats/blob/master/metricbeat/module/haproxy/stat/stat.go#L34
nodeDialInfo.FailFast = true | ||
|
||
logp.Info("Connecting to MongoDB node at %v", nodeDialInfo.Addrs) | ||
fmt.Printf("Connecting to MongoDB node at %v", nodeDialInfo.Addrs) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fmt should be removed here as we already log it.
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &MetricSet{ | ||
BaseMetricSet: base, | ||
dialInfo: dialInfo, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we still need to store the dialInfo as we use now mongoSession?
} | ||
defer session.Close() | ||
|
||
session.SetMode(mgo.Monotonic, true) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the monotonic mode was removed. Is this intentional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this was intentional. Monotonic Mode connects to the cluster as a whole and reads from the nearest secondary (not clear how nearest is defined). So, if your desired target is busy or goes down, mgo will read from a different node in the cluster. This failover strategy is what I was referring to earlier (which I think is undesirable) - where you may read from a node other than the target specified in the module's configuration, and obfuscate node failures.
Thus, the change I've implemented establishes direct connections to each of the nodes specified in the module's settings. You can see how I did it here. This ensures that we are reading from the intended node. And, if it's slow or fails, we will know about it.
Also, note that I've also set FailFast to True
, which helps identity latent and non-responsive servers sooner rather than later.
@@ -17,43 +15,52 @@ TODOs: | |||
* add a metricset for "metrics" data | |||
*/ | |||
|
|||
var debugf = logp.MakeDebug("mongodb.status") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume you removed this, because it was not used. We normally keep this in to make it easy to add debug messages.
Thanks for all the feedback! I'm offline this week, but I'll implement your suggestions startkng next week! |
…csets in alphabetical order
Implemented all of your suggestions. Ready for you to take another look =) |
jenkins, test it |
@scottcrespo LGTM. Could you add an update the CHANGELOG.asciidoc and squash the commits into 1? |
jenkins, test it |
@scottcrespo Merged. Thanks a lot for this contribution. |
You're welcome! It's been great working with you on this. Thanks for all of the help and feedback. |
@ruflin
Opening the PR early so we have a place to discuss implementation. At this point, I've copied the patterns used by the
status
metricset, but I have some questions and recommendations. Keep in mind, I'm still learning about the platform, so I might be making some incorrect assumptions!