prometheus內核

  • 2019 年 12 月 29 日
  • 筆記

架構

image.png

這篇文章會著重分析 其中的 discovery => scrap => storage 的流程

配置

配置有兩部分,一部分來自命令行的啟動參數,一部分來自 prometheus.yml

來自命令行的配置參數

配置核心的部分分成幾塊,其中以 web.Options 為重點,比如 notifier.Options 等其他配置在初始化的過程中最終會被轉換為 web.Options 中的一部分。

  • configFile: prometheus.yml 文件路徑
  • storage: 本地存儲配置,可配置的有
    • localStoragePath: 數據存儲位置
    • WALSegmentSize
    • newFlagRetentionDuration 存儲時間
    • NoLockfile/ AllowOverlappingBlocks/ WALCompression: lock 壓縮等
    • RemoteFlushDeadline: shutdown或者reload等時候需要 flush,flush超時時間
    • RemoteReadSampleLimit/ RemoteReadConcurrencyLimit/ RemoteReadBytesInFrame: Remote其他配置 sample數量、並發、大小等
  • alert
    • outageTolerance/ forGracePeriod/ resendDelay: alert 觸發相關
    • notifier.QueueCapacity/notifierTimeout: notifier相關
  • query
    • lookbackDelta: maximum lookback duratio
    • queryTimeout/queryConcurrency/queryMaxSamples: 超時/並發/最大數量
  • web: prometheus 作為一個 web server的配置
    • web.ListenAddress/ webTimeout/ MaxConnections/ prometheusURL/ web.RoutePrefix/ web.UserAssetsPath 基本 web參數
    • web.EnableLifecycle/ web.EnableAdminAPI/ web.ConsoleTemplatesPath/ web.ConsoleLibrariesPath
    • web.PageTitle/ corsRegexString/
// prometheus/prometheus/cmd/prometheus/main.go

來自 prometheus.yml 的配置參數

來自 prometheus.yml 的參數對大部分組件是 reloadable 的,實現方式為 這些組件都實現了一個 ApplyConfig(*config.Config) error 的函數, 比如:

  • remoteStorage: 遠程存儲。支援比如 influx db/ open tsdb作為存儲後端的核心
  • webHandler: web 核心
  • scrapeManager: 指標抓取
  • discoveryManagerScrape:發現抓取目標
  • discoveryManagerNotify:發現 alert 推送目標
  • ruleManager: 抓取規則(recording / alerting rules)管理
  • notifierManager: 分發 alert notifications 到 alert manager

也就是 當 prometheus.yml 本更新之後,這些組件無需重啟(SIGHUP,web觸發)就可以得到更新

配置的格式參考這裡, 這裡我們做簡要的解釋。

global:      # 這兩個是全局的刮取間隔以及超時設置    # How frequently to scrape targets by default.    [ scrape_interval: <duration> | default = 1m ]    # How long until a scrape request times out.    [ scrape_timeout: <duration> | default = 10s ]      # 多久evaluate rules一次    [ evaluation_interval: <duration> | default = 1m ]      # 全局的 external label, 當 prometheus (federation, remote storage, Alertmanager)    # 和外部交互的時候很有用 . 舉個例子:當多個prometheus數據聚合到同一個federation prometheus    # 或者 remote storage 的時候,可以加一個 id/cluster label作為區分    external_labels:      [ <labelname>: <labelvalue> ... ]    # 規則文件路徑,規則分為兩種,一種叫 recoding rule, 另一種叫 alter rule  # 他們都是 隔一段時間內部 evaluate 一次,生成新的 metrics 或者 產生 alter notification  rule_files:    [ - <filepath_glob> ... ]    # 抓取配置,這個是配置文件的大頭,各種抓取規則都在這裡,下面會分解細講  scrape_configs:    [ - <scrape_config> ... ]    # Alertmanager相關的配置  alerting:    alert_relabel_configs:      [ - <relabel_config> ... ]    alertmanagers:      [ - <alertmanager_config> ... ]    # remote_write/read 相關的配置  remote_write:    [ - <remote_write> ... ]  remote_read:    [ - <remote_read> ... ]    

scrape_configs

# 1. 每一個抓取任務叫一個 job,通常配置裡面會有多個抓取任務,比如抓取node監控,server監控等  job_name: <job_name>    # 2. 對於這個job設置的抓取間隔和超時時間  [ scrape_interval: <duration> | default = <global_config.scrape_interval> ]  [ scrape_timeout: <duration> | default = <global_config.scrape_timeout> ]    # 3. 這個job的抓取路徑  [ metrics_path: <path> | default = /metrics ]  [ scheme: <scheme> | default = http ]  params:    [ <string>: [<string>, ...] ]    # 4. 其他抓取相關的配置略,主要是抓取鑒權/代理相關,配置 prometheus 如何發送抓取請求  basic_auth/ bearer_token/ bearer_token_file/ tls_config/ proxy_url      # 5. 當 抓取到的指標 label 和 服務端生成的 label產生衝突的時候如何處理,服務端生成的 label  # 包括 job / instance/ 配置 labels, labels 和 service discovery (sd) implementations 生成的 label;  # 最後一種很重要也很容易讓人迷惑,後面會具體的講一下 sd 是如何生成和配置的  [ honor_labels: <boolean> | default = false ]  # 是否完全使用 抓取到的指標中的時間戳  [ honor_timestamps: <boolean> | default = true ]        # 6. sd 相關的配置,由於 prometheus 是主動抓取,而抓取目標往往是快速變化的,比如一個容器,他的生命周期可能很短  # 那麼就存在一個如何自動發現抓取目標,已經在抓取數據上添加各種 [元Label] 的問題  # sd 就是為這個產生,根據抓取目標、服務發現機制的不同,sd 有多種實現,下面 discovery 會我們把用的比較多的  # kubernetes_sd_configs 細講一下,其他(azure_sd_configs/consul_sd_configs/dns_sd_configs等等)略  kubernetes_sd_configs:    [ - <kubernetes_sd_config> ... ]    # 這裡面的主要參數就是 api_server/role/namespaces,其他鑒權相關略  # kubernetes api server 地址,不填就是 incluster config      [ api_server: <host> ]      # 角色 見discover一節,支援endpoints, service, pod, node, or ingress      role: <role>      # 抓取對象的 namespace,不填就是所有空間      namespaces:        names:          [ - <string> ]      # 7. 固定的靜態配置的 label  static_configs:    [ - <static_config> ... ]    # 8. 這部分和 如何 抓取/保存 metrics 數據有關係,也比較令人困惑,下面的 scrape 一節會細講  # 注意這裡 relabel_configs,metric_relabel_configs 都是用的 relabel_config,不同的是 relabel_configs  # 還會影響如何抓取(在 scrape 之前)的動作,而 metric_relabel_configs 只會影響 抓取之後的 存儲  relabel_configs:    [ - <relabel_config> ... ]      # 靜態配置的地址      targets:        [ - '<host>' ]      # Labels assigned to all metrics scraped from the targets.      labels:        [ <labelname>: <labelvalue> ... ]    # List of metric relabel configurations.  metric_relabel_configs:    [ - <relabel_config> ... ]    # Per-scrape limit on number of scraped samples that will be accepted.  # If more than this number of samples are present after metric relabelling  # the entire scrape will be treated as failed. 0 means no limit.  [ sample_limit: <int> | default = 0 ]

Discovery

一個sd config對應一個 provider, 下面等程式碼以 kubernetes sd 為例. 從 discover.Manager 裡面啟動,讀取 sd config的部分

// Run implements the discoverer interface.  func (d *Discovery) Run(ctx context.Context, ch chan<- []*targetgroup.Group) 

discover 更新流程

  • 根據 Role 的不同,啟動不同的封裝, 比如 Role endpoint會啟動 endpointsInformer; serviceInformer; podInformer; 也就是說對於這個 namespace的 endpoint; service; pod 都會 watch
  • 從下面的程式碼可以看出,對於每個 provider 都要run 一次,也就是要 watch k8s api一次,而NewSharedInformer 其實什麼也沒 share,會不會造成對 k8s api的壓力,和重複對快取 ?答案是不會,因為如果 provider的配置相同在上一步就被過濾了。
// prometheus/prometheus/discovery/targetgroup/targetgroup.go  type Group struct {  	// Targets is a list of targets identified by a label set. Each target is  	// uniquely identifiable in the group by its address label.  	Targets []model.LabelSet  	// Labels is a set of labels that is common across all targets in the group.  	Labels model.LabelSet    	// Source is an identifier that describes a group of targets.  	Source string  }      // prometheus/prometheus/discovery/kubernetes/kubernetes.go  func (d *Discovery) Run(ctx context.Context, ch chan<- []*targetgroup.Group) {  	switch d.role {  	case RoleEndpoint:  		for _, namespace := range namespaces {  		    // ....  			eps := NewEndpoints(  				log.With(d.logger, "role", "endpoint"),  				cache.NewSharedInformer(slw, &apiv1.Service{}, resyncPeriod),  				cache.NewSharedInformer(elw, &apiv1.Endpoints{}, resyncPeriod),  				cache.NewSharedInformer(plw, &apiv1.Pod{}, resyncPeriod),  			)  			d.discoverers = append(d.discoverers, eps)  			go eps.endpointsInf.Run(ctx.Done())  			go eps.serviceInf.Run(ctx.Done())  			go eps.podInf.Run(ctx.Done())  			
  • watch 之後的變化根據 role 類型會做轉化,但是最終轉化為的東西叫 Group, Group 的定義非常簡單,就是一組 label; 那麼怎麼體現出 發現 這個過程呢。核心就在一些特殊的 label 裡面。

下面以 service 為例子

Label

含義

__address__

service 的地址, 比如 aa.default.svc:80; 這個 label 更為特殊,決定了具體 scrape 的目標地址

__meta_kubernetes_service_port_name

service 埠名

__meta_kubernetes_service_port_protocol

service 協議

__meta_kubernetes_service_name

serivce 名字

__meta_kubernetes_service_cluster_ip

serivce cluster ip

__meta_kubernetes_service_external_name

serivce external name

__meta_kubernetes_namespace

namespace 名字

__meta_kubernetes_service_label_xxx

其他來自 service 的 labels 的 label

__meta_kubernetes_service_labelpresent_xxx

true

__meta_kubernetes_service_annotation_xxx

其他來自 service 的 annotation 的 label

__meta_kubernetes_service_annotationpresent_xxx

true

  • 除了 __address__ 其實還有 __scheme__ 和 __metrics_path__ 會影響抓取目標,但是因為 relabel 的靈活性,可以在上面的 任何一個 label 裡面(可能是 pod 的label)得到 相關的資訊,在relabel的過程中寫入到 __scheme__ 和 __metrics_path__就可以了
  • 對於 role 為endpoint,__address__ 是 endpoint 地址,同時還會把 關聯的 pod 和 service label都加上去。
  • role 為 node 的 __address__ 是各個node對應的 kubelet 地址
  • role 為 pod 的 __address__ 是 podip+container port,同時會被其 contoller的 name/id 也加上去
  • 所以可以看出 role 的不同,影響是抓取對象地址 以及對應數據上的 meta_label; 區別並不大,所以在很多配置上 可以看到 抓取的 role 全都是 endpoint, 因為這種 role 已經能把大部分目標都囊括了 (endpoint 接近 pod,同時 可以體現 service 和 node, 比如用 kubelet 的 endpoint 就抓取了 node 目標,只是有部分 label的差異,比如 pod 有 pod ready的相關label)
  • 官方介紹

Scrape

Scrape 的核心是 scrapePool

  • scrapePools => scrapePool, 這部分由 discovery 發現的 targetSets map[string][]*targetgroup.Group 觸發生成並 定期 reload, 每一個 scrape_configs 對應了一個 scrapePool, key 都是 job_name
  • 生成 target 的過程會 populateLabels(/scrape/target.go),這個過程 就會做 relabel 操作, 同時一些特殊的配置也變成label了 方便後面統一處理
  • scrapePool.sync 中 對 targets中的每一個 target 都生成了一個 loop
  • scrapeloop 中由幾個快取,一種 pool(buffer) 是快取各種大小的 []byte, 避免頻繁記憶體申請;另一種是 scrapeCache
  • scrapeCache 保存 metric string和 label sets的關係,同時跟蹤 scrape 之間的 staleness of series

relabel

  • label 在 prometheus 被廣泛的使用,在prometheus甚至任何數據都看看成是一組 label.
  • relabel 的動作不僅會影響數據的抓取 (target address / filtering targets)也會影響數據的保存 (filtering data / label-modification)
// prometheus/scrape/scrape.go  // 抓取數據  func (sl *scrapeLoop) run(interval, timeout time.Duration, errc chan<- error)    =>    // 通過 append 保存  func (sl *scrapeLoop) append(b []byte, contentType string, ts time.Time) (total, added, seriesAdded int, err error)    =>    // 修改 label  // 1. lset是數據中的 label  // 2. target 裡面有 配置的, discover出來的 label,經過了 relabel_configs 的修改  // 3. rc 是 metric_relabel_configs,這裡再次 relabel process  // 這裡有個潛規則,label 修改為空了,那麼表示 metrics 都不要了,整體丟棄  func mutateSampleLabels(lset labels.Labels, target *Target, honor bool, rc []*relabel.Config) labels.Labels 

relabel 部分的配置格式

# 來源 labels (label key),多個用, 分隔 => 用於 replace, keep, and drop  [ source_labels: '[' <labelname> [, ...] ']' ]  # label value 連接的分隔符  [ separator: <string> | default = ; ]    # 生成的 label 名,用於 replace 必填  [ target_label: <labelname> ]    # 用於提取 source label 內容的正則表達式,默認 (.*)  [ regex: <regex> | default = (.*) ]    # hash 使用  [ modulus: <uint64> ]    # 選擇 regex 使用,用於 replace  [ replacement: <string> | default = $1 ]    # Action,可用 replace;keep;drop;hashmod;labelmap;labeldrop;labelkeep  [ action: <relabel_action> | default = replace ]

relabel 的處理功能強大,但是程式碼卻十分簡潔

action

類別

作用

Drop

抓取動作

Regex 匹配 label value, 匹配則丟棄數據

Keep

抓取動作

Regex 匹配 label value, 不匹配則丟棄數據

Replace

修改 label: 增刪改

Regex 匹配 label value,根據TargetLabel和Replacement模板生成新 label 對修改

HashMod

修改 label: 增

target label: hash(source label value)

LabelMap

修改 label: 改

匹配 label key, 修改 label key,

LabelDrop

修改 label: 刪

匹配 label key 則丟棄這個 label

LabelKeep

修改 label: 刪

不匹配 label key 則丟棄這個 label

// prometheus/pkg/relabel/relabel.go  func Process(labels labels.Labels, cfgs ...*Config) labels.Labels {  	for _, cfg := range cfgs {  		labels = relabel(labels, cfg)  		if labels == nil {  			return nil  		}  	}  	return labels  }    func relabel(lset labels.Labels, cfg *Config) labels.Labels {  	// 省略部分程式碼,val 是 label key 對應的 label value 連接  	val := strings.Join(values, cfg.Separator)  	lb := labels.NewBuilder(lset)    	switch cfg.Action {  	case Drop: if cfg.Regex.MatchString(val)  return nil  	case Keep: if !cfg.Regex.MatchString(val) return nil  	case Replace: lb[TargetLabel(val)] = Replacement(val)  	case HashMod: lb.Set(cfg.TargetLabel, fmt.Sprintf("%d", mod := sum64(md5.Sum([]byte(val))) % cfg.Modulus))  	case LabelMap: for l in lset { if cfg.Regex.MatchString(l.Name) { lb.Set(cfg.Regex.ReplaceAllString(l.Name, cfg.Replacement), l.Value) } }  	case LabelDrop: for l in lset { if cfg.Regex.MatchString(l.Name) { lb.Del(l.Name) } }  	case LabelKeep: for l in lset { if !cfg.Regex.MatchString(l.Name) { lb.Del(l.Name) } }  	}    	return lb.Labels()  }
  • 舉個使用得比較多的 keep 和 replace 的例子, 下面這個例子表示: 滿足條件 endpoint.service.labels["k8s_app"] == "kube-state-metrics" && endpoint.port_name == "http-main" 的 target 才會被抓取, 並且會設置 label namespace=endpoint.namespace
  relabel_configs:    - source_labels: [__meta_kubernetes_service_label_k8s_app]      separator: ;      regex: kube-state-metrics      replacement: $1      action: keep    - source_labels: [__meta_kubernetes_endpoint_port_name]      separator: ;      regex: http-main      replacement: $1      action: keep    - source_labels: [__meta_kubernetes_namespace]      separator: ;      regex: (.*)      target_label: namespace      replacement: $1      action: replace

Storage

參考

  • fanout storage 通過一個 Appendable 的interface 暴露給 scrape, Appendable 返回 Appender, fanout storage 實現 fanoutAppender,底層委託給 local storage 和 remote storage, 分別為 primary, 和 secondaries (secondary可以有多個)
  • fanoutAppender 的邏輯很簡單,就是依次對 primary, secondaries storage 執行對應 interface的操作
  • 查詢的時候 也是有個 mergeQuerier 對 primaryQuerier 和 其他 queriers進行包裝
type Appender interface {  	Add(l labels.Labels, t int64, v float64) (uint64, error)    	AddFast(l labels.Labels, ref uint64, t int64, v float64) error    	// Commit submits the collected samples and purges the batch.  	Commit() error    	Rollback() error  }    // Querier provides reading access to time series data.  type Querier interface {  	// Select returns a set of series that matches the given label matchers.  	Select(*SelectParams, ...*labels.Matcher) (SeriesSet, Warnings, error)    	// LabelValues returns all potential values for a label name.  	LabelValues(name string) ([]string, Warnings, error)    	// LabelNames returns all the unique label names present in the block in sorted order.  	LabelNames() ([]string, Warnings, error)    	// Close releases the resources of the Querier.  	Close() error  }

Tags: