#1 fasthttp: 十倍速
Golang fasthttp 2021-04-26https://github.com/valyala/fasthttp
在高并发、短请求、简单 HTTP 服务等场景,fasthttp 通过对象复用、减少内存分配、避免部分抽象开销,可获得 5 倍甚至更高的吞吐提升(官方提供的压测数据)。
说 10 倍是有标题党嫌疑啦,主要是经常在网上看到文章说 fasthttp 快 10 倍之类。
| 场景 | net/http | fasthttp | 加速倍数 |
|---|---|---|---|
| GOMAXPROCS=1,1 Req/Conn | 15327 | 2733 | 5.61× |
| GOMAXPROCS=1,2 Req/Conn | 11533 | 2140 | 5.39× |
| GOMAXPROCS=1,10 Req/Conn | 8734 | 1550 | 5.63× |
| GOMAXPROCS=1,10K Req/Conn | 7151 | 1422 | 5.03× |
| GOMAXPROCS=4,1 Req/Conn | 4542 | 1494 | 3.04× |
| GOMAXPROCS=4,2 Req/Conn | 3559 | 963 | 3.69× |
| GOMAXPROCS=4,10 Req/Conn | 2707 | 522 | 5.18× |
| GOMAXPROCS=4,10K Req/Conn | 2179 | 451 | 4.83× |
需要注意的是,真实业务中,HTTP 框架只占总耗时很小一部分,甚至可以说忽略不计,通常还有 JSON 编解码、数据库、Redis、网络延迟、业务逻辑等开销更大一些。
一般来说,除非做了实际压测确认 HTTP 框架是瓶颈,切换 fasthttp 有明显收益,一律默认使用基于标准库的 HTTP 框架,毕竟还得考虑 API 兼容性和生态成本。
性能好的原因分析:
- 对象池复用:RequestCtx、Request、Response 等对象通过对象池循环使用,减少频繁创建和销毁,从而降低堆分配与 GC 压力。
- []byte 与 Buffer 复用:HTTP 解析尽量直接操作已有字节缓冲区,减少 string 转换、数据复制和临时对象创建,同时降低内存分配。
- Worker Pool:通过 worker pool 复用连接处理 goroutine,并控制并发规模,避免高并发下大量创建 goroutine,降低调度和资源开销。
- 减少抽象层:直接使用
RequestCtx处理请求和响应,减少net/http中间抽象及对象转换,为更激进的内存复用提供空间。 - 生命周期设计:RequestCtx、[]byte、Buffer 等资源只在请求生命周期内安全使用,结束后立即复用;用更严格的生命周期约束换取更低的内存分配。
标准库的目标不是追求极限 Benchmark,而是通用性、兼容性、稳定性和可维护性。激进的对象池、零拷贝会引入严格生命周期约束,增加 API 心智负担,并限制 HTTP/2、生态兼容等能力。
net/http 更倾向于提供长期稳定的基础设施,而不是为极端高 QPS 场景牺牲通用性。
fasthttp
package main
import (
"flag"
"fmt"
"log"
"github.com/valyala/fasthttp"
)
var (
addr = flag.String("addr", ":8080", "TCP address to listen to")
compress = flag.Bool("compress", false, "Whether to enable transparent response compression")
)
func main() {
flag.Parse()
h := requestHandler
if *compress {
h = fasthttp.CompressHandler(h)
}
if err := fasthttp.ListenAndServe(*addr, h); err != nil {
log.Fatalf("Error in ListenAndServe: %v", err)
}
}
func requestHandler(ctx *fasthttp.RequestCtx) {
fmt.Fprintf(ctx, "Hello, world!\n\n")
fmt.Fprintf(ctx, "Request method is %q\n", ctx.Method())
fmt.Fprintf(ctx, "RequestURI is %q\n", ctx.RequestURI())
fmt.Fprintf(ctx, "Requested path is %q\n", ctx.Path())
fmt.Fprintf(ctx, "Host is %q\n", ctx.Host())
fmt.Fprintf(ctx, "Query string is %q\n", ctx.QueryArgs())
fmt.Fprintf(ctx, "User-Agent is %q\n", ctx.UserAgent())
fmt.Fprintf(ctx, "Connection has been established at %s\n", ctx.ConnTime())
fmt.Fprintf(ctx, "Request has been started at %s\n", ctx.Time())
fmt.Fprintf(ctx, "Serial request number for the current connection is %d\n", ctx.ConnRequestNum())
fmt.Fprintf(ctx, "Your ip is %q\n\n", ctx.RemoteIP())
fmt.Fprintf(ctx, "Raw request is:\n---CUT---\n%s\n---CUT---", &ctx.Request)
ctx.SetContentType("text/plain; charset=utf8")
// Set arbitrary headers
ctx.Response.Header.Set("X-My-Header", "my-header-value")
// Set cookies
var c fasthttp.Cookie
c.SetKey("cookie-name")
c.SetValue("cookie-value")
ctx.Response.Header.SetCookie(&c)
}
r.Body->ctx.PostBody()r.URL.Path->ctx.Path()r.URL->ctx.URI()r.Method->ctx.Method()r.Header->ctx.Request.Headerr.Header.Get()->ctx.Request.Header.Peek()r.Host->ctx.Host()r.Form->ctx.QueryArgs()+ctx.PostArgs()r.PostForm->ctx.PostArgs()r.FormValue()->ctx.FormValue()r.FormFile()->ctx.FormFile()r.MultipartForm->ctx.MultipartForm()r.RemoteAddr->ctx.RemoteAddr()r.RequestURI->ctx.RequestURI()r.TLS->ctx.IsTLS()r.Cookie()->ctx.Request.Header.Cookie()r.Referer()->ctx.Referer()r.UserAgent()->ctx.UserAgent()w.Header()->ctx.Response.Headerw.Header().Set()->ctx.Response.Header.Set()w.Header().Set("Content-Type")->ctx.SetContentType()w.Header().Set("Set-Cookie")->ctx.Response.Header.SetCookie()w.Write()->ctx.Write(),ctx.SetBody(),ctx.SetBodyStream(),ctx.SetBodyStreamWriter()w.WriteHeader()->ctx.SetStatusCode()w.(http.Hijacker).Hijack()->ctx.Hijack()http.Error()->ctx.Error()http.FileServer()->fasthttp.FSHandler(),fasthttp.FShttp.ServeFile()->fasthttp.ServeFile()http.Redirect()->ctx.Redirect()http.NotFound()->ctx.NotFound()http.StripPrefix()->fasthttp.PathRewriteFunc
fasthttp/router
/user/{user}named/user/{user}_admin/user/{user?}optional/user/{name:[a-zA-Z]{5}}regex/user/{name?:[a-zA-Z]{5}}/user/{name:*}cache all
fasthttp/session
package main
import (
"log"
"time"
"github.com/fasthttp/session/v2"
"github.com/fasthttp/session/v2/providers/redis"
)
var serverSession *session.Session
func init() {
var provider session.Provider
var err error
encoder := session.MSGPEncode
decoder := session.MSGPDecode
provider, err = redis.New(redis.Config{
KeyPrefix: "session",
Addr: "127.0.0.1:6379",
PoolSize: 8,
IdleTimeout: 30 * time.Second,
})
if err != nil {
log.Fatal(err)
}
cfg := session.NewDefaultConfig()
cfg.EncodeFunc = encoder
cfg.DecodeFunc = decoder
serverSession = session.New(cfg)
if err = serverSession.SetProvider(provider); err != nil {
log.Fatal(err)
}
}
// 获取 SessionStore
store, err := serverSession.Get(ctx)
if err != nil {
ctx.Error(err.Error(), fasthttp.StatusInternalServerError)
return
}
defer func() {
if err := serverSession.Save(ctx, store); err != nil {
ctx.Error(err.Error(), fasthttp.StatusInternalServerError)
}
}()
// Set
store.Set("foo", "bar")
// Get
val := store.Get("foo")
if val == nil {
// ...
}
store.Delete(key)
store.Set(bytesKey)
val := store.GetBytes(bytesKey)
store.DeleteBytes(bytesKey)
data := store.GetAll() // Dict
data := store.Ptr() // *Dict
store.Flush()
store.Reset()
// Session ID,默认 sessionid
id := store.GetSessionID()
store.SetSessionID(bytesID)
// Session 有效期
changed := store.HasExpirationChanged()
duration := store.GetExpiration()
err := store.SetExpiration(duration)
savsgio/atreugo
构建与 fasthttp 和 fasthttp/router 之上的 HTTP 框架,做了一些简单的封装工作。
提供了几个示例。
基础用法:
package main
import (
"github.com/savsgio/atreugo/v11"
)
func main() {
config := atreugo.Config{
Addr: "0.0.0.0:8000",
}
server := atreugo.New(config)
server.GET("/", func(ctx *atreugo.RequestCtx) error {
return ctx.TextResponse("Hello World")
})
server.GET("/echo/{path:*}", func(ctx *atreugo.RequestCtx) error {
return ctx.TextResponse("Echo message: " + ctx.UserValue("path").(string))
})
v1 := server.NewGroupPath("/v1")
v1.GET("/", func(ctx *atreugo.RequestCtx) error {
return ctx.TextResponse("Hello V1 Group")
})
if err := server.ListenAndServe(); err != nil {
panic(err)
}
}