运维常见题-网站维护
DeepSeek V4 Flash 辅助生成。为确保内容的可靠性,笔者已对大部分关键论点进行了人工复核与校验。但鉴于大模型的固有局限,本文仍可能存在认知偏差或未尽准确之处。若您在阅读中发现存疑或矛盾之处,欢迎反馈讨论,笔者将及时核实与修正。🤔 简述 HTTP/1.0、1.1、2、3 区别?
HTTP协议经历了四代演进,每一代解决前一代的核心痛点:连接复用、并发能力、传输效率。HTTP/1.0(1996,RFC 1945)- 特点:每个请求/响应使用一个独立的
TCP连接,请求完成后连接立即关闭(短连接) - 问题:一次页面加载需要多个资源(
HTML、CSS、JS、图片),每个资源都要新建TCP连接,TCP握手(尤其HTTPS的TLS握手)开销巨大 - 其他:无
Host头(同一IP上无法在HTTP层区分多个域名,即虚拟主机);缓存控制简陋(仅有Expires/Pragma/Last-Modified,无Cache-Control/ETag)
- 特点:每个请求/响应使用一个独立的
HTTP/1.1(1997首版RFC 2068,1999定稿RFC 2616,现为RFC 9110/9111/9112)- 持久连接(
Keep-Alive) :默认复用同一个TCP连接发送多个请求,解决频繁握手问题 Host头:支持一个IP上部署多个域名(虚拟主机)- 管线化(Pipelining):客户端可以连续发送多个请求而不必等前一个响应——但由于响应必须按请求顺序返回(
FIFO),一个慢响应会阻塞后面的所有响应(队头阻塞),实际很少启用 - 其他:
chunked传输编码、更完善的缓存控制(Cache-Control、ETag) - 核心问题:队头阻塞(
Head-of-Line Blocking) ——同一连接上的请求必须串行处理,一个响应慢,后续全等
- 持久连接(
HTTP/2(2015,RFC 7540,现为9113)- 二进制分帧:把
HTTP消息拆成二进制帧(HEADERS、DATA等),替代HTTP/1.1的纯文本格式 - 多路复用(
Multiplexing) :多个请求/响应可以在同一个TCP连接上并行交错传输,解决了应用层的队头阻塞 - 头部压缩(
HPACK) :压缩首部(请求和响应两侧),减少重复头部传输 - 服务器推送(
Server Push) :规范层面仍保留(RFC 9113 §8.4),但Chrome 106(2022-09)、Firefox(2022)已移除实现、Safari从未实现,实际未普及;HTTP/3无此功能,业界替代是103 Early Hints(RFC 8297) - 流优先级:
RFC 7540的依赖-权重方案已被RFC 9218(Extensible Prioritization,2022)取代(原方案"并不成功") - 局限:虽然解决了应用层队头阻塞,但
TCP层队头阻塞仍然存在——TCP保证数据有序,一个TCP包丢失会导致后续所有包等待重传,即使它们属于不同的请求流
- 二进制分帧:把
HTTP/3(2022,RFC 9114)- 传输层改用
QUIC协议(RFC 9000,承载于UDP之上)替代TCP - 解决
TCP层队头阻塞:QUIC在用户空间实现可靠传输和有序交付,但丢失的包只影响它所在的流,其他流不受影响 0-RTT连接恢复:基于TLS 1.3会话恢复票据(PSK),第二次连接可免握手直接发送数据(注意0-RTT有重放风险,RFC 9001)- 连接迁移:
QUIC用连接ID标识连接,IP地址变化(如Wi-Fi切到移动网络)连接不中断 - 内建
TLS 1.3:加密是QUIC的强制部分,没有明文HTTP/3 - 头部压缩用
QPACK(RFC 9204)而非HPACK(应对乱序到达的首部) - 部署现状:
W3Techs 2026-08数据显示约40%的网站已支持HTTP/3
- 传输层改用
四代对比速览
1.0:短连接,一请求一连接1.1:持久连接,但有队头阻塞2:多路复用(解决应用层队头阻塞),基于TCP3:多路复用 + 解决传输层队头阻塞,基于UDP/QUIC
协助记忆
- 一代一痛点:
1.0连接费(短连接)→1.1连接省(持久连接)但排队(队头阻塞)→2并行(多路复用)但堵在TCP→3换路(QUIC/UDP)彻底不堵 - 队头阻塞有两个层面:应用层(
HTTP/1.1的FIFO)和传输层(TCP包丢失重传) ———HTTP/2解决前者,HTTP/3解决后者
- 一代一痛点:
进阶思考
为什么
HTTP/2多路复用后还要升级到HTTP/3?HTTP/2解决的是应用层队头阻塞——多个请求可以在一个TCP连接上并行,不需要按序等待。但TCP本身有传输层队头阻塞:TCP保证数据按序交付,如果某一个TCP段丢失,接收方必须等待该段重传才能继续处理后续数据,即使这些数据属于完全不同的请求流。在丢包率高的弱网环境(移动网络)这个问题尤其严重。HTTP/3用QUIC(UDP)解决了这个问题——每个流独立处理,一个流的丢包不影响其他流
HTTP/3用了UDP,可靠性和顺序性怎么保证?QUIC在UDP之上自己实现了可靠传输和有序交付——它有类似TCP的序号、确认、重传机制,但作用域是"流"而不是整个连接。每个流独立编号、独立确认、独立重传,所以一个流的丢包只重传该流的数据,不影响其他流。这也是QUIC解决队头阻塞的本质:把TCP的"全局有序"变成QUIC的"流内有序"
🤔 HTTP 有哪些常见的状态码?
HTTP状态码是服务器在响应中返回的三位数字,用于告知客户端请求的处理结果。按首位数字分为五类:1xx(信息)、2xx(成功)、3xx(重定向)、4xx(客户端错误)、5xx(服务器错误)。1xx:信息性响应100 Continue:客户端可以继续发送请求体101 Switching Protocols:协议切换(如升级到WebSocket)103 Early Hints:提前发送提示(如预加载资源链接),配合301/308重定向场景做性能优化- 日常运维很少直接见到
1xx,多由协议层内部处理
2xx:成功200 OK:请求成功,最常见201 Created:资源创建成功(常用于POST创建资源)202 Accepted:请求已接受但尚未处理完成(异步任务)204 No Content:请求成功但无内容返回(常用于DELETE、以及POST/PUT的"成功无内容"场景)
3xx:重定向301 Moved Permanently:永久重定向,后续请求应使用新URL(默认允许缓存,受Cache-Control控制)302 Found:临时重定向,后续请求仍用原URL303 See Other:方法强制改为GET(SHOULD use GET,RFC 9110 §15.4.4)——常用于表单提交后重定向到结果页304 Not Modified:资源未修改,可使用缓存(配合If-Modified-Since/ETag)307 Temporary Redirect:临时重定向,且保留请求方法和请求体308 Permanent Redirect:永久重定向,且保留请求方法和请求体- 方法语义完整图谱:
301/302(MAY改方法)→303(SHOULD用GET)→307/308(必须保留方法)
4xx:客户端错误400 Bad Request:请求语法错误,服务器无法或不愿处理401 Unauthorized:未认证(未登录或凭据无效)。注意:401是"没证明你是谁",403是"不让进"403 Forbidden:服务器理解请求但拒绝处理/授权——常因权限不足,但也可能是未认证、IP封禁、WAF拦截;未认证时服务器也可能返回403而非401(如不想暴露资源存在性)404 Not Found:资源不存在,最常见405 Method Not Allowed:请求方法不被允许(如对只读接口发POST)408 Request Timeout:请求超时409 Conflict:请求与当前资源状态冲突(如版本冲突)410 Gone:资源已永久删除(区别于404的"不存在",410是"曾经存在但没了")413 Content Too Large:请求体过大(RFC 9110新名,旧名Payload Too Large仍广泛使用)429 Too Many Requests:请求过多,触发限流(RFC 6585定义,常配合Retry-After头)
5xx:服务器错误500 Internal Server Error:服务器内部错误,通用错误码501 Not Implemented:服务器不支持请求的方法502 Bad Gateway:网关/代理收到上游服务器的无效响应(Nginx作为反向代理时常见)503 Service Unavailable:服务暂时不可用(过载、维护中),常配合Retry-After头,通常过一段时间会恢复504 Gateway Timeout:网关/代理等待上游响应超时
协助记忆
五位分类:
1信、2成、3转、4客户错、5服务器错易混三对:
301vs308:都是永久,但301方法可能变(POST→GET),308保留方法302vs307:都是临时,302方法可能变,307保留方法401vs403``:401是"你是谁",403是"不让进"(注:403不要求已认证,是"拒绝授权")
502vs503vs504:502是上游答了但答错(无效响应)、503是自己没空(过载)、504是上游没答(超时)
进阶思考
301和308到底有什么区别?什么时候用哪个?- 核心区别是请求方法是否保留。
301(和302)在重定向时,很多浏览器和客户端会把POST请求改为GET请求(RFC使用MAY,历史行为普遍存在);303明确要求改用GET;308(和307)明确要求保留原方法和请求体。实践中:永久迁移且希望客户端用新URL重新发起GET请求,用301;API场景希望POST/PUT的请求体和方法原样转发到新地址,用308。现代Web的API重定向推荐308/307以保证语义一致
- 核心区别是请求方法是否保留。
反向代理场景,
502、503、504分别对应什么排查方向?502(Bad Gateway):上游服务器响应了但响应无效——如上游进程崩溃返回空响应、返回了非法响应格式。排查上游应用是否挂了、端口是否监听。503(Service Unavailable):本机(代理)或上游过载/维护——如后端连接数打满、限流触发。排查后端负载、连接池。504(Gateway Timeout):上游没有及时响应——如后端处理超时、慢查询。排查后端应用延迟、数据库慢查询。- 一句话:
502是上游答了但答错,503是没空,504是没答
🤔 浏览器访问域名经历了哪些流程?
从在地址栏输入域名到页面显示,浏览器经历了一条完整的链路:
URL解析 →DNS解析 →TCP连接 →TLS握手(HTTPS)→HTTP请求 → 服务器处理 →HTTP响应 → 浏览器渲染。每一步都有对应的可排查点。第一步:
URL解析- 浏览器解析输入的
URL,拆分成协议(https)、域名(www.example.com)、端口(默认443)、路径(/index.html)、查询参数等 - 判断协议是否合法、域名格式是否正确
- 浏览器解析输入的
第二步:
DNS解析(把域名变成IP)- 按缓存层级逐级查找:浏览器缓存 → 系统解析器(
hosts文件、系统缓存、DNS服务器的相对顺序由操作系统决定 ———Windows通常是hosts→DNS Client缓存 →DNS服务器,Linux按nsswitch.conf默认files dns通常hosts优先) - 客户端向递归
DNS服务器发起递归查询;递归解析器对根DNS→ 顶级域DNS(.com)→ 权威DNS(example.com的NS)的逐级查询称为迭代查询(浏览器本身不逐级问根服务器) - 细节:现代浏览器和系统可能启用
DNS-over-HTTPS(DoH,RFC 8484),DNS查询走HTTPS加密通道
- 按缓存层级逐级查找:浏览器缓存 → 系统解析器(
第三步:
TCP连接(三次握手)- 拿到
IP后,浏览器与服务器建立TCP连接:SYN→SYN-ACK→ACK(RFC 9293) - 涉及
TCP相关优化:Fast Open(TFO)、连接复用(keep-alive);若启用HTTP/3,TCP+TLS合并为QUIC一次握手
- 拿到
第四步:
TLS握手(HTTPS专属)HTTPS下进行TLS握手(以TLS 1.3为主,现行规范RFC 9846):- 客户端发送
ClientHello(支持的加密套件、TLS版本、key_share密钥共享) - 服务器返回
ServerHello(含密钥共享)、证书;证书在加密消息中发送(EncryptedExtensions→Certificate→CertificateVerify→Finished) - 客户端验证证书(信任链、域名匹配、有效期)
- 双方确认,握手完成,开始加密通信
- 客户端发送
注:
TLS 1.2流程是"独立的密钥交换回合(ECDHE等)",TLS 1.3把密钥共享直接放进ClientHello/ServerHello,更快更简洁TLS 1.3首次握手1-RTT、会话恢复0-RTT(注意0-RTT有重放攻击风险,规范要求默认禁用,浏览器默认不开early data)
第五步:发送
HTTP请求- 浏览器构造
HTTP请求(方法、URL、头部、cookie),通过已建立的加密通道发送 - 经过
CDN时,请求先到最近的CDN节点,CDN命中缓存则直接返回,未命中则回源
- 浏览器构造
第六步:服务器处理
- 请求到达服务器(或反向代理
Nginx),经过:负载均衡 →Web服务器 → 应用代码 → 数据库/缓存查询 - 服务器生成响应(状态码、响应头、响应体)
- 请求到达服务器(或反向代理
第七步:
HTTP响应返回- 服务器返回响应:状态码(
200/404/500等)、响应头(Content-Type、Cache-Control、Set-Cookie等)、响应体(HTML) - 浏览器判断状态码:
2xx正常渲染,3xx跟随重定向(重新走流程),4xx/5xx显示错误页
- 服务器返回响应:状态码(
第八步:浏览器渲染
- 解析
HTML构建DOM树 - 解析
CSS构建CSSOM DOM+CSSOM合并生成渲染树(Render Tree) (注:Render Tree是WebKit/Blink的实现术语,非统一Web标准定义,Gecko叫Frame tree)- 布局(Layout/Reflow) :计算每个元素的几何位置
- 绘制(Paint) :绘制到屏幕
- 合成(Composite) :把各层合并呈现 ———
transform/opacity动画只触发合成,不触发重排重绘 - 脚本与样式对渲染的影响:
<script>是parser-blocking(阻塞DOM解析进而阻塞渲染,除非async/defer;<script type="module">默认defer),但现代浏览器有预加载扫描器提前并行下载脚本(下载并行、执行阻塞);CSS是render-blocking但不阻塞DOM构建 JavaScript执行修改DOM/CSSOM会触发重排(Reflow)和重绘(Repaint)
- 解析
协助记忆
- 八步链路:拆
URL→ 查DNS→ 连TCP→ 握TLS→ 发请求 → 服务器处理 → 收响应 → 渲染页面 - “三握一握”:
TCP三次握手 +TLS一次握手(HTTPS专属) - 渲染五步:
DOM(结构)→CSSOM(样式)→Render Tree(合并)→Layout+Paint(画出来)→Composite(合成呈现) - 排查口诀:访问慢,先看
DNS快不快,再看TCP/TLS握多久,再看服务器响应时间,最后看渲染卡不卡
- 八步链路:拆
进阶思考
为什么
HTTPS站点首次访问比HTTP慢?慢在哪?HTTP只需TCP三次握手(1个RTT);HTTPS除了TCP还要TLS握手 ———TLS 1.2需要2个RTT、TLS 1.3需要1个RTT(首次),加上TCP总共2~3个RTT才能发出第一个请求。RTT越大(物理距离越远),差距越明显。优化手段:TLS 1.3会话恢复(0-RTT,需权衡重放风险)、HTTP/3(QUIC把TCP+TLS合并为一次握手)、连接复用与TFO。在HTTP/3或连接复用场景下HTTPS与HTTP差距已不明显,但全新连接下HTTPS仍多约1个RTT
输入域名后浏览器直接显示"无法访问此网站",最可能是哪一步出了问题?
- 按经验上最常见的顺序:一是
DNS解析失败(域名拼错、DNS服务器故障、本地hosts被改)——浏览器提示"找不到服务器IP地址";二是TCP连接失败(服务器宕机、端口不通、防火墙拦截)——— 提示"连接超时"或"连接被拒绝";三是TLS握手失败(证书过期、不受信任)——提示"您的连接不是私密连接";四是服务器返回5xx错误(有响应但处理失败)。浏览器通常给出具体错误提示,根据提示即可定位到是哪一步
- 按经验上最常见的顺序:一是
🤔 HTTP 请求头和响应头有哪些内容?
HTTP头部(Header)是请求/响应中"键值对"形式的元数据,用于传递请求上下文、内容描述、缓存策略、认证信息等。现代分类(RFC 9110)不再使用传统的"通用头/实体头"分类,头部按用途分为请求头、响应头、表示头、内容头等;本文按常见场景组织,仍会说明新旧分类对应关系。请求行 / 状态行结构(头部之前)
- 请求行:
GET /index.html HTTP/1.1(方法 + 路径 + 版本) - 状态行:
HTTP/1.1 200 OK(版本 + 状态码 + 原因短语)
- 请求行:
请求头(客户端 → 服务器)
Host:目标主机和端口(HTTP/1.1必需,虚拟主机依据)User-Agent:客户端标识(浏览器/版本/操作系统)Accept:客户端可接受的内容类型(如text/html、application/json)Accept-Encoding:客户端支持的压缩算法(gzip、br、zstd——2024-2025主流浏览器已支持)Accept-Language:客户端语言偏好Content-Type / Content-Length:请求体类型和长度(POST/PUT时;注意这属于"表示头/内容头",同一字段在请求和响应中含义一致)Authorization:认证凭据(Bearer token、Basic)Cookie:客户端携带的cookie(RFC 6265定义,注意不是RFC 9110)Referer:来源页面URL(注意拼写是Referer而非Referrer)Origin:跨域请求的来源(协议+域名+端口,CORS用)X-Forwarded-For:经过代理时的原始客户端IP(非标准头,事实标准;标准替代为Forwarded,RFC 7239)
响应头(服务器 → 客户端)
Content-Type / Content-Length:响应体类型和长度Set-Cookie:服务器设置 cookie(RFC 6265 定义,含 Secure、HttpOnly、SameSite 等属性)Location:重定向目标地址(3xx 响应;也用于 201 Created 返回新资源地址)Cache-Control:响应缓存策略(RFC 9111 定义)ETag:资源版本标识(配合 If-None-Match 缓存验证;也配合 If-Match 做乐观并发控制)Last-Modified:资源最后修改时间(配合 If-Modified-Since)Expires:缓存过期时间(HTTP/1.0 遗留,被 Cache-Control 取代)Server:服务器软件信息Date:响应时间Access-Control-Allow-Origin:CORS 允许的跨域来源Retry-After:多久后重试(配合 429/503)Strict-Transport-Security:强制 HTTPS(HSTS,RFC 6797)
传统分类与新分类的对应(旧文档常见,帮助理解)
- 传统四分类:通用头、请求头、响应头、实体头
RFC 9110已同时废弃"通用头"和"实体头"概念,重构为:请求头、响应头、表示头(Representation,描述资源表示,如 Content-Type、ETag、Last-Modified)、内容头(Content,描述消息内容,如 Content-Length)、验证器字段(Validator,ETag/Last-Modified 单独归为验证器)- 旧教材把 ETag 归入实体头是简化说法,严格按 RFC 2616 ETag 是响应头
常见安全响应头(现代 Web 标配)
CSP(Content-Security-Policy):内容安全策略,限制资源加载来源X-Content-Type-Options: nosniff:禁止 MIME 嗅探X-Frame-Options / frame-ancestors:点击劫持防护Referrer-Policy:控制 Referer 发送策略Permissions-Policy:限制浏览器功能权限COOP/COEP/CORP:跨源隔离相关头
协助记忆
- 请求头记一组:
Host、UA、Accept家族、Cookie、Authorization——“你是谁、要什么、带什么” - 响应头记一组:
Content-Type、Set-Cookie、Location、Cache-Control、ETag——— “给你什么、让你存什么、跳去哪” - 排查缓存问题看这四个:
Cache-Control、ETag、Last-Modified、Expires - 区分两个"内容":
Content-Type(媒体类型)≠Content-Length(字节长度)
- 请求头记一组:
进阶思考
Cookie和Authorization都能传递身份信息,有什么区别?- 机制不同。
Authorization是请求头,每次请求由客户端代码显式携带(如Bearer token),无状态、常用于API。Cookie是浏览器自动管理的状态机制——服务器Set-Cookie后,浏览器自动在后续请求中带上对应域名的cookie,有状态、常用于Web会话。Cookie会自动附加到同域请求(不受代码控制),Authorization需要代码显式添加。安全上:Authorization适合API token,Cookie需配合Secure/HttpOnly/SameSite防窃取
- 机制不同。
Nginx反代场景,如何保证后端能看到真实客户端IP?Nginx在转发请求时把客户端真实IP写入X-Forwarded-For或X-Real-IP头,后端应用从这些头读取。但X-Forwarded-For是可伪造的——客户端可以直接构造这个头。安全做法:在Nginx层用set_real_ip_from+real_ip_header处理,或在信任的代理边界重写X-Forwarded-For(不要直接信任客户端传入的值)。这是反代场景的常见安全坑
🤔 网站显示中文乱码会是什么原因?
中文乱码的根本原因是编码不一致——网页的字节是按
A编码写入的,但浏览器用B编码解读,导致字节序列被错误解码。排查方向就是找出"写入编码"和"读取编码"哪里对不上。编码机制速览(先理解再排查)
- UTF-8:现代标准,兼容
ASCII,中文字符占3字节,全栈默认。2025-2026年UTF-8已占全网超98%,且Encoding标准已把UTF-8定为新协议的强制编码 GBK / GB2312:中文传统编码,中文字符占2字节,旧系统常见(注意GBK是GB2312的超集,现行国标是GB18030)- 乱码的本质:同一串字节,用不同编码解读得到不同字符
- UTF-8:现代标准,兼容
常见原因一:
HTML文件内部声明与实际编码不符HTML文件实际以UTF-8保存,但<meta charset="GBK">声明为GBK(或反过来)——浏览器按声明解码,乱码- 注意
file命令的局限:file能可靠识别UTF-8/UTF-16(靠BOM和字节特征),但对GBK等东亚双字节编码通常只报含糊的 “ISO-8859 text” 或 “data"。排查GBK更可靠的方式:用iconv -f GBK -t UTF-8文件 和iconv -f UTF-8 -t GBK文件 双向试转,看哪个方向能转出正常文字;或用chardet/uchardet检测 - 修复:统一文件保存编码和
meta声明
常见原因二:
HTTP响应头charset与meta声明冲突HTTP响应头Content-Type: text/html; charset=UTF-8与HTML内meta声明不一致时,HTTP头的优先级更高- 排查:
curl -I <url>查看响应头charset;curl -s <url> | head -20查看meta声明 - 修复:让两者一致(改
Nginx/应用配置或改meta) - 补充:还有更高优先级的
BOM——— 文件开头的UTF-8 BOM优先级高于包括HTTP头在内的一切声明。这是"改了meta还是乱码"的另一个隐蔽原因 meta声明必须位于文件前1024字节内才生效
常见原因三:数据库存储编码与连接编码不一致
- 数据库表是
UTF-8,但应用连接数据库时用了错误的连接字符集(如latin1),导致"存进去是乱码"或"读出来是乱码” - 机制(
MySQL):服务器按character_set_client解释客户端发来的字节,再转character_set_connection,最后转目标列字符集;读出的结果由character_set_results决定 - 排查:
SHOW VARIABLES LIKE 'character_set%'查看各字符集配置 - 修复:连接时
SET NAMES utf8mb4(同时设置client/connection/results三个变量);建库时CREATE DATABASE ... DEFAULT CHARACTER SET utf8mb4 - 注:
MySQL 8.0+默认即utf8mb4,旧的utf8(utf8mb3)已废弃、未来大版本移除
- 数据库表是
常见原因四:编码被二次转换
- 数据经过多个环节时,某个环节做了一次错误的编码转换
- 是否可逆取决于原始字节是否被改写:纯显示层乱码(字节仍是原始
UTF-8,只是被按GBK显示)可逆 ——— 著名例证是"锟斤拷" (UTF-8替换符被按GBK显示);只有经真实转码(如iconv转错后落库)才不可逆
常见原因五:页面完全没有编码声明
HTTP头无charset、HTML内也无meta声明时,浏览器按默认(现代浏览器默认UTF-8)尝试解码,非UTF-8字节就会乱码- 这比"用户改了浏览器设置"常见得多——排查时先确认页面是否有编码声明,而不是去查浏览器设置
协助记忆
- 一句话:乱码 = 写的时候用
A编码,读的时候用B编码,对不上 - 读取侧优先级(从高到低) :
HTTP头charset→BOM→meta声明 → 默认UTF-8 - 写入侧检查:文件实际编码(iconv 双向试转)、数据库/连接字符集
- 排查命令:
curl -I看响应头、iconv -f X -t Y文件 试转换、chardet检测 utf8mb4记住:MySQL存emoji必须用utf8mb4而非utf8
- 一句话:乱码 = 写的时候用
进阶思考
HTTP响应头charset和HTML meta charset哪个生效?HTTP响应头的Content-Type: text/html; charset=xxx优先级更高——浏览器先看HTTP头,如果HTTP头没有明确charset,才看HTML内的meta声明。但注意BOM优先级最高:文件开头的UTF-8 BOM会覆盖包括HTTP头在内的一切声明。所以"改了meta还是乱码"要查两个地方:HTTP头里的旧charset、文件是否带BOM
数据库已经是
UTF-8,为什么页面还乱码?- 存储编码和连接编码是两回事。表字符集是
UTF-8只保证数据以UTF-8存,但写入时服务器按character_set_client(连接时声明的客户端字符集)解释应用发来的字节——如果连接用的还是latin1或GBK,写入时就错了。MySQL排查:character_set_server(服务器默认)、character_set_database(当前库)、character_set_client/connection/results(连接链路)。统一SET NAMES utf8mb4+ 建库指定utf8mb4通常能解决
- 存储编码和连接编码是两回事。表字符集是
🤔 网站访问很慢,该如何排查?
网站访问慢的排查要遵循"分层定位"思路:把一次访问拆成多个阶段,先定位慢在哪一层(DNS?网络?服务器?数据库?),再针对该层深入。不要一上来就盯着服务器 CPU——很多"网站慢"其实是网络或前端问题。
第一步:先量化"慢"在哪——用
curl计时curl -o /dev/null -s -w 'DNS: %{time_namelookup}s\nTCP: %{time_connect}s\nTLS: %{time_appconnect}s\nTTFB:%{time_starttransfer}s\nTotal: %{time_total}s\n' https://example.com重要:这些变量是"从
curl启动到该阶段完成的累计时间",不是各阶段本身耗时。要算阶段耗时需做差:DNS解析耗时 =time_namelookupTCP握手耗时 =time_connect−time_namelookupTLS握手耗时 =time_appconnect−time_connect- 服务器处理时间 =
time_starttransfer−time_appconnect(近似)
TTFB(time_starttransfer):从curl启动到收到第一个字节的累计时间,包含DNS查询 +TCP握手 +TLS握手 + 服务器处理(MDN定义)判断方法:
DNS差值长 →DNS问题;TCP差值长 → 网络问题;DNS/TCP/TLS各阶段差值都正常但TTFB仍长 → 服务器处理慢;Total长但TTFB正常 → 响应体传输慢(带宽/大文件)
第二步:按阶段定位
DNS慢:dig + time = 2 example.com看解析耗时,dig @8.8.8.8 example.com对比公共DNS;检查本地DNS服务器、递归解析器、上游权威、DNSSEC;考虑DoH/DoT。注意:智能DNS/CDN解决的是"解析到哪个节点"的调度准确性,不直接加速解析耗时本身- 网络慢:
ping看RTT,mtr看中间链路是否有丢包或高延迟节点;跨地域访问考虑CDN。2025新坑:HTTP/3走UDP,部分网络对UDP做QoS限速/防火墙拦截——如果time_connect长但ping正常,可怀疑UDP被限速 - 服务器响应慢(
TTFB高) :进入服务器端排查(第三步) - 传输慢(
Total−TTFB长) :响应体太大、带宽不足、压缩未开(gzip/brotli)、大图片未优化
第三步:服务器端深入
Nginx 层:
- 先确认日志格式包含耗时字段:默认
combined格式没有$request_time,需要在log_format中显式配置,如log_format main '$remote_addr $request_time $upstream_response_time';,然后 awk '{print $2}' access.log | sort -n | tail -20看最慢的请求(注意取的是配置里的耗时字段列) - 检查反向代理超时配置(
proxy_read_timeout等)、upstream是否有慢节点
- 先确认日志格式包含耗时字段:默认
应用层:
- 看应用日志的请求处理时间,定位是哪个接口慢
- 检查应用是否有死锁、线程池打满、
GC频繁(Java)、GIL阻塞(Python)
数据库层:
- 开慢查询日志,看是否有全表扫描、缺索引的
SQL SHOW PROCESSLIST看是否有长时间运行的查询- 检查数据库连接池是否打满(连接等待导致
TTFB升高)
- 开慢查询日志,看是否有全表扫描、缺索引的
缓存层:
- Redis/缓存是否生效(缓存命中率低会导致每次都打数据库)
- 缓存穿透/击穿/雪崩
资源层:
top/free看CPU、内存是否打满iostat看磁盘IO是否瓶颈- 连接堆积用
ss -s(汇总统计)或ss -ant state established | wc -l看连接数(注意ss -lntp只显示监听中的socket,看不到ESTABLISHED堆积;-lntp中Send-Q列可看backlog溢出)
第四步:前端/浏览器侧(如果服务器很快但用户觉得慢)
Chrome DevTools的Network面板:看每个资源的加载时间,Waterfall(瀑布图)能看出哪些资源是瓶颈Core Web Vitals(2025年核心指标) :LCP(最大内容绘制)、INP(交互延迟,2024年取代FID)、CLS(布局偏移)——用Lighthouse或RUM(CrUX字段数据)审计- 常见前端问题:页面引用了太多大资源、图片未优化、未用
CDN、缓存策略未配置 - 注意:
HTTP/2时代"JS/CSS合并文件"已不必要(多路复用下合并反而有害),现代实践是bundling+code splitting+tree-shaking+ 按需加载 - 补充:
103 Early Hints可提前TTFB、Server-Timing响应头可暴露服务端耗时
协助记忆
- 分层定位口诀:先
curl计时做差分层,再按层深挖 ———DNS慢查解析,TCP慢查网络,TTFB慢查服务器,Total慢查传输 curl变量是累计值,算阶段要"做差":TCP=connect−namelookup,TLS=appconnect−connect,服务器 =starttransfer−appconnect- 排查顺序:网络层 → 服务器层 → 数据库层 → 缓存层 → 前端层,别跳过
- 分层定位口诀:先
进阶思考
TTFB高但服务器CPU/内存都正常,可能是什么原因?TTFB包含网络RTT+ 服务器处理。如果CPU/内存正常,可能的隐藏原因:一是数据库慢查询——请求卡在等数据库返回,但数据库CPU不高(如缺索引全表扫描、锁等待);二是外部依赖慢——应用调第三方API,等外部响应;三是连接池/线程池打满——请求排队等待可用连接;四是磁盘IO——日志或缓存写盘慢。排查:看应用日志确认请求在等什么,必要时用APM(链路追踪)定位耗时分布
用户分布广(全国/全球),如何区分是"网站慢"还是"用户网络慢"?
- 用多点拨测——在不同地域的机器上执行同样的
curl计时(云厂商都有多地拨测工具)。如果所有地域TTFB都高 → 服务器端问题;如果只有某些地域高 → 网络/CDN节点问题(该地域到服务器链路差,或CDN在该地域节点少)。这是"网站慢"与"网络慢"的经典区分方法
- 用多点拨测——在不同地域的机器上执行同样的
🤔 QPS、TPS、PV、UV 是什么?怎么统计?
QPS/TPS是实时性能指标(衡量系统处理能力),PV/UV是业务流量指标(衡量用户访问量)。两者维度不同,但常放在一起讨论。QPS(Queries Per Second,每秒查询数)含义:系统每秒处理的请求/查询数量,衡量系统吞吐能力
注意:
QPS在不同语境含义不同 ———Web场景指每秒HTTP请求数(严格说这是RPS,Requests Per Second),数据库场景指每秒SQL查询数。中文语境常混用,面试和讨论中要明确语境统计方法:
- 监控系统:
Prometheus+nginx-prometheus-exporter(从nginx stub_status取数,nginx_http_requests_total速率为QPS)或应用埋点 Nginx access log:按时间窗口统计日志条数- 压测工具:
ab、wrk、JMeter直接报QPS
- 监控系统:
相关概念:峰值
QPS(扩容依据,行业更常用最大5分钟窗口均值或TP99,而非秒级瞬时毛刺值)vs 平均QPS(容量规划参考)
TPS(Transactions Per Second,每秒事务数)- 含义:系统每秒处理的事务数量。事务是业务层面的完整操作,一个事务可能包含多个请求/查询
- 典型:一次下单 = 一个事务,但可能包含"查库存 + 扣款 + 生成订单 + 通知"等多个请求
- 通常
TPS≤QPS(一个事务由多个请求组成时),但取决于事务定义与接口粒度——批量/批处理接口(一个HTTP请求处理N笔转账)会出现TPS>QPS - 统计方法:应用埋点(在事务完成点打点)、
APM工具 - 适用:电商下单、支付、银行转账等有明确"事务边界"的业务
PV(Page View,页面浏览量)含义:页面被浏览的次数。每次刷新/打开页面都算一次
PV,同一用户重复浏览累加统计方法:
Nginx access log:统计HTML页面请求数(需过滤静态资源、排除非2xx、过滤爬虫)- 前端埋点 / RUM(真实用户监测)
爬虫污染:
2025年AI爬虫(GPTBot、ClaudeBot等)激增,严重污染PV/UV统计,行业标准做法是UA白名单过滤注意
PV↔QPS不能直接换算:1次PV(页面加载)通常产生1个HTML+ 数个静态资源/接口请求(1 PV≈5~20个请求);容量规划估算:峰值QPS≈(日均PV× 页面平均请求数 × 峰值系数)/86400
UV(Unique Visitor,独立访客数)含义:去重后的独立访客数,同一用户只算一次
统计方法(去重口径,不同口径结果不同):
- 基于
Cookie/ 第一方标识:给访客分配唯一ID去重。2025年隐私环境下的主流已演变为"第一方Cookie+ 登录ID+ 设备指纹"的混合识别(GA4等基于first-party标识去重)——— 因为第三方Cookie在Safari/Firefox默认拦截、GDPR/个保法限制下受限(Chrome 2024年放弃强制淘汰第三方Cookie、2025年终止Privacy Sandbox计划) - 基于
IP:按IP去重(不准确 ——NAT后多人同IP算1个;同一人换IP算多个) - 基于用户登录
ID:最准确,但只能统计登录用户
- 基于
注意:
UV是"人"的维度,PV是"次"的维度。PV/UV比值 = 人均浏览页数 ——— 不能简单解读为"内容吸引",比值高也可能因导航差反复点击、长文分页;需结合跳出率、停留时长解读
四个指标的维度对比
QPS:系统处理能力(实时、按请求数)TPS:业务事务处理能力(实时、按事务数)PV:页面访问量(累计、按次数)UV:独立访客数(累计、按人去重,与QPS无换算关系)
协助记忆
QPS是"每秒能扛多少请求",TPS是"每秒能完成多少业务",PV是"被看了多少次",UV是"有多少人来看"QPS/TPS看系统强不强,PV/UV看业务火不火PV/UV比值:人均看几页,需结合跳出率解读
进阶思考
如何从
Nginx access log统计QPS和PV?combined格式时间戳如10/Feb/2025:14:23:45,$4是完整时间戳。按分钟统计:awk '{print $4}' access.log | cut -d: -f2-3 | sort | uniq -c(必须sort再uniq,多worker写日志时同分钟行不连续,不sort会重复计数);按秒看峰值:cut -d: -f2-4 | sort | uniq -c | sort -rn | head。统计PV:过滤静态资源 + 非2xx+ 爬虫 ———grep -E '\.(html|htm)'在URL带query string或动态路由站点会失效,更可靠的是按URL类型/路径前缀区分或用GoAccess、ELK等分析工具。QPS更推荐用监控系统(nginx-prometheus-exporter)而非日志
UV统计中"基于Cookie“和"基于IP“哪个更准确?为什么行业多用Cookie?- 基于
Cookie更接近真实人数,是行业主流——因为IP在NAT场景下多人共享一个IP(会被算成1个UV),而同一用户在不同网络下IP会变(会被算成多个UV),IP口径误差大。Cookie的问题是用户清浏览器缓存会重新计数(略微高估)、禁Cookie则无法统计。2025年隐私环境下的主流是"第一方Cookie+ 登录ID+ 设备指纹"混合识别(GA4基于first-party标识去重),第三方Cookie因Safari/Firefox拦截和法规限制已不可依赖
- 基于
扩展信息
·
Apache HTTP Server自带的基准测试工具,单进程模型(-c靠fork子进程,非线程),无HTTP/2- 官方自认"未完整实现
HTTP/1.x"、解析脆弱——定位是快速冒烟测试(“给你当前Apache安装表现的一个印象”),不适合严谨压测 - 适用:随手测一下吞吐量。
2025年已属过时,正式压测选下面这些
wrk/wrk2wrk:多线程 +epoll/kqueue事件驱动,单颗多核CPU即可产生高负载,LuaJIT脚本定制请求- 维护状态:长期未实质更新(
wrk停留在2014年前后);wrk2是事实上的标准改进fork——— 恒定吞吐(-R参数)+HdrHistogram精确延迟记录,修复"协调遗漏(Coordinated Omission)",可报99.9999%分位延迟 - 适用:命令行快速吞吐/延迟测试;要正确的高分位延迟用
wrk2
JMeterApache开源、纯Java的全功能负载测试工具,从Web扩展到JDBC/JMS/FTP/LDAP/TCP等十余种协议- 图形化
Test IDE+CLI无头模式 + 动态HTML报告,Groovy/JSR223脚本化,高度可扩展 - 注意:官方明确”
JMeter is not a browser” ——— 协议层工作,不执行JS - 适用:功能/负载测试一体化、多协议、团队
GUI协作(重量级)
现代压测工具(2025 年主流)
k6(最活跃):Go内核 +JS脚本"tests as code",HTTP/WebSocket/gRPC/Browser,阈值/SLO、CI集成,2025年事实上的新一代主流Locust(活跃,Microsoft赞助):纯Python写场景,gevent协程 +Web UI+ 分布式,适合高并发用户模拟Vegeta:Go,恒定速率 +UNIX组合式CLI+Go库,内置Prometheus exporterhey:Go单文件,自称 “ApacheBench (ab) replacement",HTTP/2、限速、CSV输出,小而够用oha(活跃):Rust+tokio,实时TUI,HTTP/2/3(实验)、burst、--latency-correction(同样修复协调遗漏)- 高分位延迟的正确性:
wrk2/Vegeta/oha都处理协调遗漏,ab和wrk不处理
APM(Application Performance Monitoring,应用性能监控)- 核心能力四件套:链路追踪(跨服务请求流、瓶颈、根因、依赖分析)、性能剖析(方法级
CPU/内存profiling)、错误监控(异常捕获、聚合、告警)、指标 + 仪表盘 + 告警 Jaeger:CNCF毕业项目,纯分布式追踪平台,已发v2、原生拥抱OpenTelemetry(OTLP、ClickHouse存储后端) ——— 轻量自托管选它SkyWalking/Pinpoint:Java agent无侵入字节码注入,覆盖trace+ 指标 + 日志 +profiling的全栈APM——— 全栈自托管选它们Datadog APM/New Relic:商业SaaS,自动埋点 + 分布式追踪 + 错误监控 + 持续剖析 +SLO/告警的托管全栈——省事省运维选它们
- 核心能力四件套:链路追踪(跨服务请求流、瓶颈、根因、依赖分析)、性能剖析(方法级
一句话选型:快速冒烟测试用
ab;命令行脚本化压测用wrk/wrk2、hey;CI常态化压测首选k6;Python团队用Locust;多协议/团队协作用JMeter;自托管链路追踪用Jaeger;全栈APM自托管用SkyWalking。
🤔 简述 HTTP 和 HTTPS 区别?
HTTP和HTTPS的核心区别:HTTPS是在HTTP与TCP之间插入了一层TLS(旧称SSL)加密层,保证传输安全。HTTP是明文传输,HTTPS是加密传输。HTTP(HyperText Transfer Protocol)- 明文传输:请求和响应内容(
URL、请求头、Cookie、表单数据、响应体)在网络中以明文传输,可被中间人抓包直接读取 - 默认端口
80 - 无身份验证:无法确认你连的服务器就是目标服务器(可能被
DNS劫持/中间人冒充) - 无认证性完整性保护:数据可能被中间人篡改而不被发现(
TCP校验和只能发现传输错误,防不了恶意篡改) - 适用:非敏感场景。如今内网也推荐
HTTPS(零信任趋势)
- 明文传输:请求和响应内容(
HTTPS(HTTP over TLS)加密传输:在
HTTP和TCP之间加了一层TLS(Transport Layer Security,旧称SSL) ,数据加密后传输默认端口
443提供三方面安全能力:
- 机密性:内容加密,中间人抓包只能看到密文(但
SNI域名与流量元数据仍可见,这正是ECH要解决的——见下文) - 完整性:
AEAD认证加密(TLS 1.2及以前用MAC)保证数据未被篡改 - 身份认证:通过服务器证书验证服务器身份,防止中间人冒充
- 机密性:内容加密,中间人抓包只能看到密文(但
证书机制:服务器需要部署
CA(证书颁发机构)签发的证书,包含公钥和服务器身份信息,客户端验证证书的信任链
HTTPS的建立流程(简版)TCP三次握手 →TLS握手(协商加密套件、交换密钥、验证证书)→ 加密的HTTP通信TLS 1.3首次完整握手1-RTT;PSK会话恢复通常仍为1-RTT;开启early data(0-RTT)可零往返发送幂等请求,但有重放风险、无前向保密,规范要求默认不启用(部分CDN对QUIC 0-RTT会开启)
性能差异
HTTPS比HTTP多一次TLS握手开销(首次连接多1~2个RTT:TLS 1.3为1-RTT、TLS 1.2为2-RTT)- 现代优化(
TLS 1.3、会话恢复、HTTP/3+QUIC 0-RTT)已让差异很小
应用现状(截至 2026 年)
HTTPS已是绝对主流:主流浏览器对HTTP站点显示"不安全"警告,搜索引擎降权HTTP站点;TLS 1.3自2018发布以来已广泛部署,TLS 1.2是主要回退版本- 免费证书普及:
Let's Encrypt提供免费自动化证书(certbot),HTTPS部署成本几乎为零 HSTS强制浏览器只走HTTPS——— 注意其生效前提是首次请求已走HTTPS(或加入preload list),否则第一个请求仍可能被SSL-strip降级ECH(Encrypted Client Hello,2026年3月标准化为RFC 9849) :加密整个ClientHello(含SNI域名),配合DoH防域名泄露——解决"抓包只见密文但域名仍可见"的最后一块短板。Chrome/Firefox已默认启用,OpenSSL 4.0、nginx已支持HTTP/3基于QUIC,本身就要求TLS 1.3加密 ——— 现代Web已无"明文HTTP/3“这回事
协助记忆
- 一句话:
HTTPS=HTTP+TLS加密层,明文变密文 - 三个安全能力:机密性(加密)、完整性(防篡改)、身份认证(防冒充)
- 端口记忆:
80明文,443加密 - 类比:
HTTP是明信片(谁都能看内容),HTTPS是密封信(只有收发双方能看)
- 一句话:
进阶思考
HTTPS能防止所有攻击吗?- 不能。
HTTPS保护的是传输过程,不保护端点本身 ———Web应用漏洞(SQL注入、XSS)发生在应用层,HTTPS管不了;钓鱼网站本身就用HTTPS(有合法证书),HTTPS只证明"你是连到了证书对应的服务器”,不证明"这个服务器是可信的”。HTTPS解决的只是"传输中不被偷看、篡改、冒充"
- 不能。
为什么
HTTP/3没有明文版本?HTTP/3基于QUIC,QUIC在设计上强制内置TLS 1.3(加密是协议的一部分,不是可选项)。这和HTTP/2不同 ———HTTP/2虽然规范支持h2c(明文),但浏览器从未实现明文HTTP/2。所以到了HTTP/3时代,明文HTTP事实上只剩HTTP/1.1还在用
🤔 Session 共享是什么?有哪些实现方式?
Session是服务器端保存的用户会话状态(登录状态、购物车、临时数据)。单机部署时Session存在本机内存即可;多实例部署时,用户的请求可能落到不同的服务器——如果Session只存在某台服务器上,落到其他服务器的请求就"不认识"这个用户了。Session共享就是把Session从单台服务器内存中拿出来,让集群中所有服务器都能访问同一份会话状态。为什么需要
Session共享- 单机时代:
Session存在本机内存,一个进程处理所有请求,天然可用 - 集群时代:多台服务器 + 负载均衡,请求会分散到不同机器,
Session存在A机器,请求落到B机器就丢失登录状态 - 水平扩展:缩容掉持有
Session的那台机器,用户就被登出
- 单机时代:
实现方式一:
Session复制(已过时,不推荐)- 各服务器之间互相复制
Session。Tomcat集群两种实现:DeltaManager(all-to-all,每台机器保存全量Session)和BackupManager(只复制到一台备份节点,主备模式) - 细节:成员发现/心跳走
multicast(默认228.0.0.4:45564),会话数据复制走TCP——— 大集群下all-to-all TCP复制 +multicast心跳的网络开销剧增 - 已基本被淘汰,适合小集群
- 各服务器之间互相复制
实现方式二:
Session Sticky(粘性会话,治标不治本)- 负载均衡器把同一个用户的请求始终转发到同一台服务器(基于
IP hash、Cookie或URL参数) - 优点:实现简单,
Session仍可存在单台机器内存 - 缺点:只是"绕开"了问题而不是解决——某台服务器宕机,粘在上面的用户全部掉线;扩容/缩容会破坏
hash映射(用一致性哈希可大幅减少重映射,如Nginx hash ... consistent);无法做到真正的负载均衡(热点用户集中在一台机器) - 定位:不推荐作为唯一会话方案(云原生场景如
K8s ingress/ALB中sticky仍是常规实践,但通常与集中共享叠加使用)
- 负载均衡器把同一个用户的请求始终转发到同一台服务器(基于
实现方式三:集中式
Session存储(主流方案)- 把
Session从各服务器内存中抽出来,存到一个集中存储(Redis/Memcached) - 所有服务器从同一个地方读写
Session,天然共享 Redis方案(最常用):支持TTL过期,正好匹配Session的过期机制;读写快、支持集群- 框架支持:
Java的Spring Session(支持Redis/JDBC,切换不改应用代码)、Hazelcast/Infinispan(社区扩展)、云托管Redis - 优点:服务器无状态、支持水平扩展、单台服务器宕机不影响其他机器
- 注意点:
Redis是新的单点——本身需要高可用(主从、哨兵、集群);Session是热数据,Redis内存开销需要考虑
- 把
实现方式四:客户端存储 / 无状态
Token(趋势方案)不把会话状态存在服务器,而是放到客户端:
JWT(JSON Web Token):签名后的token存在客户端(localStorage/Cookie),服务器验证签名即可,无需存储Session。注意"无法主动失效"是简化说法——严格可用黑名单/jti/版本号实现服务端撤销,但代价是重新引入状态- 签名/加密
Cookie直接存会话数据(如Rails cookie_store、Flask、Django signed_cookies):注意这是"消除共享需求"而非"共享"——数据在Cookie里服务端已无状态,不应再叫Session;受Cookie约4KB大小限制
优点:服务器完全无状态、天然支持分布式、不需要额外
Session存储缺点:
JWT主动失效难、token泄露后有攻击窗口、体积比Session ID大适用:
API/前后端分离场景主流
2026年演进补充OIDC/SSO(企业认证标准,基于JWT)成为多应用统一登录的主流方案Passkeys/WebAuthn(无密码认证)兴起,减少对传统Session/Cookie的依赖- 第三方
Cookie逐步淘汰对依赖Cookie的Session/Sticky方案有冲击;SameSite默认Lax影响跨站Cookie会话
方案对比速览
Session复制:已淘汰(同步开销大)Sticky:临时过渡(宕机掉线、不均衡)Redis集中存储:主流(服务器无状态、支持扩展)JWT无状态:趋势(完全无状态、但难失效)
协助记忆
Session共享的本质:把"存在单台机器内存里的会话"搬到"所有机器都能访问的地方"- 一条主线:
Session存储位置从"服务器内存"→“集中存储(Redis)"→“客户端(JWT)",越来越无状态 - 选型:传统
Web应用用Redis集中存储(Spring Session);前后端分离/API用JWT
进阶思考
Redis存Session和JWT怎么选?- 看业务需求。需要主动失效(登出、踢人、封禁)、需要服务端可控(管理在线用户)、传统服务端渲染应用 →
Redis存Session(服务端可随时删Session);需要跨域/多端无缝(小程序、App、Web共享登录)、追求服务端零存储、API场景 →JWT。混合方案也常见:JWT做认证 +Redis做登出黑名单
- 看业务需求。需要主动失效(登出、踢人、封禁)、需要服务端可控(管理在线用户)、传统服务端渲染应用 →
Session Sticky和Session共享是二选一吗?- 不是,
Sticky是"让请求总去同一台机器"来规避共享问题,Session共享是"让所有机器共享同一份状态”。两者可以叠加:生产常见做法是Sticky保性能 + 集中共享存储保故障转移——正常时请求粘在本地减少跨节点访问,节点宕机时共享存储兜底。但只依赖Sticky的方案(宕机全掉线)不可接受
- 不是,
🤔 Tomcat 8005、8009、8080 端口分别作用是什么?
Tomcat的三个经典端口各有分工:8005是Shutdown(关闭)端口、8009是AJP端口(与Apache httpd集成)、8080是HTTP端口(对外服务)。都在conf/server.xml中配置。8005:Shutdown(关闭)端口- 作用:用于关闭
Tomcat的端口。向该端口发送特定的SHUTDOWN命令字符串,Tomcat会优雅关闭。它只接受一个固定的SHUTDOWN字符串,没有其他管理能力(真正的管理机制是JMX,默认随机端口) - 配置:
<Server port="8005" shutdown="SHUTDOWN">,关闭命令字符串默认为SHUTDOWN - 现状:
8005在Tomcat 9/10/11的默认server.xml中始终启用,且默认只监听localhost(127.0.0.1)。“禁用"是社区加固建议,不是版本默认行为 - 安全注意:如果配置不当绑定到
0.0.0.0或防火墙未限制,任何人连接8005发SHUTDOWN就能关掉Tomcat(DoS) - 加固方式(注意别把
Tomcat停不掉):- 确保监听
127.0.0.1、修改shutdown字符串 - 用
port="-1"完全禁用该端口 ——— 但注意catalina.sh stop默认正是通过8005发SHUTDOWN,禁用后必须:设置 ·(此时catalina.sh走kill路径)、或用jsvc/Apache Commons Daemon停止,否则Tomcat无法优雅停止
- 确保监听
- 作用:用于关闭
8009:AJP端口(与Apache httpd集成)- 作用:
AJP(Apache JServ Protocol)连接器端口,用于Tomcat与前端Apache httpd之间的通信(通过mod_jk或mod_proxy_ajp)。注意Nginx不支持AJP———Nginx与Tomcat集成应走HTTP(proxy_pass) - 场景:
Apache httpd作为前端静态服务器 +Tomcat处理动态请求,两者通过AJP通信 - 配置:
<Connector port="8009" protocol="AJP/1.3" />———8.5+默认server.xml中8009已注释(需手动启用,默认示例address="::1") Ghostcat漏洞(CVE-2020-1938) :AJP端口未限制时可读取webapps下任意文件甚至执行代码。修复版(9.0.31/8.5.51+)起默认要求secret(secretRequired默认true)且默认只监听loopback- 现状:
AJP未被官方废弃(Tomcat 10/11仍支持),但主要存在于历史遗留架构(Apache httpd+Tomcat集成);与HTTP相比对HTTP/2等现代特性支持不足
- 作用:
8080:HTTP连接器端口(对外服务)- 作用:
Tomcat对外提供HTTP服务的默认端口 - 配置:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" /> redirectPort="8443":当应用配置了SSL要求(web.xml中security-constraint且transport-guarantee=CONFIDENTIAL)时,HTTP请求自动重定向到8443(HTTPS端口)- 默认
8080而非80:Unix下1024以下端口需root权限而Tomcat默认非root运行(Windows无此限制),兼为避免与既有Web服务冲突
- 作用:
Tomcat 10/11的变化(2020年后)- 最大变化:
javax.servlet→jakarta.servlet包名迁移(Tomcat 10起),老应用需改包名才能运行 - 三个端口的机制与默认配置不变:
8005/8080默认启用、8009默认注释
- 最大变化:
协助记忆
- 三个端口一句话:
8005关Tomcat(管理)、8009连Apache(AJP集成)、8080给用户(HTTP服务) 8080→8443重定向:HTTP强制跳HTTPS时用(redirectPort)- 安全三查:
8005别绑外网(禁用前先配CATALINA_PID)、8009别裸奔(Ghostcat)、8080按需开放
- 三个端口一句话:
进阶思考
8005端口关闭命令的安全性如何保障?8005的SHUTDOWN默认字符串是固定的SHUTDOWN,一旦端口可达任何人都能关掉Tomcat。保障手段:确保监听127.0.0.1(默认);防火墙限制本机访问;修改shutdown字符串;最彻底是port="-1"禁用 ——— 但禁用后必须设置CATALINA_PID或改用jsvc,否则catalina.sh stop无法优雅停止(它默认走8005发SHUTDOWN)
为什么现在很多架构不用
8009 AJP了?- 一是
Ghostcat漏洞(CVE-2020-1938)暴露了安全风险;二是现代前端架构变了 ———Nginx已成为主流反向代理,直接用proxy_pass走HTTP(8080)就能完成转发,而且Nginx根本不支持AJP;三是AJP对HTTP/2等现代特性支持不足。AJP的优势(减少解析开销)在现代硬件上可忽略。所以AJP主要存在于Apache httpd+Tomcat的历史架构中
- 一是
🤔 Tomcat 如何性能优化?
Tomcat性能优化要分层进行:JVM参数 → 连接器(Connector)→ 应用层 → 操作系统层。核心原则是"先找准瓶颈再优化”,不要盲目调大参数。第一层:
JVM参数优化- 堆内存:设置
-Xms(初始堆)和-Xmx(最大堆)为相同值,避免运行时动态扩缩堆造成性能抖动。参考值:-Xms2g-Xmx2g(按实际内存调整;容器场景下JDK 10+默认启用UseContainerSupport,-Xmx受容器配额约束) - 元空间:
-XX:MaxMetaspaceSize设置上限,防止元空间无限增长 GC选择:JDK 9+默认G1,适合大堆(4GB+);超大堆(几十GB)追求低延迟用ZGC(JDK 11实验、15生产可用)GC日志:-Xlog:gc*启用GC日志,用于排查GC停顿- 常见误区:盲目调大
-Xmx不一定提升性能,堆过大反而增加GC停顿
- 堆内存:设置
第二层:连接器(
Connector)优化连接器模式:
Tomcat 8.5/9默认NIO(装了tomcat-native时自动用APR);Tomcat 11起默认纯Java NIO。BIO在8.5已移除线程池参数(
Executor):maxThreads:最大工作线程数,默认200。不是越大越好——线程过多导致上下文切换开销增大;建议结合压测确定,一般200~500minSpareThreads:保底存活线程数(Connector属性默认10,Executor上默认25);注意是"保底水位”,线程按需增长到该水位,并非启动即全部创建acceptCount:等待队列长度,默认100。高并发时调大(如500~1000)maxConnections:最大连接数,NIO默认8192
连接超时:
connectionTimeout文档默认60000ms,但随Tomcat发行的默认server.xml显式配置为20000ms——— 如果自定义server.xml未写该属性,实际生效的是60000ms;keepAliveTimeout控制长连接存活时间压缩:
compression="on"开启gzip压缩,减少传输量(对文本类资源效果明显)静态资源:配置资源缓存或交给前端
Nginx处理(推荐)
第三层:应用层优化
- 数据库连接池:配置合理的连接池参数(初始/最大连接数),避免频繁创建销毁连接
- 缓存:热点数据用
Redis/本地缓存,减少数据库压力 - 异步处理:长耗时操作用
Servlet 3.1异步处理,释放线程 - 避免阻塞:
IO操作异步化,减少线程占用
第四层:操作系统层
- 文件描述符:调大
ulimit -n(默认1024太小,Tomcat高并发会报too many open files) - 内核参数:
net.core.somaxconn(配合acceptCount)、net.ipv4.tcp_*相关优化
- 文件描述符:调大
协助记忆
- 优化四层:
JVM(堆 +GC)→ 连接器(线程池 + 超时)→ 应用(连接池 + 缓存)→ 系统(fd + 内核) - 核心原则:先压测找瓶颈,再对症下药;盲目调大参数反而更糟
- 三个默认值:
maxThreads 200、acceptCount 100、maxConnections 8192(NIO)
- 优化四层:
进阶思考
Tomcat线程数(maxThreads)是不是越大越好?- 不是。线程是"资源"而非"能力"——每个线程占用栈内存,线程过多导致内存占用大、
CPU上下文切换开销大(频繁切换反而降低吞吐)。正确做法:用压测工具(JMeter/k6)逐步加压,观察吞吐量拐点,在吞吐不再增长的点设置maxThreads。通常200~500是常见范围,但具体要压测确定。还要配合maxConnections和acceptCount一起看——三者共同决定了并发处理能力
- 不是。线程是"资源"而非"能力"——每个线程占用栈内存,线程过多导致内存占用大、
Tomcat 10/11有哪些值得关注的性能相关变化?Tomcat 10最大变化是jakarta命名空间迁移(javax→jakarta);Tomcat 11对应Jakarta EE 11。真正值得关注的是虚拟线程(Virtual Threads) ———Tomcat 10.1和11都支持(通过StandardVirtualThreadExecutor,需JDK 21+,且默认都不启用、需在server.xml显式配置)。注意概念澄清:Tomcat NIO自6.0起就不是"1线程1连接" ———acceptor+poller线程负责连接,工作线程池处理的是请求而非连接;虚拟线程改变的是"处理请求的工作线程"的实现(平台线程→虚拟线程),让每个请求跑在KB级栈的虚拟线程上,突破maxThreads=200的平台线程上限。仅对IO密集型(阻塞占比高)应用收益明显;CPU密集型无增益;注意JDK 21上synchronized会固定(pin)虚拟线程(JDK 24的JEP 491才解决)、ThreadLocal规模放大问题
🤔 简述 LVS 的三种模式及其工作原理?
LVS(Linux Virtual Server)是Linux内核内置的负载均衡方案,工作在内核空间(第四层,基于IP+ 端口),性能高。LVS有三种工作模式:NAT、DR(Direct Routing)、TUN(IP Tunneling),核心区别在于数据报文如何流转、响应流量是否经过调度器。模式一:
NAT模式(VS/NAT)原理:调度器同时改写请求和响应的
IP地址——请求进来做DNAT(目标地址改为RS(Real Server; 真实服务器)),响应方向做逆NAT(源地址从RS改回VIP,基于连接跟踪的双向改写)报文流转:请求 → 调度器 →
RS;响应 → 调度器 → 客户端。请求和响应都经过调度器特点:
- 优点:
RS可以使用任意操作系统和私网IP,配置简单;是唯一支持端口映射的模式(VIP端口可与RS端口不同) - 缺点:调度器是瓶颈——所有响应都经过它,吞吐受限于网卡带宽 + 连接表/
CPU(每个NAT连接在哈希表占两个节点) RS的网关必须指向调度器
- 优点:
适用:
RS数量少、流量不是特别大的场景
模式二:
DR模式(VS/DR,直接路由)- 原理:调度器只改写数据帧的目标
MAC地址(源MAC也会改写)把请求转发给RS;RS处理完直接把响应绕过调度器返回客户端 - 报文流转:请求 → 调度器 →
RS;响应 →RS→ 客户端(不经过调度器) - 关键配置:
VIP同时配置在调度器和所有RS上 ———RS的VIP配置在lo接口上,并抑制ARP响应(arp_ignore=1、arp_announce=2) - 特点:
- 优点:响应不经过调度器,调度器只处理请求流量,性能高,生产环境最常用
- 缺点:
RS和调度器必须在同一二层网络(同网段);RS端口必须与VIP端口相同;RS需额外配置VIP和ARP抑制 - 适用:大规模、高流量场景(
Web集群主流)
- 原理:调度器只改写数据帧的目标
模式三:
TUN模式(VS/TUN,IP隧道)- 原理:调度器把请求封装在
IP隧道(IP-in-IP,也支持GRE/SIT/GUE)中转发给RS;RS解封装后处理,响应直接返回客户端 - 报文流转:请求 → 调度器(封装)→
RS(解封装)→ 处理 → 响应 → 客户端(不经过调度器) - 关键配置(和
DR一样) :VIP配置在RS的非ARP设备(tunl0/dummy/lo)上,同样要抑制ARP响应 ——— 否则RS会抢答ARP MTU坑(TUN实战第一坑) :IPIP每包+20字节头,MTU 1500网络中DF(不分片)大包会被拒(PMTU问题);内核可配置pmtu_disc关闭让TUN方法分片- 特点:
- 优点:
RS可以跨地域(不在同一网络),响应不经过调度器 - 缺点:需
RS支持隧道协议(modprobe ipip、tunl0 up);封装带来开销和MTU问题;配置复杂度高 - 适用:异地多活、
RS分布在多个机房的场景
- 原理:调度器把请求封装在
三种模式对比速览
NAT:请求和响应都过调度器(调度器瓶颈),唯一支持端口映射DR:请求过调度器,响应直接回(同二层网络,最常用),RS端口必须等于VIP端口TUN:请求隧道封装,响应直接回(可跨地域),RS端口必须等于VIP端口
2026 年生态现状
ipvs至今在内核6.x中活跃维护;kube-proxy的IPVS模式是K8s大规模集群的常用方案- 演进方向:
DPVS(DPDK版ipvs)、Katran/Cilium(XDP/eBPF负载均衡)、ECMP+BGP替代Keepalived/VRRP
协助记忆
- 一句话区分:
NAT是"快递员两头跑"(进出发都经调度器),DR是"只送件不取件"(请求经调度器、响应直达),TUN是"打包寄送"(隧道封装跨地域) DR两个关键:VIP放lo接口 + 抑制ARP(TUN同样要)- 选型口诀:同网段高流量用
DR,跨地域用TUN,简单场景用NAT
- 一句话区分:
进阶思考
DR模式为什么要抑制RS的ARP响应?不抑制会怎样?VIP同时配置在调度器和所有RS上。如果RS不抑制ARP,局域网设备对VIP发起ARP请求时所有RS都会响应(ARP是广播的),导致MAC地址混乱——有的请求被转发到RS,有的被RS直接抢答。抑制ARP(arp_ignore=1、arp_announce=2)让只有调度器响应VIP的ARP,RS的VIP只用于接收转发请求,不对外宣告
LVS和Nginx负载均衡怎么选?LVS工作在四层(IP+ 端口),内核态,性能高(并发连接数可达百万级,注意是并发连接数而非QPS);但只做转发,本身不带健康检查和故障摘除,需Keepalived等外部组件配合。Nginx工作在七层(HTTP),能做URL路由、重写、限流、缓存,但含TLS终止/HTTP解析,开销天然更大(可比对象是Nginx stream四层模块)。生产架构常见组合:LVS(四层入口)→Nginx(七层反向代理)→ 应用服务器,高可用由Keepalived实现 ———VRRP做VIP漂移 + 对RS健康检查并动态增删ipvs规则
🤔 LVS 支持哪些调度算法?
LVS(ipvs)支持的调度算法分为静态和动态两大类:静态算法不考虑后端实时负载,动态算法根据后端当前负载/连接数做决策。截至2026年,ipvs共有14个调度算法(RR/WRR/DH/SH/MH+LC/WLC/SED/NQ/LBLC/LBLCR/FO/OVF/TWOS)。静态调度算法(不考虑后端实时状态)
RR(Round Robin,轮询) :请求依次分发到每个RS,轮流来WRR(Weighted Round Robin,加权轮询) :按权重比例分发,权重高的RS分到更多请求DH(Destination Hashing,目标地址哈希) :按请求的目标IP哈希,相同目标IP的请求始终分发到同一台RS(用于缓存场景)SH(Source Hashing,源地址哈希) :按客户端源IP哈希,相同来源的请求到同一台RS(天然实现会话保持)。注意"始终"是简化说法——RS增删/权重变化会重建哈希表,且SH有sh-port(IP+端口哈希)和sh-fallback(故障回退)两个flagMH(Maglev Hashing,Maglev哈希,Linux 4.18合入) :基于源IP的一致性哈希(Google Maglev论文,NSDI'16),表容量按权重分配,支持mh-port/mh-fallback flag。天然适合会话保持,RS变化时受影响连接少
动态调度算法(根据后端实时负载决策)
LC(Least Connections,最少连接) :把请求分给当前连接数最少的RS。严格语义是"活动连接数加权"(内核公式 (activeconns<<8) +inactconns)WLC(Weighted Least Connections,加权最少连接) :LC+ 权重,按"连接数/权重"最小的原则分配。是ipvsadm的缺省算法(内核层面无默认)SED(Shortest Expected Delay,最短期望延迟) :考虑"活动连接数 + 1"与权重的比值((active+1)/weight),预测哪个RS延迟最短NQ(Never Queue,永不排队) :SED的改进——如果某台RS活动连接数为0,直接分配给第一个空闲者,避免请求排队;否则用SEDLBLC(Locality-Based Least Connections,基于局部性的最少连接) :目标IP哈希 + 最少连接结合,适合Cache集群(相同目标IP优先到同一台RS)LBLCR(带复制的局部性最少连接) :LBLC的改进——每个目标IP对应一个服务器集合(set),集合内按最少连接选、目标IP热度高时集合扩容(注意不是"复制流量",是集合内多台RS可选)FO(Weighted Failover,加权故障转移) :把连接发给当前可用且权重最高的RS——— 类似主备故障转移(注意:不是"失效次数/权重",那是网上的以讹传讹,内核源码就是"选权重最高的dest全给流量")OVF(Overflow-Connection,溢出连接) :优先把连接给权重最高的RS,当活动连接数超过其weight时"溢出"到权重次高者;全部过载则调度失败(无WLC回退);只统计活动连接,不适合UDPTWOS(Power of Two Choices,两随机选择,Linux 6.4合入) :按权重随机抽两台候选RS,比较活动连接数归一化开销,选更空闲的一台
选型建议
- 通用
Web集群、无特殊需求:WLC(ipvsadm默认) - 后端性能差异大:
WRR(静态权重明确) - 需要会话保持:
SH/MH(源地址哈希,RS变化影响小)或配合持久性(persistence) Cache/缓存集群:DH或LBLC(目标地址哈希保证缓存命中)- 防止单台过载:
OVF - 高可用故障转移倾向:
FO
- 通用
协助记忆
- 静态五兄弟:
RR、WRR、DH、SH、MH(“轮询、加权轮询、目标哈希、源哈希、Maglev哈希”) - 动态九兄弟:
LC、WLC、SED、NQ、LBLC、LBLCR、FO、OVF、TWOS - 默认是
WLC(ipvsadm缺省) ,大多数场景够用;有特殊需求(会话保持、缓存命中)再换
- 静态五兄弟:
进阶思考
SH(源地址哈希)和持久性(persistence)都能做会话保持,有什么区别?SH是基于IP哈希的确定性分配 ——— 同一源IP按哈希到同一台RS,不依赖连接状态;但NAT后面的多个用户共享一个IP会被分到同一台(负载不均);且RS宕机时哈希到它的用户受影响(可通过sh-fallback缓解)。持久性(persistence)是基于连接跟踪的超时绑定——在一定时间窗口(默认300秒)内把同一来源的请求绑定到同一台RS,超时重新分配。持久性更灵活(支持故障转移、负载更均衡),是更推荐的会话保持方式
加权算法(
WRR/WLC)的权重怎么确定?- 权重反映
RS的处理能力差异——通常按CPU核数、内存大小或压测结果定。例如两台服务器,一台8核一台4核,权重可设为2:1。注意:权重是相对值不是绝对值;权重相同(如1:1)就退化为普通轮询/最少连接。权重要根据实际压测调整,不能拍脑袋
- 权重反映
🤔 简述 HTTP Cookie 和 Session 区别和联系?
Cookie和Session都是Web应用中记录用户状态的机制,核心区别:Cookie存在客户端(浏览器),Session存在服务端。两者常配合使用 ———Session通过Cookie传递Session ID来识别用户。Cookie(客户端状态)定义:由服务器通过
Set-Cookie响应头下发的小段文本数据,浏览器保存在本地,后续请求自动通过Cookie请求头携带特点:
- 存储位置:浏览器(客户端)
- 容量:约
4KB上限(单个Cookie约4096字节,浏览器实现惯例;注意RFC 6265规范并未强制规定这个数值) - 生命周期:可通过
Expires/Max-Age设置过期时间——持久Cookie或会话Cookie(无Expires/Max-Age的"会话Cookie“与服务器Session是两个概念,仅名字相似) - 作用域:同域(
Domain+Path限定),默认同站发送 - 可被篡改:客户端可修改
Cookie内容(需签名/加密防篡改)
常见属性:
Secure(仅HTTPS)、HttpOnly(禁止JS读取)、SameSite(缓解CSRF)、Path/Domain(作用域)
Session(服务端状态)- 定义:服务器为每个会话创建的状态数据,存储在服务端(内存、文件、数据库、Redis)
- 特点:
- 存储位置:服务端
- 容量:无
Cookie那样的硬限制(受服务端内存/存储限制) - 生命周期:由服务端超时控制(如
30分钟无活动过期),也可主动销毁 - 标识:通过
Session ID(一串随机字符串)识别,Session ID对应服务端存储的会话数据 - 安全性:数据在服务端,客户端无法直接篡改
联系:
Session靠Cookie传递Session ID典型流程:用户登录 → 服务器创建
Session并生成Session ID→ 通过Set-Cookie下发Session ID→ 浏览器存储 → 后续请求自动携带 → 服务器根据Session ID找到对应Session数据Session ID的传递方式:Cookie方式(主流) :Session ID存在Cookie中,自动携带URL重写方式(备用) :Session ID拼在URL参数中(如?jsessionid=xxx)——适用于禁用了Cookie的场景,但有泄露风险(URL可能被记录在日志/历史中)
也就是说:
Session本身不一定依赖Cookie,但实践中几乎都靠Cookie传递Session ID
核心区别对比
- 存储位置:客户端(
Cookie)vs 服务端(Session) - 容量:约
4KB(Cookie)vs 无硬限制(Session) - 安全性:可篡改(
Cookie)vs 服务端可控(Session) - 生命周期控制:客户端可控制 vs 服务端控制
- 性能:每次请求都传输(
Cookie)vs 服务端查询(Session) - 跨域:
Cookie同域限制 vsSession数据本身无域概念(但Session ID靠Cookie传递时仍受Cookie域限制)
- 存储位置:客户端(
2026年演进SameSite默认Lax:自Chrome 80(2020)/Firefox 86/Safari 13.1起默认。精确语义:跨站子资源、fetch/XHR、iframe内请求默认不发送,但顶级导航GET请求仍携带(用户从别的站点击链接进入本站时会带Cookie);且默认值在Chromium系稳定为Lax,其他浏览器略有差异- 第三方
Cookie逐步淘汰:Safari(ITP,2020起)默认全面阻止;Firefox默认启用ETP+Total Cookie Protection(第三方Cookie按站点分区存储,标准模式仅拦截已知追踪器,并非拦截所有);Chrome至今不默认拦截,仅无痕模式或用户设置时拦截,2024年放弃强制淘汰后逐步推出用户选择模式 - 无状态趋势:
JWT等无状态token兴起 ——— 不依赖服务端Session存储,天然适合分布式;但主动失效难。2026年业界出现回调反思——纯JWT存在吊销难、密钥轮换等痛点,部分场景回归"分布式服务端Session(Redis)“或混合方案
协助记忆
- 一句话:
Cookie是"存在浏览器里的便签”,Session是"存在服务器上的档案柜”,Session ID是"打开档案柜的钥匙"(钥匙放便签里) - 钥匙在客户端,档案在服务端:客户端只有
Session ID(钥匙),真正的数据(档案)在服务端 - 安全对比:档案(
Session数据)比便签(Cookie)安全,因为档案在服务器上,便签在用户手里随时可能被改
- 一句话:
进阶思考
Session数据都放服务端,为什么还需要Cookie?不能不用Cookie吗?HTTP是无状态协议,服务器不记得"你是谁"。Session数据虽然存在服务端,但服务器要知道"这个请求对应哪份Session数据" ——— 这个对应关系(·)必须由客户端每次带来。· 是最自然的携带方式(自动发送)。如果不用Cookie,只能靠URL重写传Session ID,但URL易泄露(日志、历史记录、分享链接),所以实践中几乎都用Cookie
Cookie和Session哪个更安全?- 单从"存储"角度
Session更安全 ——— 数据在服务端,客户端碰不到。Cookie的主要风险其实是被窃取(XSS窃取、明文传输窃听、CSRF)而非篡改 ——— 内容篡改在服务端签名校验后危害有限。Session的风险在Session ID失窃(XSS窃取Cookie、中间人窃听)导致会话劫持;注意会话固定(Session Fixation)与窃取机制不同——它是攻击者预先固定一个ID给受害者使用(RFC 6265 §8.4专述)。完整方案需组合:Session ID用HttpOnly+Secure Cookie保护、传输用HTTPS、防XSS(CSP)、SameSite缓解CSRF
- 单从"存储"角度
🤔 网站跨域报错 Access-Control-Allow-Origin 如何解决?
跨域报错是浏览器同源策略(
Same-Origin Policy)的保护机制:浏览器默认阻止网页脚本跨域读取响应。CORS(Cross-Origin Resource Sharing)是让服务器声明"允许哪些来源访问"的机制,报错Access-Control-Allow-Origin说明服务器没有正确声明允许的来源。先理解跨域和同源
- 同源:协议 + 域名 + 端口都相同才算同源。
http://a.com和https://a.com不同源(协议不同)、和http://a.com:8080不同源(端口不同) - 跨域请求:前端页面(
http://a.com)向后端(http://api.b.com)发请求,就是跨域 - 同源策略:浏览器只允许脚本读取同源响应,跨域响应默认被浏览器拦截(注意:请求可能已发出、服务器可能已处理,只是浏览器拦截了响应)
- 同源:协议 + 域名 + 端口都相同才算同源。
两种请求类型(决定处理方式)
- 简单请求:满足特定条件(
GET/HEAD/POST+Content-Type限定值 + 仅safelisted请求头) ——— 浏览器直接发送,服务器只需返回Access-Control-Allow-Origin。注意Content-Type只有三个值算简单请求:application/x-www-form-urlencoded、multipart/form-data、text/plain(application/json会触发预检) - 预检请求(Preflight) :非简单请求(如带
Authorization头、Content-Type: application/json、PUT/DELETE方法)——— 浏览器先发OPTIONS请求探测,服务器必须正确响应OPTIONS(且必须返回2xx)并返回允许的方法/头,才继续发真实请求 2024-2026新机制PNA(Private Network Access) :Chrome从2024年底(localhost场景Chrome 130、内网RFC1918场景Chrome 137)起,对"公网页面 → 内网/本地"的请求无条件强制预检——无论方法、无论mode(甚至no-cors和同源请求),新增Access-Control-Request-Private-Network: true/Access-Control-Allow-Private-Network: true头。2025-2026年"莫名OPTIONS_预检/请求被拦"很大比例是PNA导致
- 简单请求:满足特定条件(
解决方案:后端加
CORS响应头(根本解法)在服务器响应中加:
Access-Control-Allow-Origin: http://a.com(允许的来源)Access-Control-Allow-Methods: GET, POST, PUT, DELETE(允许的方法)Access-Control-Allow-Headers: Content-Type, Authorization(允许的请求头)Access-Control-Allow-Credentials: true(允许携带凭证,值为字面量true;配了它就不能用*)Access-Control-Max-Age: 3600(预检结果缓存时间,减少OPTIONS请求)Access-Control-Expose-Headers(可选):默认前端只能读6个简单响应头,自定义响应头需在此列出
配置位置:
Nginx:add_header Access-Control-Allow-Origin ...Spring Boot:@CrossOrigin注解或全局CORS配置Node.js:cors中间件
注意通配符的坑:
Access-Control-Allow-Origin: *允许所有来源,但不能和credentials(携带Cookie)一起用——用了withCredentials时不允许用*,必须写具体来源
解决方案:
Nginx反向代理(同源化)- 前端和后端域名不同导致跨域时,用
Nginx反代把/api转发到后端,前端只访问同源路径 ——— 从根上消除跨域 - 配置示例:前端访问
http://a.com/api,Nginx把/api代理到http://api.b.com - 优点:不需要改后端代码、避免暴露多个
CORS配置 - 适用范围:前后端域名固定对应时是常见推荐方案;但
API需被多个未知域名、第三方或移动端调用时(SaaS、开放平台),CORS头方案才是唯一可行且是行业标配
- 前端和后端域名不同导致跨域时,用
解决方案:
JSONP(历史遗留方案)- 利用
script标签不受同源策略限制的特点,通过回调函数拿数据 - 缺点:只支持
GET、安全性差(需要服务端配合)、已过时 - 仅用于兼容老系统的特殊场景
- 利用
协助记忆
- 一句话:跨域报错 = 服务器没声明允许这个来源访问
- 两条路:后端加
CORS头(改后端)或Nginx反代同源化(改架构) - 通配符的坑:
*和credentials不能同时用 - 记住预检:带
Authorization/JSON body的请求会先发OPTIONS预检,服务器要正确处理(OPTIONS必须返回2xx)
进阶思考
为什么有时候
OPTIONS请求返回403/404?- 预检请求(
OPTIONS)可能没被正确处理。常见原因:一是网关/防火墙拦截了OPTIONS方法(只放行GET/POST);二是后端没有对OPTIONS返回正确的CORS响应头(Access-Control-Allow-Methods/Headers);三是Nginx的add_header默认只对部分2xx/3xx状态码添加(200/201/204/206/301/302/303/304/307/308),需加always参数才对所有响应码添加——且预检必须返回2xx,OPTIONS返回403时即使加了头也必然失败。排查:用curl手动发OPTIONS请求模拟预检,看响应头是否包含Allow-Methods/Headers且状态码为2xx
- 预检请求(
Access-Control-Allow-Origin: *和withCredentials为什么冲突?- 安全设计。如果服务器返回
*允许所有来源,同时又允许携带凭证,任意站点就能代表用户跨域发带凭证请求并读取响应——比只写不读的CSRF更严重。所以浏览器规定:凭证请求下ACAO必须为具体来源(*判失败),且要配合Access-Control-Allow-Credentials: true。补充:现代浏览器Cookie默认SameSite=Lax,跨站fetch/XHR(非顶级导航)默认不携带Lax Cookie,只有SameSite=None;Secure的Cookie会带上——实际攻击面比"任何恶意网站都能带上Cookie“更小,但"带凭证 + 可读响应"的底线依然不能破
- 安全设计。如果服务器返回
扩展信息
安全响应头是Nginx层可以统一配置的Web安全防护。核心是CSP(Content-Security-Policy,内容安全策略)——— 它告诉浏览器"页面允许加载哪些来源的资源”,CSP是浏览器侧缓解XSS最重要、最有效的防御机制之一,但不能替代输入校验(Sanitization)与输出编码(Encoding)等后端/开发层面的安全措施。CSP是什么CSP通过响应头声明"允许加载什么",浏览器强制执行:不符合策略的资源(脚本、图片、样式、iframe)被阻止加载- 缓解
XSS原理:即使攻击者注入<script>,CSP如果没允许内联脚本/不可信来源,浏览器会拒绝执行 CSP的威胁模型:缓解内容注入(XSS)与页面被恶意嵌入(clickjacking——注意这是UI redress而非"内容注入")
CSP常用指令default-src:大多数fetch类指令的兜底策略。但需注意:frame-ancestors、base-uri、form-action、sandbox等指令不会继承default-src(未指定即默认为无限制)。script-src:脚本来源(最关键的指令)style-src:样式来源img-src:图片来源connect-src:XHR/fetch/WebSocket连接来源(注意它和CORS是两层不同机制——CSP是浏览器端限制,CORS是服务端授权)frame-ancestors:允许哪些来源嵌入本页(防点击劫持,取代X-Frame-Options)object-src:<object>/<embed>来源(建议 ’none’)- base-uri:
<base>标签来源(限制可被篡改的基准URL);form-action:表单提交目标 upgrade-insecure-requests:页面内HTTP请求自动升级为HTTPSreport-to:违规上报(report-uri已废弃,用Reporting-Endpoints+report-to替代)- 源关键字:
'self'、'none'、'unsafe-inline'(不推荐)、'unsafe-eval'(不推荐)、'strict-dynamic'(注意:strict-dynamic应与nonce或hash一起使用。在支持该特性的浏览器中,会忽略传统脚本白名单(如'self'、主机白名单),改由受信任脚本继续加载其他脚本。)、'nonce-xxx'(每次响应随机生成)
CSP配置案例- 基础案例(静态站) :
Content-Security-Policy: default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none' - 进阶案例(带
CDN和API) :Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; img-src 'self' data: https://cdn.example.com; connect-src 'self' https://api.example.com; style-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'self'; frame-ancestors 'none' - 上线前先开报告模式(只报告不拦截):
Content-Security-Policy-Report-Only: default-src 'self'; object-src 'none'; base-uri 'self' Reporting-Endpoints: csp-endpoint="https://example.com/csp-violations" Content-Security-Policy-Report-Only: ...; report-to csp-endpoint; 注意两个坑:Report-Only模式下frame-ancestors不生效(既不拦截也不产生报告);若未配置report-to/report-uri(或Reporting-Endpoints),Report-Only依然在浏览器端生效:它会在开发者工具控制台输出违规警告,并可被前端securitypolicyviolation事件捕获,只是不会向服务器发送HTTP违规上报包。 Nginx配置CSP:add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" always;always必须加:add_header默认只对200/201/204/206/301/302/303/304/307/308生效,加always才对所有响应码生效(错误页也带CSP)add_header继承坑:子location写了add_header会覆盖父级全部(nginx 1.29.3+可用add_header_inherit merge; 改为追加)
Nginx其他安全头(一起配,注意与 CSP 语义一致)1 2 3 4 5add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "DENY" always; # 与 frame-ancestors 'none' 配套 add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; # 仅 HTTPS 站点- 策略一致性:
CSP的frame-ancestors 'none'应配X-Frame-Options "DENY"(或'self' + SAMEORIGIN),两者语义要一致——不要出现"CSP禁止嵌入、XFO允许同源嵌入"的自相矛盾 HSTS的坑:includeSubDomains要求子域全部HTTPS,否则误伤子域;preload需去hstspreload.org提交后才生效X-Frame-Options有效值只有两档(DENY/SAMEORIGIN)———ALLOW-FROM已废弃且会导致整个头被浏览器忽略
- 基础案例(静态站) :
- 进阶思考
- 为什么
'unsafe-inline'不推荐?nonce和它什么关系?'unsafe-inline'允许所有内联脚本执行——XSS注入的正是内联脚本,等于开大后门。完全禁止内联脚本后,页面自带的<script>也会被拦截,需要移到外部.js或用nonce/hash白名单。在必须用内联脚本时,nonce远比'unsafe-inline'安全、且比完全禁止更灵活 ———nonce每次响应随机生成,攻击者无法预知。实操坑:nonce每次响应都变,页面被浏览器/CDN缓存后nonce过期、脚本被误杀——这是高频事故,缓存页面慎用nonce
CSP和X-Frame-Options都能防点击劫持,用哪个?frame-ancestors(CSP)是现代推荐、X-Frame-Options是旧方案。frame-ancestors更灵活(可精确指定多个来源),X-Frame-Options只有DENY/SAMEORIGIN两档。两者同时存在时,CSP优先(仅在enforce模式成立;X-Frame-Options在响应含frame-ancestors时被忽略)。为兼容老浏览器可两者都配,但语义必须一致
- 为什么
🤔 简述 CDN 工作原理?
CDN(Content Delivery Network,内容分发网络)的核心思想:把内容缓存到离用户更近的节点,让用户从"附近的服务器"而不是"远方的源站"获取内容。本质是"空间换时间"——用分布在全国/全球的缓存节点,换取更短的访问延迟。CDN的核心组件- 边缘节点(
Edge Node/POP,Points of Presence) :分布在各地区/运营商的缓存服务器,真正响应最终用户请求 - 中心节点 / 源站(
Origin) :内容的原始来源(你的服务器) GSLB(Global Server Load Balancing,全局负载均衡) :负责把用户引导到"最合适"的边缘节点——通常以智能DNS方式实现,根据用户地理位置、所属运营商、节点负载,返回最优边缘节点IP
- 边缘节点(
CDN工作原理流程(一次访问)- 用户访问
http://www.example.com,发起DNS解析 - 智能
DNS/GSLB调度:DNS服务器根据用户所属网络的递归解析器出口IP判断地理位置和运营商,返回就近的边缘节点IP(北京用户解析到北京节点,电信用户到电信节点。严格说DNS默认看到的是递归解析器出口IP,启用EDNS Client Subnet(RFC 7871)才能获知用户真实IP前缀) - 用户请求打到边缘节点
- 边缘节点判断缓存:
- 命中:边缘节点有缓存且未过期,直接返回
- 未命中:边缘节点回源——向源站请求内容,缓存一份后返回(实际大型
CDN是多级缓存L1边缘/L2区域/L3中心,边缘未命中常先查上层节点而非直接回源)
- 后续同地区用户请求同一内容,直接命中边缘节点缓存
- 用户访问
CDN缓存与回源的关键点- ``TTL
(缓存有效期):由源站响应头控制 ———Cache-Control: s-maxage(共享缓存专用,优先于max-age)、max-age、HTTP/1.0的Expires;也可在CDN平台配置"强制缓存"忽略源站头 revalidation(304 校验) :缓存未命中时,边缘节点先发If-Modified-Since/ETag条件请求回源,源站返回304则复用缓存、不传body——这是缓存回源的重要环节- 缓存刷新:源站内容更新后,需主动刷新(
purge)CDN缓存或等TTL过期 - 回源率 / 缓存命中率(Cache Hit Ratio) :衡量
CDN效率的关键指标——命中率越高,源站压力越小
- ``TTL
CDN的作用- 加速访问:用户从就近节点拿内容,延迟大幅降低(尤其跨地域、跨境场景)
- 减轻源站压力:大部分请求被边缘节点缓存吸收,源站只处理未命中请求
- 抗
DDoS:用海量带宽池(Anycast)吸收流量型攻击、用WAF与限速过滤应用层(CC)攻击,同时隐藏源站IP使其无法被直接打击 - 隐藏源站
IP:用户只和边缘节点通信,源站IP降低暴露面(需配合回源IP白名单)
现状与前沿(2026 年)
- 已是标配(成熟多年) :
HTTP/3(QUIC 2021标准化、HTTP/3 2022标准化,主流CDN 2019年已商用,如今是标配);CDN证书托管/自动续期(用户侧边缘证书 + 源站回源证书是两个独立维度,可选mTLS);基础边缘计算(Cloudflare Workers 2017、Lambda@Edge 2017) - 当前热点(2023-2026) :边缘
AI推理(Workers AI、GPU边缘节点、AI Gateway)、边缘无服务器渲染/SSR(Vercel/Netlify edge runtime)、边缘KV/数据库(Durable Objects等)、SASE/零信任与CDN融合(Cloudflare Zero Trust等)
- 已是标配(成熟多年) :
协助记忆
- 一句话:
CDN= 把内容放到离用户近的地方,就近取货 - 三个角色:边缘节点(附近的分店)、源站(总仓库)、GSLB(导购员,告诉你哪家分店近)
- 类比快递:CDN 是"提前把货放到你楼下的便利店",用户不用每次去总部仓库取
- 一句话:
进阶思考
CDN一定能提升访问速度吗?什么场景反而更慢?- 不一定。
CDN加速依赖"就近取货"——纯动态内容(用户专属页面、实时数据)不适合缓存(核心原因是缓存收益低/需个性化,不完全是"缓存了必然旧数据",一致性可由 TTL 控制);小体量站点用CDN可能增加DNS解析和节点跳转额外开销;跨运营商调度不当反而更慢。适合CDN的是:静态资源(图片、CSS、JS、视频)、大文件下载、API只读数据。补充:现代CDN有动态加速(DSA) ——— 不缓存,但通过优化TCP/TLS、智能路由选路来加速动态请求
- 不一定。
CDN隐藏源站IP就一定安全吗?- 不是绝对的。
CDN隐藏源站IP是"降低暴露面",不是"绝对隐藏"——攻击者仍可能通过:源站直接对外解析的历史DNS记录、邮件头/证书透明度日志、源站自身的外发请求、子域爆破等找到真实IP。真正加固需要:源站只允许 CDN 回源IP访问(回源IP白名单)、关闭源站不必要的对外端口。CDN隐藏IP只是多层防护中的一层
- 不是绝对的。
🤔 四层与七层负载均衡有什么区别?
四层(
L4)和七层(L7)负载均衡工作在网络模型的不同层级,核心区别:L4在传输层(TCP/UDP)基于IP+端口转发,L7在应用层(HTTP)基于内容路由。四层负载均衡(
L4)工作层级:传输层(
OSI第4层),基于IP+ 端口转发,不解析应用层内容(需解析TCP/UDP头)代表产品:
LVS、HAProxy(tcp模式)、云厂商NLB、Katran(Meta开源,XDP/eBPF转发面) ;F5 BIG-IP LTM是L4–L7一体化 · 设备(不能仅归L4);Nginx的stream模块也能做L4转发方式:
NAT、DR(DSR,直接服务器返回——返回流量不经过LB)、隧道(LVS三模式)特点:
- 性能高:内核态或硬件转发,权威性能指标是
pps(每秒包数)而非并发连接数 - 透明:看不到
HTTP内容 - 无法做:
URL路由、内容改写、Cookie会话保持(传统上基于源IP/五元组哈希——注意标准术语是五元组:源/目的IP+端口+协议,Katran即用5-tuple一致性哈希)
- 性能高:内核态或硬件转发,权威性能指标是
TLS:传统软件
L4(LVS、nginx stream透传、HAProxy tcp passthrough)只能透传,不能终止;但现代云L4(AWS/阿里云NLB等,2019年起)支持TLS监听器/终止(多在网卡/硬件卸载,ACM管理证书);另有灰色地带——L4可只读TLS ClientHello的SNI做路由而不解密(nginx stream ssl_preread)健康检查:传统
LVS是TCP端口探测;现代L4(NLB等)同样支持HTTP/HTTPS健康检查与响应码匹配适用:高吞吐场景、
TCP/UDP通用协议、作为七层的入口
七层负载均衡(
L7)工作层级:应用层(
OSI第7层),解析HTTP内容代表产品:
Nginx、HAProxy(http模式)、Envoy/Traefik(云原生/服务网格默认L7)、云厂商ALB特点:
- 功能丰富:
URL路径路由、Header改写、Cookie会话保持、限流、缓存、TLS终止、gRPC/HTTP3支持 - 性能低于
L4(需解析报文) - 能感知应用状态(基于应用层健康检查)
- 功能丰富:
会话保持:基于
Cookie健康检查:
HTTP探测(检查具体URL的响应码,能感知应用是否真的可用)适用:
HTTP/HTTPS业务、需要路由/改写的场景
关键区别对比
- 工作层级:传输层 vs 应用层
- 性能:
L4高(pps指标)vsL7低(需解析) - 功能:
L4简单转发 vsL7内容路由/改写 TLS终止:传统L4只能透传,现代云L4支持 vsL7原生支持- 会话保持:
L4传统基于五元组 vsL7基于Cookie - 健康检查:
L4传统 TCP 探测 vsL7 HTTP探测(现代L4也支持HTTP) - 典型代表:
LVS/KatranvsNginx/Envoy
常见架构组合
- 大型架构:
L4(入口,扛高并发)→L7(应用路由)→ 后端服务器 - 经典组合:
LVS/Katran(四层)→Nginx/Envoy(七层)→ 应用
- 大型架构:
协助记忆
- 一句话:
L4是"快递分拣员"(只看地址不分内容),L7是"前台接待员"(听你需求再安排) L4管"到不到"(IP+端口转发),L7管"给谁"(URL/Host路由)——仅为简化助记,严格说L4也做健康检查、L7路由同样基于IP+端口- 选型:只要转发选
L4,要路由/改写/TLS 终止选L7,大流量先用L4挡一层
- 一句话:
进阶思考
L4能做TLS终止吗?- 分情况。传统软件
L4(LVS、nginx stream透传、HAProxy tcp passthrough)不能 ———TLS终止需要解密报文,L4只转发加密TCP流,只能透传到后端解密。现代云L4(AWS/阿里云NLB等,2019年起)支持TLS监听器,多在网卡/硬件上卸载加解密。所以"HTTPS业务 +TLS终止"传统选L7(Nginx/ALB),但现代架构也可以用支持TLS的云L4做入口
- 分情况。传统软件
四层负载均衡的性能优势有多大?
L4权威指标是pps(每秒包数)———Katran等XDP/eBPF方案在DPDK/网卡卸载下可达千万级pps;L7(Nginx)通常几万到几十万QPS(受HTTP解析和TLS开销限制),数字随硬件/配置浮动。差距来自:L4不解析应用层内容、L7要解析HTTP报文且常做TLS终止。注意量纲:L4的"百万级"通常是并发连接数/pps,L7的"几万"是QPS——— 两者不是同一个量纲,对比要说清楚
🤔 网站报错 502 Bad Gateway 怎么解决?
502 Bad Gateway的含义:网关/反向代理(Nginx)成功收到了请求,但从上游(upstream)没有得到有效响应——即"请求到了Nginx,但Nginx没能从后端拿到结果"。定位核心:先区分是Nginx自己产生的502,还是后端返回的502,再看error log定位。第一步:区分
502是谁产生的- 后端自己返回
502:Nginx只是透传 ———error log无记录,access log里$upstream_status=502(此时去后端日志查) Nginx产生502:error log有upstream相关记录(此时按下面的error log信息定位)- 判断方法:
tail -f /var/log/nginx/error.log看有没有报错,再看access log的upstream_status
- 后端自己返回
502的本质与504的区别502 Bad Gateway:上游返回了无效响应 / 连接没建立起来(后端挂了、端口不通、协议错误)- 504 Gateway Timeout:网关等待上游响应超时(后端还在处理但太慢)
- 一句话:
502是"拿不到结果",504是"等太久"
常见原因与排查路径(按
error log信息定位)后端服务未启动 / 挂了 /
upstream全不可用:connect() failed (111: Connection refused)或no live upstreams while connecting to upstream- 后者触发机制:
upstream所有server被判不可用(max_fails/fail_timeout,默认1次/10s)、全部down/backup、或域名解析失败(host not found in upstream) - 检查:
systemctl status <服务>、ss -lntp | grep <端口>
- 后者触发机制:
后端端口不通(防火墙/安全组/监听地址) :
refused或timeout- 检查:后端进程监听
127.0.0.1还是0.0.0.0?防火墙/云安全组是否放行?跨机房网络是否通? - 注意:防火墙
REJECT策略也表现为Connection refused,不一定是没监听
- 检查:后端进程监听
连接超时:
connect() failed (110: Connection timed out)——— 网络层不通(防火墙DROP丢包、跨机房、安全组)后端处理超时:
upstream timed out (110: Connection timed out) while reading response header from upstream- 后端接口处理太慢,超过
proxy_read_timeout(默认60s) - 解决:调大
proxy_read_timeout,或优化后端接口性能 - 注意:
errno 110出现阶段不同含义不同——建连阶段是网络不通,读响应阶段是后端太慢
- 后端接口处理太慢,超过
响应头无效 / 过大:
upstream sent invalid header while reading response header from upstream(或sent too big header)- 后端返回畸形/空响应头,或响应头超过
proxy_buffer_size(默认4k/8k,超限判为无效响应)——生产环境502头号来源之一 - 解决:修后端响应头问题,或调大
proxy_buffer_size/proxy_buffers
- 后端返回畸形/空响应头,或响应头超过
HTTPS/TLS upstream场景:SSL_do_handshake() failed、upstream SSL certificate verify errorproxy_ssl_verify on时证书过期/SNI不匹配/自签证书- 解决:更新证书或按需关闭校验
PHP-FPM常见场景(经典) :Unix socket路径不存在:connect() failed (2: No such file or directory) while connecting to upstream———sock路径写错、fpm没启动socket权限不足:connect() failed (13: Permission denied)——listen.owner/listen.group/listen.mode(默认0660,连接需要读/写权限)worker进程耗尽:fpm日志报server reached pm.max_children———pm.max_children太小,调大或调优pm模式PHP代码问题:fpm正常返回500→Nginx透传500;fpm进程崩溃/连接中断 →Nginx报prematurely closed产生502——— 看fpm的error log区分
连接被对端提前关闭:
upstream prematurely closed connection while reading response header from upstream- 最高频场景:后端进程崩溃/重启(
PHP-FPM reload、Java OOM kill、容器Pod重启) - 一般
Nginx会按proxy_next_upstream(默认error timeout)自动重连/换后端,无需人工reload;若配置了keepalive且后端频繁重启,可调小keepalive数
- 最高频场景:后端进程崩溃/重启(
后端连接数满 / 队列溢出:
refused或timeout(取决于tcp_abort_on_overflow)+ 后端日志报accept queue满——后端负载过高资源耗尽:后端内存不足(
OOM)、文件描述符耗尽——检查dmesg | grep -i oom、ulimit -n容器/K8s 场景(2026 年高频) :
Pod重启/滚动更新、Service无Endpoint、DNS解析失败(no resolver defined to resolve)、网络策略未放行Pod IP
排查步骤(应急 → 根治)
- 先看
Nginx error log+access log:tail -f /var/log/nginx/error.log(区分透传502还是自身502,再定位upstream错误类型) - 本地验证后端:
curl -v http://127.0.0.1:<端口>/(后端本身通不通) - 检查后端进程与日志:
systemctl status <服务>、journalctl -u <服务> -n 50 - 检查网络层:
telnet <后端IP> <端口>(连通性)、防火墙/安全组 - 检查资源:内存(
free -h)、fd(ulimit -n、ss -s) - 定位后修复:重启服务、调超时、调 fpm 参数、改权限、扩容
- 先看
协助记忆
- 一句话:
502= 网关替后端"背锅",后端没给它结果 - 三步定位:先看
error log(区分透传)→ 再curl后端 → 最后查服务状态 - 记忆口诀:
refused= 连接被拒(没监听或防火墙REJECT);timeout= 网络丢包不通(DROP)或后端太慢;prematurely closed= 连接被对端掐断(后端重启/崩溃)
- 一句话:
进阶思考
502和503有什么区别?502是"网关拿到了请求、但上游没给出有效响应"(后端挂了、超时、端口不通、响应头无效);503 Service Unavailable是"服务暂不可用"——通常是主动拒绝(负载过高限流、维护模式、健康检查不过被摘除)。Nginx场景:upstream全部无健康节点时可能返回502或503(取决于配置),两者都要查后端
后端明明在跑,为什么还 502?
- 最常见三个:① 监听地址不对——进程活着但监听
127.0.0.1,Nginx从别的IP/容器连不上;② 端口不通——防火墙/安全组没放行;③ 处理超时——后端在跑但响应太慢超过proxy_read_timeout。所以"进程活着"不等于"能访问",本地curl才是最直接的验证
- 最常见三个:① 监听地址不对——进程活着但监听
🤔 网站 QPS 突降,如何快速定位问题?
QPS突降本质只有两类:来的人(流量)少了 或 能处理的人(能力)少了。快速定位的核心方法论:先验监控数据真伪 → 分层对比 + 时间线对齐 → 用"QPS/RT/错误码"组合区分故障类型。第 0 步:先确认监控数据是真的
2026年可观测性场景,第一排查项是采集端故障 ———agent挂掉、指标上报中断、Prometheus target失联、面板采样问题,造成"假突降"- 检查:
target up状态、数据是否连续、对比两个监控源、直接看access log数一下真实请求量
第一步:入口流量下降,还是后端处理能力下降?
看入口层
QPS(LB/CDN):- 入口
QPS也同步下降 → 流量没进来(DNS/CDN/攻击/上游调度/业务自然下降) - 入口
QPS正常、应用层QPS下降 → 流量进来了但处理不了(后端故障、限流、连接池)
- 入口
注意:入口掉先对比历史同期/同比——业务流量自然减少(活动结束、反爬拦截、投放停止)是最常见的原因,别一上来就当故障
第二步:用"
QPS/RT/ 错误码"三信号组合区分故障类型信号组合 故障类型 典型原因 应用 QPS降 + 错误码升(429/503/5xx)+RT平稳或降快速失败型 限流、熔断、连接池拒绝、健康检查摘除 应用 QPS降 +RT升 + 错误码平稳排队积压型 死锁、慢查询、 GC停顿、线程池占满入口与应用 QPS同时断崖归零入口型 DDoS黑洞、DNS故障、CDN故障入口正常 + 缓存命中率骤降 + DB层负载飙升缓存雪崩 缓存大批量过期,请求打穿到 DB- 关键点:快速失败的请求 RT 很短;RT 上升恰恰说明请求在排队等待而非快速失败——两者不要混淆
第三步:分层对比——流量在哪一层掉的?
- 链路:
CDN/LB(入口)→Nginx→ 应用 → 数据库/缓存 - 对比各层
QPS监控曲线:哪一层先掉、掉多少 - 入口掉 → 查
DNS/CDN/攻击/业务自然下降 - 入口正常、应用掉 → 查应用(发布、限流、死锁、连接池)
- 应用正常、
DB掉 → 查数据库(慢查询、锁、连接数)
- 链路:
第四步:对齐时间线——那个时间点发生了什么?
- 发布/变更:服务发布、配置变更、数据库迁移、证书更新/过期——先看是否有发布
- 告警:监控告警、电话告警
- 外部事件:
DNS变更、CDN故障、机房故障、攻击
常见原因清单(按可能性,
2026视角)入口类:
- 业务流量自然减少(活动结束、反爬拦截、投放停止)——最常见但最容易漏
DNS解析故障/变更:解析到错误地址、DNS污染CDN故障/回源失败:CDN节点异常、回源配置错误DDoS攻击触发黑洞:流量被运营商黑洞封禁,直接QPS断崖归零(注意黑洞前通常有bps/pps流量突增前兆;黑洞若发生在运营商侧,LB层QPS也会归零)WAF误拦:误封IP、规则误触发导致流量被拦
应用类:
- 发布/回滚:新版本有
bug,连接池/线程池耗尽 K8s/容器场景(2020s 最高频) :Pod驱逐/OOMKilled、HPA缩容、滚动发布副本不足、liveness/readiness探针失败摘流量、Ingress/CoreDNS故障、NetworkPolicy/CNI异常HTTPS证书过期:TLS握手大面积失败,QPS断崖——极高发根因- 死锁/卡死:应用线程全部阻塞,请求排队
- 连接池耗尽:应用连不上
DB/缓存,请求快速失败 - 限流/熔断触发:限流规则配置错误、熔断阈值过低——注意熔断被触发是保护在起作用,问题通常是阈值/判定窗口配置不当,属"正常触发但配置不当"
- 发布/回滚:新版本有
缓存/存储类:
- 缓存雪崩:缓存大批量过期,请求全部打到
DB——— 初期特征是缓存命中率骤降 +DB层QPS/负载飙升 +RT飙升,对外吞吐下降是DB过载后的结果,不是判断特征
- 缓存雪崩:缓存大批量过期,请求全部打到
资源类:
CPU打满、内存OOM、磁盘满数据库类:慢查询、锁等待、连接数打满
快速排查命令
- 看监控大盘:各层
QPS、错误码、RT、缓存命中率曲线(先确认数据真实,再定位掉在哪一层) - 看错误码分布:统计
access log状态码(先head -1确认字段位置 ——$9只在默认combined格式下是状态码,自定义log_format后要换字段序号)1 2head -1 /var/log/nginx/access.log awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn - 看后端资源:
top、vmstat 1、free -h、iostat -x 1 - 看连接:
ss -s、ss -lntp(-p需root) - 看数据库:
show processlist;(MySQL 8建议查performance_schema/sys库)、慢查询日志 - 看错误日志:
journalctl -u <服务> -n 200(仅systemd场景)
- 看监控大盘:各层
协助记忆
- 一句话:
QPS突降 = 要么没人来了(入口问题),要么来了也处理不了(后端问题) - 四步定位:先验监控真假 → 看入口
QPS分内外 → 用QPS/RT/错误码三信号分型 → 对齐时间线找事件 - 口诀:快速失败看错误码,排队积压看
RT,缓存雪崩看命中率,入口断崖看黑洞
- 一句话:
进阶思考
QPS突降和QPS不变但RT飙升,是一回事吗?- 不是。
QPS突降通常是入口流量减少或后端快速失败(限流/熔断/连接池拒绝——这些失败请求RT很短);QPS不变/略增 +RT飙升是典型的排队积压(资源打满、死锁、慢查询、GC停顿)——请求堵在处理队列里,吞吐上不去。最迷惑的组合是"QPS降 +RT升":这是排队积压型(请求变慢、还没失败),不是快速失败型,两者排查方向完全不同
- 不是。
限流规则配错了,怎么快速确认?
- 两个特征:①
QPS断崖式下降到限流阈值附近并稳定——比如配了100 QPS限流,QPS恰好稳定在100附近,基本就是限流;② 错误码特征 ———Nginx limit_req默认拒绝返回503(limit_req_status可改为429),看到大量503/429且QPS压线稳定,可确认。熔断同理:看熔断器状态指标(open 状态、错误率口径)
- 两个特征:①
🤔 简述 HTTPS 工作原理?
HTTPS=HTTP over TLS。一句话:在TCP之上加了一层TLS(传输层安全协议),用"混合加密"实现传输的三重保护——加密(防窃听)、完整性(防篡改)、身份验证(防冒充) 。核心难点在于:怎么在不安全的信道上安全地协商出密钥——这就是TLS握手解决的问题。两个加密体系(混合加密)
- 非对称加密:握手阶段使用 ———
RSA/ECDSA用于签名、RSA加密(仅TLS 1.2)、ECDH用于密钥协商。慢 - 对称加密(
AES-GCM/CHACHA20-Poly1305):双方用同一个密钥加解密。快,用于实际数据传输 - 为什么混合:非对称加密慢,对称加密快但需要双方共享同一个密钥——TLS 握手就是用非对称加密安全地把对称密钥协商出来
- 非对称加密:握手阶段使用 ———
TLS 1.2握手流程ClientHello:客户端发送支持的TLS版本、加密套件列表、客户端随机数(Client Random)ServerHello:服务器选择TLS版本、加密套件,返回服务器随机数(Server Random)Certificate:服务器发送证书(含公钥和CA签名)ServerKeyExchange:服务器发送DH参数(使用ECDHE时)ServerHelloDone:服务器告知"我的消息发完了"- 客户端验证证书(此时才验证):证书链完整、在有效期内、域名匹配(
SAN)、(可选)吊销状态——浏览器默认不做实时吊销检查(Chrome用CRLSets、Firefox用CRLite,OCSP自2023年起可选) ClientKeyExchange:客户端发送自己的DH公钥参数(或RSA加密的预主密钥)- 生成会话密钥:双方用随机数 + 预主密钥(
Pre-Master Secret)通过PRF生成主密钥(Master Secret),再派生出会话密钥 ChangeCipherSpec+Finished:双方确认开始用对称加密通信- 之后所有数据用对称加密传输
前向保密(
Forward Secrecy)- 核心思想:即使服务器的长期私钥泄露,历史上抓包记录的数据也无法被解密
- 实现方式:
ECDHE(临时DH)——每次会话生成临时密钥对,会话结束即销毁;即使服务器长期私钥被窃,也无法推导出过去的会话密钥 RSA密钥交换没有前向保密:私钥泄露 = 所有历史通信可解密(所以TLS 1.3移除了RSA密钥交换)
证书与 CA(身份验证)
- 证书 = 公钥 + 域名 + 有效期 + CA 的数字签名
- 验证链:客户端信任根 CA → 验证中级 CA 证书 → 验证服务器证书(信任链锚定)
- 数字签名用非对称加密的"私钥签名、公钥验签"实现:CA 用私钥给证书签名,客户端用 CA 公钥验签——验签通过说明证书确实由该 CA 签发、未被篡改
- 注意区分:传输完整性由 AEAD 的认证标签(对称 MAC)提供,不是数字签名——“签名防篡改"指的是 CA 给证书签名防证书被篡改
TLS 1.3的变化(2026 年主流)- 握手更快:
1-RTT(一次往返完成握手,相比 1.2 的 2-RTT);支持 0-RTT(会话恢复时首包即可发数据,但有重放风险) - 强制前向保密:只保留 ECDHE 密钥交换,移除 RSA 密钥交换
- 精简加密套件:移除弱算法(RC4、CBC 模式、SHA-1),只保留 AEAD(
AES-GCM/CHACHA20-Poly1305) - 握手消息精简:
key_share并入ClientHello/ServerHello(取代1.2的独立ServerKeyExchange/ClientKeyExchange消息),ServerHello后双方即算出握手密钥;ChangeCipherSpec被移除(仅保留为兼容性no-op)
- 握手更快:
HTTPS全链路(一次访问)TCP三次握手建立连接TLS握手协商出会话密钥(上述流程)- 之后
HTTP请求/响应全部用对称加密传输 - 连接关闭
2026年行业状态TLS 1.3已是主流(2025 年约 84% 站点支持),TLS 1.2仍兼容(降级兜底)- 证书有效期分阶段缩短:2025 年 4 月 CABF 通过 SC081v3 提案,2026 年 3 月起分阶段缩减,最终 2029 年 3 月降至 47 天(此前公开信任证书上限是 398 天,自 2020 年 9 月起)——— 自动化续期(Let’s Encrypt ACME)成为刚需
ECDSA证书占比上升(运算比RSA快,但Web PKI中RSA仍占绝大多数)
协助记忆
- 一句话:
HTTPS= 先"锁匠换钥匙”(握手协商对称密钥),再"快递柜存件"(对称加密传输) - 三个保障:加密防偷看、认证标签防篡改、证书防冒充
- 握手记忆:你好(ClientHello)→ 收到(ServerHello)→ 亮证件(证书)→ 我完了(ServerHelloDone)→ 验证件(验证证书)→ 换钥匙(密钥交换)→ 开锁(对称加密通信)
- 一句话:
进阶思考
为什么说
RSA密钥交换不安全,一定要ECDHE?RSA密钥交换下,客户端用服务器公钥加密预主密钥发给服务器——如果攻击者一直抓包记录流量,将来拿到服务器私钥,就能解密当年的预主密钥,从而还原所有历史会话。ECDHE每次会话用临时密钥对,私钥泄露也无法回推历史会话——这就是前向保密。2026年TLS 1.3已强制ECDHE
证书过期了,客户端为什么拒绝访问?
- 证书验证包含有效期检查——过期证书导致验证失败,浏览器显示"连接不安全"。这也解释了为什么证书有效期正在分阶段缩短到 47 天(2029 年前)后,自动化续期(ACME)是刚需:手动续期跟不上节奏,证书过期直接导致 HTTPS 大面积报错
🤔 如何设计高可用的 Web 服务架构?
高可用的本质:消除单点 + 快速恢复。设计目标不是"不出故障",而是"出了故障用户无感知或感知最小"。可用性指标:99.9%(一年宕机 < 8.76 小时)、99.99%(< 52.6 分钟)——不可用时间每差 10 倍,对架构的冗余和自动化要求指数级上升。
第一原则:无状态化(
Stateless)- 应用层无状态:每个请求不依赖本地存储,任何节点都能处理任何请求 ——— 这是水平扩展和故障转移的前提
Session不存本地:外置到Redis(或 JWT 无状态 token)- 有状态的组件(DB、Redis)单独部署,与应用解耦
分层冗余:每一层都要有多副本
- 接入层:
DNS多IP/ 多解析(智能DNS)、CDN(静态加速 + 分散攻击流量;大流量DDoS需专门的云高防如阿里云DDoS高防/AWS Shield;静态CDN对动态API无效) - 负载均衡层:L4(LVS/NLB)与 L7(Nginx/ALB)是分层串联关系(L4 入口 → L7 路由 → 应用),各层内部多实例——L4 主备 VIP 漂移、L7 多活,健康检查自动摘除故障节点
- 应用层:多副本部署(生产建议 ≥3 副本,如 3 个可用区各 1 — 2 副本时单 AZ 故障容量直接减半)、无状态水平扩展
- 数据层:主从复制(读写分离)、数据库集群、多副本备份
- 缓存层:Redis 主从 + 哨兵(或集群模式),避免单点
- 接入层:
故障转移与自我保护(失败处理)
- 健康检查:LB 定期探测,故障节点自动摘除(TCP/HTTP 探测)
- 超时控制:所有外部调用必须设置超时(连接超时 + 读超时),防止故障蔓延
- 重试有界:重试要有次数上限和退避(指数退避 + 抖动),避免重试风暴放大故障
- 熔断:下游故障时快速失败(断路器打开),保护调用方自身的线程/连接资源不被耗尽,同时给下游恢复窗口(如
Sentinel、Resilience4j) - 限流降级:流量超限时优先保核心业务(降级非核心功能),防止雪崩
- 幂等设计:接口幂等(重试不产生副作用),配合重试机制
数据层高可用
- 主从复制:一主多从,主挂自动切换(
Orchestrator、MySQL InnoDB Cluster/Group Replication,或云RDS自带高可用 ——— 注意MHA已停止维护,不再推荐新用) - 读写分离:读流量走从库,减轻主库压力;对一致性敏感的流量(写后立即读)仍走主库(主从延迟会导致"写后读"不一致)
- 分库分表:数据量超单库上限时水平拆分(但要权衡复杂度,通常先优化再分)
- 备份与恢复:定期全量 +
binlog增量,定期演练恢复(备份没演练过等于没有) - 容灾架构层级:同城双活(机房级故障,
Active-Active)→ 两地三中心(同城双活 + 异地灾备中心,灾备不承载流量)→ 异地多活/单元化(城市级故障,多城同时承载流量,复杂度极高)
- 主从复制:一主多从,主挂自动切换(
缓存高可用
Redis主从 + 哨兵自动故障转移,或Redis Cluster- 防缓存雪崩:过期时间加随机抖动、多级缓存(本地 + 分布式)
- 防缓存穿透:布隆过滤器、空值缓存
- 防缓存击穿:热点 key 加锁重建、永不过期 + 异步更新
弹性伸缩
- 水平扩缩容:云上
AS(Auto Scaling)、K8s HPA(按CPU/内存/自定义指标)、KEDA(事件驱动)、Cluster Autoscaler/Karpenter(节点级) - 容量规划:压测摸清单节点容量,预留
buffer(如 70% 水位告警)
- 水平扩缩容:云上
可观测性
- 监控:指标(QPS、RT、错误率、资源)、日志、链路追踪(
Metrics/Logs/Traces三支柱,Profiling作为第四信号已由OpenTelemetry推进) - 告警:分级告警、值班响应
- 故障演练(混沌工程) :主动注入故障(杀节点、断网络)验证架构韧性,如
Chaos Mesh、Chaos Monkey
- 监控:指标(QPS、RT、错误率、资源)、日志、链路追踪(
2026 年云原生实践
- K8s 多可用区部署(Pod 分布到不同 AZ,故障域隔离)
- 服务网格(Istio,Ambient Mesh 模式已 GA——sidecar 非唯一形态)提供熔断/重试/超时(但引入复杂度,按需选)
- 不可变基础设施(镜像部署,避免配置漂移)
协助记忆
- 一句话:高可用 = 消除单点(冗余)+ 快速恢复(自动化)
- 口诀:无状态、多副本、超时熔断限流、备份演练
- 比喻:高可用架构像"备用发电机"——平时用不到,停电时立刻顶上;设计目标是停电时用户家里的灯不闪
进阶思考
99.9%和99.99%差多少?值不值得追求?- 不可用时间相差
10倍 ———99.9%一年允许8.76小时宕机,99.99%只有52.6分钟。每多一个9,成本不是线性而是指数增长:需要更多冗余(多机房多活)、更完善的自动化(自动故障转移)、更快的恢复(分钟级)。中小业务99.9%通常够用;金融/交易类才需要99.99%+。设计时先问业务:宕机1小时损失多少?
- 不可用时间相差
为什么说"异地多活"是大厂才做的?
- 异地多活不只是"多部署几份"——它要求单元化:流量按用户维度路由到固定单元、数据按单元分片、跨单元数据一致性靠异步同步,还有 “全局路由 + 冲突解决"等难题。复杂度是数量级上升,投入产出比只有超大流量和强合规场景才划算。注意区分:两地三中心 ≠ 异地多活——两地三中心通常是"同城双中心双活 + 异地灾备中心(不承载流量)",异地多活要求多个城市节点同时承载流量。中小企业优先做同城双活 + 快速恢复
🤔 秒杀系统,怎么优化?
秒杀的本质挑战:瞬时超高并发 + 有限的库存 + 不能超卖。核心设计思想一句话:流量削峰、分层过滤、异步化、限流保护、防超卖——把"百万请求同时打到数据库"变成"百万请求被层层拦截,只有极少数真正到数据库”。
第一层:前端/页面优化(把流量挡在最外面)
- 页面静态化 +
CDN:秒杀页面预生成静态页,用户浏览不占后端资源 - 按钮置灰 + 倒计时:秒杀开始前按钮不可点,减少无效请求
- 人机校验(验证码/滑块) :拦截脚本刷单(反作弊,防止秒杀器)
- 注意:前端校验可被脚本/抓包绕过,必须后端同样限流——前端校验只是减负,不是安全边界
- 页面静态化 +
第二层:接入层优化(网关/Nginx)
- 网关限流:按 IP/用户/接口限流(令牌桶),超限直接返回"拥挤"
WAF防刷:拦截恶意流量Nginx层快速失败:秒杀接口在接入层就丢弃大部分流量(能挡在前面就不放进来)
第三层:应用层优化
- 本地缓存热点:秒杀商品信息(名称、价格、图片)放本地缓存 + Redis 二级缓存,秒杀期间商品信息接口根本不查库(注意热点
key击穿:key失效瞬间并发回源打爆DB,可用"永不过期 + 异步刷新"或互斥重建) - 请求排队(MQ 削峰) :下单请求先写入
MQ,后端异步消费处理——把"瞬时洪峰"变成"平稳水流"(削峰填谷) - 库存预扣减放
Redis:秒杀前把库存预热到Redis,扣减用Redis+Lua脚本原子操作——脚本内完成"检查库存 > 0"和"扣减"(一个原子操作,Redis执行Lua期间不插入其他命令),避免超卖(Redis 7+官方推荐用Functions FCALL替代裸EVAL做脚本治理) - 令牌桶/信号量:应用层并发控制,限制同时处理的下单数
- 本地缓存热点:秒杀商品信息(名称、价格、图片)放本地缓存 + Redis 二级缓存,秒杀期间商品信息接口根本不查库(注意热点
第四层:数据层优化
- 数据库兜底扣减:Redis 预扣减后,真正落库时用条件更新(原子自减) :
UPDATE t_stock SET stock = stock - 1 WHERE product_id = ? AND stock > 0影响行数为 0 说明库存不足——InnoDB 行锁保证同库存行串行,不超卖(这是防超卖最后一道防线。注:这常被通俗叫"乐观锁",但与教科书 version 版本号乐观锁不同,秒杀场景WHERE stock > 0是主流) - 异步落库:成功扣减的请求异步写订单,最终一致性
- 超时未支付回补:订单超时未支付,回补
Redis库存(延迟消息/定时任务)——若创建订单时已做DB兜底扣减,需同时回补DB与Redis,且防重复回补(幂等)
- 数据库兜底扣减:Redis 预扣减后,真正落库时用条件更新(原子自减) :
核心链路(一次秒杀请求)
- 用户点秒杀 → 前端校验 → 网关限流 → 应用层
- 应用层直接执行 Lua 原子扣减(脚本内完成"检查库存 + 扣减")→ 成功则写 MQ(注意:Redis 扣减与写 MQ 之间不是同一事务,存在失败窗口,靠对账/回补兜底)
- 后端消费 MQ → 异步创建订单 → 数据库条件更新兜底
- DB 兜底扣减失败时,必须回补 Redis 库存(幂等) ——否则失败请求的库存被吞,Redis 与实际销量不一致
- 用户支付;超时未支付 → 回补库存(DB + Redis)
防超卖的三道防线
Redis + Lua原子扣减(第一道,拦截绝大多数并发)- 数据库条件更新
WHERE stock > 0(第二道,兜底,DB失败需回补Redis) - 对账(第三道,事后发现超卖/漏单,补偿)
2026年云原生弹性(秒杀的天然场景)KEDA事件驱动伸缩:按MQ队列深度/Kafka lag自动扩缩(可缩到 0)——秒杀洪峰来了自动扩容,峰值过后缩容,避免按峰值长期备机Serverless按量计费:突发流量按调用量计费,适合秒杀这种"一年就几次"的场景- 策略:预置容量到预估峰值(保证开场不被击穿)+ KEDA 按积压自动扩容 + 峰值后缩容
协助记忆
- 一句话:秒杀 = 把"洪水"变成"细水长流"——层层拦截 + 排队处理
- 口诀:静态化挡浏览、限流挡请求、MQ 挡洪峰、Redis 挡超卖、条件更新兜底
- 秒杀像"春运抢票"——窗口(数据库)就几个,黄牛(脚本)要拦,先排队(MQ)再出票(异步落库),票(库存)没了就显示无票(不会超卖)
进阶思考
为什么扣库存一定要用
Lua原子脚本,直接用Redis的get + decr不行吗?- 不行。
get和decr是两个独立命令,高并发下两个线程可能同时读到库存=1,然后都执行 decr——导致超卖。Lua 脚本把"检查库存 > 0"和"扣减"合并成一个原子操作(Redis 执行 Lua 脚本时不会插入其他命令),从机制上杜绝了竞态。同理数据库侧用 UPDATE … WHERE stock > 0 而不是"先 SELECT 再 UPDATE"
- 不行。
Redis扣减成功了,但MQ消费失败怎么办?库存会不一致吗?- 会——这是分布式事务问题。常见方案:① 本地消息表:落库扣减与消息记录同库同事务,定时任务扫表投递 MQ(注意:Redis 预扣减与投递之间的窗口无法同事务,靠对账/回补兜底;也可用 RocketMQ 事务消息/半消息);② 对账补偿:定时任务对比 Redis 库存、订单、支付状态,发现不一致就补偿(回补或重发);③ 消费端幂等:同一个请求只能创建一个订单(订单号唯一索引),重复消费不产生重复订单。秒杀场景通常接受"最终一致"——不要求实时强一致,但必须保证不超卖、不漏单
🤔 在高并发情况下,如何避免数据库的性能瓶颈?
数据库瓶颈的本质:数据库是有状态的、共享的、单点写(传统单主架构下) 的资源——请求越多,连接数、锁竞争、磁盘 IO、CPU 都成为瓶颈。核心思路:能不进 DB 就不进 DB(缓存挡)、能不进主库就不进主库(读写分离)、能减少请求就减少请求(异步化)、能快速返回就快速返回(索引与 SQL 优化) 。
第一层:缓存前置(把读流量挡在数据库外面)
- 多级缓存:本地缓存(
Caffeine/Guava)+ 分布式缓存(Redis) - 热点数据(商品信息、配置、用户资料)缓存化,命中率高的场景能挡掉
80%+的读流量 - 缓存三防:
- 穿透:查不存在的
key每次都打DB——— 布隆过滤器、空值缓存 - 击穿:热点
key失效瞬间并发回源——互斥锁重建、永不过期+异步刷新 - 雪崩:大量
key同时过期——过期时间加随机抖动、多级缓存
- 穿透:查不存在的
- 多级缓存:本地缓存(
第二层:读写分离(让主库专心写)
- 一主多从:读流量走从库,写流量走主库
- 主从复制有延迟 ——— 对一致性敏感(写后立即读)的流量必须走主库,其余读走从库
- 中间件:应用层路由(读写分离框架)或代理层透明路由(
ProxySQL、MySQL Router、MaxScale、ShardingSphere等) - 共享存储一写多读架构(如
Aurora、PolarDB):从库直接读共享存储,显著降低传统binlog异步复制的延迟,是"主从延迟"问题的重要解法 - 适用前提:读多写少的业务(大部分互联网业务)
第三层:连接治理(防止连接被打爆)
- 连接池:应用侧连接池(
HikariCP/Druid),池大小要合理——不是越大越好,过大反而增加上下文切换和锁竞争。经验公式(源自PostgreSQL,HikariCP wiki转引):连接数 ≈CPU核数 ×2+ 有效磁盘数 ———SSD场景下趋近核数×2(SSD 越快阻塞越少),最终以压测为准 - 数据库侧
max_connections设置合理上限,防止连接数被打满 - 排查连接打满:
show processlist(MySQL 8建议查performance_schema/sys库)、ss -lntp | grep 3306看连接数
- 连接池:应用侧连接池(
第四层:
SQL与索引优化(让每个请求更快)- 慢查询日志定位:
slow_query_log开启,mysqldumpslow分析 EXPLAIN分析执行计划:确认走索引、避免全表扫描(MySQL 8.0.18+可用EXPLAIN ANALYZE看实际执行)- 常见索引失效:函数包裹索引列(
MySQL 8.0支持函数索引可解)、隐式类型转换、前导模糊查询%xx、OR连接的列并非全部可走索引(OR两侧都是索引列时可用index_merge) - 避免大事务:事务要短、批量操作分批、避免长事务持有锁
- 避免深分页:
LIMIT 100000,10→ 用游标/WHERE id> 上次最大值 方式
- 慢查询日志定位:
第五层:异步化与削峰(减少对 DB 的瞬时冲击)
MQ削峰:写操作(日志、统计、非实时数据)先入队,异步批量落库——注意:订单等对"写后立即可见"敏感的流量不能异步,需同步落库或异步后读主库兜底- 批量写入:合并多条
insert为一条批量insert,减少事务与网络开销
第六层:分库分表(水平扩展,最后手段)
- 适用:单库数据量巨大、写入吞吐超单库上限、缓存与读写分离都无法解决
- 拆分方式:垂直拆分(按业务域拆库)、水平拆分(按分片键拆表,如
user_id取模/范围) - 代价:跨分片
join、分布式事务、全局唯一ID、扩容搬迁——复杂度极高,先优化再分,能不分就不分 - 替代路线:分布式数据库(
TiDB等)——对应用透明地水平扩展,避免自研分库分表中间件的复杂度;部分分布式数据库还提供HTAP列存(TiDB TiFlash、PolarDB列存索引),分析型查询走列存,与在线交易隔离
第七层:参数与硬件兜底
InnoDB innodb_buffer_pool_size:经验值取内存的60%-70%(需为连接、排序缓冲、OS缓存留余量);MySQL官方推荐专用服务器用innodb_dedicated_server自动配置(>4GB内存时取75%);MySQL 8.0默认仅128MB需调大,8.0.14+支持在线调整SET GLOBAL innodb_buffer_pool_size- 磁盘用
SSD/NVMe,监控IOPS与延迟 - 云数据库(RDS):自带高可用与弹性,规格不够直接升配;
Serverless模式(如Aurora Serverless、PolarDB Serverless)按负载自动扩缩,适合流量波动大的场景
协助记忆
- 数据库是后厨的"一口炒锅"——客人再多,锅只有一口。怎么让店不崩?
- 橱窗里摆好常点的菜(缓存),客人直接拿走,不用惊动后厨;
- 菜单按食材分类排好(索引),大厨一翻就能找到;
- 点单先写单排队(
MQ削峰),后厨一桌桌做; - 加个帮厨(从库)专管切配(读),主厨只管炒菜(写);
- 真要人满为患,就开分店(分库分表)——但开分店要协调食材采购、跨店调货,麻烦得很,能不开就不开
- 口诀(一句) :缓存挡、索引快、队列削、分离扛
- 数据库是后厨的"一口炒锅"——客人再多,锅只有一口。怎么让店不崩?
进阶思考
连接池是越大越好吗?
- 不是。连接池过大,空闲连接占用数据库内存,且线程切换、锁竞争反而加剧。业界经验公式:
连接数≈CPU 核数×2+有效磁盘数(SSD 下趋近核数×2)——但真实值要结合压测(响应时间、吞吐)调优。宁可"少量连接快速周转",不要"大量连接排队等待"
- 不是。连接池过大,空闲连接占用数据库内存,且线程切换、锁竞争反而加剧。业界经验公式:
读写分离一定能解决瓶颈吗?什么场景不行?
- 读写分离只解决读多写少场景。如果瓶颈在写(写入量大、单库写入上限),读写分离无效——此时需要分库分表或分布式数据库;如果业务强一致性要求高(写后立即读、金融类),主从延迟会导致读从库读到旧数据,需要读走主库或引入同步手段(共享存储一写多读架构可降低延迟)。先定位瓶颈在"读"还是"写",再选方案
缓存和数据库的一致性怎么保证?
- 没有完美的强一致方案,常见做法:先更新数据库,再删除缓存(Cache Aside 模式)——而不是先更新缓存(并发下容易脏数据)。删除缓存后下一次读会 miss 并回源重建,天然兜底。极端一致要求下可引入 binlog 订阅(Canal)异步刷新缓存或监听变更。接受"最终一致",把不一致窗口压缩到最小
🤔 如何设计高并发的 Web 服务架构?
高并发架构的本质:用"水平扩展"换吞吐,用"削峰填谷"扛峰值。核心三件事:无状态化(能加机器就扛得住)、分层削峰(把瞬时峰值摊平)、单点加速(让每个请求更快)。和高可用架构不同——高可用解决"挂了怎么办"(冗余+恢复),高并发解决"来得多怎么办"(吞吐+性能)。
第一原则:无状态化 + 水平扩展(高并发的根基)
- 应用层无状态:请求不依赖本地存储,任何节点都能处理任何请求——这是"加机器就能扛"的前提
- 状态外置:
Session放Redis(或JWT无状态)、文件放OSS、配置放配置中心 - 水平扩展能力 = 架构的"弹性上限":用压测验证近似线性扩展(追求"加 2 倍节点扛 2 倍流量"的目标,实际受有状态组件、热点、GC、网络影响,接近线性已属优秀)
- 有状态组件(DB、Redis)是水平扩展的卡脖子点——所以核心思路是把状态全部外置,让应用层彻底无状态
第二层:入口分流(流量先分层打散)
- CDN:静态资源(图片、JS、CSS)就近缓存,挡掉大量重复请求
- L4(LVS/NLB)→ L7(Nginx/ALB)分层负载均衡:L4 扛连接、L7 做路由,各自水平扩展
- 多机房流量调度(GSLB/智能 DNS):流量按地域/运营商分流
第三层:缓存(让绝大多数请求不落库)
- 多级缓存:浏览器缓存 →
CDN→ 本地缓存(Caffeine/Guava)→ 分布式缓存(Redis) - 命中率是核心指标:缓存命中率高,数据库压力成比例下降(前提是缓存数据正确)
- 缓存与数据库一致性(Cache Aside):先更新数据库,再删除缓存——而不是先更新缓存(并发下容易脏数据);删缓存后下次读 miss 回源重建,天然兜底;极端一致要求可
binlog订阅(Canal)异步刷新 - 缓存三防:穿透(布隆过滤器/空值)、击穿(互斥重建/永不过期异步刷新)、雪崩(随机过期/多级缓存)
- 热点
key专项:读热点(单 key 打爆 Redis 单分片)——本地缓存 + 热点识别;写热点(秒杀库存)——Redis 原子操作 + MQ 削峰
- 多级缓存:浏览器缓存 →
第四层:异步化(削峰填谷)
MQ削峰:非实时写操作(日志、统计、消息通知)先入队,后端按能力消费——把瞬时洪峰变成平稳水流- 异步落库:读多写少场景,写请求异步合并批量落库
- 注意:强一致/写后立即读的流量不能异步,需同步处理
第五层:限流与保护(防止被峰值打垮)
- 令牌桶限流(网关/应用层):控制进入系统的请求速率,超限快速失败(返回"拥挤");也可在服务网格(Istio/Envoy)层统一做限流/熔断 /重试
- 熔断:下游故障时快速失败,保护本服务线程/连接资源
- 降级:非核心功能(推荐、搜索联想)在压力下自动关闭,保核心链路
- 超限请求怎么处理(两种语义要分清) :拒绝(直接丢弃,快速失败)还是排队(延迟放行,类似漏桶)——按业务容忍度选;这和
MQ削峰的"排队"不是一回事(一个是限流策略,一个是异步解耦)
第六层:数据库层(写瓶颈的最后解法)
- 读写分离:读走从库、写走主库(读多写少场景)——注意主从延迟:写后立即读的流量必须走主库
- 分库分表:写瓶颈/数据量大时水平拆分(按分片键),或改用分布式数据库(对应用透明)
- 连接池:应用侧合理池大小,避免连接打爆数据库
第七层:弹性伸缩(按需扩容)
- 指标驱动扩缩:
K8s HPA(CPU/内存/自定义指标)、KEDA(按MQ队列深度/Kafka lag事件驱动——本质是监控事件源并喂数据给HPA,0↔1由KEDA负责、1↔N由HPA负责) Serverless/Knative:事件驱动 + 从 0 冷启动 + 按量计费,适合流量波动大的场景- 容量规划:压测摸清单节点容量,按峰值预留(预置到预估峰值的 80%,配合弹性兜底余量)
- 扩容要有"提前量":弹性扩容有分钟级延迟,突发流量靠"预置容量 + 快速扩缩"双保险
- 指标驱动扩缩:
性能细节(让每个请求更快)
- 连接复用:HTTP/2 多路复用、HTTP/3(QUIC,弱网场景收益明显)、连接池、长连接
- 接口批量:合并多次调用为一次批量接口(减少 RTT)
- 压缩:gzip/br 压缩响应体,减少带宽与传输时间
- 协议选型:内部服务间用 gRPC(二进制 + HTTP/2)比 JSON over HTTP/1.1 吞吐更高
- 线程模型有两条路线:① 非阻塞事件驱动(Netty/Reactor 回调式);② 同步阻塞写法 + 虚拟线程(JDK 21+,线程廉价化,让开发者继续写
thread-per-request代码,JVM 在阻塞点自动让出载体线程)—— 两者都能提升吞吐,路线不同
协助记忆
- 高并发 = 节假日的高速收费站。
- 车道要多(水平扩展),而且车不能都堵在一个窗口办手续——车过收费站不排队,直接 ETC 抬杆放行(缓存命中),少数没装 ETC 的才停车办证(落库);
- 车流太大时匝道限流(限流),一辆辆放进去,别把站口挤爆;
- 大货车不赶时间,先分流到服务区歇着(MQ 异步),高峰期过了再走;
- 想要扛住国庆,收费站平时就要演练"开满所有车道"(压测+弹性伸缩)
- 口诀(一句) :无状态、加机器、缓存挡、异步削、限流保
- 高并发 = 节假日的高速收费站。
进阶思考
水平扩展是"加机器就行"吗?什么会卡脖子?
- A:前提是无状态化——如果应用把
Session存本地、文件存本地,加机器反而出乱子。真正的卡脖子点是有状态组件:数据库单库写入上限、Redis单实例内存上限。所以高并发架构的本质工作,是把一切状态外置(Session→Redis、文件→OSS、数据→DB/分片),让应用层成为"纯计算资源",才能无限加机器。写瓶颈最终要靠分库分表或分布式数据库解决
- A:前提是无状态化——如果应用把
QPS、并发数、RT是什么关系?Little's Law:并发数 =QPS×RT(平均响应时间) 。例:RT=100ms时,要扛1000 QPS,需要约100个并发(1000 × 0.1)。注意:这里的"并发"指系统内平均在途请求数(in-flight),不等于压测工具的线程数 ——— 压测线程数 ≠ 在途请求数,规划线程池/连接池时勿直接套用。RT 越长,同样的 QPS 需要越多并发资源(线程/连接)——所以"降RT"既是用户体验优化,也是容量优化:RT 降一半,同等资源能扛 2 倍 QPS
限流算法令牌桶和漏桶有什么区别?
- 令牌桶:以固定速率往桶里放令牌,请求要拿令牌才能过——允许突发(桶里攒的令牌可以一次性放行一波),适合大部分业务(允许短时突发);漏桶:请求进桶,按固定速率流出——完全平滑,不允许突发,适合需要严格匀速的(如对下游 API的调用)。令牌桶是"允许偶尔冲一下",漏桶是"永远匀速"
🤔 网站会监控哪些指标?
网站监控的本质:分层监控 + 分级关注——从"用户感知"到"应用"到"基础设施"逐层覆盖,同时分清哪些是红线指标(可用性、错误率),哪些是辅助指标(资源水位)。核心方法论两个:RED(看应用服务)和 USE(看基础设施资源)。
第一层:用户侧指标(用户实际感受到的)
可用性(Uptime) :多地域拨测(HTTP 探测),红线指标——网站能不能打开
Core Web Vitals(核心 Web 指标) :
- LCP(Largest Contentful Paint,最大内容绘制):页面主要内容加载速度,阈值 ≤ 2.5s 为良好
- INP(Interaction to Next Paint,交互到下一帧):页面交互响应速度,阈值 ≤
200ms为良好。已取代FID——FID只衡量"首次输入到开始处理"的延迟,INP 覆盖整个交互过程(输入延迟→事件处理→下一帧绘制),且统计的是最差的那次交互(交互较多时取第 98 百分位) - CLS(Cumulative Layout Shift,累积布局偏移):页面视觉稳定性,阈值 ≤ 0.1 为良好
前端 JS 错误率:页面报错比例,通过前端埋点(window.onerror)采集
第二层:应用层指标(RED 方法)
- Rate(速率/QPS) :每秒请求数,衡量吞吐——按接口/服务维度
- Errors(错误率) :5xx 比例(服务端错误)、4xx 比例(客户端错误,关注异常升高如被刷)、业务失败率(下单失败率)
- Duration(耗时/RT) :响应时间,重点看分位数而非平均值——P50(中位数,一半请求慢于它)、P95(95% 请求快于它,代表大多数用户体验)、P99(99% 请求快于它,近似代表最差体验)——平均值会被长尾拉高,掩盖问题
- 业务指标:订单量、支付成功率、注册量等(技术指标是手段,业务指标是目的)
第三层:中间件层指标
Nginx:连接数(活跃/等待)、QPS、upstream响应时间、5xx/4xx数量Redis:命中率(低命中率 = 缓存失效/穿透)、内存使用(接近maxmemory会淘汰)、连接数、慢命令MQ:队列积压量(积压 = 消费者跟不上)、消费速率、消息堆积时间
第四层:数据库层指标
- 连接数(接近
max_connections会拒连) - 慢查询数量与耗时(慢查询日志)
- 主从延迟(从库落后主库时间,延迟大 → 读旧数据)
QPS/TPS、缓冲池命中率(InnoDB buffer pool hit ratio)
- 连接数(接近
第五层:基础设施层指标(USE 方法)
Utilization(利用率) :CPU 使用率、内存使用率、磁盘使用率、带宽使用率Saturation(饱和度) :CPU 运行队列长度(load average 近似——它包含 D 态不可中断睡眠任务,受 IO 阻塞影响)、内存 swap 使用、磁盘 IO 等待(iowait/await)、TCP 连接队列溢出——饱和比利用率更能反映"快撑不住了"Errors(错误) :网卡丢包、磁盘 IO 错误、硬件告警(SMART)- 资源水位告警线:如磁盘 80% 告警、90% 紧急——留出处理时间
指标背后的设计原则
- 指标要可行动:一个指标对应一个明确的处理动作(磁盘 90% → 清理/扩容)
- 指标要有分级:红线(可用性、错误率)优先告警,辅助指标(资源水位)低级别告警
- SLI/SLO:把指标和承诺挂钩 ———
SLI(Service Level Indicator)是实测指标值(如"请求延迟P99=450ms"),SLO(Service Level Objective)是目标(如"P99 < 500ms"或"99.9% 的请求延迟 < 500ms",二选一,不要双重百分位嵌套)——监控告警围绕 SLO 设计,超 SLO 才真正需要打扰人 - 可观测性三支柱:Metrics(指标)+ Logs(日志)+ Traces(链路追踪)配合使用——指标发现异常、日志定位原因、链路定位调用关系(Profiling 作为第四信号正被推进)
协助记忆
- 监控网站 = 每年给网站做体检。
- 挂号能挂上、医生看得见人(可用性拨测)——— 人都不来医院就说明大门关了;
- 分科室查:内科查心脏(CPU)、血压(内存)、血常规(磁盘 IO),外科看伤口(错误率);
- 心电图(RT 曲线)不能只看"平均心跳",要看 P99 那个"最差的心跳间隔"——偶尔漏跳一次才是隐患;
- 体检报告要有"危急值"(红线指标)和"参考值"(资源水位)——危急值半夜也要叫醒你,参考值偏高只是提醒你该注意了
- 口诀:可用性是红线,QPS/RT/错误率三件套,RED 看应用、USE 看资源
- 监控网站 = 每年给网站做体检。
进阶思考
为什么看 RT 要看 P95/P99,而不是平均值?
- 平均值会被长尾拖累或掩盖。例子:100 个请求,99 个 10ms,1 个 10s——平均值约 110ms,看起来"还行",但 P99 是 10s,说明有 1% 的用户体验极差。分位数能暴露"最差的那批用户体验"——P99 才是用户能感受到的"最坏情况",优化目标是压低 P99 而不是平均值
QPS、RT、错误率都正常,网站就一定没问题吗?
- 不一定,三个盲区:
- 业务指标:接口全绿但订单量暴跌(支付通道故障、业务逻辑 bug),技术指标反映不出来;
- 资源饱和度:QPS 没涨但 CPU 运行队列堆积(load 高),说明在排队,用户体感变慢;
- 未知的未知:没监控到的环节(第三方 API、DNS 故障)出问题,指标根本采集不到。
- 监控要"技术 + 业务"结合,且要主动发现盲区(拨测、故障演练)
- 不一定,三个盲区:
指标告警一直响但查不出问题,怎么办?
- 大概率是告警设计问题:阈值太敏感(抖动就报警)、指标无关联信息(只有数字没有上下文)。改进:
- 阈值用分位数而不是瞬时值(如"P99 连续 5 分钟超阈值");
- 告警带上关联信息(错误日志片段、链路追踪链接);
- 收敛告警(同类合并、抑制依赖告警);
- 定期清理无效告警规则——告警疲劳是监控体系失效的头号原因
- 大概率是告警设计问题:阈值太敏感(抖动就报警)、指标无关联信息(只有数字没有上下文)。改进:
🤔 HTTP 长连接和短连接有什么区别及适用场景?
核心区别一句话:短连接是"每次请求都新建 TCP 连接、用完即关";长连接(Keep-Alive)是"同一条 TCP 连接上复用多个请求/响应"。区别的本质在于:建连成本(TCP 三次握手 + HTTPS 下还有 TLS 握手)和服务器连接资源占用之间的权衡。
短连接(HTTP/1.0 默认行为)
- 机制:每个请求都新建 TCP 连接,请求/响应完成后立即关闭
- 开销:每次都要 TCP 三次握手(HTTPS 还要加 TLS 握手——比 TCP 握手更贵,通常 1-2 个额外 RTT + 加解密计算)
- 优点:连接用毕即释放,不长期占用服务器连接资源(文件描述符、内存)
- 缺点:频繁建连带来握手开销和高延迟;大量建连导致 TIME_WAIT 堆积(详见进阶思考)
- 控制方式:HTTP/1.0 用
Connection: keep-alive显式开启;HTTP/1.1 默认开启无需声明,用Connection: close关闭
长连接(Keep-Alive,HTTP/1.1 默认行为)
- 机制:一次 TCP 连接建立后,可连续发送多个请求/响应,连接空闲超时或达到最大请求数才关闭
- 开销:只在首次建连时握手,后续请求省掉握手 RTT——高频交互场景延迟和开销显著降低
- 优点:省握手、低延迟、吞吐高
- 缺点:长期占用服务器连接资源——连接数上限受服务器 fd/内存限制,连接数打满时新请求进不来
- 控制方式:HTTP/1.1 默认开启;服务端通过 keepalive_timeout(空闲多久关闭)、keepalive_requests(最多复用多少次)、keepalive_time(单连接最长存活)三个维度控制
关键点:长连接不是"永不断开"
- 服务端有
keepalive_timeout(如 Nginx 默认 75s):连接空闲超时后关闭,释放资源 - keepalive_requests:连接上最多处理的请求数(Nginx 1.19.10+ 默认 1000,旧版默认 100),防单连接无限占用
- keepalive_time:单连接最长存活时间(Nginx 1.19.10+,默认 1 小时)
- 中间设备(防火墙、负载均衡)可能静默断开空闲连接——客户端要能处理"连接被重置"(重试机制)
- 服务端有
反向代理场景的
keepalive(upstream连接复用)- 浏览器 ↔ Nginx 之外,Nginx ↔ 后端也要配:upstream 块里 keepalive 32;——注意语义:它表示每个 worker 进程最多保留 32 条"空闲"上游连接(LRU 淘汰),并不限制 worker 向上游打开的总连接数(官方文档明确)
- 生效前提:需配合 proxy_http_version 1.1; + proxy_set_header Connection “";(清空 Connection 头)
- 效果:Nginx 到后端的 TCP 连接复用,避免每个请求都向后端重新建连(后端连接数压力大时收益明显)
- 版本说明:Nginx 1.29.7+ 的 upstream keepalive 默认启用(默认 keepalive 32 local,每 worker),1.27.x 及更早需手动配置
HTTP/2与HTTP/3的演进HTTP/2:在同一 TCP 连接上多路复用多个并发请求,解决的是 HTTP/1.1 同连接串行请求的应用层队头阻塞——但 TCP 层队头阻塞仍在(丢包会阻塞整条 TCP 连接),本质是更彻底的长连接HTTP/3(QUIC):基于 UDP,用 Connection ID 标识连接(可跨 IP/端口迁移)、0-RTT 建连(有重放攻击风险,需应用层防护)、多流独立消除了 TCP 层队头阻塞;连接模型是 QUIC 长连接而非 TCP 连接
适用场景对比
- 短连接适用:请求频率低、交互间隔长、并发不高——用完即释放,不浪费资源。例:低频 API 调用、客户端与服务端偶发通信、一次性的查询请求
- 长连接适用:请求密集、高频交互、低延迟要求——省握手成本。例:网页加载(页面 + 静态资源 + 接口多次请求)、频繁调用的 API
- 关键判断:请求频率 × 建连成本 > 连接资源占用成本 → 用长连接;反之用短连接
- 注意:
WebSocket是经HTTP Upgrade建立的独立全双工连接,机制上不同于HTTP keep-alive的请求复用——它是另一类长连接,不属于本问题讨论的keep-alive
协助记忆
- 短连接 = 每次有事都要重新拨号(拨号 = TCP 握手),说完就挂,下次再拨
- 长连接 = 电话不挂断,一件事接一件事说。
- 偶尔才联系一次的(低频),挂断重拨无所谓——短连接合适;
- 天天要打几十通的(高频),每次都重拨烦死了——长连接合适。但电话一直占着线(长连接占资源),别人就打不进来了,所以要有"通话超时 自动挂断”(keepalive_timeout)
- 口诀:高频长连省握手,低频短连省资源,长连管好超时数,短连小心 TIME_WAIT
进阶思考
为什么
HTTPS下长连接的收益更大?- 因为 TLS 握手比 TCP 握手贵得多——TCP 三次握手只要 1 个 RTT,TLS 1.2 完整握手要 2 个 RTT(TLS 1.3 也要 1-RTT),加上证书验证的加解密计算。短连接下每次请求都重复这套成本;长连接只在首次建连时付一次,后续请求全部省掉。所以 HTTPS 场景(尤其移动端弱网)长连接收益显著
TIME_WAIT是谁产生的?堆积了会有什么后果?- 谁先主动关闭(先发 FIN)谁产生
TIME_WAIT,保持约 2MSL(60s 左右),目的是让迟到的包在网络中消亡、防止新连接收到旧包。短连接下服务端/中间件通常先关闭(响应无 Content-Length 时靠关闭连接标识消息结束)——服务端 TIME_WAIT 堆积是经典问题,但服务端监听端口固定,TIME_WAIT 不阻塞入站新连接,主要消耗连接表项/内存;客户端侧 TIME_WAIT 才消耗本地临时端口(默认约 3 万,极端耗尽报Cannot assign requested address)。 - 缓解:
- 优先用连接池/长连接(从根源减少建连);
- 客户端侧可开 tcp_tw_reuse(依赖 tcp_timestamps,NAT 环境有隐患,且内核 4.12+ 复用条件收紧;tcp_tw_recycle 已在 4.12 移除);
- 扩大 net.ipv4.ip_local_port_range
- 谁先主动关闭(先发 FIN)谁产生
长连接是不是越多越好?
- 不是。长连接虽然省握手,但每一条都占用服务器资源(fd、内存、内核连接表)。连接数超过服务器上限,新请求直接拒绝。所以生产 要控三件事:
- 服务端
keepalive_timeout不能太长(空闲连接尽快释放); keepalive_requests/keepalive_time限制单连接复用次数与最长存活;- 连接数监控告警(接近
max时扩容或调参)。“长连接 + 合理超时 + 连接数监控"才是正确姿势
- 服务端
- 不是。长连接虽然省握手,但每一条都占用服务器资源(fd、内存、内核连接表)。连接数超过服务器上限,新请求直接拒绝。所以生产 要控三件事:
🤔 HTTPS 中 CA 证书在客户端还是在服务端?作用是什么?
这个问题有一个常见歧义,要先拆开:“CA 证书"其实指两个不同的东西——CA 的根证书(信任锚) 和 CA 签发的服务器证书。一句话回答:服务器证书在服务端,CA 根证书在客户端(浏览器/操作系统的信任库) 。两者的作用合在一起,完成 HTTPS 的"身份验证”:证明"我连的这个服务器确实是它声明的那个域名”。
两个证书要分清
- 服务器证书(叶子证书,在服务端) :由
CA签发,绑定"域名 + 公钥",存放在 Web 服务器(Nginx 配置的 ssl_certificate),TLS 握手时发给客户端 CA根证书(信任锚,在客户端) :CA自签的根证书,存放在客户端的信任库(浏览器/操作系统证书库——Chrome 用系统信任库,Firefox 用自带的 NSS 信任库),客户端用它验证服务器证书的签名——信任链的起点
- 服务器证书(叶子证书,在服务端) :由
证书链(三层)
- 根
CA证书 → 中间CA证书 → 服务器证书(叶子) - 服务端在握手时发送:服务器证书 + 中间 CA 证书(链拼接) ——— 规范与实践均推荐不发送根证书(客户端信任库里已有,无需传输;发送了客户端也会忽略)
- 客户端验证流程:用本地信任的根证书公钥 → 验证中间 CA 的签名 → 用中间 CA 的公钥 → 验证服务器证书的签名 → 再检查域名匹配(SAN,由浏览器应用层执行)、有效期、(可选)吊销状态
- 根
TLS 握手中的位置与"验证"和"密钥协商"的分工
ClientHello/ServerHello———TLS 1.3中ECDHE密钥协商参数(key_share)在这两条消息里就已交换- 服务器发送 Certificate 消息(服务器证书 + 中间 CA 证书)
- 客户端用本地信任库的
CA根证书验证证书链,再验证CertificateVerify签名(证明服务器持有证书对应私钥)——失败则报"证书不受信任/连接不安全" - 注意:证书验证与密钥协商是两回事 ———
TLS 1.3中密钥协商先于证书验证完成,证书公钥不参与密钥协商,只用于验证持有私钥(“用证书公钥加密协商密钥"只是已废弃的TLS 1.2 RSA密钥交换模式)
双向
TLS(mTLS)的例外- 默认
HTTPS只验证服务器身份(单向) - mTLS(双向认证):服务器也要求客户端出示客户端证书——此时客户端证书存在客户端(用户证书/设备证书),服务器用自己的信任库验证客户端
- 适用:企业内网、服务间调用(服务网格 mTLS)、高安全要求场景
- 默认
几个常见场景澄清
- 自签名证书:服务器证书没有
CA签名(或自己签)——客户端信任库里没有对应根证书,验证失败;除非手动把自签证书加入客户端信任库(适合内网测试,不适合公网) - 企业内网
CA:公司自建CA签服务器证书——必须把企业CA根证书分发并安装到所有客户端的信任库,否则客户端不信任(这是"CA 证书要装到客户端"的典型运维场景) - 证书过期/吊销:证书验证含有效期检查;吊销状态浏览器默认不实时执行(Chrome 用 CRLSets、Firefox 用 CRLite 替代,OCSP 装订由服务端提供)
- 自签名证书:服务器证书没有
作用总结(各司其职)
- 服务器证书(服务端) :身份凭证——声明"我是这个域名”,携带公钥供验证;配合私钥(ssl_certificate_key)证明持有
- CA 根证书(客户端) :验证依据——提供可信的验签锚点,让客户端能判断"这张服务器证书是不是由可信机构签发、有没有被篡改"
- 合起来完成 HTTPS 三大保障之一:身份验证(防冒充) ——防止中间人伪造服务器
协助记忆
- 服务器证书 = 对方的护照(服务端随身携带,证明身份
- CA 根证书 = 你手里那本**“可信发证机关名录”**(客户端持有,用来核验护照上的签发机关)。过海关时:对方递上护照(服务器证书)→ 你翻开名录找到签发机关(CA)→ 对照护照上的防伪印章(数字签名)→ 章对得上,人才放行(验证通过才开始正常通信)。如果对方拿的护照签发机关不在名录里(自签名/不受信任的 CA),直接拒绝
- 口诀:服务端拿证书、客户端存信任、握手验签名、链上见真伪
进阶思考
为什么服务端不发根证书,只发中间证书?
- 因为客户端信任库里已经有根证书——再发一遍既浪费带宽,又没必要(根证书是信任锚,如果靠服务器"自证"根证书,就失去信任意义了)。客户端用本地已有的根证书去验证中间证书和服务器证书的签名,形成"本地锚 + 链式验证"。这也解释了为什么企业自建 CA 必须预先把根证书装进客户端——客户端信你,才认你签的证书
服务器证书和私钥的关系?私钥泄露会怎样?
- 证书是"公开的身份凭证"(可以公开,任何客户端都能拿到),私钥是"持有身份的证明"(必须保密)。TLS 握手靠"持有对应私钥"来证明你就是证书的主人(CertificateVerify 签名验证)。私钥泄露 = 身份被冒用:攻击者拿到私钥后,可以用你的证书伪装成你的服务器做中间人。所以私钥要:权限收紧(chmod 600)、不随代码仓库分发、定期轮换
客户端信任库里的根证书会不会被攻击者利用?
- 信任库本身是"信任锚",一旦被污染,攻击者可以为任意域名签发"受信任"的证书。历史上有两类真实案例:
- 信任锚被攻破 —— DigiNotar 原本是受信任的根 CA,被入侵后签发了 500+ 欺诈证书(被用于伊朗 Gmail 用户的中间人攻击),随后各浏览器/OS 将其根从信任库移除、公司破产;
- 恶意根证书被安装——联想
Superfish、Dell eDellRoot事件,厂商在设备里预装带固定私钥的根证书,导致 HTTPS 可被解密。所以"随便装证书到信任库"是高风险操作——信任锚被污染,整个 HTTPS 信任体系都失效。浏览器侧的封禁机制:Chrome 用 CRLSets 紧急封禁被入侵 CA 签发的中间/叶子证书,Firefox 用 CRLite 压缩吊销数据;有问题的根证书则由信任库更新移除
- 信任库本身是"信任锚",一旦被污染,攻击者可以为任意域名签发"受信任"的证书。历史上有两类真实案例:
扩展信息
数字证书家族辨析(服务器证书 / 客户端证书 / 代码签名证书)
网上常见的"服务器证书、客户端证书"说法,容易让人以为是两种完全不同的证书——实际上它们都是同一种 X.509 数字证书,区别在于"用途"(Extended Key Usage,EKU 扩展密钥用法)和"持有/验证角色"不同。可以这样理解:证书的"结构"只有一种,靠 EKU 字段声明"这张证书是拿来干什么的"。- 先理清"数字证书"这个统称
- 数字证书(Digital Certificate)通常即 X.509 证书(“数字证书"是通用概念,X.509 是其中主流实现):一个包含公钥、主体信息(是谁/哪个域名)、有效期、CA 数字签名的数据结构
- 服务器证书、客户端证书、代码签名证书、邮件签名证书……都是数字证书的具体用途形态——结构相同(X.509),EKU 不同
- 公开信任的数字证书由同一套 PKI(公钥基础设施)支撑:证书签发、验证、吊销与信任锚的完整生命周期管理——这套体系回答的是"这张证书是谁签的、是否可信”
- 服务器证书 vs 客户端证书(不是两种类型,是两种用途)
- 服务器证书:绑定域名/IP,由服务器持有并在 TLS 握手中出示,客户端验证它——EKU 通常声明 serverAuth(服务器身份验证)。作用:证明"我是这个域名"
- 客户端证书:绑定个人/设备/服务身份,由客户端持有并在 mTLS(双向认证)中出示,服务器验证它——EKU 通常声明
clientAuth(客户端身份验证)。作用:证明"我是这个被授权的用户/设备" - 一句话:同一个"身份证模板"(X.509),一张印着"这是服务器"(serverAuth),一张印着"这是客户端"(clientAuth)—— 机制同源,但签发渠道(公共 CA vs 企业/私有 CA)和验证方向(谁验谁)相反
- 注意细节:EKU 缺失时证书用途不受限(老证书常见);现代服务器证书 EKU 常同时含 serverAuth 与 clientAuth
- 代码签名证书(Code Signing)——给软件"盖章"
- 用途:给软件/代码签名,证明"这份代码确实来自某发布者、且未被篡改"——EKU 声明
codeSigning - 典型场景:Windows 安装包/驱动、macOS 应用、移动 App(注意:移动端是平台专有签名体系,机制与桌面代码签名证书不同)
- 用户双击安装时,系统验证:签名者身份是否可信 + 代码自签名后是否被改过——防的是"木马冒充正规软件"和"安装包被篡改"
- 强约束:公开信任的代码签名证书,私钥必须生成、存储、使用于硬件加密模块(HSM/USB token),不允许存在普通磁盘上——这是 CA/B Forum 基线要求的强制项
- Windows 弹"未知发布者"不代表没签名:未签名、签名无效,或签名者声誉未建立(如标准证书的新签名者)都会触发——只有特定高级别证书才能立即建立
SmartScreen声誉 macOS不只是签名:Developer ID签名之外,分发软件还须经Apple公证(notarization) ,Gatekeeper 才会放行——“签名 + 公证"两步- 与 TLS 证书的区别(容易混淆的点):TLS 服务器证书验证的是"网站是不是真的”(防假冒网站、中间人);代码签名证书验证的是"软件是不是真的"(防假冒软件、被篡改)——一个保护"你在访问谁",一个保护"你在运行什么"
- 用途:给软件/代码签名,证明"这份代码确实来自某发布者、且未被篡改"——EKU 声明
- 其他常见证书用途(顺带认识)
- 邮件签名证书(EKU emailProtection):给邮件数字签名(S/MIME),证明发件人身份 + 邮件未被篡改
- 文档签名证书:给 PDF/文档签名(如 Adobe 文档签名)
- 时间戳证书:给签名"盖时间章",证明"在这个时间点签名有效"
- 这些和服务器证书一样,都是 X.509 数字证书 + 对应 EKU 用途的组合
- 怎么快速看一张证书的用途?
- 命令行查看证书 EKU:
1 2openssl x509 -in cert.pem -noout -text | grep -A 1 ExtendedKeyUsage # 输出如:TLS Web Server Authentication(serverAuth)/ TLS Web Client Authentication(clientAuth)... - 浏览器地址栏锁图标 → 证书信息,也能看到"增强型密钥用法"
- 命令行查看证书 EKU:
- 先理清"数字证书"这个统称
🤔 如何进行压力测试?有哪些指标?如何判断系统是否达到性能瓶颈?
压测的实战心法:选对工具 + 会读输出 + 会设计场景。理论(梯度加压、拐点、分位数)前面已讲,这里全部落到工具实操——每个工具给"最常用一条命令"、参数怎么理解、输出怎么看。
工具选型速览(先知道什么时候用哪个)
ab(Apache Bench):最轻量,单条URL快速摸底,装apache2-utils就有——适合"快速看看这接口能扛多少"wrk / wrk2:高并发压测主力,单进程多线程 +epoll,单机能打满高吞吐服务——适合"往死里压"k6:脚本化(JS)、场景丰富(爬坡/混合场景)、输出规范、可进 CI——适合"正式的压测工程"JMeter:功能最全(GUI + 复杂场景 + 断言 + 分布式),较重——适合"复杂业务流程压测"hey / bombardier / vegeta:轻量替代品(hey 是 Go 版 ab 替代)- 云压测:大流量(几十万 QPS 以上)、多地域——单机工具打不满时用
ab 实战(最轻量,快速摸底)
安装:
CentOS/RHEL yum install -y httpd-tools;Ubuntu/Debian apt install -y apache2-utils最常用一条命令:
ab -n 10000 -c 100 -k http://127.0.0.1:8080/api/user参数解读:
-n总请求数、-c并发数(一次同时发多少个)、-k启用keep-alive连接复用(默认每次新建连接,压的是短连接)、-H自定义请求头(如-H "Authorization: Bearer xxx")、-p POST数据文件(配合-T指定Content-Type,如-p body.json -T application/json)、-t按时间压(如-t 60压60秒,内部隐含-n 50000)
输出怎么看(关键几行):
Requests per second:QPSTime per request:有两行,含义正好相反,最容易看反——第一行 (mean) 是单请求平均耗时(RT均值,公式timetaken/done);第二行带across all concurrent requests是总耗时 ÷ 请求数(吞吐视角,约等于1000/QPS)。很多人把第二行的小数值当成RT,得出严重偏低的结论——记住:第一行才是单请求耗时Percentage of the requests served within a certain time (ms):底部分位数表(50%/90%/99%)——先看分位数与错误率,QPS 作参考Failed requests:错误数
局限:单
URL、不支持复杂场景、请求行始终是HTTP/1.0(-k只是加Connection: Keep-Alive头启用连接复用,并不切换协议版本)
wrk 实战(高并发主力)
安装:源码编译
make(依赖gcc+libssl-dev);或用包管理器最常用一条命令:
wrk -t8 -c400 -d30s --latency http://127.0.0.1:8080/api/user参数解读:
-t线程数(建议 ≤CPU核数)、-c连接数(总连接,均分到各线程)、-d时长(30s/2m)、--latency打印延迟分位数、-H请求头、-s Lua脚本(自定义请求/鉴权)输出怎么看:
1 2 3 4Thread Stats Avg Stdev Max +/- Stdev Latency 5.23ms 3.10ms 88.91ms 92.33% Req/Sec 12.41k 1.20k 16.00k 85.12% Requests/sec: 98615.33Requests/sec:QPSThread Stats里Latency行:Avg平均延迟、Stdev标准差、Max最大延迟(默认输出没有分位数,加--latency后底部出现50%/75%/90%/99%分位数表)Req/Sec行:每个线程的吞吐(12.41k × 8 线程 ≈ 98k,和总量对得上)Lua脚本示例(自定义请求头/POST body):1 2 3wrk.method = "POST" wrk.headers["Content-Type"] = "application/json" wrk.body = '{"page":1}'
k6实战(脚本化正式压测)安装:官方二进制 /
Docker(docker run grafana/k6)最小脚本
loadtest.js:1 2 3 4 5 6 7 8 9 10 11import http from 'k6/http'; import { check } from 'k6'; export const options = { vus: 100, // 虚拟用户数(并发) duration: '30s', // 时长 thresholds: { http_req_failed: ['rate<0.01'] } // 断言:错误率 < 1% }; export default function () { const res = http.get('http://127.0.0.1:8080/api/user'); check(res, { 'status is 200': (r) => r.status === 200 }); }执行:
k6 run loadtest.js输出怎么看(关键几行):
http_req_duration:响应时间——默认含avg、min、med、max、p(90)、p(95);需要p(99)要在options里加summaryTrendStats: ['p(99)']http_reqs:总请求数;http_req_failed:失败率;iterations:完成迭代次数
进阶:
ramping-vus做爬坡(每10s加100 VU)、多阶段场景、混合接口
JMeter实战(复杂流程)- 概念三件套:
Thread Group(并发线程数、循环次数)→Sampler(HTTP请求:URL、方法、body)→Listener(查看结果:聚合报告Aggregate Report) GUI建好测试计划后,生产用命令行跑:jmeter -n -t plan.jmx -l result.jtl -e -o report_dir-n非GUI模式、-t测试计划文件、-l原始结果日志、-e生成HTML报告、-o报告输出目录
- 聚合报告关键列:
Samples(请求数)、Average(平均耗时)、Error%(错误率)、p90/p95/p99(需配置Percentiles) - 分布式压测:一个
Master调度 + 多个Slave执行(跨机器扩容)
- 概念三件套:
实战要点:本机快速起一个测试服务
- 临时起个 HTTP 服务练手:
python3 -m http.server 8080# 静态文件服务,目录下放个文件 - 或压测一个真实接口前,先用
curl -v确认请求能通、响应格式符合预期(避免压了半天压的是 404)
- 临时起个 HTTP 服务练手:
协助记忆
- 压测 = 火锅店开业前的"试营业压力测试"。
- 先来 10 桌(低并发),看看顺不顺,再加到 50 桌、100 桌(梯度加压);
- 看翻台率(QPS):每多 10 桌翻台率还在涨,说明没到顶;加到某个数翻台率不涨反降——这就是瓶颈点(拐点)
- 客人等位时间(RT)突然从 5 分钟变 1 小时,说明店里排队了(排队积压);
- 还要找出"谁先撑不住":后厨炒不过来(CPU)、服务员跑不过来(线程池)、灶台不够(连接池)、收银机卡住(DB)——哪一环先满,哪一环就是短板
- 口诀:梯度加压找拐点,QPS/RT/错误率三看,资源打满定短板
- 压测 = 火锅店开业前的"试营业压力测试"。
进阶思考
ab和wrk压同一个接口,数字差很多,谁是对的?- 可能是连接模型差异:
ab默认不开keep-alive(-k),每次请求新建连接,压的是"短连接开销 + 业务";wrk 默认长连接复用,压的是"纯业务处理能力"。真实线上一般有 keep-alive,wrk 的数字更接近生产;但两者都要看分位数和错误率,别只比一个 QPS 数字。如果并发、时长等条件相同、差异还大,检查压测机资源是否打满
- 可能是连接模型差异:
压测工具打不满目标,怎么判断是工具的问题还是服务的问题?
- 看压测机自身资源:压测机
CPU打满、或报端口耗尽(Cannot assign requested address)——工具到顶了,换更强压测机/分布式/云压测;压测机资源充足但QPS上不去——服务侧瓶颈,继续查系统资源与锁
- 看压测机自身资源:压测机
为什么 k6 输出直接给 p(90)/p(95),ab 只有分位数表?
k6把"分位数统计"内置为默认指标(http_req_duration自带p系列),ab只在底部打印分位数表、不进入统计行——本质都提供,但k6更"工程化":还能用 thresholds 断言(如错误率 < 1% 才算通过),直接当 CI 门禁。这也解释了为什么"正式压测"推荐 k6/JMeter,ab/wrk 适合快速摸底
扩展信息
P95、P99到底是什么?(分位数/百分位数)前面很多回答反复出现P50/P95/P99——它们叫分位数(Percentile,百分位数),是统计学里描述"数据分布"的指标。一句话定义:把一组数据从小到大排序,第n百分位 = “有n%的数据小于等于它"的那个值。用在延迟上:P99=99%的请求耗时不高于这个值(也意味着约 1% 的请求比它慢)。- 常见档位及含义(以响应时间为例)
P50(中位数) :一半请求比它快、一半比它慢——“典型体验”(但典型不代表多数用户感受)P90/P95:90%/95%的请求比它快——“大多数用户体验”,一般业务看P95P99:99%的请求比它快——“尾部体验”,代表最慢的那1%用户;SLO常承诺P99(如"P99 < 500ms”)P999:99.9%的请求比它快——极端长尾,常用于高敏感场景(支付、实时系统)Max:最慢的单个请求——不稳定,受偶发抖动影响大,一般不当指标用- 档位越高越能暴露"长尾问题",但也越受单次抖动影响(P999 往往被一次 GC 停顿或网络抖动拉爆)
- 为什么不用平均值?(经典长尾例子)
100个请求:98个10ms,2个10s平均值 = (98×10ms + 2×10s) / 100 = 209.8ms ≈ 210ms—— 看起来"还行"?P99(排在第99位)=10s—— 真相:有 1% 的用户体验极差结论:平均值被极端值"平均掉"了,掩盖了最差的那批体验;分位数能还原"分布的形状"。反过来也有"平均值高但中位数(P50)低"的情况(少数请求极慢拉高平均)——注意极慢请求会同时拉高平均值和高分位数(P99/P999),低的只是中位数,所以平均值 + 分位数一起看才完整
- 怎么算的?(排序即可,不用背公式)
- 把 N 个请求的耗时从小到大排成一列,第 n 百分位 ≈ 第 ceil(n/100 × N) 个位置的值(精确实现有插值差异——如 numpy/Excel 的 R-7 插值——理解思路即可)
- 例:100 个请求,P99 = 排在第 99 位那个请求的耗时(99% 的请求 ≤ 它)
- 注意:样本量太少时 P99 没意义——只有 20 个请求,P99 就是"第 20 个"(最慢那个),和 Max 没区别
- 什么场景看什么档位
- 用户体验优化:P95/P99(用户能感知的"慢")
- SLO/告警承诺:P99(对外承诺"99% 请求快于 X")
- 容量规划/压测:P99 比平均值更能反映真实承载能力(压测时看 P99 拐点)
- 高敏感交易:P999
- 健康巡检:P50 + P99 组合(P50 看常态,P99 看异常尾巴)
- 在工具/监控里怎么看
ab:输出底部 Percentage of the requests served within a certain time 分位数表wrk:加 –latency 参数,输出底部 50%/75%/90%/99% 分位数表k6:http_req_duration 默认含 avg/min/med/max/p(90)/p(95),p(99) 需在 options 配置 summaryTrendStats: [‘p(99)’](注意:会覆盖默认列表,要写上想保留的全部项)Prometheus:histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m])) 从直方图算 P99——这是估算,精度取决于直方图 bucket 划分(bucket 越细越准);新指标可优先用原生直方图(native histograms) —— 自适应 bucket,免手动设计;另注意分位数不能跨实例直接取平均(无加法性),应先聚合直方图再算日志/APM:很多 APM 直接展示 P95/P99 曲线
- 常见坑(面试加分)
- 样本量太小:P99 ≈ Max,没有意义
- 只盯平均值:掩盖长尾
- 只盯 P99:被单次抖动(GC、网络)干扰,建议 P50 + P99 一起看
- 直方图 bucket 太粗:Prometheus 估算偏差大
- 分位数不可跨实例求平均:应聚合直方图后统一计算
- 不同工具的分位数算法有差异(插值方式),跨工具对比时注意口径
- 常见档位及含义(以响应时间为例)