Docker容器"僵尸状态"问题排查与自动重启方案

Docker容器"僵尸状态"问题排查与自动重启方案

问题背景

在运行基于BSC(币安智能链)的节点集群时,遇到了一个诡异的问题:归档节点(archive node)的Docker容器显示为运行状态,但实际上内部的geth进程已经停止工作。

环境信息

  • 节点类型: 5个验证节点 + 1个归档节点
  • Docker镜像: myorg/chain:0.0.4
  • 同步模式: full sync + archive模式
  • 运行时长: 2天

问题现象

1. 容器状态显示正常

 
 
bash
$ sudo docker ps
CONTAINER ID   IMAGE                        STATUS      NAMES
6f96594ef9a4   myorg/chain:0.0.4            Up 2 days   bsc-archive-1

容器状态显示 Up 2 days,看起来一切正常。

2. 资源使用异常

 
 
bash
$ sudo docker stats
CONTAINER ID   NAME            CPU %     MEM USAGE / LIMIT
6f96594ef9a4   bsc-archive-1   0.00%     0B / 0B

关键异常点

  • CPU使用率:0%
  • 内存使用:0B
  • 网络I/O:0B
  • 磁盘I/O:0B

这明显不正常!归档节点正常运行时应该有持续的资源消耗。

3. 无法执行命令

 
 
bash
$ sudo docker exec -it 6f96594ef9a4 geth attach /root/.ethereum/geth.ipc --exec "eth.blockNumber"
cannot exec in a stopped state

$ sudo docker exec -it 6f96594ef9a4 ps aux
cannot exec in a stopped state

虽然 docker ps 显示容器运行中,但无法在容器内执行任何命令。

4. 日志停滞

最后一条日志停留在昨天:

 
 
log
INFO [01-08|22:31:37.346] Re-queue blocks    number=1,681,468 
hash=0x2a9ad1f2fb1235dd3a84a06f42f85ef853049acbde9e1062e90b50a8a167e63e

日志停在了区块重组(re-queue blocks)操作,之后再无输出。

问题分析

什么是"僵尸容器"状态?

这是一种特殊的容器故障模式:

 
 
┌─────────────────────────────────────┐
│   Docker Container (Running)        │
│  ┌───────────────────────────────┐  │
│  │  tini (PID 1) - 存活 ✓        │  │
│  │  ┌─────────────────────────┐  │  │
│  │  │  geth 进程 - 已挂 ✗     │  │  │
│  │  └─────────────────────────┘  │  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘

关键点

  1. 容器的init进程(tini)仍在运行 → Docker认为容器是健康的
  2. geth主进程已经停止或卡死 → 实际服务已失效
  3. Docker没有进程级别的健康检查 → 无法发现内部进程故障

为什么会出现这种情况?

从日志分析,可能的原因包括:

1. 区块重组时数据库问题

 
 
Re-queue blocks number=1,681,468

这个操作可能触发了:

  • 数据库锁死(database lock)
  • 数据损坏(corruption)
  • 状态树重建失败

2. 内存不足

配置中设置了:

 
 
yaml
--cache 8000  # 8GB缓存
--gcmode archive  # 归档模式,不清理历史状态
--txlookuplimit 0  # 无限制事务索引

归档模式 + 大缓存 + 全量索引,内存需求极高,可能触发OOM。

3. 磁盘I/O瓶颈

从统计数据看,validator1的磁盘读取达到了345GB,archive节点可能在密集I/O时遇到了问题。

为什么配置了 restart: always 却不重启?

这是Docker的设计机制:

 
 
yaml
restart: always  # 仅在容器退出时重启

restart策略只关注容器退出

  • 容器崩溃(exit code ≠ 0)→ 重启 ✓
  • 容器正常退出(exit code = 0)→ 重启 ✓
  • 容器运行但内部进程挂了 → 不重启

Docker无法感知容器内部进程的健康状态!

解决方案

方案一:添加健康检查(HealthCheck)

修改 docker-compose.yml,添加健康检查配置:

 
 
yaml
archive:
  image: myorg/chain:0.0.4
  user: "1000:1000"
  command:
    - --datadir
    - /data
    - --gcmode
    - archive
    # ... 其他参数 ...
  
  restart: unless-stopped
  
  # 核心配置:健康检查
  healthcheck:
    test: ["CMD-SHELL", "geth attach --exec 'eth.blockNumber' /data/geth.ipc || exit 1"]
    interval: 60s       # 每60秒检查一次
    timeout: 30s        # 30秒超时
    retries: 3          # 连续失败3次才标记为unhealthy
    start_period: 300s  # 启动后5分钟才开始检查
  
  volumes:
    - ./data/archive-data:/data
    - ./logs/archive:/data/logs
  
  network_mode: "host"
  stop_grace_period: 120s

工作原理

  1. Docker每60秒尝试连接geth IPC并获取区块高度
  2. 如果命令失败(进程卡死/IPC不响应),标记为失败
  3. 连续3次失败后,容器状态变为 unhealthy

查看健康状态

 
 
bash
$ sudo docker ps
CONTAINER ID   STATUS                    NAMES
6f96594ef9a4   Up 2 hours (healthy)      bsc-archive-1

方案二:自动治愈容器(Autoheal)

仅有healthcheck还不够,因为Docker不会自动重启 unhealthy 的容器。需要添加一个监控容器:

 
 
yaml
services:
  archive:
    # ... 上面的配置 ...
    healthcheck:
      test: ["CMD-SHELL", "geth attach --exec 'eth.blockNumber' /data/geth.ipc || exit 1"]
      interval: 60s
      timeout: 30s
      retries: 3
      start_period: 300s
  
  validator1:
    # ... 其他验证节点 ...
  
  # 自动治愈容器
  autoheal:
    image: willfarrell/autoheal:latest
    container_name: autoheal
    restart: always
    environment:
      - AUTOHEAL_CONTAINER_LABEL=all  # 监控所有容器
      - AUTOHEAL_INTERVAL=10          # 每10秒检查一次
      - AUTOHEAL_START_PERIOD=300     # 启动后5分钟才开始监控
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock  # 访问Docker API

工作流程

 
 
┌─────────────────────────────────────────────────┐
│  1. Archive容器geth进程卡死                     │
│     ↓                                            │
│  2. HealthCheck连续3次失败 (3分钟)              │
│     ↓                                            │
│  3. 容器状态变为 unhealthy                       │
│     ↓                                            │
│  4. Autoheal检测到unhealthy状态                 │
│     ↓                                            │
│  5. 自动执行 docker restart bsc-archive-1       │
│     ↓                                            │
│  6. 容器重启,geth进程恢复正常                   │
└─────────────────────────────────────────────────┘

方案三:自定义监控脚本(推荐用于生产环境)

如果不想依赖第三方镜像,可以自己写监控脚本。

创建 ./scripts/container-monitor.sh

 
 
bash
#!/bin/bash

# 配置
CONTAINER_NAME="bsc-archive-1"
CHECK_INTERVAL=60
LOG_FILE="/data/chain/logs/monitor.log"

# 日志函数
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

# 检查容器健康状态
check_container_health() {
    local health=$(docker inspect --format='{{.State.Health.Status}}' "$CONTAINER_NAME" 2>/dev/null)
    echo "$health"
}

# 检查geth进程是否存在
check_geth_process() {
    docker exec "$CONTAINER_NAME" pgrep -x "geth" > /dev/null 2>&1
    return $?
}

# 检查geth是否响应
check_geth_responsive() {
    timeout 30 docker exec "$CONTAINER_NAME" \
        geth attach --exec 'eth.blockNumber' /data/geth.ipc > /dev/null 2>&1
    return $?
}

# 重启容器
restart_container() {
    log "🔄 重启容器: $CONTAINER_NAME"
    docker restart "$CONTAINER_NAME"
    
    # 等待容器启动
    sleep 30
    
    # 验证重启是否成功
    if docker ps | grep -q "$CONTAINER_NAME"; then
        log "✅ 容器重启成功"
    else
        log "❌ 容器重启失败,请人工介入!"
        # 可以在这里添加告警通知
    fi
}

# 主监控循环
main() {
    log "🚀 监控服务启动,监控容器: $CONTAINER_NAME"
    
    while true; do
        # 检查容器是否存在
        if ! docker ps | grep -q "$CONTAINER_NAME"; then
            log "⚠️  容器不存在或已停止"
            sleep "$CHECK_INTERVAL"
            continue
        fi
        
        # 方法1:检查健康状态(如果配置了healthcheck)
        health=$(check_container_health)
        if [ "$health" == "unhealthy" ]; then
            log "❌ 容器健康检查失败 (状态: unhealthy)"
            restart_container
            sleep 120  # 重启后等待2分钟再继续监控
            continue
        fi
        
        # 方法2:检查geth进程
        if ! check_geth_process; then
            log "❌ geth进程不存在"
            restart_container
            sleep 120
            continue
        fi
        
        # 方法3:检查geth响应性
        if ! check_geth_responsive; then
            log "❌ geth进程无响应"
            restart_container
            sleep 120
            continue
        fi
        
        # 一切正常
        log "✓ 容器运行正常 (health: $health)"
        
        sleep "$CHECK_INTERVAL"
    done
}

# 捕获退出信号
trap 'log "📛 监控服务停止"; exit 0' SIGTERM SIGINT

# 启动监控
main

使用systemd管理监控脚本

创建 /etc/systemd/system/chain-monitor.service

 
 
ini
[Unit]
Description=BSC Container Health Monitor
After=docker.service
Requires=docker.service

[Service]
Type=simple
User=ubuntu
WorkingDirectory=/data/chain
ExecStart=/bin/bash /data/chain/scripts/container-monitor.sh
Restart=always
RestartSec=10

# 日志配置
StandardOutput=append:/data/chain/logs/monitor-service.log
StandardError=append:/data/chain/logs/monitor-service.log

[Install]
WantedBy=multi-user.target

启用服务

 
 
bash
# 1. 创建脚本
sudo mkdir -p /data/chain/scripts
sudo vim /data/chain/scripts/container-monitor.sh
# 粘贴上面的脚本内容

# 2. 设置权限
sudo chmod +x /data/chain/scripts/container-monitor.sh
sudo chown ubuntu:ubuntu /data/chain/scripts/container-monitor.sh

# 3. 创建systemd服务
sudo vim /etc/systemd/system/chain-monitor.service
# 粘贴服务配置

# 4. 重载并启动
sudo systemctl daemon-reload
sudo systemctl enable chain-monitor
sudo systemctl start chain-monitor

# 5. 查看状态
sudo systemctl status chain-monitor

# 6. 查看日志
sudo journalctl -u chain-monitor -f

方案对比

方案 优点 缺点 适用场景
HealthCheck 简单,Docker原生支持 只标记状态,不会自动重启 配合其他方案使用
Autoheal容器 配置简单,开箱即用 依赖第三方镜像,需要Docker socket权限 测试/开发环境
自定义脚本 完全可控,可自定义逻辑,无第三方依赖 需要额外维护脚本 生产环境(推荐)

完整配置示例

docker-compose.yml(推荐配置)

 
 
yaml
version: '3.8'

services:
  archive:
    image: myorg/chain:0.0.4
    container_name: bsc-archive-1
    user: "1000:1000"
    command:
      - --datadir
      - /data
      - --http
      - --http.addr
      - "0.0.0.0"
      - --http.port
      - "8555"
      - --http.api
      - "eth,net,web3,debug,admin,txpool"
      - --syncmode
      - full
      - --gcmode
      - archive
      - --cache
      - "4000"  # 降低缓存,避免OOM (原来是8000)
      - --txlookuplimit
      - "0"
      - --ipcpath
      - /data/geth.ipc
      - --log.file=/data/logs/node.log
      - --log.rotate
      - --log.maxsize=100
      - --log.maxbackups=10
      - --verbosity=3  # 降低日志级别 (原来是5)
    
    volumes:
      - ./data/archive-data:/data
      - ./logs/archive:/data/logs
      - ./config:/chain/config
    
    network_mode: "host"
    restart: unless-stopped
    stop_grace_period: 120s
    
    # 健康检查配置
    healthcheck:
      test: ["CMD-SHELL", "geth attach --exec 'eth.blockNumber' /data/geth.ipc || exit 1"]
      interval: 60s
      timeout: 30s
      retries: 3
      start_period: 300s
    
    # 资源限制(可选)
    deploy:
      resources:
        limits:
          memory: 10G
        reservations:
          memory: 6G
    
    logging:
      driver: "json-file"
      options:
        max-size: "100m"
        max-file: "10"
  
  # 验证节点配置类似...
  validator1:
    # ... 配置 ...
  
  # 自动治愈容器
  autoheal:
    image: willfarrell/autoheal:latest
    container_name: autoheal
    restart: always
    environment:
      - AUTOHEAL_CONTAINER_LABEL=all
      - AUTOHEAL_INTERVAL=10
      - AUTOHEAL_START_PERIOD=300
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

验证和测试

1. 应用新配置

 
 
bash
# 停止现有容器
sudo docker-compose down

# 启动新配置
sudo docker-compose up -d

# 查看容器状态
sudo docker ps

2. 查看健康状态

 
 
bash
# 方法1:docker ps 会显示健康状态
$ sudo docker ps
CONTAINER ID   STATUS                        NAMES
6f96594ef9a4   Up 5 minutes (healthy)        osc-archive-1

# 方法2:查看详细健康检查信息
$ sudo docker inspect osc-archive-1 | grep -A 20 "Health"

3. 模拟故障测试

 
 
bash
# 手动杀掉geth进程,测试自动恢复
sudo docker exec -it bsc-archive-1 pkill geth

# 观察容器状态变化
watch -n 5 'sudo docker ps | grep archive'

# 大约3-5分钟后,应该看到容器自动重启

4. 查看autoheal日志

 
 
bash
# 实时查看autoheal的工作日志
sudo docker logs -f autoheal

# 应该能看到类似输出:
# [2026-01-09 15:30:45] Container bsc-archive-1 is unhealthy - Restarting
# [2026-01-09 15:30:50] Container bsc-archive-1 restarted successfully

额外优化建议

1. 降低资源配置

原配置可能导致内存不足:

 
 
yaml
--cache 8000  # 8GB → 改为 4000 (4GB)
--verbosity 5  # debug → 改为 3 (info)

2. 添加资源限制

防止单个容器耗尽系统资源:

 
 
yaml
deploy:
  resources:
    limits:
      memory: 10G
      cpus: '4'

3. 定期清理日志

 
 
bash
# 添加到crontab
0 2 * * * find /data/osc/logs -name "*.log" -mtime +7 -delete

4. 监控告警(可选)

在监控脚本中添加告警通知:

 
 
bash
# 发送邮件告警
send_alert() {
    echo "Archive node restarted at $(date)" | \
        mail -s "OSC Node Alert" admin@example.com
}

# 发送Telegram通知
send_telegram() {
    curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
        -d chat_id="${CHAT_ID}" \
        -d text="⚠️ OSC Archive容器已自动重启"
}

总结

问题本质

Docker容器的"僵尸状态"是由于:

  1. Docker只监控容器的init进程,不关心内部应用进程
  2. 当geth进程挂掉但容器init进程存活时,Docker认为一切正常
  3. restart: always 只在容器退出时生效,无法处理内部进程故障

解决关键

实现完整的自动恢复机制需要三个组件:

 
 
HealthCheck → 检测问题
     ↓
Autoheal/Script → 执行重启
     ↓
restart: always → 确保重启

最佳实践

生产环境推荐配置

  1. ✅ 配置 healthcheck - 及时发现问题
  2. ✅ 使用自定义监控脚本 + systemd - 安全可控
  3. ✅ 降低资源配置 - 避免OOM
  4. ✅ 添加资源限制 - 防止资源耗尽
  5. ✅ 配置日志轮转 - 避免磁盘满
  6. ✅ 添加告警通知 - 及时了解故障

测试/开发环境

  • 可以使用 autoheal 容器,配置简单快速

预防措施

除了自动重启,还应该:

  1. 定期检查磁盘空间
  2. 监控内存使用情况
  3. 定期备份关键数据
  4. 查看日志中的warning信息
  5. 关注区块同步进度

通过这套完整的监控和自动恢复方案,可以大大提高BSC节点的稳定性和可用性。


相关资源

posted @ 2026-01-09 10:00  若-飞  阅读(9)  评论(0)    收藏  举报