在这里插入图片描述

👋 大家好,欢迎来到我的技术博客!
💻 作为一名热爱 Java 与软件开发的程序员,我始终相信:清晰的逻辑 + 持续的积累 = 稳健的成长
📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。
🎯 本文将围绕Gateway这个话题展开,希望能为你带来一些启发或实用的参考。
🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获!


文章目录

Gateway - 容器化日志收集:对接 ELK 或 Loki 🧾

在现代云原生架构中,容器化应用的部署已成为主流趋势。随着应用规模的扩大和复杂度的提升,有效地收集、分析和检索容器化应用的日志变得至关重要。容器化应用的日志通常分散在各个 Pod 中,传统的日志查看方式已难以满足运维和开发的需求。因此,构建一套高效、可靠的容器化日志收集系统显得尤为关键。本文将深入探讨如何在 Kubernetes 环境下,通过 Gateway 技术对接流行的日志收集平台 ELK (Elasticsearch, Logstash, Kibana) 或 Loki (由 Grafana Labs 开发的开源日志聚合系统),实现对容器化应用日志的集中化管理和可视化分析。我们将结合 Java 应用的实际场景,提供详细的实现方案和代码示例。

引言:容器化日志收集的重要性 🤔

在传统的单体应用时代,日志文件通常存储在应用服务器的本地磁盘上,运维人员可以通过直接访问文件系统来查看日志。然而,随着容器化技术(如 Docker)和编排工具(如 Kubernetes)的普及,应用被拆分成一个个独立的容器实例运行,每个容器都拥有自己的生命周期和文件系统。这使得传统的日志查看方式面临巨大挑战:

  • 日志分散: 每个 Pod 都会产生日志,这些日志分布在不同的节点上,难以统一管理。
  • 生命周期短暂: 容器的生命周期短,一旦容器终止,其产生的日志也随之消失。
  • 可扩展性差: 随着应用规模的增长,手动查看日志变得不可行。
  • 查询和分析困难: 难以快速地根据关键词、时间范围、应用模块等条件进行日志搜索和分析。

因此,构建一个高效的日志收集系统,将容器化应用的日志集中存储、索引和展示,成为了现代云原生应用运维不可或缺的一部分。这个系统不仅需要能够实时捕获日志,还需要支持强大的搜索、过滤和可视化能力,以便于问题定位、性能分析和安全审计。

容器化日志收集的核心概念 🧠

1. 容器日志的本质

容器内的应用进程会将输出信息(标准输出 stdout 和标准错误 stderr)写入到容器的标准流中。这些数据在容器的生命周期内存在,当容器退出时,这些日志数据通常会被丢弃,除非被日志驱动程序(Log Driver)捕获并持久化。

2. Kubernetes 中的日志收集

Kubernetes 本身并不直接提供日志收集功能,而是依赖于底层的容器运行时(如 Docker、containerd)和日志驱动程序。Kubernetes 通过 kubelet 组件负责收集容器的日志文件,并将其存储在节点的特定目录下(通常是 /var/log/pods/)。然后,需要一个专门的日志收集器(Log Agent)来读取这些日志文件,并将其发送到中央日志系统。

3. 日志收集代理(Log Agent)

日志收集代理是部署在每个 Kubernetes 节点上的守护进程,负责读取容器日志、进行预处理(如过滤、格式化、标签添加)并将日志转发到下游的日志存储和分析系统。常见的日志收集代理包括:

  • Fluentd: 由 Fluentd 社区开发的开源日志收集器,具有强大的插件生态系统。
  • Fluent Bit: 轻量级的、高性能的日志收集器,适合资源受限的环境。
  • Vector: 一个现代化的、高性能的数据管道,支持多种输入和输出。
  • Promtail: Loki 的官方日志收集器,专门为 Loki 设计。

4. 日志收集系统的选择

选择合适的日志收集系统是构建日志中心化的关键。本文将重点介绍两种主流的日志系统:

ELK Stack (Elasticsearch, Logstash, Kibana)

ELK 是一个经典的日志收集和分析平台,由三个核心组件组成:

  • Elasticsearch: 分布式搜索引擎,负责存储和索引日志数据,提供强大的全文搜索和分析能力。
  • Logstash: 数据处理管道,负责接收、解析、转换日志数据,并将其输出到 Elasticsearch 或其他存储系统。
  • Kibana: 数据可视化工具,提供丰富的图表、仪表板和探索界面,用于分析和展示日志数据。
Loki + Promtail + Grafana

Loki 是一个由 Grafana Labs 开发的开源日志聚合系统,旨在解决 ELK 的一些痛点:

  • Loki: 高效的日志聚合服务,专注于元数据(labels)而不是全文搜索。它与 Prometheus 的设计哲学一致,强调指标和日志的关联性。
  • Promtail: Loki 的日志收集器,类似于 Fluent Bit,负责收集日志并将其发送到 Loki。
  • Grafana: 作为 Loki 的前端界面,提供日志查询、过滤和可视化功能,与 Prometheus 和其他数据源无缝集成。

环境准备与部署 🛠️

1. 前提条件

  • Kubernetes 集群: 已部署并可访问的 Kubernetes 集群(推荐 1.21+ 版本)。
  • kubectl: 已配置好的 Kubernetes 命令行工具。
  • Helm: 已安装 Helm CLI 工具。
  • Docker: 用于构建和推送 Java 应用镜像。

2. 准备 Java 应用

我们首先创建一个简单的 Java Spring Boot 应用来演示日志收集。

Maven 依赖 (pom.xml)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.7.0</version> <!-- 使用兼容的版本 -->
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>log-demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>log-demo</name>
	<description>Demo project for Spring Boot with Log Collection</description>
	<properties>
		<java.version>11</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>
主应用类 (src/main/java/com/example/logdemo/LogDemoApplication.java)
package com.example.logdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class LogDemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(LogDemoApplication.class, args);
	}

}
控制器类 (src/main/java/com/example/logdemo/LogController.java)
package com.example.logdemo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;

import java.util.Random;

@RestController
@RequestMapping("/api")
public class LogController {

    private static final Logger logger = LoggerFactory.getLogger(LogController.class);
    private final Random random = new Random();

    @GetMapping("/hello")
    public String hello(@RequestParam(defaultValue = "World") String name) {
        // 生成不同级别的日志
        logger.info("Handling hello request for name: {}", name);
        logger.debug("Debug info for hello request");
        logger.warn("Warning during hello processing for name: {}", name);

        // 模拟一些业务逻辑
        int statusCode = random.nextInt(100);
        if (statusCode > 90) {
            logger.error("Simulated error occurred during hello processing for name: {}, status code: {}", name, statusCode);
        } else if (statusCode > 80) {
            logger.warn("High latency detected during hello processing for name: {}, status code: {}", name, statusCode);
        }

        return String.format("Hello %s! Status Code: %d", name, statusCode);
    }

    @PostMapping("/users")
    public String createUser(@RequestBody User user) {
        logger.info("Creating user: {}", user.getName());
        logger.debug("User details: id={}, email={}", user.getId(), user.getEmail());

        // 模拟创建用户时的业务逻辑
        try {
            Thread.sleep(random.nextInt(100)); // 模拟耗时操作
            logger.info("User {} created successfully", user.getName());
            return String.format("User %s created with ID: %d", user.getName(), user.getId());
        } catch (InterruptedException e) {
            logger.error("Error creating user {} due to interruption", user.getName(), e);
            Thread.currentThread().interrupt();
            return "Error creating user";
        }
    }

    @GetMapping("/health")
    public String health() {
        logger.info("Health check requested");
        return "OK";
    }

    // 内部类用于模拟用户对象
    public static class User {
        private Long id;
        private String name;
        private String email;

        // Getters and Setters
        public Long getId() {
            return id;
        }

        public void setId(Long id) {
            this.id = id;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getEmail() {
            return email;
        }

        public void setEmail(String email) {
            this.email = email;
        }
    }
}
配置文件 (src/main/resources/application.properties)
server.port=8080
logging.level.root=INFO
# 设置日志格式为 JSON 以方便解析
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
构建镜像

使用 Docker 构建应用镜像。

FROM openjdk:11-jre-slim
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

构建并推送到你的镜像仓库(例如 Docker Hub):

# 假设你的镜像仓库是 example.com
docker build -t example.com/log-demo:latest .
docker push example.com/log-demo:latest

3. 部署 Java 应用到 Kubernetes

部署 Service 和 Deployment
# java-app-deployment.yaml
apiVersion: v1
kind: Service
metadata:
  name: java-app-service
  namespace: default
spec:
  selector:
    app: java-app
  ports:
    - protocol: TCP
      port: 8080
      targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: java-app-deployment
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: java-app
  template:
    metadata:
      labels:
        app: java-app
    spec:
      containers:
      - name: java-app
        image: example.com/log-demo:latest # 替换为你的镜像地址
        ports:
        - containerPort: 8080
        resources:
          requests:
            memory: "64Mi"
            cpu: "250m"
          limits:
            memory: "128Mi"
            cpu: "500m"
kubectl apply -f java-app-deployment.yaml

4. 部署 ELK Stack

使用 Helm 部署 ELK
# 添加 Elastic Helm 仓库
helm repo add elastic https://helm.elastic.co
helm repo update

# 创建命名空间
kubectl create namespace elk

# 部署 Elasticsearch
helm install elasticsearch elastic/elasticsearch \
  --namespace elk \
  --set replicas=1 \
  --set minimumMasterNodes=1 \
  --set resources.requests.memory=1Gi \
  --set resources.limits.memory=2Gi \
  --set persistence.enabled=false

# 部署 Logstash (可选,如果需要处理和转换)
# helm install logstash elastic/logstash \
#   --namespace elk \
#   --set config.logstash.yml="input { stdin { } } output { stdout { } }"

# 部署 Kibana
helm install kibana elastic/kibana \
  --namespace elk \
  --set replicas=1 \
  --set service.type=NodePort \
  --set service.nodePort=30080
访问 Kibana
kubectl get svc -n elk kibana-kibana
# 输出类似:
# NAME                 TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)                      AGE
# kibana-kibana        NodePort   10.100.100.100   <none>        5601:30080/TCP               5m

# 登录 UI: http://<NODE_IP>:30080

5. 部署 Loki + Promtail

使用 Helm 部署 Loki
# 添加 Grafana Helm 仓库
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update

# 创建命名空间
kubectl create namespace loki

# 部署 Loki
helm install loki grafana/loki \
  --namespace loki \
  --set persistence.enabled=false \
  --set singleBinary.replicas=1

# 部署 Promtail
helm install promtail grafana/promtail \
  --namespace loki \
  --set config.lokiAddress=http://loki.loki.svc.cluster.local:3100/loki/api/v1/push
访问 Grafana
kubectl get svc -n loki grafana
# 输出类似:
# NAME      TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)                      AGE
# grafana   NodePort   10.100.100.100   <none>        80:30080/TCP                 5m

# 登录 UI: http://<NODE_IP>:30080
# 默认用户名: admin
# 默认密码: admin (首次登录后建议修改)

6. 部署日志收集代理 (Fluentd / Fluent Bit)

部署 Fluentd
# 创建 Fluentd 配置
kubectl create configmap fluentd-config --from-file=fluentd.conf

# 部署 Fluentd DaemonSet
kubectl apply -f fluentd-daemonset.yaml

Fluentd 配置文件 (fluentd.conf)

# Fluentd 配置文件
<source>
  @type tail
  path /var/log/containers/*.log
  pos_file /var/log/fluentd-containers.log.pos
  tag kubernetes.*
  read_from_head true
  parser_type json
</source>

# 输出到 Elasticsearch
<match kubernetes.**>
  @type elasticsearch
  host elasticsearch.elasticsearch.svc.cluster.local
  port 9200
  logstash_format true
  logstash_prefix kubernetes
  include_tag_key true
  tag_key @log_name
</match>

# 输出到 Loki (可选,如果使用 Loki)
# <match kubernetes.**>
#   @type loki
#   url http://loki.loki.svc.cluster.local:3100/loki/api/v1/push
#   <label>
#     job $tag
#   </label>
# </match>

Fluentd DaemonSet 配置 (fluentd-daemonset.yaml)

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd
  namespace: default
spec:
  selector:
    matchLabels:
      app: fluentd
  template:
    metadata:
      labels:
        app: fluentd
    spec:
      tolerations:
      - key: node-role.kubernetes.io/master
        effect: NoSchedule
      containers:
      - name: fluentd
        image: fluent/fluentd:v1.15-debian-elasticsearch
        volumeMounts:
        - name: varlog
          mountPath: /var/log
        - name: varlibdockercontainers
          mountPath: /var/lib/docker/containers
          readOnly: true
        - name: config-volume
          mountPath: /fluentd/etc
      volumes:
      - name: varlog
        hostPath:
          path: /var/log
      - name: varlibdockercontainers
        hostPath:
          path: /var/lib/docker/containers
      - name: config-volume
        configMap:
          name: fluentd-config
部署 Fluent Bit
# 创建 Fluent Bit 配置
kubectl create configmap fluent-bit-config --from-file=fluent-bit.conf

# 部署 Fluent Bit DaemonSet
kubectl apply -f fluent-bit-daemonset.yaml

Fluent Bit 配置文件 (fluent-bit.conf)

[SERVICE]
    Flush     1
    Log_Level info

[INPUT]
    Name              tail
    Path              /var/log/containers/*.log
    Parser            docker
    Tag               kube
    Refresh_Interval  5
    Exit_On_Eof       On

[FILTER]
    Name                grep
    Match               kube
    Regex               log ^.*ERROR.*$

[OUTPUT]
    Name                es
    Match               kube
    Host                elasticsearch.elasticsearch.svc.cluster.local
    Port                9200
    Index               kubernetes
    Logstash_Format     On
    Logstash_Prefix     kubernetes
    Include_Tag_Key     On
    Tag_Key             @log_name

[OUTPUT]
    Name                loki
    Match               kube
    Url                 http://loki.loki.svc.cluster.local:3100/loki/api/v1/push
    Labels              job=$tag

Fluent Bit DaemonSet 配置 (fluent-bit-daemonset.yaml)

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluent-bit
  namespace: default
spec:
  selector:
    matchLabels:
      app: fluent-bit
  template:
    metadata:
      labels:
        app: fluent-bit
    spec:
      tolerations:
      - key: node-role.kubernetes.io/master
        effect: NoSchedule
      containers:
      - name: fluent-bit
        image: fluent/fluent-bit:latest
        ports:
        - containerPort: 2020
        volumeMounts:
        - name: varlog
          mountPath: /var/log
        - name: varlibdockercontainers
          mountPath: /var/lib/docker/containers
          readOnly: true
        - name: config-volume
          mountPath: /fluent-bit/etc
      volumes:
      - name: varlog
        hostPath:
          path: /var/log
      - name: varlibdockercontainers
        hostPath:
          path: /var/lib/docker/containers
      - name: config-volume
        configMap:
          name: fluent-bit-config

ELK 日志收集实践 🧪

1. 通过 Fluentd 收集日志到 ELK

在上述部署过程中,我们已经通过 Fluentd 将 Kubernetes 日志收集到 Elasticsearch。现在我们来详细看看如何配置和使用。

Fluentd 配置详解

上面的 fluentd.conf 文件定义了两个主要部分:

  1. <source>: 配置 Fluentd 从哪里读取日志。这里我们监听 /var/log/containers/*.log 文件,这是 Kubernetes 保存容器日志的标准位置。parser_type json 表示日志是以 JSON 格式存储的。
  2. <match>: 定义匹配到的日志应该如何处理。第一个 <match> 块将所有 kubernetes.* 类型的日志发送到 Elasticsearch。logstash_format true 表示使用 Logstash 的日志格式,logstash_prefix kubernetes 为索引添加前缀。
在 Kibana 中查看日志
  1. 打开 Kibana UI (http://<NODE_IP>:30080)。
  2. 点击左侧导航栏的 “Stack Management” -> “Index Patterns”。
  3. 点击 “Create index pattern”。
  4. 输入索引模式 kubernetes-* 并点击 “Next step”。
  5. 选择 @timestamp 字段作为时间字段并点击 “Create index pattern”。
  6. 返回左侧导航栏,点击 “Discover”,就可以开始浏览和搜索日志了。
示例日志查询

在 Kibana 的 Discover 页面,你可以进行各种查询和过滤:

  • 按关键字搜索: 在搜索框中输入 ERROR,可以筛选出包含 “ERROR” 的日志条目。
  • 按时间范围筛选: 使用顶部的时间选择器,可以筛选特定时间段的日志。
  • 按标签过滤: 如果你在日志中加入了标签(如 service: java-app),可以通过 service: java-app 进行过滤。

2. 通过 Fluent Bit 收集日志到 ELK

Fluent Bit 是一个轻量级的日志收集器,更适合资源受限的环境。其配置方式与 Fluentd 类似。

Fluent Bit 配置详解

fluent-bit.conf 文件定义了:

  1. [SERVICE]: 全局服务配置,如刷新间隔和日志级别。
  2. [INPUT]: 定义输入源,这里是 tail 插件,用于监听容器日志文件。
  3. [FILTER]: 定义过滤规则。grep 过滤器用于筛选包含 “ERROR” 的日志。
  4. [OUTPUT]: 定义输出目的地。第一个 es 输出将日志发送到 Elasticsearch,第二个 loki 输出将日志发送到 Loki(如果需要的话)。
高级配置示例

为了更精确地处理日志,可以增加更多的过滤和处理步骤。例如,添加一个 modify 过滤器来添加自定义标签:

[FILTER]
    Name                modify
    Match               kube
    Add                 environment production
    Add                 application java-app

3. Java 应用日志格式化

为了让日志更容易被解析和分析,建议将日志格式化为结构化格式(如 JSON)。

修改 Spring Boot 配置

application.properties 中,可以设置日志格式:

# 使用 JSON 格式日志
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
# 或者更复杂的 JSON 格式 (需要额外依赖)
# logging.pattern.console={"timestamp":"%d{yyyy-MM-dd HH:mm:ss.SSS}","level":"%level","logger":"%logger","message":"%msg"}
使用 Logback 配置文件

创建 src/main/resources/logback-spring.xml 文件:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/>

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
            <providers>
                <timestamp/>
                <logLevel/>
                <loggerName/>
                <message/>
                <arguments/>
                <mdc/>
                <stackTrace/>
            </providers>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>

需要添加 logstash-logback-encoder 依赖到 pom.xml

<dependency>
    <groupId>net.logstash.logback</groupId>
    <artifactId>logstash-logback-encoder</artifactId>
    <version>7.2</version> <!-- 请根据实际情况选择版本 -->
</dependency>

这样,应用产生的日志就会是 JSON 格式,更易于 Fluentd/Fluent Bit 解析。

Loki 日志收集实践 🧪

1. 通过 Promtail 收集日志到 Loki

Loki 采用独特的设计理念,它不存储日志的完整内容,而是存储日志的元数据(labels)和日志内容的哈希值。这使得 Loki 在存储和查询方面更加高效。

Promtail 配置详解

Promtail 会自动从 Kubernetes Pod 中提取标签并作为日志的标签。我们之前通过 Helm 安装时已经配置了基本的 Promtail。

自定义 Promtail 配置

如果需要更精细的控制,可以自定义 Promtail 的配置文件:

# promtail-config.yaml
server:
  http_listen_port: 9080
  grpc_listen_port: 0

positions:
  filename: /tmp/positions.yaml

clients:
  - url: http://loki.loki.svc.cluster.local:3100/loki/api/v1/push

scrape_configs:
  - job_name: kubernetes-pods
    kubernetes_sd_configs:
      - role: pod
    relabel_configs:
      - source_labels:
          - __meta_kubernetes_pod_annotation_promtail_io_config
        target_label: __config_hash
      - source_labels:
          - __meta_kubernetes_pod_annotation_promtail_io_job
        target_label: job
      - source_labels:
          - __meta_kubernetes_pod_annotation_promtail_io_instance
        target_label: instance
      - source_labels:
          - __meta_kubernetes_pod_annotation_promtail_io_path
        target_label: __path__
      - source_labels:
          - __meta_kubernetes_pod_annotation_promtail_io_labels
        target_label: __labels__
      - source_labels:
          - __meta_kubernetes_pod_annotation_promtail_io_service
        target_label: service
      - source_labels:
          - __meta_kubernetes_pod_annotation_promtail_io_version
        target_label: version
      - source_labels:
          - __meta_kubernetes_pod_annotation_promtail_io_environment
        target_label: environment
      - source_labels:
          - __meta_kubernetes_pod_annotation_promtail_io_team
        target_label: team
部署自定义 Promtail
# 创建 ConfigMap
kubectl create configmap promtail-config --from-file=promtail-config.yaml

# 更新 Promtail DaemonSet 以使用自定义配置
kubectl patch daemonset promtail -n loki -p '{"spec":{"template":{"spec":{"containers":[{"name":"promtail","volumeMounts":[{"name":"config","mountPath":"/etc/promtail"}]}],"volumes":[{"name":"config","configMap":{"name":"promtail-config"}}]}}}}'

2. 在 Grafana 中使用 Loki

添加 Loki 数据源
  1. 登录 Grafana UI (http://<NODE_IP>:30080)。
  2. 点击左侧导航栏的 “Configuration” -> “Data Sources”。
  3. 点击 “Add data source”。
  4. 选择 “Loki”。
  5. 在 “URL” 字段中填入 http://loki.loki.svc.cluster.local:3100
  6. 点击 “Save & Test” 测试连接。
创建日志查询面板
  1. 在 Grafana 中创建一个新的 Dashboard。
  2. 添加一个新的 Panel。
  3. 选择 “Logs” 作为面板类型。
  4. 在查询编辑器中输入 Loki 查询语句,例如:
    • job="kubelet": 查询 kubelet 的日志。
    • {job="java-app"}: 查询标记为 job=java-app 的日志。
    • {job="java-app"} |= "ERROR": 查询包含 “ERROR” 的日志。
    • {job="java-app"} |= "ERROR" |~ "User.*not found": 查询包含 “ERROR” 并且包含 “User.*not found” 的日志。
使用标签进行过滤

Loki 的强大之处在于其基于标签的查询能力。你可以轻松地通过组合多个标签来过滤日志:

{job="java-app", environment="production"} |= "ERROR" | json

这个查询语句表示:

  1. {job="java-app", environment="production"}: 选择标记为 job=java-appenvironment=production 的日志。
  2. |= “ERROR”: 进一步筛选包含 “ERROR” 的日志。
  3. | json: 将日志内容解析为 JSON 格式(假设日志是 JSON 格式)。

3. Java 应用日志与 Loki 标签集成

为了充分利用 Loki 的标签功能,可以在应用日志中加入自定义标签。

使用 MDC (Mapped Diagnostic Context)

MDC 是 Logback 提供的一个特性,允许在日志中动态添加键值对信息。

修改 LogController.java

package com.example.logdemo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC; // 导入 MDC
import org.springframework.web.bind.annotation.*;

import java.util.Random;
import java.util.UUID;

@RestController
@RequestMapping("/api")
public class LogController {

    private static final Logger logger = LoggerFactory.getLogger(LogController.class);
    private final Random random = new Random();

    @GetMapping("/hello")
    public String hello(@RequestParam(defaultValue = "World") String name) {
        // 设置 MDC 上下文
        MDC.put("requestId", UUID.randomUUID().toString());
        MDC.put("userId", "user-" + random.nextInt(1000));

        try {
            logger.info("Handling hello request for name: {}", name);
            logger.debug("Debug info for hello request");
            logger.warn("Warning during hello processing for name: {}", name);

            int statusCode = random.nextInt(100);
            if (statusCode > 90) {
                logger.error("Simulated error occurred during hello processing for name: {}, status code: {}", name, statusCode);
            } else if (statusCode > 80) {
                logger.warn("High latency detected during hello processing for name: {}, status code: {}", name, statusCode);
            }

            return String.format("Hello %s! Status Code: %d", name, statusCode);
        } finally {
            // 清除 MDC 上下文
            MDC.clear();
        }
    }

    @PostMapping("/users")
    public String createUser(@RequestBody User user) {
        MDC.put("requestId", UUID.randomUUID().toString());
        MDC.put("userId", user.getId() != null ? "user-" + user.getId() : "unknown");

        try {
            logger.info("Creating user: {}", user.getName());
            logger.debug("User details: id={}, email={}", user.getId(), user.getEmail());

            try {
                Thread.sleep(random.nextInt(100));
                logger.info("User {} created successfully", user.getName());
                return String.format("User %s created with ID: %d", user.getName(), user.getId());
            } catch (InterruptedException e) {
                logger.error("Error creating user {} due to interruption", user.getName(), e);
                Thread.currentThread().interrupt();
                return "Error creating user";
            }
        } finally {
            MDC.clear();
        }
    }

    @GetMapping("/health")
    public String health() {
        MDC.put("requestId", UUID.randomUUID().toString());
        logger.info("Health check requested");
        return "OK";
    }

    // 内部类用于模拟用户对象
    public static class User {
        private Long id;
        private String name;
        private String email;

        // Getters and Setters
        public Long getId() {
            return id;
        }

        public void setId(Long id) {
            this.id = id;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getEmail() {
            return email;
        }

        public void setEmail(String email) {
            this.email = email;
        }
    }
}
Promtail 配置中启用 MDC 解析

在 Promtail 配置中,可以使用 json 过滤器来提取 JSON 日志中的字段作为标签:

scrape_configs:
  - job_name: kubernetes-pods
    kubernetes_sd_configs:
      - role: pod
    relabel_configs:
      # ... (其他 relabel_config)
      - source_labels: [__meta_kubernetes_pod_annotation_promtail_io_config]
        target_label: __config_hash
      # ... (其他 relabel_config)
    pipeline_stages:
      - json:
          expressions:
            requestId: requestId
            userId: userId
      - labels:
          requestId:
          userId:

这样,Promtail 会从日志的 JSON 结构中提取 requestIduserId 字段,并将其作为标签添加到日志中。

实际案例:Java 应用日志收集与分析 🧪

1. 模拟生产环境场景

假设我们有一个电商应用,其中包含用户服务、订单服务和支付服务。每个服务都有自己的 Java 应用,并部署在 Kubernetes 集群中。我们需要收集所有服务的日志,并能够快速定位问题。

部署多服务应用
# multi-service-deployment.yaml
apiVersion: v1
kind: Service
metadata:
  name: user-service
  namespace: default
spec:
  selector:
    app: user-service
  ports:
    - protocol: TCP
      port: 8080
      targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service-deployment
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
        service: user-service
        environment: production
    spec:
      containers:
      - name: user-service
        image: example.com/user-service:latest
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: order-service
  namespace: default
spec:
  selector:
    app: order-service
  ports:
    - protocol: TCP
      port: 8080
      targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service-deployment
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
        service: order-service
        environment: production
    spec:
      containers:
      - name: order-service
        image: example.com/order-service:latest
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: payment-service
  namespace: default
spec:
  selector:
    app: payment-service
  ports:
    - protocol: TCP
      port: 8080
      targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: payment-service-deployment
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: payment-service
  template:
    metadata:
      labels:
        app: payment-service
        service: payment-service
        environment: production
    spec:
      containers:
      - name: payment-service
        image: example.com/payment-service:latest
        ports:
        - containerPort: 8080
kubectl apply -f multi-service-deployment.yaml
为每个服务添加标签

Deploymentmetadata.labels 中,我们已经添加了 serviceenvironment 标签。这些标签将被 Promtail 提取并作为日志的标签。

在 Grafana 中创建仪表板
  1. 创建一个新的 Dashboard。
  2. 添加一个 “Logs” panel,查询所有服务的日志:
    {service=~"user-service|order-service|payment-service"}
    
  3. 添加一个 “Graph” panel,统计错误率:
    rate({service=~"user-service|order-service|payment-service"} |= "ERROR" [1m])
    
  4. 添加一个 “Table” panel,展示最近的错误日志:
    {service=~"user-service|order-service|payment-service"} |= "ERROR" | json
    

2. 故障排查示例

假设在生产环境中,用户报告说支付失败。我们可以通过日志分析快速定位问题。

步骤 1: 使用 Grafana 查找相关日志
  1. 在 Grafana 的 Logs panel 中,输入查询语句:
    {service="payment-service"} |= "ERROR" | json
    
  2. 查看返回的日志,找出错误发生的时间点和具体错误信息。
步骤 2: 通过标签进一步细化

如果错误信息不够明确,可以结合用户 ID 或订单 ID 进行查询:

{service="payment-service", userId="user-12345"} |= "ERROR" | json

或者查询特定订单:

{service="payment-service", orderId="order-67890"} |= "ERROR" | json
步骤 3: 深入分析

找到具体的错误日志后,可以查看其上下文,比如:

{service="payment-service", orderId="order-67890"} | json

或者查看一段时间内的日志:

{service="payment-service", orderId="order-67890"} | json | time > 1678886400

3. 性能监控与告警

在 Grafana 中设置告警
  1. 在 “Alerting” -> “Alert rules” 中创建一个新的告警规则。
  2. 选择 “Query” 类型。
  3. 输入查询语句,例如:
    rate({service="payment-service"} |= "ERROR" [1m]) > 0.1
    
  4. 设置告警条件和通知渠道(如 Slack, Email)。
使用 Prometheus 与 Loki 结合

虽然 Loki 本身不提供指标收集功能,但可以结合 Prometheus 和 Grafana 来实现全面的监控。Prometheus 可以收集应用的指标(如响应时间、吞吐量),而 Loki 则收集日志。两者结合可以提供更完整的可观测性。

GitOps 与日志收集集成 🧠

1. GitOps 管理日志收集配置

通过 GitOps,可以将日志收集相关的配置(如 Fluentd/Fluent Bit 配置、Promtail 配置、Kibana 索引模式、Grafana dashboard 配置)纳入版本控制。

示例 Git 仓库结构
log-collection-configs/
├── README.md
├── applications/
│   ├── elasticsearch-app.yaml
│   ├── kibana-app.yaml
│   ├── loki-app.yaml
│   └── promtail-app.yaml
├── environments/
│   ├── development/
│   │   ├── fluentd/
│   │   │   ├── fluentd-config.yaml
│   │   │   └── fluentd-daemonset.yaml
│   │   └── promtail/
│   │       ├── promtail-config.yaml
│   │       └── promtail-daemonset.yaml
│   ├── staging/
│   │   └── ... (类似 development)
│   └── production/
│       └── ... (类似 development)
└── utils/
    └── kustomization.yaml
ArgoCD Application 示例
# argocd-log-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: log-collection-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: <YOUR_LOG_CONFIG_REPO_URL>
    targetRevision: HEAD
    path: environments/production
  destination:
    server: https://kubernetes.default.svc
    namespace: default
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

2. 多环境配置管理

通过 Git 仓库的不同分支或目录,可以轻松管理不同环境下的日志收集配置:

  • 分支模型: main (生产), staging, development
  • 目录模型: environments/prod, environments/staging, environments/dev

例如,在 environments/production 下,可能会有更严格的日志保留策略和更高的资源限制。

高级日志处理与分析技巧 🧠

1. 日志预处理

使用 Fluentd/Fluent Bit 进行日志过滤和重写

在日志发送到 Elasticsearch 或 Loki 之前,可以使用过滤器进行处理:

  • 过滤: 移除不需要的日志条目。
  • 重写: 修改日志字段的值。
  • 添加标签: 根据日志内容动态添加标签。
示例:使用 Fluentd 过滤器
# 过滤掉包含 "DEBUG" 的日志
<filter kubernetes.**>
  @type grep
  exclude1 message ^.*DEBUG.*$
</filter>

# 添加自定义标签
<filter kubernetes.**>
  @type record_transformer
  enable_ruby true
  <record>
    environment production
    application ${record["kubernetes"]["namespace"]}
  </record>
</filter>

2. 日志聚合与统计

使用 Kibana 聚合分析

Kibana 提供了强大的聚合功能,可以对日志数据进行统计分析:

  • Terms Aggregation: 统计某个字段的不同值及其出现次数。
  • Date Histogram Aggregation: 按时间窗口统计日志数量。
  • Average Aggregation: 计算数值字段的平均值。
使用 Grafana 聚合分析

Grafana 可以通过 PromQL 查询 Loki 的日志数据,实现复杂的聚合分析:

# 计算每分钟的错误数
rate({job="java-app"} |= "ERROR" [1m])

# 按服务统计错误数
count by (service) ({job="java-app"} |= "ERROR")

# 计算响应时间的 95% 分位数
histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service))

3. 日志安全与合规

日志脱敏

对于包含敏感信息(如用户密码、信用卡号)的日志,需要进行脱敏处理:

  • Fluentd/Fluent Bit 过滤器: 使用正则表达式匹配并替换敏感字段。
  • 应用层: 在应用代码中避免记录敏感信息。
日志保留策略

制定合理的日志保留策略,平衡存储成本和审计需求:

  • 短期保留: 最近 7 天的日志,用于日常运维。
  • 中期保留: 1 个月的日志,用于问题排查。
  • 长期保留: 1 年的日志,用于合规审计。

性能优化与监控 🛠️

1. 日志收集器性能优化

资源限制

为日志收集器设置合理的 CPU 和内存限制,避免过度消耗节点资源:

# 示例:为 Fluentd 设置资源限制
resources:
  requests:
    memory: "128Mi"
    cpu: "100m"
  limits:
    memory: "256Mi"
    cpu: "200m"
配置优化
  • 缓冲区大小: 调整缓冲区大小以平衡延迟和吞吐量。
  • 批处理: 合理设置批处理大小,提高发送效率。
  • 重试机制: 配置合理的重试策略,应对临时网络问题。

2. 日志存储性能优化

Elasticsearch 优化
  • 分片策略: 合理规划索引分片,避免单个分片过大。
  • 索引模板: 使用索引模板统一配置索引设置。
  • 压缩: 启用索引压缩以节省存储空间。
Loki 优化
  • 标签优化: 避免过多的唯一标签值,导致索引膨胀。
  • 存储后端: 选择合适的存储后端(如 S3、GCS)。
  • 缓存: 合理配置缓存策略。

3. 监控与告警

监控日志收集器
  • 资源使用率: 监控 Fluentd/Fluent Bit 的 CPU 和内存使用情况。
  • 日志处理速率: 监控日志的接收和发送速率。
  • 错误率: 监控日志发送失败的错误率。
监控日志存储系统
  • Elasticsearch: 监控集群健康状态、索引大小、查询性能。
  • Loki: 监控存储容量、查询延迟、日志摄入速率。
Grafana 告警规则

在 Grafana 中创建告警规则,及时发现潜在问题:

# 告警:日志摄入速率下降
rate({job="fluent-bit"} [5m]) < 100

# 告警:Elasticsearch 集群状态异常
elasticsearch_cluster_health_status{cluster="elasticsearch"} == 0

# 告警:Loki 存储空间不足
loki_storage_size_bytes / 1024 / 1024 / 1024 > 80

故障排除与调试 🛠️

1. 常见问题排查

问题: 日志未被收集
  • 检查日志收集器状态: 确保 Fluentd/Fluent Bit Pod 正常运行。
  • 检查日志路径: 确认日志文件确实存在于 /var/log/containers/ 目录下。
  • 检查配置文件: 确认 Fluentd/Fluent Bit 的配置文件正确无误。
  • 查看日志收集器日志: 使用 kubectl logs 查看日志收集器的内部日志。
问题: 日志发送失败
  • 检查网络连通性: 确保日志收集器能够访问 Elasticsearch/Loki。
  • 检查目标服务状态: 确认 Elasticsearch/Loki 服务正常运行。
  • 查看错误日志: 检查日志收集器的错误日志,了解失败原因。
问题: 日志格式不正确
  • 检查日志格式: 确认应用输出的日志格式符合预期。
  • 检查解析器: 确认 Fluentd/Fluent Bit 的解析器配置正确。
  • 使用测试工具: 可以使用 fluent-bit 命令行工具测试日志解析。

2. 调试技巧

使用 kubectl exec 进入 Pod
kubectl exec -it <POD_NAME> -- /bin/bash
查看日志文件
# 查看 Pod 日志文件
kubectl exec -it <POD_NAME> -- ls /var/log/containers/
kubectl exec -it <POD_NAME> -- cat /var/log/containers/<CONTAINER_NAME>.log
使用 curl 测试服务
# 从 Pod 内部测试服务
kubectl exec -it <POD_NAME> -- curl -v http://elasticsearch.elasticsearch.svc.cluster.local:9200
kubectl exec -it <POD_NAME> -- curl -v http://loki.loki.svc.cluster.local:3100/ready

安全性考量 🔐

1. 日志传输安全

  • TLS 加密: 在日志收集器和目标系统之间启用 TLS 加密。
  • 认证授权: 配置必要的认证和授权机制,防止未授权访问。

2. 日志存储安全

  • 访问控制: 严格控制对日志存储系统的访问权限。
  • 加密存储: 对存储的日志数据进行加密。
  • 备份策略: 制定日志数据的备份和恢复策略。

3. 敏感信息保护

  • 日志脱敏: 在日志中移除或替换敏感信息。
  • 审计日志: 记录所有对日志系统的访问操作。

总结与展望 📈

通过本文的详细介绍,我们深入了解了在 Kubernetes 环境下如何通过 Gateway 技术对接 ELK 或 Loki 来实现容器化应用日志的集中化收集、管理和分析。从基础概念到实际部署,再到高级实践和故障排除,我们构建了一个完整的日志收集和分析体系。

容器化日志收集的重要性不言而喻。它不仅解决了传统日志管理的诸多痛点,还为运维和开发人员提供了强大的工具来洞察应用的行为和性能。无论是使用经典的 ELK Stack 还是现代化的 Loki + Promtail 方案,关键在于选择合适的工具组合,并根据实际需求进行定制化配置。

通过 GitOps 的方式管理日志收集配置,进一步提升了配置的可追溯性、可复现性和安全性。这对于大规模、多环境的云原生应用运维至关重要。

未来,随着可观测性领域的不断发展,我们可以期待更多创新的技术和工具出现。例如,更智能的日志分析算法、更直观的可视化界面、以及更完善的与 AI/ML 技术的融合,都将为日志收集和分析带来新的可能性。

通过构建高效、可靠、安全的日志收集系统,我们能够更好地保障应用的稳定运行,快速定位和解决问题,最终提升用户体验和业务价值。


参考资料与链接:


希望这篇博客能帮助你更好地理解和实践容器化日志收集!🚀

注意: 本文中提及的所有代码示例和配置文件均为演示目的,实际使用时请根据你的环境和需求进行调整。同时,请确保你拥有相应的权限来部署和管理 Kubernetes 资源。


🙌 感谢你读到这里!
🔍 技术之路没有捷径,但每一次阅读、思考和实践,都在悄悄拉近你与目标的距离。
💡 如果本文对你有帮助,不妨 👍 点赞、📌 收藏、📤 分享 给更多需要的朋友!
💬 欢迎在评论区留下你的想法、疑问或建议,我会一一回复,我们一起交流、共同成长 🌿
🔔 关注我,不错过下一篇干货!我们下期再见!✨

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐