Forráskód Böngészése

[Go]Add Hertz framework (#7546)

* feat: add hertz in go

* fix: concurrency queries problem

* fix: update README.md

* Update go.mod

* Update go.mod

* Delete main.go

* Delete go.sum

* Delete go.sum

* update main

* Update hertz-gorm.dockerfile

* fix: update hertz to v0.4
Edward zZhang 2 éve
szülő
commit
3a6ecb9545

+ 43 - 0
frameworks/Go/hertz/README.md

@@ -0,0 +1,43 @@
+# Hertz Benchmarking Test
+
+### Test Type Implementation Source Code
+
+* [JSON](hello.go)
+* [PLAINTEXT](hello.go)
+* [DB](hello.go)
+* [QUERY](hello.go)
+* [CACHED QUERY](not implemented)
+* [UPDATE](hello.go)
+* [FORTUNES](hello.go)
+
+## Important Libraries
+The tests were run with techempower suite
+
+## Test URLs
+### JSON
+
+http://localhost:8080/json
+
+### PLAINTEXT
+
+http://localhost:8080/plaintext
+
+### DB
+
+http://localhost:8080/db
+
+### QUERY
+
+http://localhost:8080/dbs?queries=
+
+### CACHED QUERY
+
+NA
+
+### UPDATE
+
+http://localhost:8080/updates?queries=
+
+### FORTUNES
+
+http://localhost:8080/fortunes

+ 50 - 0
frameworks/Go/hertz/benchmark_config.json

@@ -0,0 +1,50 @@
+{
+  "framework": "hertz",
+  "tests": [{
+    "default": {
+      "json_url": "/json",
+      "db_url": "/db",
+      "query_url": "/dbs?queries=",
+      "fortune_url": "/fortunes",
+      "update_url": "/update?queries=",
+      "plaintext_url": "/plaintext",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Micro",
+      "database": "MySQL",
+      "framework": "hertz",
+      "language": "Go",
+      "flavor": "None",
+      "orm": "Raw",
+      "platform": "None",
+      "webserver": "None",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "hertz",
+      "notes": "",
+      "versus": "go"
+    },
+    "gorm": {
+      "db_url": "/db",
+      "json_url": "/json",
+      "query_url": "/queries?queries=",
+      "update_url": "/updates?queries=",
+      "plaintext_url": "/plaintext",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Fullstack",
+      "database": "postgres",
+      "framework": "hertz-gorm",
+      "language": "Go",
+      "flavor": "None",
+      "orm": "Full",
+      "platform": "None",
+      "webserver": "None",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "hertz-gorm",
+      "notes": "",
+      "versus": "go"
+    }
+  }]
+}

+ 21 - 0
frameworks/Go/hertz/config.toml

@@ -0,0 +1,21 @@
+[framework]
+name = "hertz"
+
+[main]
+urls.plaintext = "/plaintext"
+urls.json = "/json"
+urls.db = "/db"
+urls.query = "/dbs?queries="
+urls.update = "/update?queries="
+urls.fortune = "/fortunes"
+approach = "Realistic"
+classification = "Micro"
+database = "MySQL"
+database_os = "Linux"
+os = "Linux"
+orm = "Raw"
+platform = "None"
+webserver = "None"
+versus = "go"
+
+

+ 31 - 0
frameworks/Go/hertz/go.mod

@@ -0,0 +1,31 @@
+module hertz
+
+go 1.18
+
+require (
+	github.com/cloudwego/hertz v0.4.0
+	github.com/go-sql-driver/mysql v1.6.0
+)
+
+require (
+	github.com/bytedance/go-tagexpr/v2 v2.9.2 // indirect
+	github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7 // indirect
+	github.com/bytedance/sonic v1.5.0 // indirect
+	github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06 // indirect
+	github.com/cloudwego/netpoll v0.2.6 // indirect
+	github.com/fsnotify/fsnotify v1.5.4 // indirect
+	github.com/golang/protobuf v1.5.0 // indirect
+	github.com/henrylee2cn/ameda v1.4.10 // indirect
+	github.com/henrylee2cn/goutil v0.0.0-20210127050712-89660552f6f8 // indirect
+	github.com/klauspost/cpuid/v2 v2.0.9 // indirect
+	github.com/nyaruka/phonenumbers v1.0.55 // indirect
+	github.com/stretchr/testify v1.7.1 // indirect
+	github.com/tidwall/gjson v1.13.0 // indirect
+	github.com/tidwall/match v1.1.1 // indirect
+	github.com/tidwall/pretty v1.2.0 // indirect
+	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+	golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
+	golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
+	google.golang.org/protobuf v1.28.0 // indirect
+	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
+)

+ 187 - 0
frameworks/Go/hertz/hello.go

@@ -0,0 +1,187 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"github.com/cloudwego/hertz/pkg/app"
+	"github.com/cloudwego/hertz/pkg/app/server"
+	"github.com/cloudwego/hertz/pkg/common/config"
+	"github.com/cloudwego/hertz/pkg/common/utils"
+	"log"
+	"math/rand"
+	"runtime"
+	"sort"
+	"strconv"
+
+	"database/sql"
+
+	_ "github.com/go-sql-driver/mysql"
+)
+
+const (
+	// Database
+	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
+)
+
+type World struct {
+	Id           uint16 `json:"id"`
+	RandomNumber uint16 `json:"randomNumber"`
+}
+
+type Fortune struct {
+	Id      uint16 `json:"id"`
+	Message string `json:"message"`
+}
+
+type Fortunes []*Fortune
+
+func (s Fortunes) Len() int      { return len(s) }
+func (s Fortunes) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+
+type ByMessage struct{ Fortunes }
+
+func (s ByMessage) Less(i, j int) bool { return s.Fortunes[i].Message < s.Fortunes[j].Message }
+
+var (
+	// Database
+	worldStatement   *sql.Stmt
+	fortuneStatement *sql.Stmt
+	updateStatement  *sql.Stmt
+)
+
+func parseQueries(c context.Context, ctx *app.RequestContext) int {
+	n, err := strconv.Atoi(ctx.Query("queries"))
+	if err != nil {
+		n = 1
+	} else if n < 1 {
+		n = 1
+	} else if n > 500 {
+		n = 500
+	}
+	return n
+}
+
+// / Test 1: JSON serialization
+func json(c context.Context, ctx *app.RequestContext) {
+	ctx.JSON(200, utils.H{"message": "Hello, World!"})
+}
+
+// / Test 2: Single database query
+func db(c context.Context, ctx *app.RequestContext) {
+	var world World
+	err := worldStatement.QueryRow(rand.Intn(worldRowCount)+1).Scan(&world.Id, &world.RandomNumber)
+	if err != nil {
+		ctx.AbortWithError(500, err)
+		return
+	}
+	ctx.JSON(200, &world)
+}
+
+// / Test 3: Multiple database queries
+func dbs(c context.Context, ctx *app.RequestContext) {
+	numQueries := parseQueries(c, ctx)
+
+	worlds := make([]World, numQueries)
+	for i := 0; i < numQueries; i++ {
+		err := worldStatement.QueryRow(rand.Intn(worldRowCount)+1).Scan(&worlds[i].Id, &worlds[i].RandomNumber)
+		if err != nil {
+			ctx.AbortWithError(500, err)
+			return
+		}
+	}
+	ctx.JSON(200, &worlds)
+}
+
+// / Test 4: Fortunes
+func fortunes(c context.Context, ctx *app.RequestContext) {
+	rows, err := fortuneStatement.Query()
+	if err != nil {
+		ctx.AbortWithError(500, err)
+		return
+	}
+
+	fortunes := make(Fortunes, 0, 16)
+	for rows.Next() { //Fetch rows
+		fortune := Fortune{}
+		if err := rows.Scan(&fortune.Id, &fortune.Message); err != nil {
+			ctx.AbortWithError(500, err)
+			return
+		}
+		fortunes = append(fortunes, &fortune)
+	}
+	fortunes = append(fortunes, &Fortune{Message: "Additional fortune added at request time."})
+
+	sort.Sort(ByMessage{fortunes})
+	ctx.HTML(200, "fortune.html", fortunes)
+}
+
+// / Test 5: Database updates
+func update(c context.Context, ctx *app.RequestContext) {
+	numQueries := parseQueries(c, ctx)
+	world := make([]World, numQueries)
+	for i := 0; i < numQueries; i++ {
+		if err := worldStatement.QueryRow(rand.Intn(worldRowCount)+1).Scan(&world[i].Id, &world[i].RandomNumber); err != nil {
+			ctx.AbortWithError(500, err)
+			return
+		}
+		world[i].RandomNumber = uint16(rand.Intn(worldRowCount) + 1)
+		if _, err := updateStatement.Exec(world[i].RandomNumber, world[i].Id); err != nil {
+			ctx.AbortWithError(500, err)
+			return
+		}
+	}
+	ctx.JSON(200, world)
+}
+
+// / Test 6: plaintext
+func plaintext(c context.Context, ctx *app.RequestContext) {
+	ctx.String(200, "Hello, World!")
+}
+
+func main() {
+	h := server.New(config.Option{F: func(o *config.Options) {
+		o.Addr = ":8080"
+	},
+	})
+	serverHeader := []string{"Hertz"}
+	h.Use(func(c context.Context, ctx *app.RequestContext) {
+		ctx.Header("Server", serverHeader[0])
+	})
+	h.LoadHTMLGlob("/templates/fortune.html")
+	h.GET("/json", json)
+	h.GET("/db", db)
+	h.GET("/dbs", dbs)
+	h.GET("/fortunes", fortunes)
+	h.GET("/update", update)
+	h.GET("/plaintext", plaintext)
+	h.Spin()
+}
+
+func init() {
+	runtime.GOMAXPROCS(runtime.NumCPU())
+
+	dsn := "benchmarkdbuser:benchmarkdbpass@tcp(%s:3306)/hello_world"
+	dbhost := "tfb-database"
+
+	db, err := sql.Open("mysql", fmt.Sprintf(dsn, dbhost))
+	if err != nil {
+		log.Fatalf("Error opening database: %v", err)
+	}
+	db.SetMaxIdleConns(maxConnectionCount)
+	worldStatement, err = db.Prepare(worldSelect)
+	if err != nil {
+		log.Fatal(err)
+	}
+	fortuneStatement, err = db.Prepare(fortuneSelect)
+	if err != nil {
+		log.Fatal(err)
+	}
+	updateStatement, err = db.Prepare(worldUpdate)
+	if err != nil {
+		log.Fatal(err)
+	}
+}

+ 12 - 0
frameworks/Go/hertz/hertz-gorm.dockerfile

@@ -0,0 +1,12 @@
+FROM golang:1.18
+
+ENV GO111MODULE=on
+WORKDIR /src/
+ADD ./hertz-gorm /src/
+
+RUN go mod tidy
+#- original submission
+RUN go build -o app
+#RUN go build -tags=jsoniter -o app - tryed this but slower on my pc
+
+ENTRYPOINT ["/src/app"]

+ 43 - 0
frameworks/Go/hertz/hertz-gorm/README.md

@@ -0,0 +1,43 @@
+# Hertz-Gorm Benchmarking Test
+
+### Test Type Implementation Source Code
+
+* [JSON](main.go)
+* [PLAINTEXT](main.go)
+* [DB](main.go)
+* [QUERY](main.go)
+* [CACHED QUERY](not implemented)
+* [UPDATE](main.go)
+* [FORTUNES](not implemented)
+
+## Important Libraries
+The tests were run with techempower suite
+
+## Test URLs
+### JSON
+
+http://localhost:8080/json
+
+### PLAINTEXT
+
+http://localhost:8080/plaintext
+
+### DB
+
+http://localhost:8080/db
+
+### QUERY
+
+http://localhost:8080/queries?queries=
+
+### CACHED QUERY
+
+NA
+
+### UPDATE
+
+http://localhost:8080/updates?queries=
+
+### FORTUNES
+
+NA

+ 44 - 0
frameworks/Go/hertz/hertz-gorm/go.mod

@@ -0,0 +1,44 @@
+module gin-gorm/main
+
+go 1.18
+
+require (
+	github.com/cloudwego/hertz v0.4.0
+	gorm.io/driver/postgres v1.4.5
+	gorm.io/gorm v1.24.1-0.20221019064659-5dd2bb482755
+)
+
+require (
+	github.com/bytedance/go-tagexpr/v2 v2.9.2 // indirect
+	github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7 // indirect
+	github.com/bytedance/sonic v1.5.0 // indirect
+	github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06 // indirect
+	github.com/cloudwego/netpoll v0.2.6 // indirect
+	github.com/fsnotify/fsnotify v1.5.4 // indirect
+	github.com/golang/protobuf v1.5.0 // indirect
+	github.com/henrylee2cn/ameda v1.4.10 // indirect
+	github.com/henrylee2cn/goutil v0.0.0-20210127050712-89660552f6f8 // indirect
+	github.com/jackc/chunkreader/v2 v2.0.1 // indirect
+	github.com/jackc/pgconn v1.13.0 // indirect
+	github.com/jackc/pgio v1.0.0 // indirect
+	github.com/jackc/pgpassfile v1.0.0 // indirect
+	github.com/jackc/pgproto3/v2 v2.3.1 // indirect
+	github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
+	github.com/jackc/pgtype v1.12.0 // indirect
+	github.com/jackc/pgx/v4 v4.17.2 // indirect
+	github.com/jinzhu/inflection v1.0.0 // indirect
+	github.com/jinzhu/now v1.1.4 // indirect
+	github.com/klauspost/cpuid/v2 v2.0.9 // indirect
+	github.com/nyaruka/phonenumbers v1.0.55 // indirect
+	github.com/stretchr/testify v1.8.0 // indirect
+	github.com/tidwall/gjson v1.13.0 // indirect
+	github.com/tidwall/match v1.1.1 // indirect
+	github.com/tidwall/pretty v1.2.0 // indirect
+	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+	golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
+	golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect
+	golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
+	golang.org/x/text v0.3.7 // indirect
+	google.golang.org/protobuf v1.28.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+)

+ 160 - 0
frameworks/Go/hertz/hertz-gorm/main.go

@@ -0,0 +1,160 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"github.com/cloudwego/hertz/pkg/app"
+	"github.com/cloudwego/hertz/pkg/app/server"
+	"github.com/cloudwego/hertz/pkg/common/config"
+	"github.com/cloudwego/hertz/pkg/common/utils"
+	postgres "gorm.io/driver/postgres"
+	"gorm.io/gorm"
+	"gorm.io/gorm/logger"
+	"math/rand"
+)
+
+// World represents an entry int the World table
+type World struct {
+	ID           int64 `json:"id"`
+	RandomNumber int64 `json:"randomNumber" gorm:"column:randomnumber"`
+}
+
+// TableName Override GORM convention for table mapping
+func (World) TableName() string {
+	return "World"
+}
+
+// implements the basic logic behind the query tests
+func getWorld(db *gorm.DB) World {
+	// we could actually precompute a list of random
+	// numbers and slice them but this makes no sense
+	// as I expect that this 'random' is just a placeholder
+	// for an actual business logic
+	randomId := rand.Intn(10000) + 1
+
+	var world World
+	db.Take(&world, randomId)
+
+	return world
+}
+
+// implements the logic behind the updates tests
+func processWorld(tx *gorm.DB) (World, error) {
+	// we could actually precompute a list of random
+	// numbers and slice them but this makes no sense
+	// as I expect that this 'random' is just a placeholder
+	// for an actual business logic in a real test
+	randomId := rand.Intn(10000) + 1
+	randomId2 := int64(rand.Intn(10000) + 1)
+
+	var world World
+	tx.Take(&world, randomId)
+
+	world.RandomNumber = randomId2
+	err := tx.Save(&world).Error
+
+	return world, err
+}
+
+func main() {
+	/* SETUP DB AND WEB SERVER */
+
+	dsn := "host=tfb-database user=benchmarkdbuser password=benchmarkdbpass dbname=hello_world port=5432 sslmode=disable"
+	db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
+		PrepareStmt: true,                                  // use prep statements
+		Logger:      logger.Default.LogMode(logger.Silent), // new, not inserted in original submission 2x on query
+	})
+
+	if err != nil {
+		panic("failed to connect database")
+	}
+
+	sqlDB, err := db.DB()
+	if err != nil {
+		panic("failed to get underlying db conn pooling struct")
+	}
+
+	// SetMaxIdleConns sets the maximum number of connections in the idle connection pool.
+	sqlDB.SetMaxIdleConns(500)
+
+	h := server.New(config.Option{F: func(o *config.Options) {
+		o.Addr = ":8080"
+	}}) // use default middleware
+
+	// setup middleware to add server header
+	// this slows things a little, but it is the best design decision
+	serverHeader := []string{"Hertz-gorm"}
+	h.Use(func(c context.Context, ctx *app.RequestContext) {
+		ctx.Header("Server", serverHeader[0])
+	})
+
+	/* START TESTS */
+	// JSON TEST
+	h.GET("/json", func(c context.Context, ctx *app.RequestContext) {
+		// ctx.Header("Server", "example") - original submission now using middleware
+		ctx.JSON(200, utils.H{"message": "Hello, World!"})
+	})
+	// PLAINTEXT TEST
+	h.GET("/plaintext", func(c context.Context, ctx *app.RequestContext) {
+		// ctx.Header("Server", "example") - original submission now using middleware
+		ctx.String(200, "Hello, World!")
+	})
+	// SINGLE QUERY
+	h.GET("/db", func(c context.Context, ctx *app.RequestContext) {
+		world := getWorld(db)
+		// ctx.Header("Server", "example") - original submission now using middleware
+		ctx.JSON(200, world)
+	})
+
+	type NumOf struct {
+		Queries int `form:"queries" query:"queries"`
+	}
+	// MULTIPLE QUERIES
+	h.GET("/queries", func(c context.Context, ctx *app.RequestContext) {
+		var numOf NumOf
+		if ctx.Bind(&numOf) != nil { // manage missing query num
+			numOf.Queries = 1
+		} else if numOf.Queries < 1 { // set at least 1
+			numOf.Queries = 1
+		} else if numOf.Queries > 500 { // set no more than 500
+			numOf.Queries = 500
+		}
+
+		worlds := make([]World, numOf.Queries)
+		for i := 0; i < numOf.Queries; i++ {
+			worlds[i] = getWorld(db)
+		}
+		ctx.JSON(200, &worlds)
+	})
+	// MULTIPLE UPDATES
+	h.GET("/updates", func(c context.Context, ctx *app.RequestContext) {
+		var numOf NumOf
+
+		if ctx.Bind(&numOf) != nil { // manage missing query num
+			numOf.Queries = 1
+		} else if numOf.Queries < 1 { // set at least 1
+			numOf.Queries = 1
+		} else if numOf.Queries > 500 { // set no more than 500
+			numOf.Queries = 500
+		}
+
+		worlds := make([]World, numOf.Queries, numOf.Queries) // prealloc
+		var err error = nil
+
+		for i := 0; i < numOf.Queries; i++ {
+			worlds[i], err = processWorld(db)
+
+			if err != nil {
+				fmt.Println(err)
+				ctx.JSON(500, utils.H{"error": err})
+				break
+			}
+		}
+
+		ctx.JSON(200, worlds)
+	})
+
+	/* START SERVICE */
+
+	h.Spin() // listen and serve on 0.0.0.0:8080
+}

+ 15 - 0
frameworks/Go/hertz/hertz.dockerfile

@@ -0,0 +1,15 @@
+FROM golang:1.18
+
+ENV GO111MODULE=on
+
+ADD ./ /hertz
+COPY ./templates /templates
+WORKDIR /hertz
+
+RUN go mod tidy
+
+RUN go build -o hello hello.go
+
+EXPOSE 8080
+
+CMD ./hello

+ 20 - 0
frameworks/Go/hertz/templates/fortune.html

@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Fortunes</title>
+</head>
+<body>
+<table>
+<tr>
+<th>id</th>
+<th>message</th>
+</tr>
+{{range .}}
+<tr>
+<td>{{.Id}}</td>
+<td>{{.Message}}</td>
+</tr>
+{{end}}
+</table>
+</body>
+</html>