Golang Reverse Proxy Practices

llance_24

L2ncE

Posted on November 25, 2022

Golang Reverse Proxy Practices

Hertz

Hertz is an ultra-large-scale enterprise-level microservice HTTP framework, featuring high ease of use, easy expansion, and low latency etc.

Hertz uses the self-developed high-performance network library Netpoll by default. In some special scenarios, Hertz has certain advantages in QPS and latency compared to go net.

In internal practice, some typical services, such as services with a high proportion of frameworks, gateways and other services, after migrating to Hertz, compared to the Gin framework, the resource usage is significantly reduced, CPU usage is reduced by 30%-60% with the size of the traffic.

For more details, see cloudwego/hertz.

Reverse proxy

In computer networks, a reverse proxy is an application that sits in front of back-end applications and forwards client (e.g. browser) requests to those applications.

Reverse proxies help increase scalability, performance, resilience and security. The resources returned to the client appear as if they originated from the web server itself.

Using reverse proxy in Hertz

Using a reverse proxy with Hertz requires pulling the reverseproxy extension provided by the community.

$ go get github.com/hertz-contrib/reverseproxy
Enter fullscreen mode Exit fullscreen mode

Basic using

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()
}
Enter fullscreen mode Exit fullscreen mode

We set the target path of the reverse proxy /proxy through the NewSingleHostReverseProxy function. Next, the path of the registered route is the sub-path /proxy/backend of the target path of the reverse proxy, and finally the reverse proxy service proxy.ServeHTTP is mapped by registering /backend. In this way, when we access /backend through the GET method, we will access the content in /proxy/backend.

curl 127.0.0.1:8000/backend

{"msg":"proxy success!!"}
Enter fullscreen mode Exit fullscreen mode

Custom configuration

Of course, the extension is not only able to implement a simple reverse proxy, there are many customizable options provided in the reverseproxy extension.

Method Description
SetDirector use to customize protocol.Request
SetClient use to customize client
SetModifyResponse use to customize modify response function
SetErrorHandler use to customize error handler

SetDirector & SetClient

We practice using SetDirector and SetClient by implementing a simple service registration discovery.

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()
}
Enter fullscreen mode Exit fullscreen mode

The sample code on the server side in the hertz-contrib/registry extension is used here. Since this is not the main content of this article, it will not be expanded. For more information, you can Go to the registry repository.

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()
}
Enter fullscreen mode Exit fullscreen mode

In the Client section we used a reverse proxy for service discovery. First, specify the client using the service discovery middleware as our forwarding client through SetClient, and then use SetDirector to specify our protocol.Request , and configure the use of service discovery in the new Request.

SetModifyResponse & SetErrorHandler

SetModifyResponse and SetErrorHandler respectively set the response from the backend and the handling of errors arriving in the backend. SetModifyResponse is actually modifyResponse in the reverse proxy extension. If the backend returns any response, no matter what the status code is, this method will be called. If the modifyResponse method returns an error, the errorHandler method will be called with the error as an argument.

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()
}
Enter fullscreen mode Exit fullscreen mode

Here, modifyResponse is modified by SetModifyResponse to change the processing content of the response.

Test

curl 127.0.0.1:8000/modifyResponse

change response success
Enter fullscreen mode Exit fullscreen mode
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()
}
Enter fullscreen mode Exit fullscreen mode

We use SetErrorHandler to specify how to handle errors that arrive in the background. When an error arrives in the background or there is an error from modifyResponse, the specified processing logic will be run.

Test

curl 127.0.0.1:8002/backend

fake 404 not found
Enter fullscreen mode Exit fullscreen mode

Middleware usage

In addition to basic use, Hertz reverse proxy also supports use in middleware.

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()
}
Enter fullscreen mode Exit fullscreen mode

In this example code, two Hertz instances are initialized first, then NewSingleHostReverseProxy is used to set the reverse proxy target to port 9997, and finally two routes with the same path are registered for the two instances respectively.

Test1

curl 127.0.0.1:9997/backend

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

{"message":"pong1"}
Enter fullscreen mode Exit fullscreen mode

The main part of this code is the middleware usage part. We use the middleware through Use. In the middleware logic, when the logic of ctx.Query("country") == "cn" is established, call proxy. ServeHTTP(c, ctx) uses a reverse proxy, and when you request /backend through instance r, it will request the content of the reverse proxy target port 9997.

Test2

curl 127.0.0.1:9998/backend?country=cn

{"message":"pong2"}
Enter fullscreen mode Exit fullscreen mode

Tips

  • For NewSingleHostReverseProxy function, if no config.ClientOption is passed it will use the default global client.Client instance. When passing config.ClientOption it will initialize a local client.Client instance. Using ReverseProxy.SetClient if there is need for shared customized client.Client instance.
  • The reverse proxy resets the header of the response, any such modifications before the request is made will be discarded.

Reference

💖 💪 🙅 🚩
llance_24
L2ncE

Posted on November 25, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related