使用 Prometheus 监控 Golang 应用程序

语言: CN / TW / HK

C T O

   

 

Go Rust Python Istio containerd CoreDNS Envoy etcd Fluentd Harbor Helm Jaeger Kubernetes Open Policy Agent Prometheus Rook TiKV TUF Vitess Arg o Buildpacks CloudEvents CNI Contour Cortex CRI-O Falco Flux gRPC KubeEdge Linkerd NATS Notary OpenTracing Operator Framework SPIFFE SPIRE     Thanos

使用 Prometheus 监控 Golang 应用程序

为了确保我们的应用程序的质量,需要执行某种质量监控和质量检查。这些质量检查通常会比较从应用程序捕获的给定指标,例如吞吐量或错误率,以及一些定义的值,例如错误率 < 0.1%

Prometheus 是一个开源监控和警报工具,可帮助我们以简单可靠的方式从应用程序中收集和公开这些指标。

在本文中,您将了解 Prometheus 的基础知识,包括什么是指标、不同类型的指标以及何时使用它们。之后,您将公开 Golang 应用程序的指标并使用 Grafana 将它们可视化。

指标和标签

简单地说,指标衡量一个特定的值,例如随着时间的推移应用程序的响应时间。一旦使用某种检测系统从应用程序中公开指标, Prometheus 就会将它们存储在时间序列数据库中,并使用查询使它们迅速可用。

# Total number of HTTP request
http_requests_total

# Response status of HTTP request
response_status

# Duration of HTTP requests in seconds
http_response_time_seconds

如果您对特定指标有多个服务,则可以添加标签以指定该指标来自哪个服务。例如,您可以将服务标签添加到 http_requests_total 指标以区分每个服务的请求。另一个有用的指标是不同响应状态的 URL

# Total number of HTTP request
http_requests_total{service="builder"}

# Response status of HTTP request
response_status{path="/"}
response_status{path="/articles"}

使用正确的标签来增强指标将使查询它们变得容易,尤其是当您有许多不同的服务时。

指标类型

Prometheus 提供了四种不同的指标类型,每种类型都有其优点和缺点,这使得它们适用于不同的用例。在本文的这一部分,我们将仔细研究这四个。

Counters:

Counters 是一种简单的度量类型,只能在重新启动时递增或重置为零。它通常用于计算原始数据,例如对服务的请求总数或完成的任务数。因此,大多数 Counters 都使用 _total 后缀命名,例如 http_requests_total

# Total number of HTTP request
http_requests_total

# Total number of completed jobs
jobs_completed_total

这些 Counters 的绝对值通常无关紧要,并且不会为您提供有关应用程序状态的太多信息。真实信息可以通过它们随时间的演变来收集,这可以使用 rate() 函数获得。

Gauges:

Gauges 也表示单个数值,但与 Counters 不同,该值可以上升也可以下降。因此,仪表通常用于测量值,例如温度、湿度或当前内存使用情况。

Counters 不同, Gauges 的当前值是有意义的,可以直接用于图表和测试。

Histograms:

Histograms 用于测量属于特定预定义存储桶的值观察的频率。这意味着他们将提供有关响应时间和信号异常值等指标分布的信息。

默认情况下, Prometheus 提供以下存储桶: .005, .01, .025, .05, .075, .1, .25, .5, .75, 1, 2.5, 5, 7.5, 10。 这些存储桶不适合每次测量,因此可以轻松更改。

Summaries

Summaries Histograms 非常相似,因为它们都暴露了给定数据集的分布。一个主要区别是 Histograms 估计分位数在 Prometheus 服务器上,而摘要是在客户端计算的。

对于某些预定义的分位数,摘要更准确,但由于客户端计算,可能会耗费更多的资源。这就是为什么建议在大多数用例中使用 Histograms

设置我们的 Go 项目

在我们可以使用 Prometheus 之前,我们首先需要构建一个简单的应用程序来公开一些基本指标。为此,我们将构建一个简单的 Golang HTTP 服务器,在访问 localhost:9000 时提供静态 HTML CSS 文件。

让我们从创建项目所需的文件开始。这可以使用以下命令完成:

mkdir static
touch main.go Dockerfile static/index.html

HTTP 服务器是使用 Mux 编写的,它将为包含您在上面创建的 HTML CSS 文件的静态目录提供服务。

package main

import (
 "fmt"
 "github.com/gorilla/mux"
 "log"
 "net/http"
)


func main() {
 router := mux.NewRouter()

 // Serving static files
 router.PathPrefix("/").Handler(http.FileServer(http.Dir("./static/")))

 fmt.Println("Serving requests on port 9000")
 err := http.ListenAndServe(":9000", router)
 log.Fatal(err)
}

HTML 文件将仅包含带有 “Hello World!” H1 标签。作为其内容并导入 CSS 文件。

<html>
<head>
    <title>Hello server</title>
    <link rel="stylesheet" href="style.css"/>
</head>
<body>
<div>
  <h1>Hello World!</h1>
</div>
</body>
</html>

向应用程序添加指标

现在应用程序的基本功能已经完成,我们可以开始公开 Prometheus 稍后将抓取的指标。 Golang Prometheus 官方库会自动公开一些内置指标,只需要导入并添加到 HTTP 服务器即可。

package main

import (
 "fmt"
 "github.com/gorilla/mux"
 "log"
 "net/http"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)


func main() {
 router := mux.NewRouter()

 // Serving static files
 router.PathPrefix("/").Handler(http.FileServer(http.Dir("./static/")))
    
    // Prometheus endpoint
 router.Path("/prometheus").Handler(promhttp.Handler())

 fmt.Println("Serving requests on port 9000")
 err := http.ListenAndServe(":9000", router)
 log.Fatal(err)
}

现在我们已经添加了 Prometheus 库并在 /prometheus 上公开了处理程序,我们可以通过启动应用程序并导航到 localhost:9000/prometheus 来查看指标。输出应与此类似:

# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 2.07e-05
go_gc_duration_seconds{quantile="0.25"} 7.89e-05
go_gc_duration_seconds{quantile="0.5"} 0.000137
go_gc_duration_seconds{quantile="0.75"} 0.0001781
go_gc_duration_seconds{quantile="1"} 0.0002197
go_gc_duration_seconds_sum 0.0071928
go_gc_duration_seconds_count 56
# HELP go_goroutines Number of goroutines that currently exist.
# TYPE go_goroutines gauge
go_goroutines 8
# HELP go_info Information about the Go environment.
# TYPE go_info gauge
go_info{version="go1.15"} 1
# HELP go_memstats_alloc_bytes Number of bytes allocated and still in use.
# TYPE go_memstats_alloc_bytes gauge
go_memstats_alloc_bytes 4.266136e+06
# HELP go_memstats_alloc_bytes_total Total number of bytes allocated, even if freed.
# TYPE go_memstats_alloc_bytes_total counter
go_memstats_alloc_bytes_total 1.17390144e+08
# HELP go_memstats_buck_hash_sys_bytes Number of bytes used by the profiling bucket hash table.
# TYPE go_memstats_buck_hash_sys_bytes gauge
go_memstats_buck_hash_sys_bytes 1.456289e+06
# HELP go_memstats_frees_total Total number of frees.
# TYPE go_memstats_frees_total counter
go_memstats_frees_total 435596
# HELP go_memstats_gc_cpu_fraction The fraction of this program's available CPU time used by the GC since the program started.
# TYPE go_memstats_gc_cpu_fraction gauge
go_memstats_gc_cpu_fraction 1.5705717722141224e-06
# HELP go_memstats_gc_sys_bytes Number of bytes used for garbage collection system metadata.
# TYPE go_memstats_gc_sys_bytes gauge
go_memstats_gc_sys_bytes 4.903096e+06
# HELP go_memstats_heap_alloc_bytes Number of heap bytes allocated and still in use.
# TYPE go_memstats_heap_alloc_bytes gauge
go_memstats_heap_alloc_bytes 4.266136e+06
# HELP go_memstats_heap_idle_bytes Number of heap bytes waiting to be used.
# TYPE go_memstats_heap_idle_bytes gauge
go_memstats_heap_idle_bytes 6.1046784e+07
# HELP go_memstats_heap_inuse_bytes Number of heap bytes that are in use.
# TYPE go_memstats_heap_inuse_bytes gauge
go_memstats_heap_inuse_bytes 5.210112e+06
# HELP go_memstats_heap_objects Number of allocated objects.
# TYPE go_memstats_heap_objects gauge
go_memstats_heap_objects 17572
# HELP go_memstats_heap_released_bytes Number of heap bytes released to OS.
# TYPE go_memstats_heap_released_bytes gauge
go_memstats_heap_released_bytes 6.0588032e+07
# HELP go_memstats_heap_sys_bytes Number of heap bytes obtained from system.
# TYPE go_memstats_heap_sys_bytes gauge
go_memstats_heap_sys_bytes 6.6256896e+07
# HELP go_memstats_last_gc_time_seconds Number of seconds since 1970 of last garbage collection.
# TYPE go_memstats_last_gc_time_seconds gauge
go_memstats_last_gc_time_seconds 1.61550102568985e+09
# HELP go_memstats_lookups_total Total number of pointer lookups.
# TYPE go_memstats_lookups_total counter
go_memstats_lookups_total 0
# HELP go_memstats_mallocs_total Total number of mallocs.
# TYPE go_memstats_mallocs_total counter
go_memstats_mallocs_total 453168
# HELP go_memstats_mcache_inuse_bytes Number of bytes in use by mcache structures.
# TYPE go_memstats_mcache_inuse_bytes gauge
go_memstats_mcache_inuse_bytes 27776
# HELP go_memstats_mcache_sys_bytes Number of bytes used for mcache structures obtained from system.
# TYPE go_memstats_mcache_sys_bytes gauge
go_memstats_mcache_sys_bytes 32768
# HELP go_memstats_mspan_inuse_bytes Number of bytes in use by mspan structures.
# TYPE go_memstats_mspan_inuse_bytes gauge
go_memstats_mspan_inuse_bytes 141576
# HELP go_memstats_mspan_sys_bytes Number of bytes used for mspan structures obtained from system.
# TYPE go_memstats_mspan_sys_bytes gauge
go_memstats_mspan_sys_bytes 147456
# HELP go_memstats_next_gc_bytes Number of heap bytes when next garbage collection will take place.
# TYPE go_memstats_next_gc_bytes gauge
go_memstats_next_gc_bytes 6.42088e+06
# HELP go_memstats_other_sys_bytes Number of bytes used for other system allocations.
# TYPE go_memstats_other_sys_bytes gauge
go_memstats_other_sys_bytes 1.931943e+06
# HELP go_memstats_stack_inuse_bytes Number of bytes in use by the stack allocator.
# TYPE go_memstats_stack_inuse_bytes gauge
go_memstats_stack_inuse_bytes 851968
# HELP go_memstats_stack_sys_bytes Number of bytes obtained from system for stack allocator.
# TYPE go_memstats_stack_sys_bytes gauge
go_memstats_stack_sys_bytes 851968
# HELP go_memstats_sys_bytes Number of bytes obtained from system.
# TYPE go_memstats_sys_bytes gauge
go_memstats_sys_bytes 7.5580416e+07
# HELP go_threads Number of OS threads created.
# TYPE go_threads gauge
go_threads 13
# HELP process_cpu_seconds_total Total user and system CPU time spent in seconds.
# TYPE process_cpu_seconds_total counter
process_cpu_seconds_total 1.83
# HELP process_max_fds Maximum number of open file descriptors.
# TYPE process_max_fds gauge
process_max_fds 1.048576e+06
# HELP process_open_fds Number of open file descriptors.
# TYPE process_open_fds gauge
process_open_fds 10
# HELP process_resident_memory_bytes Resident memory size in bytes.
# TYPE process_resident_memory_bytes gauge
process_resident_memory_bytes 2.8770304e+07
# HELP process_start_time_seconds Start time of the process since unix epoch in seconds.
# TYPE process_start_time_seconds gauge
process_start_time_seconds 1.61549436213e+09
# HELP process_virtual_memory_bytes Virtual memory size in bytes.
# TYPE process_virtual_memory_bytes gauge
process_virtual_memory_bytes 1.564209152e+09
# HELP process_virtual_memory_max_bytes Maximum amount of virtual memory available in bytes.
# TYPE process_virtual_memory_max_bytes gauge
process_virtual_memory_max_bytes -1
# HELP promhttp_metric_handler_requests_in_flight Current number of scrapes being served.
# TYPE promhttp_metric_handler_requests_in_flight gauge
promhttp_metric_handler_requests_in_flight 1
# HELP promhttp_metric_handler_requests_total Total number of scrapes by HTTP status code.
# TYPE promhttp_metric_handler_requests_total counter
promhttp_metric_handler_requests_total{code="200"} 447
promhttp_metric_handler_requests_total{code="500"} 0
promhttp_metric_handler_requests_total{code="503"} 0

这些指标很棒,但大多数时候它们并不是很有用。我们现在想要公开自定义指标,而不是低级指标,这些指标将公开我们应用程序的内部信息,我们以后可以可视化或在测试或健康检查中使用这些信息。

让我们从一个相当基本的指标开始:向服务器发出的 HTTP 请求总数以计数器表示。

package main

import (
 "fmt"
 "github.com/gorilla/mux"
 "github.com/prometheus/client_golang/prometheus"
 "github.com/prometheus/client_golang/prometheus/promauto"
 "github.com/prometheus/client_golang/prometheus/promhttp"
 "log"
 "net/http"
 "strconv"
)

var totalRequests = prometheus.NewCounterVec(
 prometheus.CounterOpts{
  Name: "http_requests_total",
  Help: "Number of get requests.",
 },
 []string{"path"},
)

func prometheusMiddleware(next http.Handler) http.Handler {
 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  rw := NewResponseWriter(w)
  next.ServeHTTP(rw, r)

  totalRequests.WithLabelValues(path).Inc()
 })
}

func init() {
 prometheus.Register(totalRequests)
}

func main() {
 router := mux.NewRouter()
 router.Use(prometheusMiddleware)

 // Prometheus endpoint
 router.Path("/prometheus").Handler(promhttp.Handler())

 // Serving static files
 router.PathPrefix("/").Handler(http.FileServer(http.Dir("./static/")))

 fmt.Println("Serving requests on port 9000")
 err := http.ListenAndServe(":9000", router)
 log.Fatal(err)
}

让我们分解代码更改以更好地理解:

该指标需要使用 prometheus 包创建 .NewCounterVec() 方法用于创建新的计数器度量。要在 HTTP 处理程序中公开创建的指标,我们必须使用 register() 方法将指标注册到 Prometheus 。最后,我们需要在代码中实现指标的功能。在这里,我们创建并注册了一个新的 HTTP 中间件,该中间件在每次服务器接收到 HTTP 请求时运行,并使用 Inc() 方法增加指标计数器。

以下代码块包含另外两个具有不同指标类型的指标: response_status response_time

package main

import (
 "fmt"
 "github.com/gorilla/mux"
 "github.com/prometheus/client_golang/prometheus"
 "github.com/prometheus/client_golang/prometheus/promauto"
 "github.com/prometheus/client_golang/prometheus/promhttp"
 "log"
 "net/http"
 "strconv"
)

type responseWriter struct {
 http.ResponseWriter
 statusCode int
}

func NewResponseWriter(w http.ResponseWriter) *responseWriter {
 return &responseWriter{w, http.StatusOK}
}

func (rw *responseWriter) WriteHeader(code int) {
 rw.statusCode = code
 rw.ResponseWriter.WriteHeader(code)
}

var totalRequests = prometheus.NewCounterVec(
 prometheus.CounterOpts{
  Name: "http_requests_total",
  Help: "Number of get requests.",
 },
 []string{"path"},
)

var responseStatus = prometheus.NewCounterVec(
 prometheus.CounterOpts{
  Name: "response_status",
  Help: "Status of HTTP response",
 },
 []string{"status"},
)

var httpDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
 Name: "http_response_time_seconds",
 Help: "Duration of HTTP requests.",
}, []string{"path"})

func prometheusMiddleware(next http.Handler) http.Handler {
 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  route := mux.CurrentRoute(r)
  path, _ := route.GetPathTemplate()

  timer := prometheus.NewTimer(httpDuration.WithLabelValues(path))
  rw := NewResponseWriter(w)
  next.ServeHTTP(rw, r)

  statusCode := rw.statusCode

  responseStatus.WithLabelValues(strconv.Itoa(statusCode)).Inc()
  totalRequests.WithLabelValues(path).Inc()

  timer.ObserveDuration()
 })
}

func init() {
 prometheus.Register(totalRequests)
 prometheus.Register(responseStatus)
 prometheus.Register(httpDuration)
}

func main() {
 router := mux.NewRouter()
 router.Use(prometheusMiddleware)

 // Prometheus endpoint
 router.Path("/prometheus").Handler(promhttp.Handler())

 // Serving static files
 router.PathPrefix("/").Handler(http.FileServer(http.Dir("./static/")))

 fmt.Println("Serving requests on port 9000")
 err := http.ListenAndServe(":9000", router)
 log.Fatal(err)
}

将应用程序 Docker 化

现在指标已在应用程序中实现,我们可以对应用程序进行 Docker 化,以便更轻松地使用 Prometheus 运行它。

FROM golang:1.15.0

# Set the Current Working Directory inside the container
WORKDIR /app

RUN export GO111MODULE=on

# Copy go mod and sum files
COPY go.mod go.sum ./

# Download all dependencies. Dependencies will be cached if the go.mod and go.sum files are not changed
RUN go mod download

COPY . .

# Build the application
RUN go build -o main .

# Expose port 9000 to the outside world
EXPOSE 9000

# Command to run the executable
CMD ["./main"]

Dockerfile 将下载依赖项,复制所有文件并构建应用程序。完成 Dockerfile 后,我们可以将容器和 Prometheus 放入一个 Docker-Compose `文件中。

version: '3.1'

services:
  golang:
    build:
      context: ./
      dockerfile: Dockerfile
    container_name: golang
    restart: always
    ports:
      - '9000:9000'
  prometheus:
    image: prom/prometheus:v2.24.0
    volumes:
      - ./prometheus/:/etc/prometheus/
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--web.console.libraries=/usr/share/prometheus/console_libraries'
      - '--web.console.templates=/usr/share/prometheus/consoles'
    ports:
      - 9090:9090
    restart: always

volumes:
  prometheus_data:

在启动应用程序之前,我们现在唯一需要做的就是配置 Prometheus 端点。为此,我们将创建一个配置文件:

mkdir prometheus
touch prometheus/prometheus.yml

在这里,我们定义了 Prometheus 应该从中抓取数据的页面的 URL ,对于我们的应用程序,它等于 ContainerIP:Port/prometheus

global:
  scrape_interval:     15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: prometheus
    static_configs:
      - targets: ['localhost:9090']
  - job_name: golang 
    metrics_path: /prometheus
    static_configs:
      - targets:
        - golang:9000

添加配置后,我们可以使用 docker-compose 启动应用程序:

docker-compose up -d

现在我们可以通过在浏览器中访问 localhost:9090 来访问 Prometheus

使用 Grafana 可视化指标

现在 Prometheus 已成功收集指标,您将继续使用 Grafana 可视化数据。为此,您需要首先通过将 Grafana 容器添加到 docker-compose 文件来启动它。

version: '3.1'

services:
  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    ports:
      - "3000:3000"
    volumes:
      - grafana-storage:/var/lib/grafana
  golang:
    build:
      context: ./
      dockerfile: Dockerfile
    container_name: golang
    restart: always
    ports:
      - '9000:9000'
  prometheus:
    image: prom/prometheus:v2.24.0
    volumes:
      - ./prometheus/:/etc/prometheus/
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--web.console.libraries=/usr/share/prometheus/console_libraries'
      - '--web.console.templates=/usr/share/prometheus/consoles'
    ports:
      - 9090:9090
    restart: always

volumes:
  grafana-storage:
  prometheus_data:

添加 Grafana 容器和将保存 Grafana 配置和数据的卷后,您可以重新启动 docker-compose

docker-compose up -d

现在 Grafana 已启动,您可以通过在浏览器中访问 http://localhost:3000 来访问它。它将要求您输入用户凭据,默认为 admin 作为用户名和密码。

登录后,您可以通过导航到配置>数据源并单击“添加数据源”来创建新数据源。之后,选择 Prometheus ,然后填写必要的信息。

成功添加数据源后,您可以继续创建新仪表板以可视化您的指标。

仪表板由可让您可视化指标的面板组成,因此请单击“添加面板”开始。

现在您可以通过在度量字段中指定一个度量来选择它:例如 http_requests_total

由于您没有经常访问该应用程序,因此您的仪表板可能不会显示与我一样多的数据。获取更多测试数据的最佳方法是使用负载测试工具。

我喜欢使用 hey 负载测试工具,这是一个用于负载生成的开源 CLI 应用程序,但您也可以使用其他工具。下载后,您可以使用以下命令生成流量。

hey -z 5m -q 5 -m GET -H "Accept: text/html" http://127.0.0.1:9000

您现在可以通过添加其他带有指标的面板来试验仪表板并根据自己的喜好对其进行自定义。如果您想要一个可视化我们已实施的所有指标的示例仪表板,您可以从 Github 下载它,然后将其导入。

https://github.com/TannerGabriel/learning-go/tree/master/advanced-programs/PrometheusHTTPServer/grafana

来源

以下是用于撰写本文的资源列表:

  • https://blog.pvincent.io/2017/12/prometheus-blog-series-part-1-metrics-and-labels/

  • https://prometheus.io/docs/guides/go-application/

  • https://dev.to/eminetto/using-prometheus-to-collect-metrics-from-golang-applications-35gc

结论

在本文中,您在 Golang 应用程序中设置了 Prometheus ,并实现了您自己的自定义 Prometheus 指标,然后您在 Grafana 中对其进行了可视化。

更多文档

请关注微信公众号 云原生CTO [1]

参考资料

[1]

参考地址: https://gabrieltanner.org/blog/collecting-prometheus-metrics-in-golang