|
@@ -32,42 +32,62 @@ type Fortune struct {
|
|
|
Message string `json:"message"`
|
|
|
}
|
|
|
|
|
|
-// Databases
|
|
|
const (
|
|
|
- // Go 1.4's sql.DB has scalability problem when using (explicitly reused) prepared statement.
|
|
|
- // https://github.com/golang/go/issues/9484
|
|
|
+ helloWorldString = "Hello, World!"
|
|
|
+
|
|
|
+ // Databases
|
|
|
//
|
|
|
- // Using db.Query() instead of stmt.Query() avoid the issue.
|
|
|
- // But it makes 3 round trips per query: prepare, execute and close.
|
|
|
// `interpolateParams=true` enables client side parameter interpolation.
|
|
|
// It reduces round trips without prepared statement.
|
|
|
//
|
|
|
- // Before Go 1.5 is released, we can see real power of Go with this benchmark.
|
|
|
- // After Go 1.5 is released, we can see prepared statement vs interpolation by comparing
|
|
|
- // this and another lightweight Go framework.
|
|
|
- connectionString = "benchmarkdbuser:benchmarkdbpass@tcp(localhost:3306)/hello_world?interpolateParams=true"
|
|
|
- worldSelect = "SELECT id, randomNumber FROM World WHERE id = ?"
|
|
|
- worldUpdate = "UPDATE World SET randomNumber = ? WHERE id = ?"
|
|
|
- fortuneSelect = "SELECT id, message FROM Fortune;"
|
|
|
- worldRowCount = 10000
|
|
|
- maxConnectionCount = 256
|
|
|
+ // We can see difference between prepared statement and interpolation by comparing go-raw and go-raw-interpolate
|
|
|
+ connectionString = "benchmarkdbuser:benchmarkdbpass@tcp(localhost:3306)/hello_world?interpolateParams=true"
|
|
|
+ worldSelect = "SELECT id, randomNumber FROM World WHERE id = ?"
|
|
|
+ worldUpdate = "UPDATE World SET randomNumber = ? WHERE id = ?"
|
|
|
+ fortuneSelect = "SELECT id, message FROM Fortune"
|
|
|
+ worldRowCount = 10000
|
|
|
+ maxConnections = 256
|
|
|
)
|
|
|
|
|
|
-const helloWorldString = "Hello, World!"
|
|
|
-
|
|
|
var (
|
|
|
+ helloWorldBytes = []byte(helloWorldString)
|
|
|
+
|
|
|
// Templates
|
|
|
tmpl = template.Must(template.ParseFiles("templates/layout.html", "templates/fortune.html"))
|
|
|
|
|
|
// Database
|
|
|
- db *sql.DB
|
|
|
-
|
|
|
- helloWorldBytes = []byte(helloWorldString)
|
|
|
+ db *sql.DB
|
|
|
+ worldSelectPrepared *sql.Stmt
|
|
|
+ worldUpdatePrepared *sql.Stmt
|
|
|
+ fortuneSelectPrepared *sql.Stmt
|
|
|
)
|
|
|
|
|
|
var prefork = flag.Bool("prefork", false, "use prefork")
|
|
|
var child = flag.Bool("child", false, "is child proc")
|
|
|
|
|
|
+func initDB() {
|
|
|
+ var err error
|
|
|
+ db, err = sql.Open("mysql", connectionString)
|
|
|
+ if err != nil {
|
|
|
+ log.Fatalf("Error opening database: %v", err)
|
|
|
+ }
|
|
|
+ db.SetMaxIdleConns(maxConnections)
|
|
|
+ db.SetMaxOpenConns(maxConnections)
|
|
|
+
|
|
|
+ worldSelectPrepared, err = db.Prepare(worldSelect)
|
|
|
+ if err != nil {
|
|
|
+ log.Fatal(err)
|
|
|
+ }
|
|
|
+ worldUpdatePrepared, err = db.Prepare(worldUpdate)
|
|
|
+ if err != nil {
|
|
|
+ log.Fatal(err)
|
|
|
+ }
|
|
|
+ fortuneSelectPrepared, err = db.Prepare(fortuneSelect)
|
|
|
+ if err != nil {
|
|
|
+ log.Fatal(err)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
func main() {
|
|
|
var listener net.Listener
|
|
|
flag.Parse()
|
|
@@ -77,18 +97,17 @@ func main() {
|
|
|
listener = doPrefork()
|
|
|
}
|
|
|
|
|
|
- var err error
|
|
|
- db, err = sql.Open("mysql", connectionString)
|
|
|
- if err != nil {
|
|
|
- log.Fatalf("Error opening database: %v", err)
|
|
|
- }
|
|
|
- db.SetMaxIdleConns(maxConnectionCount)
|
|
|
+ initDB()
|
|
|
|
|
|
+ http.HandleFunc("/json", jsonHandler)
|
|
|
http.HandleFunc("/db", dbHandler)
|
|
|
+ http.HandleFunc("/dbInterpolate", dbInterpolateHandler)
|
|
|
http.HandleFunc("/queries", queriesHandler)
|
|
|
- http.HandleFunc("/json", jsonHandler)
|
|
|
+ http.HandleFunc("/queriesInterpolate", queriesInterpolateHandler)
|
|
|
http.HandleFunc("/fortune", fortuneHandler)
|
|
|
+ http.HandleFunc("/fortuneInterpolate", fortuneInterpolateHandler)
|
|
|
http.HandleFunc("/update", updateHandler)
|
|
|
+ http.HandleFunc("/updateInterpolate", updateInterpolateHandler)
|
|
|
http.HandleFunc("/plaintext", plaintextHandler)
|
|
|
if !*prefork {
|
|
|
http.ListenAndServe(":8080", nil)
|
|
@@ -144,56 +163,123 @@ func doPrefork() (listener net.Listener) {
|
|
|
return listener
|
|
|
}
|
|
|
|
|
|
+func getQueriesParam(r *http.Request) int {
|
|
|
+ n := 1
|
|
|
+ if nStr := r.URL.Query().Get("queries"); len(nStr) > 0 {
|
|
|
+ n, _ = strconv.Atoi(nStr)
|
|
|
+ }
|
|
|
+
|
|
|
+ if n < 1 {
|
|
|
+ n = 1
|
|
|
+ } else if n > 500 {
|
|
|
+ n = 500
|
|
|
+ }
|
|
|
+ return n
|
|
|
+}
|
|
|
+
|
|
|
// Test 1: JSON serialization
|
|
|
func jsonHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
+ w.Header().Set("Server", "Go")
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
json.NewEncoder(w).Encode(&Message{helloWorldString})
|
|
|
}
|
|
|
|
|
|
// Test 2: Single database query
|
|
|
func dbHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
+ var world World
|
|
|
+ err := worldSelectPrepared.QueryRow(rand.Intn(worldRowCount)+1).Scan(&world.Id, &world.RandomNumber)
|
|
|
+ if err != nil {
|
|
|
+ log.Fatalf("Error scanning world row: %s", err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ w.Header().Set("Server", "Go")
|
|
|
+ w.Header().Set("Content-Type", "application/json")
|
|
|
+ json.NewEncoder(w).Encode(&world)
|
|
|
+}
|
|
|
+
|
|
|
+func dbInterpolateHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
var world World
|
|
|
err := db.QueryRow(worldSelect, rand.Intn(worldRowCount)+1).Scan(&world.Id, &world.RandomNumber)
|
|
|
if err != nil {
|
|
|
log.Fatalf("Error scanning world row: %s", err.Error())
|
|
|
}
|
|
|
|
|
|
+ w.Header().Set("Server", "Go")
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
json.NewEncoder(w).Encode(&world)
|
|
|
}
|
|
|
|
|
|
// Test 3: Multiple database queries
|
|
|
func queriesHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
- n := 1
|
|
|
- if nStr := r.URL.Query().Get("queries"); len(nStr) > 0 {
|
|
|
- n, _ = strconv.Atoi(nStr)
|
|
|
- }
|
|
|
+ n := getQueriesParam(r)
|
|
|
|
|
|
- if n < 1 {
|
|
|
- n = 1
|
|
|
- } else if n > 500 {
|
|
|
- n = 500
|
|
|
+ world := make([]World, n)
|
|
|
+ for i := 0; i < n; i++ {
|
|
|
+ err := worldSelectPrepared.QueryRow(rand.Intn(worldRowCount)+1).Scan(&world[i].Id, &world[i].RandomNumber)
|
|
|
+ if err != nil {
|
|
|
+ log.Fatalf("Error scanning world row: %v", err)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
+ w.Header().Set("Server", "Go")
|
|
|
+ w.Header().Set("Content-Type", "application/json")
|
|
|
+ json.NewEncoder(w).Encode(world)
|
|
|
+}
|
|
|
+
|
|
|
+func queriesInterpolateHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
+ n := getQueriesParam(r)
|
|
|
+
|
|
|
world := make([]World, n)
|
|
|
for i := 0; i < n; i++ {
|
|
|
err := db.QueryRow(worldSelect, rand.Intn(worldRowCount)+1).Scan(&world[i].Id, &world[i].RandomNumber)
|
|
|
if err != nil {
|
|
|
- log.Fatalf("Error scanning world row: %s", err.Error())
|
|
|
+ log.Fatalf("Error scanning world row: %v", err)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ w.Header().Set("Server", "Go")
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
json.NewEncoder(w).Encode(world)
|
|
|
}
|
|
|
|
|
|
// Test 4: Fortunes
|
|
|
func fortuneHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
+ rows, err := fortuneSelectPrepared.Query()
|
|
|
+ if err != nil {
|
|
|
+ log.Fatalf("Error preparing statement: %v", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ fortunes := fetchFortunes(rows)
|
|
|
+ fortunes = append(fortunes, &Fortune{Message: "Additional fortune added at request time."})
|
|
|
+
|
|
|
+ sort.Sort(ByMessage{fortunes})
|
|
|
+ w.Header().Set("Server", "Go")
|
|
|
+ w.Header().Set("Content-Type", "text/html")
|
|
|
+ if err := tmpl.Execute(w, fortunes); err != nil {
|
|
|
+ http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func fortuneInterpolateHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
rows, err := db.Query(fortuneSelect)
|
|
|
if err != nil {
|
|
|
log.Fatalf("Error preparing statement: %v", err)
|
|
|
}
|
|
|
|
|
|
+ fortunes := fetchFortunes(rows)
|
|
|
+ fortunes = append(fortunes, &Fortune{Message: "Additional fortune added at request time."})
|
|
|
+
|
|
|
+ sort.Sort(ByMessage{fortunes})
|
|
|
+ w.Header().Set("Server", "Go")
|
|
|
+ w.Header().Set("Content-Type", "text/html")
|
|
|
+ if err := tmpl.Execute(w, fortunes); err != nil {
|
|
|
+ http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func fetchFortunes(rows *sql.Rows) Fortunes {
|
|
|
+ defer rows.Close()
|
|
|
+
|
|
|
fortunes := make(Fortunes, 0, 16)
|
|
|
for rows.Next() { //Fetch rows
|
|
|
fortune := Fortune{}
|
|
@@ -202,46 +288,53 @@ func fortuneHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
}
|
|
|
fortunes = append(fortunes, &fortune)
|
|
|
}
|
|
|
- rows.Close()
|
|
|
- fortunes = append(fortunes, &Fortune{Message: "Additional fortune added at request time."})
|
|
|
-
|
|
|
- sort.Sort(ByMessage{fortunes})
|
|
|
- w.Header().Set("Content-Type", "text/html")
|
|
|
- if err := tmpl.Execute(w, fortunes); err != nil {
|
|
|
- http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
- }
|
|
|
+ return fortunes
|
|
|
}
|
|
|
|
|
|
// Test 5: Database updates
|
|
|
func updateHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
- n := 1
|
|
|
- if nStr := r.URL.Query().Get("queries"); len(nStr) > 0 {
|
|
|
- n, _ = strconv.Atoi(nStr)
|
|
|
+ n := getQueriesParam(r)
|
|
|
+
|
|
|
+ world := make([]World, n)
|
|
|
+ for i := 0; i < n; i++ {
|
|
|
+ if err := worldSelectPrepared.QueryRow(rand.Intn(worldRowCount)+1).Scan(&world[i].Id, &world[i].RandomNumber); err != nil {
|
|
|
+ log.Fatalf("Error scanning world row: %v", err)
|
|
|
+ }
|
|
|
+ world[i].RandomNumber = uint16(rand.Intn(worldRowCount) + 1)
|
|
|
+ if _, err := worldUpdatePrepared.Exec(world[i].RandomNumber, world[i].Id); err != nil {
|
|
|
+ log.Fatalf("Error updating world row: %v", err)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
+ w.Header().Set("Server", "Go")
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
encoder := json.NewEncoder(w)
|
|
|
+ encoder.Encode(world)
|
|
|
+}
|
|
|
+
|
|
|
+func updateInterpolateHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
+ n := getQueriesParam(r)
|
|
|
|
|
|
- if n < 1 {
|
|
|
- n = 1
|
|
|
- } else if n > 500 {
|
|
|
- n = 500
|
|
|
- }
|
|
|
world := make([]World, n)
|
|
|
for i := 0; i < n; i++ {
|
|
|
if err := db.QueryRow(worldSelect, rand.Intn(worldRowCount)+1).Scan(&world[i].Id, &world[i].RandomNumber); err != nil {
|
|
|
- log.Fatalf("Error scanning world row: %s", err.Error())
|
|
|
+ log.Fatalf("Error scanning world row: %v", err)
|
|
|
}
|
|
|
world[i].RandomNumber = uint16(rand.Intn(worldRowCount) + 1)
|
|
|
if _, err := db.Exec(worldUpdate, world[i].RandomNumber, world[i].Id); err != nil {
|
|
|
- log.Fatalf("Error updating world row: %s", err.Error())
|
|
|
+ log.Fatalf("Error updating world row: %v", err)
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ w.Header().Set("Server", "Go")
|
|
|
+ w.Header().Set("Content-Type", "application/json")
|
|
|
+ encoder := json.NewEncoder(w)
|
|
|
encoder.Encode(world)
|
|
|
}
|
|
|
|
|
|
// Test 6: Plaintext
|
|
|
func plaintextHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
+ w.Header().Set("Server", "Go")
|
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
|
w.Write(helloWorldBytes)
|
|
|
}
|