2024年8月

前言

Opentelemetry

分布式链路跟踪(
Distributed Tracing
)的概念最早是由 Google 提出来的,发展至今技术已经比较成熟,也是有一些协议标准可以参考。目前在
Tracing
技术这块比较有影响力的是两大开源技术框架:Netflix 公司开源的
OpenTracing
和 Google 开源的
OpenCensus
。两大框架都拥有比较高的开发者群体。为形成统一的技术标准,两大框架最终磨合成立了
OpenTelemetry
项目,简称
otel
。otel 有链路追踪和监控告警两大块,关于监控告警,可以查看另一篇文章:
Go 链路追踪入门 Opentelemetry

Prometheus

Prometheus
源自 SoundCloud,拥有一整套开源系统监控和警报工具包,是支持
OpenTelemetry
的系统之一,是
CNCF
的第二个项目。

Grafana

Grafana 是一个开源的分析和可视化平台,它允许你查询、可视化和警报来自各种数据源的数据。它提供了一个用户友好的界面,用于创建和共享仪表板、图表和警报。Grafana 支持广泛的数据源,其中就包括
Prometheus

基础概念

这里为了简单入门,尽量简单的介绍一些抽象概念,结合着代码理解,如果不能理解也没关系,代码写着写着自然就明白了:

Meter Provider
用于接口化管理全局的
Meter
创建,相当于全局的监控指标管理工厂。

Meter
用于接口化创建并管理全局的
Instrument
,不同的
Meter
可以看做是不同的程序组件。

Instrument
用于管理不同组件下的各个不同类型的指标,例如
http.server.request.total

Measurements
对应指标上报的具体的
DataPoint
指标数据,是一系列的数值项。

Metric Reader
用于实现对指标的数据流读取,内部定义了具体操作指标的数据结构。
OpenTelemetry
官方社区提供了多种灵活的
Reader
实现,例如
PeridRader、ManualReader
等。

Metric Exporter
Exporter
用于暴露本地指标到对应的第三方厂商,例如:
Promtheus、Zipkin
等。

指标类型

OpenTelemetry metrics
有许多不同指标类型,可以把它想象成类似于
int, float
这种的变量类型:

Counter:
只增不减的指标,比如
http
请求总数,字节大小;

Asynchronous Counter:
异步 Counter;

UpDownCounter:
可增可减的指标,比如
http
活动连接数;

Asynchronous UpDownCounter:
异步 Counter;

Gauge:
可增可减的指标,瞬时计量的值,比如
CPU
使用,它是异步的;

Histogram
:分组聚合指标,这个较为难以理解一些,可以移步
此处
查看,当然,后文也会有一个详细的例子来使用它。

实战:采集指标

废话了一堆,终于可以实战了。我们先以
http
请求总数为例来走一遍整个采集指标流程。安装扩展:

go get github.com/prometheus/client_golang
go get go.opentelemetry.io/otel/exporters/prometheus
go get go.opentelemetry.io/otel/metric
go get go.opentelemetry.io/otel/sdk/metric

打开
main.go
,编写以下代码:

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"os"
	"os/signal"

	"github.com/prometheus/client_golang/prometheus/promhttp"
	"go.opentelemetry.io/otel/exporters/prometheus"
	api "go.opentelemetry.io/otel/metric"
	"go.opentelemetry.io/otel/sdk/metric"
)

const meterName = "oldme_prometheus_testing"

var (
	requestHelloCounter api.Int64Counter
)

func main() {
	ctx := context.Background()

	// 创建 prometheus 导出器
	exporter, err := prometheus.New()
	if err != nil {
		log.Fatal(err)
	}

	// 创建 meter
	provider := metric.NewMeterProvider(metric.WithReader(exporter))
	meter := provider.Meter(meterName)

	// 创建 counter 指标类型
	requestHelloCounter, err = meter.Int64Counter("requests_hello_total")
	if err != nil {
		log.Fatal(err)
	}

	go serveMetrics()

	ctx, _ = signal.NotifyContext(ctx, os.Interrupt)
	<-ctx.Done()
}

func serveMetrics() {
	log.Printf("serving metrics at localhost:2223/metrics")
	http.Handle("/metrics", promhttp.Handler())

	http.Handle("/index", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// 记录 counter 指标
		requestHelloCounter.Add(r.Context(), 1)

		_, _ = w.Write([]byte("Hello, Otel!"))
	}))

	err := http.ListenAndServe(":2223", nil) //nolint:gosec // Ignoring G114: Use of net/http serve function that has no support for setting timeouts.
	if err != nil {
		fmt.Printf("error serving http: %v", err)
		return
	}
}

在我们的代码中,我们定义一个名字为
requests_hello_total

Int64Counter
指标类型,
Int64Counter
代表这是一个只增不减的
int64
数值,用作记录请求总数正好合适。运行我们的程序,如果不出错的话,访问
http://localhost:2223/index
可以看到
Hello, Otel!
。并且我们访问
http://localhost:2223/metrics
可以看到指标数据:

这里数据还没有进行可视化,我们先把流程走通,多访问几次
http://localhost:2223/index
可以看到
requests_hello_total
会增加:

Histogram

接下来我们采集一下
Histogram
指标,统计在
0.1, 0.2, 0.5, 1, 2, 5
秒以内的
http
请求数,在
main.go
中加上相关代码,可以直接复制过去:

package main

import (
	"context"
	"fmt"
	"log"
	"math/rand"
	"net/http"
	"os"
	"os/signal"
	"time"

	"github.com/prometheus/client_golang/prometheus/promhttp"
	"go.opentelemetry.io/otel/exporters/prometheus"
	api "go.opentelemetry.io/otel/metric"
	"go.opentelemetry.io/otel/sdk/metric"
)

const meterName = "oldme_prometheus_testing"

var (
	requestHelloCounter      api.Int64Counter
	requestDurationHistogram api.Float64Histogram
)

func main() {
	ctx := context.Background()

	// 创建 prometheus 导出器
	exporter, err := prometheus.New()
	if err != nil {
		log.Fatal(err)
	}

	// 创建 meter
	provider := metric.NewMeterProvider(metric.WithReader(exporter))
	meter := provider.Meter(meterName)

	// 创建 counter 指标类型
	requestHelloCounter, err = meter.Int64Counter("requests_hello_total")
	if err != nil {
		log.Fatal(err)
	}

	// 创建 Histogram 指标类型
	requestDurationHistogram, err = meter.Float64Histogram(
		"request_hello_duration_seconds",
		api.WithDescription("记录 Hello 请求的耗时统计"),
		api.WithExplicitBucketBoundaries(0.1, 0.2, 0.5, 1, 2, 5),
	)
	if err != nil {
		log.Fatal(err)
	}

	go serveMetrics()
	go goroutineMock()

	ctx, _ = signal.NotifyContext(ctx, os.Interrupt)
	<-ctx.Done()
}

func serveMetrics() {
	log.Printf("serving metrics at localhost:2223/metrics")
	http.Handle("/metrics", promhttp.Handler())

	http.Handle("/index", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// 记录 counter 指标
		requestHelloCounter.Add(r.Context(), 1)

		// 计算请求处理时间
		startTime := time.Now()
		// 模拟请求处理时间
		time.Sleep(time.Duration(rand.Intn(3)) * time.Second)
		defer func() {
			duration := time.Since(startTime).Seconds()
			requestDurationHistogram.Record(r.Context(), duration)
		}()

		_, _ = w.Write([]byte("Hello, Otel!"))
	}))

	err := http.ListenAndServe(":2223", nil) //nolint:gosec // Ignoring G114: Use of net/http serve function that has no support for setting timeouts.
	if err != nil {
		fmt.Printf("error serving http: %v", err)
		return
	}
}

// 随机模拟若干个协程
func goroutineMock() {
	for {
		go func() {
			// 等待若干秒
			var s = time.Duration(rand.Intn(10))
			time.Sleep(s * time.Second)
		}()
		time.Sleep(1 * time.Millisecond)
	}
}

走到这里,代码层面结束了,已经成功一半了,代码开源在
Github
。之后我们就可以安装
Prometheus
服务端和
Grafana
来进行数据可视化。

安装 Prometheus

Prometheus
有多种安装方式,我这里依旧采用
Docker
安装,当然,你也可以使用其他方式安装,具体安装方式可以参考其他文章,后续
Grafana
同理,不在赘述,在
Prometheus.yml
中填写
targets
我们的地址:

scrape_configs:
  - job_name: "prometheus"

    static_configs:
      - targets: ["localhost:2223"]

Prometheus
会自动去
{{target}}/metrics
中拉取我们的指标。之后在浏览器打开
Promethues
的地址,例如我的是:http://localhost:9090,如果全部正常的话可以在
status:targets
中看见我们的指标:


Promethues
的首页查询
requests_hello_total
指标可以看到可视化的图表:

安装 Grafana

我的
Grafana
安装好了,登录进去后是这样的(我更改后默认颜色):


Data source
中添加
Prometheus
服务器,然后在
Dashboard
中添加我们想要监控的指标,即可看到更美观的图表:


为了加速
DETR
收敛,论文提出了简单而有效的
Spatially Modulated Co-Attention

SMCA
)机制,通过在初始边界框位置给予较高的协同注意力响应值的约束来构建
DETR
的回归感知协同注意力。此外,将
SMCA
扩展为多头注意力和尺度选择注意力后,对比
DETR
可以实现更好的性能(
108
周期
45.6 mAP
vs
500
周期
43.3 mAP

来源:晓飞的算法工程笔记 公众号

论文: Fast Convergence of DETR with Spatially Modulated Co-Attention

Introduction


最近提出的
DETR
通过消除手工设置的锚框和非极大值抑制(
NMS
),显著简化了目标检测流程。但与两阶段或一阶段检测器相比,
DETR
的收敛速度较慢(500 vs 40 周期),导致算法设计周期变长。研究人员难以进一步扩展该算法,从而阻碍了其广泛使用。


DETR
中,对象查询向量负责检测不同空间位置的对象。每个对象查询都与卷积神经网络 (
CNN
) 编码的空间视觉特征交互,通过协同注意力机制自适应地从空间位置收集信息,估计边界框位置和对象类别。但在
DETR
的解码器中,每个对象查询的协同关注视觉区域可能与查询要预测的边界框无关。因此,
DETR
的解码器需要长时间的训练来搜索适当的协同关注视觉区域,以准确识别相应的对象。

受这一观察的启发,论文提出了一种名为空间调制协同注意(
SMCA
)的新模块。取代
DETR
中现有的协同注意力机制,实现更快的收敛和更高的性能。

SMCA
动态预测每个对象查询对应的框的初始中心和尺寸,生成
2D
空间的类高斯权重图。权重图逐元素与对象查询和图像特征产生的协同注意力特征图相乘,可以有效地聚合来自视觉特征图的查询对象的相关信息。通过这种方式,空间权重图有效地调节每个对象查询的协同注意力的搜索范围,使其适当地围绕最初预测对象的中心和尺寸。通过利用预测的高斯分布空间先验,
SMCA
可以显著加快
DETR
的训练速度。

虽然将
SMCA
机制简单地融入
DETR
中可以加速收敛速度,但与
DETR
相比性能较差(50 个周期时为
41.0 mAP
,108 个周期时为
42.7 mAP
vs. 500个周期时为
43.3 mAP
)。受到多头注意力和多尺度特征的启发,论文将其与
SMCA
集成来进一步增强:

  • 对于编码器中的多尺度视觉特征,不是简单地对
    CNN
    主干的多尺度特征进行缩放以形成联合多尺度特征图,而是引入了尺度内和多尺度混合的自注意力机制来进行多尺度视觉特征之间的信息传播。
  • 在解码器中,每个对象查询可以通过尺度选择注意力自适应地选择适当尺度的编码特征。对于解码器中的多个协同注意头,都会估计特定的对象中心和尺度,生成不同的空间权重图用于调整协同注意力特征。

论文的贡献如下:

  • 提出了一种新颖的空间调制协同注意力(
    SMCA
    ),通过位置约束的目标回归来加速
    DETR
    的收敛。没有多尺度特征和多头注意力的基础版
    SMCA
    已经可以在 50 个周期时达到
    41.0 mAP
    ,在 108 个周期时达到
    42.7 mAP
  • 完整版
    SMCA
    进一步集成了多尺度特征和多头空间调制,可以通过更少的训练迭代进一步显着改进和超越
    DETR

    SMCA
    在 50 个周期时可以达到
    43.7 mAP
    ,在 108 个周期时可以达到
    45.6 mAP

  • COCO 2017
    数据集进行了广泛的消融研究,以验证
    SMCA
    模块和网络设计。

Spatially Modulated Co-Attention


A Revisit of DETR

DETR
将目标检测转化为集合预测问题。对
DETR
不熟悉的,可以看看之前的文章,【
DETR:Facebook提出基于Transformer的目标检测新范式 | ECCV 2020 Oral
】。

DETR
先用卷积神经网络 (
CNN
) 从图像
\(I\in\mathbb{R}^{3\times H_{0}\times W_{0}}\)
中提取视觉特征图
\(f\in\mathbb{R}^{C\times H\times W}\)
,其中
\(H,W\)

\(H_{0},W_{0}\)
分别是输入图像和特征图的高度和宽度。

经过位置嵌入增强的视觉特征
\(f_{pe}\)
将被输入到__
Transformer
__的编码器中,对
\(f_{p e}\)
生成的键、查询和值特征进行自注意力计算,在所有空间位置的特征之间交换信息。为了增加特征多样性,这些特征将沿着通道维度分为多个组进行多头自注意力:

\[
\begin{array}{l c r}{{E_{i}=\mathrm{Softmax}(K_{i}^{T}Q_{i}/\sqrt{d})V_{i},}}\ {{E=\mathrm{Concat}(E_{1},\cdot\cdot\cdot\cdot\cdot E_{H}),}}\end{array}

\quad\quad (1)

\]

其中
\(K_{i}\)
,
\(Q_{i}\)
,
\(V\_{i}\)
表示键、查询和值特征的第
\(i\)
个特征组。每种类型的特征都有
\(H\)
组,输出的编码器特征
\(E\)
经过进一步变换后输入到
Transformer
的解码器中。

对于编码器编码的视觉特征
\(E\)

DETR
在对象查询
\(O\_q\in\mathbb{R}^{N\times C}\)

\(E\in\mathbb{R}^{L\times C}\)
之间执行协同注意力(
\(N\)
表示预先指定的对象查询的数量,
\(L\)
是空间视觉特征的数量):

\[
\begin{array}{l}

{{Q=\mathrm{FC}(O\_{q}),\ K,V=\mathrm{FC}(E)}}

\

{{C\_{i}=\mathrm{Softmax}(K\_i^T Q\_i/\sqrt{d})V\_i,}}

\

{{C_{i}=\mathrm{Concat}(C_{1},\ldots\cdot C\_{H}),}}

\end{array}

\quad\quad (2)

\]

其中
\(\mathrm{FC}\)
表示单层线性变换,
\(C\_i\)
表示第
\(i\)
个协同注意力头关于对象查询
\(O\_q\)
的协同特征。解码器的每个对象查询的输出特征由多层感知器 (
MLP
) 处理,输出每个对象的类分数和框位置。

给定预测的框和类别,在预测结果和
GT
之间应用匈牙利算法来识别每个对象查询的学习目标。

Spatially Modulated Co-Attention

DETR
中的原始协同注意力不知道预测的边界框,因此需要多次迭代才能为每个对象查询生成正确的注意力图。论文提出的
SMCA
的核心思想是,将可学习的协同注意力图与手工设计的查询空间先验相结合,使得关注的特征限制在对象查询的初始输出位置周围,从而与最终的对象预测更加相关。

  • Dynamic spatial weight maps

每个对象查询先动态预测其负责对象的中心和比例,用于生成类似
2D
高斯的空间权重图。对象查询
\(O\_q\)
的类高斯分布归一化中心
\(c\_h^{norm}\)

\(c\_w^{norm}\)
和尺度
\(s\_h\)

\(s\_w\)
的计算为:

\[
\begin{array}{c}{{c_{h}^{\mathrm{norm}},c_{w}^{\mathrm{norm}}={\mathrm{sigmoid}}(\mathrm{MLP}(O_{q})),}}\ {{s_{h},s_{w}=\mathrm{FC}(O_{q}),}}\end{array}

\quad\quad (3)

\]

由于物体有不同的尺度和高宽比,预测与宽度和高度无关的尺度
\(s_{h}\)

\(s_{w}\)
可以更好地处理现实场景中的复杂物体。对于大或小物体,
SMCA
动态生成不同的
\(s_{h}\)

\(s_{w}\)
值,以便通过空间权重图
\(G\)
来调整协同注意力图,从而聚合来自大物体所有部分的足够信息或抑制小物体的背景杂乱。

在得到上述对象信息后,
SMCA
生成类高斯权重图:

\[
G(i,j)=\mathrm{exp}(-\frac{(i-c\_w)^2}{\beta s^2\_w} - \frac{(j-c\_h)^2}{\beta s^2\_h})

\quad\quad (4)

\]

其中
\((i,j)\in0,W\;\times0,H\)
是权重图
\(G\)
的空间索引,
\(\beta\)
是用于调整类高斯分布
bandwidth
的超参数。一般来说,权重图
\(G\)
对靠近中心的空间位置分配高重要性,对远离中心的位置分配低重要性。
\(\beta\)
可以通过手动调整来确保
\(G\)
在训练开始时覆盖较大的空间范围,使得网络能够接收到更多信息的梯度。

  • Spatially-modulated co-attention

给定动态生成的空间先验
\(G\)
,用于调整对象查询
\(O\_Q\)
和自注意力编码特征
\(E\)
之间的每个协同注意图
\(C\_i\)
(在基础版的
SMCA
中,
\(G\)
是所有协同注意力头共享的):

\[
C_{i}=\mathrm{softmax}\bigl(K_{i}^{T}Q_{i}/\sqrt{d}+\mathrm{log}G\bigr)V_{i}

\quad\quad (5)

\]

SMCA
在空间图
\(G\)
的对数和协同注意力点积
\(K_{h}^{T}Q_{h}/\sqrt{d}\)
之间执行元素加法,然后对所有空间位置进行
softmax
归一化。这样,解码器的协同注意力在预测的边界框位置周围的权重更大,可以限制协同注意力的搜索空间,从而提高收敛速度。类高斯权重图如图 2 所示,限制协同注意力更多地关注预测边界框位置附近的区域,从而显着提高
DETR
的收敛速度。在
SMCA
基本版中,多个注意力头的协同注意力图
\(C\_{i}\)
共享相同的类高斯权重图
\(G\)

  • SMCA with multi-head modulation

论文还研究了针对不同的协同注意头使用不同的类高斯权重图调整协同注意特征。每个头从共享的中心
\(c_{w}, c_{h}\)
开始,预测特定的相对于中心的偏移
\(\Delta c_{w,i},\Delta c_{h,i}\)
和特定的尺度
\(s_{w,i},s_{h,i}\)
。基于上述信息生成特定的类高斯空间权重图
\(G\_{i}\)
,随后得到协同注意特征图
\(C\_1\cdots C\_H\)

\[
C_{i}=\mathrm{softmax}(K_{i}^{T}Q_{i}/\sqrt{d}+\log G_{i})V\_{i}\quad\mathrm{for}\,i=1,\cdot\cdot,H.

\quad\quad (6)

\]

多个空间权重图可以强调不同的上下文并提高检测精度。

  • SMCA with multi-scale visual features

特征金字塔在目标检测框架中很流行,对比单尺度特征编码能带来显着的性能提升。因此,论文还将多尺度特征集成到
SMCA
中,通过在
Transformer
的编码器中用多尺度特征编码替换单尺度特征编码来进一步改进框架。

给定图像,从
CNN
主干网络提取采样率为
16

32

64
多尺度视觉特征
\(f_{16}\)

\(f_{32}\)

\(f\_{64}\)
,编码器中的自注意力机制在不同尺度的所有特征像素之间传播和聚合信息。但由于多尺度特征的像素数量都非常大,自注意力操作的计算成本很高。为了解决这个问题,论文引入了尺度内自注意力编码,仅在每个尺度内的特征像素之间计算自注意力,而
Transformer
块的权重(自注意力和前馈子网络)在不同尺度上共享。论文的实证研究表明,跨尺度的参数共享增强了尺度内自注意力编码的泛化能力。

对于
SMCA
编码器的最终设计,采用
2
个尺度内自注意力编码块,然后是
1
个多尺度自注意力块,再接另外
2
个尺度内自注意力块。该设计具有与
5
个多尺度自注意力编码块非常相似的检测性能,但计算量要小得多。

给定编码器输出的多尺度编码特征
\(E_{16}\)

\(E_{32}\)

\(E_{64}\)
,解码器执行协同注意力的简单解决方案是首先重新缩放并连接多尺度特征以形成单尺度特征图,然后在对象查询和生成的特征图之间计算协同注意力。然而,论文注意到一些对象查询可能只需要特定尺度的信息,并不总是需要所有尺度的信息。例如,低分辨率特征图
\(E_{64}\)
中缺少小对象的信息,负责小物体的对象查询应该更有效地仅从高分辨率特征图中获取信息。与将每个边界框显式分配给特定尺度的特征图的传统方法(例如
FPN
)不同,论文使用可学习的尺度注意力机制自动地为每个框选择尺度:

\[
\alpha_{16},\alpha_{32},\alpha_{64}=\mathrm{Sortmax}(\mathrm{FC}(O_{q})),

\quad\quad (7)

\]

其中
\(\alpha_{16}\)

\(\alpha_{32}\)

\(\alpha\_{64}\)
代表选择对应特征的重要程度。

为了在对象查询
\(O_{q}\)
和多尺度视觉特征
\(E_{16}\)

\(E_{32}\)

\(E_{64}\)
之间计算协同注意力,首先通过不同的线性变化从对应的编码特征中获得注意力头
\(i\)
的多尺度键
\(K_{i,16},K_{i,32},K_{i,64}\)
和值特征
\(V_{i,16},V_{i,32},V_{i,64}\)
,随后执行添加尺度选择权重和多尺度的
SMCA
计算:

\[
\begin{array}{l}

{{C_{i,j}=\mathrm{Softmax(K_{i,j}^T\ Q_i/\sqrt{d}+log G\_i)V_{i,j}\odot \alpha\_{j}}}}

\quad\quad\ (8)

\

{{C_{i,j}=\underset{\mathrm{all}\ j}{\sum}\mathrm{C}_{i,j},\quad\mathrm{for} j\in{16,32,64},}}

\quad\quad\quad\quad\quad (9)

\end{array}

\]

其中
\(C\_{i,j}\)
代表查询和第
\(j\)
个尺度视觉特征在第
\(i\)
个协同注意力头中的协同注意力特征。通过这种尺度选择注意力机制,与每个对象查询最相关的尺度被平滑地选择,同时抑制其它尺度的视觉特征。

加上尺度内注意力和尺度选择注意力机制后,完整的
SMCA
可以比基础版更好地处理目标检测。

  • SMCA box prediction

在对象查询和编码图像特征之间进行协同注意力后,得到对象查询
\(O\_q\)
的更新特征
\(D\in\mathbb{R}^{N\times C}\)
。在原始
DETR
中,分别使用 3 层
MLP
和 1 个线性层来预测边界框和分类置信度:

\[
\begin{array}{l}

{{\mathrm{Box}=\mathrm{Sigmoid}(\mathrm{MLP}(D))}}

\quad\quad (10)

\

{{\mathrm{Score}=\mathrm{FC}(D)}}

\quad\quad\quad\quad\quad\quad\ \ (11)

\end{array}

\]

其中
\(\mathrm{Box}\)
代表预测框在归一化坐标系中的中心、高度、宽度,
\({\mathrm{Score}}\)
代表分类预测。而在
SMCA
中,由于协同注意力被限制在初始预测的对象中心
\(c_{h}^{\mathrm{norm}},c_{w}^{\mathrm{norm}}\)
周围,将初始中心作为约束边界框预测的先验进行校正预测:

\[
\begin{array}{c}

{{\widehat{\mathrm{Box}}=\mathrm{MLP}(D)}}

\

{{\widehat{\mathrm{Box}}:2=\widehat{\mathrm{Box}}:2 + c\_h^\widehat{norm},c\_w^\widehat{norm}}}

\

{{\mathrm{Box}=\mathrm{Sigmoid(\widehat{Box})}}}

\end{array}

\quad\quad (12)

\]


Sigmoid
之前进行中心坐标的相加,能确保边界框预测与
SMCA
中突出的协同注意区域高度相关。

Experiments


表 1 展示了
SMCA
与其它
DETR
进行对比。

表 3 和表 4 展示了论文提出的空间调制协同注意力、多头调整注意力和多尺度特征之间的对比实验。

图 3 展示了
SMCA
的特征可视化。

表 5 与SOTA模型进行对比。

如果本文对你有帮助,麻烦点个赞或在看呗~undefined更多内容请关注 微信公众号【晓飞的算法工程笔记】

work-life balance.

前言

Opentelemetry

分布式链路跟踪(
Distributed Tracing
)的概念最早是由 Google 提出来的,发展至今技术已经比较成熟,也是有一些协议标准可以参考。目前在
Tracing
技术这块比较有影响力的是两大开源技术框架:Netflix 公司开源的
OpenTracing
和 Google 开源的
OpenCensus
。两大框架都拥有比较高的开发者群体。为形成统一的技术标准,两大框架最终磨合成立了
OpenTelemetry
项目,简称
otel
。otel 有链路追踪和监控告警两大块,关于监控告警,可以查看另一篇文章:
Go 监控告警入门 Opentelemetry

Jaeger

Jaeger\ˈyā-gər\
是 Uber 开源的分布式追踪系统,是支持
OpenTelemetry
的系统之一,也是
CNCF
项目。

安装 Jaeger

Jaeger 为我们准备了 Docker 镜像,我们可以很容易的安装。

docker run --rm --name jaeger \
  -p 16686:16686 \
  -p 4317:4317 \
  -p 4318:4318

截至到 2024-04-18, Jaeger 的最新版本是 1.56。

简单的介绍以下这三个端口。16686 用做 Jaeger 服务的 Web 面板,一会我们可以在浏览器中访问它;4317 和 4318 都用做上传追踪数据,不同之处在于前者是 gRPC 协议,后者是 HTTP 协议。

Jaeger 还有很多可用的端口,本篇只介绍和 otel 相关的,具体可以查看
Jaeger
官方文档哦。

安装后,在浏览器中输入 IP:16686:

看到 gopher 侦探在追踪足迹的可爱图片就代表 Jaeger 安装成功咯。

编写 Go 代码

安装依赖:

go get "go.opentelemetry.io/otel" \
  "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" \
  "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" \
  "go.opentelemetry.io/otel/propagation" \
  "go.opentelemetry.io/otel/sdk/metric" \
  "go.opentelemetry.io/otel/sdk/resource" \
  "go.opentelemetry.io/otel/sdk/trace" \
  "go.opentelemetry.io/otel/semconv/v1.24.0" \
  "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"

我这里贴出 HTTP 和 gRPC 的全部代码,直接复制过去,改成自己的地址即可:

HTTP

func TestTraceHttp(t *testing.T) {
	ctx := context.Background()

	// 创建 OTLP HTTP 导出器,连接到 Jaeger
	exporter, err := otlptracehttp.New(ctx,
		otlptracehttp.WithEndpointURL("http://srv.com:4318/v1/traces"))

	if err != nil {
		log.Fatalf("创建导出器失败: %v", err)
	}

	// 创建资源
	res, err := resource.New(ctx,
		resource.WithAttributes(
			semconv.ServiceNameKey.String("otel-traces-demo-http"),
		),
	)
	if err != nil {
		log.Fatalf("创建资源失败: %v", err)
	}

	// 创建 Tracer 提供器
	tp := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exporter),
		sdktrace.WithResource(res),
	)

	// 设置全局 Tracer 提供器
	otel.SetTracerProvider(tp)

	// 创建一个新的 trace
	tracer := otel.Tracer("example-tracer")
	ctx, span := tracer.Start(ctx, "root-span")
	// 暂停 100ms
	time.Sleep(100 * time.Millisecond)
	// 结束 span
	span.End()

	// 创建子span
	_, childSpan := tracer.Start(ctx, "child-span")
	// 暂停 50ms
	time.Sleep(50 * time.Millisecond)
	childSpan.End()

	// 确保所有的 spans 都被发送
	if err := tp.Shutdown(ctx); err != nil {
		log.Fatalf("关闭 Tracer 提供器失败: %v", err)
	}
}

gRPC

func TestTraceGrpc(t *testing.T) {
	ctx := context.Background()

	// 创建 OTLP gRPC 导出器,连接到 Jaeger
	exporter, err := otlptracegrpc.New(ctx,
		otlptracegrpc.WithEndpoint("srv.com:4317"),
		otlptracegrpc.WithInsecure(),
	)

	if err != nil {
		log.Fatalf("创建导出器失败: %v", err)
	}

	// 创建资源
	res, err := resource.New(ctx,
		resource.WithAttributes(
			semconv.ServiceNameKey.String("otel-traces-demo-grpc"),
		),
	)
	if err != nil {
		log.Fatalf("创建资源失败: %v", err)
	}

	// 创建 Tracer 提供器
	tp := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exporter),
		sdktrace.WithResource(res),
	)

	// 设置全局 Tracer 提供器
	otel.SetTracerProvider(tp)

	// 创建一个新的 trace
	tracer := otel.Tracer("example-tracer")
	ctx, span := tracer.Start(ctx, "root-span")
	// 暂停 100ms
	time.Sleep(100 * time.Millisecond)
	// 结束 span
	span.End()

	// 创建子span
	_, childSpan := tracer.Start(ctx, "child-span")
	// 暂停 50ms
	time.Sleep(50 * time.Millisecond)
	childSpan.End()

	// 确保所有的 spans 都被发送
	if err := tp.Shutdown(ctx); err != nil {
		log.Fatalf("关闭 Tracer 提供器失败: %v", err)
	}
}

效果

执行后,在面板中即可看到我们上传的数据。

可以看到我们的两个 span 已经上传到
Jaeger
中了,就是如此的简单!文中的代码开源在
Github

一、加载问题

用高德地图做了个进京证路线规划的功能,官网也是有 React
代码示例
。但是吧,这个Demo有问题,地图是能加载成功,但是其他功能再用 map 这个变量肯定不行,果不其然是null,处理也简单,把公共变量都管理起来就行了。

const [map, setMap] = useState(null);
const [AMap, setAMap] = useState(null);
const [driving, setDriving] = useState(null);
const [mass, setMass] = useState(true);

useEffect(() => {
    window._AMapSecurityConfig = {
        securityJsCode: "「你申请的安全密钥」",
    };
    AMapLoader.reset();
    AMapLoader.load({
        key: "", // 申请好的Web端开发者Key,首次调用 load 时必填
        version: "2.0", // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
        plugins: ["AMap.Driving"], //需要使用的的插件列表,如比例尺'AMap.Scale',支持添加多个如:['...','...']
    }).then((_AMap) => {
        const _map = new _AMap.Map("container", {
            resizeEnable: true,
            viewMode: '2D', // 默认使用 2D 模式,如果希望使用带有俯仰角的 3D 模式,请设置 viewMode: '3D'
            zoom: 11, // 初始化地图层级
            center: [116.397428, 39.93000] // 初始化地图中心点
        });
        _map.on('complete', () => {
            setAMap(_AMap)
        });

        setMap(_map);

        const driving = new _AMap.Driving({
            map: _map
        });
        setDriving(driving);
		
    }).catch((e) => {
        console.log(e);
    });
    return () => {
        map?.destroy();
    };
}, [])

二、标注点问题

普通点标记多了会很慢,高德提供了海量点标记功能(摄像头太多了),如果文字都显示是又慢又乱,所有单独绑定单击事件,并用 Text 文本标记。

const camera = []; //你的数组
const datas = [];
const styles = [{
    url: 'https://a.amap.com/jsapi_demos/static/demo-center/icons/poi-marker-default.png',
    anchor: 'bottom-center',
    size: new AMap.Size(21, 27),
    zIndex: 1,
}]
for (let index = 0; index < camera.length; index++) {
    const c = camera[index];
    datas.push({
        lnglat: c.position,
        name: c.name,
        style: 0 //该数据的取值为样式数组对应的对象索引
    });
}
// 海量标记摄像头
const _mass = new AMap.MassMarks(datas, {
    style: styles
})
let marker = null;
_mass.on('click', (e) => {
    if (marker === null || e.data.lnglat !== marker._opts.position) {
        if (marker !== null) {
            map.remove(marker)
        }
        marker = new AMap.Text({
			map: map,
			position: e.data.lnglat,
			anchor: 'top-center',
			offset: [0, -60],
			text: e.data.name,
			style: {
				"font-Size": "14px",
				"padding": "5px"
			},
			zIndex: 2
		});
    }
});
_mass.setMap(map)
setMass(_mass);

三、效率问题

目前规划路线的效率有点慢,主要是摄像头过多,按步全量循环算路太耗时,下一步更新要把所有的摄像头分区,按线路走向过滤算路,理论上能减少一半以上的计算时间,期待的搓搓小手。大家感兴趣的可以在微信公众号体验一下,希望可以帮助到有需要的人。

Metabase 是一个开源的商业智能(BI)工具,帮助用户轻松地从数据库中提取数据,并将其转化为易于理解的图表和仪表板。与传统的 BI 工具相比,Metabase 不需要用户具备编写 SQL 的能力,非技术人员也能通过简单的操作创建有用的报告和可视化数据分析。

Metabase主要特性

  • 部署便捷
    :比传统的 BI 应用安装要简单很多,Metabase 只有主程序+数据库两个服务,只需五分钟内即可完成部署。

  • 简洁的用户界面
    :Metabase 的用户界面设计直观,适合技术和非技术人员使用。用户可以通过简单的点击和拖放操作快速创建查询和可视化。

  • 无需编写 SQL 的查询构建器
    :Metabase 提供了一个图形化的查询构建器,无需编写 SQL 代码即可构建复杂的查询。只需选择数据源和表格,设定过滤条件、分组和排序,系统会自动生成相应的 SQL 查询。

  • 丰富的可视化选项
    :Metabase 提供了多种可视化图表类型,包括柱状图、折线图、饼图、面积图、散点图、地图等。可以根据数据特点选择合适的图表类型,帮助团队直观地分析数据。

  • 动态仪表板
    :Metabase 支持创建动态仪表板,可以将多个查询结果整合到一个页面上,并通过动态过滤器实时调整数据展示。仪表板可以轻松分享给团队成员,促进协作。

  • 定时报告和警报
    :Metabase 支持定时报告功能,允许设置自动发送查询结果的频率和接收人。还可以设置警报,当数据达到预设的条件时,系统会自动发送通知。

  • 多种数据源支持
    :Metabase 支持连接多种数据源,包括 MySQL、PostgreSQL、MongoDB、SQL Server、Google BigQuery 等。

  • 权限管理
    :Metabase 提供了细粒度的权限管理,管理员可以控制用户对数据源、表格、查询和仪表板的访问权限。这确保了敏感数据的安全性,同时允许不同团队根据需要访问数据。

  • API 集成
    :Metabase 提供了 REST API,用户可以通过编程接口自动化查询、导出数据、管理仪表板和执行其他操作,可以与其他系统无缝集成,成为企业数据工作流的一部分。

Metabase使用场景

个人使用场景

对于个人用户,Metabase 是一个非常方便的工具,可以帮助我们分析和可视化个人项目或小型业务的数据。

  • 个人项目数据分析
    :个人开发者或数据爱好者可以使用 Metabase 来分析他们的个人项目数据,如网站流量、个人博客的访问量、应用程序的用户行为等。

团队使用场景

对于团队而言,Metabase 提供了一个协作平台,帮助团队成员共同分析和分享数据。

  • 产品开发团队的数据监控
    :产品经理和开发团队可以使用 Metabase 监控产品的关键指标,如用户活跃度、功能使用率和错误报告等。这些数据可以帮助团队做出数据驱动的产品改进决策。
  • 营销团队的活动分析
    :营销团队可以使用 Metabase 分析广告活动的效果、网站流量来源、转化率等数据,并通过可视化图表向团队成员或管理层展示活动成果。
  • 运营团队的业务监控
    :运营团队可以使用 Metabase 实时监控业务的关键运营指标,如库存水平、订单处理速度、客户服务响应时间等,并设置警报以在指标异常时及时采取行动。

业务使用场景

在业务层面,Metabase 可以作为企业级数据分析和商业智能工具,支持更复杂和广泛的业务需求。

  • 业务分析
    :大型企业可以使用 Metabase 集成来自不同部门的数据源,如销售、市场、财务和运营,进行统一的数据分析和报告。
  • 客户数据洞察
    :企业可以使用 Metabase 深入分析客户行为和需求,通过分析购买模式、客户满意度调查结果等数据,定制个性化服务或产品。

Metabase 部署

Metabase 部署非常简单,支持在任何有 Java 的环境中使用 Java 运行,同时也提供了 Docker 镜像。更多请参阅
Metabase 部署文档

Docker 部署

Metabase 提供了使用 Docker 镜像的部署方式,但 Metabase 镜像存放在 Docker Hub 上了,目前从国内无法访问 Docker Hub 镜像,因此 Rainbond 提供了镜像加速的方式,部署方式如下:

docker run -d -p 3000:3000 --name metabase docker.rainbond.cc/metabase/metabase:latest

Rainbond 部署

对于不熟悉 Kubernetes 的伙伴,又想在 Kubernetes 中安装 Metabase,可以使用 Rainbond 来部署。Rainbond 是一个无需了解 Kubernetes 的云原生应用管理平台,可以很简单的将你的应用部署到 Kubernetes中。

首先需要在你的服务器上部署 Rainbond,只需一条命令即可完成部署,更多部署方式请参阅
Rainbond 部署文档

curl -o install.sh https://get.rainbond.com && bash ./install.sh

部署完成后,进入 Rainbond 平台,选择通过应用市场部署,在开源应用商店中搜索
Metabase
并进行一键安装。

部署完成后拓扑图如下,Rainbond 的 Metabase 应用模板提供了生产可用的 Metabase,包含 Metabase 和 PG 数据库两个服务。

这时点击访问按钮即可访问到 Metabase Web页面,开启你的数据分析之旅~

最后

Metabase 是一个强大的商业智能工具,它的简洁性、易用性以及多功能性使得它适用于个人、团队以及公司级的各种数据分析需求。无论你是需要监控个人项目的数据、与团队协作进行产品分析,还是在企业层面上整合多部门数据,Metabase 都能够提供灵活且高效的解决方案。

在部署方面,无论你选择在本地使用 Docker 进行部署,还是通过 Rainbond 一键安装,Metabase 都能快速上线并投入使用,让你可以专注于数据分析本身,而无需担心复杂的配置问题。