服务端面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 05月27日 22:35

GraphQL Schema 设计有哪些最佳实践

核心原则GraphQL Schema 设计的关键在于:以业务领域建模、保持扁平结构、控制查询深度。面试中常围绕命名规范、分页策略、N+1 问题和 Schema 演进四个方向展开。一、命名规范类型用 PascalCase,字段用 camelCase,枚举值用 SCREAMINGSNAKECASE,这是社区共识,违反会导致代码风格混乱。输入类型建议加操作前缀,如 CreateUserInput、UpdateUserInput,避免与对象类型混淆。# 规范命名type UserProfile { firstName: String! isActive: Boolean!}input CreateUserInput { name: String! email: String!}enum PostStatus { DRAFT PUBLISHED}追问:为什么字段用 camelCase 而不是 snake_case? 因为 GraphQL 规范遵循 JavaScript 命名惯例,与前端代码风格一致,减少心智负担。二、分页设计列表字段必须分页,否则数据量大时查询会拖垮服务端。GraphQL 社区推荐 Relay 风格的游标分页:type PostConnection { edges: [PostEdge!]! pageInfo: PageInfo! totalCount: Int!}type PostEdge { node: Post! cursor: String!}type Query { posts(after: String, first: Int): PostConnection!}游标分页适合实时数据流(消息列表、动态 Feed),偏移分页适合静态列表(后台管理表格)。选错分页方式是常见踩坑点。三、解决 N+1 查询Schema 允许客户端一次查询多层关联数据,但 Resolver 逐条加载会产生 N+1 问题——查 10 篇文章的作者,执行 1+10 次 SQL。解决方案是 DataLoader:const userLoader = new DataLoader(async (ids) => { const users = await User.findAll({ where: { id: ids } }); return ids.map(id => users.find(u => u.id === id));});DataLoader 将同一次请求中的多个加载操作合并为一次批量查询,是生产环境的标配。追问:DataLoader 的批量函数中为什么要按 ids 顺序返回? 因为 DataLoader 按 id 顺序映射结果,顺序不一致会导致数据错位。四、Schema 演进策略GraphQL 的优势是无版本化演进——加字段不影响旧客户端,删字段用 @deprecated 标记:type User { id: ID! name: String! fullName: String @deprecated(reason: "Use 'name' instead")}关键原则:只增不删、弃用标记、空值可缺。新增字段设为可空,避免旧客户端查询时报错。五、错误处理模式Mutation 返回建议用 Payload 模式,而非直接返回对象或抛异常:type CreateUserPayload { user: User errors: [FieldError!]!}type FieldError { code: String! message: String! field: String}这样客户端可以在同一个响应中拿到数据和错误信息,不用靠 try-catch 处理 GraphQL Error。六、控制嵌套深度过深嵌套不仅影响性能,还增加理解成本。建议列表字段加 limit 参数限制返回数量,服务端配置查询深度上限(如最大 10 层),防止恶意查询。
服务端阅读 05月27日 22:33

GraphQL 与 REST API 的核心区别是什么

核心答案GraphQL 和 REST 的根本区别在于谁决定返回什么数据:REST 由服务端定义固定响应结构,GraphQL 由客户端按需声明字段。这一差异向下传导,影响了端点设计、版本策略、缓存机制等几乎所有技术选型。关键区别数据获取REST 需要多个端点拼装数据,容易过度获取或获取不足。GraphQL 单次请求即可精确拿到所需字段,典型场景:一个页面需要用户信息 + 订单列表,REST 要两次请求,GraphQL 一条 query 搞定。端点与版本REST 每个资源一个 URL,变更时走 v1/v2 版本控制。GraphQL 只有一个端点,通过 Schema 演进和字段废弃来避免破坏性变更,无需版本号。缓存REST 直接利用 HTTP 缓存(ETag、Cache-Control),成熟且零成本。GraphQL 因查询体在 POST 中,无法原生使用 HTTP 缓存,需要 Apollo Client 等方案在应用层实现。错误处理REST 用 HTTP 状态码(404、500)表达语义。GraphQL 无论成功失败都返回 200,错误信息放在响应体的 errors 字段中,客户端必须自行解析。实时数据REST 依赖轮询或 WebSocket 补丁方案。GraphQL 原生支持 Subscription,基于 WebSocket 实现服务端推送。选型建议选 GraphQL:多端异构客户端、复杂嵌套查询、移动端省流量、快速迭代频繁变更字段选 REST:简单 CRUD、公共 API 需要易用性、团队不熟悉 GraphQL、强依赖 HTTP 缓存追问方向GraphQL 的 N+1 查询问题怎么解决?(DataLoader 批量加载)为什么说 GraphQL 不适合做文件上传?(二进制传输需 multipart,query 本身是 JSON,需额外规范)两者能混用吗?(可以,REST 做健康检查/文件上传,GraphQL 做业务查询)
服务端阅读 05月27日 22:28

Kafka Consumer Group Rebalance 什么时候触发?四种分配协议有什么区别?

核心答案Consumer Group Rebalance 是 Kafka 在组成员或订阅变化时重新分配 Partition 的过程。面试抓住三点:什么触发、怎么分配、如何减影响。什么情况会触发 Rebalance?三个维度:成员变化:Consumer 加入/退出/宕机/心跳超时(超过 session.timeout.ms)订阅变化:Consumer 订阅的 Topic 发生变动分区变化:Topic 分区数增减追问:Consumer 处理慢会触发吗?会。两次 poll() 间隔超过 max.poll.interval.ms,Coordinator 认为 Consumer 已死,踢出并触发 Rebalance。Rebalance 分几步?JoinGroup:所有 Consumer 向 Group Coordinator 发请求,Coordinator 选出 Leader ConsumerSyncGroup:Leader 制定分区分配方案,发给 Coordinator,再下发给所有 Consumer开始消费:Consumer 按新方案消费只有 Leader Consumer 负责分配,其他 Consumer 被动接收方案。四种分配策略有什么区别?| 策略 | 分配方式 | 优点 | 缺点 ||------|----------|------|------|| Range(默认) | 按 Partition 序号范围连续分配 | 简单 | 不整除时不均匀 || RoundRobin | 轮询分配所有 Partition | 多 Topic 时更均匀 | 订阅不一致时可能不均 || Sticky | 均匀分配 + 尽量保持上次分配 | 减少 Partition 迁移 | — || CooperativeSticky | 增量式,只重分配受影响 Partition | 不停消费,减少 STW | Kafka 2.4+ 支持 |关键区分:Range 和 RoundRobin 属 Eager 协议——Rebalance 时所有 Consumer 先停消费再重新分配。CooperativeSticky 属 Cooperative 协议,只迁移需要变动的 Partition,其余不受影响。怎么减少 Rebalance 的负面影响?1. 调参数避免误判max.poll.interval.ms=600000session.timeout.ms=30000heartbeat.interval.ms=100002. 用 CooperativeSticky 策略避免全量 STW,只重分配受影响分区。3. 手动管理 Offsetenable.auto.commit=falseconsumer.commitSync();自动提交在 Rebalance 期间可能丢数据或重复消费,手动提交更可控。4. 静态成员(Static Membership)设置 group.instance.id,Consumer 重启后 Coordinator 仍认为同一成员,不触发 Rebalance。追问方向Eager 协议下 Rebalance 期间能消费吗?不能。Cooperative 协议下未受影响分区可以怎么监控?关注 RebalanceRatePerSec 和 RebalanceTotal 指标Kafka 4.0 消费者组协议变化?原生支持增量 Rebalance,不再需要 Eager 回退
服务端阅读 05月27日 22:26

GraphQL 性能优化有哪些策略?

核心答案GraphQL 性能优化的关键在于六个方向:解决 N+1 查询、限制查询复杂度、缓存策略、查询持久化、分页优化、监控分析。其中 N+1 问题是面试最高频考点。N+1 查询问题与 DataLoader查询嵌套关系时,每个父对象都触发一次子查询,10 篇文章就产生 10 次 author 查询。DataLoader 通过批量 + 缓存解决:const DataLoader = require('dataloader');const userLoader = new DataLoader(async (ids) => { const users = await User.findAll({ where: { id: ids } }); return ids.map(id => users.find(u => u.id === id));});// Resolver 中Post: { author: (post) => userLoader.load(post.authorId)}DataLoader 在单次 tick 内收集所有 load 调用,合并为一次批量查询,相同 id 自动去重。追问:DataLoader 的缓存策略在什么场景下会失效? 当跨请求复用同一 DataLoader 实例时,缓存会返回脏数据。正确做法是每个请求创建新实例。查询深度与复杂度限制恶意查询可以无限嵌套,必须设防:const depthLimit = require('graphql-depth-limit');const { createComplexityLimitRule } = require('graphql-validation-complexity');const server = new ApolloServer({ typeDefs, resolvers, validationRules: [ depthLimit(7), createComplexityLimitRule(1000) ]});深度限制防嵌套炸弹,复杂度限制防字段爆炸。两者缺一不可。缓存策略三层缓存各有分工:字段级 Redis 缓存:对 Resolver 结果按 fieldName:args 做 TTL 缓存,适合读多写少的数据Apollo Client 缓存:客户端用 InMemoryCache 做 normalized 缓存,按 __typename:id 去重CDN 缓存:配合持久化查询(见下文),GET 请求可直接走 CDN查询持久化客户端将查询文本算 hash,服务端只传 hash 执行:const { createPersistedQueryLink } = require('@apollo/client/link/persisted-queries');const { sha256 } = require('crypto-hash');const link = createPersistedQueryLink({ sha256, useGETForHashedQueries: true });好处:请求体从几 KB 缩到几十字节,可走 GET + CDN 缓存,还降低了查询被篡改的风险。游标分页offset 分页在数据变更时会出现重复或遗漏,游标分页用有序字段(如 id、createdAt)做游标,稳定性不受数据量影响:type Query { posts(after: String, first: Int): PostConnection!}监控与分析上线前接 Apollo Studio 或自建 metrics,记录每个 Resolver 的耗时和错误率。性能问题不是"有没有"的问题,而是"在哪"的问题,没有监控就是盲调。追问:如果 Resolver 耗时突然翻倍,你会从哪些方向排查? 先看数据库慢查询日志,再检查 DataLoader 是否失效(缓存击穿),最后确认是否有新增的深嵌套查询绕过了复杂度限制。
服务端阅读 05月27日 22:23

Kafka 和 RabbitMQ、RocketMQ 怎么选?

核心区别一图看懂| 维度 | Kafka | RabbitMQ | RocketMQ ||------|-------|----------|----------|| 定位 | 分布式流处理平台 | 传统消息代理 | 分布式消息中间件 || 吞吐量 | 百万级 TPS | 万级 TPS | 十万级 TPS || 延迟 | 毫秒级 | 微秒级 | 毫秒级 || 消息留存 | 按时间/容量保留,消费后不删 | 消费确认后删除 | 可配置保留策略 || 消费模型 | Pull 拉取 | Push 推送为主 | Push + Pull 均支持 || 路由能力 | 基于 Topic 和分区 | Exchange 多种路由模式 | Topic + Tag 两级过滤 || 顺序保证 | 分区内有序 | 队列内有序 | 全局顺序消息 || 事务消息 | 支持(0.11+) | 不支持 | 原生支持,最完善 || 适用场景 | 日志流、事件流、大数据管道 | 任务队列、微服务通信、复杂路由 | 电商订单、金融交易、事务消息 |为什么 Kafka 吞吐量远超另外两个?Kafka 的核心设计围绕"顺序写磁盘 + 零拷贝 + 分区并行"三个点。磁盘顺序写速度可达 600MB/s,远超随机写的 100KB/s。零拷贝(sendfile 系统调用)让数据直接从页缓存到网卡,跳过用户态拷贝。分区机制则将负载分散到多个 Broker 并行处理。RabbitMQ 的优势不在于吞吐,而在于路由灵活性——四种 Exchange 类型(direct、topic、fanout、headers)能实现复杂的消息分发逻辑,延迟也在微秒级别,适合对实时性要求高但吞吐不大的场景。RocketMQ 在事务消息上做得最完善:半消息 + 本地事务 + 回查机制,保证分布式事务的最终一致性,这是 Kafka 和 RabbitMQ都不具备的。选型怎么决策?三个问题就能定:消息量大吗? 日均亿级以上选 Kafka,百万级以下 RabbitMQ 够用需要消息回溯吗? Kafka 天然支持,RabbitMQ 消费完就删涉及钱吗? 金融、订单场景选 RocketMQ,事务消息是刚需很多团队的做法是 Kafka 做事件流 + RabbitMQ 做任务队列,各取所长。面试追问方向Kafka 为什么用 Pull 不用 Push? Push 模式下消费者处理能力不一,慢消费者会拖垮 Broker;Pull 让消费者按自己节奏消费,还方便回溯和批量拉取RocketMQ 的 NameServer 和 Kafka 的 ZooKeeper 有什么区别? NameServer 无状态、无主从,部署简单但功能弱于 ZK;Kafka 新版 KRaft 模式已去 ZK 依赖消息积压怎么处理? Kafka 扩分区 + 增 Consumer;RabbitMQ 临时加消费者队列;RocketMQ 调大消费线程池
服务端阅读 05月27日 22:18

GraphQL 生态中有哪些必须掌握的工具?

核心工具分类GraphQL 生态工具按职责分四层:服务端框架、客户端库、开发辅助、治理安全。面试中常考的是每层选型依据和它们之间的配合方式。服务端框架Apollo Server 是目前使用最广的 GraphQL 服务端,开箱支持 Federation、订阅和插件体系。GraphQL Yoga 更轻量,适配边缘运行时(Bun、Deno),适合对包体积敏感的场景。// Apollo Server 最小启动const { ApolloServer } = require("@apollo/server");const server = new ApolloServer({ typeDefs: `type Query { hello: String! }`, resolvers: { Query: { hello: () => "world" } }});选型关键:需要多团队联邦架构选 Apollo Server;追求轻量和多运行时部署选 Yoga。客户端库三足鼎立:Apollo Client(生态最全,缓存最强)、Relay(Meta 出品,编译时优化,适合超大规模应用)、urql(7KB,交换器架构,适合中小项目)。// urql 最小配置import { createClient, cacheExchange, fetchExchange } from "urql";const client = createClient({ url: "/graphql", exchanges: [cacheExchange, fetchExchange]});面试追问方向:Apollo Client 的 normalized cache 如何工作?Relay 的 fragment 机制为什么能减少过度请求?开发辅助GraphQL Code Generator 从 Schema 自动生成 TypeScript 类型和 React Hooks,是类型安全的核心工具。Apollo Sandbox / GraphiQL 提供交互式查询调试。DataLoader 批量合并请求,解决经典的 N+1 查询问题。// DataLoader 解决 N+1const DataLoader = require("dataloader");const userLoader = new DataLoader(async ids => { const users = await User.findByIds(ids); return ids.map(id => users.find(u => u.id === id));});治理与安全GraphQL Shield 用声明式规则做字段级权限控制,Envelop 提供可插拔插件做限流和查询深度限制。Apollo Studio 负责全链路监控、Schema 变更追踪和性能分析。// GraphQL Shield 权限规则const { shield, rule } = require("graphql-shield");const isAdmin = rule()((_, __, ctx) => ctx.user?.role === "ADMIN");const permissions = shield({ Query: { users: isAdmin }, Mutation: { deleteUser: isAdmin }});选型速查| 场景 | 推荐工具 ||------|----------|| 服务端搭建 | Apollo Server / Yoga || 客户端状态管理 | Apollo Client || 超大规模前端 | Relay || 类型生成 | Code Generator || N+1 优化 | DataLoader || 权限控制 | GraphQL Shield || 全链路监控 | Apollo Studio |实际项目中,服务端 Apollo Server + Code Generator + DataLoader、客户端 Apollo Client 或 urql 是最常见的组合。工具选型没有绝对标准,关键是理解每个工具解决什么问题,再按团队规模和技术栈做取舍。
服务端阅读 05月27日 22:18

GraphQL 客户端开发需要掌握哪些核心知识?

GraphQL 客户端开发需要掌握哪些核心知识?GraphQL 客户端的核心职责是把查询、缓存、状态管理三件事做好。Apollo Client 是目前最主流的选择,面试中围绕它的问题也最多。Apollo Client 的缓存机制是怎样的?Apollo Client 使用规范化缓存(Normalized Cache),把每个对象按 __typename:id 拆开存储,而不是按查询维度缓存整棵树。这意味着同一用户在不同查询中返回时,缓存只存一份,修改一处全局生效。InMemoryCache 的 typePolicies 可以自定义合并策略。分页场景下用 merge 函数拼接新旧数据,用 keyArgs 声明哪些参数影响缓存键。追问:cache-and-network 和 network-only 有什么区别?cache-and-network 先返回缓存数据再发请求更新,适合需要即时反馈的列表页;network-only 跳过缓存直接请求,适合对数据新鲜度要求高的场景。useQuery 的 fetchPolicy 怎么选?常用策略按优先级排列:cache-first(默认):有缓存就用,没有才请求。适合读多写少的详情页cache-and-network:缓存和请求并行,先展示旧数据再更新。适合列表页network-only:每次都请求,适合订单状态等实时数据cache-only:只用缓存,离线场景或纯本地数据时使用选错策略是缓存不更新的头号原因。比如详情页用了 cache-first,编辑后返回列表,数据没变——因为缓存没失效。如何处理 Mutation 后的缓存更新?三种方式,按推荐程度排序:cache.modify + writeFragment:手动更新缓存中受影响的字段,精确且高效refetchQueries:Mutation 成功后重新查询指定 Query,简单但多一次网络请求乐观更新(Optimistic Response):先假设成功更新 UI,服务端返回后再修正,体验最好但实现复杂实际项目中推荐方式 1 为主,关键操作加乐观更新。避免滥用 refetchQueries,它会让请求量翻倍。分页加载怎么实现?偏移分页用 fetchMore 传入新的 offset,在 updateQuery 中拼接结果。游标分页用 after 游标,配合 pageInfo.hasNextPage 判断是否还有数据。游标分页更适合实时性强的场景(聊天记录、动态流),因为偏移分页在数据插入后会导致重复或遗漏。但游标分页不支持跳页,只能顺序加载。全局错误处理怎么配置?用 onError Link 统一拦截。graphQLErrors 是业务逻辑错误(校验失败、权限不足),networkError 是网络层错误(断网、超时)。认证过期是常见场景:在 onError 中检测 401 错误码,静默刷新 Token 后用 forward(operation) 重发请求,用户无感知。Subscription 在生产环境要注意什么?WebSocket 连接不稳定是最大问题。必须实现自动重连:Apollo Client 的 split Link 按 Operation 类型分流,Query/Mutation 走 HTTP,Subscription 走 WebSocket。WebSocket 断开后用指数退避重连,避免服务器压力过大。还要注意连接鉴权——WebSocket 建连时通过 connectionParams 传递 Token,服务端在 onConnect 中验证。Apollo Link 链式调用原理是什么?Apollo Link 采用中间件模式,每个 Link 处理请求后传给下一个。常用链路:authLink → errorLink → httpLink。setContext 注入请求头,onError 捕获错误,HttpLink 发出请求。顺序很重要——errorLink 放在 httpLink 前面才能拦截到网络错误。掌握缓存策略选择、Mutation 缓存更新、分页实现、错误处理这四块,基本覆盖了 GraphQL 客户端面试的核心考察点。理解规范缓存的存储模型是串联所有知识点的基础。
服务端阅读 05月27日 22:13

Nginx 如何进行安全配置?有哪些安全最佳实践?

Nginx 如何进行安全配置?有哪些安全最佳实践?核心思路是:隐藏信息、限制访问、加密传输、防御攻击,四层递进。一、隐藏服务器指纹攻击者第一步是信息收集,版本号是最直接的突破口。server_tokens off;关掉之后,响应头和错误页不再暴露 Nginx 版本。面试追问:还能怎么隐藏? 可以用 more_set_headers 模块改掉 Server 字段本身,或者在前端反代层抹掉这个头。二、访问控制与限流IP 限制和速率限制是防暴力攻击的第一道门。# IP 白名单location /admin { allow 192.168.1.0/24; deny all;}# 限流limit_req_zone $binary_remote_addr zone=req:10m rate=10r/s;limit_req zone=req burst=20 nodelay;limit_conn_zone $binary_remote_addr zone=conn:10m;limit_conn conn 10;面试追问:burst 和 nodelay 区别是什么? burst 允许突发排队,nodelay 让排队的请求立即处理而不延时等待,否则超出的请求会被延迟处理。三、SSL/TLS 与安全头HTTPS 是底线配置,安全头加固浏览器端防护。ssl_protocols TLSv1.2 TLSv1.3;ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;ssl_prefer_server_ciphers on;ssl_session_cache shared:SSL:50m;ssl_session_tickets off;add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;add_header X-Frame-Options "SAMEORIGIN" always;add_header X-Content-Type-Options "nosniff" always;add_header Content-Security-Policy "default-src 'self'" always;面试追问:为什么关 sslsessiontickets? 默认的 session ticket key 是明文保存在 worker 共享内存中的,多台 Nginx 之间无法复用,且存在前向安全性问题。集群部署时应手动轮换 key 或直接关闭。四、文件与目录防护禁止访问隐藏文件和敏感后缀,堵住信息泄露的口子。location ~ /\. { deny all; }location ~* \.(htaccess|ini|log|sql|bak|swp)$ { deny all; }autoindex off;五、超时与缓冲区防止慢速攻击和缓冲区溢出。client_body_timeout 10;client_header_timeout 10;keepalive_timeout 5;send_timeout 10;client_max_body_size 10m;client_header_buffer_size 1k;面试追问:如果只改一个配置,改哪个? 先关 server_tokens,零成本高风险。然后上 HTTPS,这是生产环境硬性要求。限流和访问控制按业务场景逐步加。常见误区用 if 正则匹配 SQL 注入或 XSS 关键词来拦截攻击,这是错误的。Nginx 的 if 在 location 中行为不稳定,且正则容易被编码绕过。防注入应交给 WAF(如 ModSecurity)或应用层参数校验,Nginx 只做流量层防护。
服务端阅读 05月27日 22:13

Nginx 性能调优需要关注哪些关键参数?

Nginx 性能调优需要关注哪些关键参数?Nginx 调优的核心思路是:减少不必要的系统调用、充分利用内核零拷贝能力、压缩传输体积、避免连接浪费。下面按影响程度从大到小逐项说明。Worker 进程与连接worker_processes 设为 auto,让 Nginx 自动匹配 CPU 核心数。worker_rlimit_nofile 调到 100000,避免文件描述符耗尽。每个 worker 的 worker_connections 可设 65535,理论最大并发 = worker 数 × 65535。multi_accept on 让 worker 一次性接收所有就绪连接,accept_mutex off 在高并发下减少锁争用。worker_processes auto;worker_rlimit_nofile 100000;events { worker_connections 65535; use epoll; multi_accept on; accept_mutex off;}零拷贝与传输优化sendfile on 跳过用户态拷贝,数据从内核直接到 socket。tcp_nopush on 让数据攒满一个包再发,配合 sendfile 减少系统调用次数。tcp_nodelay on 禁用 Nagle 算法,避免小包延迟。三者同时开启并不矛盾:tcpnopush 在 sendfile 阶段生效,最后一个包由 tcpnodelay 立即发出。sendfile on;tcp_nopush on;tcp_nodelay on;Gzip 压缩压缩能将文本响应体积缩减 60%-80%,直接降低带宽和传输耗时。gzip_comp_level 建议设 4-6,再高收益递减但 CPU 开销上升。gzip_min_length 设 1024,避免压缩小响应反而变大。别忘了通过 gzip_types 覆盖 JSON、SVG 等常见 MIME 类型。gzip on;gzip_vary on;gzip_min_length 1024;gzip_comp_level 5;gzip_types text/plain text/css application/json application/javascript application/xml image/svg+xml;连接复用与超时keepalive_timeout 控制客户端长连接保持时间,默认 75s,可视业务缩短到 30-60s。keepalive_requests 限制单连接最大请求数,防止连接泄漏。对上游服务器也要开长连接:upstream 块中 keepalive 32 保持 32 条空闲连接,减少反复握手开销。keepalive_timeout 60s;keepalive_requests 1000;upstream backend { server 10.0.0.1:8080; keepalive 32;}缓冲区与文件缓存client_body_buffer_size 和 client_header_buffer_size 按业务调整,过小会触发临时文件写入拖慢请求。open_file_cache 缓存频繁访问的文件描述符,减少磁盘 stat 调用,对静态资源场景效果显著。client_header_buffer_size 2k;client_body_buffer_size 128k;open_file_cache max=10000 inactive=20s;open_file_cache_valid 30s;open_file_cache_min_uses 2;日志降耗写日志是高并发下的隐性瓶颈。静态资源 access_log off 直接关掉;动态请求的日志加 buffer=32k flush=5s,写操作先进内存再批量落盘。追问:调了参数怎么验证效果?用 wrk 或 ab 做基准测试,对比调优前后的 QPS、P99 延迟和错误率。生产环境通过 stub_status 模块持续监控活跃连接数和请求处理量。记住一个原则:每次只改一个参数,观察效果后再改下一个,否则无法定位哪个改动真正有效。
服务端阅读 05月27日 22:13

Nginx 日志怎么配置?有哪些格式和优化方法?

Nginx 如何配置日志?有哪些日志格式和优化方法?Nginx 提供了访问日志(accesslog)和错误日志(errorlog)两种日志类型。面试中常考的是日志格式自定义、条件记录和性能优化三条主线。访问日志与自定义格式访问日志通过 log_format 定义格式,再用 access_log 引用。常用三种格式:main 格式:记录 IP、请求行、状态码、Referer、UA、耗时等基础信息detailed 格式:在 main 基础上增加 upstream 地址、状态、X-Forwarded-For、request_idJSON 格式:用 escape=json 转义,方便 ELK/Grafana Loki 等工具直接解析log_format main "$remote_addr - $remote_user [$time_local] " ""$request" $status $body_bytes_sent " ""$http_referer" "$http_user_agent" " "$request_time $upstream_response_time";log_format json_combined escape=json "{" ""time_local":"$time_local", ""remote_addr":"$remote_addr", ""request":"$request", ""status":"$status", ""request_time":"$request_time"" "}";access_log /var/log/nginx/access.log main;追问:accesslog 和 errorlog 有什么区别? accesslog 记录每次请求的详细信息,可自定义格式;errorlog 记录服务器错误,不支持自定义格式,只能设级别(debug/info/notice/warn/error/crit/alert/emerg)。日志性能优化高并发场景下日志写入会成为瓶颈,核心优化手段:关闭不必要的日志:静态资源、健康检查路径用 access_log off缓冲写入:access_log ... buffer=32k flush=5s,减少磁盘 I/O 次数gzip 压缩:access_log ... gzip=9,节省磁盘空间条件记录:用 map + if= 只记录特定状态码或慢请求map $status $loggable { ~^[23] 0; default 1;}access_log /var/log/nginx/access.log main if=$loggable;location ~* \.(css|js|jpg|png|gif|ico)$ { access_log off;}access_log /var/log/nginx/access.log main buffer=32k flush=5s;追问:buffer 和 flush 参数分别控制什么? buffer 控制缓冲区大小,写满才刷盘;flush 控制最大等待时间,超时强制刷盘。两者配合保证日志既不丢又不多写。日志轮转与分离单文件日志会无限增长,必须配合 logrotate 轮转:# /etc/logrotate.d/nginx/var/log/nginx/*.log { daily rotate 14 compress delaycompress missingok sharedscripts postrotate [ -f /var/run/nginx.pid ] && kill -USR1 $(cat /var/run/nginx.pid) endscript}kill -USR1 让 Nginx 重新打开日志文件,不会中断服务。按业务分离日志同样重要:API 请求写独立文件,不同 server 块各写各的,便于精准排查问题。常用变量速查| 变量 | 说明 ||------|------|| $remoteaddr | 客户端 IP || $status | 响应状态码 || $requesttime | 请求总耗时 || $upstreamresponsetime | 上游响应时间 || $httpxforwarded_for | 真实客户端 IP |生产环境建议用 JSON 格式 + 缓冲写入 + logrotate 轮转 + 条件过滤,四板斧组合基本够用。
服务端阅读 05月27日 22:12

Nginx 监控运维怎么做?stub_status、Prometheus、ELK 怎么选?

答案前置:Nginx 监控运维的核心思路Nginx 监控围绕三条线展开:指标采集(stub_status / 日志)→ 存储与展示(Prometheus+Grafana / ELK / Zabbix)→ 告警与响应(阈值告警 + 自动化脚本)。面试中常考的不是你背了多少工具名,而是能不能说清楚每一步为什么这么做、不同规模场景怎么选型。内置指标:stub_status 能拿到什么?stub_status 是 Nginx 自带的状态模块,开启后在指定路径暴露一组关键指标:location /nginx_status { stub_status on; access_log off; allow 127.0.0.1; deny all;}访问后返回:Active connections(当前活跃连接数)、accepts/handled/requests(累计连接与请求数)、Reading/Writing/Waiting(读请求头、写响应、空闲等待的连接数)。追问:Waiting 数量大说明什么? 如果 Active connections ≈ Waiting,说明大量连接是 keep-alive 空闲态,连接复用率高是好事;但如果同时 Writing 偏低而请求排队,就要检查上游响应是否过慢。日志监控:比 stub_status 更细的粒度stubstatus 只有粗粒度指标,真正定位问题靠日志。关键是自定义 logformat 加入上游响应时间:log_format detailed '$remote_addr - [$time_local] "$request" $status ' 'rt=$request_time uct=$upstream_connect_time ' 'uht=$upstream_header_time urt=$upstream_response_time';access_log /var/log/nginx/detailed.log detailed;request_time 是总耗时,upstream_response_time 是上游处理耗时,两者差值就是 Nginx 自身开销。如果差值大,排查 Nginx 层面的缓冲、压缩或 DNS 解析。对于 ELK 场景,直接输出 JSON 格式日志省掉 Logstash 的 grok 解析:log_format json_log escape=json '{"time":"$time_local","ip":"$remote_addr",' '"status":$status,"rt":$request_time,"urt":"$upstream_response_time"}';access_log /var/log/nginx/json.log json_log;Prometheus + Grafana:云原生场景首选nginx-prometheus-exporter 把 stub_status 的指标转为 Prometheus 格式,Grafana 做可视化:# prometheus.ymlscrape_configs: - job_name: 'nginx' static_configs: - targets: ['localhost:9113']选型逻辑: 容器化/K8s 环境下 Prometheus 是事实标准,开箱即用 Service Discovery,Grafana 社区模板丰富。Nginx Plus 用户还可以用官方 nginx-plus-exporter 拿到更细的 upstream 指标。ELK Stack:日志深度分析场景当需求不只是看指标曲线,而是要按 IP、URL、状态码做聚合分析和历史追溯,ELK 更合适。Filebeat 采集 → Logstash 清洗 → Elasticsearch 存储 → Kibana 可视化,链路长但灵活度高。选型对比: Prometheus 适合指标型监控(数字曲线),ELK 适合日志型分析(文本检索+聚合)。小团队二选一推荐 Prometheus,监控告警闭环更短。Zabbix:传统企业环境的选择Zabbix 通过 Agent 调用 stub_status 页面,用正则提取指标配置监控项。适合已有 Zabbix 基建的企业,不推荐新项目为 Nginx 单独搭 Zabbix。告警:监控闭环的关键有监控没告警等于没监控。核心告警规则:5xx 比率超阈值(如 > 1%)触发 criticalActive connections 突增超过 2 倍标准差触发 warningupstreamresponsetime P99 > 2s 触发 warningPrometheus 用 Alertmanager 配路由和静默,ELK 用 Watcher 或 ElastAlert,Zabbix 自带触发器机制。运维常用命令速查nginx -t # 测试配置语法nginx -s reload # 平滑重载,不中断连接nginx -s quit # 优雅停止,处理完当前请求后退出nginx -s reopen # 重新打开日志文件(配合 logrotate)日志轮转用 logrotate,配置 postrotate 里发 USR1 信号让 Nginx 重新打开文件句柄,避免写入已轮转的老文件。面试追问方向stub_status 的 Waiting 和 keep-alive 是什么关系? Waiting 连接就是 keep-alive 空闲连接,keepalive_timeout 控制超时回收。Nginx 502 怎么排查? 先查 upstream 是否存活,再看 Nginx error log 里 connect() failed 的具体原因,最后检查 proxy_read_timeout 配置。如何不重启更新配置? nginx -t && nginx -s reload,reload 会 fork 新 worker 加载新配置,老 worker 处理完手头请求后退出。Prometheus 和 ELK 怎么选? 指标监控选 Prometheus(轻量、告警闭环好),日志分析选 ELK(全文检索、聚合灵活),大团队通常两套都搭。
服务端阅读 05月27日 22:12

Nginx 如何配置 WebSocket 代理?

Nginx 如何配置 WebSocket 代理?WebSocket 建立在 HTTP/1.1 之上,通过 Upgrade 机制将 HTTP 连接升级为全双工长连接。Nginx 默认会清除 Upgrade 头,所以必须手动配置才能正确代理 WebSocket。核心配置(三行必写)location /ws { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";}proxy_http_version 1.1:WebSocket 要求 HTTP/1.1,Nginx 默认用 1.0Upgrade $http_upgrade:转发客户端的协议升级请求Connection "upgrade":告知 Nginx 这不是普通 HTTP,需要保持升级状态推荐用 map 管理 Connection 头多 location 场景下,硬编码 "upgrade" 会导致非 WebSocket 请求也被标记。用 map 按需切换更安全:map $http_upgrade $connection_upgrade { default upgrade; '' close;}server { location /ws { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; }}没有 Upgrade 头时走 close,普通请求不受影响。超时:为什么连着连着就断了?Nginx 的 proxy_read_timeout 默认 60 秒。WebSocket 是长连接,如果 60 秒内没有数据传输,Nginx 会主动断开。解决办法:proxy_read_timeout 3600s; # 1小时proxy_send_timeout 3600s;proxy_connect_timeout 60s; # 建连超时保持短即可按业务调,不是越大越好。过长超时意味着僵死连接不会被回收。WSS(WebSocket over TLS)在 SSL server 块里照加那三行即可,Nginx 负责 TLS 卸载,后端仍用 ws://:server { listen 443 ssl; ssl_certificate /etc/nginx/ssl/cert.pem; ssl_certificate_key /etc/nginx/ssl/key.pem; location /ws { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; }}负载均衡要注意会话保持WebSocket 是有状态长连接,轮询策略会导致重连时切到不同后端。必须用 ip_hash:upstream ws_backend { ip_hash; server 10.0.0.1:8080; server 10.0.0.2:8080;}如果后端是无状态的(如用 Redis Pub/Sub 做消息同步),也可以用轮询。追问:连接断开怎么排查?查 Nginx error log,确认是否超时断开检查 Upgrade / Connection 头是否正确转发确认 proxy_buffering off 已设置,避免数据被缓冲检查防火墙或 CDN 是否拦截长连接用 curl -H "Upgrade: websocket" 手动测试握手是否返回 101
服务端阅读 05月27日 22:11

Nginx 反向代理怎么配置?

Nginx 反向代理怎么配置?反向代理是 Nginx 最核心的用途之一:客户端请求先到 Nginx,再由 Nginx 转发给后端服务器,客户端感知不到真实后端的存在。和正向代理(代理客户端出国)相反,反向代理代理的是服务端。最小可用配置server { listen 80; server_name example.com; location / { proxy_pass http://192.168.1.100:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }}proxy_pass 指定后端地址,三条 proxy_set_header 是标配——不带这些头,后端拿到的全是 Nginx 的 IP,日志和鉴权都会出问题。多后端负载均衡upstream backend { server 192.168.1.100:8080 weight=3; server 192.168.1.101:8080 weight=1; server 192.168.1.102:8080 backup;}server { listen 80; location / { proxy_pass http://backend; }}四种策略选哪个?轮询(默认)无状态通用;ip_hash 保会话粘性但分布不均;least_conn 适合长连接;加权轮询按机器性能分配。生产环境常用加权轮询 + 健康检查。WebSocket 代理location /ws/ { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s;}不加 Upgrade 头,WebSocket 握手直接失败;proxy_read_timeout 不加长,空闲连接会被 Nginx 主动断开,这是最常见的踩坑点。面试追问:反向代理和正向代理的区别?反向代理代理服务端,客户端不知道真实后端是谁;正向代理代理客户端,服务端不知道真实客户端是谁。一句话:正向代理帮客户端藏身份,反向代理帮服务端藏身份。生产环境别漏这些超时三件套:proxy_connect_timeout、proxy_send_timeout、proxy_read_timeout,默认 60s,慢接口要调大缓冲默认开着,大文件上传场景注意 proxy_buffer_size 和 proxy_buffers 调大HTTPS 场景在 Nginx 做 SSL 终止,后端走内网 HTTP,证书只管 Nginx 一层proxy_redirect off 防止后端 302 重定向把内网地址暴露给客户端
服务端阅读 05月27日 22:10

Kafka 事务消息的核心机制是什么?

Kafka 事务消息的核心机制是什么?Kafka 事务消息从 0.11 版本引入,解决的是跨分区、跨会话的原子写入问题。它不是 RocketMQ 那种"半消息+本地事务回查"的模式,而是围绕 Transaction Coordinator + 两阶段提交 实现的 Exactly-Once 语义。核心三要素:幂等 Producer(PID + Sequence Number 去重)、Transaction Coordinator(事务协调器)、__transaction_state 内部 Topic(持久化事务日志)。事务消息的完整流程是怎样的?1. 查找 CoordinatorProducer 发送 FindCoordinatorRequest,根据 transactional.id 哈希定位到某个 Broker 作为该事务的 Coordinator。2. 初始化 PIDProducer 向 Coordinator 发送 InitPidRequest。Coordinator 分配 PID 和 epoch,并将 transactional.id → PID 映射写入 __transaction_state。这一步保证新 Producer 启动后,旧的同 ID Producer 被 fence 掉(Zombie Fencing)。3. 开始事务beginTransaction() 是 Producer 本地操作,仅更新内部状态,不与 Broker 交互。4. 注册分区Producer 第一次向某个 TopicPartition 发消息前,先发 AddPartitionsToTxnRequest 给 Coordinator,注册该分区到当前事务。Coordinator 将分区信息写入事务日志。5. 发送消息Producer 正常发送 ProduceRequest,消息携带 PID、epoch、sequence number。Broker 写入日志但消息对 read_committed 消费者不可见。6. 提交消费偏移(Consume-Transform-Produce 模式)Producer 发 AddOffsetsToTxnRequest 将消费偏移纳入事务,再发 TxnOffsetCommitRequest 写入 __consumer_offsets。保证消费位移和生产消息的原子性。7. 提交或中止事务Commit:Producer 发 EndTxnRequest(commit),Coordinator 先写入 PREPARECOMMIT 到事务日志,再向各分区 Leader 发 WriteTxnMarker 写入 Control Batch(commit marker),最后写入 COMPLETECOMMIT。Abort:流程对称,写 abort marker,消费者丢弃对应消息。Consumer 如何处理事务消息?isolation.level=read_committed:Broker 端过滤,只返回已 commit 事务的消息。Consumer 还会收到 abort 标记,丢弃回滚消息。isolation.level=read_uncommitted:所有消息立即可见,包括未提交的。性能更好但可能读到脏数据。关键配置有哪些?Producer 必须配置:enable.idempotence=truetransactional.id=my-txn-idacks=allConsumer 读事务消息需配置:isolation.level=read_committedBroker 端关注 transaction.state.log.replication.factor 和 transactional.id.expiration.ms(默认 7 天过期)。事务消息的典型代码怎么写?Properties props = new Properties();props.put("bootstrap.servers", "localhost:9092");props.put("transactional.id", "order-txn-1");props.put("enable.idempotence", "true");KafkaProducer<String, String> producer = new KafkaProducer<>(props);producer.initTransactions();try { producer.beginTransaction(); producer.send(new ProducerRecord<>("orders", "key1", "value1")); producer.send(new ProducerRecord<>("inventory", "key2", "value2")); producer.commitTransaction();} catch (ProducerFencedException e) { producer.close(); // 被 fence,不可恢复} catch (KafkaException e) { producer.abortTransaction();}面试追问:事务消息有什么局限?Consumer 侧保证弱:read_committed 只能过滤未提交消息,无法阻止 Consumer 重复处理已提交消息,消费端幂等需自行保证。事务超时问题:transaction.timeout.ms 默认 60s,长事务易超时被 Coordinator 主动 abort。跨系统不原子:Kafka 事务只覆盖 Kafka 内部的生产和消费位移提交,与外部数据库的联动需自行实现补偿机制,无法做到真正的跨系统两阶段提交。
服务端阅读 05月27日 22:10

Kafka 消息丢失的原因有哪些?怎么解决?

答案概览Kafka 消息丢失发生在三个环节:Producer 发送端、Broker 存储端、Consumer 消费端。核心对策:Producer 配 acks=all + 重试,Broker 配多副本 + 禁脏选举,Consumer 关自动提交改手动确认。Producer 端:消息发出去就丢了?丢失原因:acks=0 或 acks=1,Leader 写入成功就返回,Follower 还没同步 Leader 就挂了异步发送不带回调,发送失败无感知retries 未配置,网络抖动直接丢消息解决方案:acks=allretries=3enable.idempotence=truemax.in.flight.requests.per.connection=5acks=all 要求所有 ISR 副本确认写入才算成功。enable.idempotence=true 开启幂等生产者,防止重试导致消息重复。注意:开启幂等性时 max.in.flight.requests.per.connection 需小于等于 5,否则幂等性失效。用 producer.send(record, callback) 代替 producer.send(record),在回调里处理失败逻辑。Broker 端:写进去了但读不到了?丢失原因:副本数只有 1,Broker 宕机直接丢数据Leader 崩溃后,未同步完的 Follower 被选为新 Leader(脏选举),未同步消息丢失异步刷盘,数据还在 PageCache 没落盘就宕机解决方案:default.replication.factor=3min.insync.replicas=2unclean.leader.election.enable=falsereplication.factor=3 保证三副本冗余。min.insync.replicas=2 要求至少 2 个副本在 ISR 中,否则 Producer 写入报错——宁可不可用也不丢数据。unclean.leader.election.enable=false 禁止落后副本参与选举,这是防丢的关键开关。Consumer 端:消费了但白消费了?丢失原因:enable.auto.commit=true,消息拉取后自动提交 offset,但业务还没处理完就挂了,重启后这条消息不会再投递多线程消费时,处理慢的线程还没完成,offset 已被其他线程推进解决方案:enable.auto.commit=false关闭自动提交,业务处理完成后手动调用 consumer.commitSync()。多线程场景下,每个线程维护自己的 offset,处理完再提交。消费者还需实现幂等性:同一条消息可能被重复投递(rebalance 后),用唯一标识去重。追问:acks=all 就一定不丢消息吗?不一定。如果 ISR 中只剩 Leader 自己,acks=all 退化为 acks=1。所以 min.insync.replicas=2 必须配合使用——当 ISR 不足时拒绝写入,用可用性换可靠性。配置速查| 环节 | 关键配置 | 推荐值 ||------|---------|--------|| Producer | acks | all || Producer | retries | ≥3 || Producer | enable.idempotence | true || Broker | replication.factor | 3 || Broker | min.insync.replicas | 2 || Broker | unclean.leader.election.enable | false || Consumer | enable.auto.commit | false |
服务端阅读 05月27日 22:10

Kafka 支持哪些压缩算法?生产环境怎么选?

Kafka 支持哪些压缩算法Kafka 支持 Gzip、Snappy、LZ4、Zstd 四种压缩算法,以及不压缩(none)。压缩在生产者端以 batch 为单位执行,Broker 原样存储和转发,Consumer 端解压。理解各算法的取舍是选型的关键。四种算法核心差异Gzip:压缩率最高(文本数据可达 70-80%),但 CPU 开销大、速度慢。适合带宽极度受限、对延迟不敏感的场景。Snappy:Google 出品,速度与压缩率较均衡,是 Kafka 早期版本的默认推荐。适合追求稳定、不想过度调优的常规业务。LZ4:压缩和解压速度最快,CPU 消耗极低,但压缩率一般。适合高吞吐、低延迟的实时流处理场景。Zstd:Facebook 开源,压缩率接近 Gzip,速度接近 Snappy,Kafka 2.1.0 起支持。是当前综合表现最优的选择。快速对比:| 算法 | 压缩率 | 压缩速度 | CPU 消耗 | Kafka 最低版本 ||------|--------|----------|----------|---------------|| Gzip | 最高 | 慢 | 高 | 0.8.0 || Snappy | 中等 | 快 | 低 | 0.8.0 || LZ4 | 较低 | 最快 | 极低 | 0.8.2 || Zstd | 高 | 较快 | 中等 | 2.1.0 |生产环境怎么选首选 Zstd:如果你的 Kafka 版本 >= 2.1.0,Zstd 几乎是最优解——压缩率比 Snappy 高 20-30%,速度远快于 Gzip,CPU 开销可控。高吞吐场景选 LZ4:实时计算、日志采集等对延迟敏感的场景,LZ4 的极低 CPU 开销和最快解压速度更有优势。老旧集群选 Snappy:无法升级 Kafka 版本时,Snappy 仍然是可靠的兜底方案。Gzip 适合归档:只有"带宽贵过 CPU、数据量极大、延迟无所谓"的场景才考虑 Gzip,比如离线数据同步到冷存储。配置要点# Producer 端配置compression.type=zstdbatch.size=32768linger.ms=10batch.size 和 linger.ms 直接影响压缩效果——batch 越大,同一批消息的重复模式越多,压缩率越高。但 batch 过大也会增加延迟,需要权衡。注意 compression.type=producer 是 Broker 端的默认值,表示"由 Producer 决定压缩方式",Broker 不会主动解压或重新压缩。常见追问压缩对消息顺序有影响吗? 没有。压缩以 batch 为单位,batch 内消息顺序不变,batch 之间也保持顺序。Broker 端会解压吗? 一般不会。Broker 收到压缩 batch 后直接落盘和转发。只有当 Broker 端配置了不同的 compression.type 时,才会解压再重压缩——这会浪费 CPU,应避免。Consumer 端需要配置压缩算法吗? 不需要。Kafka 在消息头中记录了压缩算法,Consumer 自动识别并解压。选压缩算法本质上是在 CPU、带宽、存储三个资源之间做权衡。先明确瓶颈在哪,再对号入座,而不是盲目追求压缩率。
服务端阅读 05月27日 22:10

Nginx 如何配置 HTTPS 和 SSL 证书?

答案Nginx 启用 HTTPS 的核心是在 server 块中监听 443 端口并指定证书与私钥路径:server { listen 443 ssl http2; server_name example.com; ssl_certificate /etc/nginx/ssl/example.com.crt; ssl_certificate_key /etc/nginx/ssl/example.com.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m;}同时配置 HTTP 自动跳转 HTTPS:server { listen 80; server_name example.com; return 301 https://$host$request_uri;}追问一:SSL 证书有哪些类型?怎么选?自签名证书:测试用,浏览器不信任Let's Encrypt:免费 DV 证书,90 天有效期,Certbot 自动申请与续期商业证书(OV/EV):CA 机构签发,验证组织身份,适合生产环境Let's Encrypt 申请示例:sudo certbot --nginx -d example.com -d www.example.com追问二:如何提升 HTTPS 安全性?四个关键措施:仅启用 TLS 1.2+,禁用 SSLv3 和 TLS 1.0HSTS 头,防止降级攻击:add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;OCSP Stapling,减少证书验证延迟:ssl_stapling on; ssl_stapling_verify on;强密钥,至少 2048 位 RSA 或 256 位 ECC追问三:证书链不完整怎么办?浏览器验证证书需要完整的信任链。若缺少中间证书,需将服务器证书与中间证书合并:cat example.com.crt intermediate.crt > bundle.crtNginx 中指向合并后的文件:ssl_certificate /etc/nginx/ssl/bundle.crt;追问四:多域名如何共用证书?通配符证书(*.example.com)覆盖子域SAN 证书支持多个域名,Certbot 申请时加多个 -d 参数SNI(Server Name Indication)让同一 IP 托管多张证书,Nginx 原生支持配置验证用 nginx -t,无停机重载用 nginx -s reload。
服务端阅读 05月27日 21:53

如何优化 Vercel 应用的性能?

核心答案:Vercel 应用的性能优化可以从四个维度入手——渲染策略选择、构建产物瘦身、缓存配置、以及 Edge 能力利用。渲染策略:选对模式比什么都重要Vercel 上最影响性能的决策是渲染方式。SSG(静态生成)最快,ISR 兼顾更新和速度,SSR 只在需要实时数据时使用。大多数页面应该优先 SSG + ISR:export async function getStaticProps() { const data = await fetchData(); return { props: { data }, revalidate: 60 }; // ISR: 60秒后台重新生成}Next.js App Router 默认就是 Server Component,不要滥用 'use client',每个客户端组件都会增加 JS 体积。构建优化:砍掉不必要的 JS动态导入:非首屏组件用 dynamic() 按需加载。const Chart = dynamic(() => import('./Chart'), { ssr: false });Tree Shaking:用 import { debounce } from 'lodash' 而非 import _ from 'lodash'。用 @next/bundle-analyzer 定位大包。图片和字体:next/image 自动输出 WebP/AVIF 并按设备尺寸裁剪;next/font 消除字体布局偏移(CLS)。缓存:Vercel 上最容易被忽视的杠杆在 vercel.json 中配置 Cache-Control 头:{ "headers": [{ "source": "/static/:path*", "headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }] }]}客户端用 SWR 做请求去重和后台刷新;服务端用 @vercel/kv 缓存计算结果。Vercel 默认对 HTML 响应设置 max-age=0, must-revalidate,静态资源则自动长期缓存。Edge Runtime:把计算推到离用户最近的地方Edge Function 冷启动在毫秒级,适合认证、重定向、A/B 测试等轻量逻辑:export const runtime = 'edge';export default function handler(req) { return new Response('Hello from Edge');}Edge Middleware 可以在请求到达源站之前拦截,做地理路由或权限校验,减少回源延迟。但注意 Edge 环境不支持 Node.js API,Prisma 等库无法直接使用。数据库连接:别让连接池成为瓶颈Serverless 环境下每次请求可能创建新的数据库连接。用全局单例复用 Prisma Client:const globalForPrisma = globalThis;export const prisma = globalForPrisma.prisma ?? new PrismaClient();if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;高并发场景考虑连接池服务(如 Vercel Postgres 自带的 pgBouncer)。追问方向:ISR 的 revalidate 时间怎么定?Edge Runtime 和 Serverless Function 的选型边界在哪?Vercel Analytics 的 Web Vitals 指标(LCP/FID/CLS)分别对应哪些优化手段?Fluid Compute 如何缓解冷启动?
服务端阅读 05月27日 21:53

Prettier 是如何工作的?

Prettier 是什么?Prettier 是一个"有主见"(opinionated)的代码格式化工具,它通过解析代码生成 AST,再用统一的规则重新输出,从而消除团队中的代码风格争议。工作原理Prettier 的格式化流程分三步:1. 解析(Parse):将源代码解析为 AST(抽象语法树)。根据语言不同,选用对应解析器(JavaScript 用 babel,TypeScript 用 typescript 解析器,CSS 用 postcss)。2. 打印(Print):遍历 AST 生成中间表示 Doc。Doc 的关键设计是"可测量"——Prettier 会先尝试将内容放在一行,超出行宽(默认 80 字符)则自动换行缩进。这比直接输出字符串灵活得多。// Prettier 内部 Doc 示意(简化)const doc = group([ "function", " ", "hello", "(", line, "world", ")", " ", "{", indent([line, "console.log(arg);"]), line, "}"]);// 一行放得下 → 单行输出;放不下 → 自动折行3. 输出:将 Doc 转换为最终字符串写回文件。相同输入永远产生相同输出(确定性)。注释处理注释不属于 AST 节点,是格式化器的经典难题。Prettier 通过独立算法将注释附着到 AST 节点上,再在打印阶段输出到正确位置。核心设计取舍有限的配置项:有意不支持大量选项(如"函数括号前是否加空格"已被移除),避免团队为风格配置争论多语言支持:JavaScript、TypeScript、CSS、HTML、JSON、Markdown 等均可用同一工具格式化插件机制:通过 parsers 和 printers 扩展新语言或自定义格式化规则Prettier vs ESLint| 维度 | Prettier | ESLint ||------|----------|--------|| 职责 | 代码格式(缩进、换行、空格) | 代码质量(未使用变量、潜在 bug) || 可配置性 | 少量选项,有态度 | 规则丰富,高度可配 || 输出 | 直接修改代码 | 报错或自动修复 |两者组合使用时,需安装 eslint-config-prettier 关闭 ESLint 中与 Prettier 冲突的格式规则。Git 提交前自动格式化结合 husky + lint-staged 可在 commit 前自动格式化:// package.json{ "lint-staged": { "*.{js,ts,css,md}": "prettier --write" }}npx husky initecho "npx lint-staged" > .husky/pre-commit追问Prettier 如何处理超长单行代码的折行?Doc 的 group + line 机制是如何工作的?为什么 Prettier 要引入 Doc 中间表示而不是直接从 AST 输出字符串?Prettier 的确定性输出有什么前提条件?什么情况下可能出现不一致?
服务端阅读 05月27日 21:52

Nginx 的事件驱动模型是什么?如何实现高并发?

答案Nginx 采用事件驱动 + 非阻塞 I/O 模型,核心是 Master-Worker 进程架构 + epoll 事件循环。每个 Worker 进程单线程运行一个事件循环,通过 epoll 同时监听数千个连接的读写事件,事件就绪时回调处理,I/O 等待期间不阻塞进程,从而用少量进程支撑数万并发连接。事件驱动核心机制epoll 的工作方式:内核维护一个就绪队列,只有活跃连接才会触发事件通知,时间复杂度 O(1)。与传统 select/poll 的 O(n) 轮询不同,epoll 不受 FD 数量影响——这正是 Nginx 解决 C10K 问题的根基。事件循环流程:Worker 进程启动 → 注册监听 FD 到 epoll → epoll_wait 阻塞等待事件 → 事件就绪返回 → 回调处理(accept/read/write) → 继续 epoll_wait连接状态机:每个连接在 Worker 内部以状态机方式管理,经历 等待读 → 处理请求 → 等待写 → 发送响应 → 等待新请求(keepalive) 的状态转换,I/O 等待时让出 CPU 给其他连接处理。Master-Worker 进程模型worker_processes auto; # 通常等于 CPU 核心数worker_rlimit_nofile 65535; # 文件描述符上限events { worker_connections 10240; # 单 Worker 最大连接数 use epoll; # Linux 选择 epoll multi_accept on; # 一次 accept 多个连接 accept_mutex off; # 高并发下关闭,减少锁争用}Master:管理 Worker 生命周期,加载配置,不处理业务请求Worker:各自独立运行事件循环,互不干扰,进程隔离保证稳定性理论并发:worker_processes × worker_connections,4 Worker × 10240 = 40960 并发与 Apache 的本质区别| 对比维度 | Nginx | Apache (prefork) ||---------|-------|------------------|| 模型 | 事件驱动,非阻塞 | 每连接一个进程,阻塞 || 内存 | 10 连接 vs 10 连点约 2MB | 10 连接约 200MB || C10K | 原生支持 | 受限于进程数 || 上下文切换 | 极少 | 频繁 |高并发调优关键参数worker_processes:设为 auto 或 CPU 核心数worker_connections:根据内存调整,通常 10240-65535accept_mutex:高并发下关闭(off),低并发开启防惊群系统级:fs.file-max、net.core.somaxconn、tcp_tw_reuse追问epoll 的 LT 和 ET 模式有什么区别?Nginx 默认用哪个? — LT 水平触发会重复通知,ET 边沿触发只通知一次,Nginx 默认 LT,配合非阻塞 I/O 确保数据读完为什么 Worker 单线程还能处理上万连接? — 因为 99% 时间连接在等 I/O,事件驱动只在 I/O 就绪时才占用 CPUaccept_mutex 是什么?什么时候该关? — 惊群控制锁,防止所有 Worker 同时争抢新连接。连接数远大于 Worker 数时关闭可提升吞吐