Docker容器以及Kubernetes內應用日誌採集方案介紹與實踐

本文主要分爲兩部分,第一部分介紹容器內日誌收集主要的解決方案,並對各解決方案的優劣進行比較。第二部分首先介紹鏡像內集成日誌採集組件部署方案,再介紹Sidecar(邊車)模式日誌採集部署方案。文章中若有不正之處,請指出。

第一部分:容器內日誌收集主要的解決方案

方案1.在宿主機上實現日誌採集(Logging at the node Level)

方案1的實現方式如下圖1所示,通過在容器所在的宿主機部署日誌採集插件(Log-collector)來對容器中的日誌進行採集,採集預處理後輸出到外部的消息隊列當中(入Kafka等),在由通過類似於ELK的框架進行處理,最終日誌持久化到ElasticSearch當中。接下來將詳細該實現方式。
圖1
我們知道,容器內應用輸出到標準輸出(stdout)和(標準錯誤輸出)stderr的日誌會被容器引擎所處理,例如docker就可以配置其日誌引擎(logging driver),默認的logging driver是json-file,那麼通過在宿主機中查看對應容器的logs文件,就可以查看到應用容器輸出到標準輸出的日誌內容。同樣在Kubernetes環境下也是一樣的,pod內應用容器打印到控制檯的日誌也會被按照json格式存儲在宿主機當中。基於這樣的原理,如果我們在宿主機上部署日誌採集工具(Log-collector),我們就能夠實現容器內應用日誌的採集。這樣方式的部署,優點在於部署簡單,日誌採集資源開銷小,但是會帶來以下幾個問題:

問題1.應用容器的日誌輸出到stdout和stderr,同時被容器引擎處理後存儲到宿主機本地,隨着日誌的增多(如果應用容器不停止),會大量佔用宿主機磁盤空間。
解決辦法:引入滾動日誌工具,將日誌進行切割,並滾動生成,這樣落盤的日誌就不會一直消耗本地存儲空間。

問題2.我們在宿主機上進行日誌採集,會統一部署一套日誌採集工具,那麼如何來標記日誌是屬於哪個應用以及日誌屬於哪個容器?
解決辦法:對日誌採集工具進行開發或者引入開源的日誌採集工具,例如:阿里的log-pilot (https://github.com/AliyunContainerService/log-pilot)。核心思路是日誌採集工具能夠通過容器引擎提供的API(如:docker api)監聽到容器的啓動和停止,從而能建立容器與日誌的對應關係,從而在採集日誌的時候標識出日誌屬於哪個容器。那麼如何標識日誌屬於哪個應用呢?實現方式較多,既可以通過應用層在輸出日誌時進行標識,也可以在啓動容器的時候通過環境變量來標識容器所屬應用。

問題3.json-file格式的日誌,對於某些開發語言(例如JAVA)異常堆棧信息是被分行處理,日誌採集後還需要進行二次加工。
解決辦法:目前來看只能通過對日誌採集工具進行二次開發或者對日誌進行二次處理來解決。

可以看出,爲了解決上述問題,我們不得不進行一些二次開發和處理工作,這增加了工作量和難度,所以除非有充足資源的情況下,可以採取該方案。

方案2.將鏡像內集成日誌採集組件部署方案(Logging at the App Level)
如圖2所示,日誌採集插件(Log-collector)與應用(App)部署在一起,日誌採插件作爲後臺成運行,應用將日誌寫入容器環境的操作系統目錄當中,日誌採集插件讀取日誌並進行預處理,完成後送到MQ當中,最後與方案1一樣,將日誌持久化到ElasticSearch當中,相面詳細介紹該方案。

圖2
爲了解決宿主機日誌收集存在的問題,同時不增加工作量和難度,我們可以考慮將日誌採集工具集成到應用的基礎鏡像之中,應用基於基礎鏡像來生成應用鏡像,在鏡像啓動的時候會把日誌採集工具以後臺進程的方式拉起來,蒐集到日誌後,日誌採集工具可以將日誌送到消息中間件,如Kafka,再由Logstash解析入ElasticSearch,最終用戶可以通過Kibana進行日誌檢索。基於上述方式的部署,我們將採集工具和應用部署到一起,這樣我們就能輕鬆的在日誌採集工程中標識出日誌所屬的應用,同時因爲日誌是按照文件格式落到容器內部,一般的日誌採集工具都能夠通過multiline的方式來處理異常堆棧信息。對於日誌增多的問題,還是需要應用引入滾動日誌工具來解決。
該方案優點在於實現難度小,不增加工作量,同時能夠準確的標識出日誌所述的應用和所屬的文件,使得應用方能夠更加精確的管理日誌。缺點在於每個應用鏡像中都會引入額外的日誌採集工具,增加了應用容器額外的資源開銷,同時日誌採集工具和應用基礎鏡像緊耦合。
方案3.基於Sidecar日誌採集方式(Logging base on Sidecar)
如圖3所示,該方案僅針對於Kubernetes環境,將日誌通過Sidecar(邊車)方式部署到應用Pod中,實現日誌採集,下面將詳細介紹該方案。

圖3
在Kubernetes中,Pod中可以存在多個容器,這些容器之間可以通過emptyDir的方式共享文件系統,基於上述的原理,我們能夠將日誌採集工具從應用基礎鏡像中剝離出來,單獨部署於一個容器中,這個日誌容器與應用容器存在於一個Pod之中,他們之間共享應用容器的日誌文件,從而實現應用容器將日誌寫入本地容器內部文件,日誌採集容器讀取文件內容並進行解析和傳輸。
該方案是第二個方案的優化版本,其減小了日誌採集工具與應用基礎鏡像的耦合,缺點在於每個應用Pod內都需要啓動一個Sidecar日誌採集容器,不過通過實際觀察來看,該Sidecar日誌採集容器只需要很小的資源,即可完成日誌採集。

三種方案選擇思路
三種方案各有優劣點,方案一是比較理想的實施方式,其部署方式資源開銷小,同時對應用侵入小,如果能夠解決日誌標識等問題,該方案應該是首選。方案二和方案三實施技術難度低,但對應用侵入較大,系統資源開銷也稍多一些。當在這三種方案中進行抉擇的時候,需要根據實際情況進行決定。

第二部分:詳細方案介紹
1.方案2部署實踐(Logging at the App Level)
1.1 部署環境
 應用鏡像: Tomcat
 日誌採集工具: filebeat6.2.4
 容器引擎:Docker
 消息組件:Kafka
1.2 應用鏡像介紹
首先看一下基礎鏡像的Dockerfile,可以看到在應用的鏡像中添加了filebeat-6.2.4-x86_64.rpm,並進行了安裝,最後通過start.sh將Tomcat進行啓動,我們繼續看看start.sh做了什麼。

FROM 192.168.1.2:5000/centos:7.3.1611
WORKDIR /home/tomcat
RUN mkdir -p /usr/java
ADD openjdk-8u40.tar /usr/java
ENV JAVA_HOME /usr/java/java-se-8u40-ri
ENV CATALINA_HOME /home/tomcat
ENV PATH=$JAVA_HOME/bin:$CATALINA_HOME/bin:$PATH
ENV LANG en_US.UTF-8
ENV FILEBEAT_OUTPUT kafka
ADD start.sh /home/tomcat/
ADD config_filebeat.sh /home/tomcat/
ADD filebeat-6.2.4-x86_64.rpm /home/
RUN rpm -ivh /home/filebeat-6.2.4-x86_64.rpm
RUN /bin/cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime;\
        /bin/echo -e "ZONE="Asia/Shanghai"/nUTC=false/nRTC=false" > /etc/sysconfig/clock;\
        rm bin/*.bat;\
        groupadd -g 8000 xkx;\
        useradd --create-home --no-log-init --password mypasswd1 -g xkx -u 8000 --shell /home/tomcat xkx;\
        chown -Rf xkx.xkx /home/tomcat;
user xkx
EXPOSE 8080
CMD ["sh","/home/tomcat/start.sh"]

在start.sh中我們首先通過環境變量來生成Filebeat的配置文件(filebeat.yml),環境變量主要包含日誌的位置、應用標識、Kafka brokers的地址和Kafka Topic名稱,接下來通過後臺方式啓動Filebeat,完成啓動後首先檢查Filebeat是否啓動成功,如果啓動成功,則以前臺方式啓動Tomcat。(注意:一定要以前臺方式啓動Tomcat,不然可能出現應用已經退出,而容器還存在)
#獲取容器中的環境變量並執行腳本配置filebeat,生成配置文件

sh /home/tomcat/config_filebeat.sh

#啓動filebeat
nohup /usr/bin/filebeat -c /etc/filebeat/filebeat.yml >/dev/null 2>&1 &
#檢查filebeat啓動是否成功
ps -ef | grep filebeat |grep -v grep
PROCESS_1_STATUS=$?
echo $PROCESS_1_STATUS
if [ $PROCESS_1_STATUS -ne 0 ]; then
    echo "Failed to start filebeat: $PROCESS_1_STATUS"
    exit $PROCESS_1_STATUS
fi
sleep 5

#最後通過前臺方式啓動Tomcat
sh /home/tomcat/bin/catalina.sh run

接下來我們看下config_filebeat.sh腳本的片段,可以看到,我們主要通過環境變量來配置filebeat.yml文件

cat > $FILEBEAT_CONFIG << EOF
logging.level: error
logging.to_syslog: false
logging.to_files: false
logging.metrics.enabled: false
filebeat.prospectors:
- type: log
  enabled: true
  paths:
    - ${TOMCATLOG}*
  fields:
    type: tomcat_catalina_log
    PORT0: ${PORT0}
    host: ${HOST}
    app: ${_APP}
  fields_under_root: true
  multiline.pattern: ^[0-9]
  multiline.negate: true
  multiline.match: after
EOF
}

kafka() {
assert_not_empty "$KAFKA_BROKERS" "KAFKA_BROKERS required"
KAFKA_BROKERS=$(/usr/bin/echo $KAFKA_BROKERS|awk -F, '{for(i=1;i<=NF;i++){printf "\"%s\",", $i}}')
KAFKA_BROKERS=${KAFKA_BROKERS%,}

cat >> $FILEBEAT_CONFIG << EOF
$(base)
output.kafka:
    hosts: [$KAFKA_BROKERS]
    topic: '%{[type]}'
EOF
}

最終生成的filebeat.yml效果如圖4所示:
圖4
完成上述工作後,我們的鏡像就製作好了,這時候應用只需要將日誌輸出到指定的位置,Filebeat就採集日誌並輸出到Kafka當中,最後通過ELK框架完成持久化和展示分析。如圖5所示,是Kibana中查詢到的日誌信息。 圖5描述
2.方案3詳細部署實踐
2.1 部署環境
 容器編排平臺:Kubernetes
 應用鏡像:Tomcat
 日誌採集工具: Fluentd
 消息組件:Kafka
2.2 日誌採集鏡像介紹
日誌採集鏡像是基於Fluentd +Kafka plugin來的製作的,下面是Dockerfile內容.

FROM alpine:3.8
ARG VERSION=1.1
LABEL maintainer "crystonesc"
LABEL Description="Fluentd docker image" Vendor="Fluent Organization" Version=${VERSION}
ENV DUMB_INIT_VERSION=1.2.1

ENV SU_EXEC_VERSION=0.2

ARG DEBIAN_FRONTEND=noninteractive

RUN echo "#aliyun" > /etc/apk/repositories
RUN echo "https://mirrors.aliyun.com/alpine/v3.8/main/" >> /etc/apk/repositories
RUN echo "https://mirrors.aliyun.com/alpine/v3.8/community/" >> /etc/apk/repositories
# Do not split this into multiple RUN!
# Docker creates a layer for every RUN-Statement
# therefore an 'apk delete' has no effect
RUN apk update \
 && apk upgrade \
 && apk add --no-cache \
        ca-certificates \
        ruby ruby-irb ruby-etc ruby-webrick \
        su-exec==${SU_EXEC_VERSION}-r0 \
        dumb-init==${DUMB_INIT_VERSION}-r0 \
 && apk add --no-cache --virtual .build-deps \
        build-base \
        ruby-dev wget gnupg \
 && update-ca-certificates \
 && echo 'gem: --no-document' >> /etc/gemrc \
 && gem install oj -v 3.3.10 \
 && gem install json -v 2.1.0 \
 && gem install fluentd -v 1.3.1 \
 && gem install ruby-kafka -v 0.6.8 \
 && gem install fluent-plugin-kafka -v 0.7.9 \
 && gem install bigdecimal -v 1.3.5 \
 && apk del .build-deps \
 && rm -rf /var/cache/apk/* \
 && rm -rf /tmp/* /var/tmp/* /usr/lib/ruby/gems/*/cache/*.gem


# for log storage (maybe shared with host)
RUN mkdir -p /fluentd/log
# configuration/plugins path (default: copied from .)
RUN mkdir -p /fluentd/etc /fluentd/plugins
RUN mkdir -p /usr/local/tomcat/logs

COPY fluent.conf /fluentd/etc/
COPY entrypoint.sh /bin/
#RUN mkdir -p /home/fluent/hello/
RUN chmod +x /bin/entrypoint.sh
ENV FLUENTD_OPT=""
ENV FLUENTD_CONF="fluent.conf"

ENV LD_PRELOAD=""
ENV DUMB_INIT_SETSID 0
EXPOSE 24224 5140

ENTRYPOINT ["/bin/entrypoint.sh"]

CMD exec fluentd -c /fluentd/etc/${FLUENTD_CONF} -p /fluentd/plugins $FLUENTD_OPT

在entrypoint.sh當中,我們完成Fluentd配置文件的生成,entrypoint.sh內容如下:

#!/usr/bin/dumb-init /bin/sh

uid=${FLUENT_UID:-1000}

# check if a old fluent user exists and delete it
cat /etc/passwd | grep fluent
if [ $? -eq 0 ]; then
    deluser fluent
fi

# (re)add the fluent user with $FLUENT_UID
adduser -D -g '' -u ${uid} -h /home/fluent fluent

#source vars if file exists
DEFAULT=/etc/default/fluentd

if [ -r $DEFAULT ]; then
    set -o allexport
    source $DEFAULT
    set +o allexport
fi

#chown home and data folder
chown -R fluent:fluent /home/fluent
chown -R fluent:fluent /fluentd
chown -R fluent:fluent /usr/local/tomcat


if [ -z "$LOGPATH" ]
then
  echo "LOGPATH is not configure"
  exit 1
fi

if [ -z "$BROKERS" ]
then
  echo "BROKERS is not configure"
  exit 1
fi

if [ -z "$KAFKA_TOPIC" ]
then
  echo "KAFKA_TOPIC is not configure"
  exit 1
fi


export REAL_LOGPATH="$(echo $LOGPATH | sed 's/\//\\\//g')"
sed -i "s/{LOGPATH}/${REAL_LOGPATH}/g" /fluentd/etc/fluent.conf
sed -i "s/{BROKERS}/${BROKERS}/g" /fluentd/etc/fluent.conf
sed -i "s/{KAFKA_TOPIC}/${KAFKA_TOPIC}/g" /fluentd/etc/fluent.conf
sed -i "s/{TENANTID}/${TENANTID}/g" /fluentd/etc/fluent.conf
sed -i "s/{_SYS}/${_SYS}/g" /fluentd/etc/fluent.conf
sed -i "s/{_SAPP}/${_SAPP}/g" /fluentd/etc/fluent.conf
sed -i "s/{_SGRP}/${_SGRP}/g" /fluentd/etc/fluent.conf
sed -i "s/{_ITG}/${_ITG}/g" /fluentd/etc/fluent.conf
sed -i "s/{HOST}/${HOST}/g" /fluentd/etc/fluent.conf

exec su-exec fluent "$@"

生成的Fluentd配置文件如下所示,通過在日誌中添加{SYS}.{HOST} 來標識日誌所屬的系統和POD IP地址,這裏要說明的是,雖然Kubernetes中Pod IP地址發生變化,但是在運行態中,其能夠唯一標識出日誌對應的容器位置,這樣方便開發者定位到輸出日誌的容器。

<source>
  @type tail
  path {LOGPATH}
  pos_file /fluentd/catalina.fluent.pos
  path_key filepath
  tag {SYS}.{HOST}.log
  format multiline
  format_firstline /\d{4}-\d{1,2}-\d{1,2}/
  format1 /(?<message>.*)/
</source>

<filter {SYS}.{HOST}.log>
  @type record_transformer
  <record>
    topic {KAFKA_TOPIC}
  </record>
</filter>


<match {SYS}.{HOST}.log>
  @type kafka_buffered

  # list of seed brokers
  brokers {BROKERS} 

  # buffer settings
  buffer_type file
  buffer_path /home/fluent/td-agent/buffer/td
  flush_interval 3s

  # topic settings
  default_topic {KAFKA_TOPIC}
  #get_kafka_client_log true

  # data type settings
  output_data_type json
  compression_codec gzip

  # producer settings
  max_send_retries 1
  required_acks -1
</match>

<system>
  # equal to -qq option
  log_level info 
</system>

2.3 Kubernetes 應用Yaml文件介紹
完成日誌採集組件鏡像製作後,我們就可以進行部署,下面是Deployment Yaml文件內容,可以看到在Pod中我們啓動了兩個容器,一個爲應用容器,另一個爲日誌採集容器,應用容器共享/usr/local/tomcat/logs目錄給日誌採集容器,日誌採集容器採集該目錄下的所有日誌。
kind: Deployment
apiVersion: extensions/v1beta1
metadata:
name: logtest-app
namespace: xkx
labels:
app:xkx
version: v1
spec:
# replicas: 2
template:
metadata:
labels:
app: logtest-app
spec:
nodeSelector:
node-mode: app1
containers:
- name: logtest-app
image: logtest:3.0
volumeMounts:
- name: logdir
mountPath: /usr/local/tomcat/logs/
ports:
- containerPort: 8080
resources:
limits:
cpu: 800m
memory: 2Gi
requests:
cpu: 400m
memory: 1Gi
imagePullPolicy: IfNotPresent
readinessProbe:
tcpSocket:
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
livenessProbe:
tcpSocket:
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
- name: fluentd-log-collector
image: fluentd-plugin-kafka:v1.6
volumeMounts:
- name: logdir
mountPath: /usr/local/tomcat/logs/
resources:
limits:
cpu: 300m
memory: 256M
requests:
cpu: 150m
memory: 128M
env:
- name: LOGPATH
value: “/usr/local/tomcat/logs/*”
- name: _SYS
value: “xkx”
- name: _SAPP
value: “logtest”
- name: BROKERS
value: “192.168.100.2:6667,192.168.100.3:6667,192.168.100.4:6667,192.168.100.5:6667,192.168.100.6:6667”
- name: _APP
value: “logtest”
- name: KAFKA_TOPIC
value: “testt”
- name: HOST
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: status.podIP
imagePullSecrets:
- name: xkx-rep
volumes:
- name: logdir
emptyDir: {}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章