程序员社区

使用Go的反向代理获取服务指标

使用Go的反向代理获取服务指标插图

有时候必须从无法直接访问的服务中获取指标。Go标准库包含一个反向代理函数?。通过将客户端请求代理到服务端,能够在不修改服务本身的情况下捕获指标。

什么是反向代理?

在计算机网络中,反向代理是一种代理服务器,它代表客户端从一个或多个服务器中获取请求资源。然后将这些资源返回给客户端,看起来就像资源来自代理服务器本身一样。
--源自维基百科

本质上,反向代理将客户端请求转发到代理后面的一组服务器。目前有多反向代理应用程序。例如:负载均衡、TLS终止和A/B测试等等。反向代理还可以用于对后端服务做一些指令插入,而不必修改服务本身。

使用Go的反向代理获取服务指标插图1
反响代理

如果你想了解更多关于代理的知识,我建议你看看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的标准库实现了大部分工作。从这里,您可以将代理扩展到其他任意用途。

赞(0) 打赏
未经允许不得转载:IDEA激活码 » 使用Go的反向代理获取服务指标

一个分享Java & Python知识的社区