Nginx – proxy_module处理请求流程

ngx_http_proxy_module是nginx众多反向代理模块中非常常用的一个,它接受客户端的http请求,再转发给上游服务器。nginx作为负载均衡服务器,它必不可少。对于一个请求,proxy 模块处理的流程大致可以分为以下几个步骤。

 

判断proxy缓存

proxy模块是在content阶段发生,对于一个到来的请求,首先会判断是否开启了proxy_cache,如果开启了缓存,并且命中会直接将缓存文件中对应的key的内容返回。通常会对一些变化不频繁的内容做缓存以减轻nginx和上游服务器的压力。

构造发送给上游的header与body

proxy模块提供了很多指令来改变发给上游请求的headers和body,比如proxy_set_header,proxy_set_body,proxy_request_pass_request,prxy_request_pass_headers等等。nginx会先根据这些指令的内容生成header和body。(客户端请求的headers是在请求的11个阶段开始之前nginx框架就接受了,因为设计到分配变量,选择server,location等等)

读取请求body

上一步提到了,客户端与nginx建立连接后,nginx不会立即与上游服务器建立连接,先生成header和body,如果用户发送的是一个post请求,那么nginx通常是会先将客户端请求的body读取完再建立连接(由proxy_request_buffering指令决定,而且不管proxy_set_body,proxy_pass_request_body这些指令的设置如何,用户请求的body肯定是要接受的,是否redefine或者丢弃是另外一回事),这么做的好处是对于并发支持有限的上游服务器,这是一种能提升并发能力的做法,举一个例子,先做以下假设

  1. 客户端的请求是上传一个文件,文件size比较大
  2. 客户端的上行带宽有限
  3. 上游服务器和nginx处于同一个内网中

这时候如果我们没有打开proxy_request_buffering,那么nginx会立即与上游服务器建立连接,然后边接受客户端的请求内容,边将内容发给上游,此时如果上游服务器的并发连接数比较小,而这个请求可能会很长,如果此时有多个这样的上传请求,那么上游服务器的并发能力就会下降。如果打开了buffering,那么nginx会等所有的上传文件内容都接受完,再建立连接发送给上游,那么这次请求处理的就很快(内网网速非常快)。这样虽然对客户端来说没有什么区别,但是却能提升上游服务器的并发能力。

选择服务器

在上一步打开proxy_request_buffering后,nginx接受完完整请求的body(body保存在临时文件中),nginx会根据upstream中指定的负载均衡算法来选择一台上游服务器提供服务。

建立连接,发送请求

确定了连接的上游服务器,nginx便开始根据指定的参数(比如max_conn,fail_timeout等TCP连接参数)与上游服务器建立连接,连接建立成功后就开始将客户端请求的内容转发给上游。

接受响应

上游服务器处理完请求,nginx开始接受响应,包括header和body(先是header再是body),这时又会有一个proxy_buffering来决定nginx的行为,如果打开了proxy_buffering,那么nginx会等接受完完整的上游响应body后(Tips:1.如果body小于proxy_buffers设置的大小,通常为64K,那么即使打开proxy_buffering,也不会保存到临时文件中。2. 如果上游服务器的响应headers里有X-Accel-Buffering: yes|no,会覆盖proxy_buffering的指值。 3.如果headers的大小大于proxy_buffer_size大小,或产生502错误),再发给客户端,通常nginx与上游都是在同一个内网中,所以这个配置是打开的。在这一步,nginx同样提供了很多指令来改变发给客户端的响应内容。

保存缓存

如果打开了proxy_cache,那么nginx会将响应内容保存到缓存文件中,cache的行为包括缓存文件的路径,缓存内容的key的规则,keys对应的共享内存的name和size等等都有不同的指令完成。

关闭或复用连接

上一步中,如果没有打开cache,那么nginx会根据upstream中keepalive指令来决定是否关闭或者复用tcp连接。

 

(tip: nginx作为负载均衡服务,通常与上游的连接数理论上最大为65535个(local port限制),如果想要提高并发数,增加上游服务的ip数量或者监听的端口数量来避开linux的系统限制)

 

以上就是proxy_module处理和转发请求的大致流程,其中有一些细节十分巧妙,nginx就是这样,在很多功能上的设计都是如此,这也是它越发流行的原因之一吧。

Nginx处理http请求头部流程

主要流程如上图,大致步骤如下:

  1. 三次握手,接受到对方的syn报文,tcp(内核)回复syn+ack,等再次接受对端的ack,内核会根据负责均衡算法选择一个worker处理,然后epoll会将并将刚刚建立的socket句柄交给nginx的事件处理器,事件处理判断是一个建立连接的读事件就进行accept系统调用,此时会给该链接分配连接内存池,默认512B。
  2. accept成功后会触发ngx_http_init_connection回调方法,该回调是在ngx_http_add_listening方法中设置的。
    ls->handler = ngx_http_init_connection;

    在init_connection方法中会将新建连接对应的读写事件通过epoll_ctl系统调用加入到epoll中,并设置一个定时器client_header_timeout,用来保证在指定时间内如果没有收到header就断开连接。

  3. 用户发送请求行和header,内核回复ack,并且epoll_wait拿到fd,开始调用ngx_http_wait_request_handler方法。内核需要将data从内核读缓冲区复制到用户空间读缓冲区,所以该方法会从connection_pool中分配client_header_buffer_size(通常是1k),用来保存数据。同时nginx还会分配一块内存叫做request_pool,用来服务nginx处理请求。
    size = cscf->client_header_buffer_size;
    ngx_palloc(c->pool, size);
  4. 当接收到的uri和header超过1k,nginx还会从request_pool中分配一块large_client_header_buffers内存用开保存更大的用户数据。
  5. 接收到uri后,会接着接受header,所有的header接受完后,会移除之前设置的client_header_timeout。之后就是交给http模块来处理核心逻辑。

TLS疑点杂记

客户端拿到证书链后有哪些操作 

1. 验证证书是否过期,证书中会包含颁发日期和过期日期,不过这里有个问题是怎么验证证书的过期时间是有效的?

2. 验证证书是否吊销,有两种做法,第一是CRL即证书吊销列表。证书被吊销后会被记录在CRL中,CA会定期发布CRL。浏览器可以去CRL服务器去下载这个列表来检查证书是否被吊销了,缺点显而易见就是不实时以及下载列表以及比对非常耗时。第二是OCSP,来代替CRL,它是一个协议,规定了客户端如果发送一个请求,客户端带着证书去查询,OCSP服务器会给出该证书的相关信息。

3. 验证证书链是否合法,即验证每层证书是否是上一级证书颁发的,一直到根证书。一般所有CA机构的根证书(包括二级证书)列表都会保存在操作系统或者浏览器中。

4. 数字签名的验证,服务器会给客户端发送一个数字签名,也就是对证书内容散列算出来的,而浏览器又可以拿到CA的公钥(内置在操作系统或浏览器),于是用公钥对证书内容同样的散列,结果与数字签名比对。

客户端和服务器如何磋商对称加密使用的密钥

早期的TLS协议会用三个random随机数来生成一个密钥,这个做法会有一些隐患,比如斯诺登事件,具体可以google。现在主流的做法是用DHCE椭圆加密算法,这个算法会有一些中间参数,在Certificates和Server Hello Done中间发送给客户端,客户端会根据这些参数用算法生成一对公私钥,并将公钥发送给服务器端。服务器用自己的私钥以及客户端的公钥生成一个密钥,同理客户端用服务器的公钥以及自己的私钥生成一个密钥,而DHCE算法可以保证两边生成的密钥是一致的。

如何生成一个免费的tls证书

推荐Let‘s Encrypt,参考:http://guitoo.cc/wordpress/2018/07/05/lets-encryp-https/

 

有意思的加法器

cpu的alu中,最重要的逻辑部件应该是加法器了,计算机内任何逻辑运算都可以用加法器来完成。

加法器又分为好几种,针对不同的使用场景,可以分为带标示加法器和无标示加法器。

无标示加法器

最基本的一位加法器又三个输入两个输出,分别是低位进位cin,输入a,输入b,高位进位cout以及结果F,逻辑电路比较简单,用逻辑运算表达式则为

F = a Xor b Xor cin
cout = a && b || a && cin || b &&cin
 

举个例子:

1 + 1

cin第一位上的运算为0,a和b都是1,所以F是0,满2进1所以进位cout为1,代表第二位进1。

而n位加法器则是有多个1位加法器组合,前一位的cout作为后一位的cin。

 

带标示的n位加法器

无标示的n位加法器的局限性比较多,比如无法处理溢出行为,举个例子,两个8位无符号数加法

1 1 1 1 1 1 1 1
0 0 0 0 0 0 0 1

结果最高有效位会产生进位,结果会溢出,无标示的n位加法器无法处理这种情况,只能做取余操作,即舍掉最高位的进位,那么结果就错误了。还有一个情况是有符号数的运算,有符号数的最高位是符号位,且计算机内部是用补码进行计算,所以最高位的符号位会参与运算,那么如果没有一个标示位标示符号位的运算,那么结果也会出现错误。

所以带标示的n位加法器解决了这个问题。与普通的n位加法器类似,带标示的n位加法器,也有输入输入及进位,只不过最后结果会有好几个标示,这些标示会存放在专用寄存器中,以便控制器处理。主要有以下几个标示:

  1. 溢出标示,处理最高位的进位问题
  2. 0标示,如果所有位上的加法器运算结果F都为0,那么0标示为1,标示结果是0
  3. 符号标示,标示结果的符号位
  4. 进位/借位标示,加法则是进位标示,减法则是借位标示(计算机对减法的运算会转成加法2-1 = 2 + (-1))

有了这些标示,加法器就可以处理很多复杂的运算,比如和mux结合实现带符号数的加法等等

Mysql为什么会选错索引

mysql经常在执行某些sql时会选错索引,导致和预期的执行时间差距甚大,这其实不是mysql的bug,mysql选择索引是一个非常复杂的算法,所以选错也时有发生。

选择索引是优化器的工作,优化器会根据多种情况做为衡量因素来选择一个mysql认为最优的索引,通常一个主要因素就是扫描行数。在执行sql之前,mysql会预估一个扫描行数,(既然是预估那么肯定和实际的扫描行数有差距),而如何预估是取决于一个索引基数,索引值的重复率越低,基数越大,那基数是如何确定的?mysql是采取采样统计,即在索引段中取出N个页,统计重复率然后得到每页不重复值的平均值,再乘上这个索引一共有多少页(其实这里我有个疑问没想明白,索引扫描是很快的,而且不涉及到回表,何不直接扫描索引统计,这样更加准确。猜测mysql对基数的使用弱化了准确性的严格程度,第二个采样统计可能效率更高,更少的磁盘io,特别是索引比较大的情况下)。

show index from table

可以查看每个索引的基数值(cardinality列),这个值是可以重新统计的

analyze table t;

可以手动重新采样统计,或者索引表更比较频繁(1/M),会自动重新统计

刚刚说了扫描行数是通过基数预估出来的,第一个是预估,第二个是基数也不是非常准确,所以这个扫描行数可能会很不准确,从而导致选错索引,除了这点,还有一些其他原因可能会导致选错索引:

  1. 考虑回表的代价:比如possible_keys有主键id和a,对id扫描10万次的代价可能比索引a扫描50000次的代价小,因为在没有索引覆盖的情况下,50000次回表的随机io代价不可忽略。
  2. 排序的影响,mysql会创建临时表对无序的数据进行排序,这个开销也是不可忽略的,所以优化器在索引选择时会考虑到这点,比如possible_keys有索引a和b,sql最后需要按照b排序,那么b扫描100000次的代价可能比a扫描50000次的代价小,因为索引的有序性,所以选择索引a会重新对数据进行排序。