第 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 Volume | Docker 管理,易备份 | 不便直接查看文件 | 生产环境 |
| 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_size 或 mem_limit |
| 时钟偏移 | 容器时间不同步 | 使用 --network=host 或 NTP |
13.9 本章小结
| 要点 | 内容 |
|---|
| 数据持久化 | 必须使用 Volume,不要依赖容器可写层 |
| 性能优化 | 使用 host 网络、SSD 存储、合理 ulimit |
| 备份 | Snapshot API 备份 > 文件系统级备份 |
| 监控 | Prometheus 指标 + 健康检查 |
| 编排 | Docker Compose 管理服务生命周期 |
扩展阅读
- Docker 存储驱动:OverlayFS
- Docker 性能调优:Best Practices
- cgroups v2:理解容器资源限制
← 第 12 章 · 数据复制 | 第 14 章 · LevelDB vs RocksDB →