Hertz

Hertz 是一个超大规模的企业级微服务 HTTP 框架,具有高易用性、易扩展、低时延等特点。

Hertz 默认使用自研的高性能网络库 Netpoll,在一些特殊场景中,相较于 go net,Hertz 在 QPS、时延上均具有一定优势。

在内部实践中,某些典型服务,如框架占比较高的服务、网关等服务,迁移 Hertz 后相比 Gin 框架,资源使用显著减少,CPU 使用率随流量大小降低 30%—60%

关于 Hertz 更多的信息可移步至 cloudwego/hertz

反向代理

反向代理在计算机网络中是代理服务器的一种。

服务器根据客户端的请求,从其关系的一组或多组后端服务器(如 Web 服务器)上获取资源,然后再将这些资源返回给客户端,客户端只会得知反向代理的 IP 地址,而不知道在代理服务器后面的服务器集群的存在。

Hertz 反向代理实战

在 Hertz 中使用反向代理需要拉取社区提供的 reverseproxy 拓展。

$ go get github.com/hertz-contrib/reverseproxy

基本使用

package main

import (
        "context"

        "github.com/cloudwego/hertz/pkg/app"
        "github.com/cloudwego/hertz/pkg/app/server"
        "github.com/cloudwego/hertz/pkg/common/utils"
        "github.com/hertz-contrib/reverseproxy"
)

func main() {
        h := server.Default(server.WithHostPorts("127.0.0.1:8000"))
        proxy, err := reverseproxy.NewSingleHostReverseProxy("http://127.0.0.1:8000/proxy")
        if err != nil {
                panic(err)
        }
        h.GET("/proxy/backend", func(cc context.Context, c *app.RequestContext) {
                c.JSON(200, utils.H{
                        "msg": "proxy success!!",
                })
        })
        h.GET("/backend", proxy.ServeHTTP)
        h.Spin()
}
NewSingleHostReverseProxy/proxy/proxy/backend/backendproxy.ServeHTTP/backend/proxy/backend
curl 127.0.0.1:8000/backend

{"msg":"proxy success!!"}

自定义配置

当然,拓展不只是能够实现简单的反向代理,在 reverseproxy 拓展中提供了许多可以自定义的可选项。

SetDirectorSetClientSetModifyResponseSetErrorHandler

SetDirector & SetClient

SetDirectorSetClient
Server
package main

import (
        "context"

        "github.com/cloudwego/hertz/pkg/app"
        "github.com/cloudwego/hertz/pkg/app/server"
        "github.com/cloudwego/hertz/pkg/app/server/registry"
        "github.com/cloudwego/hertz/pkg/common/utils"
        "github.com/cloudwego/hertz/pkg/protocol/consts"
        "github.com/hertz-contrib/registry/nacos"
)

func main() {
        addr := "127.0.0.1:8000"
        r, _ := nacos.NewDefaultNacosRegistry()
        h := server.Default(
                server.WithHostPorts(addr),
                server.WithRegistry(r, &registry.Info{
                        ServiceName: "demo.hertz-contrib.reverseproxy",
                        Addr:        utils.NewNetAddr("tcp", addr),
                        Weight:      10,
                }),
        )
        h.GET("/backend", func(cc context.Context, c *app.RequestContext) {
                c.JSON(consts.StatusOK, utils.H{"ping": "pong"})
        })
        h.Spin()
}

这里使用了 hertz-contrib/registry 拓展中 server 端的示例代码,由于这并不是本文的主要内容所以不做展开,关于更多信息可以去到 registry 库中。

Client
package main

import (
        "github.com/cloudwego/hertz/pkg/app/client"
        "github.com/cloudwego/hertz/pkg/app/middlewares/client/sd"
        "github.com/cloudwego/hertz/pkg/app/server"
        "github.com/cloudwego/hertz/pkg/common/config"
        "github.com/cloudwego/hertz/pkg/protocol"
        "github.com/hertz-contrib/registry/nacos"
        "github.com/hertz-contrib/reverseproxy"
)

func main() {
        cli, err := client.NewClient()
        if err != nil {
                panic(err)
        }
        r, err := nacos.NewDefaultNacosResolver()
        if err != nil {
                panic(err)
        }
        cli.Use(sd.Discovery(r))
        h := server.New(server.WithHostPorts(":8741"))
        proxy, _ := reverseproxy.NewSingleHostReverseProxy("http://demo.hertz-contrib.reverseproxy")
        proxy.SetClient(cli)
        proxy.SetDirector(func(req *protocol.Request) {
                req.SetRequestURI(string(reverseproxy.JoinURLPath(req, proxy.Target)))
                req.Header.SetHostBytes(req.URI().Host())
                req.Options().Apply([]config.RequestOption{config.WithSD(true)})
        })
        h.GET("/backend", proxy.ServeHTTP)
        h.Spin()
}
SetClientSetDirector

SetModifyResponse & SetErrorHandler

SetModifyResponseSetErrorHandlerSetModifyResponsemodifyResponsemodifyResponseerrorHandler
SetModifyResponse
package main

import (
        "github.com/cloudwego/hertz/pkg/app/server"
        "github.com/cloudwego/hertz/pkg/protocol"
        "github.com/hertz-contrib/reverseproxy"
)

func main() {
        h := server.Default(server.WithHostPorts("127.0.0.1:8000"))
        // modify response
        proxy, _ := reverseproxy.NewSingleHostReverseProxy("http://127.0.0.1:8000/proxy")
        proxy.SetModifyResponse(func(resp *protocol.Response) error {
                resp.SetStatusCode(200)
                resp.SetBodyRaw([]byte("change response success"))
                return nil
        })
        h.GET("/modifyResponse", proxy.ServeHTTP)

        h.Spin()
}
SetModifyResponsemodifyResponse

测试

curl 127.0.0.1:8000/modifyResponse

change response success
SetErrorHandler
package main

import (
        "context"

        "github.com/cloudwego/hertz/pkg/app"
        "github.com/cloudwego/hertz/pkg/app/server"
        "github.com/cloudwego/hertz/pkg/common/utils"
        "github.com/hertz-contrib/reverseproxy"
)

func main() {
        h := server.Default(server.WithHostPorts("127.0.0.1:8002"))
        proxy, err := reverseproxy.NewSingleHostReverseProxy("http://127.0.0.1:8000/proxy")
        if err != nil {
                panic(err)
        }
        proxy.SetErrorHandler(func(c *app.RequestContext, err error) {
                c.Response.SetStatusCode(404)
                c.String(404, "fake 404 not found")
        })

        h.GET("/proxy/backend", func(cc context.Context, c *app.RequestContext) {
                c.JSON(200, utils.H{
                        "msg": "proxy success!!",
                })
        })
        h.GET("/backend", proxy.ServeHTTP)
        h.Spin()
}
SetErrorHandlermodifyResponse

测试

curl 127.0.0.1:8002/backend

fake 404 not found

中间件使用

除了基本使用外,Hertz 反向代理还支持在中间件中使用。

package main

import (
        "context"
        "github.com/cloudwego/hertz/pkg/app"
        "github.com/cloudwego/hertz/pkg/app/server"
        "github.com/cloudwego/hertz/pkg/common/utils"
        "github.com/hertz-contrib/reverseproxy"
)

func main() {
        r := server.Default(server.WithHostPorts("127.0.0.1:9998"))
        r2 := server.Default(server.WithHostPorts("127.0.0.1:9997"))

        proxy, err := reverseproxy.NewSingleHostReverseProxy("http://127.0.0.1:9997")
        if err != nil {
                panic(err)
        }

        r.Use(func(c context.Context, ctx *app.RequestContext) {
                if ctx.Query("country") == "cn" {
                        proxy.ServeHTTP(c, ctx)
                        ctx.Response.Header.Set("key", "value")
                        ctx.Abort()
                } else {
                        ctx.Next(c)
                }
        })

        r.GET("/backend", func(c context.Context, ctx *app.RequestContext) {
                ctx.JSON(200, utils.H{
                        "message": "pong1",
                })
        })

        r2.GET("/backend", func(c context.Context, ctx *app.RequestContext) {
                ctx.JSON(200, utils.H{
                        "message": "pong2",
                })
        })

        go r.Spin()
        r2.Spin()
}
NewSingleHostReverseProxy

测试1

curl 127.0.0.1:9997/backend

{"message":"pong2"}
curl 127.0.0.1:9998/backend

{"message":"pong1"}
Usectx.Query("country") == "cn"proxy.ServeHTTP(c, ctx)/backend

测试2

curl 127.0.0.1:9998/backend?country=cn

{"message":"pong2"}

注意项

NewSingleHostReverseProxyconfig.ClientOptionclient.Clientconfig.ClientOptionclient.Clientclient.ClientReverseProxy.SetClient
  • 反向代理会重置响应头,如果在请求之前修改了响应头将不会生效,这与标准库的行为不一致。

参考