1. pushprox 应用场景

监控系统有 pull 和 push 两种模式。pull 模式下,server 侧会主动去找各个 node 获取 metric ,主动权在 server,server需要知悉 node 的信息,可以随时调整收集哪些 node 的哪些 metric,以什么频次去收集。pull 模式比较适合数据中心的场景,prometheus是典型的 pull 模式。 push 模式是由 node 将 metric 上报给 server,node 侧需要配置要上报什么 metric,以什么频次上报给什么 server。push 模式更适合 iot 的场景,open-falcon 是 push 模式。部署监控系统时除了考虑 pull、push 的因素,还会考虑监控系统的生态,比如 k8s 环境下选择 prometheus 会带来较多的便利性。

在使用 prometheus 的情况下,由于 prometheus server 需要能够直接访问到 node。如果 node 是在 NAT 环境下,仍然需要 prometheus server 去收集这些 node 的 metric,就需要使用一些附加的方式,pushprox 就是应用于这种场景。

2. 架构

pushprox 主要由 proxy 和 client 两部分组成,proxy 负责接收来自 prometheus server 的 metric 请求,然后 proxy 将请求信息 proxy 给 client,client 找本地设备获取到 metric 后,将结果返回给 proxy。要求 proxy 部署在 prometheus 能够访问到的地方,client 则部署在需要采集的 node 所在的网络。

在这里插入图片描述

  1. 启动 proxy、client
  2. client 使用 http POST 调用 proxy 的 /poll 接口,这一步会建立 client 和 proxy 之间的一个长连接,这个长连接依赖 http1.1 的 KeepAlive 来实现。这条连接建立后表示 client 已经做好准备,可以获取 client 对应的 node(fqdn-x) 的 metric 了。
  3. prometheus 找 proxy 要 fqdn-x 的 metric
  4. proxy 收到请求后会将该次请求作为 第(1)步的 /poll 接口的 response 报文返回给 client
  5. client 收到第(3)步的 scrape 的请求后,新起一个 http 请求找 node(fqdn-x) 请求 metric
  6. node(fqdn-x) 将 metric 返回给 client
  7. client 获取到 node(fqdn-x) 的 metric 后,使用区别 poll 的另一个长连接用于 client 将 metric push 给 proxy。之后 client 有其他的 metric 需要 push 给 proxy 会复用这个长连接。所以,如果没有 第(2)步 prometheus 或其他服务来跟 proxy 要 metric,client 和 proxy 之间只会维持一个 poll 相关的长连接。当有 metric 请求后,client 和 proxy 之间会在维持一个用于 push metric 的长连接。
  8. proxy 收到 第(6)步 client push 的 metric 后,将 metric 返回给 prometheus

我们通常说 HTTP 是无状态的,上面说的长连接主要是居于 http1.1 的 KeepAlive 实现的类似于长连接的效果。具体的连接情况和对应的抓包如下:

# ~ netstat -natp|grep 43.243.130.151
tcp        0      0 192.168.10.212:45922    43.243.130.151:8080     ESTABLISHED 4019233/./pushprox- # 用于 poll
tcp        0      0 192.168.10.212:45930    43.243.130.151:8080     ESTABLISHED 4019233/./pushprox- # 用于 push

pushprox-dataflow

3. 部署和测试

3.1. 组件

  1. node_exporter 暴露 node 的一些基础 metric
  2. pushprox-client 和 node_exporter 一起部署在需要收集的 NAT 环境下的 node 上
  3. pushprox-proxy 和 prometheus 都部署在外网。prometheus 可以访问到 pushprox-proxy 的服务。

3.2. proxy 对 client 进行 TLS 认证

由于目前 proxy 和 client 之间没有做认证,client 所在 ip 不确定,proxy 无法针对 client 配置防火墙策略,需要增加 proxy 和 client 之间的认证。

使用 tls 来对 client 进行校验。pushprox-proxy 不支持 tls,需要 nginx 作为反向代理到 pushprox-proxy,然后 nginx 上配置 tls,并且在 nginx 上开启 ssl_verify_client 。pushprox-client 支持 tls 客户端连接到 nginx 。pushprox-client 要求 tls 要支持 SAN(Subject Alternative Name) 是SSL 标准x509 中定义的一个扩展)。所以生成自签名证书的时候需要支持 SAN 。

3.2.1. 生成支持 SAN 的自签名证书

参考 https://github.com/ljq/gen-tlsv3-san-ca 来生成自签名证书

3.2.2. 启动 pushprox-proxy

pushprox-proxy 需要部署在公网的服务器上,而且 pushprox-proxy 要跟每个 client 保持一个长连接,如果 client 比较多, proxy 要考虑负载均衡。

./pushprox-proxy

3.2.3. 配置 nginx

nginx 反向代理到 pushprox-proxy, nginx 上配置 tls

pushprox.conf

upstream prox-backend {
    server 127.0.0.1:8080;
}

server {
        listen 443 ssl;
        server_name promprox.xk.com;
        ssl on;
        ssl_certificate /opt/xk-self-cert/server.crt;#配置证书位置
        ssl_certificate_key /opt/xk-self-cert/server.key;#配置秘钥位置
        ssl_client_certificate /opt/xk-self-cert/ca.crt;#双向认证
        ssl_verify_client on; #双向认证
        ssl_session_timeout 5m;
        ssl_protocols SSLv2 SSLv3 TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; #按照这个协议配置
        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE; #按照这个套件配置
        ssl_prefer_server_ciphers on;

        location / {
            proxy_pass       http://prox-backend/;
        }
}

3.2.4. 启动 client

pushprox-client 要配置自签名证书中的 ca 和 client crt

./pushprox-client --proxy-url=https://promprox.xk.com --tls.cacert=/opt/xk-self-cert/ca.crt --tls.cert=/opt/xk-self-cert/client.crt --tls.key=/opt/xk-self-cert/client.key

3.2.5. 启动 prometheus

./prometheus --config.file=prometheus-prox.yml
# prometheus-prox.yml

# my global config
global:
  scrape_interval:     15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
  # scrape_timeout is set to the global default (10s).

# Alertmanager configuration
alerting:
  alertmanagers:
  - static_configs:
    - targets:
      # - alertmanager:9093

# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
  # - "first_rules.yml"
  # - "second_rules.yml"

# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
  - job_name: node
    proxy_url: http://43.243.130.151:8080/ # 配置为 proxy 的地址
    static_configs:
      - targets: ['ceph-mon2:9100']  # Presuming the FQDN of the client is "client".

4. 代码走读

4.1. 代码目录

代码地址: https://github.com/prometheus-community/PushProx

主要的代码都在 cmd 目录下:

├── client
│   ├── main.go
│   └── main_test.go
└── proxy
    ├── coordinator.go
    └── main.go

4.2. client 的代码

代码流转:

  1. main()
  2. Coordinator.loop(),这里会循环执行 Coordinator.doPoll,如果调用失败使用 backoff 策略来确定失败后再次调用 doPoll 的频率
  3. Coordinator.doPoll,doPoll 是使用 http POST 调用 proxy 的 poll 接口,如果没有收到 proxy 的 response 报文,doPoll 会 hung 在这边等 proxy 给 response。如果收到 proxy 的 response 报文,表名有 scrape metric 的任务,会新起一个携程执行 Coordinator.doScrape
  4. Coordinator.doScrape 会找 node(fqdn-x) 要到 metric,然后调用 proxy 的 push 接口将 metric push 给 proxy

4.3. proxy 的代码

代码流转:

  1. main()
  2. newHTTPHandler(),这个是处理的路口,http server 的路由在这里配置,执行 poll,push 等接口对应的 handler 函数。proxy 还暴露了一些关于接口调用的 metric,比如pushprox_http_requests_total,pushproxy_proxied_requests_total,在 newHTTPHandler 里面使用了 promhttp.InstrumentHandlerCounter 这样的方式作为 http handlerFunc 的 middleware 在实际的 handler 函数前后插入了一些 metric 打点的逻辑。
  3. httpHandler.handlePoll 用于处理 poll 请求

5. proxy 和 client 之间如何复用 TCP 连接

pushprox 是如何处理 client 和 proxy 的连接,来满足 prometheus metric 获取的低耗时以及一台 proxy 能够支持尽量多的 client。应该说 pushprox 仅仅使用 http 协议的交互来达到 client 和 proxy 之间长连接的效果,然后多个请求复用同一个 tcp 连接,还是可以借鉴的,因为实现起来非常容易。

原本一直认为 HTTP 是无状态的,所以天生就应该是短连接(这边的短连接是指 client 发一个 request 给 server,server 回应一个 response,然后连接关闭,下次再要通信就在发起一个 tcp 连接)的。目前 pushprox 这个 client 和 proxy 的依赖于 http1.1 的长连接使用起来还是比较稳定的,资源消耗也挺小。

5.1 http keepalive

keepalive 在 http 1.0(rfc1945)中默认是关闭的,需要在http头加入"Connection: Keep-Alive",才能启用Keep-Alive;http 1.1(rfc2068)中默认启用Keep-Alive,如果加入"Connection: close ",才关闭。
使用 keepalive 的好处在 rfc2068 有说明,比如请求一个网页,可能需要有图片、css等多个文件请求,放在一个 tcp 连接里面可以提升效率。
那么如果启用了 keepalive,server 和 client 之前如何知道什么时候该关闭请求呢?比如,client 就找 server 要资源A,然后结束,server 给了 client response 报文后,如果 server 不关闭连接,client 如何知道已经收到了 server 关于资源A的所有内容更应该要关闭连接了呢?目前主要是两种情况:

  1. 如果资源A是静态资源而且比较小能够一次响应给 client,通常 server 侧会在 response 报文的 header 添加 Conent-Length,用来表示 response 报文的大小,client 收到该大小的 response 后就知道已经收到所有内容,可以主动关闭 tcp 连接。

req-resp-http.png

  1. 如果资源A是动态的或者比较大,需要多次分段给 client,这种情况下通常使用 chunk 模式。server 将 response 报文的 “Transfer-Encoding: chunked”,chunk 编码将数据分成一块一块的发送,最后一块由一个标明长度为0的chunk标示结束。关于 chunk 模式可以参考 RFC 2616

5.2 golang http server 和 client 之间的 keepalive

我原本以为 golang 实现 http server 和 client 的长连接需要在 client 侧设置 http.Transport 中 DialContext 的 KeepAlive 等相关的值,但是使用 gin 作为 http server,默认的 http client,很容易就能实现这个长连接的效果。

5.2.1 gin http server

func main() {
	r := gin.Default()
	r.POST("/echo", func(c *gin.Context) {
		body, _ := ioutil.ReadAll(c.Request.Body)
		fmt.Println("---body/--- \r\n " + string(body))

		time.Sleep(1000000000 * time.Second) # hung 住请求

		c.JSON(200, gin.H{
			"msg": "ok",
		})
	})
	r.Run(":8282") // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

5.2.2 http client

var server = flag.String("server", "http://127.0.0.1:8282/echo", "http server url")

func init() {
	flag.Parse()
}

func main() {
	client := &http.Client{}
	doPost(client)
}

func doPost(client *http.Client) {
	resp, _ := client.Post(*server, "application/x-www-form-urlencoded", strings.NewReader("name=moon"))
	defer resp.Body.Close()
	body, _ := ioutil.ReadAll(resp.Body)
	fmt.Println(string(body))
}

5.2.3 gin http server 和 http client 抓包

golang-http-keepalive

从上面这张抓包的图可以看出来 http1.1 是如何来实现 keepalive 的,因为 server 侧一直没有给 response 报文,所以 client 一直到等报文,但是这个 tcp 连接并没有超时,依赖的是 tcp 的 keepalive 报文,基本上是每 15s,处在应用层的 http1.1 协议会为了维持住这个 tcp 连接,server 侧会给 client 发 tcp keepalive 报文,这个 15s 是要参考服务器上关于 tcp 连接超时的配置的。

6. 参考

  • https://github.com/prometheus-community/PushProx
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐