二、Java性能优化--Nginx篇(二)

272 阅读11分钟

Nginx 高性能背后的原因,如何使用 Nginx 做数据缓存提升服务性能

在上一篇文章【Java性能优化--Nginx篇(一)】中,我们介绍了 Nginx 的基本概念和基础应用,并使用 Nginx 的反向代理对服务水平扩展,提升系统吞吐量,接下来让我们一起解析下 Nginx 高性能原因,以及使用 Nginx 进一步提升系统性能。

一、Nginx 高性能原因分析

在目前软件开发的技术选型中,通常会选择 Nginx 作为 Web 应用的容器以及做负载均衡的软性反向代理,主要原因就是因为 Nginx 的高性能,而 Nginx 高性能的背后主要是因为以下几个因素:

  • 使用 epoll 多路复用模型解决 IO 阻塞的回调通知问题;

  • 使用 master worker 进程模型实现了平滑的过渡、平滑的重启,结合 worker 单线程模型,epoll 多路复用机制完成高效操作;

  • 使用协程机制将用户的请求对应到线程中的某一个协程中,然后在协程中通过 epoll 多路复用机制来完成同步调用开发。

1. epoll 多路复用模型为啥是高性能的王者?

俗话说,有对比有差距,如果要弄清楚 epoll 为什么高效,需要对比其他几种模型

模型工作机制缺点优点
Java BIO一连接一线程阻塞式的线程模型,线程资源浪费,扩展性差编程简单
Linux Select单线程轮询文件描述符(FD)每次轮询都要遍历所有FD,效率低支持多路复用
Linux epoll内核事件驱动通知回调机制编程复杂支持大并发,性能高
(1)Java BIO 模型
✅工作原理:

服务端 ServerSocket.accept()接受链接,每当有一个客户端连接,就新建一个线程处理它,每个线程主要阻塞在 InputStream.read() 或 OutputStream.write()上,如图:

image.png

🧠 核心流程:
  1. 建立连接:客户端通过网卡与Linux服务器建立 socket 连接;
  2. 客户端发送请求:客户端程序通过网卡将数据发送出去,Linux 主机通过调用 read 系统调用,将这份请求数据从接收网卡中读取到服主机内的内核缓冲区;
  3. 服务端获取请求:Java服务端程序通过 read 系统调用,从Linux 内核缓冲区读取数据到用户缓冲区;
  4. 服务端业务处理:Java服务端在自己的用户空间中完成客户端的请求所对应的业务处理。
  5. 服务端返回数据:Java服务端完成数据处理后,构建好响应数据,从用户缓冲区通过 write 系统调用,将数据写入 Linux 内核缓冲区;
  6. 发送响应数据给客户端:Linux 系统将内核缓冲区中的数据通过 write 系统调用,将内核缓冲区的数据写入网卡,网卡通过底层。

总结:以上就是Java BIO 一次完整请求的过程,在此过程中线程会一直在阻塞中等待系统完成 read 和 write 操作,而 read 和 write 调用是由操作系统自动完成,用户程序无感知。

❌缺点:

假设有10个客户端连接进来,服务端就得创建10个线程,每个线程都阻塞等待客户端发消息,线程资源消耗大,每个连接都要一个线程,并发能力差,线程切换成本高,CPU开销大。


(2)Linux Select 模型(IO 多路复用)

与 Java BIO 相比,Select 模型 不用一个连接就使用一个线程处理了,而是使用一个线程监听多个 socket 连接,大大减少了CPU 上下文频繁切换带来的性能消耗,其工作原理如下所示。

✅工作原理
  • 用户态构建监听集合并调用 select()

  • 内核将用户态的集合拷贝到内核态,并轮询检测每个 fd(文件描述符) 的就绪状态。

  • 如果某个 fd 没有就绪,内核将当前线程挂到该 fd 的 等待队列 中,等待事件发生;

  • 当 fd 事件发生时,内核会唤醒等待队列中的线程,遍历就绪的 fd 进行相应的 I/O 操作。

❌缺点
  • 监听的数量太少,最多只能同时监听1024个套接字,不适宜于广域网开发;
    • 随着select的持续使用,会产生大量的拷贝开销和挂载开销;
  • 通过轮询遍历的方式,遍历所有文件描述符,筛选出哪些可读/可写,非常消耗CPU,会导致服务器吞吐能力会非常差。

如果要详细了解 select 模型可以参考此文章:select详解&如何使用select模型实现简易的一对多服务器

(3)Linux Epoll 模型(事件驱动 IO,多路复用升级版)
✅ 工作原理:
  • 使用 epoll_create() 创建 epoll 实例。
  • 使用 epoll_ctl() 注册 socket 和你关心的事件(比如可读)。
  • 使用 epoll_wait() 等待事件。
  • 在epoll 模型中,还是通过监听 socket 连接是否发生变化,并对每个连接设置回调函数,如果发生变化则直接唤醒并执行回调函数,而不需要遍历 fd 进行相应的 I/O 操作。
🧠 核心机制:
  • 内核和用户空间之间建立了一种 回调通知机制(event ready)
  • 支持 边缘触发(Edge Triggered)和 水平触发(Level Triggered)。
  • 支持超大连接数(上万、十万级别),适合高并发服务端
🔥 优点
  • 性能比 select 高很多。
  • 没有 FD 数量限制。
  • IO 事件是 回调通知式 的,不用每次全遍历。

2. master worker 进程模型的到底做了啥?

Nginx 中 使用的是 master worker 模型来做高性能的网络交互的

image.png

🧠 核心层
  • 创建 master 进程:在服务器中,我们使用的 OpenResty 来操作 Nginx 为例(如有疑问,请看我的 Nginx篇(一)),首先使用 sbin/nginx -c conf/nginx.conf 命令发送信号启动 Nginx ,这时 Nginx 会启动一个 master 进程,如下图所示;

  • 创建 worker 子进程:Nginx在创建好master 进程后,还会根据 nginx.conf 配置文件中的 worker_processes 配置来创建指定个数的 worker 进程,比如我这边使用的是默认的 worker_processes 1 配置,因此只创建了一个 worker 子进程;

    image.png

  • master 进程不处理任何 socket 连接:master 进程管理所有 worker 进程的内存空间,客户端发起连接时,由 worker 进程处理客户端连接,连接成功后,Nginx 则使用 epoll 多路复用机制来处理数据的读写;当 Nginx 配置文件有更新时,master 进程负责加载新配置,然后创建一个新的 worker 进程,将原有 worker 进程的socket 连接交给新的 worker 进程,达到无感刷新的效果。

3. 协程机制为啥高效?

  • 依附于线程的内存模型,切换开销小:协程是线程的一个内存模型,它的切换不需要消耗CPU的损耗,只需要消耗内存的切换开销;

  • 遇到阻塞及时归还执行权限,代码同步:若协程程序发生了阻塞,Nginx 对应的协程机制会自动剥夺协程的执行权限,交由另一个非阻塞的协程处理。

  • 无需加锁:由于协程是基于线程的执行方式,所有协程都是串行执行的过程无需加锁。

二、使用 Nginx 缓存,解决服务的查询压力,提升服务整体性能

要说到缓存,大家应该都会想到使用 Redis、Guava 以及 Memcached 等等,但是无论哪种缓存技术都是先通过Nginx的反向代理请求到Java服务端后在业务方法中去读取缓存的,这难免会有网络消耗,而根据金字塔原则,距离请求客户越近,消耗就越小,因此如果能在 Nginx 层对热点数据做缓存不失为一种提升性能的捷径,而在 Nginx 中就提供了这种技术,下面就来看下实际的应用。

1. Nginx Proxy Cache 缓存

💡 Nginx Proxy Cache 是啥?

Nginx Proxy Cache 是基于 Nginx 的反向代理缓存机制,可以把后端服务器的响应内容临时存到本地磁盘,下次同样的请求就直接从缓存读取,不用再请求后端

🧠 Nginx Proxy Cache 核心原理
  • 以 Nginx 反向代理为基础:通过反向代理将请求发送到后台服务器,并将请求内容缓存到Nginx 本地,下次请求时直接从缓存读取;

  • 依靠内存缓存文件地址(缓存 Key):在 Nginx Proxy Cache 缓存Key存储在内存中,缓存 Key 在内存中的内容就是缓存内容在文件中的地址;

  • 依靠文件系统存储索引级的文件 (缓存内容):Nginx Proxy Cache 缓存的内容以文件的方式存储在本地,下次请求进来,先看本地是否有这个文件来决定是否去 Nginx Proxy Cache 读取响应内容。

🔧 Nginx Proxy Cache 实际使用
  • 配置 Nginx 缓存文件信息
proxy_cache_path /usr/local/openresty/nginx/tmp_cache levels=1:2 keys_zone=tmp_cache:100m inactive=7d max_size=10g;
参数名作用举例说明
/usr/local/openresty/nginx/tmp_cache缓存文件存储路径缓存会落地在这个目录
levels=1:2子目录层级结构避免单目录文件太多,例如生成 a/bc/ 结构的子目录
keys_zone=tmp_cache:100m定义一个叫 tmp_cache 的缓存区域,使用 100MB 的共享内存保存缓存的 key 索引不存内容,只存 metadata(key、header 等)
inactive=7d如果 7 天没有访问该缓存,就清理它类似于 LRU 逻辑
max_size=10g缓存最大磁盘空间超过后会自动清理旧缓存(先淘汰最久未用的)
  • 配置指定路径的反向代理请求才使用 Nginx Proxy Cache
# 配置使用 Nginx Proxy Cache 的反向代理动态请求
location ^~ /item/getFromNginxProxyCache {
    proxy_pass http://backend_server;
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    # ============ 配置 proxy cache ============
    # 启用 `tmp_cache` 的缓存区
    proxy_cache tmp_cache;
    # 
    proxy_cache_key $uri$is_args$args;
    proxy_cache_valid 200 206 304 302 7d;
}

完整的 nginx.conf 配置文件如下:

user  nobody;
worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;

    upstream backend_server {
        server 自己的服务器IP:8090 weight=1;
    }

    proxy_cache_path /usr/local/openresty/nginx/tmp_cache levels=1:2 keys_zone=tmp_cache:100m inactive=7d max_size=10g;

    server {
        listen       80;
        server_name  localhost;

        location /resources/ {
            alias  /usr/local/openresty/nginx/html/resources/;
            index  index.html index.htm;
        }

        location ^~ /item/getFromNginxProxyCache {
            proxy_pass http://backend_server;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

            proxy_cache tmp_cache;
            proxy_cache_key $uri$is_args$args;
            proxy_cache_valid 200 206 304 302 7d;
        }

        location / {
            proxy_pass http://backend_server;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}

重新加载 nginx.conf 后,请求对应的接口,如图:

image.png

可以看到目录下多了一个 tmp_cache 目录,进入后可以看到有两个文件目录(levels=1:2配置的),进入目录后可以通过 cat 命令查看缓存内容,如图:

image.png

后续在请求这个接口,Nginx就直接从 proxy cache 缓存中获取数据直接返回,不会再请求到后端服务器。


2. Nginx Lua 实现请求缓存

上面我们使用 Nginx Proxy Cache 缓存来提高请求性能,但是 Nginx Proxy Cache 是基于文件系统,每次访问都要扫描磁盘,而扫描磁盘是极其耗时的行为,所以将缓存数据存放在内存中是非常有必要的,恰巧 OpenResty 可以通过 Lua 脚本的方式来操作内存空间。

🧠 在 OpenResty 操作 Nginx 时,在 Nginx 各阶段都有提供 lua 脚本的插载点,如下图:

image.png

🔵 Initialization Phase(初始化阶段)
1. init_by_lua*
  • 阶段:Nginx 主进程启动时(只执行一次)
  • 适合做的事
    • 加载全局配置
    • 加载共享数据(例如从 Redis/MySQL)
    • 初始化第三方库(如数据库连接池)
2. init_worker_by_lua*
  • 阶段:每个 Nginx worker 启动时执行(每个 worker 执行一次)
  • 适合做的事
    • 设置定时任务(ngx.timer.at
    • 初始化 worker 独有的数据结构
    • 与外部服务建立长连接(Redis、MySQL 等)
🔵 Rewrite/Access Phase(请求预处理阶段)

请求进来之后,Secure Request 判断是否是 HTTPS 请求,如果是 HTTPS 请求,会执行 ssl_certificate_by_lua* 用来自定义证书获取逻辑,然后进入请求预处理阶段,此阶段在每个客户端请求到达时触发,主要用于处理 URL 重写、权限控制、路由分发等,这个阶段的lua挂载点有以下几个:

1. ssl_certificate_by_lua*
  • 阶段:TLS 握手阶段
  • 适合做的事
    • 动态生成或选择 SSL 证书
    • 支持 SNI(Server Name Indication)动态切换证书
    • 结合 Vault 等系统实现证书热加载
2. set_by_lua*
  • 阶段:变量设置阶段
  • 适合做的事
    • 动态设置变量值(如 $backend
    • 计算和赋值用于后续 rewrite 或 proxy_pass 的变量
3. rewrite_by_lua*
  • 阶段:重写 URL 规则阶段
  • 适合做的事
    • 动态路由重写
    • 添加/修改请求头、URL、参数
    • 权限预校验(不建议放太复杂逻辑)
4. access_by_lua*
  • 阶段:访问控制阶段
  • 适合做的事
    • 鉴权、令牌验证、IP 白名单/黑名单
    • 与第三方系统联动进行访问校验
    • 拒绝不合法请求 (ngx.exit(403))
🔵 Content Phase(内容生成阶段)

Content generated by?判断是由谁来生成响应内容;如果是 Lua 模块自己生成响应,则走 content_by_lua* 用于构造和返回请求内容(如 JSON、HTML、文件等);如果是由其他指令(如 proxy_pass、fastcgi_pass)转发到上游服务器(Upstream)则走 balancer_by_lua* 自定义负载均衡逻辑,决定请求该转发到哪个 upstream 服务器,这个阶段的 lua 挂载点有以下几个:

1. content_by_lua*
  • 阶段:生成响应内容阶段(可替代传统 proxy_pass
  • 适合做的事
    • 完全自定义响应内容(如返回 JSON、HTML)
    • 实现 API 网关、Mock 接口、调试接口等
    • 与 Redis、MySQL、HTTP API 通信返回数据
2. balancer_by_lua*
  • 阶段:upstream 负载均衡决策阶段(仅用于 proxy_pass
  • 适合做的事
    • 实现自定义负载均衡算法(如权重、最小连接数)
    • 动态修改 upstream 地址(IP、端口)
    • 服务发现(结合 Nacos、Consul)
3. header_filter_by_lua*
  • 阶段:响应头处理阶段
  • 适合做的事
    • 修改响应头(如添加 CORS 头、Content-Type)
    • 设置缓存头、压缩相关头
    • 日志追踪 ID 添加
4. body_filter_by_lua*
  • 阶段:响应体过滤阶段(分块处理)
  • 适合做的事
    • 替换、压缩或加密响应体内容
    • 响应敏感信息脱敏
    • 添加 HTML 片段、水印等
🔵 Log Phase(日志处理阶段)
1. log_by_lua*
  • 阶段:请求处理完成后(日志阶段)
  • 适合做的事
    • 自定义访问日志格式
    • 记录请求耗时、状态码、业务日志
    • 将日志推送至日志系统(如 Kafka、ElasticSearch)
🔧 Nginx Lua 实际使用

在Nginx