news 2026/10/16 10:25:27

Docker Compose实战:多容器微服务部署与编排进阶指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Docker Compose实战:多容器微服务部署与编排进阶指南

说实话,我第一次用Docker Compose之前,也觉得自己挺懂容器化。单容器跑起来没问题,docker run 那一堆参数背得滚瓜烂熟。但一旦项目超过两三个服务——前端、后端、数据库、缓存、消息队列——你就会立刻意识到,手工 docker run 根本不是人能维护的事情。

Docker Compose 解决的就是这个痛点:把多个容器的启动方式、网络关系、数据卷挂载、环境变量全部写进一个声明式配置文件里,一条 docker compose up -d 全部拉起,一条 docker compose down 全部清理。这篇文章我会从核心概念讲起,再带你完整经历一个模拟微服务项目的容器化改造过程——包含后端 API、MySQL、Redis、前端静态资源、定时任务五个服务。无论你是刚接触容器的小白,还是已经在用 Compose 但总在网络上栽跟头的人,这篇都能给你一些可以直接抄作业的东西。

1. 先把概念打牢:Compose 到底解决了什么问题?

1.1 从单容器到多容器,痛点到底从哪来

你单独跑一个 Nginx 容器很简单,docker run -p 80:80 nginx 就完事。但真实项目是多个服务互相依赖:后端要连数据库,数据库要挂数据卷,前端要反代到后端,Redis 要配密码,队列要消费消息。如果用 docker run 一个个启动,你会遇到的问题包括但不限于:

  • 容器启动顺序完全靠人工,今天先起数据库明天忘起 Redis,然后挨个排查依赖失败。
  • 不同容器要共享网络,你得手动 docker network create,再把每个容器加进去,命令又长又容易漏。
  • 环境变量、端口映射、卷挂载分散在十几个 shell 脚本里,换台机器部署就是一场灾难。
  • 想一键停掉所有服务?不存在的,你得一个个 docker stop,还得记得容器名。

Compose 的核心思路非常简单:把容器编排的意图写进代码,而不是记在脑子里或散落在脚本中。它不是一个容器运行时,而是 Docker 官方提供的容器编排工具,早年叫 fig,现在直接内置在 Docker 客户端里,连安装都可以省略。

1.2 Compose 文件的顶层逻辑其实就三块

一份标准的 docker-compose.yml 看起来字段很多,但顶层结构永远只有三大块:

version: "3.8" services: # 你要启动的所有容器,每个容器称为一个“服务” networks: # 自定义网络,让服务通过服务名互访 volumes: # 具名数据卷,实现持久化存储

很多初学者一上来就盯着 services 里的几十个配置项,反而把整体结构搞混。我习惯把一个 Compose 文件当作一张「部署蓝图」:services决定跑什么,networks决定怎么连,volumes决定数据放哪。三者独立定义,再用缩进和引用建立关系,这种松耦合设计让单个服务可以很轻易地挪到别的编排环境里。

要注意一个细节:新版 Docker Compose 已经不太强调 version 字段,老版本的version: "3.8"写了也不会报错,但如果你的 Docker 版本够新,省略它更清爽。这只是历史遗留习惯,不影响功能。

2. 服务、网络、数据卷:三个核心要素逐一拆解

2.1 services 服务定义里最常用的字段长什么样

services 是 Compose 文件的核心,我最常用到的字段是这几个:

字段作用备注
image指定镜像名或镜像仓库地址本地没有时自动拉取
build指定 Dockerfile 所在目录和 image 二选一或同时用
container_name自定义容器名不写的话 Compose 自动生成
ports宿主机端口和容器端口映射格式"8080:80"
environment环境变量敏感信息应改用 env_file
volumes卷挂载支持具名卷和 bind mount
depends_on声明服务依赖有局限,后面细说
restart重启策略no/always/on-failure
command覆盖容器默认启动命令常用于微调启动参数

一个典型的后端服务定义长这样:

services: api: build: ./backend container_name: order-api ports: - "8080:8080" environment: SPRING_PROFILES_ACTIVE: dev volumes: - ./backend/logs:/app/logs depends_on: - mysql - redis restart: always

这里有几个值得展开的点。

ports的写法我建议始终使用"宿主机端口:容器端口"的字符串格式,别写不带引号的8080:8080。在部分 YAML 解析器中,这种写法会被误读为其他类型,虽然新版 Compose 已经修正,但加上引号是最稳妥的习惯。

environment里如果想覆盖默认值,我常用.env文件配合${VAR}插值语法,而不是直接把所有明文写进 compose 文件。比如${MYSQL_PASSWORD:-root123456}表示环境变量里有就用,没有就用root123456做兜底,这种写法在本地和 CI 里都能无缝切换。

2.2 networks 网络模型:服务名就是你的 DNS

Compose 默认会为整个项目创建一个网络,所有服务都加入其中,于是服务之间可以用服务名直接互相访问。这一点很多人会忽略,因为太自然了——你根本不需要去查某个容器的 IP 是多少,只要在代码里写mysql:3306就能连上数据库,写redis:6379就能连上缓存。

如果你希望服务之间隔离,可以自己定义多个网络。比如后端服务需要同时访问数据库和 Redis,前端只需要访问后端,就可以这样设计:

services: frontend: build: ./frontend networks: - frontend-net api: build: ./backend networks: - frontend-net - backend-net mysql: image: mysql:8.0 networks: - backend-net networks: frontend-net: backend-net:

这样前端的请求只能打到api服务上,碰不到数据库。很多人觉得容器化以后安全就不用管了,实际上网络隔离就是一道很好的防线。

还有一点:服务名解析依赖 Docker 内置的 DNS,和容器是否暴露宿主机端口无关。也就是说,api服务即使不配置ports,frontend也能通过api:8080访问它。我经常在测试环境故意不映射端口,只保留服务间通信,以减少宿主机端口冲突。

2.3 volumes 数据持久化:容器删了数据不能跟着没

容器是临时性的,docker compose down之后再up,默认情况下容器内生成的文件会全部消失——包括 MySQL 的数据、Redis 的 RDB 文件、应用上传的图片。要让数据保留下来,就得上卷。

Compose 支持三种挂载方式:

  1. 具名卷(named volume):mysql-data:/var/lib/mysql,由 Docker 管理,存储在宿主机的/var/lib/docker/volumes下,适合数据库这种「我不关心文件放哪,但你别给我删」的场景。
  2. 绑定挂载(bind mount):./config:/app/config,把宿主机目录直接映射进容器,适合配置文件、日志、开发时的热更新代码。
  3. 匿名卷:直接写/container/path,不指定宿主机路径,通常只在 Dockerfile 里使用,Compose 里用得少。

实际项目中我推荐的组合是:MySQL 和 Redis 用具名卷,日志和配置用绑定挂载。字段长这样:

services: mysql: image: mysql:8.0 volumes: - mysql-data:/var/lib/mysql - ./mysql/init:/docker-entrypoint-initdb.d volumes: mysql-data:

注意./mysql/init这种初始化脚本目录:MySQL 官方镜像会在首次启动时自动执行/docker-entrypoint-initdb.d下的所有.sql和.sh文件。这个特性非常适合初始化表结构。缺点是只有第一次启动时执行,后续即使更新脚本文件也不会重跑,我在项目里踩过这个坑,后来都是靠删卷重新 up 来重置。

3. 真实微服务项目:模拟订单系统的容器化全过程

3.1 项目拓扑与镜像准备思路

纸上谈兵没意思,我们直接进入一个模拟项目:一个简化版的订单系统,包含五个服务:

  • frontend:Nginx 托管的静态前端页面,反向代理后端 API
  • api:Java Spring Boot 写的主后端,提供订单接口
  • worker:Python 写的定时任务服务,消费订单状态
  • mysql:MySQL 8.0,存储订单数据
  • redis:Redis 7,做缓存和队列

整体依赖关系是:api 依赖 mysql、redis;worker 依赖 redis;frontend 依赖 api;api 调用 worker 的逻辑是通过 Redis 队列完成。

这个过程最容易犯的错是:想着把现有服务直接扔进容器,结果各种路径、配置文件写死、依赖缺失。我的建议是,先把每个服务单独容器化跑通,再写 Compose 文件整合。如果一上来就五六个服务一起 up,出问题你根本不知道从哪查。

3.2 五服务 Compose 文件实战

我先给出完整的 docker-compose.yml,再逐段说明为什么这么写:

version: "3.8" services: mysql: image: mysql:8.0 container_name: order-mysql restart: always environment: MYSQL_ROOT_PASSWORD: root_pass MYSQL_DATABASE: order_db MYSQL_USER: order_user MYSQL_PASSWORD: order_pass volumes: - mysql-data:/var/lib/mysql - ./mysql/init:/docker-entrypoint-initdb.d ports: - "3306:3306" healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 5s timeout: 3s retries: 10 redis: image: redis:7-alpine container_name: order-redis restart: always command: ["redis-server", "--requirepass", "redis_pass"] volumes: - redis-data:/data ports: - "6379:6379" healthcheck: test: ["CMD", "redis-cli", "-a", "redis_pass", "ping"] interval: 5s timeout: 3s retries: 10 api: build: ./backend container_name: order-api restart: always depends_on: mysql: condition: service_healthy redis: condition: service_healthy environment: DB_HOST: mysql DB_PORT: 3306 DB_NAME: order_db DB_USER: order_user DB_PASSWORD: order_pass REDIS_HOST: redis REDIS_PORT: 6379 REDIS_PASSWORD: redis_pass ports: - "8080:8080" volumes: - ./backend/logs:/app/logs worker: build: ./worker container_name: order-worker restart: always depends_on: redis: condition: service_healthy environment: REDIS_HOST: redis REDIS_PORT: 6379 REDIS_PASSWORD: redis_pass volumes: - ./worker/logs:/app/logs frontend: build: ./frontend container_name: order-frontend restart: always depends_on: - api ports: - "80:80" volumes: - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro volumes: mysql-data: redis-data:

注意一个关键点:depends_on只保证启动顺序,不保证依赖服务已经可用。新版本 Compose 支持了condition: service_healthy这种完整形态——只有依赖服务健康检查通过,才开始启动当前服务。这比原来的死等模式可靠得多,建议直接用。

3.3 各服务的 Dockerfile 与启动命令

后端./backend/Dockerfile示例:

FROM maven:3.8-eclipse-temurin-17 AS builder WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn package -DskipTests FROM eclipse-temurin:17-jre WORKDIR /app COPY --from=builder /build/target/order-api.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]

这里用多阶段构建,先用 Maven 镜像打包,再拷贝到精简 JRE 镜像里。这样最终镜像体积能瘦掉一大半,而且不会把源代码和构建依赖带进生产环境。

前端./frontend/Dockerfile更简单,就是打包静态文件后用 Nginx 托管:

FROM node:20-alpine AS builder WORKDIR /build COPY package*.json . RUN npm ci COPY . . RUN npm run build FROM nginx:alpine COPY --from=builder /build/dist /usr/share/nginx/html EXPOSE 80

worker 是一个 Python 脚本,反复从 Redis 队列里取消息:

FROM python:3.12-alpine WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple COPY worker.py . CMD ["python", "worker.py"]

requirements.txt里只需要redis和python-dotenv两个依赖就够了。跑定时任务时要注意:容器内的 Python 进程必须以前台方式运行,不能直接写死 sleep 循环后退出,否则容器会因为主进程结束而自动停止。

3.4 启动、验证、日志常用命令速查

启动前先检查语法:

docker compose config

这个命令会校验 compose 文件语法,并把最终的配置打印出来,环境变量插值也会被解析成实际值。我每次改配置后都会先跑一遍,能省下大量排错时间。

启动服务:

docker compose up -d

-d是后台运行。想看实时日志就换成docker compose up,或者启动后用:

docker compose ps docker compose logs -f api

验证服务间连通性最直接的方式是进入容器测试网络解析:

docker exec -it order-api ping mysql docker exec -it order-api curl http://frontend

如果 ping 不通某个服务名,优先检查是不是两个服务不在同一个网络里。这是我在实际项目里排查过最多的问题之一。

4. 生产化改造:环境差异、健康检查与安全细节

4.1 多环境配置:override 文件与环境变量插值

真实项目至少要分本地开发、测试、生产三套环境,不可能每个环境都维护一份完整 compose 文件。最佳实践是:docker-compose.yml写通用配置,docker-compose.override.yml写本机开发覆盖,生产环境用-f指定另一份覆盖文件。

默认情况下,执行docker compose up会自动加载同目录下的docker-compose.override.yml,这个文件不需要显式指定。而生产环境我习惯单独建一份docker-compose.prod.yml,部署时手动指定:

docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

开发环境的 override 文件常用这个套路:

services: api: build: ./backend volumes: - ./backend/src:/app/src environment: SPRING_PROFILES_ACTIVE: dev

把宿主机源码挂载进容器,配合后端的热部署插件,就能实现改代码实时生效,不用每次 rebuild 镜像。这个体验非常贴近本地开发,但要注意文件挂载性能在 macOS 上可能不太理想,实测大规模依赖扫描时会明显变慢。

环境变量插值则靠项目根目录的.env文件,Compose 会自动加载。比如:

MYSQL_ROOT_PASSWORD=root_pass MYSQL_DATABASE=order_db

在 compose 文件里写${MYSQL_ROOT_PASSWORD}就能引用。注意,.env文件不要提交到 Git 仓库,养成在.gitignore里加.env的好习惯。

4.2 健康检查:别让 depends_on 成为摆设

前面已经提到condition: service_healthy,这里展开说说健康检查本身。每个服务都可以加healthcheck节点,Docker 会按固定间隔执行检查命令,连续失败达到重试次数就标记服务为 unhealthy。

MySQL 的健康检查命令是mysqladmin ping,Redis 是redis-cli ping,后端 Java 服务没有内置健康检查命令的话,可以用 curl 打它的 actuator 端点:

healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:8080/actuator/health || exit 1"] interval: 10s timeout: 5s retries: 3 start_period: 30s

start_period很关键。像 Spring Boot 启动可能要二三十秒,如果不设置这个字段,健康检查可能因为首次检查时应用还没起完,就直接被标记为 unhealthy。我调试过不少这种“看着明明起来了但服务就是显示不健康”的疑难杂症,原因就是没给足启动缓冲时间。

4.3 资源限制与安全加固:配置项容易漏但很重要

容器如果没有资源限制,就可能出现一个服务吃光宿主机内存的情况。Compose 支持deploy.resources.limits字段,即使不用 Swarm 模式,在本地 Docker 环境也会生效:

deploy: resources: limits: cpus: "1.0" memory: 512M

另一个容易忽略的点是容器运行用户。默认情况 Docker 容器以 root 运行,虽然方便但风险高。官方镜像大多支持user指令,或者干脆装好普通用户。MySQL 和 Redis 官方镜像默认就不以 root 跑主进程,但自己写的应用容器一定要留意这一点:

services: api: user: "1000:1000"

前提是宿主机的用户 ID 与容器内用户能对上,或者镜像里预先创建了同 ID 的用户,否则可能出现数据卷目录没权限写入的尴尬情况。这个我在挂载应用日志目录时踩过坑,后来统一在 Dockerfile 里用USER appuser声明,Compose 侧反而省心。

还要避免把数据库端口暴露到公网,尤其是3306、6379。如果只是服务间通信,完全可以去掉ports映射。万不得已需要远程维护,也尽量限制来源 IP 或用 SSH 隧道。

5. 常见报错与排查心得实录

5.1 端口占用与容器 IP 变动

报错信息基本长这样:

Error response from daemon: driver failed programming external connectivity on endpoint order-api: Bind for 0.0.0.0:8080 failed: port is already allocated

这个不用想太多,就是宿主机端口被占了。排查命令:

lsof -i :8080

找到占用进程后处理掉,或者直接修改 Compose 文件里的端口映射。这种事在本地开发时见得多,因为前一个docker compose up没正确 down,旧的容器还占着端口。可以先跑docker compose down,再确认docker ps里确实没有残留容器。

容器 IP 变动的问题比较隐形。你手动进入容器查到的 IP,和另一个容器访问到的 IP 可能不一样,因为 Docker DNS 是按服务名动态解析的。所以一定要养成在应用配置里写服务名而不是 IP 的习惯。

5.2 数据卷权限与热更新失效

挂载目录后经常遇到 Permission denied,尤其是宿主机目录的 UID 和容器内用户的 UID 不一致时。解决思路有三个:

  1. 改宿主机目录权限,chmod -R 777这种最暴力但别在正式环境用。
  2. 在 Dockerfile 里创建和宿主机 UID 一致的用户。
  3. 在 Compose 里给服务指定user: "UID:GID"。

我一般推荐第二种,改 Dockerfile 最干净,镜像换个机器也不会有问题。

热更新失效是另一类坑:明明把./backend/src挂载进了容器,但改了代码不生效。排查顺序是——先确认路径对不对,再用docker exec进容器看文件内容有没有同步到,最后确认后端框架的 devtools 是否在容器里启用。很多时候是依赖没装全,或启动参数没带 dev 配置,而不是 Compose 的问题。

5.3 健康检查一直失败的排查思路

服务明明能访问,但健康检查一直不过,现象是docker compose ps里 STATUS 列显示unhealthy。我总结了一套排查顺序:

  1. 手动执行健康检查命令,确认它在容器内真的能跑通。很多情况是命令不存在,或者依赖的客户端没装。
  2. 检查端口是容器内端口还是宿主机端口,健康检查命令测试的是容器内地址。
  3. 看start_period是否太短,应用启动慢的先把起始等待期拉长到 30s 以上。
  4. 看应用是否有防火墙或安全组策略,拦截了回环地址的访问,容器内 localhost 访问有时和预想不同。

有一次我排查了半天,最后发现是健康检查命令里用了curl localhost,但应用其实监听在 IPv6 的::1上,curl 默认走 IPv4 自然失败。改成curl http://127.0.0.1:8080/actuator/health后立刻正常。

5.4 问题排查速查表

现象常见原因排查命令/手段
服务起不来端口被占、镜像不存在、启动命令错误docker compose ps、docker logs
服务间连不上不在同一网络、服务名拼错docker exec 进入容器 ping 服务名
数据丢失没挂卷或卷被误删docker volume ls、检查 compose 中 volumes
权限不足UID 不一致docker exec 查看容器内用户、ls -l 宿主目录
健康检查失败命令不对或 start_period 过短手动在容器内执行检查命令
配置不生效环境变量拼写错误或插值未解析docker compose config 查看解析结果

6. 从 Compose 到更复杂的编排:下一步往哪走

6.1 单机 Compose 的边界,心里要有数

Compose 擅长的是单机编排,一台宿主机上把服务安排得明明白白。但如果你的服务需要横向扩展到多台机器,或者需要一个调度器来自动处理某节点宕机后容器迁移,Compose 就力不从心了。这时候要么上云托管服务,要么引入容器编排平台。但从 Compose 过渡过去并不难,因为 Compose 文件里的 services、networks、volumes 概念和编排平台中的工作负载、网络策略、持久卷声明几乎一一对应。

我见过很多团队用 Compose 把一个系统的可复现性做得很好,即使规模不大,也建议先把 Compose 的配置管理、资源限制、健康检查这套基本功打扎实。这些经验在未来换到任何集群环境都不浪费。

6.2 日志采集与配置管理:还没被解决的两个问题

单机 Compose 默认情况下,日志是写到 Docker 的 JSON 文件里,docker compose logs能查历史日志,但服务一多、流量一大,日志文件就会不断膨胀,轮转策略也需要手动配置。我建议在 Compose 里给每个服务加日志限制:

logging: driver: json-file options: max-size: "20m" max-file: "5"

配置管理则是另一个被低估的问题。Compose 文件的 env_file 和 .env 已经解决了一部分,但密钥管理、动态配置下发这些需求,单靠 Compose 做得很初级。实操上我一般把敏感配置先交给专门的秘密管理方案,Compose 里的环境变量只保留非敏感部分,或者启动时拉取并注入成临时文件。

6.3 给新手的上手路径建议

我的体验是,学 Compose 最快的方式不是读文档,而是把一个现有项目一步步容器化。一开始别追求所有服务都用 Compose 管理,先把最核心的后端和数据库容器化了,跑通之后再加入 Redis、前端、队列、定时任务,每加一个服务解决一个新的实际需求。

过程中你会自然碰到网络、数据卷、健康检查、资源限制这些绕不开的坎,每解决一个,你对容器编排的理解就深一层。

7. 最后再分享两个实操心得

第一个心得是关于「清理」。我几乎每周都会遇到因为上次实验没清理干净导致环境异常的情况。所以现在养成了习惯:每次动手前先docker compose down加docker volume prune确认清理一致,然后再up。虽然多花几秒,但能避免很多莫名其妙的问题。

第二个心得是镜像标签。不要在 compose 文件里写latest,也不要在代码里依赖latest。镜像一旦更新,同样的image: mysql:8.0拉到的内容可能已经变了,复现环境就会不稳定。我现在都会锁定具体的版本标签,哪怕是本地测试也会用mysql:8.0.36这种明确的版本号。这个习惯在排查问题时帮过大忙——当你发现“昨天还能跑,今天就不行了”,很可能就是镜像版本悄悄变了。

带过不少人从零入门容器化,几乎每个人都倒在了同一个地方:只盯着命令和配置,没有先想明白「服务怎么连、数据怎么存、依赖怎么等」这三个问题。先把这三件事想清楚,再回头写 compose 文件,你会发现自己不需要背任何语法,一切都很自然。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 10:24:22

LSTM参数量计算全解析:从单层到多层双向的完整推导

1. 为什么需要手动计算 LSTM 参数量 很多刚接触循环神经网络的朋友,在搭建模型时习惯性地调用框架封装好的 API,一行代码就把 LSTM 层加到网络里,然后直接看模型摘要里输出的参数总量。这样做当然没问题,但一旦遇到下面几种情况&a…

作者头像 李华
网站建设 2026/10/16 10:23:49

BrowserSkill:基于CDP的命令行浏览器自动化工具

1. BrowserSkill不是浏览器,而是浏览器能力的“指挥官”你搜“BrowserSkill下载”,点开一堆页面,发现既不是Chrome插件商店里的扩展,也不是Edge官网能直接安装的组件——它压根就不是个浏览器,更不是什么“浏览器精简版…

作者头像 李华
网站建设 2026/10/16 10:22:57

MPU6050 DMP读取实战:不写卡尔曼滤波,四元数欧拉角照样输出

简介:一套面向嵌入式开发者的MPU6050工程示例,利用传感器内置DMP实现四元数与pitch、roll、yaw姿态角的直接读取,省去自行完成姿态解算的复杂计算,也规避了万向锁问题。DMP库负责数据融合,输出经校正的四元数&#xff…

作者头像 李华
网站建设 2026/10/16 10:22:56

C++实现进程伪装:修改PEB与Ldr让进程改名换姓

简介:资源是一份C编写的进程伪装工具PEBFake,面向安全研究、逆向工程和Windows底层开发人员,核心围绕进程环境块(PEB)展开;工具通过修改PEB中的ImagePath、ProcessName等关键字段,隐藏或篡改进程…

作者头像 李华
网站建设 2026/10/16 10:22:33

HBuilderX编辑器打包成APP

HBuilderX编辑器打包成APPHBuilderX编辑器打包成APPHBuilderX打包IOS全过程HBuilderX编辑器打包成APP 作者:梦想艳雪 点我跳转链接:https://www.jianshu.com/p/495a87607414 来源:简书 著作权归作者所有。商业转载请联系作者获得授权&#x…

作者头像 李华
网站建设 2026/10/16 10:20:55

Linux连接跟踪表与conntrack命令实战:原理、排障与性能调优

1. 连接跟踪表到底在跟踪什么很多刚接触Linux网络运维的朋友,第一次听到“连接跟踪表”这个词,脑子里浮现的往往是“这不就是个看网络连接的命令吗,跟netstat差不多吧”。我一开始也这么想,直到有一次线上环境出现大量丢包&#x…

作者头像 李华