I am trying to configure reading from primary and two secondary nodes of mongo replica set to provide better load balancing. Each of 3 nodes are on different machines with IP addresses: ip1, ip2, ip3.
My GoLang site, which is the martini web server with two urls /insert and /get:
package main
import (
"github.com/go-martini/martini"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
"net/http"
)
const (
dialStr = "ip1:port1,ip2:port2,ip3:port3"
dbName = "test"
collectionName = "test"
elementsCount = 1000
)
var mainSessionForSave *mgo.Session
func ConnectToMongo() {
var err error
mainSessionForSave, err = mgo.Dial(dialStr)
mainSessionForSave.SetMode(mgo.Monotonic, true)
if err != nil {
panic(err)
}
}
func GetMgoSessionPerRequest() *mgo.Session {
var sessionPerRequest *mgo.Session
sessionPerRequest = mainSessionForSave.Copy()
return sessionPerRequest
}
func main() {
ConnectToMongo()
prepareMartini().Run()
}
type Element struct {
I int `bson:"I"`
}
func prepareMartini() *martini.ClassicMartini {
m := martini.Classic()
sessionPerRequest := GetMgoSessionPerRequest()
m.Get("/insert", func(w http.ResponseWriter, r *http.Request) {
for i := 0; i < elementsCount; i++ {
e := Element{I: i}
err := collection(sessionPerRequest).Insert(&e)
if err != nil {
panic(err)
}
}
w.Write([]byte("data inserted successfully"))
})
m.Get("/get", func(w http.ResponseWriter, r *http.Request) {
var element Element
const findI = 500
err := collection(sessionPerRequest).Find(bson.M{"I": findI}).One(&element)
if err != nil {
panic(err)
}
w.Write([]byte("get data successfully"))
})
return m
}
func collection(s *mgo.Session) *mgo.Collection {
return s.DB(dbName).C(collectionName)
}
I run this GoLang site with the command go run site.go and to prepare my experiment requested http://localhost:3000/insert - after about a minute my test data was inserted.
Then I started to test reading from secondary and primary nodes
in attacker.go:
package main
import (
"fmt"
"time"
vegeta "github.com/tsenart/vegeta/lib"
)
func main() {
rate := uint64(4000) // per second
duration := 4 * time.Second
targeter := vegeta.NewStaticTargeter(&vegeta.Target{
Method: "GET",
URL: "http://localhost:3000/get",
})
attacker := vegeta.NewAttacker()
var results vegeta.Results
for res := range attacker.Attack(targeter, rate, duration) {
results = append(results, res)
}
metrics := vegeta.NewMetrics(results)
fmt.Printf("99th percentile: %s\n", metrics.Latencies.P99)
}
Running it go run attacker.go I just requested URL http://localhost:3000/get 4000 times per second. While attacker was working I opened all my 3 servers and run htop command to watch resources consumption. The PRIMARY node shows that it is under high load with CPU about 80%. The SECONDARIES were calm.
As I used mgo.Monotonic ...
mainSessionForSave.SetMode(mgo.Monotonic, true)
... I expected to read from all nodes: ip1, ip2, ip3 and I expected to watch all the nodes under equal load and with equal CPU consumption. But it is not so. What did I configure wrong? In fact mgo.Monotonic is not working in my case and I read only from the PRIMARY node.
The sessionPerRequest is only created once: prepareMartini is called at server startup, and sessionPerRequest is set then. The closures passed to m.Get() access that variable. Then, after the first write (during your test setup), mgo will only access the primary:
Monotonic consistency will start reading from a slave if possible, so that the load is better distributed, and once the first write happens the connection is switched to the master.
(If mgo just continued reading from the secondary after writing to the primary, a read wouldn't necessarily reflect a write you just made, which could be a pain. And switching to the primary should only get you newer data than you were getting from the secondary, never older, which preserves monotonicity. That's how it should ideally work, anyway; see the "open issues" link below for more.)
The solution is to push creating the session down into your handlers, e.g., remove sessionPerRequest and put something explicit atop each handler, like
coll := mainSessionForSave.Copy().DB(dbName).Collection(collName)
All consistency promises should be read in light of open issues with MongoDB consistency: right now, during network partitions, reads can see old data and writes that will later be rolled back, even when mgo is trying to read from the primary. (Compare-and-set doesn't have this issue, but of course that's a larger slower operation.) It's also worth perusing that post just for the discussion of the consistency levels and the descriptions of how different database behaviors might manifest for an app's end-users.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With