上一篇 下一篇 分享链接 返回 返回顶部

Docker 扛高并发实战:Nginx 多实例、Redis 缓存与限流完整源码示例

发布人:慈云数据-客服中心 发布时间:10小时前 阅读量:3

Docker 高并发解决方案|附源码

在互联网业务中,“高并发”几乎是每个后端系统都绕不开的话题。无论是秒杀活动、直播弹幕、在线教育、即时通讯,还是企业内部的订单系统,只要访问量在短时间内快速上涨,系统就可能面临 CPU 飙升、内存耗尽、数据库连接打满、接口超时、容器频繁重启等问题。

Docker 作为当前主流的容器化部署方案,能够帮助我们快速交付应用、隔离运行环境、提高资源利用率。但需要注意的是:Docker 本身并不会自动解决高并发问题。如果只是把一个单体应用打包进容器,系统架构、服务配置、数据库连接池、缓存策略、限流熔断等都没有优化,那么在高并发场景下依然会出现性能瓶颈。

本文将围绕 Docker 高并发解决方案展开,结合一个可运行的示例项目,讲解如何通过:

  • Nginx 负载均衡
  • 多实例应用部署
  • Docker Compose 编排
  • Redis 缓存
  • 接口限流
  • 数据库连接池优化
  • 容器资源限制
  • 健康检查与自动重启

来构建一个更适合高并发场景的 Docker 部署方案。


一、高并发场景下 Docker 常见问题

在实际项目中,很多团队初次使用 Docker 部署系统时,常见做法如下:

docker run -d -p 8080:8080 my-app

这种方式在开发、测试环境中非常方便,但如果直接用于生产高并发环境,往往会暴露出很多问题。

1. 单容器承载所有流量

如果所有请求都打到一个应用容器上,当访问量快速增长时,该容器可能会出现:

  • CPU 使用率过高
  • 内存快速上涨
  • 线程池耗尽
  • 请求排队严重
  • 接口响应变慢
  • 容器 OOM 被杀死

单实例部署是高并发场景下最常见的瓶颈之一。


2. 缺少负载均衡

没有负载均衡时,系统无法把请求分散到多个应用实例上。即使启动了多个容器,如果没有统一入口和流量分发机制,也很难发挥多实例的性能优势。


3. 数据库压力过大

高并发系统中,数据库通常是最容易成为瓶颈的组件。常见问题包括:

  • 查询慢
  • 数据库连接池打满
  • 大量重复查询
  • 锁竞争严重
  • 写入压力过大
  • 慢 SQL 堆积

如果所有请求都直接访问数据库,即使应用层扩容了,数据库也可能最先崩溃。


4. 缺少缓存机制

对于一些高频读取的数据,例如商品详情、用户信息、配置数据、首页推荐内容,如果每次请求都查询数据库,会造成严重资源浪费。

Redis 缓存可以显著降低数据库压力,提高接口响应速度。


5. 缺少限流与降级

高并发系统不是无限承载流量,而是需要在超出系统能力时进行保护。常见保护手段包括:

  • 限流
  • 熔断
  • 降级
  • 排队
  • 拒绝部分请求

如果没有限流机制,突发流量可能直接拖垮整个服务。


二、整体架构设计

本文采用如下 Docker 高并发部署架构:

用户请求
   │
   ▼
Nginx 负载均衡
   │
   ├── app-1
   ├── app-2
   └── app-3
   │
   ▼
Redis 缓存
   │
   ▼
MySQL 数据库

架构说明:

  1. 用户请求统一进入 Nginx。
  2. Nginx 将请求分发给多个后端应用容器。
  3. 应用优先查询 Redis 缓存。
  4. 缓存未命中时查询 MySQL。
  5. 查询结果写入 Redis,提高后续访问效率。
  6. 应用层增加限流逻辑,防止瞬时流量击穿服务。
  7. Docker Compose 管理多个服务,便于一键启动和扩容。

三、项目目录结构

示例项目目录如下:

docker-high-concurrency-demo
├── app
│   ├── main.py
│   ├── requirements.txt
│   └── Dockerfile
├── nginx
│   └── nginx.conf
├── mysql
│   └── init.sql
└── docker-compose.yml

本文示例使用 Python FastAPI 编写接口服务,原因是代码简单、性能较好、便于演示。实际生产环境中,你也可以换成 Java Spring Boot、Go、Node.js 等技术栈,整体部署思路是一致的。


四、应用服务源码

1. app/main.py

import time
import json
import redis
import pymysql
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
from threading import Lock

app = FastAPI()

redis_client = redis.Redis(
    host="redis",
    port=6379,
    db=0,
    decode_responses=True
)

db_config = {
    "host": "mysql",
    "user": "root",
    "password": "123456",
    "database": "demo",
    "charset": "utf8mb4",
    "cursorclass": pymysql.cursors.DictCursor
}

# 简单令牌桶限流配置
RATE_LIMIT = 100
INTERVAL = 1

tokens = RATE_LIMIT
last_refill = time.time()
lock = Lock()


def get_db_connection():
    return pymysql.connect(**db_config)


def allow_request():
    """
    简单令牌桶限流:
    每秒最多允许 RATE_LIMIT 个请求通过。
    实际生产环境建议使用 Redis + Lua 或网关限流。
    """
    global tokens, last_refill

    with lock:
        now = time.time()
        elapsed = now - last_refill

        if elapsed >= INTERVAL:
            tokens = RATE_LIMIT
            last_refill = now

        if tokens > 0:
            tokens -= 1
            return True

        return False


@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
    if not allow_request():
        return JSONResponse(
            status_code=429,
            content={
                "code": 429,
                "message": "Too Many Requests"
            }
        )

    response = await call_next(request)
    return response


@app.get("/health")
def health():
    return {
        "status": "ok"
    }


@app.get("/product/{product_id}")
def get_product(product_id: int):
    cache_key = f"product:{product_id}"

    # 1. 查询 Redis 缓存
    cached_data = redis_client.get(cache_key)
    if cached_data:
        return {
            "source": "redis",
            "data": json.loads(cached_data)
        }

    # 2. 缓存未命中,查询 MySQL
    conn = get_db_connection()

    try:
        with conn.cursor() as cursor:
            sql = "SELECT id, name, price, stock FROM product WHERE id = %s"
            cursor.execute(sql, (product_id,))
            product = cursor.fetchone()

            if not product:
                raise HTTPException(status_code=404, detail="Product not found")

            # 3. 写入 Redis,设置过期时间
            redis_client.setex(cache_key, 60, json.dumps(product, ensure_ascii=False))

            return {
                "source": "mysql",
                "data": product
            }

    finally:
        conn.close()


@app.post("/order/{product_id}")
def create_order(product_id: int):
    """
    模拟下单接口。
    注意:该示例仅用于演示。
    真实秒杀场景需要 Redis 预扣库存、消息队列异步削峰、防止超卖等机制。
    """
    conn = get_db_connection()

    try:
        with conn.cursor() as cursor:
            conn.begin()

            cursor.execute(
                "SELECT stock FROM product WHERE id = %s FOR UPDATE",
                (product_id,)
            )
            product = cursor.fetchone()

            if not product:
                raise HTTPException(status_code=404, detail="Product not found")

            if product["stock"] <= 0:
                raise HTTPException(status_code=400, detail="Stock not enough")

            cursor.execute(
                "UPDATE product SET stock = stock - 1 WHERE id = %s",
                (product_id,)
            )

            cursor.execute(
                "INSERT INTO orders(product_id, create_time) VALUES(%s, NOW())",
                (product_id,)
            )

            conn.commit()

            # 删除缓存,避免读取到旧库存
            redis_client.delete(f"product:{product_id}")

            return {
                "message": "order created successfully"
            }

    except Exception as e:
        conn.rollback()
        raise e

    finally:
        conn.close()

这个服务主要提供了三个接口:

接口 说明
/health 健康检查接口
/product/{product_id} 查询商品信息,优先走 Redis 缓存
/order/{product_id} 模拟下单接口,使用数据库事务扣减库存

2. app/requirements.txt

fastapi==0.111.0
uvicorn==0.30.1
redis==5.0.4
pymysql==1.1.0

3. app/Dockerfile

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .

RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

COPY main.py .

EXPOSE 8000

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]

这里需要重点关注:

--workers 2

uvicorn 的 worker 数量会影响并发处理能力。一般可以按照 CPU 核数进行配置,例如:

workers = CPU核心数 * 2 + 1

但这只是经验值,最终还需要结合压测结果调整。


五、MySQL 初始化脚本

mysql/init.sql

CREATE DATABASE IF NOT EXISTS demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

USE demo;

CREATE TABLE IF NOT EXISTS product (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    price DECIMAL(10, 2) NOT NULL,
    stock INT NOT NULL,
    KEY idx_name(name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE IF NOT EXISTS orders (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    product_id BIGINT NOT NULL,
    create_time DATETIME NOT NULL,
    KEY idx_product_id(product_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO product(name, price, stock)
VALUES
('iPhone 15 Pro', 7999.00, 1000),
('MacBook Pro', 15999.00, 500),
('AirPods Pro', 1899.00, 2000);

在高并发场景下,数据库表结构设计也非常关键。常见优化方式包括:

  • 给查询字段添加索引
  • 避免大事务
  • 避免长时间锁表
  • 尽量使用短 SQL
  • 热点数据放入缓存
  • 订单表按时间或业务维度分库分表

六、Nginx 负载均衡配置

nginx/nginx.conf

events {
    worker_connections 4096;
}

http {
    upstream backend {
        least_conn;
        server app1:8000 max_fails=3 fail_timeout=30s;
        server app2:8000 max_fails=3 fail_timeout=30s;
        server app3:8000 max_fails=3 fail_timeout=30s;
    }

    server {
        listen 80;

        location / {
            proxy_pass http://backend;
            proxy_http_version 1.1;

            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_connect_timeout 3s;
            proxy_read_timeout 10s;
            proxy_send_timeout 10s;
        }

        location /nginx_status {
            stub_status;
            access_log off;
        }
    }
}

关键配置说明:

1. worker_connections

worker_connections 4096;

表示每个 worker 进程最大连接数。高并发场景中需要适当调大。


2. upstream backend

upstream backend {
    least_conn;
    server app1:8000;
    server app2:8000;
    server app3:8000;
}

这里配置了三个后端服务实例。least_conn 表示请求优先转发给当前连接数最少的服务实例,适合接口耗时不均匀的场景。

常见负载均衡策略包括:

策略 说明
round-robin 默认轮询
least_conn 最少连接
ip_hash 按客户端 IP 分配,适合会话保持
weight 权重分配

3. 超时配置

proxy_connect_timeout 3s;
proxy_read_timeout 10s;
proxy_send_timeout 10s;

超时配置非常重要。如果后端服务已经卡住,而 Nginx 一直等待,会导致连接资源被大量占用。


七、Docker Compose 编排文件

docker-compose.yml

version: "3.8"

services:
  nginx:
    image: nginx:1.25
    container_name: demo-nginx
    ports:
      - "80:80"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - app1
      - app2
      - app3
    restart: always
    networks:
      - demo-net

  app1:
    build: ./app
    container_name: demo-app1
    depends_on:
      - redis
      - mysql
    restart: always
    networks:
      - demo-net
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M

  app2:
    build: ./app
    container_name: demo-app2
    depends_on:
      - redis
      - mysql
    restart: always
    networks:
      - demo-net
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M

  app3:
    build: ./app
    container_name: demo-app3
    depends_on:
      - redis
      - mysql
    restart: always
    networks:
      - demo-net
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M

  redis:
    image: redis:7.2
    container_name: demo-redis
    command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
    volumes:
      - redis-data:/data
    restart: always
    networks:
      - demo-net

  mysql:
    image: mysql:8.0
    container_name: demo-mysql
    environment:
      MYSQL_ROOT_PASSWORD: 123456
    volumes:
      - ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql
      - mysql-data:/var/lib/mysql
    command:
      --max_connections=1000
      --innodb_buffer_pool_size=512M
      --character-set-server=utf8mb4
      --collation-server=utf8mb4_unicode_ci
    restart: always
    networks:
      - demo-net

volumes:
  redis-data:
  mysql-data:

networks:
  demo-net:
    driver: bridge

需要注意的是,deploy.resources 在普通 docker-compose 模式下不一定完全生效,它在 Docker Swarm 中支持更完整。如果你使用的是新版 Docker Compose,也可以使用如下方式限制资源:

mem_limit: 512m
cpus: 1.0

八、启动项目

进入项目根目录,执行:

docker compose up -d --build

查看容器状态:

docker compose ps

查看日志:

docker compose logs -f app1

访问接口:

curl http://localhost/product/1

第一次请求可能返回:

{
  "source": "mysql",
  "data": {
    "id": 1,
    "name": "iPhone 15 Pro",
    "price": "7999.00",
    "stock": 1000
  }
}

第二次请求可能返回:

{
  "source": "redis",
  "data": {
    "id": 1,
    "name": "iPhone 15 Pro",
    "price": "7999.00",
    "stock": 1000
  }
}

这说明缓存已经生效。


九、如何横向扩容应用容器

在 Docker 高并发方案中,最常见的扩容方式是横向扩容,即增加应用实例数量。

如果使用手动定义多个服务的方式,可以继续增加:

app4:
  build: ./app
  depends_on:
    - redis
    - mysql
  restart: always
  networks:
    - demo-net

然后在 Nginx 中加入:

server app4:8000;

不过这种方式维护成本较高。实际生产中更推荐使用:

  • Docker Swarm
  • Kubernetes
  • Nginx 动态服务发现
  • Consul
  • Traefik
  • Spring Cloud Gateway
  • 云厂商负载均衡

如果使用 Docker Compose 的副本模式,可以这样启动:

docker compose up -d --scale app=5

但前提是 compose 文件中的服务名、端口暴露方式要适配这种模式,不能给每个副本指定固定 container_name


十、接口限流方案

本文代码中使用了一个简单的本地令牌桶限流方式:

RATE_LIMIT = 100
INTERVAL = 1

表示单个应用实例每秒最多允许 100 个请求。如果部署 3 个实例,理论上整体可以承载约 300 QPS。

但是需要注意:本地限流只对当前容器生效。如果有多个应用实例,每个实例都有自己的限流计数器,整体流量控制并不精确。

生产环境更推荐使用:

1. Nginx 限流

http {
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/s;

    server {
        location / {
            limit_req zone=api_limit burst=50 nodelay;
            proxy_pass http://backend;
        }
    }
}

适合在入口层快速拦截异常流量。


2. Redis + Lua 分布式限流

Redis 可以保证多个应用实例共享同一个计数器,Lua 脚本可以保证限流逻辑的原子性。

示例 Lua:

local key = KEYS[1]
local limit = tonumber(ARGV[1])
local expire = tonumber(ARGV[2])

local current = tonumber(redis.call('get', key) or "0")

if current + 1 > limit then
    return 0
else
    redis.call("incr", key)
    redis.call("expire", key, expire)
    return 1
end

应用层调用时,如果返回 0,表示超过限制,可以直接返回 429。


十一、Redis 缓存优化策略

Redis 在高并发系统中非常重要,但使用不当也会带来问题。

1. 缓存穿透

缓存穿透是指查询一个数据库中也不存在的数据,导致请求每次都打到数据库。

解决方案:

  • 缓存空值
  • 使用布隆过滤器
  • 对非法参数提前校验

2. 缓存击穿

缓存击穿是指某个热点 key 过期瞬间,大量请求同时访问数据库。

解决方案:

  • 热点数据永不过期
  • 加互斥锁
  • 逻辑过期
  • 后台异步刷新缓存

3. 缓存雪崩

缓存雪崩是指大量 key 在同一时间过期,导致请求集中打到数据库。

解决方案:

  • 过期时间增加随机值
  • 多级缓存
  • Redis 高可用集群
  • 服务降级

示例:

import random

expire_time = 60 + random.randint(1, 30)
redis_client.setex(cache_key, expire_time, value)

十二、数据库连接池优化

本文示例为了简洁,每次请求都会创建一个 MySQL 连接:

conn = pymysql.connect(**db_config)

这在高并发场景下并不推荐。因为频繁创建和关闭连接会带来额外开销。

生产环境中应使用连接池,例如:

  • Java:HikariCP、Druid
  • Python:SQLAlchemy Pool、DBUtils
  • Go:database/sql 内置连接池
  • Node.js:mysql2 pool

连接池需要关注以下参数:

参数 说明
max connections 最大连接数
min idle 最小空闲连接
connection timeout 获取连接超时时间
idle timeout 空闲连接回收时间
max lifetime 连接最大生命周期

同时要注意:应用实例数量增加后,总数据库连接数也会增加。例如:

3 个应用实例 × 每个实例最大 100 个连接 = 300 个数据库连接

如果 MySQL 最大连接数只有 200,就会出现连接不够的问题。因此扩容应用时,也要同步评估数据库承载能力。


十三、容器资源限制

在生产环境中,不建议让容器无限制使用宿主机资源。否则某个异常容器可能占满 CPU 或内存,影响其他服务。

常见配置:

services:
  app:
    image: my-app
    mem_limit: 512m
    cpus: 1.0

资源限制的意义:

  • 防止单个容器拖垮宿主机
  • 便于容量规划
  • 方便压测和扩容
  • 提高系统稳定性

但资源限制也不能设置过小,否则应用容易出现频繁 GC、OOM、响应变慢等问题。


十四、健康检查与自动恢复

高并发系统中,应用实例可能因为各种原因不可用,例如:

  • 内存溢出
  • 死锁
  • 依赖服务超时
  • 网络异常
  • 线程池耗尽

因此需要配置健康检查。

Docker Compose 示例:

app1:
  build: ./app
  healthcheck:
    test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
    interval: 10s
    timeout: 3s
    retries: 3
  restart: always

健康检查可以帮助我们发现服务异常,而 restart: always 可以在容器退出后自动重启。

不过要注意,Docker Compose 的健康检查并不能完全替代 Kubernetes 的 readinessProbe 和 livenessProbe。在更复杂的生产环境中,建议使用 Kubernetes 管理服务生命周期。


十五、压测验证

可以使用 abwrkhey 进行压测。

使用 wrk

wrk -t4 -c200 -d30s http://localhost/product/1

参数说明:

参数 说明
-t4 4 个线程
-c200 200 个并发连接
-d30s 持续压测 30 秒

如果缓存命中率较高,接口吞吐量会明显提升。

使用 ab

ab -n 10000 -c 200 http://localhost/product/1

参数说明:

参数 说明
-n 10000 总请求数
-c 200 并发数

压测时需要重点关注:

  • QPS
  • 平均响应时间
  • P95/P99 延迟
  • 错误率
  • CPU 使用率
  • 内存占用
  • Redis 命中率
  • MySQL 慢查询
  • Nginx 连接数

十六、生产环境建议

本文示例适合学习和中小型项目验证,但如果要用于生产环境,还需要继续完善。

1. 使用 Kubernetes 替代单机 Compose

Docker Compose 适合单机编排,而 Kubernetes 更适合生产级容器编排,支持:

  • 自动扩缩容
  • 滚动发布
  • 服务发现
  • 故障自愈
  • 配置管理
  • 密钥管理
  • 探针检测
  • 弹性调度

2. 引入消息队列削峰

对于下单、支付回调、日志写入、短信发送等场景,建议引入消息队列,例如:

  • Kafka
  • RabbitMQ
  • RocketMQ
  • Redis Stream

典型流程:

用户请求
  │
  ▼
快速写入消息队列
  │
  ▼
后台消费者异步处理

这样可以避免瞬时流量直接打垮数据库。


3. 静态资源走 CDN

图片、视频、JS、CSS 等静态资源不应该全部由应用服务承担,可以通过 CDN 加速,降低源站压力。


4. 日志与监控

高并发系统必须配套监控体系,例如:

  • Prometheus
  • Grafana
  • ELK
  • Loki
  • SkyWalking
  • OpenTelemetry

重点监控:

  • 容器 CPU
  • 容器内存
  • 网络 IO
  • 磁盘 IO
  • 接口耗时
  • 错误率
  • 慢 SQL
  • Redis 内存
  • Redis QPS
  • Nginx 状态

十七、总结

Docker 高并发解决方案不是简单地“把应用放进容器”就完成了,而是需要从整体架构出发进行系统设计。

本文给出的方案核心包括:

  1. 使用 Nginx 作为统一入口和负载均衡器。
  2. 启动多个应用容器,实现横向扩容。
  3. 使用 Redis 缓存热点数据,降低数据库压力。
  4. 使用限流机制保护服务,避免突发流量拖垮系统。
  5. 优化 MySQL 参数和表索引,提高数据库承载能力。
  6. 为容器设置资源限制,避免单个服务占满宿主机资源。
  7. 配置健康检查和自动重启,提高系统可用性。
  8. 使用压测工具持续验证性能瓶颈。

对于真正的生产级高并发系统,还需要进一步引入 Kubernetes、消息队列、分布式限流、链路追踪、灰度发布、自动扩缩容、多级缓存和数据库分库分表等能力。

最终要记住一句话:

Docker 解决的是应用交付和运行环境一致性问题,高并发解决的是系统架构、资源调度和流量治理问题。只有把容器化与架构优化结合起来,才能真正构建稳定可靠的高并发系统。

目录结构
全文