Skip to content

feat(otel): Metrics basic components and metrics collection for TCP - #89

Open
guowei-gong wants to merge 1 commit into
dobyte:mainfrom
guowei-gong:feat/metrics-tcp
Open

feat(otel): Metrics basic components and metrics collection for TCP#89
guowei-gong wants to merge 1 commit into
dobyte:mainfrom
guowei-gong:feat/metrics-tcp

Conversation

@guowei-gong

Copy link
Copy Markdown
Contributor

Hi @dobyte ,我这次 PR 的主旨是为 due 提供可插拔、遵守 otel 规范的 metrics 指标监控能力,默认不开启,开启方式是在配置文件中新增。

使用示例

func main() {
	// 创建容器
	container := app.NewContainer()

	// 创建监控
	metricsComponent := metrics.NewMetrics()

	// 创建 TCP 服务器
	server := tcp.NewServer()

	// 创建用户定位器
	locator := redis.NewLocator()

	// 创建服务发现
	registry := consul.NewRegistry()

	// 创建网关组件
	component := gate.NewGate(
		gate.WithServer(server),
		gate.WithLocator(locator),
		gate.WithRegistry(registry),
	)

	container.Add(metricsComponent, component)

	// 启动容器
	container.Serve()
}
# 指标采集
[network.tcp.server.metrics]
enable = true

# OpenTelemetry 指标导出配置,默认关闭
[otel]
  enable = true
  serviceName = "Ashen"
  serviceInstanceID = "1"
  [otel.metrics]
    protocol = "grpc"
    endpoint = "localhost:4317"
    insecure = true
    exportInterval = "10s"
    exportTimeout = "3s"

开启指标采集需要配置两处 enable 开关,这样做的目的如下。

  1. 不让业务埋点方去读取 otel 包的配置产生耦合
  2. 业务方可以自己控制是否需要采集指标。未来很多模块都可以采集指标,我们可以自由的选择某个模块是否要采集。no-op 不代表没有消耗,我设计的出发点是,如果业务方没有开启,直接不会运行到指标相关的代码(init 除外,init 中初始化的 metrics 只是指标属性,指标属性不多,消耗很小,可以忽略不计),比如我的 otel 的 enable 是 true,但是 network.tcp.server.metrics 的 enable 是 false,那么 tcp 模块也不会采集指标。

效果图

image

设计思路

目前可观测性的官方最佳实践流程是:

业务应用打点 Otel SDK -> 通过 OTLP 推送到 Collector -> Prometheus 定时拉取 Collector 暴露的 /metrics

在 due 中,我选择的集成方式是,定义应用打点指标并采集,通过 OTLP 推送到 Collector,我们可以把这个能力看作 Exporter。框架不关心:

  • 如何启动 Collector 服务;
  • 使用 Prometheus 还是什么服务。

为什么需要 Collector,我不能直接推送到 Prometheus?或者直接让 Prometheus 定时拉取我业务应用打点产出的数据呢?

当然可以!但如果没有 Collector 且我们是分布式的情况下,每个进程都暴露 /metrics,服务发现的成本就很高了,Prometheus 需要去知道每个实例的地址;为每个实例去增加配置;实例发生变化,Prometheus 也要跟着修改。Collector 作用不止于此,它单独作为进程,还有聚合过滤指标、批处理和稳定性的原因,就不详细展开了。

为了遵守项目风格,我选择在 component 包下,新增 otel/metrics 包来组织代码。otel 是可观测性的简写,为未来扩展 trace 和 log 留下口子。

otel/metrics 包,主要负责把管道建好并持续导出数据,也就是:

  • 创建 Resource(比如 service.name)
  • 创建 OTLP Exporter(负责网络传输)
  • 创建 MeterProvider(管理指标生产) + PeriodicReader(定时把指标收走并导出)

框架的打点和指标类别的定义,是在各个包自己做的!前者打点在对应的包去做,这点很好理解,为什么指标类型的定义也放到业务方呢?
目的是为了降低耦合,如果指标定义都塞到 otel/metrics,组件会依赖所有业务细节,每加一个指标都要改公共组件,并且组件也不知道业务要打哪些点。

本次集成 metrics 的组件只有 network/tcp,采集了 7 个指标:

var (
	messages         metric.Int64ObservableCounter // 收发消息数量
	errorsTotal      metric.Int64Counter           // 网络错误数量
	receiveBytes     metric.Int64ObservableCounter // 接收字节总数
	sendBytes        metric.Int64ObservableCounter // 发送字节总数
	connectionsOpen  metric.Int64Counter           // 建立连接总数
	connectionsClose metric.Int64Counter           // 关闭连接总数
	connectionsAlive metric.Int64UpDownCounter     // 当前活跃连接数
)

这些公共指标定义和网络通用的上报函数放在在 network 包的 metrics.go 中,特有的采集方法放在 network/tcp 包中,比如 TCP 才有的错误。

在 network/metrics.go 中,使用 atomic.Int64 在网络热路径中完成无分配的本地累计,再由 OpenTelemetry callback 通过 ObserveInt64 周期读取累计值。避免每个网络消息同步进入 OTel 的属性解析与聚合流程。如果没有这么做,我通过 pprof 测试下来,吞吐量会下降 8%~9%,测试参数在下面的性能测试模块

cluster 的 id(实例ID) 和 name(实例名词)目前是在 otel 中独立去设置,没有去读取 cluster 的相关配置,这是为了解耦,默认为空,它们是用于构建 resource,非必填。

性能测试

使用了现有的 benchmark 代码作为客户端,在 gate 引入指标组件,CPU pprof 文件,结果如下。

	samples := []struct {
		c    int // 并发数
		n    int // 请求数
		size int // 数据包大小
	}{
		{
			c:    100,
			n:    1000000,
			size: 1024,
		},
		{
			c:    500,
			n:    1000000,
			size: 1024,
		},
		{
			c:    1000,
			n:    1000000,
			size: 1024,
		},
		{
			c:    1000,
			n:    1000000,
			size: 2 * 1024,
		},
	}
image

指标组件打开后,对网关吞吐的影响已经接近可以忽略,只占约 0.13% CPU。

其他

  • 无阻塞风险
  • network 包中 init() 存在 PANIC
  • golang.org/x/sys 包因为 otel 包的依赖,从 v0.28.0 升级为了 0.47.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant