前八讲我们构建了一个高性能的协同编辑系统,完成了从文档引擎到性能优化的全部核心功能。现在,是时候把这个系统部署到生产环境,让它真正服务用户了。
这一讲,我们来实现完整的部署和运维体系。
一、系统架构总览
1.1 生产环境架构
┌──────────────┐ │ CDN │ │ (静态资源) │ └──────┬───────┘ │ ┌──────────┐ ┌────────┴────────┐ ┌──────────┐ │ Browser │◀────────▶│ Load Balancer │◀────────▶│ Browser │ │ (Client) │ HTTPS │ (Nginx/HA) │ HTTPS │ (Client) │ └──────────┘ └────────┬────────┘ └──────────┘ │ ┌────────────────┼────────────────┐ ▼ ▼ ▼ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ WebSocket│ │ WebSocket│ │ WebSocket│ │ Server 1 │ │ Server 2 │ │ Server 3 │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ └───────────────┼───────────────┘ │ ┌───────▼───────┐ │ Redis │ │ (Pub/Sub + │ │ Session) │ └───────┬───────┘ │ ┌───────▼───────┐ │ PostgreSQL │ │ (持久化存储) │ └───────────────┘ 监控体系: ┌──────────┐ ┌──────────┐ ┌──────────┐ │ Prometheus│ │ Grafana │ │ ELK │ │ (指标) │ │ (可视化) │ │ (日志) │ └──────────┘ └──────────┘ └──────────┘1.2 技术栈选型
组件 | 技术选型 | 用途 |
|---|---|---|
Web 服务器 | Nginx | 反向代理、SSL终止、负载均衡 |
应用服务器 | Python + Uvicorn | WebSocket 服务 |
消息队列 | Redis Pub/Sub | 跨进程广播 |
持久化 | PostgreSQL | 文档存储 |
缓存 | Redis | Session、临时数据 |
容器化 | Docker + Docker Compose | 部署 |
编排 | Kubernetes | 生产集群 |
监控 | Prometheus + Grafana | 指标采集与可视化 |
日志 | ELK Stack | 日志聚合 |
二、Docker 容器化
2.1 Dockerfile
# Dockerfile FROM python:3.11-slim AS builder WORKDIR /app # 安装系统依赖 RUN apt-get update && apt-get install -y --no-install-recommends \ gcc \ libpq-dev \ && rm -rf /var/lib/apt/lists/* # 安装 Python 依赖 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 第二阶段:运行镜像 FROM python:3.11-slim WORKDIR /app # 复制依赖 COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages COPY --from=builder /usr/local/bin /usr/local/bin # 复制应用代码 COPY . . # 创建非 root 用户 RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app USER appuser # 健康检查 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:9000/health')" EXPOSE 9000 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "9000", "--workers", "4"]2.2 docker-compose.yml
# docker-compose.yml version: '3.8' services: # WebSocket 服务器 ws-server: build: . image: collab-editor:latest ports: - "9000:9000" environment: - REDIS_URL=redis://redis:6379/0 - DATABASE_URL=postgresql://collab:password@postgres:5432/collab_editor - LOG_LEVEL=info depends_on: redis: condition: service_healthy postgres: condition: service_healthy restart: unless-stopped deploy: replicas: 3 resources: limits: cpus: '1' memory: 512M reservations: cpus: '0.5' memory: 256M # Nginx 反向代理 nginx: image: nginx:alpine ports: - "80:80" - "443:443" volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./nginx/ssl:/etc/nginx/ssl:ro - ./static:/usr/share/nginx/html:ro depends_on: - ws-server restart: unless-stopped # Redis redis: image: redis:7-alpine ports: - "6379:6379" volumes: - redis-data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 5 restart: unless-stopped # PostgreSQL postgres: image: postgres:15-alpine ports: - "5432:5432" environment: POSTGRES_DB: collab_editor POSTGRES_USER: collab POSTGRES_PASSWORD: password volumes: - postgres-data:/var/lib/postgresql/data - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql healthcheck: test: ["CMD-SHELL", "pg_isready -U collab"] interval: 5s timeout: 3s retries: 5 restart: unless-stopped # Prometheus 监控 prometheus: image: prom/prometheus:latest ports: - "9090:9090" volumes: - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml - prometheus-data:/prometheus command: - '--config.file=/etc/prometheus/prometheus.yml' - '--storage.tsdb.path=/prometheus' restart: unless-stopped # Grafana 可视化 grafana: image: grafana/grafana:latest ports: - "3000:3000" environment: - GF_SECURITY_ADMIN_PASSWORD=admin volumes: - grafana-data:/var/lib/grafana - ./monitoring/grafana/dashboards:/etc/grafana/provisioning/dashboards - ./monitoring/grafana/datasources:/etc/grafana/provisioning/datasources depends_on: - prometheus restart: unless-stopped volumes: redis-data: postgres-data: prometheus-data: grafana-data:三、Nginx 配置
3.1 反向代理配置
# nginx/nginx.conf events { worker_connections 1024; multi_accept on; use epoll; } http { upstream ws_backend { # 最少连接负载均衡 least_conn; server ws-server:9000 weight=3; server ws-server:9001 weight=2; server ws-server:9002 weight=1; # 健康检查 keepalive 32; } # HTTP -> HTTPS 重定向 server { listen 80; server_name collab.example.com; return 301 https://$server_name$request_uri; } # HTTPS 服务器 server { listen 443 ssl http2; server_name collab.example.com; # SSL 配置 ssl_certificate /etc/nginx/ssl/cert.pem; ssl_certificate_key /etc/nginx/ssl/key.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; # 安全头 add_header Strict-Transport-Security "max-age=63072000" always; add_header X-Frame-Options DENY; add_header X-Content-Type-Options nosniff; add_header X-XSS-Protection "1; mode=block"; # 静态资源 location /static/ { root /usr/share/nginx/html; expires 30d; add_header Cache-Control "public, immutable"; } # API 路由 location /api/ { proxy_pass http://ws_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # WebSocket 升级 location /ws/ { proxy_pass http://ws_backend; proxy_http_version 1.1; # WebSocket 必要头 proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; # 超时设置 proxy_read_timeout 86400s; proxy_send_timeout 86400s; # 缓冲关闭 proxy_buffering off; proxy_request_buffering off; } # 健康检查 location /health { access_log off; return 200 "OK\n"; add_header Content-Type text/plain; } # 指标端点 location /metrics { access_log off; proxy_pass http://ws_backend/metrics; } } }四、数据库初始化
4.1 SQL 脚本
-- sql/init.sql -- 协同编辑器数据库初始化 -- 文档表 CREATE TABLE IF NOT EXISTS documents ( id VARCHAR(36) PRIMARY KEY, title VARCHAR(255) NOT NULL DEFAULT 'Untitled', content TEXT NOT NULL DEFAULT '', version INTEGER NOT NULL DEFAULT 0, owner_id VARCHAR(36), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), is_deleted BOOLEAN DEFAULT FALSE ); -- 操作历史表 CREATE TABLE IF NOT EXISTS operation_history ( id BIGSERIAL PRIMARY KEY, document_id VARCHAR(36) NOT NULL REFERENCES documents(id), operation_json JSONB NOT NULL, site_id VARCHAR(36) NOT NULL, sequence INTEGER NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(document_id, site_id, sequence) ); -- 文档快照表 CREATE TABLE IF NOT EXISTS document_snapshots ( id BIGSERIAL PRIMARY KEY, document_id VARCHAR(36) NOT NULL REFERENCES documents(id), content TEXT NOT NULL, version INTEGER NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(document_id, version) ); -- 用户会话表 CREATE TABLE IF NOT EXISTS user_sessions ( session_id VARCHAR(64) PRIMARY KEY, user_id VARCHAR(36) NOT NULL, document_id VARCHAR(36) REFERENCES documents(id), connected_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), disconnected_at TIMESTAMP WITH TIME ZONE, is_active BOOLEAN DEFAULT TRUE ); -- 索引 CREATE INDEX IF NOT EXISTS idx_operation_history_doc_id ON operation_history(document_id); CREATE INDEX IF NOT EXISTS idx_operation_history_created ON operation_history(created_at DESC); CREATE INDEX IF NOT EXISTS idx_document_snapshots_doc_version ON document_snapshots(document_id, version DESC); CREATE INDEX IF NOT EXISTS idx_user_sessions_active ON user_sessions(is_active) WHERE is_active = TRUE; -- 触发器:自动更新 updated_at CREATE OR REPLACE FUNCTION update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ language 'plpgsql'; CREATE TRIGGER update_documents_updated_at BEFORE UPDATE ON documents FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- 插入测试文档 INSERT INTO documents (id, title, content, version, owner_id) VALUES ('welcome-doc', '欢迎文档', '欢迎使用协同编辑器!', 1, 'system'), ('demo-doc', '演示文档', '这是一个多人实时编辑的演示文档。', 1, 'system') ON CONFLICT (id) DO NOTHING;五、监控与告警
5.1 Prometheus 配置
# monitoring/prometheus.yml global: scrape_interval: 15s evaluation_interval: 15s alerting: alertmanagers: - static_configs: - targets: ['alertmanager:9093'] rule_files: - 'alerts.yml' scrape_configs: # WebSocket 服务器 - job_name: 'ws-server' static_configs: - targets: ['ws-server:9000'] metrics_path: '/metrics' # Redis - job_name: 'redis' static_configs: - targets: ['redis-exporter:9121'] # PostgreSQL - job_name: 'postgres' static_configs: - targets: ['postgres-exporter:9187'] # Node 指标 - job_name: 'node' static_configs: - targets: ['node-exporter:9100']5.2 告警规则
# monitoring/alerts.yml groups: - name: collab_editor_alerts rules: # 服务器宕机 - alert: ServerDown expr: up{job="ws-server"} == 0 for: 1m labels: severity: critical annotations: summary: "WebSocket 服务器 {{ $labels.instance }} 宕机" # 高延迟 - alert: HighLatency expr: histogram_quantile(0.95, rate(ws_message_duration_seconds_bucket[5m])) > 0.5 for: 5m labels: severity: warning annotations: summary: "P95 消息延迟超过 500ms" # 连接数过高 - alert: HighConnectionCount expr: ws_active_connections > 1000 for: 5m labels: severity: warning annotations: summary: "活跃连接数超过 1000" # 错误率过高 - alert: HighErrorRate expr: rate(ws_errors_total[5m]) / rate(ws_messages_total[5m]) > 0.05 for: 5m labels: severity: critical annotations: summary: "错误率超过 5%" # Redis 不可用 - alert: RedisDown expr: redis_up == 0 for: 1m labels: severity: critical annotations: summary: "Redis 不可用" # 磁盘空间不足 - alert: DiskSpaceLow expr: (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100 < 10 for: 5m labels: severity: critical annotations: summary: "磁盘剩余空间不足 10%"5.3 Grafana Dashboard
{ "dashboard": { "title": "协同编辑器监控", "panels": [ { "title": "活跃连接数", "type": "graph", "targets": [ { "expr": "sum(ws_active_connections)", "legendFormat": "活跃连接" } ] }, { "title": "消息吞吐量", "type": "graph", "targets": [ { "expr": "rate(ws_messages_total[1m])", "legendFormat": "消息/秒" } ] }, { "title": "操作延迟 (P50/P95/P99)", "type": "graph", "targets": [ { "expr": "histogram_quantile(0.50, rate(ws_op_duration_seconds_bucket[5m]))", "legendFormat": "P50" }, { "expr": "histogram_quantile(0.95, rate(ws_op_duration_seconds_bucket[5m]))", "legendFormat": "P95" }, { "expr": "histogram_quantile(0.99, rate(ws_op_duration_seconds_bucket[5m]))", "legendFormat": "P99" } ] }, { "title": "错误率", "type": "graph", "targets": [ { "expr": "rate(ws_errors_total[5m]) / rate(ws_messages_total[5m])", "legendFormat": "错误率" } ] }, { "title": "内存使用", "type": "graph", "targets": [ { "expr": "process_resident_memory_bytes", "legendFormat": "{{ instance }}" } ] }, { "title": "文档操作统计", "type": "stat", "targets": [ { "expr": "sum(ws_operations_total)", "legendFormat": "总操作数" } ] } ] } }六、部署脚本
6.1 一键部署
#!/bin/bash # scripts/deploy.sh # 协同编辑器部署脚本 set -e echo "========================================" echo " 🚀 协同编辑器部署脚本" echo "========================================" # 配置 PROJECT_NAME="collab-editor" DEPLOY_DIR="/opt/${PROJECT_NAME}" BACKUP_DIR="${DEPLOY_DIR}/backups" LOG_DIR="${DEPLOY_DIR}/logs" # 颜色 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' log() { echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1" } error() { echo -e "${RED}[ERROR]${NC} $1" exit 1 } # 1. 环境检查 log "检查环境..." command -v docker >/dev/null 2>&1 || error "Docker 未安装" command -v docker-compose >/dev/null 2>&1 || error "Docker Compose 未安装" # 2. 创建目录 log "创建目录..." mkdir -p ${DEPLOY_DIR} ${BACKUP_DIR} ${LOG_DIR} # 3. 备份当前版本 if [ -d "${DEPLOY_DIR}/current" ]; then log "备份当前版本..." BACKUP_NAME="backup-$(date '+%Y%m%d_%H%M%S')" cp -r ${DEPLOY_DIR}/current ${BACKUP_DIR}/${BACKUP_NAME} # 保留最近7天的备份 find ${BACKUP_DIR} -name "backup-*" -mtime +7 -exec rm -rf {} \; fi # 4. 拉取最新代码 log "拉取最新代码..." cd ${DEPLOY_DIR} git pull origin main || git clone https://github.com/your-repo/collab-editor.git . # 5. 构建镜像 log "构建 Docker 镜像..." docker-compose build --no-cache ws-server # 6. 数据库迁移 log "运行数据库迁移..." docker-compose run --rm ws-server python scripts/migrate.py # 7. 启动服务 log "启动服务..." docker-compose up -d # 8. 健康检查 log "健康检查..." sleep 10 for i in {1..30}; do if curl -sf http://localhost:9000/health > /dev/null 2>&1; then log "服务健康检查通过!" break fi if [ $i -eq 30 ]; then error "服务启动失败,请检查日志" fi sleep 2 done # 9. 清理旧镜像 log "清理旧镜像..." docker image prune -f log "========================================" log " ✅ 部署完成!" log "========================================" echo "" echo " 访问地址: https://collab.example.com" echo " 监控面板: https://collab.example.com:3000" echo " 日志目录: ${LOG_DIR}" echo ""6.2 CI/CD 配置
# .github/workflows/deploy.yml name: Deploy on: push: branches: [ main ] pull_request: branches: [ main ] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Python uses: actions/setup-python@v4 with: python-version: '3.11' - name: Install dependencies run: | pip install -r requirements.txt pip install pytest pytest-asyncio - name: Run tests run: pytest tests/ -v --cov=./ --cov-report=xml - name: Upload coverage uses: codecov/codecov-action@v3 build-and-deploy: needs: test runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v3 - name: Build Docker image run: docker build -t collab-editor:${{ github.sha }} . - name: Push to registry run: | docker tag collab-editor:${{ github.sha }} registry.example.com/collab-editor:latest docker push registry.example.com/collab-editor:latest - name: Deploy to production uses: appleboy/ssh-action@master with: host: ${{ secrets.DEPLOY_HOST }} username: ${{ secrets.DEPLOY_USER }} key: ${{ secrets.DEPLOY_KEY }} script: | cd /opt/collab-editor docker-compose pull docker-compose up -d --force-recreate七、运维命令
7.1 日常运维
# scripts/ops.sh # 日常运维命令集 # 查看服务状态 alias collab-status='docker-compose ps' alias collab-logs='docker-compose logs -f --tail=100' alias collab-logs-ws='docker-compose logs -f ws-server' # 重启服务 alias collab-restart='docker-compose restart' alias collab-restart-ws='docker-compose restart ws-server' # 扩容/缩容 alias collab-scale-up='docker-compose up -d --scale ws-server=5' alias collab-scale-down='docker-compose up -d --scale ws-server=2' # 备份数据库 collab-backup() { local backup_file="collab_backup_$(date '+%Y%m%d_%H%M%S').sql" docker-compose exec -T postgres pg_dump -U collab collab_editor > "backups/$backup_file" echo "Backup saved: backups/$backup_file" } # 恢复数据库 collab-restore() { if [ -z "$1" ]; then echo "Usage: collab-restore <backup_file>" return 1 fi cat "$1" | docker-compose exec -T postgres psql -U collab collab_editor echo "Database restored from: $1" } # 清理旧数据 collab-cleanup() { echo "Cleaning up old data..." # 清理30天前的操作历史 docker-compose exec -T postgres psql -U collab collab_editor -c " DELETE FROM operation_history WHERE created_at < NOW() - INTERVAL '30 days'; " # 清理过期会话 docker-compose exec -T postgres psql -U collab collab_editor -c " DELETE FROM user_sessions WHERE is_active = false AND disconnected_at < NOW() - INTERVAL '7 days'; " echo "Cleanup completed!" } # 查看性能指标 collab-metrics() { echo "=== 活跃连接 ===" curl -s http://localhost:9000/metrics | grep ws_active_connections echo "=== 消息速率 ===" curl -s http://localhost:9000/metrics | grep ws_messages_total echo "=== 内存使用 ===" docker-compose stats --no-stream ws-server }八、总结
8.1 本讲成果
组件 | 文件 | 功能 |
|---|---|---|
Dockerfile |
| 容器化构建 |
docker-compose.yml |
| 多服务编排 |
Nginx 配置 |
| 反向代理与SSL |
SQL 初始化 |
| 数据库建表 |
Prometheus 配置 |
| 指标采集 |
告警规则 |
| 异常告警 |
部署脚本 |
| 一键部署 |
运维命令 |
| 日常运维 |
8.2 运维要点
高可用:多副本部署 + 负载均衡
可观测:Prometheus + Grafana + ELK
自动化:CI/CD + 一键部署
安全:HTTPS + 非root用户 + 定期备份
伸缩:水平扩展 + 资源限制
8.3 下一讲预告
第10讲:项目总结与展望
最后一讲,我们将回顾整个项目的架构设计,总结经验教训,并探讨未来的发展方向:
架构演进路线
性能优化方向
功能扩展计划
开源社区建设
让我们一起完成这场精彩的旅程!
🧰开发之余的小工具推荐
处理 Base64、JWT 解析、JSON 格式化、Crontab 计算、PDF 合并压缩这些碎片需求,我常用一个纯前端本地工具箱:zz365.top(子页 PDF 大师:PDF 大师 - zz365工具箱)。所有计算在浏览器完成,文件不上传服务器,关页即清。免费、无登录、无广告,适合开发者当常驻标签页。