『宝藏代码胶囊开张啦!』—— 我的 CodeCapsule 来咯!✨写代码不再头疼!我的新站点 CodeCapsule 主打一个 “白菜价”+“量身定制”!无论是卡脖子的毕设/课设/文献复现,需要灵光一现的算法改进,还是想给项目加个“外挂”,这里都有便宜又好用的代码方案等你发现!低成本,高适配,助你轻松通关!速来围观 👉 CodeCapsule官网

容器监控与日志收集实战:构建可观测的Python应用

一、引言:容器时代的可观测性挑战

在容器化和微服务架构日益普及的今天,一个应用往往由数十个甚至上百个容器组成,它们动态地运行在集群中,生命周期短暂。当系统出现故障时,传统的“登录服务器查看日志”方式已难以为继:容器随时可能重启或迁移,日志随之消失;多个服务间的调用链错综复杂,问题定位如同大海捞针。

监控与日志收集成为容器化环境下不可或缺的可观测性基石。监控让我们实时了解容器的资源使用、业务指标;日志收集则帮助我们在海量记录中追溯异常现场。然而,如何为容器应用设计合理的监控指标?如何将分散的日志统一收集、存储和分析?如何确保监控系统本身不成为性能瓶颈?

本文将从一个 Python Flask 应用 的实战出发,通过 Prometheus + Grafana 搭建监控体系,通过 Elasticsearch + Filebeat + Kibana(EFK)搭建日志收集体系,并运用多阶段构建优化镜像体积,带你全面掌握容器监控与日志收集的核心实践。

二、核心概念:可观测性的三大支柱

现代可观测性通常包含三个维度:指标(Metrics)日志(Logs)追踪(Traces)。本文聚焦前两者。

容器应用可观测性

指标监控

日志收集

分布式追踪

Prometheus
采集与存储

Grafana
可视化

Alertmanager
告警

Filebeat/Fluentd
日志采集

Elasticsearch
存储与索引

Kibana
查询与分析

Jaeger/Zipkin
调用链跟踪

2.1 指标监控(Metrics)

指标是随时间变化的数值度量,如 CPU 使用率、请求延迟、错误计数。Prometheus 是目前容器监控的事实标准,通过 Pull 模型定期从目标暴露的 HTTP 端点采集指标,存储在时序数据库中,并通过 PromQL 查询。

2.2 日志收集(Logs)

日志是离散的事件记录,描述应用运行时的详细信息。容器日志默认输出到标准输出和标准错误,由 Docker 收集。为了集中管理,需要使用日志采集代理(如 Filebeat、Fluentd)将日志发送到中央存储(如 Elasticsearch),再通过 Kibana 进行可视化查询。

2.3 指标与日志的协同

指标用于快速发现异常(如 500 错误激增),日志用于深入分析异常原因(查看具体错误堆栈)。两者结合,构成完整的可观测性视图。

三、实战演练:构建可观测的 Python 应用

本节将创建一个简单的 Flask 应用,为其添加 Prometheus 指标,并通过 Docker Compose 部署完整的监控和日志栈。

3.1 项目结构

observability-demo/
├── app/
│   ├── app.py              # Flask应用,包含指标和日志
│   ├── requirements.txt    # Python依赖
│   ├── Dockerfile          # 多阶段构建
│   └── .dockerignore
├── docker-compose.yml      # 编排所有服务
├── prometheus/
│   └── prometheus.yml      # Prometheus配置
├── filebeat/
│   └── filebeat.yml        # Filebeat配置
└── .env                    # 环境变量

3.2 Flask 应用代码

app/app.py:实现一个计数器接口,使用 Prometheus 客户端库暴露指标,并输出 JSON 格式日志。

import os
import time
import random
import logging
import json
from flask import Flask, request, jsonify
from prometheus_client import Counter, Histogram, generate_latest, REGISTRY
from prometheus_client import CONTENT_TYPE_LATEST

# 配置JSON日志
class JsonFormatter(logging.Formatter):
    def format(self, record):
        log_record = {
            'timestamp': self.formatTime(record),
            'level': record.levelname,
            'name': record.name,
            'message': record.getMessage(),
            'module': record.module,
            'funcName': record.funcName,
            'lineNo': record.lineno
        }
        if hasattr(record, 'extra'):
            log_record.update(record.extra)
        return json.dumps(log_record)

# 设置根日志器
handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
logging.basicConfig(level=logging.INFO, handlers=[handler])
logger = logging.getLogger(__name__)

app = Flask(__name__)

# 定义Prometheus指标
REQUEST_COUNT = Counter('http_requests_total', 'Total HTTP requests', ['method', 'endpoint', 'status'])
REQUEST_DURATION = Histogram('http_request_duration_seconds', 'HTTP request duration', ['method', 'endpoint'])

@app.before_request
def before_request():
    request.start_time = time.time()

@app.after_request
def after_request(response):
    # 记录请求时长
    duration = time.time() - request.start_time
    REQUEST_DURATION.labels(method=request.method, endpoint=request.path).observe(duration)
    # 计数
    REQUEST_COUNT.labels(method=request.method, endpoint=request.path, status=response.status_code).inc()
    # 结构化日志
    logger.info('Request processed', extra={
        'method': request.method,
        'path': request.path,
        'status': response.status_code,
        'duration': duration,
        'ip': request.remote_addr
    })
    return response

@app.route('/')
def hello():
    return jsonify({'message': 'Hello, Observability!'})

@app.route('/counter')
def counter():
    # 模拟一些随机延迟
    delay = random.uniform(0.1, 0.5)
    time.sleep(delay)
    # 模拟错误
    if random.random() < 0.05:
        logger.error('Random error occurred', extra={'path': '/counter'})
        return jsonify({'error': 'Internal Server Error'}), 500
    return jsonify({'count': 1})

@app.route('/metrics')
def metrics():
    """Prometheus metrics endpoint"""
    return generate_latest(REGISTRY), 200, {'Content-Type': CONTENT_TYPE_LATEST}

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

app/requirements.txt

flask==2.3.3
prometheus-client==0.19.0
gunicorn==21.2.0

3.3 多阶段构建 Dockerfile

# app/Dockerfile
FROM python:3.11-slim AS builder

WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

FROM python:3.11-slim

# 创建非root用户
RUN addgroup --system --gid 1001 appgroup && \
    adduser --system --uid 1001 --gid 1001 --no-create-home appuser

WORKDIR /app
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH

COPY . .
RUN chown -R appuser:appgroup /app

USER appuser
EXPOSE 5000
HEALTHCHECK CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:5000/')" || exit 1

CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]

app/.dockerignore

__pycache__
*.pyc
.env
.git
README.md
Dockerfile
.dockerignore

3.4 Prometheus 配置

prometheus/prometheus.yml

global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'flask-app'
    static_configs:
      - targets: ['app:5000']
    metrics_path: /metrics

3.5 Filebeat 配置

filebeat/filebeat.yml

filebeat.inputs:
- type: container
  paths:
    - /var/lib/docker/containers/*/*.log
  json.keys_under_root: true
  json.add_error_key: true
  json.message_key: message

output.elasticsearch:
  hosts: ['elasticsearch:9200']
  username: elastic
  password: ${ELASTIC_PASSWORD}

setup.kibana:
  host: 'kibana:5601'

3.6 docker-compose.yml(完整编排)

version: '3.8'

services:
  # Flask应用
  app:
    build: ./app
    container_name: observability-app
    restart: unless-stopped
    ports:
      - "5000:5000"
    networks:
      - observability
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    # 无需额外卷,日志由Docker收集

  # Prometheus监控
  prometheus:
    image: prom/prometheus:v2.47.0
    container_name: prometheus
    restart: unless-stopped
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus-data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
    ports:
      - "9090:9090"
    networks:
      - observability

  # Grafana可视化
  grafana:
    image: grafana/grafana:10.2.2
    container_name: grafana
    restart: unless-stopped
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:
      - grafana-data:/var/lib/grafana
    ports:
      - "3000:3000"
    networks:
      - observability
    depends_on:
      - prometheus

  # Elasticsearch
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
    container_name: elasticsearch
    restart: unless-stopped
    environment:
      - discovery.type=single-node
      - ES_JAVA_OPTS=-Xms512m -Xmx512m
      - ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
      - xpack.security.enabled=true
    volumes:
      - elasticsearch-data:/usr/share/elasticsearch/data
    ports:
      - "9200:9200"
    networks:
      - observability

  # Kibana
  kibana:
    image: docker.elastic.co/kibana/kibana:8.11.0
    container_name: kibana
    restart: unless-stopped
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
      - ELASTICSEARCH_USERNAME=elastic
      - ELASTICSEARCH_PASSWORD=${ELASTIC_PASSWORD}
    ports:
      - "5601:5601"
    networks:
      - observability
    depends_on:
      - elasticsearch

  # Filebeat(日志采集)
  filebeat:
    image: docker.elastic.co/beats/filebeat:8.11.0
    container_name: filebeat
    restart: unless-stopped
    user: root
    volumes:
      - ./filebeat/filebeat.yml:/usr/share/filebeat/filebeat.yml:ro
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - observability
    depends_on:
      - elasticsearch
      - kibana

networks:
  observability:
    driver: bridge

volumes:
  prometheus-data:
  grafana-data:
  elasticsearch-data:

3.7 环境变量文件(.env)

ELASTIC_PASSWORD=your_elastic_password

3.8 构建与运行

# 创建.env文件并设置密码
echo "ELASTIC_PASSWORD=your_elastic_password" > .env

# 启动所有服务
docker-compose up -d

# 查看日志
docker-compose logs -f

# 生成一些测试请求
for i in {1..100}; do curl http://localhost:5000/; curl http://localhost:5000/counter; sleep 0.1; done

# 访问服务
# Prometheus: http://localhost:9090
# Grafana: http://localhost:3000 (admin/admin)
# Kibana: http://localhost:5601

四、进阶优化:监控与日志的最佳实践

4.1 指标命名规范

遵循 Prometheus 命名惯例:

  • 使用蛇形命名(snake_case)
  • 以单位结尾(如 _seconds_total
  • 用标签区分维度,避免指标爆炸

4.2 结构化日志

始终输出 JSON 格式日志,便于 Elasticsearch 自动解析。包含必要字段:timestamp、level、message、service、trace_id 等。

4.3 多阶段构建的收益

对比镜像体积:

构建方式 基础镜像 最终镜像大小
单阶段 python:3.11 912 MB
单阶段 slim python:3.11-slim 412 MB
多阶段 python:3.11-slim 88 MB

体积减少 90%,拉取和启动更快,攻击面更小。

4.4 使用 sidecar 模式收集日志

在生产环境(如 Kubernetes)中,通常使用 sidecar 容器专门负责日志采集,与主应用容器共享日志目录,避免日志丢失。

4.5 监控告警集成

在 Prometheus 中配置告警规则,通过 Alertmanager 发送通知到 Slack、钉钉等。例如,当 5xx 错误率超过 1% 时告警。

4.6 日志生命周期管理

在 Elasticsearch 中配置索引生命周期管理(ILM),自动将旧日志转移到冷存储或删除,避免存储膨胀。

五、效果验证

5.1 Prometheus 指标查询

访问 Prometheus UI(http://localhost:9090),执行查询:

rate(http_requests_total[1m])

应看到请求速率曲线。

5.2 Grafana 仪表板

导入 Prometheus 数据源,创建 Dashboard 展示:

  • 请求速率(QPS)
  • 请求延迟(P99)
  • 错误率
  • 容器资源使用(需额外配置 cAdvisor)

5.3 Kibana 日志查询

在 Kibana 中创建索引模式(如 filebeat-*),进入 Discover 页面,搜索 level:ERROR,应看到应用产生的随机错误日志。

5.4 结构化日志示例

{
  "timestamp": "2024-01-15 10:30:45,123",
  "level": "INFO",
  "name": "__main__",
  "message": "Request processed",
  "method": "GET",
  "path": "/counter",
  "status": 200,
  "duration": 0.234,
  "ip": "192.168.1.100"
}

六、完整代码

6.1 app/app.py

(见上文)

6.2 app/requirements.txt

flask==2.3.3
prometheus-client==0.19.0
gunicorn==21.2.0

6.3 app/Dockerfile

(见上文)

6.4 app/.dockerignore

(见上文)

6.5 prometheus/prometheus.yml

(见上文)

6.6 filebeat/filebeat.yml

(见上文)

6.7 docker-compose.yml

(见上文)

6.8 .env.example

ELASTIC_PASSWORD=your_elastic_password

七、总结与最佳实践清单

通过本文的实战,我们为 Python Flask 应用添加了 Prometheus 指标和结构化日志,并通过 Docker Compose 部署了完整的监控(Prometheus + Grafana)和日志收集(EFK)栈。以下是 容器监控与日志收集的最佳实践清单

  • 指标先行:为关键业务和系统资源定义指标,使用 Prometheus 采集。
  • 日志结构化:输出 JSON 格式日志,包含时间、级别、服务名等字段。
  • 集中存储:使用 Elasticsearch 或 Loki 集中存储日志,便于检索。
  • 可视化:Grafana 展示指标,Kibana 展示日志,建立统一可观测性视图。
  • 多阶段构建:大幅减小镜像体积,加速部署。
  • 资源限制:为监控组件设置资源限制,避免影响业务容器。
  • 告警配置:基于指标设置告警,及时发现异常。
  • 日志轮转:配置 Docker 日志驱动,防止日志撑满磁盘。

容器监控与日志收集是构建可靠系统的基石。掌握这些技术,你将能快速定位问题、评估系统健康度,为微服务架构的稳定运行保驾护航。未来,随着 OpenTelemetry 的普及,指标、日志、追踪将进一步融合,但本文介绍的基础实践仍将是可观测性体系的坚固底座。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐