有时候必须从无法直接访问的服务中获取指标。Go标准库包含一个反向代理函数?。通过将客户端请求代理到服务端,能够在不修改服务本身的情况下捕获指标。
什么是反向代理?
在计算机网络中,反向代理是一种代理服务器,它代表客户端从一个或多个服务器中获取请求资源。然后将这些资源返回给客户端,看起来就像资源来自代理服务器本身一样。
--源自维基百科
本质上,反向代理将客户端请求转发到代理后面的一组服务器。目前有多反向代理应用程序。例如:负载均衡、TLS终止和A/B测试等等。反向代理还可以用于对后端服务做一些指令插入,而不必修改服务本身。
如果你想了解更多关于代理的知识,我建议你看看Matt Klein的《现代网络负载均衡和代理介绍》。Matt是Envoy Proxy的创建者,这是一个强大的代理服务器,为Istio等服务网格工具提供支持。他的帖子很好地概述了现代负载均衡器和代理所使用的方法。
Go反向代理
Go是我最喜欢的编程语言之一,原因有很多。该语言的设计者关注的是简单、实用性和性能。这些特点使Go非常简单实用。这种语言在网络应用中非常出色。部分原因是它的标准库很全面,在常见实现中还包括反向代理。
go在应用中使用代理很简单:
proxy := httputil.NewSingleHostReverseProxy(url)
我们在进一步说明下,httputil.NewSingleHostReverseProxy方法返回一个包含以下方法的ReverseProxy结构体。
func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request)
我们需要做的就是配置代理并将其连接到一个标准的go HTTP服务器,以获得一个可用反向代理,如下所示:
package main
import (
"flag"
"log"
"net/http"
"net/http/httputil"
"net/url"
"strconv"
)
func main() {
port := flag.Int("port", 8080, "port to listen on")
targetURL := flag.String("target-url", "", "downstream service url to proxy to")
flag.Parse()
u, err := url.Parse(*targetURL)
if err != nil {
log.Fatalf("Could not parse downstream url: %s", *targetURL)
}
proxy := httputil.NewSingleHostReverseProxy(u)
director := proxy.Director
proxy.Director = func(req *http.Request) {
director(req)
req.Header.Set("X-Forwarded-Host", req.Header.Get("Host"))
req.Host = req.URL.Host
}
http.HandleFunc("/", proxy.ServeHTTP)
log.Printf("Listening on port %d", *port)
log.Fatal(http.ListenAndServe(":"+strconv.Itoa(*port), nil))
}
就是这样,这个服务器可以代理HTTP请求和websocket连接。您会注意到我已经配置了代理proxy.Director属性。ReverseProxy.Director是一个在传入请求被转发之前修改请求的函数。签名如下:
Director func(*http.Request)
Director函数的一个常见使用场景就是修改请求头。Go编程语言的原则之一是类型应该具有合理的默认值并立即可用。按照这个原则,由httputil.NewSingleHostReverseProxy 返回的默认Director负责设置请求schema、主机和路径。我不想代码重复,所以包装了这个实现。注意:这里必须重新设置req.Hos来处理HTTPS服务。通过req.Header.Set设置请求头,将传入的参数覆盖请求header的值。
获取指标
让我们扩展前面的简单代理来读取和上报关于下游服务响应的指标。为此,我们将回到httputil.ReverseProxy结构体。它包含一个结构字段ReverseProxy.ModifyResponse,可以实现在返回客户端之前访问HTTP响应。
ModifyResponse func(*net/http.Response) error
Go的HTTP body实现的了io.Reader接口,因此你只能读一遍。如果希望在转发请求或响应之前解析请求或响应,则需要将请求体复制到字节缓冲区并重置请求体。一个明显的缺点是我们在内存中需无限制地缓冲整个响应。如果您收到大量响应,那么在生产中可能会导致内存问题,在我的用例中,不会出现这种问题。下面是解析和重置响应的快速实现。
func parseResponse(res *http.Response, unmarshalStruct interface{}) error {
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}
res.Body.Close()
res.Body = ioutil.NopCloser(bytes.NewBuffer(body))
return json.Unmarshal(body, unmarshalStruct)
}
解决了请求体问题后,获取指标就变得简单了。
proxy := httputil.NewSingleHostReverseProxy(u)
// 设置proxy.Director ...
// ModifyResponse在将下游响应转发回客户端之前运行
proxy.ModifyResponse = func(res *http.Response) error {
responseContent := map[string]interface{}{}
err := parseResponse(res, &responseContent)
if err != nil {
return err
}
return captureMetrics(responseContent)
}
捕获指标功能对于我的用例是非常具体的,所以我将把它留给您来实现。我最终使用Prometheus客户端库来预测标签。
package main
import (
"bytes"
"encoding/json"
"flag"
"io/ioutil"
"log"
"net/http"
"net/http/httputil"
"net/url"
"strconv"
)
func parseResponse(res *http.Response, unmarshalStruct interface{}) error {
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}
res.Body.Close()
res.Body = ioutil.NopCloser(bytes.NewBuffer(body))
return json.Unmarshal(body, unmarshalStruct)
}
func captureMetrics(m map[string]interface{}) error {
// Add your metrics capture code here
log.Printf("captureMetrics = %+v\n", m)
return nil
}
func main() {
port := flag.Int("port", 8080, "port to listen on")
targetURL := flag.String("target-url", "", "downstream service url to proxy to")
flag.Parse()
u, err := url.Parse(*targetURL)
if err != nil {
log.Fatalf("Could not parse downstream url: %s", *targetURL)
}
proxy := httputil.NewSingleHostReverseProxy(u)
director := proxy.Director
proxy.Director = func(req *http.Request) {
director(req)
req.Header.Set("X-Forwarded-Host", req.Header.Get("Host"))
req.Host = req.URL.Host
}
proxy.ModifyResponse = func(res *http.Response) error {
responseContent := map[string]interface{}{}
err := parseResponse(res, &responseContent)
if err != nil {
return err
}
return captureMetrics(responseContent)
}
http.HandleFunc("/", proxy.ServeHTTP)
log.Printf("Listening on port %d", *port)
log.Fatal(http.ListenAndServe(":"+strconv.Itoa(*port), nil))
}
Go的标准库实现了大部分工作。从这里,您可以将代理扩展到其他任意用途。