强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 13 章 · Docker 部署

第 13 章 · Docker 部署

13.1 为什么在 Docker 中使用 LevelDB

优势说明
环境一致性开发、测试、生产环境一致
快速部署一条命令启动服务
依赖隔离不影响宿主机环境
易于扩展水平扩展多个实例
CI/CD 友好自动化构建和测试
挑战说明
数据持久化容器销毁时数据丢失,需要 Volume
性能损耗文件系统抽象层可能影响 I/O
I/O 调度Docker 的 overlay2 文件系统有额外开销

13.2 单容器部署

Dockerfile

# ========== 构建阶段 ==========
FROM ubuntu:22.04 AS builder

# 安装依赖
RUN apt-get update && apt-get install -y \
    build-essential cmake git ca-certificates \
    libgflags-dev libsnappy-dev zlib1g-dev \
    libbz2-dev liblz4-dev libzstd-dev \
    && rm -rf /var/lib/apt/lists/*

# 编译 LevelDB
RUN git clone --depth 1 --branch 1.23 \
    https://github.com/google/leveldb.git /opt/leveldb-src

WORKDIR /opt/leveldb-src
RUN git submodule update --init --recursive && \
    mkdir build && cd build && \
    cmake -DCMAKE_BUILD_TYPE=Release .. && \
    cmake --build . -j$(nproc)

# 编译应用
WORKDIR /opt/app
COPY server.cpp CMakeLists.txt ./
RUN mkdir build && cd build && \
    cmake -DCMAKE_BUILD_TYPE=Release .. && \
    cmake --build . -j$(nproc)

# ========== 运行阶段 ==========
FROM ubuntu:22.04

RUN apt-get update && apt-get install -y \
    libsnappy1v5 libgflags2.2 tini \
    && rm -rf /var/lib/apt/lists/*

# 复制 LevelDB 库
COPY --from=builder /opt/leveldb-src/build/libleveldb.so* /usr/local/lib/

# 复制应用
COPY --from=builder /opt/app/build/server /usr/local/bin/

# 创建数据目录
RUN mkdir -p /data/leveldb && chown -R nobody:nogroup /data

# 使用 tini 作为 init 进程(正确处理信号)
ENTRYPOINT ["tini", "--"]
CMD ["server", "--db=/data/leveldb", "--port=8080"]

简单 HTTP Server 示例 (server.cpp)

#include <iostream>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include "leveldb/db.h"

static leveldb::DB* g_db = nullptr;

std::string HandleRequest(const std::string& request) {
    // 简单的 HTTP 解析
    if (request.substr(0, 4) == "GET ") {
        // GET /key
        auto pos = request.find(' ', 4);
        std::string key = request.substr(4, pos - 4);
        if (key[0] == '/') key = key.substr(1);

        std::string value;
        leveldb::Status s = g_db->Get(leveldb::ReadOptions(), key, &value);
        if (s.ok()) {
            return "HTTP/1.1 200 OK\r\nContent-Length: "
                   + std::to_string(value.size())
                   + "\r\n\r\n" + value;
        }
        return "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n";
    }
    return "HTTP/1.1 405 Method Not Allowed\r\n\r\n";
}

int main(int argc, char** argv) {
    // 解析参数
    std::string db_path = "/data/leveldb";
    int port = 8080;
    for (int i = 1; i < argc; i++) {
        if (strncmp(argv[i], "--db=", 5) == 0) db_path = argv[i] + 5;
        if (strncmp(argv[i], "--port=", 7) == 0) port = atoi(argv[i] + 7);
    }

    // 打开 LevelDB
    leveldb::Options opts;
    opts.create_if_missing = true;
    opts.block_cache = leveldb::NewLRUCache(128 * 1024 * 1024);
    opts.filter_policy = leveldb::NewBloomFilterPolicy(10);
    leveldb::Status s = leveldb::DB::Open(opts, db_path, &g_db);
    if (!s.ok()) {
        std::cerr << "Failed to open DB: " << s.ToString() << std::endl;
        return 1;
    }

    // 创建 HTTP 服务器
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port = htons(port);
    bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
    listen(server_fd, 128);

    std::cout << "Server listening on port " << port << std::endl;

    while (true) {
        int client_fd = accept(server_fd, nullptr, nullptr);
        char buf[4096];
        int n = read(client_fd, buf, sizeof(buf) - 1);
        if (n > 0) {
            buf[n] = 0;
            std::string response = HandleRequest(buf);
            write(client_fd, response.c_str(), response.size());
        }
        close(client_fd);
    }

    delete g_db;
    return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(leveldb-server)

set(CMAKE_CXX_STANDARD 17)
find_package(leveldb REQUIRED)
find_package(Threads REQUIRED)

add_executable(server server.cpp)
target_link_libraries(server leveldb::leveldb Threads::Threads)

13.3 数据持久化

Named Volume(推荐)

# 创建命名卷
docker volume create leveldb-data

# 运行容器
docker run -d \
    --name leveldb-server \
    -v leveldb-data:/data/leveldb \
    -p 8080:8080 \
    leveldb-server:latest

Bind Mount

# 使用宿主机目录
docker run -d \
    --name leveldb-server \
    -v /opt/data/leveldb:/data/leveldb \
    -p 8080:8080 \
    leveldb-server:latest

Volume 对比

类型优点缺点适用场景
Named VolumeDocker 管理,易备份不便直接查看文件生产环境
Bind Mount直接访问文件权限问题开发环境

⚠️ 注意:不要将 LevelDB 数据存储在 overlay2 文件系统上(即容器的可写层)。这会导致严重的性能下降。


13.4 Docker Compose 编排

docker-compose.yml

version: "3.8"

services:
  leveldb:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: leveldb-server
    ports:
      - "8080:8080"
    volumes:
      - leveldb-data:/data/leveldb
    environment:
      - LEVELDB_CACHE_SIZE=268435456  # 256MB
      - LEVELDB_BLOOM_BITS=10
      - LOG_LEVEL=info
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 1G
        reservations:
          cpus: "0.5"
          memory: 256M

volumes:
  leveldb-data:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /opt/data/leveldb

13.5 生产环境配置

性能优化

# docker-compose.prod.yml
services:
  leveldb:
    build: .
    volumes:
      # 使用 SSD 挂载点
      - /mnt/ssd/leveldb:/data/leveldb:rw
    # 禁用 swap
    memswap_limit: 1g
    # 使用 host 网络减少延迟
    network_mode: host
    # 共享内存大小(用于 /dev/shm)
    shm_size: "256m"
    # PID 限制
    pids_limit: 200
    # ulimit 配置
    ulimits:
      nofile:
        soft: 65536
        hard: 65536
      memlock:
        soft: -1
        hard: -1

日志配置

services:
  leveldb:
    logging:
      driver: "json-file"
      options:
        max-size: "50m"
        max-file: "5"
        tag: "{{.Name}}/{{.ID}}"

13.6 备份与恢复

备份脚本

#!/bin/bash
# backup.sh - LevelDB 数据备份脚本

BACKUP_DIR="/opt/backups/leveldb"
CONTAINER_NAME="leveldb-server"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/leveldb_${TIMESTAMP}.tar.gz"

# 创建备份目录
mkdir -p "${BACKUP_DIR}"

# 方法 1:通过 Snapshot API 备份(推荐)
docker exec "${CONTAINER_NAME}" /usr/local/bin/server \
    --backup=/tmp/backup_snapshot

docker cp "${CONTAINER_NAME}:/tmp/backup_snapshot" \
    "${BACKUP_DIR}/snapshot_${TIMESTAMP}"

# 方法 2:文件系统级备份(需要暂停写入)
docker exec "${CONTAINER_NAME}" touch /tmp/pause_writes
sleep 2

docker run --rm \
    -v leveldb-data:/data:ro \
    -v "${BACKUP_DIR}:/backup" \
    ubuntu:22.04 \
    tar czf "/backup/leveldb_${TIMESTAMP}.tar.gz" -C /data .

docker exec "${CONTAINER_NAME}" rm /tmp/pause_writes

echo "Backup completed: ${BACKUP_FILE}"

恢复脚本

#!/bin/bash
# restore.sh - LevelDB 数据恢复脚本

BACKUP_FILE="$1"
CONTAINER_NAME="leveldb-server"

if [ -z "${BACKUP_FILE}" ]; then
    echo "Usage: $0 <backup_file.tar.gz>"
    exit 1
fi

# 停止服务
docker stop "${CONTAINER_NAME}"

# 清空数据
docker volume rm leveldb-data 2>/dev/null
docker volume create leveldb-data

# 恢复数据
docker run --rm \
    -v leveldb-data:/data \
    -v "$(dirname ${BACKUP_FILE}):/backup:ro" \
    ubuntu:22.04 \
    bash -c "cd /data && tar xzf /backup/$(basename ${BACKUP_FILE})"

# 重启服务
docker start "${CONTAINER_NAME}"

echo "Restore completed"

13.7 监控

Prometheus 指标暴露

// 在 HTTP Server 中添加 /metrics 端点
std::string GetMetrics() {
    std::ostringstream ss;
    ss << "# HELP leveldb_block_cache_hit Block cache hits\n";
    ss << "# TYPE leveldb_block_cache_hit counter\n";
    ss << "leveldb_block_cache_hit " 
       << stats->getTickerCount(leveldb::BLOCK_CACHE_HIT) << "\n";

    ss << "# HELP leveldb_block_cache_miss Block cache misses\n";
    ss << "# TYPE leveldb_block_cache_miss counter\n";
    ss << "leveldb_block_cache_miss "
       << stats->getTickerCount(leveldb::BLOCK_CACHE_MISS) << "\n";

    // ... 更多指标
    return ss.str();
}

Docker 健康检查

HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
    CMD curl -f http://localhost:8080/health || exit 1

13.8 常见问题

问题原因解决方案
容器重启后数据丢失未挂载 Volume使用 -v 挂载持久化目录
I/O 性能差数据在 overlay2 上挂载到宿主机目录或 SSD
文件描述符不足ulimit 限制设置 ulimits.nofile
内存 OOM缓存配置过大调整 cache_sizemem_limit
时钟偏移容器时间不同步使用 --network=host 或 NTP

13.9 本章小结

要点内容
数据持久化必须使用 Volume,不要依赖容器可写层
性能优化使用 host 网络、SSD 存储、合理 ulimit
备份Snapshot API 备份 > 文件系统级备份
监控Prometheus 指标 + 健康检查
编排Docker Compose 管理服务生命周期

扩展阅读

  1. Docker 存储驱动OverlayFS
  2. Docker 性能调优Best Practices
  3. cgroups v2:理解容器资源限制

第 12 章 · 数据复制 | 第 14 章 · LevelDB vs RocksDB