Golang 反向代理 reverse proxy 示例

一个 golang 反向代理的极简实现。关于反向代理,实际包含三部分:客户端、反向代理、服务端。其请求流程如下:

客户端 --> 反向代理 --> 服务端

也就是客户端先将请求发给反向代理,反向代理再将请求转发给服务端,这里反向代理也是一个服务器,监听在某个端口。具体实现如下。

客户端

客户端向反向代理发送请求,所以客户端需要知道反向代理的地址(这不是废话吗),依我现在的看法来看,客户端只是把服务端的地址换成了反向代理的地址,其他参数没有变。以 http://localhost:8080/hello/def 为例,localhost:8080 是反向代理的地址,hello/def 这个请求参数是给真正的服务端的,反向代理可以不处理。

package main

import (
	"io"
	"log"
	"net/http"
	"os"
)

func main() {
	req, err := http.NewRequest("GET", "http://localhost:8080/hello/def", nil)
	if err != nil {
		log.Fatal(err)
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	io.Copy(os.Stdout, resp.Body)
}

反向代理

初始化反向代理需要一个参数:后端服务器的地址,也就是下面的 http://localhost:8888,也是唯一必须的一个,除此之前还可以搞点其他事情,比如:修改 Request、修改 Response、负载均衡、定制化错误处理等。下面我写了个修改 Response 的方法,就是在 Response 里添加了一个 header 参数,作为示例。初始化完了之后,这个代理就监听在 8080 端口。并将请求转发给 8888 端口。

package main

import (
	"fmt"
	"log"
	"net/http"
	"net/http/httputil"
	"net/url"
	"time"
)
// 修改 Response
func modifyResponse() func(*http.Response) error {
	return func(resp *http.Response) error {
		fmt.Printf("%v, proxy set a header for response\n", time.Now())
		resp.Header.Set("key1", "value1")
		return nil
	}
}

func main() {
	url, err := url.Parse("http://localhost:8888")
	if err != nil {
		panic(err)
	}
	proxy := httputil.NewSingleHostReverseProxy(url)
	proxy.ModifyResponse = modifyResponse()

	http.HandleFunc("/", proxy.ServeHTTP)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

服务端

服务端是真正的业务处理,其代码如下,代码比较简单,就是监听在 8888 端口,并且定义了一个 hello 处理方法。

package main

import (
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/julienschmidt/httprouter"
)
func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	fmt.Println("hello handler has been called in server")
	fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name"))
}

func main() {
	router := httprouter.New()
	router.GET("/hello/:name", Hello)
	log.Fatal(http.ListenAndServe(":8888", router))
}

实现一个负载均衡器

Let’s Create a Simple Load Balancer With Go,介绍了一个简单负载均衡器的实现,这个负载均衡器使用 reverse proxy 实现请求的转发。简单梳理下这个负载均衡器的实现。

数据结构

数据结构:Backend 代表一个后端 Server,这个数据结构包含 Server 是否 Active、其反向代理等。 ServerPool 是所有 Server 的集合,因为在实现的负载均衡算法中,使用轮询的策略,所以用了一个字段 current 来统计前一个请求连接的 Server 序号,当下一个请求来临时,在 current 的基础上加 1 并取余就好了。

// Backend 包含了一个后端 Server 的所有信息:URL、是否 Alive、反向代理
type Backend struct {
	URL          *url.URL
	Alive        bool
	mux          sync.RWMutex
	ReverseProxy *httputil.ReverseProxy
}

// ServerPool 是所有后端 Server 的集合,
type ServerPool struct {
	backends []*Backend
	current  uint64
}

// NextIndex atomically increase the counter and return an index
func (s *ServerPool) NextIndex() int {
	return int(atomic.AddUint64(&s.current, uint64(1)) % uint64(len(s.backends)))
}

探活

探活就是测试后端服务是否正常,包括主动和被动两种方式,主动就是定时发送探活请求,被动就是让这个 Server 处理请求时,发现超时了。

主动探活实现如下,简单一点。每隔两分钟对所有 Server 探活一次,如果探活失败,就标记为 failure。

func healthCheck() {
	t := time.NewTicker(time.Minute * 2)
	for {
		select {
		case <-t.C:
			log.Println("Starting health check...")
			serverPool.HealthCheck()
			log.Println("Health check completed")
		}
	}
}
// 对所有的后端进行探活
func (s *ServerPool) HealthCheck() {
	for _, b := range s.backends {
		status := "up"
		alive := isBackendAlive(b.URL)
		b.SetAlive(alive)
		if !alive {
			status = "down"
		}
		log.Printf("%s [%s]\n", b.URL, status)
	}
}

被动探活是依靠反向代理实现的,是在反向代理的 ErrorHandler 里实现的,有两种情况会触发反向代理的 ErrorHandler: 1)后端 Server 不可达。2)反向代理的 ModifyResponse 出现错误。在关于探活的实现中,如果某个 Server 的反向代理(一个后端 Server 有一个反向代理)请求某个后端失败,仍然使用这个代理重试几次,如果超过三次,就认为这个 Server 失败了,标记为 failure。

另外还有个地方需要注意一下,对于后端 Server 请求失败的次数,实现的负载均衡器是放在了 Request 的 context 参数里的。但是 golang 的 context 是不能通过电缆传输的(只能是进程内通信),这可以参考Context Propagation over HTTP in Go,再扩展一点,这篇博客里提到了将 context 的值放到 header 里来实现 context 值传输的方式,如下:

// 需要传输的 context key。
const requestIDHeader = "request-id"
// 自定义 Transport 来实现将 context 放到 header 中。
type Transport struct {
	Base http.RoundTripper
}
func (t *Transport) RoundTrip(r *http.Request) (*http.Response, error) {
	r = cloneReq(r) // per RoundTrip interface enforces
	// 取出 context key 对应的 context value 并放到 header 中。
	rid := request.IDFromContext(r.Context())
 	if rid != "" {
  		r.Header.Add(requestIDHeader, rid)
 	}
 	base := t.Base
 	if base == nil {
  		base = http.DefaultTransport
 	}
 return base.RoundTrip(r)
}

在这个负载均衡的反向代理实现中,实际处理的 Request 都是同一个本地的 Request,即 LB 自己收到的 Request,并传递给了 ReverseProxy。

上面这个自定义的 Transport 起了一个中间件的作用,拦截 Request 请求,取出其中 Context 的值,并重新初始化一个 Request,把 Context 的值放到新 Request 的 header 中,然后用 http 原生的 Transport 处理新请求。

反向代理进行请求转发

这里直接给出反向代理的实现,定义一个反向代理的输入实际只要一个后端 Server 的 URL。这里使用反向代理只是为了转发请求。另外,这里的实现中,如果一个 client 的请求,试探了三个后端 Server 都失败了,则对这个 Client 返回错误。

// TODO 根据提供的 url 生成一个代理,这个代理只是定义了错误处理的 handler
func generateProxy(serverUrl *url.URL) *httputil.ReverseProxy {
	// 使用提供的 url 初始化一个 reverse proxy
	proxy := httputil.NewSingleHostReverseProxy(serverUrl)

	proxy.ErrorHandler = func(writer http.ResponseWriter, request *http.Request, e error) {
		log.Printf("[%s] %s\n", serverUrl.Host, e.Error())
		// 从 request 的 context 里取出 value,TODO 关注下,这个 Request 从哪里产生的
		retries := GetRetryFromContext(request)
		if retries < 3 {
			// 出错了,重试,没超过 3 次
			select {
			case <-time.After(10 * time.Millisecond):
				// TODO 重新设置 context 并经请求重新发下去
				ctx := context.WithValue(request.Context(), Retry, retries+1)
				proxy.ServeHTTP(writer, request.WithContext(ctx))
			}
			return
		}

		// after 3 retries, mark this backend as down
		// TODO 超过了三次,标记这个 server 挂了
		serverPool.MarkBackendStatus(serverUrl, false)

		// if the same request routing for few attempts with different backends, increase the count
		attempts := GetAttemptsFromContext(request)
		log.Printf("%s(%s) Attempting retry %d\n", request.RemoteAddr, request.URL.Path, attempts)
		ctx := context.WithValue(request.Context(), Attempts, attempts+1)
		lb(writer, request.WithContext(ctx))
	}
	return proxy
}

参考:使用 Go 语言徒手撸一个简单的负载均衡器