前言
在對公司容器雲的日志方案進行設計的時候,發現主流的 ELK (Elasticsearch, Logstash, Kibana) 或者 EFK (Elasticsearch, Filebeat or Fluentd, Kibana) 比較重,再加上現階段對於 ES 復雜的搜索功能很多都用不上,最終選擇了 Grafana 開源的 Loki 日志系統。下面我們來介紹下 Loki 的一些基本概念和架構,當然 EFK 作為業界成熟的日志聚合解決方案也是大家應該需要熟悉和掌握的;
簡介
Loki 是 Grafana Labs 團隊最新的開源項目,是一個水平可擴展,高可用性,多租戶的日志聚合系統。它的設計非常經濟高效且易於操作,因為它不會為日志內容編制索引,而是為每個日志流編制一組標簽,專門為 Prometheus 和 Kubernetes 用戶做了相關優化。該項目受 Prometheus 啟發,官方的介紹就是: Like Prometheus,But For Logs.,類似於 Prometheus 的日志系統;
項目地址:https://github.com/grafana/loki/
與其他日志聚合系統相比, Loki 具有下面的一些特性:
- 不對日志進行全文索引。通過存儲壓縮非結構化日志和僅索引元數據,Loki 操作起來會更簡單,更省成本。
- 通過使用與 Prometheus 相同的標簽記錄流對日志進行索引和分組,這使得日志的擴展和操作效率更高,能對接alertmanager;
- 特別適合儲存 Kubernetes Pod 日志; 諸如 Pod 標簽之類的元數據會被自動刪除和編入索引;
- 受 Grafana 原生支持,避免kibana和grafana來回切換;
架構說明
組件說明
Promtail 作為采集器,類比filebeat
loki相當於服務端,類比es
loki進程包含四種角色
querier 查詢器
inester 日志存儲器
query-frontend 前置查詢器
distributor 寫入分發器
可以通過loki二進制的 -target 參數指定運行角色
read path
查詢器接受HTTP/1 數據請求
查詢器將查詢傳遞給所有ingesters請求內存中的數據
接收器接受讀取的請求,並返回與查詢匹配的數據(如果有)
如果沒有接受者返回數據, 則查詢器會從后備存儲中延遲加載數據並對其執行查詢;
查詢器將迭代所有接收到的數據並進行重復數據刪除, 從而通過HTTP/1連接返回最終數據集;
write path
分發服務器收到一個HTTP/1請求,以存儲流數據;
每個流都使用散列環散列;
分發程序將每個流發送到適當的inester和其副本(基於配置的復制因子);
每個實例將為流的數據創建一個塊或將其追加到現有塊中, 每個租戶和每個標簽集的塊都是唯一的;
分發服務器通過HTTP/1鏈接以成功代碼作為響應;
部署
本地化模式安裝
下載promtail和loki二進制
wget https://github.com/grafana/loki/releases/download/v2.2.1/loki-linux-amd64.zip
wget https://github.com/grafana/loki/releases/download/v2.2.1/promtail-linux-amd64.zip
安裝promtail
$ mkdir /opt/app/{promtail,loki} -pv
# promtail配置文件
$ cat <<EOF> /opt/app/promtail/promtail.yaml
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /var/log/positions.yaml # This location needs to be writeable by promtail.
client:
url: http://localhost:3100/loki/api/v1/push
scrape_configs:
- job_name: system
pipeline_stages:
static_configs:
- targets:
- localhost
labels:
job: varlogs
host: yourhost
__path__: /var/log/*.log
EOF
# 解壓安裝包
unzip promtail-linux-amd64.zip
mv promtail-linux-amd64 /opt/app/promtail/promtail
# service文件
$ cat <<EOF >/etc/systemd/system/promtail.service
[Unit]
Description=promtail server
Wants=network-online.target
After=network-online.target
[Service]
ExecStart=/opt/app/promtail/promtail -config.file=/opt/app/promtail/promtail.yaml
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=promtail
[Install]
WantedBy=default.target
EOF
systemctl daemon-reload
systemctl restart promtail
systemctl status promtail
安裝loki
$ mkdir /opt/app/{promtail,loki} -pv
# promtail配置文件
$ cat <<EOF> /opt/app/loki/loki.yaml
auth_enabled: false
server:
http_listen_port: 3100
grpc_listen_port: 9096
ingester:
wal:
enabled: true
dir: /opt/app/loki/wal
lifecycler:
address: 127.0.0.1
ring:
kvstore:
store: inmemory
replication_factor: 1
final_sleep: 0s
chunk_idle_period: 1h # Any chunk not receiving new logs in this time will be flushed
max_chunk_age: 1h # All chunks will be flushed when they hit this age, default is 1h
chunk_target_size: 1048576 # Loki will attempt to build chunks up to 1.5MB, flushing first if chunk_idle_period or max_chunk_age is reached first
chunk_retain_period: 30s # Must be greater than index read cache TTL if using an index cache (Default index read cache TTL is 5m)
max_transfer_retries: 0 # Chunk transfers disabled
schema_config:
configs:
- from: 2020-10-24
store: boltdb-shipper
object_store: filesystem
schema: v11
index:
prefix: index_
period: 24h
storage_config:
boltdb_shipper:
active_index_directory: /opt/app/loki/boltdb-shipper-active
cache_location: /opt/app/loki/boltdb-shipper-cache
cache_ttl: 24h # Can be increased for faster performance over longer query periods, uses more disk space
shared_store: filesystem
filesystem:
directory: /opt/app/loki/chunks
compactor:
working_directory: /opt/app/loki/boltdb-shipper-compactor
shared_store: filesystem
limits_config:
reject_old_samples: true
reject_old_samples_max_age: 168h
chunk_store_config:
max_look_back_period: 0s
table_manager:
retention_deletes_enabled: false
retention_period: 0s
ruler:
storage:
type: local
local:
directory: /opt/app/loki/rules
rule_path: /opt/app/loki/rules-temp
alertmanager_url: http://localhost:9093
ring:
kvstore:
store: inmemory
enable_api: true
EOF
# 解壓包
unzip loki-linux-amd64.zip
mv loki-linux-amd64 /opt/app/loki/loki
# service文件
$ cat <<EOF >/etc/systemd/system/loki.service
[Unit]
Description=loki server
Wants=network-online.target
After=network-online.target
[Service]
ExecStart=/opt/app/loki/loki -config.file=/opt/app/loki/loki.yaml
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=loki
[Install]
WantedBy=default.target
EOF
systemctl daemon-reload
systemctl restart loki
systemctl status loki
使用
grafana上配置loki數據源
grafana-loki-dashsource
在數據源列表中選擇 Loki,配置 Loki 源地址:
grafana-loki-dashsource-config
源地址配置 http://loki:3100 即可,保存。
保存完成后,切換到 grafana 左側區域的 Explore,即可進入到 Loki 的頁面
grafana-loki
然后我們點擊 Log labels 就可以把當前系統采集的日志標簽給顯示出來,可以根據這些標簽進行日志的過濾查詢:
grafana-loki-log-labels
比如我們這里選擇 /var/log/messages,就會把該文件下面的日志過濾展示出來,不過由於時區的問題,可能還需要設置下時間才可以看到數據:
s
grafana-loki-logs
這里展示的是 promtail 容器里面 / var/log 目錄中的日志
promtail 容器 / etc/promtail/config.yml
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: system
static_configs:
- targets:
- localhost
labels:
job: varlogs
__path__: /var/log/*log
這里的 job 就是 varlog,文件路徑就是 / var/log/*log
在grafana explore上配置查看日志
查看日志 rate({job="message"} |="kubelet"
算 qps rate({job="message"} |="kubelet" [1m])
只索引標簽
之前多次提到 loki 和 es 最大的不同是 loki 只對標簽進行索引而不對內容索引 下面我們舉例來看下
靜態標簽匹配模式
以簡單的 promtail 配置舉例
配置解讀
scrape_configs:
- job_name: system
pipeline_stages:
static_configs:
- targets:
- localhost
labels:
job: message
__path__: /var/log/messages
- 上面這段配置代表啟動一個日志采集任務
- 這個任務有 1 個固定標簽job="syslog"
- 采集日志路徑為 /var/log/messages , 會以一個名為 filename 的固定標簽
- 在 promtail 的 web 頁面上可以看到類似 prometheus 的 target 信息頁面
可以和使用prometheus一樣的標簽匹配語句進行查詢
{job="syslog"}
scrape_configs:
- job_name: system
pipeline_stages:
static_configs:
- targets:
- localhost
labels:
job: syslog
__path__: /var/log/syslog
- job_name: system
pipeline_stages:
static_configs:
- targets:
- localhost
labels:
job: apache
__path__: /var/log/apache.log
- 如果我們配置了兩個 job,則可以使用{job=~”apache|syslog”} 進行多 job 匹配
- 同時也支持正則和正則非匹配
標簽匹配模式的特點
原理
- 和 prometheus 一致,相同標簽對應的是一個流 prometheus 處理 series 的模式
- prometheus 中標簽一致對應的同一個 hash 值和 refid(正整數遞增的 id),也就是同一個 series
- 時序數據不斷的 append 追加到這個 memseries 中
- 當有任意標簽發生變化時會產生新的 hash 值和 refid,對應新的 series
loki 處理日志的模式 - 和 prometheus 一致,loki 一組標簽值會生成一個 stream - 日志隨着時間的遞增會追加到這個 stream 中,最后壓縮為 chunk - 當有任意標簽發生變化時會產生新的 hash 值,對應新的 stream
查詢過程
- 所以 loki 先根據標簽算出 hash 值在倒排索引中找到對應的 chunk?
- 然后再根據查詢語句中的關鍵詞等進行過濾,這樣能大大的提速
- 因為這種根據標簽算哈希在倒排中查找 id,對應找到存儲的塊在 prometheus 中已經被驗證過了
- 屬於開銷低
- 速度快
動態標簽和高基數
所以有了上述知識,那么就得談談動態標簽的問題了
兩個概念
何為動態標簽:說白了就是標簽的 value 不固定
何為高基數標簽:說白了就是標簽的 value 可能性太多了,達到 10 萬,100 萬甚至更多
比如 apache 的 access 日志
11.11.11.11 - frank [25/Jan/2000:14:00:01 -0500] "GET /1986.js HTTP/1.1" 200 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"
在promtail中使用regex想要匹配action和status_code兩個標簽
scrape_configs:
- job_name: system
pipeline_stages:
static_configs:
- targets:
- localhost
labels:
job: syslog
__path__: /var/log/syslog
- job_name: system
pipeline_stages:
static_configs:
- targets:
- localhost
labels:
job: apache
__path__: /var/log/apache.log
- job_name: system
pipeline_stages:
- regex:
expression: "^(?P<ip>\\S+) (?P<identd>\\S+) (?P<user>\\S+) \\[(?P<timestamp>[\\w:/]+\\s[+\\-]\\d{4})\\] \"(?P<action>\\S+)\\s?(?P<path>\\S+)?\\s?(?P<protocol>\\S+)?\" (?P<status_code>\\d{3}|-) (?P<size>\\d+|-)\\s?\"?(?P<referer>[^\"]*)\"?\\s?\"?(?P<useragent>[^\"]*)?\"?$"
- labels:
action:
status_code:
static_configs:
- targets:
- localhost
labels:
job: apache
env: dev
__path__: /var/log/apache.log
- 那么對應 action=get/post 和 status_code=200/400 則對應 4 個流
11.11.11.11 - frank [25/Jan/2000:14:00:01 -0500] "GET /1986.js HTTP/1.1" 200 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"
11.11.11.12 - frank [25/Jan/2000:14:00:02 -0500] "POST /1986.js HTTP/1.1" 200 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"
11.11.11.13 - frank [25/Jan/2000:14:00:03 -0500] "GET /1986.js HTTP/1.1" 400 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"
11.11.11.14 - frank [25/Jan/2000:14:00:04 -0500] "POST /1986.js HTTP/1.1" 400 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"
- 那四個日志行將變成四個單獨的流,並開始填充四個單獨的塊。
- 如果出現另一個獨特的標簽組合(例如 status_code =“500”),則會創建另一個新流
高基數問題
- 就像上面,如果給 ip 設置一個標簽,現在想象一下,如果您為設置了標簽 ip,來自用戶的每個不同的 ip 請求不僅成為唯一的流
- 可以快速生成成千上萬的流,這是高基數,這可以殺死 Loki
如果字段沒有被當做標簽被索引,會不會查詢很慢
Loki 的超級能力是將查詢分解為小塊並並行分發,以便您可以在短時間內查詢大量日志數據
全文索引問題
- 大索引既復雜又昂貴。通常,日志數據的全文索引的大小等於或大於日志數據本身的大小
- 要查詢日志數據,需要加載此索引,並且為了提高性能,它可能應該在內存中。這很難擴展,並且隨着您攝入更多日志,索引會迅速變大。
- Loki 的索引通常比攝取的日志量小一個數量級,索引的增長非常緩慢
加速查詢沒標簽字段
以上邊提到的 ip 字段為例 - 使用過濾器表達式查詢
{job="apache"} |= "11.11.11.11"
loki查詢時的分片(按時間范圍分段grep)
- Loki 將把查詢分解成較小的分片,並為與標簽匹配的流打開每個區塊,並開始尋找該 IP 地址。
- 這些分片的大小和並行化的數量是可配置的,並取決於您提供的資源
- 如果需要,您可以將分片間隔配置為 5m,部署 20 個查詢器,並在幾秒鍾內處理千兆字節的日志
- 或者,您可以發瘋並設置 200 個查詢器並處理 TB 的日志!
兩種索引模式對比
- es 的大索引,不管你查不查詢,他都必須時刻存在。比如長時間占用過多的內存
- loki 的邏輯是查詢時再啟動多個分段並行查詢
日志量少時少加標簽
- 因為每多加載一個 chunk 就有額外的開銷
- 舉例 如果該查詢是 {app="loki",level!="debug"}
- 在沒加 level 標簽的情況下只需加載一個 chunk 即 app="loki" 的標簽
- 如果加了 level 的情況,則需要把 level=info,warn,error,critical 5 個 chunk 都加載再查詢
需要標簽時再去添加
- 當 chunk_target_size=1MB 時代表 以 1MB 的壓縮大小來切割塊
- 對應的原始日志大小在 5MB-10MB,如果日志在 max_chunk_age 時間內能達到 10MB,考慮添加標簽
日志應當按時間遞增
- 這個問題和 tsdb 中處理舊數據是一樣的道理
- 目前 loki 為了性能考慮直接拒絕掉舊數據