查日志只有ES好使?那是你沒這樣用Clickhouse……
一、背景
石墨文檔全部應(yīng)用部署在Kubernetes上,每時(shí)每刻都會有大量的日志輸出,我們之前主要使用SLS和ES作為日志存儲。但是我們在使用這些組件的時(shí)候,發(fā)現(xiàn)了一些問題。
1、成本問題
SLS個(gè)人覺得是一個(gè)非常優(yōu)秀的產(chǎn)品,速度快,交互方便,但是SLS索引成本比較貴
- 我們想減少SLS索引成本的時(shí)候,發(fā)現(xiàn)云廠商并不支持分析單個(gè)索引的成本,導(dǎo)致我們無法知道是哪些索引構(gòu)建得不夠合理
- ES使用的存儲非常多,并且耗費(fèi)大量的內(nèi)存
2、通用問題
- 如果業(yè)務(wù)是混合云架構(gòu),或者業(yè)務(wù)形態(tài)有SAAS和私有化兩種方式,那么SLS并不能通用
- 日志和鏈路,需要用兩套云產(chǎn)品,不是很方便
3、精確度問題
- SLS存儲的精度只能到秒,但我們實(shí)際日志精度到毫秒,如果日志里面有traceid,SLS中無法通過根據(jù)traceid信息,將日志根據(jù)毫秒時(shí)間做排序,不利于排查錯(cuò)誤
我們經(jīng)過一番調(diào)研后,發(fā)現(xiàn)使用Clickhouse能夠很好地解決以上問題,并且Clickhouse省存儲空間,非常省錢,所以我們選擇了Clickhouse方案存儲日志。但當(dāng)我們深入研究后,Clickhouse作為日志存儲有許多落地的細(xì)節(jié),但業(yè)界并沒有很好闡述相關(guān)Clickhouse采集日志的整套流程,以及沒有一款優(yōu)秀的Clickhouse日志查詢工具幫助分析日志,為此我們寫了一套Clickhouse日志系統(tǒng)貢獻(xiàn)給開源社區(qū),并將Clickhouse的日志采集架構(gòu)的經(jīng)驗(yàn)做了總結(jié)。先上個(gè)Clickhouse日志查詢界面,讓大家感受下石墨最懂前端的后端程序員。
二、架構(gòu)原理圖
我們將日志系統(tǒng)分為四個(gè)部分:日志采集、日志傳輸、日志存儲、日志管理。
- 日志采集:LogCollector采用Daemonset方式部署,將宿主機(jī)日志目錄掛載到LogCollector的容器內(nèi),LogCollector通過掛載的目錄能夠采集到應(yīng)用日志、系統(tǒng)日志、K8S審計(jì)日志等
- 日志傳輸:通過不同Logstore映射到Kafka中不同的Topic,將不同數(shù)據(jù)結(jié)構(gòu)的日志做了分離
- 日志存儲:使用Clickhouse中的兩種引擎數(shù)據(jù)表和物化視圖
- 日志管理:開源的Mogo系統(tǒng),能夠查詢?nèi)罩?,設(shè)置日志索引,設(shè)置LogCollector配置,設(shè)置Clickhouse表,設(shè)置報(bào)警等
以下我們將按照這四大部分,闡述其中的架構(gòu)原理。
三、日志采集
1、采集方式
Kubernetes容器內(nèi)日志收集的方式通常有以下三種方案。
- DaemonSet方式采集:在每個(gè)node節(jié)點(diǎn)上部署LogCollector,并將宿主機(jī)的目錄掛載為容器的日志目錄,LogCollector讀取日志內(nèi)容,采集到日志中心。
- 網(wǎng)絡(luò)方式采集:通過應(yīng)用的日志SDK,直接將日志內(nèi)容采集到日志中心 。
- SideCar方式采集:在每個(gè)pod內(nèi)部署LogCollector,LogCollector只讀取這個(gè)pod內(nèi)的日志內(nèi)容,采集到日志中心。
以下是三種采集方式的優(yōu)缺點(diǎn):
我們主要采用DaemonSet方式和網(wǎng)絡(luò)方式采集日志。DaemonSet方式用于ingress、應(yīng)用日志的采集,網(wǎng)絡(luò)方式用于大數(shù)據(jù)日志的采集。以下我們主要介紹下DeamonSet方式的采集方式。
2、日志輸出
從上面的介紹中可以看到,我們的DaemonSet會有兩種方式采集日志類型,一種是標(biāo)準(zhǔn)輸出,一種是文件。
引用元乙的描述:雖然使用 Stdout 打印日志是 Docker 官方推薦的方式,但大家需要注意:這個(gè)推薦是基于容器只作為簡單應(yīng)用的場景,實(shí)際的業(yè)務(wù)場景中我們還是建議大家盡可能使用文件的方式,主要的原因有以下幾點(diǎn):
- Stdout 性能問題,從應(yīng)用輸出 stdout 到服務(wù)端,中間會經(jīng)過好幾個(gè)流程(例如普遍使用的JSONLogDriver):應(yīng)用 stdout -> DockerEngine -> LogDriver -> 序列化成 JSON -> 保存到文件 -> Agent 采集文件 -> 解析 JSON -> 上傳服務(wù)端。整個(gè)流程相比文件的額外開銷要多很多,在壓測時(shí),每秒 10 萬行日志輸出就會額外占用 DockerEngine 1 個(gè) CPU 核;
- Stdout 不支持分類,即所有的輸出都混在一個(gè)流中,無法像文件一樣分類輸出,通常一個(gè)應(yīng)用中有 AccessLog、ErrorLog、InterfaceLog(調(diào)用外部接口的日志)、TraceLog 等,而這些日志的格式、用途不一,如果混在同一個(gè)流中將很難采集和分析;
- Stdout 只支持容器的主程序輸出,如果是 daemon/fork 方式運(yùn)行的程序?qū)o法使用 stdout;
- 文件的 Dump 方式支持各種策略,例如同步/異步寫入、緩存大小、文件輪轉(zhuǎn)策略、壓縮策略、清除策略等,相對更加靈活。
從這個(gè)描述中,我們可以看出在docker中輸出文件再采集到日志中心是一個(gè)更好的實(shí)踐。所有日志采集工具都支持采集文件日志方式,但是我們在配置日志采集規(guī)則的時(shí)候,發(fā)現(xiàn)開源的一些日志采集工具,例如fluentbit、filebeat在DaemonSet部署下采集文件日志是不支持追加例如pod、namespace、container_name、container_id等label信息,并且也無法通過這些label做些定制化的日志采集。
基于無法追加label信息的原因,我們暫時(shí)放棄了DeamonSet部署下文件日志采集方式,采用的是基于DeamonSet部署下標(biāo)準(zhǔn)輸出的采集方式。
3、日志目錄
以下列舉了日志目錄的基本情況。
因?yàn)槲覀儾杉罩臼鞘褂玫臉?biāo)準(zhǔn)輸出模式,所以根據(jù)上表我們的LogCollector只需要掛載/var/log,/var/lib/docker/containers兩個(gè)目錄。
1)標(biāo)準(zhǔn)輸出日志目錄
應(yīng)用的標(biāo)準(zhǔn)輸出日志存儲在/var/log/containers目錄下,文件名是按照K8S日志規(guī)范生成的。這里以nginx-ingress的日志作為一個(gè)示例。我們通過ls /var/log/containers/ | grep nginx-ingress指令,可以看到nginx-ingress的文件名。
nginx-ingress-controller-mt2wx_kube-system_nginx-ingress-controller-be3741043eca1621ec4415fd87546b1beb29480ac74ab1cdd9f52003cf4abf0a.log
我們參照K8S日志的規(guī)范:/var/log/containers/%{DATA:pod_name}_%{DATA:namespace}_%{GREEDYDATA:container_name}-%{DATA:container_id}.log。可以將nginx-ingress日志解析為:
- pod_name:nginx-ingress-controller-mt2w
- namespace:kube-system
- container_name:nginx-ingress-controller
- container_id:be3741043eca1621ec4415fd87546b1beb29480ac74ab1cdd9f52003cf4abf0a
通過以上的日志解析信息,我們的LogCollector就可以很方便地追加pod、namespace、container_name、container_id的信息。
2)容器信息目錄
應(yīng)用的容器信息存儲在/var/lib/docker/containers目錄下,目錄下的每一個(gè)文件夾為容器ID,我們可以通過cat config.v2.json獲取應(yīng)用的docker基本信息。
4、LogCollector采集日志
1)配置
我們LogCollector采用的是fluent-bit,該工具是cncf旗下的,能夠更好地與云原生相結(jié)合。通過Mogo系統(tǒng)可以選擇Kubernetes集群,很方便地設(shè)置fluent-bit configmap的配置規(guī)則。
2)數(shù)據(jù)結(jié)構(gòu)
fluent-bit的默認(rèn)采集數(shù)據(jù)結(jié)構(gòu)
- @timestamp字段:string or float,用于記錄采集日志的時(shí)間
- log字段:string,用于記錄日志的完整內(nèi)容
Clickhouse如果使用@timestamp的時(shí)候,因?yàn)槔锩嬗蠤特殊字符,會處理得有問題。所以我們在處理fluent-bit的采集數(shù)據(jù)結(jié)構(gòu),會做一些映射關(guān)系,并且規(guī)定雙下劃線為Mogo系統(tǒng)日志索引,避免和業(yè)務(wù)日志的索引沖突。
- _time_字段:string or float,用于記錄采集日志的時(shí)間
- _log_字段:string,用于記錄日志的完整內(nèi)容
例如你的日志記錄的是{"id":1},那么實(shí)際fluent-bit采集的日志會是{"_time_":"2022-01-15...","_log_":"{\"id\":1}" 該日志結(jié)構(gòu)會直接寫入到kafka中,Mogo系統(tǒng)會根據(jù)這兩個(gè)字段_time_、_log_設(shè)置clickhouse中的數(shù)據(jù)表。
3)采集
如果我們要采集ingress日志,我們需要在input配置里,設(shè)置ingress的日志目錄,fluent-bit會把ingress日志采集到內(nèi)存里
然后我們在filter配置里,將log改寫為_log_
然后我們在ouput配置里,將追加的日志采集時(shí)間設(shè)置為_time_,設(shè)置好日志寫入的kafka borkers和kafka topics,那么fluent-bit里內(nèi)存的日志就會寫入到kafka中
日志寫入到Kafka中_log_需要為json,如果你的應(yīng)用寫入的日志不是json,那么你就需要根據(jù)fluent-bit的parser文檔,調(diào)整你的日志寫入的數(shù)據(jù)結(jié)構(gòu):https://docs.fluentbit.io/manual/pipeline/filters/parser
四、日志傳輸
Kafka主要用于日志傳輸。上文說到我們使用fluent-bit采集日志的默認(rèn)數(shù)據(jù)結(jié)構(gòu),在下圖kafka工具中我們可以看到日志采集的內(nèi)容。
在日志采集過程中,會由于不用業(yè)務(wù)日志字段不一致,解析方式是不一樣的。所以我們在日志傳輸階段,需要將不同數(shù)據(jù)結(jié)構(gòu)的日志,創(chuàng)建不同的Clickhouse表,映射到Kafka不同的Topic。這里以ingress為例,那么我們在Clickhouse中需要?jiǎng)?chuàng)建一個(gè)ingress_stdout_stream的Kafka引擎表,然后映射到Kafka的ingress-stdout Topic里。
五、日志存儲
我們會使用三種表,用于存儲一種業(yè)務(wù)類型的日志。
1、Kafka引擎表
將數(shù)據(jù)從Kafka采集到Clickhouse的ingress_stdout_stream數(shù)據(jù)表中。
create table logger.ingress_stdout_stream
(
_source_ String,
_pod_name_ String,
_namespace_ String,
_node_name_ String,
_container_name_ String,
_cluster_ String,
_log_agent_ String,
_node_ip_ String,
_time_ Float64,
_log_ String
)
engine = Kafka SETTINGS kafka_broker_list = 'kafka:9092', kafka_topic_list = 'ingress-stdout', kafka_group_name = 'logger_ingress_stdout', kafka_format = 'JSONEachRow', kafka_num_consumers = 1;
2、物化視圖
將數(shù)據(jù)從ingress_stdout_stream數(shù)據(jù)表讀取出來,_log_根據(jù)Mogo配置的索引,提取字段再寫入到ingress_stdout結(jié)果表里。
CREATE MATERIALIZED VIEW logger.ingress_stdout_view TO logger.ingress_stdout AS
SELECT
toDateTime(toInt64(_time_)) AS _time_second_,
fromUnixTimestamp64Nano(toInt64(_time_*1000000000),'Asia/Shanghai') AS _time_nanosecond_,
_pod_name_,
_namespace_,
_node_name_,
_container_name_,
_cluster_,
_log_agent_,
_node_ip_,
_source_,
_log_ AS _raw_log_,JSONExtractInt(_log_, 'status') AS status,JSONExtractString(_log_, 'url') AS url
FROM logger.ingress_stdout_stream where 1=1;
3、結(jié)果表
存儲最終的數(shù)據(jù)
create table logger.ingress_stdout
(
_time_second_ DateTime,
_time_nanosecond_ DateTime64(9, 'Asia/Shanghai'),
_source_ String,
_cluster_ String,
_log_agent_ String,
_namespace_ String,
_node_name_ String,
_node_ip_ String,
_container_name_ String,
_pod_name_ String,
_raw_log_ String,
status Nullable(Int64),
url Nullable(String),
)
engine = MergeTree PARTITION BY toYYYYMMDD(_time_second_)
ORDER BY _time_second_
TTL toDateTime(_time_second_) + INTERVAL 7 DAY
SETTINGS index_granularity = 8192;
六、總結(jié)流程
1、日志會通過fluent-bit的規(guī)則采集到kafka,在這里我們會將日志采集到兩個(gè)字段里。
_time_字段用于存儲fluent-bit采集的時(shí)間
_log_字段用于存放原始日志
2、通過mogo,在clickhouse里設(shè)置了三個(gè)表。
app_stdout_stream:將數(shù)據(jù)從Kafka采集到Clickhouse的Kafka引擎表
app_stdout_view:視圖表用于存放mogo設(shè)置的索引規(guī)則
app_stdout:根據(jù)app_stdout_view索引解析規(guī)則,消費(fèi)app_stdout_stream里的數(shù)據(jù),存放于app_stdout結(jié)果表中
3、最后mogo的UI界面,根據(jù)app_stdout的數(shù)據(jù),查詢?nèi)罩拘畔?/h4>七、Mogo界面展示
1、查詢?nèi)罩窘缑?/h4>
2、設(shè)置日志采集配置界面
以上文檔描述是針對石墨Kubernetes的日志采集。