一、Docker 核心概念
- 镜像:只读分层的文件系统模板(模板,不运行服务)
- Dockerfile:写构建镜像指令(FROM、COPY、ENTRYPOINT),执行
docker build读取 Dockerfile 生成镜像。 - 示例:
docker build -t oj-app:v1 .,.代表当前上下文目录;多 Dockerfile 用-f指定文件。
- Dockerfile:写构建镜像指令(FROM、COPY、ENTRYPOINT),执行
- 容器:基于镜像模板,创建出来的运行实例
docker run:新建容器并且直接启动(= docker create + docker start)docker start:只启动已经创建、停止的容器,不能新建docker create:只创建容器,不启动docker restart:先 stop,再 start 已经存在的容器docker ps:查看正在运行容器;docker ps -a查看全部(含停止)--restart=always:容器进程异常退出自动重启;但仅单机有效,整机宕机就失效
- 数据持久化:容器删除,容器内写入数据默认丢失;数据卷 - v 挂载用来保存数据。
- ARMS 监控 agent:
aliyun-java-agent.jar,独立 jar,不打进业务 jar。Dockerfile 里 COPY 把 agent 和业务 jar 一起放进镜像;启动参数-javaagent要写在-jar前面,JVM 加载探针,无侵入采集链路指标。
二、Docker Compose & Dockerfile 区分
- Dockerfile:构建镜像(FROM、COPY、ENTRYPOINT)
- docker-compose.yml:编排多个容器,一次性批量创建、启动多个容器,底层自动执行类似 docker run,单机使用。
三、K8s(多机器容器编排)
docker-compose 只能单台服务器管理容器;K8s 管理多台服务器组成的资源池。 能力:资源调度、Pod 监控、Pod 故障自动重建、弹性扩缩容、滚动更新;不能跨机自动迁移是 docker restart 的短板,K8s 可以做到。K8s 最小调度单元是 Pod,Pod 里面包含容器。
四、Nginx 反向代理 & 负载均衡
前端请求先到 Nginx。
- 普通项目:1 个 Nginx,后面挂载多个 Java 实例,Nginx 分发流量,够用,不用 Nginx 集群。
- 多 Nginx 集群:只有高流量或者业务要求高可用、防止整机宕机才上;上层搭配 SLB(阿里云负载均衡)。
五、项目兜底和防护方案(你准备的回答)
分层:事前预防 + 故障兜底
- 事前预防:接口层限流(Redis+Lua 令牌桶)、熔断(Resilience4j),防止服务雪崩,避免服务崩溃。
- 故障兜底: ① 容器配置
--restart=always,进程异常自动重启容器; ② 多实例部署 + 负载均衡,单个实例故障,流量切到别的正常实例; ③ 监控告警(阿里云 ARMS),出现异常通知人工排查。
短板:
--restart=always仅单机生效,服务器整机宕机无效。
六、Jar 包知识点
SpringBoot 可执行 jar 是压缩包,里面包含 class 字节码、第三方依赖、yml 配置、静态资源;不含 JDK,运行依赖外部 JDK 环境。 agent.jar 是独立探针,不和业务 jar 合并,只是一起放进镜像。
- RabbitMQ吞吐大概万级,吞吐量不算高;消息时延微秒级,时效性好。 支持多种交换机模型,自带死信队列、TTL,可以模拟延迟消息。消息可靠性强,不容易丢消息。 适合业务消息,比如订单通知、短信、内部业务异步解耦,业务量不大、对消息可靠性和实时性要求高的场景。
小纠正:不是很难丢,配置不当照样丢;它的延迟是 TTL+DLX 模拟,原生不支持延迟。
- RocketMQ十万级高吞吐,时延毫秒级;单机可以支持上千个 Topic,高可用。 Java 语言开发,源码易读,排查 bug、二次定制开发很方便。 支持事务消息、消息回溯、死信、消息重试。 适合互联网核心业务:下单、支付、积分这类业务消息,高并发业务链路,需要分布式事务能力的场景。
- Kafka十万级超高吞吐,顺序能力强,主打日志采集。 设计上优先追求高吞吐,消息堆积能力极强。 适合大数据场景:日志收集、埋点上报、流式数据传输; 缺点:业务层面的消息重试、事务支持弱,一般不建议用来做订单、支付这类核心业务消息。
二、Tomcat 线程池
三、JDK 原生 ThreadPoolExecutor
四、Spring 配置类写法 & 两种使用方式
线程池本身是同一个,只是使用方式不同。
五、Docker 整套流程(FinalShell 操作)
六、COPY 和 挂载 -v
七、挂载的作用(重点易错点)
八、宝塔面板
宝塔只是 Docker 图形化界面封装,底层逻辑不变:
一、@Async 默认线程 & 系统线程原理
@Async不指定线程池,默认SimpleAsyncTaskExecutor,每次提交任务新建线程,不是直接拿 Tomcat 的工作线程。- Tomcat 线程、自定义异步线程、GC 线程,全部都是 JVM 向操作系统申请的原生线程,共用整机 CPU、内存、系统线程配额。
- SimpleAsyncTaskExecutor 无上限创建线程:线程太多,上下文切换开销暴涨、瓜分 CPU 时间。Tomcat 线程数量虽然不变,但是分到的 CPU 时间变少,接口响应变慢。
- 目的:自定义独立线程池,做资源隔离,控制异步线程上限,避免把服务资源打垮。
- Tomcat 是自己单独实现的线程池,没有核心线程这个概念。
- 参数:
threads.max最大工作线程、min-spare最小空闲线程;空闲线程超时可以回收。 - 和 JDK
ThreadPoolExecutor不一样,JDK 的核心线程默认不会回收。 - 构造 7 个参数:核心线程数、最大线程数、空闲存活时间、时间单位、阻塞队列、线程工厂、拒绝策略。
- 任务执行顺序:核心线程满 → 进阻塞队列 → 队列满才扩容到最大线程数 → 触发拒绝策略。
- 提交方法:
execute():无返回值submit():有返回值,返回 Future
- 4 种拒绝策略:
- AbortPolicy(默认):抛异常
- DiscardPolicy:丢掉新任务,不抛异常
- DiscardOldestPolicy:丢弃队列里等待最久的老任务,尝试放入新任务
- CallerRunsPolicy:提交任务的线程来执行任务,不会中断它正在跑的任务
@Configuration配置类,@Bean返回ThreadPoolExecutor。- 使用方式:
- 搭配
@Async("beanName"):Spring 自动提交异步任务 - 注入线程池对象,代码手动 execute/submit,不需要 @Async
- 搭配
- FinalShell 连接服务器,上传 Dockerfile + jar 包到同一个文件夹。
docker build -t 镜像名:版本 .构建镜像;Dockerfile 不能直接执行,是给 build 读取的脚本,末尾.代表构建上下文。docker run:基于镜像新建容器并且启动,后面跟镜像名,这里配置-v挂载、端口映射。docker start / stop:启停已经存在的容器,后面跟容器 ID / 容器名,不能修改挂载、端口。Dockerfile COPY:build 构建阶段,把文件复制打包进镜像,生成独立副本;镜像固化文件,宿主机原文件改动不会影响镜像。- 挂载
-v 宿主机路径:容器内路径:run 创建容器的时候配置,只是映射绑定,不会复制文件,只有同一份数据。容器读写容器内路径,本质操作宿主机文件。 - 挂载配置绑定在容器上,后续 stop、start 自动沿用挂载。想要修改挂载,只能删掉容器,重新 docker run。
- ❌ 误区:重启容器丢文件
- ✅ 正确:单纯
docker restart容器,容器内部文件还在;docker rm 删除容器,容器读写层里面没有挂载的数据才会丢失。 - 挂载就是用来持久化日志、业务文件,容器删了宿主机上文件还在。
- 只有挂载的路径走宿主机;容器里其他没有挂载的目录,数据还是存在容器内部读写层。
- 页面【创建容器】=
docker run - 列表里启动 / 停止按钮 =
docker start / stop - 已经建好的容器不能直接改挂载,需要删除重建。
反射的动态,一方面是运行时可以选择不同的类,不受硬编码类名限制;另一方面,反射代码可以放在任意位置,我自己控制什么时候执行、要不要执行创建对象、调用方法。
1. 静态变量(static 变量)
核心定义:属于类本身,不属于某个具体的对象。全局共用一份。
为什么用:当需要记录整个系统全局共享的数据时用(比如你代码里的 WebSocket 在线人数、全局连接池)。
注意点:因为大家共用一份,多线程同时修改时会有线程安全问题(所以你要用
AtomicInteger和CopyOnWriteArraySet)。
🎯 2. 静态方法(static 方法)
核心定义:属于类本身,不需要
new对象就能直接通过类名.方法()调用。不消耗太多内存为什么用:为了方便多处共用同一个方法逻辑,且这个方法不需要访问对象独有的属性(比如工具类
Math.max()、你写的WebSocketServer.getOnlineCount())。注意点:静态方法里面不能用
this,也不能直接访问非静态的成员变量。
. 先看现象、抓指标(第一步!很多人跳过)
先确认基础信息,避免瞎猜:
- 是所有请求都慢,还是偶尔慢?
- 是整体 RT 拉高,还是只有个别用户 / 特定参数慢?
- 有没有报错、超时、重试? 看板重点:RT、QPS、错误率、线程池状态、CPU、内存、GC、连接数。
比如 CPU 打满,那大概率不是 IO 阻塞,是 CPU 密集(序列化、循环、复杂计算、大对象解析),你刚才思路里这块是缺的。
2. 链路追踪,定位慢节点(skywalking/pinpoint)
看链路火焰图,直接看耗时落在哪个阶段:
- MySQL 查询
- Redis 操作
- 第三方 RPC/HTTP 远程调用
- 本地业务计算、循环、解析
- 等待锁(synchronized、ReentrantLock、分布式锁)
你说的查库、远程调用就在这一步。
👉 如果慢在数据库:
- 拿 SQL 去 explain,看有没有走索引、回表、全表扫描
- 看是否锁等待:行锁、表锁、事务太长(事务开太大,迟迟不提交,锁住资源)
- 是不是一次性查大量数据,没分页,返回超大结果集
- 连接池耗尽,拿连接都要排队
👉 如果慢在远程调用(HTTP/RPC):
- 下游服务本身慢
- 没设置超时,阻塞等待
- 串行调用多个下游,可以改成并行(CompletableFuture)
- 重试策略不合理,失败反复重试,雪崩放大耗时
- 没有降级、熔断保护
👉 如果慢在 Redis:
- 大 key、热 key
- 网络延迟
- 命令复杂度高(keys、hgetall)
- Redis 连接池耗尽
3. 线程 / 阻塞问题(你关注的 IO 阻塞这块)
- Tomcat / 业务线程池被阻塞耗尽(全部卡在 DB、第三方等待,新请求排队)
- 同步阻塞调用没隔离,没有单独线程池隔离慢 IO
也就是你说的:阻塞 IO 没剥离,都占着 web 容器线程
4. 流量和防护层面(限流、扩容、异步)
- 流量突增,超出服务承载,没有限流,把 DB / 下游打垮
- 本该异步的逻辑写在主链路里(消息通知、日志、非核心统计),同步等待
- 没有削峰:适合异步的场景没改成 MQ 异步解耦
5. 容易漏掉的几个高频坑(面试很爱问)
- GC 问题:频繁 YGC / FullGC,STW 停顿,接口断断续续变慢(这个不是 IO 问题!)
- 锁竞争:大量线程抢同一把锁,自旋、排队,表面看没有 IO,但是卡住
- 序列化 / 反序列化:返回超大对象,JSON 序列化耗 CPU
- 循环里面重复查库、重复调用远程(N+1)
- 连接池配置过小(数据库连接池、http 连接池),请求排队拿连接
6. 优化方向总结(对应排查结果)
- IO 慢:加索引、优化 SQL、拆分大事务;远程调用加超时、并行、缓存
- 流量大:限流、熔断、降级、缓存(本地缓存 + Redis)、MQ 异步剥离非核心逻辑
- 线程阻塞:阻塞 IO 放到独立弹性线程池隔离,不要占用容器工作线程
- CPU 高:减少循环、避免大对象、优化序列化、减少不必要计算