Nginx调优-超详细

一、性能优化考虑点

当我需要进行性能优化时,说明我们服务器无法满足日益增长的业务。性能优化是一个比较大的课题,需要从以下几个方面进行探讨。

  • 当前系统结构瓶颈
  • 了解业务模式 原理、性能和安全

二、当前系统结构瓶颈

首先需要了解的是当前系统瓶颈,用的是什么,跑的什么业务。里面的服务是什么样子,每个服务最大支持多少并发。比如针对Nginx而言,我们处理静态资源最高的并发是多大。

  • 可以通过查看当前cpu负荷,内存使用率,进程使用率来做简单判断。还可以通过操作系统的一些工具来判断当前系统性能瓶颈,如分析对应日志,查看请求数量。
  • 也可以通过nginx vts模块来查看对应的连接数,总握手次数,总请求数。以对上线进行压力测试,来了解当前的系统的性能,并发数,做好性能评估。

三、了解业务模式

虽然我们是在做性能优化,但还是要熟悉业务,最终目的都是为了业务服务的。我们要了解每一个接口业务类型是什么样的业务,

  • 比如电商抢购模式,这种情况配是流量会很小,但是到了抢购时间,流量一下子就会猛涨。也要了解系统层级结构,每一层中间层做的是代理还是动静分离,还是后台进行直接服务。需要我们对业务接入层和系统层次要有一个梳理。

四、master-worker机制

master-worker工作原理

master-worker工作原理

一个master(进程)管理多个worker(进程)

  • 一个 master (进程) 管理多个 worker (进程)

master-worker机制

  • 争抢机制示意图
    在这里插入图片描述

  • 一个master Process管理多个worker process,也就是说Nginx采用的是多进程结构,而不是多线程结构

  • 当client发出请求(任务)时,master Process会通知管理的worker process

  • worker process开始争抢任务,争抢到的worker process会开启连接,完成任务

  • 每个worker都是一个独立的进程,每个进程里只有一个主线程

  • Nginx采用了IO多路复用机制(需要在Linux环境),使用IO多路复用机制,是Nginx在使用为数不多的worker process就可以实现高并发的关键

  • master-worker机制示意图
    在这里插入图片描述

master-worker模式

  • Nginx 在启动后会有一个 master 进程和多个相互独立的 worker 进程
  • Master 进程接收来自外界的信号,向各worker进程发送信号,每个进程都有可能来处理这个连接
  • Master进程能监控Worker进程的运行状态,当 worker 进程退出后(异常情况下),会自动启动新的worker进程

accept_mutex解决“惊群现象”

  • 所有子进程都继承了父进程sockfd,当连接进来时,所有子进程都将收到通知并 “争着” 与它建立连接,这就叫 “惊群现象”
  • 大量的进程被激活又挂起,只有一个进程可以accept()到这个连接,会消耗系统资源
  • Nginx提供了一个accept_mutex ,这是一个加在accept上的一把共享锁。即每个 worker 进程在执行 accept 之前都需要先获取锁,获取不到就放弃执行 accept() 有了这把锁之后,同一时刻,就只会有一个进程去 accpet(),就不会有惊群问题了
  • 当一个worker进程在 accept() 这个连接之后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接,完成一个完整的请求
  • 一个请求,完全由 worker 进程来处理,而且只能在一个 worker 进程中处理

用多进程结构而不用多线程结构的好处

  • 节省锁带来的开销,每个 worker 进程都是独立的进程,不共享资源,不需要加锁。在编程以及问题查找上,也会方便很多
  • 独立进程,减少风险。采用独立的进程,可以让互相之间不会影响,一个进程退出后,其它进程还在工作,服务不会中断,master 进程则很快重新启动新的 worker 进程

实现高并发的秘密-IO多路复用

  • 对于 Nginx 来讲,一个进程只有一个主线程,那么它是怎么实现高并发的呢?
  • 采用了 IO 多路复用的原理,通过异步非阻塞的事件处理机制,epoll模型,实现了轻量级和高并发
  • nginx 是如何具体实现的呢?
    举例来说:每进来一个request,会有一个 worker 进程去处理。但不是全程的处理,处理到什么程度呢?
    处理到可能发生阻塞的地方,比如向上游 (后端) 服务器转发 request,并等待请求返回
    那么,这个处理的 worker 不会这么傻等着,他会在发送完请求后,注册一个事件:“如果 upstream 返回了,告诉我一声,我再接着干”。于是他就休息去了
    此时,如果再有 request 进来,他就可以很快再按这种方式处理
    而一旦上游服务器返回了,就会触发这个事件,worker 才会来接手,这个 request 才会接着往下走
    由于 web server 的工作性质决定了每个 request 的大部份生命都是在网络传输中,实际上花费在 server 机器上的时间片不多,这就是几个进程就能解决高并发的秘密所在

Nginx的master-worker工作机制的优势

  • 支持 nginx -s reload 热部署,这个特征在前面我们使用过
  • 对于每个 worker 进程来说,独立的进程,不需要加锁,所以省掉了锁带来的开销,同时在编程以及问题查找时,也会方便很多
  • 每个 worker 都是一个独立的进程,但每个进程里只有一个主线程,通过异步非阻塞的方式 / IO 多路复用来处理请求,即使是高并发请求也能应对
  • 采用独立的进程,互相之间不会影响,一个worker进程退出后,其它 worker 进程还在工作,服务不会中断,master进程则很快启动新的worker进程
  • 一个 worker 分配一个CPU,那么worker的线程可以把一个cpu的性能发挥到极致

五、系统与Nginx性能优化

大家对相关的系统瓶颈及现状有了一定的了解之后,就可以根据邮箱性能方面做一个全体的优化。

  • 网络(网络流量、是否有丢包、网络的稳定性都会邮箱用户请求)
  • 系统(系统负载、饱和、内存使用率、系统的稳定性、硬盘磁盘是否损坏)
  • 服务(连接优化、内核性能优化、http服务请求优化都可以在nginx中根据业务来进行设置)
  • 程序(接口性能、处理请求速度、每个程序的执行效率)
  • 数据库、底层服务

文件句柄

linux/unix,一切皆文件,每一次用户发起请求就会生成一个文件句柄,文件句柄可以理解为就是一个索引,所有文件句柄就会随着请求量的增多,而进程调用的频率增加,文件句柄的产生就会越多,系统对文件句柄默认的限制是1024个,对于nginx来说非常小了,需要改大一点

ulimit -n #可以查看系统设置的最大文件句柄

在这里插入图片描述

  1. 设置方式
    系统全局性修改
    用户局部性修改
    进程局部性修改
  2. 系统全局性修改和用户局部性修改
vim /etc/security/limits.conf

在这里插入图片描述

soft:软控制,到达设定值后,操作系统不会采取措施,只是发提醒
hard:硬控制,到达设定值后,操作系统会采取机制对当前进行进行限制,这个时候请求就会受到影响
root:这里代表root用户(系统全局性修改)
*:代表全局,即所有用户都受此限制(用户局部性修改)
nofile:指限制的是文件数的配置项。后面的数字即设定的值,一般设置10000左右

尤其在企业新装的系统,这个地方应该根据实际情况进行设置,可以设置全局的,也可以设置用户级别的。

su #刷新以下环境
ulimit -n #再次查看系统最大文件句柄

在这里插入图片描述
这里就改为了,我们对root用户设置的最大文件句柄65535

  1. 进程局部性修改
vim /usr/local/nginx/conf/nginx.conf

每个进程的最大文件打开数,所以最好与ulimit -n的值保持一致。

在这里插入图片描述
重启一下服务
在这里插入图片描述

cpu的亲和配置

cpu的亲和能偶使nginx对于不同的work工作进程绑定到不同的cpu上面去。就能够减少在work间不断切换cpu,把进程通常不会在处理器之间频繁迁移,进程迁移的频率小,来减少性能损耗。
每个 worker 的线程可以把一个 cpu 的性能发挥到极致。所以 worker 数和服务器的 cpu 数相等是最为适宜的。设少了会浪费 cpu,设多了会造成 cpu 频繁切换上下文带来的损耗

在这里插入图片描述

  1. 具体设置
    Nginx允许工作进程个数一般设置CPU的核心或者核心数*2.如果不了解cpu核数,可以使用top后按1看出来,也可以查看/proc/cpuinfo文件
cat /proc/cpuinfo | grep ^processor | wc -l #查看cpu核数

在这里插入图片描述

lscpu | grep "CPU(s)" #另一种查看cpu核数的方式

在这里插入图片描述
这里我是4核
随后进入nginx配置文件进行修改。需要知道的就是nginx 1.9版本之后,就帮我们自动绑定了cpu;
所以我们只需要打开cpu亲和就行

vim /usr/local/nginx/conf/nginx.conf 
user nginx;
worker_processes  4;
worker_cpu_affinity auto;
#或者 worker_cpu_affinity 0001 0010 0100 1000;
worker_rlimit_nofile 65535;
  • worker_rlimit_nofile更改worker进程的最大打开文件数限制。如果没设置的话,这个值为操作系统的限制。设置后你的操作系统和Nginx可以处理比ulimit -a”更多的文件,使用把这个值设高,这样nginx就不会有 “too many open files”问题了。

重启一下nginx,查看一下nginx worker进程绑定对应的cpu

ps -eo pid,args,psr | grep -v grep | grep nginx

在这里插入图片描述
这样就能看到nginx的子进程都绑定到了哪些cpu核上

设置worker数量,Nginx 默认没有开启利用多核cpu,可以通过增加worker_cpu_affinity配置参数来充分利用多核cpu的性能

#2 核 cpu,开启 2 个进程
worker_processes 2;
worker_cpu_affinity 01 10;

#2 核 cpu,开启 4 个进程,
worker_processes 4;
worker_cpu_affinity 01 10 01 10;

#4 核 cpu,开启 2 个进程,0101 表示开启第一个和第三个内核,1010 表示开启第二个和第四个内核;
worker_processes 2;
worker_cpu_affinity 0101 1010;

#4 个 cpu,开启 4 个进程
worker_processes 4;
worker_cpu_affinity 0001 0010 0100 1000;

#8 核 cpu,开启 8 个进程
worker_processes 8;
worker_cpu_affinity 00000001 00000010 00000100 00001000 00010000 00100000 01000000 10000000;
  • worker_cpu_affinity 理解
    在这里插入图片描述
    在这里插入图片描述

事件处理模型优化

nginx的连接处理机制在于不同的操作系统会采用不同的I/O模型,Linux下,nginx使用epoll的I/O多路复用模型,在freebsd(类unix操作系统)使用kqueue的I/O多路复用模型,在Solaris(unix系统的一个重要分支操作系统)使用/dev/pool方式的I/O多路复用模型,在windows使用的icop等等。要根据系统类型不同选择不同的事物处理模型,我们使用的是centos,因此讲nginx的事件处理模型调整为epoll。

进入配置文件再进行修改

events {
    worker_connections  10240;
    multi_accept on;  
    use epoll;
}
  • multi_accept 告诉nginx收到一个新连接通知后接受尽可能多的连接,默认是on,设置为on后,多个worker按串行方式来处理连接,也就是一个连接只有一个worker被唤醒,其他的处于休眠状态,设置为off后,多个worker按并行方式来处理连接,也就是一个连接会唤醒所有的worker,直到连接分配完毕,没有取得连接的继续休眠。当你的服务器连接数不多时,开启这个参数会让负载有一定的降低,但是当服务器的吞吐量很大时,为了效率,可以关闭这个参数。

设置work_connections连接数

work_connections表示每个worker(子进程)可以创建多少个连接,
默认:work_connections:1024
最大:work_connections:65535
同时要根据系统的最大打开文件数来调整

系统的最大打开文件数>= worker_connections*worker_process
根据系统的最大打开文件数来调整,worker_connections 进程连接数量要小于等于系统的最大打开文件数
worker_connections 进程连接数量真实数量= worker_connections * worker_process

查看系统的最大打开文件数:

ulimit -a | grep "open files"

这里我自己改为10240(这个参数与服务器性能相关)

worker_connections  10240;
  • 根据最大连接数计算最大并发数
    如果是支持 http1.1 的浏览器每次访问要占两个连接,所以普通的静态访问最大并发数是:worker_connections * worker_processes /2
    而如果是HTTP作为反向代理来说,最大并发数量应该是 worker_connections * worker_processes/4
    因为作为反向代理服务器,每个并发会建立与客户端的连接和与后端服务的连接,会各占用两个连接

看一个示意图

在这里插入图片描述

keepalive timeout会话保持时间

  • 这里先解释一下什么是keepalive,keepalive就是建立一下长连接,这个timeout就是,当浏览器建立一个连接之后,这个连接最长能存在多少事件不给他关闭,这个65也不是说建立65s连接后就直接关闭,而是一个活跃事件,就是第一次请求和下一次请求都会刷新这个65s,再65s内没有再请求数据才会真的关闭这个连接,所以这个事件也不应该太长,60左右也差不多了。

keepalive_timeout 65;
keepalive 其他的一些配置
keepalive:像上游服务器的保留连接数
keepalive_disable:是指把某些浏览器禁用掉
keepalive_requests:这个表示当我们建立一个可复用长连接,在当前这一个连接里可以并发接收多少个请求,默认1000个
send_timeout:在向客户端发送数据时,建立好连接后,超过两次活动时间(服务器很长时间没有返回数据)那么就会将这个连接关掉
keepalive_time:一个tcp连接的总时长,超过这个时间要再请求连接一下

GZIP压缩性能优化

gzip  on;   #表示开启压缩功能
gzip_min_length 1k;  #表示允许压缩的页面最小字节数,页面字节数从header头的Content-Length中获取,默认值为0,表示不管页面多大都进行压缩,设置建议设大于1k。如果小于1k可能会越压越大。
gzip_buffers 4 32k; #压缩缓存区大小
gzip_http_version 1.1; #压缩版本
gzip_comp_level 6; #压缩比率,一般选择4-6,为了性能gzip_typs text/css text/xml application/javascript; #指>定压缩的类型 gzip_vary on; #vary header支持;
gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript text/css application/xml;
gzip_vary on ; #varyheader支持,改选项可以让前端的缓存服务器缓存经过GZIP压缩的页面,例如用Squid缓存经过nginx压缩的数据。
  • gzip_min_length 1k:设置允许压缩的页面最小字节数,页面字节数从header头的Content-Length中获取,默认值是0,不管页面多大都进行压缩,建议设置成大于1K,如果小与1K可能会越压越大。
  • gzip_buffers 4 32k:压缩缓冲区大小,表示申请4个单位为32K的内存作为压缩结果流缓存,默认值是申请与原始数据大小相同的内存空间来存储gzip压缩结果。
  • gzip_http_version 1.1:压缩版本,用于设置识别HTTP协议版本,默认是1.1,目前大部分浏览器已经支持GZIP解压,使用默认即可。
  • gzip_comp_level 6:压缩比例,用来指定GZIP压缩比,1压缩比最小,处理速度最快,9压缩比最大,传输速度快,但是处理慢,也比较消耗CPU资源。
  • gzip_types text/css text/xml application/javascript:用来指定压缩的类型,‘text/html’类型总是会被压缩。默认值: gzip_types text/html (默认不对js/css文件进行压缩)压缩类型,匹配MIME型进行压缩;

连接超时时间

其目的是保护服务器资源,cpu,内存没控制连接数,因为建立连接也是需要消耗资源的。

keepalive_timeout 60; #客户端连接保持会话超时时间,超过这个时间,服务器断开这个链接。
tcp_nodelay on; #也是防止网络阻塞,不过要包涵在keepalived参数才有效。
client_header_buffer_size 4k; #客户端请求头部的缓冲区大小,这个可以根据你的系统分页大小来设置,一般一个请求头的大小不会超过 1k,不过由于一般系统分页都要大于1k,所以这里设置为分页大小。分页大小可以用命令getconf PAGESIZE取得。
open_file_cache max=102400 inactive=20s; #这个将为打开文件指定缓存,默认是没有启用的,max指定缓存数量,建议和打开文件数一致,inactive 是指经过多长时间文件没被请求后删除缓存。
open_file_cache_valid 30s; #这个是指多长时间检查一次缓存的有效信息。
open_file_cache_min_uses 1; #open_file_cache指令中的inactive 参数时间内文件的最少使用次数,如果超过这个数字,文件描述符一直是在缓存中打开的,如上例,如果有一个文件在inactive 时间内一次没被使用,它将被移除。
client_header_timeout 15; #设置请求头的超时时间。我们也可以把这个设置低些,如果超过这个时间没有发送任何数据,nginx将返回request time out的错误。
client_body_timeout 15; #设置请求体的超时时间。我们也可以把这个设置低些,超过这个时间没有发送任何数据,和上面一样的错误提示。
reset_timedout_connection on; #告诉nginx关闭不响应的客户端连接。这将会释放那个客户端所占有的内存空间。
send_timeout 15; #响应客户端超时时间,这个超时时间仅限于两个活动之间的时间,如果超过这个时间,客户端没有任何活动,nginx关闭连接。
server_tokens off; #并不会让nginx执行的速度更快,但它可以关闭在错误页面中的nginx版本数字,这样对于安全性是有好处的。
client_max_body_size 10m; #上传文件大小限制。

proxy超时设置

proxy_connect_timeout 90; #后端服务器连接的超时时间,发起握手等候响应超时时间
proxy_send_timeout 90;  #后端服务器数据回传时间,就是在规定时间内后端服务器必须传完所有的数据
proxy_read_timeout 90;  #连接成功后,等候后端服务器响应时间,其实已经进入后端的排队之中等候处理(也可以说是后端服务器处理请求的时间,页面等待服务器响应时间)
proxy_buffers 4 32k;  #4是数量 32k是大小 该指令设置缓存区的大小和数量,从被代理的后端服务器取得的第一部分响应内容,会放置到这里,默认情况下,一个缓存区的大小等于内存页面大小,可能是4k也可能是8k取决于平台
proxy_busy_buffers_size 64k; #nginx在收到服务器数据后,会分配一部分缓冲区来用于向客户端发送数据,这个缓存区大小由proxy_busy_buffers_size决定的。大小通常是proxy_buffers单位大小的两倍,官网默认是8k/16k。
  • buffer工作原理
  1. 所有的proxy buffer参数都是作用到每一个请求的。每一个请求会按照参数的配置获得自己的buffer。proxy buffer不是global(全局配置),而是per request(在请求前执行的操作(如时间戳、签名))的

  2. proxy_buffering是为了开启response buffering of the proxied server(反向代理服务器响应数据的缓存),开启后proxy_buffers和proxy_busy_buffers_size参数才会起作用

  3. 无论proxy_buffer是否开启,proxy_buffer_size都是工作的,proxy_buffer_size所设置的buffer_size的作用是用来存储upstream端response的header。

  4. 在proxy_buffering 开启的情况下,Nginx将会尽可能的读取所有的upstream端传输的数据到buffer,直到proxy_buffers设置的所有buffer们被写满或者数据被读取完(EOF)。此时nginx开始向客户端传输数据,会同时传输这一整串buffer们。同时如果response的内容很大的话,Nginx会接收并把他们写入到temp_file里去。大小由proxy_max_temp_file_size控制。如果busy的buffer传输完了会从temp_file里面接着读数据,直到传输完毕。

  5. 一旦proxy_buffers设置的buffer被写入,直到buffer里面的数据被完整的传输完(传输到客户端),这个buffer将会一直处在busy状态,我们不能对这个buffer进行任何别的操作。所有处在busy状态的buffer size加起来不能超过proxy_busy_buffers_size,所以proxy_busy_buffers_size是用来控制同时传输到客户端的buffer数量的。

proxy_set_header

proxy_set_header用来设定被代理服务器接收到的header信息

语法:proxy_set_header field value;
field :为要更改的项目,也可以理解为变量的名字,比如host
value :为变量的值
如果不设置proxy_set_header,则默认host的值为proxy_pass后面跟的那个域名或者IP(一般写IP)
proxy_set_header X-Real-IP $remote_addr;

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
用来设置被代理端接收到的远程客户端IP,如果不设置,则header信息中并不会透传远程真实客户端的IP地址。

server {
        ...
        location /http/ {
                proxy_pass http://http_backend;
                proxy_http_version 1.1;                 # 对于http协议应该指定为1.1
                proxy_set_header Connection "";         # 清除"connection"头字段 , Connection: keep-alive
                proxy_set_header Host $http_host;		# 代理服务器匹配请求web01服务器的时候,默认加上Host头信息(域名)	
				proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;		# 追加客户端ip地址以及全链路ip地址到web服务器记录日志	
				proxy_next_upstream error timeout http_500 http_502 http_503 http_504; # 当Nginx虚拟服务池其中任意一台服务器返回错误码500,502,503,504等错误时,可以分配到下一台服务器程序继续处理,提高平台访问的成功率
				proxy_connect_timeout 60s;				# nginx代理与后端服务器连接超时时间(代理连接超时)
				proxy_read_timeout 60s;					# nginx代理等待后端服务器的响应时间(代理响应超时)
				proxy_send_timeout 60s;					# 后端服务器数据回传给nginx代理超时时间(回传数据给代理超时)
				
				proxy_buffering on;						# nginx会把后端返回的内容先放到缓冲区中,然后再返回给客户端,边收边传,不是全部接收再传给客户端
				proxy_buffers_size 4k;					# 设置nginx代理保存用户头信息的缓存区大小
				proxy_buffers 8 8k;						# 设置nginx代理缓冲区大小(8*8 = 64k)
        ...
        }
}

  • $proxy_host 自然是 proxy_pass后面跟着的host
    比如一个请求 https://zhidao.baidu.com/question/22908463.html
    Request.ServerVariables(“HTTP_HOST”) 可以获得HTTP_HOST请求标题:zhidao.baidu.com

  • $http_host始终等于HTTP_HOST请求标题

  • $host等于$http_host,小写并且没有端口号(如果存在),除非HTTP_HOST不存在或是空值。在这种情况下,$host等于server_name

  • $proxy_add_x_forwarded_for 从客户访问到到最后的后台服务所经理的所有节点的ip汇总221.221.1.100,10.30.30.109,…
    nginx的自带变量 $remote_addr 代表客户端的IP

  • $remote_addr代表客户端的IP,但它的值不是由客户端提供的,而是服务端根据客户端的ip指定的,当你的浏览器访问某个网站时,假设中间没有任何代理,那么网站的web服务器(Nginx,Apache等)就会把remote_addr设为你的机器IP,如果你用了某个代理,那么你的浏览器会先访问这个代理,然后再由这个代理转发到网站,这样web服务器就会把remote_addr设为这台代理机器的IP。

  • $remote_addr 只能获取到与服务器本身直连的上层请求ip,所以设置$remote_addr一般都是设置第一个代理上面;但是问题是,有时候是通过cdn访问过来的,那么后面web服务器获取到的,永远都是cdn的ip 而非真是用户ip,那么这个时候就要用到X-Forwarded-For 了,这个变量的意思,其实就像是链路反追踪,从客户的真实ip为起点,穿过多层级的proxy ,最终到达web 服务器,都会记录下来,所以在获取用户真实ip的时候,一般就可以设置成,proxy_set_header X-Forwarded-For

  • $proxy_add_x_forwarded_for; 这样就能获取所有的代理ip 客户ip $proxy_add_x_forwarded_for变量包含客户端请求头中的X-Forwarded-For与$remote_addr两部分,他们之间用逗号分开。

高效传输模式

sendfile on; #开启高效文件传输模式。
tcp_nopush on;#需要在sendfile开启模式才有效,防止网络阻塞,级级的减少网络报文段的数量。将响应头和正文的开始部分一起发送,而不是一个接一个的发送(也就是说数据包不会马上传出等到数据包最大时一次性传)
tcp_nodelay on; #只要有数据包产生,不管大小多少,就尽快传输

fastcgi调优

fastcgi_connect_timeout 600; #指定连接到后端FastCGI的超时时间。
fastcgi_send_timeout 600; #向FastCGI传送请求的超时时间。
fastcgi_read_timeout 600; #指定接收FastCGI应答的超时时间。
fastcgi_buffer_size 64k; #指定读取FastCGI应答第一部分需要用多大的缓冲区,默认的缓冲区大小为。fastcgi_buffers指令中的每块大小,可以将这个值设置更小。
fastcgi_buffers 4 64k; #指定本地需要用多少和多大的缓冲区来缓冲FastCGI的应答请求,如果一个php脚本所产生的页面大小为256KB,那么会分配4个64KB的缓冲区来缓存,如果页面大小大于256KB,那么大于256KB的部分会缓存到fastcgi_temp_path指定的路径中,但是这并不是好方法,因为内存中的数据处理速度要快于磁盘。一般这个值应该为站点中php脚本所产生的页面大小的中间值,如果站点大部分脚本所产生的页面大小为256KB,那么可以把这个值设置为“8 32K”、“4 64k”等。
fastcgi_busy_buffers_size 128k; #建议设置为fastcgi_buffers的两倍,繁忙时候的buffer。
fastcgi_temp_file_write_size 128k; #在写入fastcgi_temp_path时将用多大的数据块,默认值是fastcgi_buffers的两倍,该数值设置小时若负载上来时可能报502BadGateway。
fastcgi_temp_path /usr/local/nginx1.22/nginx_tmp; #缓存临时目录。
fastcgi_intercept_errors on; #这个指令指定是否传递4xx和5xx错误信息到客户端,或者允许nginx使用error_page处理错误信息。注:静态文件不存在会返回404页面,但是php页面则返回空白页!
fastcgi_cache_path /usr/local/nginx1.22/fastcgi_cache levels=1:2 keys_zone=cache_fastcgi:128m inactive=1d max_size=10g; #fastcgi_cache缓存目录,可以设置目录层级,比如1:2会生成16*256个子目录,cache_fastcgi是这个缓存空间的名字,cache是用多少内存(这样热门的内容nginx直接放内存,提高访问速度),inactive表示默认失效时间,如果缓存数据在失效时间内没有被访问,将被删除,max_size表示最多用多少硬盘空间。

此外还有

fastcgi_cache cache_fastcgi:#表示开启FastCGI缓存并为其指定一个名称。开启缓存非常有用,可以有效降低CPU的负载,并且防止502的错误放生。cache_fastcgi为proxy_cache_path指令创建的缓存区名称。
fastcgi_cache_valid 200 302 1h:#用来指定应答代码的缓存时间,实例中的值表示将200和302应答缓存一小时,要和fastcgi_cache配合使用。
fastcgi_cache_valid 301 1d:将301应答缓存一天。
fastcgi_cache_valid any 1m:将其他应答缓存为1分钟。
fastcgi_cache_min_uses 1:该指令用于设置经过多少次请求的相同URL将被缓存。
fastcgi_cache_key http://hosthosthostrequest_uri:该指令用来设置web缓存的Key值,nginx根据Key值md5哈希存储.一般根据host (域名) 、host(域名)、host(域名)、request_uri(请求的路径)等变量组合成proxy_cache_key 。
fastcgi_pass:指定FastCGI服务器监听端口与地址,可以是本机或者其它。

  • 总结
    nginx的缓存功能有:proxy_cache / fastcgi_cache
    proxy_cache的作用是缓存后端服务器的内容,可能是任何内容,包括静态的和动态。
    fastcgi_cache的作用是缓存fastcgi生成的内容,很多情况是php生成的动态的内容。
    proxy_cache缓存减少了nginx与后端通信的次数,节省了传输时间和后端宽带。
    fastcgi_cache缓存减少了nginx与php的通信的次数,更减轻了php和数据库(mysql)的压力。

expires缓存调优

缓存,主要针对于图片,css,js等元素更改机会比较少的情况下使用,特别是图片,占用带宽大,我们完全可以设置图片在浏览器本地缓存365d,css,js,html可以缓存个10来天,这样用户第一次打开加载慢一点,第二次,就非常快了!缓存的时候,我们需要将需要缓存的拓展名列出来, Expires缓存配置在server字段里面。

location ~* \.(ico|jpe?g|gif|png|jpeg|bmp|swf|flv)$
{
	expires 30d;  #过期时间为30天
	#log_not_found off;
	access_log off;
}
location ~* \.(js|css)$ 
{
	expires 7d;
	log_not_found off;
	access_log off;
}

注:log_not_found off;是否在error_log中记录不存在的错误。默认是。

Expires:是缓存到期的时间

访问限流(rate limiting)

我们构建网站是为了让用户访问它们,我们希望用于合法访问。所以不得不采取一些措施限制滥用访问的用户。这种滥用指的是从同一IP每秒到服务器请求的连接数。因为这可能是在同一时间内,世界各地的多台机器上的爬虫机器人多次尝试爬取网站的内容。

限速方法

限速分为很多种限速方法:
1)下载速度限速
2)单位时间内请求数限制
3)基于客户端的并发连接限速

nginx限速模块

Nginx官方版本限制IP的连接和并发分别有两个模块:
limit_req_zone 用来限制单位时间内的请求数,即速率限制,采用的漏桶算法 “leaky bucket”。
limit_req_conn 用来限制同一时间连接数,即并发限制。

应用场景
下载限速:限制现在速度及并发连接数,应用在下载服务器中,保护带宽及服务器的IO资源。
请求限速:限制单位时间内用户访问请求,防止恶意攻击,保护服务器及资源安全。

限速原理

  • 漏桶算法
    算法思想是:
    水(请求)从上方倒入水桶,从水桶下方流出(被处理);
    来不及流出的水存在水桶中(缓冲),以固定速率流出;
    水桶满后水溢出(丢弃)。
    这个算法的核心是:缓存请求、匀速处理、多余的请求直接丢弃。
    相比漏桶算法,令牌桶算法不同之处在于它不但有一只“桶”,还有个队列,这个桶是用来存放令牌的,队列才是用来存放请求的。

在这里插入图片描述

限速实现

1)单位时间内请求数限制

可用于防止DDoS攻击,或防止上游服务器同时被太多请求淹没。
16000个IP地址的状态信息,大约需要1MB

#基于IP对下载速率做限制  限制每秒处理1次请求,对突发超过5个以后的请求放入缓存区
http {
    limit_req_zone $binary_remote_addr zone=baism:1m rate=1r/s;
    server {
        location /abc {
            limit_req zone=baism burst=5 nodelay;
        }
}

参数解释

  • limit_req_zone $binary_remote_addr zone=baism:10m rate=1r/s;
    第一个参数:$binary_remote_addr ,表示通过remote_addr这个标识来做限制,“binary_”的目的是缩写内存占用量,是限制同一客户端ip地址。$binary_remote_addr为IPv4时占用4B,为IPv6时占用16B。
    第二个参数:zone=baism:10m,表示生成一个大小为10M,名字为baism的内存区域,用来存储访问的频次信息。
    第三个参数:rate=1r/s,表示允许相同标识的客户端的访问频次
    ,这里限制的是每秒1次,还可以有比如30r/m的,即每2秒才能处理一个请求。

  • limit_req zone=baism burst=5 nodelay;
    第一个参数:zone=baism ,设置使用哪个配置区域来做限制,与上面limit_req_zone 里的name对应。
    第二个参数:burst=5,重点说明一下这个配置,burst爆发的意思,这个配置的意思是设置一个大小为5的缓冲区,当有大量请求(爆发)过来时,超过了访问频次限制的请求可以先放到这个缓冲区内等待,但是这个等待区里的位置只有5个,超过的请求会直接报503的错误然后返回

    第三个参数:nodelay,如果设置,会在瞬时提供处理(burst+rate)个请求的能力
    ,请求超过(burst+rate)的时候就会直接返回503,永远不存在请求需要等待的情况(这里的rate的单位是:r/s)
    如果没有设置,则所有请求会依次等待排队。

burst详解及试验:
设定一个场景,一个nginx反向代理的后端服务单页请求数是30个,如果我们设置30r/s的话,那么这个burst设置就没有意义了,但是如果我们不想把rate设置成这么大,比如5r/s,也要完成这个一个页面30个请求的需求。这个时候burst就有作用了。(前后端一体化的服务,其静态资源单次刷新后,再访问时只会刷新动态请求)。
假设我们一个页面是30个请求,后面每秒都会刷新一个页面,但是新页面由于静态资源已经刷新,只有2-3个请求,因此我们把burst设置成200个(可以让用户连续强制刷新大约8次)。
当我们第一次访问时,由于rate是5个,只能处理5个,另外的25个会放到burst队列,虽然设置成nodelay会把这25个也处理了,但是队列中占用的25个位置,需要25/rate=5秒来清除,如果第二次请求是3个,那么我们实时处理了3个请求,并且rate剩余2个可以清空burst队列,这个时候,队列就是占用了23个位置。最后如果我们队列满了的话,那么就会出现503的请求。
验证效果:基本按上面的方式,连续每秒30个请求,即f5强制刷新,那么大约会再多少次后出现503的情况呢,答案大约是8次(200/25)。
burst=(每次请求数-请求限制数)*允许强制刷新的次数

例子演示:
首先我们配置了limit_req_zone,rate=10r/m,即每六秒才处理一次请求,如下:
在这里插入图片描述

1.首先测试不加burst不加nodelay的情况:

在这里插入图片描述

查看当前的tcp连接数
netstat -n | awk ‘/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}’

结果如下:
在这里插入图片描述

使用ab测试工具,发起10个并发请求:
ab -n 10 -c 10 url

ab工具返回结果:
在这里插入图片描述
可以看到一共10个请求,9个请求都失败了。且0.002秒就完成了压测

接着查看当前的tcp连接数:
在这里插入图片描述
可以观察到此时服务端的TIME_WAIT 比上图多了10,这意味着是服务端主动要求断开了所有TCP连接

接着再查看 /var/log/nginx/access.log,印证了只有一个请求成功了,其它就是都直接返回了503,即服务器拒绝了请求。
在这里插入图片描述

2. 只加burst不加nodelay和的情况:

在这里插入图片描述
查看当前的tcp连接数
netstat -n | awk ‘/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}’

结果如下:
在这里插入图片描述

使用ab测试工具,发起10个并发请求:
ab -n 10 -c 10 url

可以看到测试经过30s才结束
ab工具返回结果:
在这里插入图片描述
压测中一共10个请求,有4个请求失败了,直接返回503

查看当前的tcp连接数:
在这里插入图片描述
上图是ab测试第一秒时的截图,对比第一次截图,TIME_WAIT=19 24 表示有服务器端主动断开了5个TCP连接,即5个请求被瞬时拒绝

接着查看 /var/log/nginx/access.log日志
在这里插入图片描述
可以观察到在50分46秒,即压测第1秒时,成功处理了1个请求,另外有4个请求瞬间返回了503,剩下的5个请求每隔6s处理一次。
这是因为设置了burst=5,在服务器接收到10个并发请求后,先处理1个请求,同时将5个请求放入burst缓冲队列中,等待处理。而超过(burst+1)数量的请求就被直接抛弃了,即直接抛弃了4个请求。

3.加burst加nodelay的情况

在这里插入图片描述

查看当前的tcp连接数
netstat -n | awk ‘/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}’

结果如下:

在这里插入图片描述

使用ab测试工具,发起10个并发请求:
ab -n 10 -c 10 url
可以看到压测在0.001s内完成了,这也是添加nodelay参数的意义
ab工具返回结果:
在这里插入图片描述
压测中一共10个请求,有4个请求失败了,直接返回了503

接着查看当前的tcp连接数:所有的请求都在1s内完成了
在这里插入图片描述

接着查看 /var/log/nginx/access.log日志
在这里插入图片描述
可以发现在1s内,服务器端处理了6个请求(峰值速度:burst+原来的处理速度)。对于剩下的4个请求,直接返回503,在下一秒如果继续向服务端发送10个请求,服务端会直接拒绝这10个请求并返回503。因为设定了没6s处理1个请求,所以直到30 s 之后,才可以再处理一个请求,即如果此时向服务端发送10个请求,会返回9个503,一个200

查看/var/log/nginx/error.log日志,发现有4个请求被直接拒绝了,没有延时请求。
在这里插入图片描述

总结
  • limit_req zone=req_zone;
    严格依照在limti_req_zone中配置的rate来处理请求
    超过rate处理能力范围的,直接drop
    表现为对收到的请求无延时

  • limit_req zone=req_zone burst=5;
    依照在limti_req_zone中配置的rate来处理请求
    同时设置了一个大小为5的缓冲队列,在缓冲队列中的请求会等待慢慢处理
    超过了burst缓冲队列长度和rate处理能力的请求被直接丢弃
    表现为对收到的请求有延时

  • limit_req zone=req_zone burst=5 nodelay;
    依照在limti_req_zone中配置的rate来处理请求
    同时设置了一个大小为5的缓冲队列,当请求到来时,会爆发出一个峰值处理能力,对于峰值处理数量之外的请求,直接丢弃
    在完成峰值请求之后,缓冲队列不能再放入请求。如果rate=10r/m,且这段时间内没有请求再到来,则每6s缓冲队列就能回复一个缓冲请求的能力,直到回复到能缓冲5个请求位置。

2)限制并发连接数

limit_conn_zone 中1MB的内存空间可以存储32000个32B16000个64B的变量计数状态。
变量计数状态在32位系统平台占用32B或64B,在64位系统平台占用64B
10M的话换算下来可以保留16万个IP地址信息

#基于IP做连接限制  限制同一IP并发为1
limit_conn_zone $binary_remote_addr zone=addr:10m;
server {
        listen       80;
        server_name  localhost;
        location / {
            root   html;
            index  index.html index.htm;
        }
        location /abc {
           limit_conn addr 1;
        }
    }
}
3)限制下载速度
# 下载速度为100k
server {
        listen       80;
        server_name  localhost;
        location / {
            root   html;
            index  index.html index.htm;
        }
        location /abc {
           limit_rate 100k;
        }
    }
}
4)综合案例
http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;
    #基于IP做连接限制
    limit_conn_zone $binary_remote_addr zone=addr:10m;
    #基于IP对下载速率做限制,限制每秒处理1次请求,
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
 
    server {
        listen       80;
        server_name  localhost;
        location / {
            root   html;
            index  index.html index.htm;
        }
        location /abc {
           # 对突发超过5个以后的请求放入缓存区
           limit_req zone=one burst=5 nodelay;
           # 限制并发连接数为4
           limit_conn addr 4;
           # 限制下载速度为100K
           limit_rate 100k;
        }
    }
}
#限制用户连接数来预防DOS攻击
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m;
#限制同一客户端ip最大并发连接数
limit_conn perip 2;
#限制同一server最大并发连接数
limit_conn perserver 20;
#限制下载速度,根据自身服务器带宽配置
limit_rate 300k;

六、内核参数优化

vim /etc/sysctl.conf  #进入这个文件修改
sysctl -p   #让修改的内核信息生效


#################################################
fs.file-max = 999999
net.ipv4.ip_forward = 0
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.default.accept_source_route = 0
kernel.sysrq = 0
kernel.core_uses_pid = 1
net.ipv4.tcp_syncookies = 1
kernel.msgmnb = 65536
kernel.msgmax = 65536
kernel.shmmax = 68719476736
kernel.shmall = 4294967296
net.ipv4.tcp_max_tw_buckets = 6000
net.ipv4.tcp_sack = 1
net.ipv4.tcp_window_scaling = 1
net.ipv4.tcp_rmem = 10240 87380 12582912
net.ipv4.tcp_wmem = 10240 87380 12582912
net.core.wmem_default = 8388608
net.core.rmem_default = 8388608
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.core.netdev_max_backlog = 262144
net.core.somaxconn = 40960
net.ipv4.tcp_max_orphans = 3276800
net.ipv4.tcp_max_syn_backlog = 262144
net.ipv4.tcp_timestamps = 0
net.ipv4.tcp_synack_retries = 1
net.ipv4.tcp_syn_retries = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_mem = 94500000 915000000 927000000
net.ipv4.tcp_fin_timeout = 1
net.ipv4.tcp_keepalive_time = 30
net.ipv4.ip_local_port_range = 1024 65000
  • fs.file-max = 999999:这个参数表示进程(比如一个worker进程)可以同时打开的最大句柄数,这个参数直线限制最大并发连接数,需根据实际情况配置。
  • net.ipv4.tcp_max_tw_buckets = 6000:这个参数表示操作系统允许TIME_WAIT套接字数量的最大值,如果超过这个数字,TIME_WAIT套接字将立刻被清除并打印警告信息。该参数默认为180000,过多的TIME_WAIT套接字会使Web服务器变慢。
    注:主动关闭连接的服务端会产生TIME_WAIT状态的连接
  • net.ipv4.ip_local_port_range = 1024 65000:允许系统打开的端口范围。
  • net.ipv4.tcp_tw_recycle = 1:启用timewait快速回收。
  • net.ipv4.tcp_tw_reuse = 1:开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接。这对于服务器来说很有意义,因为服务器上总会有大量TIME-WAIT状态的连接。
  • net.ipv4.tcp_keepalive_time = 30:这个参数表示当keepalive启用时,TCP发送keepalive消息的频度。默认是2小时,若将其设置的小一些,可以更快地清理无效的连接。
  • net.ipv4.tcp_syncookies = 1:开启SYN Cookies,当出现SYN等待队列溢出时,启用cookies来处理。
  • net.core.somaxconn = 40960:web 应用中 listen 函数的 backlog 默认会给我们内核参数的。
    net.core.somaxconn:限制到128,而nginx定义的NGX_LISTEN_BACKLOG 默认为511,所以有必要调整这个值。注:对于一个TCP连接,Server与Client需要通过三次握手来建立网络连接.当三次握手成功后,我们可以看到端口的状态由LISTEN转变为ESTABLISHED,接着这条链路上就可以开始传送数据了.每一个处于监听(Listen)状态的端口,都有自己的监听队列.监听队列的长度与如somaxconn参数和使用该端口的程序中listen()函数有关。somaxconn定义了系统中每一个端口最大的监听队列的长度,这是个全局的参数,默认值为128,对于一个经常处理新连接的高负载 web服务环境来说,默认的 128 太小了。大多数环境这个值建议增加到 1024 或者更多。大的侦听队列对防止拒绝服务 DoS 攻击也会有所帮助。
  • net.core.netdev_max_backlog = 262144:每个网络接口接收数据包的速率比内核处理这些包的速率快时,允许送到队列的数据包的最大数目。
  • net.ipv4.tcp_max_syn_backlog = 262144:这个参数标示TCP三次握手建立阶段接受SYN请求队列的最大长度,默认为1024,将其设置得大一些可以使出现Nginx繁忙来不及accept新连接的情况时,Linux不至于丢失客户端发起的连接请求。
  • net.ipv4.tcp_rmem = 10240 87380 12582912:这个参数定义了TCP接受缓存(用于TCP接受滑动窗口)的最小值、默认值、最大值。
  • net.ipv4.tcp_wmem = 10240 87380 12582912:这个参数定义了TCP发送缓存(用于TCP发送滑动窗口)的最小值、默认值、最大值。
  • net.core.rmem_default = 6291456:这个参数表示内核套接字接受缓存区默认的大小。
  • net.core.wmem_default = 6291456:这个参数表示内核套接字发送缓存区默认的大小。
  • net.core.rmem_max = 12582912:这个参数表示内核套接字接受缓存区的最大大小。
  • net.core.wmem_max = 12582912:这个参数表示内核套接字发送缓存区的最大大小。
  • net.ipv4.tcp_syncookies = 1:该参数与性能无关,用于解决TCP的SYN攻击。

七、实战配置

内核参数配置

#如果想把timewait降下了就要把tcp_max_tw_buckets值减小,默认是180000
net.ipv4.tcp_max_tw_buckets = 5000
 
#开启重用功能,允许将TIME-WAIT状态的sockets重新用于新的TCP连接
net.ipv4.tcp_tw_reuse = 1
 
#系统中最多有多少个TCP套接字不被关联到任何一个用户文件句柄上。如果超过这个数字,孤儿连接将即刻被复位并打印出警告信息。这个限制仅仅是为了防止简单的DoS攻击
net.ipv4.tcp_max_orphans = 262144
 
#当keepalive起用的时候,TCP发送keepalive消息的频度。缺省是2小时。我们可以调短时间跨度
net.ipv4.tcp_keepalive_time = 30

nginx全局配置

worker_processes  1;
pid  /var/run/nginx.pid;
 
events {
    worker_connections  2048;
        multi_accept on;
        use epoll;
}
 
http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
                                          
        log_format main '{"@timestamp":"$time_iso8601",'   
        '"host":"$server_addr",'
        '"clientip":"$remote_addr",'
        '"size":$body_bytes_sent,'
        '"responsetime":$request_time,'
        '"upstreamtime":"$upstream_response_time",'
        '"upstreamhost":"$upstream_addr",'
        '"http_host":"$host",'
        '"url":"$uri",'
        '"xff":"$http_x_forwarded_for",'
        '"referer":"$http_referer",'
        '"agent":"$http_user_agent",'
        '"status":"$status"}';
        
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
        
    server_names_hash_bucket_size 128;
    server_names_hash_max_size 512;
    keepalive_timeout  65;
    client_header_timeout 15s;
    client_body_timeout 15s;
    send_timeout 60s;
        
        limit_conn_zone $binary_remote_addr zone=perip:10m;
        limit_conn_zone $server_name zone=perserver:10m;
        limit_conn perip 2;
        limit_conn perserver 20;
        limit_rate 300k; 
proxy_cache_path /data/nginx-cache levels=1:2 keys_zone=nginx-cache:20m max_size=50g inactive=168h;
        
        client_body_buffer_size 512k;
        client_header_buffer_size 4k;
        client_max_body_size 512k;
        large_client_header_buffers 2 8k;
        proxy_connect_timeout 5s;
        proxy_send_timeout 120s;
        proxy_read_timeout 120s;
        proxy_buffer_size 16k;
        proxy_buffers 4 64k;
        proxy_busy_buffers_size 128k;
        proxy_temp_file_write_size 128k;
        proxy_next_upstream http_502 http_504 http_404 error timeout invalid_header;
        
        gzip on;
        gzip_min_length 1k;
        gzip_buffers 4 16k;
        gzip_http_version 1.1;
        gzip_comp_level 4;
        gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
        gzip_vary on;
        gzip_disable "MSIE [1-6].";
 
    include /etc/nginx/conf.d/*.conf;
}

负载均衡

upstream ygoapi{ 
  server ip:port fail_timeout=5 max_fails=3;
  server ip:port fail_timeout=5 max_fails=3;
  ip_hash;  #负载均衡策略
}

server配置

#隐藏版本信息
server_tokens off;
server {
    listen       80;
    server_name  www.ygoclub.com;
    charset utf-8;
	
	#重定向HTTP请求到HTTPS
	return 301 https://$server_name$request_uri;
}

HTTPS配置(ssl配置)

这里主要是配置ssl证书
在nginx服务的目录下创建cert

server {
    listen 443;
    server_name 域名;
    ssl on;
    root /xxx/xxx/html; // 前台文件存放文件夹,一般使用 Nginx 初始化的文件夹,当然也可以自己修改
    index index.html;// 上面配置的文件夹里面的index.html
    ssl_certificate /路径/证书名称.pem;
    ssl_certificate_key /路径/证书名称.key;
    ssl_session_timeout 5m;
    # SSL协议配置
    ssl_protocols SSLv2 SSLv3 TLSv1.2; #表示使用TLS类型协议
    ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP; #表示使用加密套件的类型
    ssl_prefer_server_ciphers on;
        location / {
            proxy_redirect off;
            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 http://127.0.0.1:8080;
        }
}			   
}
 
server {
    listen 80;
    server_name your-domain.com;// 你的域名
    rewrite ^(.*)$ https://$host:443$1 permanent;// 把http的域名请求转成https且转发到443端口
}
  • 11
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

菜小徐呐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值