基于ELKFK打造日志平台

微服务的实例数众多,需要一个强大的日志日志平台,它应具有以下功能:

  • 采集:从服务端进程(k8s的Pod中),自动收集日志

  • 存储:将日志按照时间序列,存储在持久化的介质上,以供未来查找。

  • 检索:根据关键词,时间等条件,方便地检索特定日志内容。

我们将基于ELKFK,打造自己的日志平台。

你可能听说过ELK,那么ELK后面加上的FK是什么呢?

F:Filebeat,轻量级的日志采集插件

K:Kafka,用户缓存日志

日志系统的架构图如下所示:

f

搭建Kafka

Kafka消耗的资源较多,一般多采用独立部署的方式。

这里为了演示方便,我们以单机版为例。

首先下载:

  1. wget https://dlcdn.apache.org/kafka/3.0.0/kafka_2.13-3.0.0.tgz

接着,启动zk

  1. bin/zookeeper-server-start.sh config/zookeeper.properties

最后,启动broker

  1. bin/kafka-server-start.sh config/server.properties

我们来创建topic,供后续使用。

  1. bin/kafka-topics.sh --create --topic k8s-log-homs --partitions 3 --replication-factor 1 --bootstrap-server localhost:9092k8s -> (FileBeat) -> kafka

部署FileBeat

有了Kafka之后,我们在Kubernets集群上部署FileBeat,自动采集日志并发送到Kafka的队列中,配置如下:

  1. ---
  2. apiVersion: v1
  3. kind: ConfigMap
  4. metadata:
  5. name: filebeat-config
  6. namespace: kube-system
  7. labels:
  8. k8s-app: filebeat
  9. data:
  10. filebeat.yml: |-
  11. filebeat.inputs:
  12. - type: container
  13. paths:
  14. - /var/log/containers/homs*.log
  15. fields:
  16. kafka_topic: k8s-log-homs
  17. processors:
  18. - add_kubernetes_metadata:
  19. host: ${NODE_NAME}
  20. matchers:
  21. - logs_path:
  22. logs_path: "/var/log/containers/"
  23. processors:
  24. - add_cloud_metadata:
  25. - add_host_metadata:
  26. cloud.id: ${ELASTIC_CLOUD_ID}
  27. cloud.auth: ${ELASTIC_CLOUD_AUTH}
  28. output:
  29. kafka:
  30. enabled: true
  31. hosts: ["10.1.172.136:9092"]
  32. topic: '%{[fields.kafka_topic]}'
  33. max_message_bytes: 5242880
  34. partition.round_robin:
  35. reachable_only: true
  36. keep-alive: 120
  37. required_acks: 1
  38. ---
  39. apiVersion: apps/v1
  40. kind: DaemonSet
  41. metadata:
  42. name: filebeat
  43. namespace: kube-system
  44. labels:
  45. k8s-app: filebeat
  46. spec:
  47. selector:
  48. matchLabels:
  49. k8s-app: filebeat
  50. template:
  51. metadata:
  52. labels:
  53. k8s-app: filebeat
  54. spec:
  55. serviceAccountName: filebeat
  56. terminationGracePeriodSeconds: 30
  57. hostNetwork: true
  58. dnsPolicy: ClusterFirstWithHostNet
  59. containers:
  60. - name: filebeat
  61. image: docker.elastic.co/beats/filebeat:7.15.2
  62. args: [
  63. "-c", "/etc/filebeat.yml",
  64. "-e",
  65. ]
  66. env:
  67. - name: ELASTIC_CLOUD_ID
  68. value:
  69. - name: ELASTIC_CLOUD_AUTH
  70. value:
  71. - name: NODE_NAME
  72. valueFrom:
  73. fieldRef:
  74. fieldPath: spec.nodeName
  75. securityContext:
  76. runAsUser: 0
  77. # If using Red Hat OpenShift uncomment this:
  78. #privileged: true
  79. resources:
  80. limits:
  81. memory: 200Mi
  82. requests:
  83. cpu: 100m
  84. memory: 100Mi
  85. volumeMounts:
  86. - name: config
  87. mountPath: /etc/filebeat.yml
  88. readOnly: true
  89. subPath: filebeat.yml
  90. - name: data
  91. mountPath: /usr/share/filebeat/data
  92. - name: varlibdockercontainers
  93. mountPath: /var/lib/docker/containers
  94. readOnly: true
  95. - name: varlog
  96. mountPath: /var/log
  97. readOnly: true
  98. volumes:
  99. - name: config
  100. configMap:
  101. defaultMode: 0640
  102. name: filebeat-config
  103. - name: varlibdockercontainers
  104. hostPath:
  105. path: /var/lib/docker/containers
  106. - name: varlog
  107. hostPath:
  108. path: /var/log
  109. # data folder stores a registry of read status for all files, so we don't send everything again on a Filebeat pod restart
  110. - name: data
  111. hostPath:
  112. # When filebeat runs as non-root user, this directory needs to be writable by group (g+w).
  113. path: /var/lib/filebeat-data
  114. type: DirectoryOrCreate
  115. ---
  116. apiVersion: rbac.authorization.k8s.io/v1
  117. kind: ClusterRoleBinding
  118. metadata:
  119. name: filebeat
  120. subjects:
  121. - kind: ServiceAccount
  122. name: filebeat
  123. namespace: kube-system
  124. roleRef:
  125. kind: ClusterRole
  126. name: filebeat
  127. apiGroup: rbac.authorization.k8s.io
  128. ---
  129. apiVersion: rbac.authorization.k8s.io/v1
  130. kind: RoleBinding
  131. metadata:
  132. name: filebeat
  133. namespace: kube-system
  134. subjects:
  135. - kind: ServiceAccount
  136. name: filebeat
  137. namespace: kube-system
  138. roleRef:
  139. kind: Role
  140. name: filebeat
  141. apiGroup: rbac.authorization.k8s.io
  142. ---
  143. apiVersion: rbac.authorization.k8s.io/v1
  144. kind: RoleBinding
  145. metadata:
  146. name: filebeat-kubeadm-config
  147. namespace: kube-system
  148. subjects:
  149. - kind: ServiceAccount
  150. name: filebeat
  151. namespace: kube-system
  152. roleRef:
  153. kind: Role
  154. name: filebeat-kubeadm-config
  155. apiGroup: rbac.authorization.k8s.io
  156. ---
  157. apiVersion: rbac.authorization.k8s.io/v1
  158. kind: ClusterRole
  159. metadata:
  160. name: filebeat
  161. labels:
  162. k8s-app: filebeat
  163. rules:
  164. - apiGroups: [""] # "" indicates the core API group
  165. resources:
  166. - namespaces
  167. - pods
  168. - nodes
  169. verbs:
  170. - get
  171. - watch
  172. - list
  173. - apiGroups: ["apps"]
  174. resources:
  175. - replicasets
  176. verbs: ["get", "list", "watch"]
  177. ---
  178. apiVersion: rbac.authorization.k8s.io/v1
  179. kind: Role
  180. metadata:
  181. name: filebeat
  182. # should be the namespace where filebeat is running
  183. namespace: kube-system
  184. labels:
  185. k8s-app: filebeat
  186. rules:
  187. - apiGroups:
  188. - coordination.k8s.io
  189. resources:
  190. - leases
  191. verbs: ["get", "create", "update"]
  192. ---
  193. apiVersion: rbac.authorization.k8s.io/v1
  194. kind: Role
  195. metadata:
  196. name: filebeat-kubeadm-config
  197. namespace: kube-system
  198. labels:
  199. k8s-app: filebeat
  200. rules:
  201. - apiGroups: [""]
  202. resources:
  203. - configmaps
  204. resourceNames:
  205. - kubeadm-config
  206. verbs: ["get"]
  207. ---
  208. apiVersion: v1
  209. kind: ServiceAccount
  210. metadata:
  211. name: filebeat
  212. namespace: kube-system
  213. labels:
  214. k8s-app: filebeat
  215. ---

配置较多,我们解释一下:

  • 采集/var/log/containers目录下的homs*.log文件名的日志

  • 将这些日志送到k8s-log-homs这个Kafka的topic中

  • 配置Kafka的服务器地址

  • 配置其他所需的权限

实际上,上述配置是在官方原始文件基础上修改的,更多配置可以参考官方文档)。

应用上述配置:

  1. kubectl apply -f filebeat.yaml

然后我们查看Kafka收到的日志:

  1. bin/kafka-console-consumer.sh --bootstrap-server 127.0.0.1:9092 --topic k8s-log-homs --from-beginning

符合预期:

  1. ...
  2. {"@timestamp":"2021-11-15T03:18:26.487Z","@metadata":{"beat":"filebeat","type":"_doc","version":"7.15.2"},"stream":"stdout","message":"2021-11-15 03:18:26.486 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)","log":{"offset":1491,"file":{"path":"/var/log/containers/homs-start-deployment-6878f48fcc-65vcr_default_homs-start-server-d37b0467d097c00bd203089a97df371cdbacc156493f6b2d995b80395caf516f.log"}},"input":{"type":"container"},"agent":{"type":"filebeat","version":"7.15.2","hostname":"minikube","ephemeral_id":"335988de-a165-4070-88f1-08c3d6be7ba5","id":"850b6889-85e0-41c5-8a83-bce344b8b2ec","name":"minikube"},"ecs":{"version":"1.11.0"},"container":{"image":{"name":"coder4/homs-start:107"},"id":"d37b0467d097c00bd203089a97df371cdbacc156493f6b2d995b80395caf516f","runtime":"docker"},"kubernetes":{"pod":{"ip":"172.17.0.3","name":"homs-start-deployment-6878f48fcc-65vcr","uid":"7d925249-2a77-4c28-a462-001d189cdeaa"},"container":{"name":"homs-start-server"},"node":{"name":"minikube","uid":"faec4c1a-9188-408a-aeec-95b24aa47a88","labels":{"node-role_kubernetes_io/control-plane":"","node_kubernetes_io/exclude-from-external-load-balancers":"","kubernetes_io/hostname":"minikube","kubernetes_io/os":"linux","minikube_k8s_io/commit":"a03fbcf166e6f74ef224d4a63be4277d017bb62e","minikube_k8s_io/name":"minikube","minikube_k8s_io/updated_at":"2021_11_05T12_15_23_0700","node-role_kubernetes_io/master":"","beta_kubernetes_io/arch":"amd64","beta_kubernetes_io/os":"linux","minikube_k8s_io/version":"v1.22.0","kubernetes_io/arch":"amd64"},"hostname":"minikube"},"labels":{"app":"homs-start","pod-template-hash":"6878f48fcc"},"namespace_uid":"b880885d-c94a-4cf2-ba2c-1e4cb0d1a691","namespace_labels":{"kubernetes_io/metadata_name":"default"},"namespace":"default","deployment":{"name":"homs-start-deployment"},"replicaset":{"name":"homs-start-deployment-6878f48fcc"}},"orchestrator":{"cluster":{"url":"control-plane.minikube.internal:8443","name":"mk"}},"host":{"mac":["02:42:d5:27:3f:31","c6:64:9d:f9:89:7b","5a:b1:a0:66:ee:d3","46:41:6e:14:85:14","02:42:c0:a8:31:02"],"hostname":"minikube","architecture":"x86_64","os":{"kernel":"5.10.47-linuxkit","codename":"Core","type":"linux","platform":"centos","version":"7 (Core)","family":"redhat","name":"CentOS Linux"},"id":"1820c6c61258c329e88764d3dc4484f3","name":"minikube","containerized":true,"ip":["172.17.0.1","192.168.49.2"]}}
  3. {"@timestamp":"2021-11-15T03:18:26.573Z","@metadata":{"beat":"filebeat","type":"_doc","version":"7.15.2"},"log":{"offset":2111,"file":{"path":"/var/log/containers/homs-start-deployment-6878f48fcc-65vcr_default_homs-start-server-d37b0467d097c00bd203089a97df371cdbacc156493f6b2d995b80395caf516f.log"}},"stream":"stdout","input":{"type":"container"},"host":{"id":"1820c6c61258c329e88764d3dc4484f3","containerized":true,"ip":["172.17.0.1","192.168.49.2"],"name":"minikube","mac":["02:42:d5:27:3f:31","c6:64:9d:f9:89:7b","5a:b1:a0:66:ee:d3","46:41:6e:14:85:14","02:42:c0:a8:31:02"],"hostname":"minikube","architecture":"x86_64","os":{"family":"redhat","name":"CentOS Linux","kernel":"5.10.47-linuxkit","codename":"Core","type":"linux","platform":"centos","version":"7 (Core)"}},"ecs":{"version":"1.11.0"},"agent":{"version":"7.15.2","hostname":"minikube","ephemeral_id":"335988de-a165-4070-88f1-08c3d6be7ba5","id":"850b6889-85e0-41c5-8a83-bce344b8b2ec","name":"minikube","type":"filebeat"},"message":"2021-11-15 03:18:26.573 INFO 1 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext","container":{"id":"d37b0467d097c00bd203089a97df371cdbacc156493f6b2d995b80395caf516f","runtime":"docker","image":{"name":"coder4/homs-start:107"}},"kubernetes":{"replicaset":{"name":"homs-start-deployment-6878f48fcc"},"node":{"name":"minikube","uid":"faec4c1a-9188-408a-aeec-95b24aa47a88","labels":{"node-role_kubernetes_io/control-plane":"","minikube_k8s_io/commit":"a03fbcf166e6f74ef224d4a63be4277d017bb62e","kubernetes_io/os":"linux","kubernetes_io/arch":"amd64","node_kubernetes_io/exclude-from-external-load-balancers":"","beta_kubernetes_io/arch":"amd64","beta_kubernetes_io/os":"linux","minikube_k8s_io/updated_at":"2021_11_05T12_15_23_0700","node-role_kubernetes_io/master":"","kubernetes_io/hostname":"minikube","minikube_k8s_io/name":"minikube","minikube_k8s_io/version":"v1.22.0"},"hostname":"minikube"},"namespace_labels":{"kubernetes_io/metadata_name":"default"},"namespace":"default","deployment":{"name":"homs-start-deployment"},"pod":{"ip":"172.17.0.3","name":"homs-start-deployment-6878f48fcc-65vcr","uid":"7d925249-2a77-4c28-a462-001d189cdeaa"},"labels":{"app":"homs-start","pod-template-hash":"6878f48fcc"},"container":{"name":"homs-start-server"},"namespace_uid":"b880885d-c94a-4cf2-ba2c-1e4cb0d1a691"},"orchestrator":{"cluster":{"url":"control-plane.minikube.internal:8443","name":"mk"}}}
  4. {"@timestamp":"2021-11-15T03:18:27.470Z","@metadata":{"beat":"filebeat","type":"_doc","version":"7.15.2"},"input":{"type":"container"},"orchestrator":{"cluster":{"url":"control-plane.minikube.internal:8443","name":"mk"}},"agent":{"type":"filebeat","version":"7.15.2","hostname":"minikube","ephemeral_id":"335988de-a165-4070-88f1-08c3d6be7ba5","id":"850b6889-85e0-41c5-8a83-bce344b8b2ec","name":"minikube"},"stream":"stdout","message":"2021-11-15 03:18:27.470 INFO 1 --- [ main] com.homs.start.StartApplication : Started StartApplication in 3.268 seconds (JVM running for 3.738)","kubernetes":{"pod":{"name":"homs-start-deployment-6878f48fcc-65vcr","uid":"7d925249-2a77-4c28-a462-001d189cdeaa","ip":"172.17.0.3"},"container":{"name":"homs-start-server"},"labels":{"app":"homs-start","pod-template-hash":"6878f48fcc"},"node":{"labels":{"kubernetes_io/arch":"amd64","node_kubernetes_io/exclude-from-external-load-balancers":"","beta_kubernetes_io/arch":"amd64","kubernetes_io/hostname":"minikube","minikube_k8s_io/name":"minikube","minikube_k8s_io/version":"v1.22.0","kubernetes_io/os":"linux","minikube_k8s_io/commit":"a03fbcf166e6f74ef224d4a63be4277d017bb62e","minikube_k8s_io/updated_at":"2021_11_05T12_15_23_0700","node-role_kubernetes_io/control-plane":"","node-role_kubernetes_io/master":"","beta_kubernetes_io/os":"linux"},"hostname":"minikube","name":"minikube","uid":"faec4c1a-9188-408a-aeec-95b24aa47a88"},"namespace":"default","deployment":{"name":"homs-start-deployment"},"namespace_uid":"b880885d-c94a-4cf2-ba2c-1e4cb0d1a691","namespace_labels":{"kubernetes_io/metadata_name":"default"},"replicaset":{"name":"homs-start-deployment-6878f48fcc"}},"ecs":{"version":"1.11.0"},"host":{"os":{"codename":"Core","type":"linux","platform":"centos","version":"7 (Core)","family":"redhat","name":"CentOS Linux","kernel":"5.10.47-linuxkit"},"id":"1820c6c61258c329e88764d3dc4484f3","containerized":true,"ip":["172.17.0.1","192.168.49.2"],"mac":["02:42:d5:27:3f:31","c6:64:9d:f9:89:7b","5a:b1:a0:66:ee:d3","46:41:6e:14:85:14","02:42:c0:a8:31:02"],"hostname":"minikube","architecture":"x86_64","name":"minikube"},"log":{"offset":2787,"file":{"path":"/var/log/containers/homs-start-deployment-6878f48fcc-65vcr_default_homs-start-server-d37b0467d097c00bd203089a97df371cdbacc156493f6b2d995b80395caf516f.log"}},"container":{"image":{"name":"coder4/homs-start:107"},"id":"d37b0467d097c00bd203089a97df371cdbacc156493f6b2d995b80395caf516f","runtime":"docker"}}

重启deployment

  1. kubectl rollout restart deployment homs-start-deployment

启动ElasticSearch

  1. #!/bin/bash
  2. NAME="elasticsearch"
  3. PUID="1000"
  4. PGID="1000"
  5. VOLUME="$HOME/docker_data/elasticsearch"
  6. mkdir -p $VOLUME
  7. docker ps -q -a --filter "name=$NAME" | xargs -I {} docker rm -f {}
  8. docker run \
  9. --hostname $NAME \
  10. --name $NAME \
  11. --env discovery.type=single-node \
  12. -p 9200:9200 \
  13. -p 9300:9300 \
  14. --detach \
  15. --restart always \
  16. docker.elastic.co/elasticsearch/elasticsearch:7.15.2

启动ElasticSearch

在配置LogStash前,我们先要启动最终的存储,即ElasticSearch。

为了演示方便,我们使用单机模式启动:

  1. #!/bin/bash
  2. NAME="elasticsearch"
  3. PUID="1000"
  4. PGID="1000"
  5. VOLUME="$HOME/docker_data/elasticsearch"
  6. mkdir -p $VOLUME
  7. docker ps -q -a --filter "name=$NAME" | xargs -I {} docker rm -f {}
  8. docker run \
  9. --hostname $NAME \
  10. --name $NAME \
  11. --env discovery.type=single-node \
  12. -p 9200:9200 \
  13. -p 9300:9300 \
  14. --detach \
  15. --restart always \
  16. docker.elastic.co/elasticsearch/elasticsearch:7.15.2

你可以通过curl命令,检查启动是否成功:

  1. curl 127.0.0.1:9200
  2. {
  3. "name" : "elasticsearch",
  4. "cluster_name" : "docker-cluster",
  5. "cluster_uuid" : "yxLELfOmT9OXPXxjh7g7Nw",
  6. "version" : {
  7. "number" : "7.15.2",
  8. "build_flavor" : "default",
  9. "build_type" : "docker",
  10. "build_hash" : "93d5a7f6192e8a1a12e154a2b81bf6fa7309da0c",
  11. "build_date" : "2021-11-04T14:04:42.515624022Z",
  12. "build_snapshot" : false,
  13. "lucene_version" : "8.9.0",
  14. "minimum_wire_compatibility_version" : "6.8.0",
  15. "minimum_index_compatibility_version" : "6.0.0-beta1"
  16. },
  17. "tagline" : "You Know, for Search"

温馨提示:默认情况是没有用户名、密码的,用于生产环境时请务必开启。

启动Logstash

首先,配置logstash.conf,将其放到pipeline子目录下:

  1. input {
  2. kafka {
  3. bootstrap_servers => ["10.1.172.136:9092"]
  4. group_id => "k8s-log-homs-logstash"
  5. topics => ["k8s-log-homs"]
  6. codec => json
  7. }
  8. }
  9. filter {
  10. if [message] =~ "\tat" {
  11. grok {
  12. match => ["message", "^(\tat)"]
  13. add_tag => ["stacktrace"]
  14. }
  15. }
  16. grok {
  17. match => [ "message",
  18. "%{TIMESTAMP_ISO8601:logtime}%{SPACE}%{LOGLEVEL:level}%{SPACE}(?<logmessage>.*)"
  19. ]
  20. }
  21. date {
  22. match => [ "logtime" , "yyyy-MM-dd HH:mm:ss.SSS" ]
  23. }
  24. #mutate {
  25. # remove_field => ["message"]
  26. #}
  27. }
  28. output {
  29. elasticsearch {
  30. hosts => "http://10.1.172.136:9200"
  31. user =>"elastic"
  32. password =>""
  33. index => "k8s-log-homs-%{+YYYY.MM.dd}"
  34. }
  35. }

这里,我们使用了grok来拆分message字段,你可以在使用在线工具)验证规则。

接着,我们启动logstash

  1. #!/bin/bash
  2. NAME="logstash"
  3. PUID="1000"
  4. PGID="1000"
  5. VOLUME="$(pwd)/pipeline"
  6. docker ps -q -a --filter "name=$NAME" | xargs -I {} docker rm -f {}
  7. docker run \
  8. --hostname $NAME \
  9. --name $NAME \
  10. --volume "$VOLUME":/usr/share/logstash/pipeline \
  11. --detach \
  12. --restart always \
  13. docker.elastic.co/logstash/logstash:7.15.2

上述直接挂载了前面配置的pipeline目录。

Kibana

最后,我们启动kibana:

  1. #!/bin/bash
  2. NAME="kibana"
  3. PUID="1000"
  4. PGID="1000"
  5. docker ps -q -a --filter "name=$NAME" | xargs -I {} docker rm -f {}
  6. docker run \
  7. --hostname $NAME \
  8. --name $NAME \
  9. --env "ELASTICSEARCH_HOSTS=http://10.1.172.136:9200" \
  10. -p 5601:5601 \
  11. --detach \
  12. --restart always \
  13. docker.elastic.co/kibana/kibana:7.15.2

如果一切顺利,你会看到如图所示的日志:

f

至此,我们已经成功搭建了自己的日志平台。