prometheus內核
- 2019 年 12 月 29 日
- 筆記
架構

這篇文章會著重分析 其中的 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 | 含義 |
|---|---|
| service 的地址, 比如 aa.default.svc:80; 這個 label 更為特殊,決定了具體 scrape 的目標地址 |
| service 埠名 |
| service 協議 |
| serivce 名字 |
| serivce cluster ip |
| serivce external name |
| namespace 名字 |
| 其他來自 service 的 labels 的 label |
| true |
| 其他來自 service 的 annotation 的 label |
| 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 才會被抓取, 並且會設置 labelnamespace=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 }

