服务端面试题手册

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

服务端阅读 05月28日 07:28

Nginx 如何配置虚拟主机?有哪些配置方式?

Nginx 如何配置虚拟主机?有哪些配置方式?虚拟主机(Virtual Host)是 Nginx 最核心的能力之一——一台服务器、一个 Nginx 进程,就能同时服务几十个甚至上百个网站。面试中这道题考察的不只是"怎么配",更是你对其背后路由机制的理解深度。Nginx 虚拟主机的本质就是 server 块。每个 server 块是一个独立的虚拟主机,Nginx 根据请求的域名、端口或 IP 地址,将请求路由到匹配的 server 块处理。三个关键指令决定了路由规则:listen:监听的地址和端口,如 listen 80 或 listen 443 sslserver_name:匹配请求头中的 Host 字段,支持精确匹配、通配符和正则root:该虚拟主机的网站根目录基于域名的虚拟主机这是生产环境最主流的方式。 多个域名共享同一个 IP,Nginx 根据 HTTP 请求头中的 Host 字段决定将请求交给哪个 server 块处理。这也叫 Name-Based Virtual Host:server { listen 80; server_name example.com www.example.com; root /var/www/example.com; index index.html; access_log /var/log/nginx/example.com.access.log; error_log /var/log/nginx/example.com.error.log; location / { try_files $uri $uri/ =404; }}server { listen 80; server_name test.com www.test.com; root /var/www/test.com; index index.html; access_log /var/log/nginx/test.com.access.log; error_log /var/log/nginx/test.com.error.log; location / { try_files $uri $uri/ =404; }}两个 server 块都监听 80 端口,Nginx 收到请求后先匹配 server_name,命中哪个就走哪个配置。server_name 用空格分隔可以写多个域名,www.example.com 和 example.com 都会命中第一个块。面试追问:如果两个 server 块的 servername 都匹配同一个域名会怎样? Nginx 有明确的匹配优先级:精确匹配 > 最长通配符前缀 > 最长通配符后缀 > 第一个正则匹配 > defaultserver。优先级相同则按配置文件加载顺序,先加载的优先。基于端口的虚拟主机通过不同端口区分服务,适合将管理后台、API 服务与主站做物理隔离:# 主站server { listen 80; server_name example.com; root /var/www/example.com; index index.html; location / { try_files $uri $uri/ =404; }}# 管理后台server { listen 8080; server_name example.com; root /var/www/example.com/admin; index index.html; location / { try_files $uri $uri/ =404; }}# HTTPS 安全服务server { listen 8443 ssl; server_name example.com; ssl_certificate /etc/nginx/ssl/example.com.crt; ssl_certificate_key /etc/nginx/ssl/example.com.key; root /var/www/example.com/secure; index index.html; location / { try_files $uri $uri/ =404; }}端口方式的缺点是用户需要显式指定端口号(如 example.com:8080),80 和 443 之外的端口对用户不友好,因此一般只用于内部服务或管理入口。基于 IP 地址的虚拟主机服务器绑定多个 IP 时,按 IP 地址区分站点。这种方式在早期互联网常用,但在现代云环境下已很少使用——公网 IP 资源有限且费用高,基于域名的方式更经济:server { listen 192.168.1.100:80; server_name example.com; root /var/www/example.com; index index.html; location / { try_files $uri $uri/ =404; }}server { listen 192.168.1.101:80; server_name example.com; root /var/www/example.com/mirror; index index.html; location / { try_files $uri $uri/ =404; }}通配符与正则域名匹配子域名多且动态变化时,逐一配置 server_name 不现实。Nginx 提供通配符和正则两种高级匹配:通配符匹配——用 * 匹配子域名:server { listen 80; server_name *.example.com; location / { set $subdomain $host; if ($subdomain ~* ^(.*)\.example\.com$) { set $subdomain $1; } root /var/www/subdomains/$subdomain; index index.html; }}# 默认虚拟主机:兜底处理未匹配的请求server { listen 80 default_server; server_name _; root /var/www/default; return 444; # 直接关闭连接,比 404 更安全}default_server 是安全防线——任何未匹配到 server_name 的请求都会走这个块。建议返回 444(Nginx 特有状态码,直接关闭连接),比返回 404 更能防止信息泄露。正则表达式匹配——用命名捕获组提取子域名:server { listen 80; server_name ~^(?<subdomain>.+)\.example\.com$; root /var/www/example.com/$subdomain; index index.html; location / { try_files $uri $uri/ =404; }}server { listen 80; server_name ~^(?<user>.+)\.users\.example\.com$; root /var/www/users/$user; index index.html; location / { try_files $uri $uri/ =404; }}?<name> 是命名捕获组,提取的值以 $name 变量在配置中引用。通配符只能做简单的 * 匹配,正则则能处理复杂的域名模式——实际项目中正则用得更多。HTTPS 虚拟主机生产环境必须启用 HTTPS,标准做法是 HTTP 301 永久跳转到 HTTPS:# HTTP -> HTTPS 301 跳转server { listen 80; server_name example.com; return 301 https://$server_name$request_uri;}# HTTPS 服务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_session_cache shared:SSL:10m; ssl_session_timeout 10m; root /var/www/example.com; index index.html; location / { try_files $uri $uri/ =404; }}http2 参数直接在 listen 指令中启用 HTTP/2,无需额外编译模块。ssl_session_cache 将 SSL 会话缓存 10 分钟,客户端重连时可以复用,避免完整的 TLS 握手,显著提升 HTTPS 性能。SSL 协议只保留 TLSv1.2 和 TLSv1.3,禁用所有不安全的旧版本。反向代理虚拟主机虚拟主机在生产中最常见的用途是反向代理——不同域名转发到不同的后端服务,Nginx 作为网关统一入口:upstream backend1 { server 192.168.1.100:8080; server 192.168.1.101:8080;}upstream backend2 { server 192.168.1.200:8080; server 192.168.1.201:8080;}server { listen 80; server_name api.example.com; location / { proxy_pass http://backend1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }}server { listen 80; server_name admin.example.com; location / { proxy_pass http://backend2; 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_set_header 三件套必须配置:Host 让后端知道原始域名,X-Real-IP 传递客户端真实 IP,X-Forwarded-For 追加代理链路。不加这些头,后端拿到的全是 Nginx 的内网 IP,日志和鉴权都会出问题。多域名共享配置多个域名配置相似、仅 root 路径不同时,用 map 指令消除重复:map $host $root_path { example.com /var/www/example.com; test.com /var/www/test.com; default /var/www/default;}server { listen 80; server_name example.com test.com; root $root_path; index index.html; location / { try_files $uri $uri/ =404; }}map 在请求处理阶段根据 $host 变量的值映射到对应目录,一个 server 块就能服务多个域名。当域名数量超过 5 个且配置差异仅在路径时,这种方式比逐个写 server 块更易维护。配置文件分离与站点管理生产环境中,千万不要把所有虚拟主机堆在 nginx.conf 一个文件里。用 include 拆分:# /etc/nginx/nginx.confhttp { include /etc/nginx/conf.d/*.conf; include /etc/nginx/sites-enabled/*;}每个虚拟主机一个配置文件,放在 sites-available 目录,通过符号链接启用:# 启用站点ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/# 禁用站点rm /etc/nginx/sites-enabled/example.com# 测试配置语法(修改后必做)nginx -t# 重新加载配置(不影响正在处理的请求)nginx -s reloadnginx -t 是生产操作的铁律——修改配置后先检测语法,通过后再 reload。reload 是平滑重载,Nginx 会等旧请求处理完再切换到新配置,不会中断服务。而 restart 会直接杀掉工作进程,正在处理的请求会断开。PHP 应用虚拟主机WordPress、Laravel 等 PHP 应用需要配置 FastCGI 传递给 PHP-FPM:server { listen 80; server_name example.com; root /var/www/example.com; index index.php index.html; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass unix:/var/run/php/php8.0-fpm.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2)$ { expires 1y; add_header Cache-Control "public, immutable"; }}try_files 中的 /index.php?$query_string 是 PHP 框架的标配写法——先尝试找静态文件,找不到就转发给 index.php 处理,这是 Laravel 等框架 URL 重写的基础。SCRIPT_FILENAME 必须用 $document_root 拼接,否则 PHP-FPM 找不到脚本文件。静态站点虚拟主机纯静态站点可以做更激进的优化——关日志、开压缩、长缓存:server { listen 80; server_name static.example.com; root /var/www/static; index index.html; gzip on; gzip_vary on; gzip_min_length 1024; gzip_types text/plain text/css text/xml text/javascript application/json application/javascript; location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; add_header Cache-Control "public, immutable"; access_log off; } location / { try_files $uri $uri/ =404; }}静态资源关闭 access_log 能显著减少磁盘 IO。高流量站点中,日志写入是主要瓶颈之一,对不需要统计的静态资源关闭日志是常规优化手段。生产环境完整配置模板综合安全、性能和可维护性的完整配置,可作为新项目的起点:server { listen 80; server_name example.com www.example.com; return 301 https://$server_name$request_uri;}server { listen 443 ssl http2; server_name example.com www.example.com; # SSL 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_session_cache shared:SSL:10m; ssl_session_timeout 10m; # 站点根目录 root /var/www/example.com; index index.php index.html; # 日志 access_log /var/log/nginx/example.com.access.log; error_log /var/log/nginx/example.com.error.log; # 安全头 add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; # Gzip 压缩 gzip on; gzip_vary on; gzip_min_length 1024; gzip_types text/plain text/css text/xml text/javascript application/json application/javascript; # 静态资源长缓存 location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2)$ { expires 1y; add_header Cache-Control "public, immutable"; access_log off; } # PHP-FPM location ~ \.php$ { fastcgi_pass unix:/var/run/php/php8.0-fpm.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } # 主路由 location / { try_files $uri $uri/ /index.php?$query_string; } # 禁止访问隐藏文件(.git、.env 等) location ~ /\. { deny all; access_log off; log_not_found off; }}配置要点总结三种类型的选用原则:基于域名是首选方案,一个 IP 托管所有站点,配置简单且用户无感知;基于端口适合内部服务隔离,但需要用户感知端口;基于 IP 在公网环境下已基本淘汰。server_name 匹配优先级(面试必考):精确匹配 > 通配符前缀(*.example.com)> 通配符后缀(example.*)> 正则表达式 > default_server。优先级相同的,按配置文件加载顺序决定。配置排错四步法:nginx -t 检查语法 -> nginx -s reload 确认重载 -> 检查 server_name 是否匹配请求域名 -> 查看 error.log 中的具体报错。大部分"配置不生效"的问题,要么是忘记 reload,要么是 server_name 写错了。
服务端阅读 05月28日 07:26

Nginx 的 location 指令如何匹配?优先级是什么?

Nginx 的 location 指令如何匹配?优先级是什么?location 是 Nginx 中最核心的指令之一,它决定了一个请求由哪个配置块来处理。理解它的匹配规则和优先级,不仅是面试高频考点,更是排查 Nginx 配置问题的基本功。location 的四种匹配方式location 指令的语法:location [=|~|~*|^~] uri { ... }修饰符不同,匹配行为完全不同:| 修饰符 | 匹配方式 | 匹配后是否继续搜索 ||--------|---------|-------------------|| = | 精确匹配 | 否,立即停止 || ^~ | 前缀匹配 | 否,跳过正则检查 || ~ | 正则匹配(区分大小写) | 否,按配置顺序首个命中即停止 || ~* | 正则匹配(不区分大小写) | 否,按配置顺序首个命中即停止 || 无 | 前缀匹配 | 是,继续检查正则 |精确匹配(=)只有请求 URI 与指定路径完全一致时才命中,一旦匹配立即停止搜索,性能最优:location = / { # 仅匹配 /,不匹配 /index.html}前缀匹配(无修饰符)按 URI 前缀匹配,匹配成功后不会立即使用,而是先记住这个最长前缀匹配,继续检查正则表达式。如果正则没有命中,才会回退使用这个前缀匹配:location /docs/ { # 匹配以 /docs/ 开头的所有 URI # 但如果有正则也命中了,正则优先}正则匹配(~ 和 ~*)~:区分大小写~*:不区分大小写正则匹配按配置文件中出现的顺序依次检查,首个命中即停止:location ~ \.php$ { # 区分大小写,匹配 .php 结尾的请求}location ~* \.(jpg|png|gif|css|js)$ { # 不区分大小写,匹配常见静态资源}此外还有 !~ 和 !~*,表示正则不匹配,但它们不能用于 location 指令,只能用在 if 条件判断中。前缀匹配(^~)行为与无修饰符的前缀匹配类似,但关键区别是:如果 ^~ 前缀匹配成功,会跳过后续所有正则检查,直接使用该 location。这在对性能敏感的场景下非常有用:location ^~ /static/ { # 匹配 /static/ 开头的请求 # 即使有正则也匹配,也不检查,直接用这个}Nginx 完整匹配算法面试中光背优先级顺序不够,必须理解 Nginx 的完整匹配流程:Nginx 首先检查所有前缀匹配(包括 =、^~ 和无修饰符),找到最长前缀匹配如果最长前缀是精确匹配(=),立即使用,匹配结束如果最长前缀是 ^~ 匹配,立即使用,匹配结束按配置文件顺序依次检查所有正则表达式(~ 和 ~*)如果正则命中,使用该正则 location,匹配结束如果所有正则都未命中,回退使用步骤 1 中找到的最长前缀匹配这个流程说明一个关键点:正则匹配的优先级高于普通前缀匹配,但低于 = 和 ^~。优先级总结(从高到低)精确匹配 = — 最高优先级,匹配即停前缀匹配 ^~ — 匹配后跳过正则检查正则匹配 ~ / ~* — 按配置顺序,先到先得普通前缀匹配 — 优先级最低,作为兜底注意:正则匹配之间没有优先级之分,完全取决于配置文件中的书写顺序,写在前面的先匹配。配置示例与匹配结果server { listen 80; server_name example.com; location = / { return 200 "1: exact /"; } location / { return 200 "5: prefix /"; } location ^~ /images/ { return 200 "2: ^~ /images/"; } location ~ \.php$ { return 200 "3: regex .php"; } location ~* \.(jpg|png|gif)$ { return 200 "4: regex image"; } location /docs/ { return 200 "6: prefix /docs/"; }}匹配结果验证:| 请求 URI | 命中 location | 原因 ||----------|--------------|------|| / | = / | 精确匹配,优先级最高 || /images/logo.jpg | ^~ /images/ | ^~ 命中后跳过正则 || /api/test.php | ~ \.php$ | 前缀匹配 / 记住后,正则命中 || /photo.JPG | ~* \.(jpg\|png\|gif)$ | 不区分大小写正则命中 || /docs/readme.html | /docs/ | 最长前缀 /docs/ 优先于 /,且无正则命中 || /about | / | 仅前缀 / 命中,无正则匹配 |常见面试陷阱陷阱一:前缀匹配的长度优先多个普通前缀同时匹配时,Nginx 选择最长前缀,而非配置顺序:location /api/ { ... } # 前缀长度 5location /api/v1/ { ... } # 前缀长度 9,优先请求 /api/v1/users 会匹配 /api/v1/,因为前缀更长。陷阱二:正则覆盖普通前缀location /images/ { ... }location ~* \.(jpg|png)$ { ... }请求 /images/logo.jpg 会命中正则 ~*,而非前缀 /images/,因为正则优先级高于普通前缀。如果希望 /images/ 下的请求不被正则抢走,必须使用 ^~。陷阱三:= 只匹配精确路径location = /api { ... }它只匹配 /api,不匹配 /api/、/api/v1。如果需要同时匹配,应该用前缀匹配。实际配置建议高频路径用精确匹配 =,性能最优且语义清晰静态资源目录用 ^~,避免被正则拦截正则匹配控制数量,按命中频率从高到低排列避免在正则中使用复杂回溯表达式,防止 ReDoS 攻击通用兜底 location / 放在最后# 精确匹配首页location = / { proxy_pass http://frontend;}# 静态资源,跳过正则检查location ^~ /static/ { alias /var/www/static/; expires 30d;}# PHP 请求转发location ~ \.php$ { fastcgi_pass unix:/run/php/php-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params;}# 管理后台,跳过正则检查location ^~ /admin/ { auth_basic "Restricted"; auth_basic_user_file /etc/nginx/.htpasswd; proxy_pass http://admin_backend;}# API 代理location /api/ { proxy_pass http://api_backend;}# 兜底location / { try_files $uri $uri/ /index.html;}追问:location 嵌套怎么用?location 支持嵌套,但只有普通前缀匹配可以嵌套在普通前缀内,正则和精确匹配不能嵌套:location /api/ { proxy_pass http://backend; location /api/internal/ { # 更具体的路径,覆盖外层配置 deny all; }}内层 location 会完全覆盖外层的处理逻辑,而不是继承。如果内层也需要 proxy_pass,必须显式重新声明。追问:location 和 rewrite 的执行顺序?Nginx 处理请求时,rewrite 阶段在 location 匹配之前执行(server 级别的 rewrite)。如果 rewrite 修改了 URI,location 会基于修改后的 URI 重新匹配。但 location 内部的 rewrite 可能触发重新匹配,需要注意避免循环重写。
服务端阅读 05月28日 07:26

Nginx 如何实现缓存?缓存策略怎么配才能防击穿?

Nginx 如何实现缓存?如何配置缓存策略?Nginx 的缓存能力是后端服务性能优化的关键手段。面试中常从"Nginx 有哪几种缓存""proxycache 和 fastcgicache 怎么选""如何防止缓存击穿"这几个角度考察,理解原理比背配置更重要。Nginx 缓存的三大层次Nginx 缓存并不是单一机制,而是分布在请求链路的不同位置:浏览器缓存:通过响应头(Cache-Control、Expires)让客户端自行缓存,Nginx 只负责下发头信息代理缓存(proxy_cache):Nginx 作为反向代理时,缓存后端上游的响应,适用于反向代理场景FastCGI 缓存(fastcgi_cache):缓存 FastCGI 协议上游(如 PHP-FPM)的响应,适用于 PHP 直连场景面试时先说清楚这三层,再深入其中一层的配置细节,逻辑比直接贴配置更清晰。代理缓存配置详解http { proxy_cache_path /var/cache/nginx/proxy levels=1:2 keys_zone=proxy_cache:10m max_size=1g inactive=60m use_temp_path=off; server { listen 80; server_name example.com; location / { proxy_cache proxy_cache; proxy_cache_valid 200 302 10m; proxy_cache_valid 404 1m; proxy_cache_key "$scheme$request_method$host$request_uri"; proxy_cache_bypass $http_cache_control; add_header X-Cache-Status $upstream_cache_status; proxy_pass http://backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }}核心参数解读| 参数 | 作用 | 注意点 ||---|---|---|| levels=1:2 | 缓存目录分两级,避免单目录文件过多 | 值越大层级越深,1:2 是常用配置 || keys_zone=proxy_cache:10m | 共享内存区域,存储缓存键和元数据 | 10m 约可存 8 万条键,按需调大 || max_size=1g | 缓存磁盘上限 | 超出后 Nginx 自动淘汰最久未访问的缓存 || inactive=60m | 60 分钟无访问则淘汰 | 与 proxy_cache_valid 无关,是另一条淘汰链 || use_temp_path=off | 临时文件写入缓存目录而非系统临时目录 | 减少跨磁盘拷贝,生产环境建议开启 || proxy_cache_valid 200 302 10m | 200/302 响应缓存 10 分钟 | 必须显式配置,否则不缓存 || proxy_cache_key | 缓存键的计算方式 | 默认含 scheme + method + host + uri,带参请求需考虑是否加入 $args |FastCGI 缓存配置FastCGI 缓存适用于 Nginx 直连 PHP-FPM 的场景,参数与 proxy_cache 对称:http { fastcgi_cache_path /var/cache/nginx/fastcgi levels=1:2 keys_zone=fastcgi_cache:10m max_size=1g inactive=60m; server { location ~ \.php$ { fastcgi_cache fastcgi_cache; fastcgi_cache_valid 200 60m; fastcgi_cache_methods GET HEAD; fastcgi_cache_bypass $skip_cache; fastcgi_no_cache $skip_cache; add_header X-Cache-Status $upstream_cache_status; fastcgi_pass unix:/var/run/php/php8.0-fpm.sock; include fastcgi_params; } }}proxycache 与 fastcgicache 怎么选?| 对比维度 | proxycache | fastcgicache ||---|---|---|| 适用场景 | 反向代理后端(任何 HTTP 服务) | 直连 PHP-FPM 等 FastCGI 进程 || 协议 | HTTP | FastCGI || 灵活性 | 更通用,后端不限语言 | 仅限 FastCGI 协议 || 生产推荐 | 微服务、多语言后端 | 纯 PHP 架构 |两者不能同时作用于同一 location。如果用了 proxy_pass 就用 proxycache,用了 fastcgi_pass 就用 fastcgicache。缓存风暴与缓存锁定这是面试高频追问点。当缓存过期瞬间,大量并发请求同时穿透到后端,这就是缓存风暴(Cache Stampede)。# 缓存锁定:只放一个请求去后端取数据,其余等待proxy_cache_lock on;proxy_cache_lock_timeout 5s;# 过期缓存兜底:后端异常时返回旧缓存proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;# 后台异步更新:命中过期缓存时异步刷新,不阻塞请求proxy_cache_background_update on;三者配合的执行逻辑:缓存过期 → proxy_cache_lock 放行一个请求 → 其余请求读 proxy_cache_use_stale 返回旧数据 → 新数据写入后所有请求命中。这就是完整的防击穿方案。动态缓存控制不是所有请求都该缓存。用 map 指令按条件跳过缓存:# 按 URI 跳过缓存map $request_uri $skip_cache { default 0; ~*/admin/ 1; ~*/api/ 1; ~*/user/ 1;}# 按后端响应头跳过缓存map $upstream_http_cache_control $skip_cache_by_header { ~*no-cache 1; ~*private 1; default 0;}# 组合条件map $skip_cache$skip_cache_by_header $combined_skip { default 0; ~1 1;}然后在 location 中使用:proxy_cache_bypass $combined_skip;proxy_no_cache $combined_skip;proxy_cache_bypass 和 proxy_no_cache 的区别:bypass 是跳过缓存直接请求后端但可能将响应写入缓存;no_cache 则完全不写缓存。生产环境通常两者配合使用,确保该跳过的请求既不读缓存也不写缓存。静态文件与浏览器缓存静态资源的缓存走另一套逻辑,不经过 proxy_cache,直接由 Nginx 返回文件并设置浏览器缓存头:location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; add_header Cache-Control "public, immutable"; access_log off;}immutable 告诉浏览器:资源不会变,不需要发条件请求验证。搭配文件名加 hash(如 app.3a7b2c.js)效果最佳,更新时换文件名即可让浏览器重新请求。缓存清除方案Nginx 开源版不支持主动清除缓存,三种替代方案:自然过期:通过 proxy_cache_valid 设定 TTL,到期自动淘汰第三方模块 ngxcachepurge:支持按 URL 主动清除,需编译安装删除缓存文件:根据 proxy_cache_key 的 MD5 值定位文件路径并删除,rm -rf /var/cache/nginx/proxy 可全量清除生产环境中,最稳妥的方式是修改 proxy_cache_key 加入版本号参数,发布时更新版本号让旧缓存自然失效。缓存命中率监控通过 X-Cache-Status 响应头可观察缓存命中情况:add_header X-Cache-Status $upstream_cache_status;状态值含义:| 状态 | 含义 ||---|---|| MISS | 未命中,请求穿透到后端 || BYPASS | 命中跳过条件,不走缓存 || EXPIRED | 缓存已过期,需重新获取 || STALE | 后端异常,返回过期缓存 || UPDATING | 缓存正在更新,返回旧内容 || HIT | 命中缓存,直接返回 |监控思路:统计 HIT / (HIT + MISS + EXPIRED) 的比值,低于 80% 就需要调优缓存键或 TTL。缓存配置的常见踩坑1. 后端响应头导致缓存不生效后端返回 Cache-Control: no-cache 或 Set-Cookie 时,Nginx 默认不缓存。需要忽略这些头:proxy_ignore_headers X-Accel-Expires Expires Cache-Control Set-Cookie;2. 缓存键缺少关键参数默认 proxy_cache_key 不含 $args,但 API 请求 ?page=1 和 ?page=2 应该返回不同内容,需要加入查询参数:proxy_cache_key "$scheme$request_method$host$request_uri$is_args$args";3. 缓存最小请求次数导致首次不缓存proxy_cache_min_uses 2 表示请求出现 2 次才缓存,低流量接口可能永远不缓存。生产环境建议设为 1。4. keys_zone 过小导致缓存元数据丢失keys_zone 只存键和元数据,不存响应体。10m 约存 8 万条键,key 较长时需适当增大。生产环境完整配置参考http { proxy_cache_path /var/cache/nginx/proxy levels=1:2 keys_zone=proxy_cache:100m max_size=10g inactive=60m use_temp_path=off; fastcgi_cache_path /var/cache/nginx/fastcgi levels=1:2 keys_zone=fastcgi_cache:100m max_size=10g inactive=60m; map $request_uri $skip_cache { default 0; ~*/admin/ 1; ~*/api/ 1; ~*/user/ 1; } server { listen 80; server_name example.com; location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2)$ { expires 1y; add_header Cache-Control "public, immutable"; } location / { proxy_cache proxy_cache; proxy_cache_valid 200 302 10m; proxy_cache_valid 404 1m; proxy_cache_key "$scheme$request_method$host$request_uri"; proxy_cache_bypass $skip_cache; proxy_no_cache $skip_cache; proxy_cache_lock on; proxy_cache_lock_timeout 5s; proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; proxy_cache_background_update on; add_header X-Cache-Status $upstream_cache_status; proxy_pass http://backend; } location ~ \.php$ { fastcgi_cache fastcgi_cache; fastcgi_cache_valid 200 60m; fastcgi_cache_bypass $skip_cache; fastcgi_no_cache $skip_cache; add_header X-Cache-Status $upstream_cache_status; fastcgi_pass unix:/var/run/php/php8.0-fpm.sock; } }}
服务端阅读 05月28日 07:24

Nginx 如何实现访问控制?有哪些访问控制方法?

Nginx 如何实现访问控制?有哪些访问控制方法?Nginx 的访问控制是后端面试高频考点,核心思路是"在反向代理层拦截非法请求,减轻后端压力"。主要方法有五种:IP 黑白名单、HTTP 基本认证、请求方法限制、基于请求头的鉴权、地理/时间条件控制,实战中往往组合使用。下面逐一拆解原理和配置要点。一、IP 黑白名单:最基础的网络层控制Nginx 通过 allow / deny 指令按 IP 或 CIDR 段做访问控制,规则从上到下依次匹配,命中即生效:location /admin { allow 192.168.1.0/24; # 内网放行 allow 10.0.0.0/8; # VPN 段放行 deny all; # 其余全部拒绝 proxy_pass http://backend;}注意事项:当客户端经过代理时,$remote_addr 拿到的是代理 IP 而非真实客户端 IP,需要配合 $http_x_forwarded_for 或 realip 模块获取真实地址白名单优先于黑名单是安全最佳实践——默认拒绝,显式放行二、HTTP 基本认证:用户名密码验证使用 auth_basic + auth_basic_user_file 实现,密码文件通过 htpasswd 工具生成:location /admin { auth_basic "Admin Area"; auth_basic_user_file /etc/nginx/.htpasswd; proxy_pass http://backend;}生成密码文件:htpasswd -c /etc/nginx/.htpasswd admin_user关键点: Basic Auth 的凭据是 Base64 编码而非加密,生产环境务必搭配 HTTPS 使用,否则密码可被中间人截获。三、请求方法限制:只允许特定 HTTP 方法用 limit_except 指令比 if ($request_method) 更规范,它在 location 级别做方法白名单:location /api { limit_except GET POST { deny all; # 只允许 GET 和 POST,其他方法返回 403 } proxy_pass http://api_backend;}与 if 写法的区别: limit_except 是 Nginx 官方推荐的方式,不会触发 "if is evil" 问题,且与 satisfy 指令配合更好。四、基于请求头的鉴权:API Key、Referer、User-AgentAPI Key 校验:map $http_x_api_key $api_valid { default 0; "sk_prod_abc123" 1; "sk_prod_def456" 1;}location /api { if ($api_valid = 0) { return 401; } proxy_pass http://api_backend;}用 map 比 if 直接比较更灵活,支持多 key 映射且可集中管理。防盗链(Referer 校验):location /images/ { valid_referers none blocked server_names *.example.com; if ($invalid_referer) { return 403; } root /var/www/images;}UA 过滤: 屏蔽恶意爬虫if ($http_user_agent ~* (bot|crawler|spider|scraper)) { return 403;}五、地理与时间条件控制地理位置限制(基于 geo 模块):geo $allowed_country { default no; CN yes; US yes;}server { location / { if ($allowed_country = no) { return 403; } proxy_pass http://backend; }}如需精确到城市级,可用 geoip 模块配合 MaxMind 数据库。时间条件限制:map $time_iso8601 $business_hours { default 0; ~^(\d{4}-\d{2}-\d{2}T(09|1[0-9]|2[0-1])) 1;}location /admin { if ($business_hours = 0) { return 403; } proxy_pass http://backend;}适合管理后台只在工作时段开放的场景。六、组合策略:satisfy 指令与多层防护satisfy any 表示满足任一条件即可访问,satisfy all 表示必须全部满足:location /admin { satisfy any; # IP 白名单 或 密码认证,满足其一即可 allow 192.168.1.0/24; deny all; auth_basic "Admin Area"; auth_basic_user_file /etc/nginx/.htpasswd; proxy_pass http://backend;}实战建议: 管理后台常用 satisfy any——内网 IP 免密码,外网需要认证;API 接口常用 satisfy all——IP + Key 双重验证。七、安全加固:敏感文件与目录防护# 禁止访问隐藏文件(如 .git、.env)location ~ /\. { deny all; access_log off; log_not_found off;}# 禁止访问敏感后缀文件location ~* \.(htaccess|htpasswd|ini|log|sh|sql|bak|swp)$ { deny all; access_log off;}# 禁止目录遍历autoindex off;这些规则应作为 Nginx 配置的基线安全策略,防止信息泄露。面试追问与核心要点Q:Nginx 访问控制的执行顺序是什么?allow/deny 按配置顺序从上到下匹配,先命中先生效。location 内的规则优先于 server 级别,server 级别优先于 http 级别。Q:satisfy any 和 satisfy all 的区别?any 是"或"逻辑——IP 白名单和认证满足其一即可;all 是"与"逻辑——两者都必须通过。默认是 all。Q:代理场景下 IP 限制为什么不生效?因为 $remote_addr 拿到的是上一层代理的 IP。解决方案:使用 ngx_http_realip_module 设置 set_real_ip_from 和 real_ip_header X-Forwarded-For 还原真实客户端 IP。Q:if 指令在 location 中有什么陷阱?Nginx 的 if 在 location 中属于 rewrite 阶段,可能导致非预期行为("if is evil")。能用 map、limit_except、allow/deny 替代的就避免用 if。
服务端阅读 05月28日 07:23

Nginx 如何实现限流?有哪些限流策略?

Nginx 如何实现限流?有哪些限流策略?Nginx 限流的核心思路是控制单位时间内的请求量或并发连接数,防止后端服务被流量打垮。面试中这道题主要考察三个层面:你知道哪些限流模块、你理解底层算法吗、你在生产环境怎么用。限流的两种基本方式Nginx 提供两大限流模块:limit_req:限制请求速率,控制单位时间内允许的请求数limit_conn:限制并发连接数,控制同一时刻的 TCP 连接数两者的区别在于粒度——limit_req 关注的是请求频率(每秒多少个请求),limit_conn 关注的是连接数(同时存在多少个连接)。一个长连接可以承载多个请求,所以实际防护中通常两者配合使用。limit_req:请求速率限制http { limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s; server { location /api/ { limit_req zone=api_limit burst=20 nodelay; limit_req_status 429; proxy_pass http://backend; } }}关键参数解读:$binary_remote_addr:以客户端 IP 作为限流键,二进制格式比字符串格式节省内存,10MB 共享内存大约能记录 16 万个 IPzone=api_limit:10m:定义共享内存区域名称和大小rate=10r/s:每秒允许 10 个请求,也可以用 r/m 表示每分钟burst=20:允许 20 个突发请求排队等待nodelay:突发请求不延迟处理,超出 burst 容量则直接拒绝limit_req_status 429:被限流时返回 429 而非默认的 503burst 和 nodelay 到底怎么配合?这是面试的高频追问,很多人配置过但说不清楚原理。只用 limit_req zone=api_limit:严格按 rate 执行,超出的请求直接 503,体验差。加 burst=20:允许 20 个请求排队,Nginx 按 rate 速率逐个处理队列中的请求,多余请求延迟响应。好处是不误杀,坏处是用户感知延迟。再加 nodelay:队列中的请求立即处理,不延迟响应,但队列满了还是拒绝。实际效果是「短时间内允许突发,超出就拒绝」,适合大多数 API 场景。简单记:burst 控制能容忍多少突发,nodelay 决定突发请求是延迟还是立即处理。limit_conn:并发连接数限制http { limit_conn_zone $binary_remote_addr zone=conn_limit:10m; server { limit_conn conn_limit 10; proxy_pass http://backend; }}这里 limit_conn conn_limit 10 表示同一个 IP 最多同时保持 10 个连接。注意这和 limit_req 不同——limit_req 限制的是请求的到达速率,limit_conn 限制的是连接的并发数量。典型场景:防止单个客户端通过大量并发连接耗尽服务器资源(如慢速攻击 Slowloris)。底层算法:漏桶与令牌桶面试中问限流,必然会追问算法原理。漏桶算法(Leaky Bucket)请求像水一样倒入桶中,桶以固定速率漏水。如果桶满了,新请求被丢弃。特点是输出速率恒定,不管输入多猛烈,处理速度始终平稳。limit_req 不加 burst 参数时就是典型的漏桶行为——严格按 rate 处理,超出直接拒绝。令牌桶算法(Token Bucket)系统以固定速率往桶里放令牌,每个请求需要取走一个令牌。桶满了令牌不再增加。与漏桶的区别在于:令牌桶允许突发——桶里攒够了令牌时,可以一次性处理一批请求。limit_req 加上 burst 参数就实现了类似令牌桶的效果,允许一定程度的流量突发。核心区别:漏桶强制匀速输出,令牌桶允许有限突发。Nginx 的 limit_req 实际上是两者的结合——基础速率是漏桶,burst 提供了令牌桶式的突发能力。带宽限制除了请求和连接层面的限流,Nginx 还能限制响应传输速率:location /download/ { limit_rate 1m; limit_rate_after 10m; root /var/www/files;}limit_rate 1m:限速 1MB/slimit_rate_after 10m:前 10MB 不限速,之后才限速适用于大文件下载场景,防止少数大流量用户占满带宽。白名单与动态限流生产环境中通常需要对内部 IP 或特定请求方法豁免限流。基于 geo 的白名单:geo $limit_key { default $binary_remote_addr; 192.168.1.0/24 ""; 10.0.0.0/8 "";}limit_req_zone $limit_key zone=whitelist:10m rate=10r/s;白名单内的 IP 对应空字符串,不参与限流计算。基于请求方法的动态限流:map $request_method $limit_key { default $binary_remote_addr; GET ""; HEAD "";}GET 和 HEAD 请求不限流,其他方法(POST、PUT 等)参与限流,适合写操作需要更严格控制的场景。多层限流实战配置http { # 全局限流 limit_req_zone $binary_remote_addr zone=global:10m rate=50r/s; # API 接口限流 limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; # 登录接口限流 limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m; # 连接数限制 limit_conn_zone $binary_remote_addr zone=conn:10m; limit_req_status 429; limit_conn_status 429; server { limit_conn conn 20; location / { limit_req zone=global burst=50 nodelay; proxy_pass http://backend; } location /api/ { limit_req zone=global burst=50 nodelay; limit_req zone=api burst=10 nodelay; proxy_pass http://api_backend; } location /login { limit_req zone=login burst=2 nodelay; proxy_pass http://auth_backend; } }}这套配置的思路是分层防护:全局兜底防 DDoS,API 层控制接口频率,登录接口单独严控防暴力破解。同一个 location 可以叠加多个 limit_req,任一规则触发都会拒绝请求。限流日志与监控log_format limit '$remote_addr - [$time_local] "$request" ' '$status limit=$limit_req_status';limit_req_log_level warn;$limit_req_status 变量记录限流状态,limit_req_log_level 控制限流日志级别。生产环境建议用 warn 级别,避免日志量过大。配合 ELK 或 Prometheus 可以做限流趋势分析和告警。生产环境的几个经验阈值不是拍脑袋定的——先压测后端服务的极限 QPS,限流值设在其 70%-80% 作为安全水位429 响应要友好——返回 JSON 格式的错误提示,带上 Retry-After 头告诉客户端多久后重试zone 内存别省——10MB 约存 16 万 IP,如果用户量大要相应调大,内存耗尽后新请求直接 503burst 要结合业务——API 类场景 burst 可以小一些(5-10),页面访问场景可以大一些(20-50)限流不是万能的——在 Nginx 层限流只能防住从外到内的流量冲击,内部服务间的调用保护需要 Sentinel 或熔断器关注误杀——公司出口 IP 共享场景下,单 IP 限流会误伤同一 NAT 后的多个用户,可考虑基于 token 或租户维度的限流键追问:limitreq 和 limitconn 该选哪个?都要用。limit_req 防高频请求冲击,limit_conn 防连接数耗尽,两者解决不同问题。面试中如果只答一个,会被认为理解不全面。追问:Nginx 限流有什么局限?单机维度的限流,分布式环境下需要 Redis + Lua 或专门的限流服务限流键有限,复杂业务逻辑(如按用户等级限流)需要结合 OpenResty 或网关层limit_req 基于共享内存,重启后状态丢失不支持滑动窗口计数,只有固定时间窗口的速率计算
服务端阅读 05月28日 07:19

Redis 事务、Lua 脚本和分布式锁的实现原理和使用场景是什么?

Redis 事务、Lua 脚本和分布式锁是 Redis 面试中出现频率最高的三个高级特性,很多候选人只能说出命令用法,却讲不清背后的原理和边界,面试官一追问就卡壳。下面逐一拆解。Redis 事务的原理与局限Redis 事务通过 MULTI、EXEC、DISCARD、WATCH 四个命令协作完成。MULTI 开启事务后,后续命令进入队列而非立即执行;EXEC 一次性提交队列中的所有命令;DISCARD 放弃事务;WATCH 实现乐观锁,监控 key 是否在事务提交前被修改。WATCH balanceMULTIDECRBY balance 100INCRBY expense 100EXEC如果 WATCH 监控的 key 在 EXEC 之前被其他客户端修改,整个事务会被丢弃,返回 nil。事务的核心特点:命令按顺序执行,不会被其他客户端插入,这是隔离性的保证。不支持回滚。如果队列中某条命令执行失败(比如对字符串执行 LPUSH),其余命令照常执行。Redis 官方的设计哲学是:命令失败属于编程错误,应在开发阶段发现,不值得为此牺牲性能。无法使用中间结果。事务中的命令不能引用前一条命令的返回值,这极大限制了事务的表达能力。这些局限正是 Lua 脚本存在的理由。追问:Redis 事务为什么不支持回滚?Redis 作者 antirez 的原话是:回滚需要保存命令执行前的状态,这会引入与 AOF 持久化类似的复杂度,而命令失败本质是 bug,不应该出现在生产环境。所以 Redis 选择不支持回滚,换来更简单、更快的实现。Lua 脚本为什么能替代事务Lua 脚本在 Redis 服务端以原子方式执行,执行期间不会处理其他客户端的命令。和事务相比,Lua 脚本的核心优势在于可以读取中间结果并做条件判断。EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 mykey myvalue先用 SCRIPT LOAD 获取脚本 SHA1 校验和,后续用 EVALSHA 执行,避免每次传输完整脚本:SCRIPT LOAD "return redis.call('SET', KEYS[1], ARGV[1])"# 返回 sha1sumEVALSHA <sha> 1 mykey myvalue几个典型的 Lua 脚本应用场景:原子性 CAS 操作——只有当值等于预期时才更新:local current = redis.call('GET', KEYS[1])if current == ARGV[1] then redis.call('SET', KEYS[1], ARGV[2]) return 1else return 0end滑动窗口限流器——一次执行完成过期清理、计数检查、记录添加三个步骤:local key = KEYS[1]local limit = tonumber(ARGV[1])local window = tonumber(ARGV[2])redis.call('ZREMRANGEBYSCORE', key, '-inf', window)local count = redis.call('ZCARD', key)if count < limit then redis.call('ZADD', key, window, ARGV[3]) redis.call('EXPIRE', key, math.ceil((window - tonumber(ARGV[4])) / 1000)) return 1else return 0endLua 脚本需要注意三点:执行时间不能太长,否则阻塞整个 Redis 实例(默认 5 秒超时);不能使用随机函数(如 math.random),否则主从复制结果不一致;不能执行阻塞命令(如 BLPOP)。追问:Lua 脚本出错会怎样?Lua 脚本运行时出错会立即停止,但已执行的 Redis 命令不会被撤销——这点和事务一致,都不满足严格意义上的原子性。从外部看,脚本的执行是不可分割的;从内部看,部分成功部分失败是可能的。分布式锁的三种实现与踩坑分布式锁要解决的核心问题:在多进程、多机器环境下,保证同一时刻只有一个客户端能操作共享资源。SETNX + EXPIRE 的问题最早的做法是先 SETNX 获取锁,再 EXPIRE 设置过期时间。这两步不是原子操作——如果 SETNX 成功后客户端崩溃,锁永远不释放,造成死锁。SET NX EX(推荐的基础方案)Redis 2.6.12 起 SET 命令支持 NX 和 EX 参数,一条命令完成加锁和设置过期时间:public boolean tryLock(String key, String value, int expireSeconds) { String result = jedis.set(key, value, "NX", "EX", expireSeconds); return "OK".equals(result);}释放锁必须用 Lua 脚本保证原子性——先判断值是否为自己持有,再删除:if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1])else return 0end为什么不能直接 DEL?因为锁可能已经过期并被其他客户端获取,直接 DEL 会删掉别人的锁。追问:主从切换导致锁丢失怎么办?考虑这个时序:客户端 A 获取锁 → 主节点写入成功但尚未同步到从节点 → 主节点宕机 → 从节点升为主节点 → 客户端 B 获取同一把锁 → 两个客户端同时持有锁。Redlock 算法就是为解决这个问题设计的。Redlock 算法Redlock 向 N 个(通常 5 个)独立的 Redis 实例获取锁,只要在大多数实例(≥3)上成功,且总耗时未超过锁的有效期,就认为加锁成功。这依赖的是时钟同步和多数派决策,不依赖主从复制。Redlock 的争议:Martin Kleppmann 在《How to do distributed locking》一文中指出 Redlock 依赖系统时钟,当时钟跳变时可能出错,建议使用 fencing token 方案。antirez 专门写了长文反驳。实际工程中,大多数团队选择接受 Redlock 的概率性安全保证,或直接使用单节点锁 + 幂等设计来规避风险。Redisson 的工程级实现Redisson 是 Java 生态最成熟的 Redis 分布式锁实现,提供了可重入锁、公平锁、读写锁、联锁等多种锁类型。核心设计:看门狗机制:默认锁过期时间 30 秒,Redisson 会启动一个后台线程,每 10 秒(过期时间的 1/3)自动续期,直到显式释放或客户端宕机。这解决了长任务执行期间锁过期被其他客户端获取的问题。RLock lock = redisson.getLock("myLock");try { if (lock.tryLock(10, 30, TimeUnit.SECONDS)) { // 执行业务逻辑 }} finally { lock.unlock();}可重入性:Redisson 使用 Hash 结构存储锁信息,field 是客户端 ID + 线程 ID,value 是重入次数,加锁时重入次数 +1,解锁时 -1,减到 0 才真正释放。整个逻辑用 Lua 脚本保证原子性。事务、Lua 脚本与分布式锁的选型| 维度 | 事务 | Lua 脚本 | 分布式锁 ||------|------|----------|---------|| 原子性 | 命令不可插入,但不支持回滚 | 同事务 | 依赖实现方式 || 条件判断 | 不支持 | 完全支持 | 通过 Lua 脚本支持 || 网络开销 | 多次 RTT(除非 pipeline) | 一次 RTT | 多次 RTT || 典型场景 | 简单批量操作 | CAS、限流、复杂业务逻辑 | 跨进程互斥 |选择原则:事务适合不需要中间结果的简单批量操作;Lua 脚本适合需要条件判断、依赖中间结果的场景;分布式锁解决的是跨进程互斥问题,本质上依赖 Lua 脚本保证操作的原子性。面试中容易被追问的几个点Redis 事务和 MySQL 事务有什么区别? Redis 事务不支持回滚,没有隔离级别,不保证持久性——它只是批量执行命令的机制,和 ACID 事务有本质区别。Lua 脚本执行期间 Redis 宕机怎么办? 如果开启了 AOF 持久化,已执行的命令会被记录,重启后重放。如果没有持久化,数据丢失。关键点是 Lua 脚本的"原子性"只保证执行期间不被打断,不保证持久性和严格的原子性。分布式锁的过期时间怎么设置? 太短容易导致任务未完成锁就被释放,太长会导致客户端宕机后其他客户端等待过久。Redisson 的看门狗机制是最佳实践,动态续期比固定过期时间更可靠。
服务端阅读 05月28日 07:17

Puppeteer 如何实现网络请求拦截?有哪些实际应用场景?

Puppeteer 通过 CDP(Chrome DevTools Protocol)提供的 Network 域能力实现请求拦截,核心 API 是 page.setRequestInterception(true)。启用后,每个请求都会被暂停,必须手动调用 continue()、abort() 或 respond() 才能放行。这一机制在爬虫加速、接口 Mock、安全测试等场景中非常实用。启用请求拦截的基本方式const puppeteer = require("puppeteer");(async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); // 启用请求拦截 await page.setRequestInterception(true); page.on("request", (request) => { // 每个请求必须被处理,否则页面会卡住 request.continue(); }); await page.goto("https://example.com"); await browser.close();})();关键点:setRequestInterception(true) 必须在页面导航前调用;每个被拦截的请求必须调用 continue()、abort() 或 respond() 之一,否则请求会一直挂起。请求拦截的四种核心操作continue —— 放行请求直接放行原始请求,也可以在放行的同时修改请求参数:page.on("request", (request) => { // 修改请求头后放行 request.continue({ headers: { ...request.headers(), Authorization: "Bearer token123", }, });});continue() 支持覆盖 url、method、postData、headers 四个字段,可以实现请求重定向、修改 POST 数据等操作。abort —— 终止请求直接阻止请求发出,常用于屏蔽广告、图片、字体等非必要资源:page.on("request", (request) => { if (request.resourceType() === "image") { request.abort(); } else { request.continue(); }});abort() 可传入错误码,默认是 failed,常用值包括 aborted、accessdenied、connectionrefused 等。respond —— 直接返回响应不向服务器发送请求,直接在本地构造响应返回。这是接口 Mock 的核心手段:page.on("request", (request) => { if (request.url().includes("/api/user")) { request.respond({ status: 200, contentType: "application/json", body: JSON.stringify({ id: 1, name: "test-user" }), }); } else { request.continue(); }});respond() 支持 status、headers、contentType、body 四个字段,可以完整模拟服务器行为。响应监听 —— 获取服务端返回数据通过 response 事件监听服务端实际返回的内容,常用于数据采集和接口监控:page.on("response", async (response) => { if (response.url().includes("/api/data")) { const data = await response.json(); console.log("接口返回:", data); }});注意:response.json() 只能调用一次,且只有 JSON 格式的响应才能解析。资源类型过滤request.resourceType() 返回请求的资源类型,可用于批量过滤:const blockedTypes = ["image", "font", "stylesheet", "media"];page.on("request", (request) => { if (blockedTypes.includes(request.resourceType())) { request.abort(); } else { request.continue(); }});Puppeteer 支持的资源类型包括:document、stylesheet、image、media、font、script、xhr、fetch、websocket、eventsource、manifest、texttrack、other。实际应用场景爬虫加速:屏蔽非必要资源爬取数据时,图片、字体、CSS 对数据提取无用,屏蔽后页面加载速度可提升 50% 以上:await page.setRequestInterception(true);page.on("request", (request) => { const useless = ["image", "font", "stylesheet", "media"]; if (useless.includes(request.resourceType())) { request.abort(); } else { request.continue(); }});接口 Mock:前后端联调后端接口未就绪时,前端可以用 respond() 直接 Mock 数据,不依赖任何 Mock 服务:const mockData = { "/api/users": { users: [{ id: 1, name: "Alice" }] }, "/api/posts": { posts: [{ id: 1, title: "Hello" }] },};page.on("request", (request) => { for (const [path, data] of Object.entries(mockData)) { if (request.url().includes(path)) { request.respond({ status: 200, contentType: "application/json", body: JSON.stringify(data), }); return; } } request.continue();});广告与追踪屏蔽屏蔽已知广告域名和追踪脚本,减少无关请求:const blockedDomains = ["ads.example.com", "analytics.example.com", "tracker.example.com"];page.on("request", (request) => { if (blockedDomains.some((d) => request.url().includes(d))) { request.abort(); } else { request.continue(); }});自动注入认证头需要对所有请求添加 Token 时,用 continue() 覆盖 headers 即可,无需在每个请求中手动处理:page.on("request", (request) => { request.continue({ headers: { ...request.headers(), Authorization: "Bearer your-token-here", }, });});网络请求监控与性能分析记录所有请求和响应的时间戳与状态码,用于性能分析和接口排查:const logs = [];page.on("request", (request) => { logs.push({ type: "request", url: request.url(), method: request.method(), resourceType: request.resourceType(), time: Date.now(), }); request.continue();});page.on("response", (response) => { logs.push({ type: "response", url: response.url(), status: response.status(), time: Date.now(), });});await page.goto("https://example.com");console.log("请求总数:", logs.filter((l) => l.type === "request").length);console.log("响应总数:", logs.filter((l) => l.type === "response").length);错误处理page.on("requestfailed", (request) => { console.error("请求失败:", request.url()); console.error("原因:", request.failure()?.errorText);});常见失败原因包括:网络断开、DNS 解析失败、SSL 证书错误、被 abort() 主动终止等。面试追问与注意事项Q:拦截对所有请求都会生效吗?不是。导航请求(主文档请求)在部分场景下可能无法被拦截,且 WebSocket 升级请求的处理方式与普通 HTTP 请求不同。Q:请求拦截对性能有什么影响?启用拦截后,每个请求都要经过 JavaScript 事件循环处理,会增加请求延迟。对于高频请求场景(如 WebSocket 消息),建议按条件拦截而非全量拦截。Q:如何避免重复处理请求?调用 request.isInterceptResolutionHandled() 检查请求是否已被处理,避免在多个监听器中对同一请求重复调用 continue() 或 abort()。Q:与 Playwright 的请求拦截有什么区别?Playwright 使用 page.route() API,支持路由模式匹配(如 page.route("**/api/**", handler)),语法更简洁。Puppeteer 则需要手动判断 URL。两者底层都基于 CDP,核心能力一致。
服务端阅读 05月28日 07:17

Redis 与 MySQL、MongoDB、Memcached 有什么区别?如何选择?

Redis、MySQL、MongoDB、Memcached 是后端开发中最常用的四种数据存储方案,面试中经常被放在一起考察。它们的设计目标完全不同,理解本质差异才能做出正确的技术选型。核心区别:一张表看懂| 维度 | Redis | MySQL | MongoDB | Memcached ||------|-------|-------|---------|-----------|| 存储介质 | 内存为主,可持久化 | 磁盘为主 | 磁盘(内存映射) | 纯内存 || 数据模型 | Key-Value + 多种数据结构 | 关系型表 | 文档型(BSON) | Key-Value(仅String) || 事务支持 | 有限事务(MULTI/EXEC) | 完整ACID | 4.0起支持多文档事务 | 无 || 持久化 | RDB + AOF | 天然持久化 | 天然持久化 | 无,宕机即丢 || 查询能力 | 按Key操作,有限范围查询 | SQL,复杂关联聚合 | MQL,支持索引和聚合 | 仅GET/SET || 单机QPS | 10万+ | 数千~数万 | 数万 | 10万+ || 一致性模型 | 最终一致性 | 强一致性 | 可配置(最终/强) | 无一致性保证 || 水平扩展 | Cluster分片 | 分库分表(复杂) | 原生分片 | 客户端一致性Hash |Redis vs MySQL:缓存与持久化的抉择本质区别在于存储介质和一致性模型。Redis 数据驻留内存,读写延迟在亚毫秒级,但默认不保证数据持久化——即使开启 AOF,也存在最多 1 秒的数据丢失窗口。MySQL 数据落盘,通过 redo log 和 doublewrite 机制保证数据安全,但读写延迟在毫秒到十毫秒级。面试中常问的一个问题:Redis 能否替代 MySQL?答案是不能。两者的定位完全不同:Redis 适合做缓存层和实时数据层,数据可以丢失或从源头重建MySQL 适合做持久化存储层,数据不能丢失且需要事务保证生产中的经典架构是 Redis + MySQL 组合:读请求先查 Redis,命中则直接返回;未命中则查 MySQL,结果回写 Redis。但这里有几个关键问题需要注意:双写一致性:先更新 MySQL 再删缓存,存在短暂不一致窗口。对一致性要求高的场景,可用 Canal 监听 binlog 同步更新 Redis缓存穿透:查询不存在的数据,绕过缓存直击数据库。用布隆过滤器或缓存空值解决缓存击穿:热点 Key 过期瞬间大量请求涌入数据库。用互斥锁或永不过期+异步刷新缓存雪崩:大批 Key 同时过期。用随机过期时间打散Redis vs MongoDB:两种 NoSQL 的不同思路Redis 和 MongoDB 虽然都属于 NoSQL,但设计哲学完全不同。Redis 追求极致性能,数据全在内存中,数据结构精心设计,每种操作的时间复杂度都有明确保证。它更像一个高性能的数据结构服务器。MongoDB 追求灵活性,文档模型允许 schema 自由变化,嵌套文档减少了关联查询的需要。它更像一个增强版的 MySQL。关键区别:数据量:Redis 受内存限制,通常存热点数据;MongoDB 可存储 TB 级数据查询复杂度:Redis 只支持基于 Key 的操作;MongoDB 支持条件查询、聚合管道适用场景:Redis 做缓存/计数器/排行榜/分布式锁;MongoDB 做内容管理/日志/IoT 数据/用户画像Redis vs Memcached:缓存之争的终局这组对比面试频率极高。核心结论:新项目直接选 Redis,没有理由选 Memcached。Memcached 的仅存优势:多线程架构,在 value 超过 100KB 时吞吐量更高更简单的部署,适合纯缓存场景Redis 全面碾压的点:支持丰富数据结构(Memcached 只有 String)支持持久化(Memcached 宕机数据全丢)支持主从复制和集群(Memcached 靠客户端分片)支持发布订阅、Lua 脚本、Stream(Memcached 无)单线程模型反而简化了并发控制,小数据性能更优Memcached 在 2015 年之前还有市场份额,现在基本已被 Redis 完全取代。面试中回答"选 Redis"即可,但要说清楚为什么。技术选型:根据场景做决策选型不是二选一,而是根据业务特征匹配最合适的工具。选 Redis 的场景:需要亚毫秒级响应、数据可以容忍短暂丢失、操作模式简单(按 Key 读写)。典型用例:缓存、Session、排行榜、计数器、分布式锁、限流器。选 MySQL 的场景:数据必须持久化且保证一致性、需要复杂关联查询和事务、业务模型稳定。典型用例:用户系统、订单系统、支付系统。选 MongoDB 的场景:数据结构频繁变化、单文档较大且需要灵活查询、需要水平扩展。典型用例:CMS、日志分析、IoT、用户画像。选 Memcached 的场景:基本没有了。除非维护旧系统,否则没有理由新项目选 Memcached。生产架构中的组合模式实际项目中几乎不会只用一种存储,常见的组合方案:Redis + MySQL(最经典):Redis 缓存热点数据,MySQL 持久化存储。注意做好双写一致性、缓存穿透/击穿/雪崩的防护。Redis + MySQL + MongoDB:Redis 做缓存,MySQL 存核心业务数据,MongoDB 存非结构化数据(日志、配置、内容)。Redis + MySQL + Elasticsearch:Redis 缓存,MySQL 存储,ES 负责全文检索和复杂搜索。无论哪种组合,核心原则是:每种存储只做它最擅长的事,不要让 MySQL 做缓存,也不要让 Redis 做持久化。追问:缓存与数据库的一致性如何保证?这是上述选型之后必然的追问,也是面试高频考点。先更新数据库,再删缓存 是最常用的策略。为什么是删缓存而不是更新缓存?因为更新可能涉及复杂计算,且并发场景下容易产生脏数据。一致性保证的三个层级:弱一致性:先更新DB再删缓存,容忍短暂不一致,适合大多数业务最终一致性:通过消息队列或 Canal 监听 binlog 异步更新缓存,保证最终一致强一致性:读写都走数据库,或使用分布式锁,牺牲性能换一致性,仅金融等场景使用面试时回答到第二层即可,重点是说清楚各方案的 trade-off。
服务端阅读 05月28日 07:16

Puppeteer 无头模式和有头模式有什么区别?

Puppeteer 的无头模式(Headless)和有头模式(Headful)是两种浏览器运行方式,核心差异在于是否渲染图形界面,这直接决定了它们的性能表现、调试能力和适用场景。核心区别无头模式下浏览器不创建可视化窗口,所有页面渲染和脚本执行在内存中完成;有头模式则启动完整的 Chrome GUI 窗口,每一步操作都可以肉眼观察。这个看似简单的差异会引发一系列连锁影响:资源消耗:无头模式省去了 GUI 渲染的开销,内存占用通常低 30%-50%,启动速度快 20% 左右User Agent 差异:旧版无头模式的 UA 包含 HeadlessChrome 标识,网站可据此识别并拒绝请求;有头模式的 UA 与普通 Chrome 完全一致渲染一致性:部分网站在无头模式下的渲染结果与有头模式不同,原因包括 GPU 加速差异、字体渲染路径不同、视口默认值不一致等反爬检测:无头模式缺少 navigator.plugins、window.chrome 等浏览器特征,更容易被反爬系统检测三种无头模式的演进Puppeteer 的无头模式并非一成不变,Chrome 的迭代带来了三种变体:旧版无头模式(headless: true)默认值,基于独立的 HeadlessChrome 实现,与正常 Chrome 共享极少代码。问题在于它的行为与真实浏览器差异较大,容易被网站检测。const browser = await puppeteer.launch({ headless: true});新版无头模式(headless: "new")Chrome 112+ 引入,使用与有头模式完全相同的 Chrome 代码库,仅跳过可视化输出。渲染结果与有头模式几乎一致,推荐在新项目中优先使用。const browser = await puppeteer.launch({ headless: "new"});chrome-headless-shell(headless: "shell")Puppeteer 21+ 提供,是专为自动化设计的精简二进制文件,体积更小、启动更快,但不支持扩展和部分 Chrome 特性,适合纯服务端批处理场景。const browser = await puppeteer.launch({ headless: "shell"});有头模式的使用方式有头模式需要显式关闭 headless,同时可以配合 DevTools 和慢放模式辅助调试:const browser = await puppeteer.launch({ headless: false, devtools: true, // 自动打开开发者工具 slowMo: 250 // 每步操作延迟 250ms,便于观察});关键配置项:slowMo 让操作可追踪,devtools 提供完整调试面板,defaultViewport 可设置视口大小。性能对比| 指标 | 旧版 headless | 新版 headless | headless shell | 有头模式 ||------|-------------|-------------|--------------|--------|| 内存占用 | 低 | 中 | 最低 | 高 || 启动速度 | 快 | 中 | 最快 | 慢 || 渲染一致性 | 差 | 好 | 中 | 基准 || 反检测能力 | 弱 | 较强 | 弱 | 强 || 扩展支持 | 不支持 | 支持 | 不支持 | 支持 |各模式适用场景无头模式适用于:CI/CD 流水线中的自动化测试——服务器通常没有显示器大规模网页抓取——资源占用低,可并发更多实例定时任务和批量处理——截图、PDF 生成、数据采集性能基准测试——减少 GUI 对测试结果的干扰有头模式适用于:脚本开发调试阶段——实时观察页面行为,快速定位问题复杂交互场景调试——如动画、拖拽、弹窗等需要视觉确认的操作反爬对抗——部分网站检测到无头特征后拒绝服务,有头模式可以绕过教学演示——展示自动化流程的每一步无头模式被检测的常见原因及应对实际项目中,无头模式最常见的坑就是被网站识别。以下是被检测的主要原因和解决思路:User Agent 泄露:旧版 headless 的 UA 包含 HeadlessChrome,解决方法是手动覆盖:await page.setUserAgent( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");浏览器特征缺失:无头模式下 navigator.plugins 为空、navigator.languages 仅含 "en-US"、缺少 window.chrome 对象。可使用 puppeteer-extra-plugin-stealth 自动修补:const puppeteer = require("puppeteer-extra");const StealthPlugin = require("puppeteer-extra-plugin-stealth");puppeteer.use(StealthPlugin());const browser = await puppeteer.launch({ headless: "new" });WebGL 和 Canvas 指纹:无头模式下 GPU 加速不可用,Canvas 指纹与有头模式不同。新版 headless 模式已大幅改善此问题。最佳实践:优先使用新版无头模式(headless: "new")+ stealth 插件,绝大多数场景下可获得与有头模式一致的渲染和反检测效果。环境切换的工程实践在实际项目中,通常需要根据运行环境动态切换模式:const puppeteer = require("puppeteer");const isDev = process.env.NODE_ENV === "development";const browser = await puppeteer.launch({ headless: isDev ? false : "new", devtools: isDev, slowMo: isDev ? 100 : 0, args: isDev ? [] : ["--no-sandbox", "--disable-setuid-sandbox"]});开发环境用有头模式便于调试,生产环境用新版无头模式兼顾性能和一致性。--no-sandbox 参数在 Docker 等容器环境中通常必需,因为默认的沙箱机制需要特定内核权限。面试追问方向Puppeteer 新版无头模式与旧版的核心实现差异是什么?(共享 Chrome 代码库 vs 独立实现)如何让无头模式通过反爬检测?(stealth 插件 + 新版 headless + UA 覆盖)chrome-headless-shell 适合什么场景?有什么限制?(纯服务端批处理,不支持扩展)为什么同样的代码在无头和有头模式下渲染结果不同?(GPU 加速、字体渲染、视口默认值差异)
服务端阅读 05月28日 07:09

优化 Ollama 性能需要调整哪些参数?

优化 Ollama 性能该从哪些方面入手?Ollama 的性能瓶颈通常出现在三个环节:模型加载、推理计算和内存调度。优化思路可以归纳为——选对量化、调好参数、用满硬件。下面逐项展开。模型量化怎么选?量化是影响推理速度和显存占用最直接的参数。Ollama 支持多种量化级别,核心区别在于精度和速度的权衡:| 量化格式 | 模型体积 | 推理速度 | 精度损失 | 适用场景 ||---------|---------|---------|---------|---------|| Q4KM | 最小 | 最快 | 较明显 | 显存紧张、追求速度 || Q5KM | 适中 | 较快 | 轻微 | 多数场景的推荐选择 || Q8_0 | 较大 | 较慢 | 极小 | 对输出质量要求高 || F16 | 最大 | 最慢 | 无 | 调试或精度验证 |# 下载不同量化版本ollama pull llama3.1:8b-q4_k_mollama pull llama3.1:8b-q8_0实际测试中,8GB 显存的 RTX 4060 运行 Q4KM 量化的 7B 模型,速度可以从 3-4 tok/s 提升到 30-45 tok/s,差距在一个数量级。2026 年 Ollama 还新增了 NVFP4 量化支持,让本地推理结果能和云端生产环境保持一致。选择建议:先从 Q4KM 起步,如果输出质量不满意再升到 Q5KM,一般不需要更高量化。Modelfile 里哪些参数值得调?在 Modelfile 中通过 PARAMETER 指令可以精细控制推理行为:PARAMETER temperature 0.7 # 控制输出随机性,代码生成建议 0.1-0.3,对话 0.6-0.8PARAMETER top_p 0.9 # 核采样阈值,和 temperature 二选一调整即可PARAMETER top_k 40 # 候选 token 数,一般 20-60PARAMETER num_ctx 4096 # 上下文窗口,短对话设 2048 省显存PARAMETER repeat_penalty 1.1 # 重复惩罚,1.05-1.1 之间即可PARAMETER num_gpu 99 # GPU 卸载层数,不是 GPU 数量PARAMETER num_batch 512 # 批处理大小,吞吐优先可调到 1024-2048几个容易踩的坑:num_gpu 的含义是模型有多少层放到 GPU 上计算,不是 GPU 的数量。比如 Llama 2 7B 有 32 层,设成 32 就全部走 GPU,显存不够就减到 20 让部分层回退 CPU。num_ctx 直接影响显存占用,从 4096 减到 2048 可以省出相当可观的显存,短对话场景放心缩减。num_batch 调大能提高吞吐量,但也会吃更多显存,需要和 num_ctx 一起权衡。GPU 和显存怎么管?GPU 是 Ollama 性能的关键,显存不够用是最常见的问题。# 指定使用的 GPUexport CUDA_VISIBLE_DEVICES=0# 查看显存使用情况nvidia-smi --query-gpu=memory.free --format=csv,noheader,nounits# 启用 Flash Attention,显存占用可降低 30%-50%export OLLAMA_FLASH_ATTENTION=1显存不够时的降级策略:开启 OLLAMA_FLASH_ATTENTION=1,这是最优先的操作,几乎无副作用降低量化级别,从 Q80 换到 Q5KM 或 Q4K_M减少 num_ctx,短对话用 2048 甚至 1024减少 num_gpu,让部分层回退 CPU开启低显存模式,KV 缓存放到 CPU 内存,速度会下降但能跑起来苹果 M 系列芯片有独特优势——统一内存架构意味着显存等于内存。M2/M3 跑 7B-14B 模型性能接近入门级独立显卡,2026 年 Ollama 切换 MLX 引擎后 M5 芯片在 70B 以上模型上表现更是突出。并发请求怎么处理?Ollama 默认单并发,生产环境需要调整:# 环境变量方式export OLLAMA_NUM_PARALLEL=4 # 并行处理请求数export OLLAMA_MAX_LOADED_MODELS=3 # 最大同时加载模型数export OLLAMA_MAX_QUEUE=20 # 排队上限export OLLAMA_KEEP_ALIVE=30m # 模型保持加载时长,-1 表示永久也可以在 Modelfile 里设置:PARAMETER num_parallel 4OLLAMA_KEEP_ALIVE 很实用——默认 5 分钟没请求就卸载模型,设长一点能避免冷启动。频繁使用的服务建议设成 30m 或 -1。CPU 模式有什么优化空间?没有 GPU 的机器也能跑,但参数要针对性调整:PARAMETER num_thread 6 # CPU 线程数,建议设为物理核心数的 60%-80%PARAMETER num_batch 128 # 小批量减少内存压力PARAMETER num_ctx 2048 # 缩短上下文PARAMETER num_gpu 0 # 强制全走 CPU服务器级 CPU 可以开启 NUMA 优化:export OLLAMA_NUMA=1纯 CPU 模式跑 Q4 量化的 1B-7B 模型,速度大约 5-15 tok/s,能用但不快,适合低频调用场景。怎么监控和排查性能问题?# 查看当前运行的模型和资源占用ollama ps# 查看 Ollama 服务日志ollama logs# 实时监控 GPU 使用watch -n 1 nvidia-smi几个关键指标:首 token 延迟:反映模型加载和首次推理速度,正常应该在 2 秒以内推理速度:tok/s 数值,7B 模型 GPU 跑 30+ tok/s 算正常GPU 利用率:推理时应该在 80% 以上,否则说明有瓶颈显存占用:跑起来后应该接近满载,剩很多说明显存没利用好如果 GPU 利用率低,检查 num_gpu 是否设够了、num_batch 是否太小。如果频繁 OOM,按前面的降级策略逐项排查。不同硬件大致能跑什么模型?| 模型规模 | 最低显存/内存 | 推荐硬件 | 参考速度 ||---------|-------------|---------|---------|| 1B-3B | 无需 GPU | 8GB RAM | 30-60 tok/s (M2) || 7B-8B | 8GB | RTX 3080 / M2 Pro | 40-80 tok/s (GPU) || 13B-14B | 12GB | RTX 3080 Ti / M3 Max | 25-45 tok/s || 30B-34B | 24GB | RTX 4090 / M2 Ultra | 15-25 tok/s || 70B | 48GB | 双卡 4090 / M2 Ultra | 8-15 tok/s |注意这是 Q4 量化下的参考值,Q8 或 F16 所需显存会翻倍。选择模型时先看自己硬件的上限,再在量化级别上做取舍。
服务端阅读 05月28日 07:06

Ollama API 有哪些核心端点,怎样正确调用?

Ollama 启动后默认在 http://localhost:11434 提供 RESTful API,所有端点均以 JSON 交互。调用前先验证服务是否就绪:curl http://localhost:11434# 返回 "Ollama is running" 即表示正常文本生成与对话POST /api/generate —— 单轮文本生成向模型发送 prompt 并获取生成结果,适合一次性问答、代码补全等场景:curl http://localhost:11434/api/generate -d '{ "model": "qwen2.5:7b", "prompt": "用 Python 写一个快速排序", "stream": false}'关键参数:stream 控制是否流式返回(默认 true),options 可设置 temperature、num_predict 等模型超参。非流式响应在 response 字段返回完整文本,流式响应逐行返回 JSON 片段,每片包含 response 和 done 字段。POST /api/chat —— 多轮对话支持 messages 数组传入对话历史,是构建聊天应用的核心端点:curl http://localhost:11434/api/chat -d '{ "model": "qwen2.5:7b", "messages": [ {"role": "system", "content": "你是一个有帮助的助手"}, {"role": "user", "content": "解释一下 RAG 的原理"}, {"role": "assistant", "content": "RAG 是检索增强生成..."}, {"role": "user", "content": "它和微调有什么区别?"} ], "stream": false}'角色支持 system、user、assistant 三种,对话历史越长上下文越完整,但也会增加显存占用和响应延迟。模型管理GET /api/tags —— 列出本地模型返回已下载模型的名称、大小和修改时间:curl http://localhost:11434/api/tags响应中 models 数组的每个元素包含 name、size、modified_at 等字段。当你不确定本地有哪些模型可用时,先调这个接口。POST /api/show —— 查看模型详情获取模型的模版、系统提示词、参数等元信息:curl http://localhost:11434/api/show -d '{ "name": "qwen2.5:7b"}'返回的 template 字段是模型使用的提示词模板,parameters 是默认参数,system 是内置系统提示词。调试模型行为时很有用。POST /api/pull —— 下载模型从 Ollama 仓库拉取模型到本地:curl http://localhost:11434/api/pull -d '{ "name": "qwen2.5:7b"}'大模型下载耗时较长,默认以流式返回进度信息(status: "pulling ..."),完成后 status 变为 success。POST /api/copy —— 复制模型创建模型副本,常用于在微调前备份原始模型:curl http://localhost:11434/api/copy -d '{ "source": "qwen2.5:7b", "destination": "my-qwen-backup"}'DELETE /api/delete —— 删除模型删除本地模型释放磁盘空间:curl -X DELETE http://localhost:11434/api/delete -d '{ "name": "my-qwen-backup"}'注意此操作不可恢复,删除后需要重新 pull。向量与嵌入POST /api/embeddings —— 生成文本向量将文本转为向量表示,是构建 RAG 应用的关键端点:curl http://localhost:11434/api/embeddings -d '{ "model": "nomic-embed-text", "prompt": "什么是向量数据库?"}'返回的 embedding 字段是浮点数数组,维度取决于模型。嵌入模型推荐使用 nomic-embed-text 或 mxbai-embed-large,它们专为文本向量化设计,不要用对话模型生成嵌入。OpenAI 兼容端点POST /v1/chat/completionsOllama 提供与 OpenAI API 兼容的端点,方便现有项目零改造迁移:curl http://localhost:11434/v1/chat/completions -H "Content-Type: application/json" -d '{ "model": "qwen2.5:7b", "messages": [{"role": "user", "content": "你好"}], "stream": false}'同理 /v1/completions 和 /v1/embeddings 也已支持。将 OpenAI SDK 的 base_url 改为 http://localhost:11434/v1,api_key 填任意字符串即可使用。Python 集成除了直接 HTTP 调用,Ollama 官方提供了 Python SDK:pip install ollamaimport ollama# 单轮生成response = ollama.generate(model='qwen2.5:7b', prompt='用 Python 写一个快速排序')print(response['response'])# 多轮对话stream = ollama.chat( model='qwen2.5:7b', messages=[{'role': 'user', 'content': '解释一下 RAG 的原理'}], stream=True)for chunk in stream: print(chunk['message']['content'], end='', flush=True)常见问题Q: 调用 API 返回 connection refused?确认 Ollama 服务已启动。macOS 检查是否在运行,Linux 执行 systemctl status ollama。若 Ollama 监听在非默认端口,需设置环境变量 OLLAMA_HOST。Q: 生成速度很慢怎么办?检查是否使用了 GPU。执行 ollama run qwen2.5:7b 进入交互模式后输入 /show info 查看推理设备。若显示 CPU 而非 GPU,需安装对应的 CUDA 或 Metal 驱动。Q: 如何在局域网内其他机器访问?默认 Ollama 只监听 localhost。设置 OLLAMA_HOST=0.0.0.0 后重启服务即可开放局域网访问,但务必注意此操作无认证保护,不要暴露到公网。
服务端阅读 05月28日 07:06

Ollama 生产环境部署有哪些关键点和最佳实践?

核心回答Ollama 生产环境部署的核心在于三点:GPU 资源规划决定推理性能上限,反向代理与认证保障接口安全,监控与并发配置维持服务稳定。实际落地中,Ollama 更适合中小规模内网场景和企业私有化 Agent 部署,若面对高并发 API 服务需求,需评估 vLLM 等专业推理框架。硬件选型与系统要求GPU 是影响推理速度的决定性因素。生产环境推荐 NVIDIA T4 及以上,CUDA 11.0+ 驱动。Apple Silicon(M1/M2/M3/M4)凭借统一内存架构,单机也能跑 70B 参数模型。内存和存储的底线配置:8GB RAM:可跑 3B-7B 模型(qwen3:8b、llama3.3:8b)16GB RAM:适合 7B-14B 模型(deepseek-v3:16b、qwen-coder:14b)32GB RAM:14B-32B 模型(deepseek-r1:32b)64GB+ RAM:32B-70B 模型(llama3.3:70b)存储必须用 SSD,每个模型占用 4-20GB,机械硬盘会导致加载延迟不可接受。操作系统优先选 Linux(Ubuntu 20.04+),macOS 11+ 和 Windows 10/11 也支持。部署方式选择单机直接部署最简单的方式,适合开发测试和小规模内网使用:# 安装后直接启动,默认监听 0.0.0.0:11434ollama serve# 预加载模型,避免首次请求冷启动ollama run llama3.1 &注意:生产环境不要把 11434 端口直接暴露到公网,必须通过反向代理加认证。Docker 容器部署推荐的服务器部署方式,环境一致性好,方便版本管理:# 基础启动,挂载模型持久化存储docker run -d -v ollama:/root/.ollama -p 11434:11434 --gpus all ollama/ollama:0.18.0关键点:务必指定版本标签(如 0.18.0),不要用 latest。版本更新可能引入不兼容变更,生产环境需要可控的升级节奏。自定义 Modelfile 的 Dockerfile 示例:FROM ollama/ollama:0.18.0COPY my-model.gguf /root/.ollama/models/CMD ["ollama", "serve"]Kubernetes 集群部署企业级多实例场景选择 K8s,配合 GPU 调度和水平自动扩缩容。核心资源清单包括:Deployment(挂载 PVC 存模型)、Service、Ingress(TLS 终结),以及 nvidia.com/gpu 资源请求。安全配置:必须做的三件事1. 反向代理 + TLS + 认证永远不要裸奔暴露 Ollama API。用 Nginx 做 TLS 终结和 Basic Auth:upstream ollama_backend { server 192.168.1.10:11434; server 192.168.1.11:11434;}server { listen 443 ssl; server_name ollama.example.com; ssl_certificate /etc/nginx/ssl/cert.pem; ssl_certificate_key /etc/nginx/ssl/key.pem; location /api/ { auth_basic "Restricted"; auth_basic_user_file /etc/nginx/.htpasswd; proxy_pass http://ollama_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }}2. 网络层防火墙只允许应用服务器所在网段访问 11434 端口:ufw allow from 192.168.1.0/24 to any port 114343. 速率限制在 Nginx 层配置 limit_req,防止单个客户端耗尽推理资源。负载均衡与高可用多实例场景用 Nginx upstream 做负载均衡,推荐 least_conn 策略(推理请求耗时不均匀,轮询会导致热点):upstream ollama_backend { least_conn; server 192.168.1.10:11434; server 192.168.1.11:11434; server 192.168.1.12:11434;}健康检查通过 Ollama 自带的模型列表接口:curl http://localhost:11434/api/tags返回 200 表示实例正常,可纳入负载均衡池。性能调优并发配置Modelfile 中设置并行请求数:PARAMETER num_parallel 4或通过环境变量 OLLAMA_NUM_PARALLEL 全局控制。数值要根据 GPU 显存大小调整——设置过大会 OOM,过小则吞吐不足。T4 实测开启动态批处理后吞吐可提升约 40%。上下文窗口通过 OLLAMA_MAX_LOADED_MODELS 控制同时加载的模型数量,避免显存碎片化。Flash Attention启用 Flash Attention 可显著降低长上下文的显存占用和推理延迟,Ollama 默认支持,确保 CUDA 版本兼容即可。Keep-Alive 策略生产服务建议设置较长的 keep-alive,避免模型频繁卸载重载:export OLLAMA_KEEP_ALIVE=24h监控与运维核心监控指标GPU 利用率和显存占用(nvidia-smi 或 Prometheus DCGM Exporter)推理延迟 P50/P95/P99请求队列深度和超时率模型加载/卸载频率日志管理# 实时日志ollama logs -f# 调整日志级别export OLLAMA_LOG_LEVEL=debug生产环境建议接入 ELK 或 Loki 统一收集,配合 Grafana 做告警看板。备份与故障恢复# 备份整个 Ollama 数据目录(包含模型和配置)tar -czf ollama-backup-$(date +%Y%m%d).tar.gz ~/.ollama/# 恢复tar -xzf ollama-backup-20260528.tar.gz -C ~/注意:只备份 Modelfile 和自定义配置即可,官方模型可以重新 pull,不必浪费存储空间。Ollama vs vLLM:生产场景选型这是面试中常见的追问方向。两者定位不同:| 维度 | Ollama | vLLM ||------|--------|------|| 定位 | 开发者本地部署 | 生产级高并发推理 || 安装复杂度 | 极低(一条命令) | 较高(需编译配置) || 并发能力 | 单机有限 | PagedAttention 优化,高并发强 || OpenAI 兼容 | 原生支持 | 原生支持 || 适合场景 | 内网工具、私有 Agent | 对外 API、高 QPS 服务 |简单判断:内部工具、低 QPS 场景选 Ollama;对外服务、高并发需求选 vLLM。很多团队两者并用——Ollama 做开发测试,vLLM 做生产推理。追问:Ollama 离线部署怎么做?模型下载到本地后,Ollama 所有推理功能完全离线运行。气隙环境(air-gapped)的部署步骤:在联网机器上 ollama pull 所需模型打包 ~/.ollama/models/ 目录传输到目标机器,解压到相同路径启动 ollama serve 即可使用金融、医疗、军工等数据敏感行业,Ollama 的离线能力是选择它的核心理由之一。
服务端阅读 05月28日 07:03

如何在 Python、JavaScript 等编程语言中集成 Ollama?

Python 集成 OllamaPython 是 Ollama 生态中最成熟的集成语言,官方提供了 ollama 库,同时也兼容 LangChain、LlamaIndex 等主流框架。安装与基础调用pip install ollama最简单的文本生成方式:import ollamaresponse = ollama.generate(model='llama3.1', prompt='用一句话解释什么是递归')print(response['response'])generate 方法适用于单轮补全场景,直接传入 prompt 即可。多轮对话对话场景使用 chat 方法,通过 messages 数组维护上下文:import ollamamessages = [ {'role': 'user', 'content': 'Python 的 GIL 是什么?'}, {'role': 'assistant', 'content': 'GIL 是全局解释器锁,它确保同一时刻只有一个线程执行 Python 字节码。'}, {'role': 'user', 'content': '那多线程还有意义吗?'}]response = ollama.chat(model='llama3.1', messages=messages)print(response['message']['content'])每轮对话都需要把完整的消息历史传入,模型本身不保存状态。流式响应对于长文本生成,流式输出可以显著改善用户体验:import ollamastream = ollama.chat( model='llama3.1', messages=[{'role': 'user', 'content': '写一篇关于量子计算的科普文章'}], stream=True)for chunk in stream: print(chunk['message']['content'], end='', flush=True)流式模式下,chat 返回的是一个生成器,每次 yield 一个 token 片段。结构化输出当你需要模型返回 JSON 格式的数据时,可以用 format 参数约束输出:import ollamaresponse = ollama.chat( model='llama3.1', messages=[{'role': 'user', 'content': '列出三个编程语言及其主要用途'}], format='json')import jsondata = json.loads(response['message']['content'])print(data)这在构建 API 服务或数据抽取管线时特别有用。LangChain 集成如果项目已经使用 LangChain,Ollama 可以直接作为 LLM 后端:from langchain_community.llms import Ollamafrom langchain.prompts import ChatPromptTemplatefrom langchain.schema import StrOutputParserllm = Ollama(model="llama3.1")# 链式调用prompt = ChatPromptTemplate.from_template("用{style}风格解释{concept}")chain = prompt | llm | StrOutputParser()result = chain.invoke({"style": "通俗易懂", "concept": "分布式系统"})print(result)LangChain 的 Agent、RAG 等高级能力都可以基于 Ollama 后端运行,无需 OpenAI API Key。JavaScript / Node.js 集成 Ollama前端和 Node.js 项目通过 ollama npm 包接入,API 风格与 Python 库保持一致。安装与基础调用npm install ollamaimport ollama from 'ollama'// 文本生成const response = await ollama.generate({ model: 'llama3.1', prompt: '用一句话解释什么是闭包'})console.log(response.response)// 多轮对话const chat = await ollama.chat({ model: 'llama3.1', messages: [ { role: 'user', content: 'Node.js 适合做什么?' }, { role: 'assistant', content: 'Node.js 适合 I/O 密集型应用,比如 Web 服务、实时通信。' }, { role: 'user', content: '那 CPU 密集型任务呢?' } ]})console.log(chat.message.content)流式响应import ollama from 'ollama'const stream = await ollama.chat({ model: 'llama3.1', messages: [{ role: 'user', content: '详细解释 JavaScript 的事件循环' }], stream: true})for await (const chunk of stream) { process.stdout.write(chunk.message.content)}浏览器端调用Ollama 默认只监听 localhost,浏览器页面跨域调用需要配置 OLLAMA_ORIGINS 环境变量:OLLAMA_ORIGINS="http://localhost:3000" ollama serve配置后即可在前端代码中直接调用:const response = await fetch('http://localhost:11434/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'llama3.1', messages: [{ role: 'user', content: 'Hello' }] })})Go 集成 OllamaGo 生态目前没有官方封装库,直接调用 REST API 即可,Ollama 的 API 设计简洁,手动调用并不复杂。HTTP 调用示例package mainimport ( "bytes" "encoding/json" "fmt" "io" "net/http")func main() { payload := map[string]interface{}{ "model": "llama3.1", "prompt": "Go 语言的协程和线程有什么区别?", "stream": false, } body, _ := json.Marshal(payload) resp, err := http.Post( "http://localhost:11434/api/generate", "application/json", bytes.NewBuffer(body), ) if err != nil { fmt.Println("请求失败:", err) return } defer resp.Body.Close() data, _ := io.ReadAll(resp.Body) fmt.Println(string(data))}流式响应处理Go 处理流式响应需要逐行读取 NDJSON:resp, _ := http.Post( "http://localhost:11434/api/generate", "application/json", bytes.NewBuffer(streamPayload),)defer resp.Body.Close()decoder := json.NewDecoder(resp.Body)for { var chunk map[string]interface{} if err := decoder.Decode(&chunk); err != nil { break } if content, ok := chunk["response"].(string); ok { fmt.Print(content) }}REST API 通用集成任何支持 HTTP 请求的语言都能直接调用 Ollama REST API,核心端点只有两个:文本生成 /api/generatecurl http://localhost:11434/api/generate -d '{ "model": "llama3.1", "prompt": "解释什么是微服务架构", "stream": false}'对话 /api/chatcurl http://localhost:11434/api/chat -d '{ "model": "llama3.1", "messages": [ {"role": "user", "content": "Redis 和 Memcached 怎么选?"} ], "stream": false}'OpenAI 兼容接口Ollama 还提供了 OpenAI 兼容的 API 端点 /v1/chat/completions,已有 OpenAI SDK 的项目只需修改 base_url 即可切换:from openai import OpenAIclient = OpenAI( base_url="http://localhost:11434/v1", api_key="ollama" # 任意值,本地不校验)response = client.chat.completions.create( model="llama3.1", messages=[{"role": "user", "content": "用 Python 写一个快速排序"}])print(response.choices[0].message.content)这个兼容层意味着所有使用 OpenAI SDK 的项目(包括 LangChain 的 OpenAI 集成)几乎零改动就能迁移到本地模型。生产环境注意事项网络暴露:默认仅监听 localhost,对外服务需设置 OLLAMA_HOST=0.0.0.0:11434,同时做好鉴权模型管理:通过 ollama pull 预下载模型,避免首次请求时冷启动耗时过长并发处理:Ollama 会按请求顺序处理,高并发场景建议配合消息队列GPU 资源:显存不足时模型会自动卸载到内存,响应延迟会显著上升,可通过 OLLAMA_KEEP_ALIVE 调整模型驻留时间
服务端阅读 05月28日 07:02

什么是 Ollama 的 Modelfile,如何创建自定义模型?

Modelfile 是什么Modelfile 是 Ollama 用来定义和构建自定义模型的配置文件,语法设计参考了 Dockerfile——从基础模型出发,逐层叠加参数、系统提示词和模板指令,最终打包成一个可复用的模型镜像。一个最简的 Modelfile 只需要一行:FROM llama3.1这就等于直接复制了一份 llama3.1。真正的自定义发生在你往里面添加指令之后。核心指令逐一拆解FROM — 指定基础模型FROM 是唯一必填指令,支持三种来源:# 从 Ollama 仓库拉取FROM llama3.1:8b# 从本地 GGUF 文件构建FROM ./my-model-q4_k_m.gguf# 从 Safetensors 目录构建FROM ./my-safetensors-dir从本地 GGUF 导入是 Ollama 的一个重要能力,意味着你可以把 HuggingFace 上下载的任何 GGUF 量化模型直接跑起来,不需要额外转换。PARAMETER — 调整推理参数PARAMETER 控制模型运行时的行为,每行设置一个参数:PARAMETER temperature 0.7PARAMETER top_p 0.9PARAMETER num_ctx 4096PARAMETER repeat_penalty 1.1PARAMETER stop "<|end|>"几个关键参数的含义:temperature:控制输出随机性,0 附近更确定,1 以上更有创意。代码生成建议 0.1-0.3,创意写作建议 0.7-1.0num_ctx:上下文窗口大小,默认 2048,增大后会占用更多显存repeat_penalty:重复惩罚,大于 1 时抑制重复输出,1.1 是常用值stop:指定停止生成的标记SYSTEM — 设定系统提示词SYSTEM 定义模型的"人格"和行为边界,是自定义模型最常用的指令:SYSTEM You are an expert Python developer. Answer concisely with code examples.多行内容用三引号包裹:SYSTEM """You are a senior code reviewer. Follow these rules:1. Identify bugs and security issues first2. Suggest specific fixes with code3. Comment on performance if relevant"""系统提示词写得好,模型表现可以提升一个档次。核心技巧:明确角色、给出具体规则、限定输出格式。TEMPLATE — 自定义对话模板TEMPLATE 用 Go 模板语法定义对话格式,控制模型如何接收和生成文本:TEMPLATE """{{- range .Messages }}{{- if eq .Role "user" }}<|user|>{{ .Content }}<|end|>{{- else if eq .Role "assistant" }}<|assistant|}>{{ .Content }}<|end|>{{- end }}{{- end }}<|assistant|}>"""可用的模板变量:.Messages — 完整对话历史.Message.Role — 消息角色(user/assistant/system).Message.Content — 消息内容.Prompt — 仅当前用户输入.Response — 模型已生成的回复多数情况下基础模型自带的默认模板就够用,只有在你导入 GGUF 文件且模板不兼容时才需要手动指定。MESSAGE — 注入示例对话MESSAGE 用来给模型提供 few-shot 示例,引导输出风格和格式:MESSAGE user 请用一句话解释什么是递归MESSAGE assistant 递归是函数在自身定义中调用自身的编程技巧。和 SYSTEM 配合使用效果更好:SYSTEM 定规则,MESSAGE 给示范。其他指令# 添加许可证LICENSE MIT# 应用 LoRA 微调适配器ADAPTER ./my-lora-adapter.binADAPTER 指令可以将 LoRA 微调结果叠加到基础模型上,前提是适配器和基础模型必须匹配。实战:创建自定义模型场景一:基于已有模型定制最常见的需求——拿一个通用模型,改系统提示词和参数,变成专用助手:# 创建 Modelfilecat > Modelfile << 'EOF'FROM llama3.1SYSTEM You are a coding assistant specialized in Python. Provide concise answers with working code examples.PARAMETER temperature 0.3PARAMETER num_ctx 8192EOF# 构建模型ollama create my-coder -f Modelfile# 运行ollama run my-coder构建完成后,my-coder 就是一个独立的模型,可以像其他模型一样直接运行。场景二:导入 HuggingFace 上的 GGUF 模型从 HuggingFace 下载 GGUF 文件后,两步就能跑起来:# Modelfile 只需一行cat > Modelfile << 'EOF'FROM ./Qwen2.5-7B-Instruct-Q4_K_M.ggufEOFollama create my-qwen -f Modelfileollama run my-qwen如果对话格式不对,需要加 TEMPLATE 指令手动指定。场景三:量化创建模型Ollama 支持在创建时对 FP16 模型进行量化,节省磁盘空间:cat > Modelfile << 'EOF'FROM ./my-model-f16.ggufEOF# 指定量化等级ollama create my-model-q4 --quantize q4_K_M -f Modelfile常用量化等级:q4KM(平衡质量和大小)、q5KM(更高质量)、q8_0(接近原质量但体积大)。注意量化过程需要额外磁盘空间,7B 模型的 FP16 文件约 14GB,量化时需要同等大小的临时空间。常用运维命令# 查看模型的 Modelfile 配置ollama show --modelfile my-coder# 列出所有本地模型ollama list# 删除模型ollama rm my-coder# 更新模型(修改 Modelfile 后重新 create 同名即可覆盖)ollama create my-coder -f Modelfileollama show --modelfile 非常实用,可以反查任何已有模型的完整配置,包括默认模板和参数,是学习和调试的好帮手。常见问题构建报错 "must specify a FROM line":Modelfile 缺少 FROM 指令,确保第一行是 FROM。导入 GGUF 后对话格式混乱:基础模型自带模板和 GGUF 文件的模板冲突,需要手动添加 TEMPLATE 指令指定正确的对话格式。自定义模型回答风格没变化:检查 SYSTEM 指令是否生效——用 ollama show --modelfile model-name 确认配置是否正确写入。部分小模型对系统提示词的遵从度有限,换用更大参数的模型可能效果更好。量化后模型质量下降明显:尝试更高的量化等级,如从 q4KM 换成 q5KM 或 q8_0。
服务端阅读 05月28日 07:01

如何在 Ollama 中实现多模型并发运行和资源管理?

Ollama 支持两级并发:多模型同时加载和单模型并行处理请求。核心配置通过环境变量控制,资源管理由内存驱动,空闲模型自动卸载。两级并发机制Ollama 的并发能力分两层:多模型并发加载:系统内存充足时,多个模型可同时驻留在 RAM/VRAM 中,各自独立处理请求单模型并行推理:单个模型为多个请求分配并行 KV-cache 槽位,共享模型权重,吞吐量成倍提升两者可以组合使用:2 个模型各开 4 个并行槽位,总共可同时处理 8 个请求。核心环境变量配置三个关键变量控制并发行为:| 变量 | 作用 | 默认值 ||------|------|--------|| OLLAMA_NUM_PARALLEL | 单模型并行处理请求数 | 1(内存充足时自动设为 4) || OLLAMA_MAX_LOADED_MODELS | 同时加载的最大模型数 | 3 × GPU 数量,CPU 推理为 3 || OLLAMA_MAX_QUEUE | 繁忙时排队等待的最大请求数 | 512 |systemd 部署配置sudo mkdir -p /etc/systemd/system/ollama.service.dsudo tee /etc/systemd/system/ollama.service.d/parallel.conf > /dev/null <<EOF[Service]Environment="OLLAMA_NUM_PARALLEL=4"Environment="OLLAMA_MAX_LOADED_MODELS=3"Environment="OLLAMA_MAX_QUEUE=256"EOFsudo systemctl daemon-reload && sudo systemctl restart ollamaDocker 部署配置docker run -d --gpus=all --network=host -v ollama_data:/root/.ollama -e OLLAMA_NUM_PARALLEL=4 -e OLLAMA_MAX_LOADED_MODELS=3 -e OLLAMA_MAX_QUEUE=256 ollama/ollama内存驱动的资源管理Ollama 的资源管理核心原则:内存决定一切。加载与卸载策略新请求到达时,Ollama 检查可用内存是否足够加载目标模型:内存充足:模型加载到 RAM(CPU 推理)或 VRAM(GPU 推理),请求立即处理内存不足:请求进入队列,等待已加载模型空闲后被卸载释放空间空闲模型在超时后自动卸载,释放的资源按需分配给新模型并行推理的内存开销并行推理的 KV-cache 按并行数线性增长:实际内存需求 ≈ 模型权重 + OLLAMA_NUM_PARALLEL × OLLAMA_CONTEXT_LENGTH × 每token内存例如:2K 上下文开 4 并行 = 8K 上下文的 KV-cache 开销。设置并行数前务必评估可用 VRAM。GPU 约束GPU 推理时,新模型必须完整装入 VRAM 才能并发加载。如果 VRAM 装不下第二个模型,Ollama 会排队等待。Windows Radeon GPU 因 ROCm VRAM 上报限制,默认最多加载 1 个模型。查看运行状态# 查看当前加载的模型、大小和运行位置ollama ps输出示例:NAME ID SIZE PROCESSOR UNTILdeepseek-r1:32b abc123def456 19.2GB 100% GPU 4 minutes from nowqwen3:8b 789ghi012jkl 4.7GB 100% GPU 2 minutes from nowPROCESSOR 列显示模型运行在 GPU 还是 CPU 上,UNTIL 显示空闲超时时间。模型卸载与切换# 手动卸载模型释放内存ollama stop deepseek-r1:32b# 加载新模型(内存不足时自动排队等待)ollama run llama3.3:70b手动卸载适用于需要立即腾出空间加载更大模型的场景。Modelfile 中的并行参数Modelfile 的 PARAMETER num_parallel 与环境变量 OLLAMA_NUM_PARALLEL 不同:OLLAMA_NUM_PARALLEL:全局设置,作用于整个 Ollama 实例PARAMETER num_parallel:模型级设置,仅对该模型生效,可覆盖全局值FROM deepseek-r1:32b# 该模型允许 2 个并行请求PARAMETER num_parallel 2# 批处理大小PARAMETER num_batch 512如果不同模型需要不同的并行度,可在各自 Modelfile 中单独配置,无需运行多个 Ollama 实例。多 GPU 支持Ollama 支持两种多 GPU 使用方式:模型分片:超大模型(如 70B)自动拆分到多块 GPU 上运行多模型分配:不同模型分别加载到不同 GPU,各自独立推理多模型并发时,Ollama 根据 VRAM 情况自动将模型分配到不同 GPU。队列管理与过载保护OLLAMA_MAX_QUEUE 控制排队上限。队列满时,新请求返回 HTTP 503:{"error": "server busy, please try again"}客户端应实现指数退避重试:import ollamaimport timedef generate_with_retry(model, prompt, max_retries=3): for attempt in range(max_retries): try: return ollama.generate(model=model, prompt=prompt) except Exception as e: if "busy" in str(e) and attempt < max_retries - 1: time.sleep(2 ** attempt) continue raise低延迟 API 场景可将队列设为 64-128,快速失败而非长时间排队。生产环境最佳实践并行数设置:不要盲目拉满。4 并行已能覆盖多数场景,8 以上需要充足 VRAM 支撑。先用 ollama ps 观察实际内存占用再调整。模型选择:选择能完整装入 VRAM 的量化模型(Q4/Q5),比勉强加载大模型再降级到 CPU 推理更高效。8GB VRAM 适合 7B 模型,24GB VRAM 适合 32B 模型。监控指标:关注 GPU 利用率和请求延迟。并行推理下 GPU 利用率应稳定在 75% 以上,总耗时约为单请求的 2-4 倍而非线性增长,说明并行生效。多实例方案:OLLAMA_NUM_PARALLEL 是实例级全局设置。如果不同模型需要不同并行度且 Modelfile 配置无法满足,可在不同端口运行多个 Ollama 实例,各自独立配置。
服务端阅读 05月28日 07:00

如何在 Ollama 中使用流式响应(streaming)来实时生成文本?

Ollama 的流式响应(streaming)允许模型逐 token 返回结果,而不是等待全部生成完毕后一次性返回。这在聊天界面、代码补全等场景下几乎是必须的能力——用户能立刻看到内容逐步出现,感知延迟大幅降低。核心原理Ollama 的流式响应基于 HTTP 长连接 + NDJSON(Newline Delimited JSON)格式。服务端每生成一个 token 就立即写一行 JSON 到响应体,客户端逐行读取并解析。关键参数是请求中的 "stream": true——默认情况下 REST API 的流式是开启的,但在各语言 SDK 中通常默认关闭。每行 JSON 结构大致如下:{"model":"llama3.2","response":"Hello","done":false}{"model":"llama3.2","response":" world","done":false}{"model":"llama3.2","response":"","done":true,"total_duration":2500000000}当 done 为 true 时,这条消息还会携带 total_duration、eval_count 等统计信息,标志着本次生成结束。流式 vs 非流式:什么时候用哪个?| 维度 | 流式(stream: true) | 非流式(stream: false) ||------|---------------------|------------------------|| 首字延迟 | 极低,token 级返回 | 需等待全部生成完毕 || 内存占用 | 逐 token 处理,峰值低 | 需缓存完整响应 || 用户体验 | 类 ChatGPT 逐字出现 | 长时间白屏等待 || 实现复杂度 | 需处理增量拼接和中断逻辑 | 一次请求-响应,简单直接 || 适用场景 | 聊天、代码补全、实时交互 | 批量处理、后台任务、需完整结果后再操作 |面试要点:非流式适合服务端内部调用或需要完整结果后再做后处理的场景;流式适合任何用户直接交互的界面。generate 端点的流式调用/api/generate 是最基础的文本生成端点,适用于单轮 prompt 场景:curl http://localhost:11434/api/generate -d '{ "model": "llama3.2", "prompt": "用 Python 实现快速排序", "stream": true}'Python 中用 requests 库处理:import requestsimport jsonresponse = requests.post( 'http://localhost:11434/api/generate', json={'model': 'llama3.2', 'prompt': '用 Python 实现快速排序', 'stream': True}, stream=True)full_text = ''for line in response.iter_lines(): if line: chunk = json.loads(line) full_text += chunk.get('response', '') print(chunk['response'], end='', flush=True) if chunk.get('done'): print(f'总耗时: {chunk["total_duration"] / 1e9:.2f}s')chat 端点的流式调用/api/chat 支持多轮对话,是构建聊天应用的首选端点:import requestsimport jsonmessages = [ {'role': 'user', 'content': '解释一下 Transformer 的自注意力机制'}]response = requests.post( 'http://localhost:11434/api/chat', json={'model': 'llama3.2', 'messages': messages, 'stream': True}, stream=True)for line in response.iter_lines(): if line: chunk = json.loads(line) if 'message' in chunk and chunk['message']['content']: print(chunk['message']['content'], end='', flush=True)注意 chat 端点的响应结构不同——文本在 chunk['message']['content'] 而非 chunk['response'],这是面试中常见的混淆点。使用官方 Python SDKOllama 官方提供了 ollama Python 包,流式接口更简洁:import ollama# generate 流式for chunk in ollama.generate(model='llama3.2', prompt='写一首关于编程的诗', stream=True): print(chunk['response'], end='', flush=True)# chat 流式for chunk in ollama.chat( model='llama3.2', messages=[{'role': 'user', 'content': '解释递归'}], stream=True): if chunk['message']['content']: print(chunk['message']['content'], end='', flush=True)SDK 内部已经处理了连接管理和 NDJSON 解析,代码量显著减少。但在生产环境中,直接使用 requests 给你更多控制权——比如自定义超时、重试策略和连接池。JavaScript/TypeScript 实现Node.js 环境下使用 fetch API 处理流式:async function streamChat(prompt) { const response = await fetch('http://localhost:11434/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'llama3.2', messages: [{ role: 'user', content: prompt }], stream: true }) }); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split(''); buffer = lines.pop(); // 保留不完整的行 for (const line of lines) { if (!line.trim()) continue; const chunk = JSON.parse(line); if (chunk.message?.content) { process.stdout.write(chunk.message.content); } } }}这里用 buffer 拼接是因为网络传输可能把一个 JSON 行拆成多个 chunk,直接按行 split 会解析失败——这是前端处理流式响应时最常踩的坑。错误处理与重连生产环境中流式连接会因网络波动、服务重启等原因中断,必须有健壮的错误处理:import requestsimport jsonimport timedef stream_with_retry(url, payload, max_retries=3, timeout=30): for attempt in range(max_retries): try: response = requests.post( url, json=payload, stream=True, timeout=timeout ) response.raise_for_status() full_text = '' for line in response.iter_lines(): if line: chunk = json.loads(line) if chunk.get('done'): return full_text full_text += chunk.get('response', '') return full_text except (requests.ConnectionError, requests.Timeout) as e: print(f'连接失败 (尝试 {attempt + 1}/{max_retries}): {e}') time.sleep(2 ** attempt) # 指数退避 except json.JSONDecodeError as e: print(f'JSON 解析错误: {e}') continue raise ConnectionError(f'重试 {max_retries} 次后仍然失败')# 使用result = stream_with_retry( 'http://localhost:11434/api/generate', {'model': 'llama3.2', 'prompt': 'Hello', 'stream': True})三个关键点:指数退避避免雪崩、超时设置防止无限挂起、JSON 解析错误需要跳过坏行而非直接放弃。工具调用的流式支持从 Ollama v0.8.0 开始,工具调用(tool calling)也支持流式返回了。这意味着模型在生成内容的同时可以实时发起工具调用,不再需要等待完整响应:import ollamatools = [{ 'type': 'function', 'function': { 'name': 'get_weather', 'description': '获取指定城市的天气', 'parameters': { 'type': 'object', 'properties': { 'city': {'type': 'string', 'description': '城市名称'} }, 'required': ['city'] } }}]for chunk in ollama.chat( model='llama3.2', messages=[{'role': 'user', 'content': '北京今天天气怎么样?'}], tools=tools, stream=True): if chunk.get('message', {}).get('tool_calls'): for tool_call in chunk['message']['tool_calls']: print(f'调用工具: {tool_call["function"]["name"]}') print(f'参数: {tool_call["function"]["arguments"]}')收到工具调用后,你需要执行对应函数,将结果作为 tool 角色的消息追加到 messages 中,再次调用 chat 接口让模型继续生成。生产环境的几个坑连接池管理:Ollama 默认并发连接数有限(通常与模型并发数相关)。如果每个请求都新建连接,高并发下会频繁超时。用 requests.Session() 复用连接:session = requests.Session()def stream_chat(prompt): return session.post( 'http://localhost:11434/api/chat', json={'model': 'llama3.2', 'messages': [{'role': 'user', 'content': prompt}], 'stream': True}, stream=True, timeout=60 )取消生成:用户中途停止接收时,直接关闭连接即可。Ollama 服务端会检测到连接断开并停止生成,不会浪费算力。在 Python 中调用 response.close(),在 JS 中调用 reader.cancel()。上下文窗口溢出:长对话流式生成到一半突然返回 done: true 但内容截断,通常是上下文窗口超限。需要在发送请求前计算 token 数,必要时裁剪历史消息。多模型并发:Ollama 同时只能加载有限数量的模型到 GPU。如果频繁切换模型,会出现加载等待,表现为流式首字延迟骤增。生产环境建议固定使用一个模型,或通过 OLLAMA_NUM_PARALLEL 环境变量调整并发数。
服务端阅读 05月28日 07:00

Ollama 支持哪些大语言模型,如何选择合适的模型?

Ollama 支持的主要模型系列截至 2026 年,Ollama 模型库已支持超过 100 个大语言模型,覆盖主流开源模型家族。以下是按厂商分类的核心模型:Meta Llama 系列llama3.1 — 8B / 70B / 405B,通用对话基线模型llama3.2 — 1B / 3B,轻量级端侧模型llama3.3 — 70B,Meta 当前最强开源模型,推理能力接近 Llama 3.1 405B阿里通义千问系列qwen2.5 — 7B / 14B / 32B / 72B,中文理解能力突出,128K 上下文qwen2.5-coder — 7B / 32B,代码生成与调试首选qwen3 — 8B / 14B 等,强推理 + 工具调用能力,2026 年热门模型深度求索系列deepseek-r1 — 7B / 8B / 32B,链式思维推理模型,数学和逻辑推理表现优异deepseek-v3 — 大参数通用模型Google Gemma 系列gemma2 — 9B / 27B,轻量高效gemma3 — 4B / 12B / 27B,支持多模态(文本+图片输入)Mistral AI 系列mistral — 7B,经典轻量模型mixtral — 8x7B / 8x22B,MoE 架构,兼顾速度与质量代码与专用模型codellama — 7B / 13B / 34B,多语言代码生成devstral-small — 软件工程专用,适合中等硬件phi4-mini — 微软轻量模型,低资源环境可用嵌入模型mxbai-embed-large — 文本嵌入,适合 RAG 系统nomic-embed-text — 长文本嵌入如何选择合适的模型选择模型的核心逻辑是:先看硬件,再看场景,最后实测。按硬件配置选择硬件是硬约束。模型参数量越大,所需内存越多。一个反复在显存和系统内存间交换的模型,生成速度会慢到难以使用——宁可跑一个小模型跑得流畅,也不要勉强跑大模型。| 可用内存 | 推荐参数量 | 代表模型 ||---------|-----------|---------|| 8GB | 1B-7B | qwen3:4b、llama3.2:3b、phi4-mini || 16GB | 7B-14B | qwen2.5:7b、llama3.1:8b、gemma3:12b || 32GB | 14B-32B | qwen2.5-coder:32b、deepseek-r1:32b || 64GB+ | 70B | llama3.3:70b、qwen2.5:72b |Mac 用户注意:Mac 使用统一内存,16GB 机型建议预留 4-6GB 给系统,实际可跑模型控制在 9B 以下。按使用场景选择通用对话与日常问答首选 qwen2.5:7b(中文场景)或 llama3.1:8b(英文场景)中文理解、成语运用和文化常识方面,Qwen 系列在同参数量下明显优于 Llama代码生成与调试首选 qwen2.5-coder:7b(16GB 内存)或 qwen2.5-coder:32b(32GB 内存)DeepSeek Coder 和 CodeLlama 是备选推理与数学首选 deepseek-r1:7b(轻量)或 deepseek-r1:32b(高质量)DeepSeek R1 的链式思维推理在数学和逻辑题上表现突出多模态(图片理解)首选 gemma3:4b(低硬件)或 gemma3:27b(高硬件)qwen2.5-vl:7b 适合结构化图片分析RAG 检索增强文本生成用 qwen2.5:7b,嵌入用 mxbai-embed-large量化版本选择Ollama 默认使用 Q4KM 量化。如果内存紧张,可以用更激进的量化:# 默认 Q4_K_M 量化ollama run qwen2.5:7b# 更小的 Q4 量化,速度快、精度微降ollama run qwen2.5:7b-q4_0# Q8 量化,接近原始精度但内存翻倍ollama run qwen2.5:7b-q8_0量化等级越低,模型体积越小、推理越快,但精度下降。实际体验中 Q4KM 到 Q4_0 的精度差异不大,但内存占用可减少 15%-20%。实操:快速验证模型是否适合你# 拉取模型ollama pull qwen2.5:7b# 运行并测试ollama run qwen2.5:7b# 在对话中输入测试提示>>> 请写一篇关于春天的短文,200字左右观察生成速度:流畅如打字(>15字/秒)说明硬件匹配;明显卡顿则换更小参数的模型或更激进的量化。建议同时拉取 2-3 个候选模型,用相同的提示词对比效果,实测比看评测更靠谱。查看所有可用模型访问 Ollama 官方模型库 https://ollama.com/library 可浏览全部模型及变体。新模型持续更新,建议定期查看。
服务端阅读 05月28日 06:59

SSH 安全加固怎么做?生产服务器必改的 8 项配置与面试追问

SSH 安全加固是运维和后端面试中的高频考点,也是生产服务器上线前必须完成的配置。本文从实际生产场景出发,梳理 SSH 加固的核心配置项,每项给出"为什么做"和"怎么配",并在文末附上面试常见追问。修改默认端口:降低被扫描概率SSH 默认监听 22 端口,这是所有自动化扫描工具的首要目标。改成非标准端口后,扫描流量会大幅下降,日志噪音也明显减少。# /etc/ssh/sshd_configPort 2222修改端口后记得同步更新防火墙规则和 Fail2Ban 配置,否则改了端口反而把自己锁在外面。另外,改端口属于"降低攻击面"而非"增强安全性",不要因此放松其他加固措施。禁用 root 登录:权限最小化允许 root 直接 SSH 登录意味着一旦密钥或密码泄露,攻击者立即获得最高权限。正确的做法是用普通用户登录,再通过 sudo 提权。# /etc/ssh/sshd_configPermitRootLogin no部署前要确认普通用户的 sudo 权限已配置好,否则禁用 root 后将无法执行管理操作。强制公钥认证:干掉暴力破解密码认证最大的风险是暴力破解,即使设了复杂密码也难逃字典攻击。公钥认证用非对称加密,私钥不离开本地,攻击面极小。# /etc/ssh/sshd_configPasswordAuthentication noPubkeyAuthentication yes切换顺序很重要:先部署公钥到服务器,测试公钥登录成功,再禁用密码认证。如果反着来,你可能会丢掉唯一能登录的方式。限制登录用户:白名单比黑名单可靠不限制登录用户意味着系统上的所有账户(包括服务账户)都可以尝试 SSH 登录。用白名单方式只允许运维人员登录,是更安全的做法。# /etc/ssh/sshd_configAllowUsers deploy admin@10.0.0.0/8AllowGroups sshusersSSH 处理顺序是 DenyUsers → AllowUsers → DenyGroups → AllowGroups,建议用 AllowUsers 或 AllowGroups 做白名单,比 DenyUsers 黑名单更可控。启用多因素认证:密钥 + 动态口令密钥认证虽然安全,但如果私钥文件被盗,攻击者就能直接登录。多因素认证(MFA)要求同时持有密钥和动态口令,即使私钥泄露也无法单独使用。# /etc/ssh/sshd_configAuthenticationMethods publickey,keyboard-interactive:pam# 安装 Google Authenticator PAM 模块sudo apt-get install libpam-google-authenticator# 为用户生成 TOTP 密钥google-authenticator# /etc/pam.d/sshd 添加auth required pam_google_authenticator.so生产环境建议 MFA 仅对跳板机或入口机开启,内网机器可以只做密钥认证,平衡安全和效率。加密算法优化:淘汰弱算法OpenSSH 支持多种加密算法,其中一些已被证明存在安全缺陷(如 3DES、CBC 模式、SHA1)。只保留安全的算法可以防止降级攻击。# /etc/ssh/sshd_configCiphers aes256-gcm@openssh.com,chacha20-poly1305@openssh.com,aes256-ctrKexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group16-sha512MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com可以用 ssh -Q cipher 查看当前 OpenSSH 支持的算法列表,用 sshd -T | grep -i cipher 查看实际生效配置。推荐使用 ssh-audit 工具对服务器做算法安全审计。连接限制与超时:防暴力破解和会话劫持暴力破解靠大量尝试碰运气,限制认证次数和连接速率能有效遏制。空闲会话不断开则可能被他人利用,设置超时是必要的。# /etc/ssh/sshd_configMaxAuthTries 3MaxSessions 2MaxStartups 10:30:100LoginGraceTime 60ClientAliveInterval 300ClientAliveCountMax 0MaxStartups 10:30:100 的含义:未完成认证的连接超过 10 个时,新连接有 30% 概率被拒绝;超过 100 个则全部拒绝。ClientAliveCountMax 0 表示连续 0 次无响应即断开,配合 ClientAliveInterval 300 实现 5 分钟无操作自动断开。网络层防护:防火墙 + Fail2Ban + TCP WrapperSSH 加固不能只靠 sshd_config,网络层防护是第二道防线。三层配合使用效果最好。防火墙限制来源 IP:# iptables: 仅允许内网段访问iptables -A INPUT -p tcp --dport 2222 -s 10.0.0.0/8 -j ACCEPTiptables -A INPUT -p tcp --dport 2222 -j DROP# ufw: 更简洁的写法ufw allow from 10.0.0.0/8 to any port 2222Fail2Ban 自动封禁:# /etc/fail2ban/jail.local[sshd]enabled = trueport = 2222filter = sshdlogpath = /var/log/auth.logmaxretry = 3bantime = 3600findtime = 600TCP Wrapper 兜底:# /etc/hosts.allowsshd: 10.0.0.0/8# /etc/hosts.denysshd: ALL三者防护逻辑不同:防火墙在网络层过滤数据包,Fail2Ban 根据行为模式动态封禁,TCP Wrapper 在应用层做访问控制。不要只依赖其中一种。面试追问与回答Q: 只改端口能防住攻击吗?不能。改端口只是降低了被自动化工具发现的概率,端口扫描器仍然可以遍历所有端口找到 SSH 服务。改端口是辅助手段,核心安全还是要靠密钥认证、禁用 root、限制来源 IP 这些实质性加固。Q: 密钥认证就够安全了吗?什么场景还需要 MFA?密钥认证比密码安全得多,但私钥文件一旦泄露(比如开发机被入侵),攻击者就能直接登录服务器。以下场景必须上 MFA:面向公网暴露的跳板机、拥有高权限的核心服务器、合规要求(等保三级及以上)的场景。Q: ClientAliveInterval 和 LoginGraceTime 区别是什么?两者作用阶段完全不同。LoginGraceTime 控制的是"连接建立后多久没完成认证就断开",防的是半开连接占用资源;ClientAliveInterval 控制的是"认证成功后多久没操作就断开",防的是空闲会话被利用。生产环境建议前者设 60 秒,后者设 300 秒。Q: 怎么验证 SSH 加固配置是否生效?分三步验证:用 sshd -T 检查配置是否被正确加载;用 ssh-audit 工具扫描服务器,它会给出算法强度和配置问题的评分;用另一台机器尝试以 root 登录、密码登录、从非白名单 IP 登录,确认都被拒绝。修改配置前务必保留一个已登录的会话,防止配置错误把自己锁在外面。
服务端阅读 05月28日 06:59

如何安装 Ollama?常用命令和实操技巧有哪些?

各平台安装方式Ollama 支持在 macOS、Linux 和 Windows 三个主流平台上安装,同时也提供 Docker 部署方案。macOS 安装通过 Homebrew 一键安装:brew install ollama也可以从 Ollama 官网下载 macOS 版本的安装包,拖入 Applications 文件夹即可完成安装。安装后菜单栏会出现 Ollama 图标,点击可查看服务状态。Linux 安装使用官方一键安装脚本:curl -fsSL https://ollama.com/install.sh | sh如果遇到权限问题,可以加 sudo 执行。安装完成后 Ollama 会自动注册为 systemd 服务,开箱即用。Windows 安装两种方式可选:# 方式一:通过 winget 安装winget install Ollama.Ollama方式二是从官网下载 OllamaSetup.exe,双击运行安装程序。安装完成后 Ollama 默认开机自启,如需关闭可在任务管理器的启动应用中禁用。Docker 部署服务器环境下推荐使用 Docker 部署:docker run -d -v /home/ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama验证安装是否成功安装完成后执行以下命令确认:ollama --version也可以直接请求 API 端点检查服务状态:curl http://localhost:11434# 返回 "Ollama is running" 即表示服务正常核心命令速查模型管理运行模型(首次运行会自动下载):ollama run llama3.2ollama run mistralollama run codellama下载模型:ollama pull llama3.2ollama pull phi3:mini # 适合 8GB 内存的小型模型ollama pull llama3.1:70b # 需要 32GB+ 内存查看已安装模型:ollama list查看正在运行的模型:ollama ps删除模型:ollama rm llama3.2停止运行中的模型:ollama stop llama3.2查看模型详细信息:ollama show llama3.2复制模型:ollama cp llama3.2 my-llama服务管理启动 API 服务:ollama serve查看帮助信息:ollama -h自定义模型:ModelfileOllama 支持通过 Modelfile 创建自定义模型,类似于 Dockerfile 的工作方式:ollama create my-model -f ./ModelfileModelfile 示例:FROM llama3.2# 设置系统提示词SYSTEM You are a helpful coding assistant that always responds in Chinese.# 设置温度参数PARAMETER temperature 0.7# 设置模板TEMPLATE {{- .System }}{{- .Prompt }}创建后可以直接运行:ollama run my-modelAPI 调用方式Ollama 默认在 localhost:11434 提供 REST API 服务,支持两种主要接口。生成接口curl http://localhost:11434/api/generate -d '{ "model": "llama3.2", "prompt": "用 Python 实现快速排序", "stream": false}'对话接口curl http://localhost:11434/api/chat -d '{ "model": "llama3.2", "messages": [ {"role": "system", "content": "你是一个编程助手"}, {"role": "user", "content": "解释什么是 REST API"} ], "stream": false}'将 stream 设为 true 可以启用流式输出,适合前端逐字展示的场景。GPU 加速配置Ollama 默认会自动检测并使用可用的 GPU,无需额外配置。NVIDIA GPU:需要安装 NVIDIA 驱动,Ollama 自动调用 CUDA 加速,推荐 RTX 4060 及以上显卡AMD GPU:Linux 下自动支持 ROCm,macOS 使用 Metal 加速Apple Silicon:M 系列芯片通过 Metal 框架获得原生加速查看 GPU 使用情况:# Linux 下查看 NVIDIA GPU 状态nvidia-smi如果 GPU 未被识别,确认驱动已正确安装,并检查 OLLAMA_LLM_LIBRARY 环境变量是否被误设。常见问题排查端口被占用:默认端口 11434 冲突时,通过环境变量修改:export OLLAMA_HOST=0.0.0.0:11435ollama serve模型下载慢:配置代理加速:export OLLAMA_PROXY=http://your-proxy:port内存不足:优先选择量化后的小模型,如 phi3:mini 或 llama3.2,避免直接运行 70B 参数量的大模型。服务启动失败:Linux 下检查 systemd 服务状态:systemctl status ollamasystemctl restart ollama掌握以上安装方法和常用命令,就能在本地快速搭建大语言模型运行环境。建议从 ollama run llama3.2 开始体验,熟悉后再尝试 Modelfile 自定义和 API 集成。
服务端阅读 05月28日 06:58

REST API 的 CSRF 防护怎么做?认证方式不同策略完全不同

REST API 的 CSRF 防护和传统 Web 应用有明显差异。核心问题在于:REST API 可能被浏览器、移动应用、第三方服务等多种客户端调用,认证方式也不统一,防护策略必须因地制宜。REST API 为什么仍需关注 CSRF很多人认为 REST API 只用 JSON 就安全了,但事实并非如此。只要你的 API 满足以下条件,CSRF 风险就存在:使用 Cookie 进行身份认证(浏览器会自动携带)允许跨域请求(CORS 配置宽松)接受 application/json 以外的 Content-Type攻击者的思路很直接:构造一个恶意页面,让已登录用户在不知情的情况下向你的 API 发起请求。如果认证靠 Cookie,浏览器会自动带上,服务端无法区分请求来源。关键判断:你的认证方式决定防护策略Token 认证(JWT / OAuth)— 天然免疫Token 放在 Authorization 头中,浏览器不会自动发送,攻击者无法在跨站请求中携带。这是 REST API 最推荐的认证方式:// 服务端验证function authenticateJWT(req, res, next) { const token = req.headers.authorization?.replace("Bearer ", ""); if (!token) return res.status(401).json({ error: "未提供认证令牌" }); try { req.user = jwt.verify(token, JWT_SECRET); next(); } catch { res.status(401).json({ error: "令牌无效或已过期" }); }}需要注意:Token 存储在 localStorage 有 XSS 风险,存 HttpOnly Cookie 又会回到 CSRF 问题。生产中推荐短期 Token + 内存存储,刷新 Token 放 HttpOnly Cookie 并配合 CSRF 防护。Cookie 认证 — 必须防护Cookie 认证在前后端分离架构中仍然常见,尤其是需要与旧系统兼容时。这种场景下 CSRF 防护不可省略。Cookie 认证下的四种防护手段手段一:SameSite Cookie 属性最简单的第一道防线,设置 SameSite 限制跨站 Cookie 发送:app.use(session({ secret: process.env.SESSION_SECRET, cookie: { httpOnly: true, secure: true, sameSite: "strict" // 严格模式,完全禁止跨站发送 }}));strict 最安全但会影响从外部链接进入的体验,lax 是更常见的折中选择——允许 GET 请求的顶级导航携带 Cookie,阻止跨站 POST 请求。局限:并非所有浏览器都完整支持 SameSite(虽然主流浏览器已跟上),且移动端 WebView 行为不一致。不能作为唯一防线。手段二:CSRF Token + 自定义请求头这是最经典的防护方案,双重验证确保请求来源可信:// 生成并下发 CSRF Tokenapp.get("/api/csrf-token", (req, res) => { const token = crypto.randomBytes(32).toString("hex"); req.session.csrfToken = token; res.json({ csrfToken: token });});// 验证:通过自定义请求头传递 Tokenfunction csrfGuard(req, res, next) { // 安全方法跳过 if (["GET", "HEAD", "OPTIONS"].includes(req.method)) return next(); const headerToken = req.headers["x-csrf-token"]; if (!headerToken || headerToken !== req.session.csrfToken) { return res.status(403).json({ error: "CSRF 验证失败" }); } next();}前端配合:// 页面初始化时获取 Token,后续请求放在自定义头中const csrfToken = await fetch("/api/csrf-token").then(r => r.json());await fetch("/api/transfer", { method: "POST", headers: { "Content-Type": "application/json", "X-CSRF-Token": csrfToken.csrfToken }, body: JSON.stringify({ to: "user123", amount: 100 })});为什么用自定义头而非表单字段:跨域请求中浏览器不允许自定义头,攻击者无法通过表单或 iframe 伪造带 X-CSRF-Token 头的请求。这比把 Token 放在请求体中更可靠。手段三:Origin / Referer 头验证作为辅助手段,验证请求来源是否合法:function originGuard(req, res, next) { if (["GET", "HEAD", "OPTIONS"].includes(req.method)) return next(); const origin = req.headers.origin || req.headers.referer; const allowed = ["https://example.com", "https://app.example.com"]; if (!origin || !allowed.some(o => origin.startsWith(o))) { return res.status(403).json({ error: "请求来源非法" }); } next();}注意:Origin 头在某些旧浏览器中可能缺失,Referer 可能被隐私策略截断。只能作为补充,不能作为唯一防线。手段四:CORS 严格配置CORS 配置不当会直接暴露 API:app.use(cors({ origin: (origin, callback) => { const allowed = ["https://example.com", "https://app.example.com"]; // 移动端请求可能无 origin,需要其他认证方式保障 if (!origin || allowed.includes(origin)) { callback(null, true); } else { callback(new Error("CORS 拒绝")); } }, credentials: true, // 允许携带 Cookie methods: ["GET", "POST", "PUT", "DELETE"], allowedHeaders: ["Content-Type", "Authorization", "X-CSRF-Token"]}));关键点:credentials: true 时 origin 不能用 *,必须明确指定。Access-Control-Allow-Headers 必须包含 X-CSRF-Token,否则浏览器会阻止预检请求通过。混合客户端场景的分层防护实际项目中,API 往往同时服务浏览器和移动端,认证方式混合:function layeredAuth(req, res, next) { const authHeader = req.headers.authorization; if (authHeader?.startsWith("Bearer ")) { // JWT 认证 — 移动端/第三方,无需 CSRF 防护 try { req.user = jwt.verify(authHeader.slice(7), JWT_SECRET); return next(); } catch { return res.status(401).json({ error: "令牌无效" }); } } // Cookie 认证 — 浏览器端,需要 CSRF 防护 if (req.session?.userId) { if (["GET", "HEAD", "OPTIONS"].includes(req.method)) return next(); const csrfToken = req.headers["x-csrf-token"]; if (!csrfToken || csrfToken !== req.session.csrfToken) { return res.status(403).json({ error: "CSRF 验证失败" }); } req.user = { id: req.session.userId }; return next(); } res.status(401).json({ error: "需要身份认证" });}核心原则:根据认证方式决定是否启用 CSRF 防护,Token 认证跳过,Cookie 认证必检。面试回答要点面试中回答这个问题,抓住三个层次:先说判断依据:REST API 是否需要 CSRF 防护,取决于认证方式。Token 认证天然免疫,Cookie 认证必须防护。再说防护手段:SameSite Cookie 是基线,CSRF Token + 自定义请求头是核心,Origin 验证和 CORS 配置是补充,多层组合最可靠。最后说实践:混合客户端场景下,按认证方式分层处理,Token 走 JWT 验证、Cookie 走 CSRF 校验,不要一刀切。