前文请戳:NGINX 开发指南(Part 1)NGINX 开发指南(Part 2)
以下是使用preaccess阶段handler的例子:
阶段的handler可以返回如下返回值:
-
NGX_OK — 继续执行下个阶段
-
NGX_DECLINED — 继续执行当前阶段的下一个handler。如果当前handler是本阶段的最后一个handler,则执行下个阶段。
-
NGX_AGAIN, NGX_DONE — 挂起阶段处理直到事件发生。此场景可以用来处理异步I/O操作或者进行延迟处理。阶段处理应该通过对ngx_http_core_run_phases()函数的调用来恢复。
-
任何其他的返回值都被视为请求结束,尤其是HTTP应答码,这种情况下会以返回的HTTP应答码结束当前请求。
一些阶段对返回值的处理稍有不同。在content阶段,除了NGX_DECLINED之外的任何返回值都会被当成结束请求处理。对于location提供的content handler,任何返回值都会别当成结束状态码进行处理。在access阶段,如果使用了satisfy any模式,返回除了NGX_OK,NGX_DECLINED,NGX_AGAIN和NGX_DONE之外的值会被作为阻断处理。如果没有其他的access handler对请求放行或者通过一个返回码阻断,则前述导致阻断的返回值会被当成结束状态码。
变量
访问已有变量
变量可以通过索引(即index,这是最常用的方式)或者名字(参考下文关于创建变量的章节)。索引是在配置阶段,当一个变量添加到配置中的时候创建。变量索引可以通过ngx_http_get_variable_index()函数获取:
这里,cf变量是一个指向nginx配置的指针,name则指向变量名称字符串。该函数在执行出错时候返回NGX_ERROR,其他情况下典型的做法是将返回的索引存储在模块配置中以便后续使用。
所有的HTTP变量都是基于HTTP请求的上下文而计算的,其结果也是与HTTP请求相关并存储于其中。所有用于计算变量的函数的返回值都是ngx_http_variable_value_t类型,该类型代表了一个变量的值。
说明:
-
len — 值的长度
-
data — 值本身
-
valid — 值是有效的
-
not_found — 变量没有找到,因此data和len成员无意义;例如,像尝试获取$arg_foo这种类型的变量的值,而请求中却没有名为foo的参数时,就可能发生这种情况。
-
no_cacheable — 禁止缓存结果值
-
escape — 由日志模块内部使用,用来标记在输出时需要进行转移的变量值
ngx_http_get_flushed_variable()和ngx_http_get_indexed_variable()函数用来获取变量值。它们拥有相同的接口 —— 一个HTTP请求r作为计算变量值的上下文以及一个index参数,用于指示哪个变量。以下是一个典型的用法:
这两个函数的区别是,ngx_http_get_indexed_variable()返回缓存的变量值而ngx_http_get_flushed_variable()函数对于不可缓存的变量进行刷新处理。
有一些场景中需要处理那些在配置阶段还不知道名字的变量,这些变量无法通过使用索引来访问,例如SSI和Perl模块。对于这类场景,可以使用ngx_http_get_variable(r, name, key)函数。该函数通过变量名字和它的哈希key来查找变量。
创建变量
ngx_http_add_variable()函数用来创建一个变量。其参数有:配置(注册变量的配置),变量名和用来控制变量行为的标记位:
NGX_HTTP_VAR_CHANGEABLE — 允许变量被重新定义;如果另外一个模块使用同样的名字定义变量,不会产生冲突。例如,这个特点允许用户使用set指令覆盖变量。
NGX_HTTP_VAR_NOCACHEABLE — 禁止缓存,在类似于$time_local这样的变量上使用。
NGX_HTTP_VAR_NOHASH — 标识这个变量只能通过索引访问,不允许通过变量名访问。这是一个小的优化,可以在类似于SSI或者Perl这样的模块中不需要此变量的时候使用。
NGX_HTTP_VAR_PREFIX — 该变量的名字是一个前缀。相关的handler必须实现额外的逻辑来获取指定的变量值。例如,所有"arg_"变量都被同一个handler处理,该handler在请求的参数中查找并返回对应的参数值。
此函数在失败时返回NULL,否则返回一个指向ngx_http_variable_t类型的指针:
get和set handler被用来获取以及设置变量的值,data成员会被传递给变量handler,index成员中存储的是分配的变量索引,用来引用变量。
通常,一个以null结尾的上述结构体数组会在模块中创建,并在preconfiguration阶段将数组中的变量添加到配置中:
HTTP模块上下文中的preconfiguration成员会被赋值为这个函数,并在解析HTTP配置之前被调用,所以它可以处理这些变量。
get handler负责为某个请求计算变量的值,例如:
如果内部出现错误(比如分配内存失败)则返回NGX_ERROR,否则返回NGX_OK。变量计算结果的状态可以通过ngx_http_variable_value_t的flags成员的值来了解(参考前文相关描述)。
set handler允许设置变量所指向的属性。例如,$limit_rate变量的set handler修改了请求的limit_rate成员的值:
复杂值
复杂值提供了一种简单的方法来计算一个包含有文本、变量以及文本变量组合等情况的表达式的值。
由ngx_http_compile_complex_value所表示的复杂值在配置阶段被编译到ngx_http_complex_value_t类型中,该编译的结果在运行阶段可以被用来计算表达式的值。
这里,ccv里包含了全部初始化复杂值cv所需的参数:
cf — 配置指针
value — 待解析的字符串 (输入)
complex_value — 编译后的值 (输出)
zero — 是否对结果进行0结尾处理
conf_prefix — 是否将结果带上配置前缀(nginx当前查找配置的目录)
root_prefix — 是否将结果带上根前缀(通常是nginx的安装目录)
zero标记位在需要把结果传递给要求0结尾字符串的库时,非常有用,而前缀相关的标记位在处理文件名时很方便。
对于正确的编译,可以从cv.lengths成员获取到表达式中是否存在变量的情况。如果为NULL,则表示表达式中只是纯文本,所以没有必要将其保存成一个复杂值,使用简单的字符串就可以了。
ngx_http_set_complex_value_slot()可以在声明指令的时候对复杂值进行初始化。
在运行阶段,复杂值可以使用ngx_http_complex_value()函数来计算:
给定请求r和之前编译的cv,该函数会对表达式的值进行急计算并将结果存放在res变量中。
请求重定向
HTTP请求总是通过ngx_http_request_t结构体的loc_conf成员来绑定到某个location上。这意味着在任意时刻,任何模块都可以通过调用ngx_http_get_module_loc_conf(r, module)来获取到location的配置。
在HTTP请求的生命周期内,其location可能会改变多次。初始时,default server的default location会被分配给HTTP请求。一旦这个请求切换到了另外一个不同的server(比如通过HTTP的"Host"头,或者通过SSL的SNI扩展),该server的default location也同样会分配给这个请求。
接下来在NGX_HTTP_FIND_CONFIG_PHASE阶段中会重新为请求选择location。在这个阶段里,location的选择是基于请求的URI,在此server中全部的非命名location中查找得来的。
ngx_http_rewrite_module模块也可能在NGX_HTTP_REWRITE_PHASE阶段对请求的URI进行修改,这样的话请求会重新发送回NGX_HTTP_FIND_CONFIG_PHASE阶段使用新的URI进行location匹配。
也可以在任意时候通过对ngx_http_internal_redirect(r, uri, args)和ngx_http_named_location(r, name)函数进行调用来实现将请求重定向到一个新的location。
ngx_http_internal_redirect(r, uri, args)函数修改请求的URI并且将请求发送回NGX_HTTP_SERVER_REWRITE_PHASE阶段。之后请求被分配到server默认的location上,然后在NGX_HTTP_FIND_CONFIG_PHASE阶段根据请求新的URI来选择location。
下面是一个同时带有新的请求参数的内部重定向的例子。
ngx_http_named_location(r, name)函数将请求重定向到一个命名location。目标location的名称通过参数传递,并在当前server中的全部命名location中查找,接着请求会被发送到NGX_HTTP_REWRITE_PHASE阶段。
下面是一个将请求重定向到命名location @foo的例子:
当ngx_http_internal_redirect(r, uri, args)和ngx_http_named_location(r, name)这两个函数被调用时,nginx模块可能已经向HTTP请求的ctx成员中存储了一些上下文。这些上下文在请求发生location切换之后可能会变得不一致。为了避免这种不一致性,所有的请求上下文会被这两个函数清除。
被重定向以及被重写的请求成为了内部请求进而可以访问内部location。内部请求的internal标记位被设置为真。
子请求
子请求主要用来将一个请求的输出合并到另外一个请求中,很可能和其他数据混合。一个子请求看起来就像是一个普通的请求,但是和其父请求共享某些数据。
具体来说,所有和客户端输入相关的数据都是共享的,因为子请求不从客户端接收任何额外的数据。子请求的请求结构中的parent成员保存了指向其父请求的指针,如果是main request则此成员为空。成员main存储了指向一组请求中main请求的指针。
子请求从NGX_HTTP_SERVER_REWRITE_PHASE阶段开始。它经历的其他阶段和普通请求相同,并基于其URI来分配location。
子请求的输出头总是被忽略。子请求的输出体通过ngx_http_postpone_filter插入到父请求产生的数据中的合适位置。
子请求和活动请求的概念相关。一个请求r被认为是活动的,如果c->data == r,c是表示nginx和客户端连接的对象。在任意时候,只有一组请求中的活动请求才允许将其输出缓冲发送给客户端。
一个非活动请求仍然可以将其数据发送到过滤链中,但是这些数据不会通过ngx_http_postpone_filter过滤并且数据会一直保留在这个过滤器中,直到请求变成活动状态。下面是一些关于请求活动性的规则:
-
开始时,main请求是活动的
-
- 一个活动请求的第一个子请求在被创建之后立刻变为活动的
如果活动请求的子请求队列上的下一个请求之前的数据都已经发送完,则ngx_http_postpone_filter会将此请求激活
- 当一个请求结束了,它的父请求变为活动请求
一个子请求是用过调用ngx_http_subrequest(r, uri, args, psr, ps, flags)函数来创建的,其中r是父请求,uri和args分别是子请求的URI和请求参数,psr是一个输出参数,含有新创建的子请求的引用,ps是一个回调函数,用来在子请求结束的时候通知父请求,flags是子请求的创建标记位。有如下标记位可以使用:
NGX_HTTP_SUBREQUEST_IN_MEMORY - 子请求的输出不需要发送给客户端,而是在内存中保留。此标记位只对代理子请求有效。在子请求结束后,它的输出会以ngx_buf_t类型存放在r->upstream->buffer中。
NGX_HTTP_SUBREQUEST_WAITED - 子请求的done标记位会被设置,即使当其结束时处于非活动状态。这个标记位被SSI过滤器使用。
NGX_HTTP_SUBREQUEST_CLONE - 子请求作为父请求的克隆来创建。如此创建的子请求将继承父请求的location并从父请求所在的阶段继续执行。
下面的例子中创建了一个URI为"/foo"的子请求。
这个例子是将当前请求进行克隆并为子请求设置了一个结束回调函数。
子请求通常在body过滤器中创建。在这种情况下,子请求的输出可以被当成任意的显式请求输出处理。这意味着子请求的输出会在其他全部先于子请求创建的显式缓冲之后,以及在除此之外的任何缓冲之前,发送给客户端。
这个顺序对于大型的子请求层次结构也同样有效。下面演示了将一个子请求插入到所有请求数据缓冲之后,但是在拥有last_buf的最后一个缓冲之前的例子。
一个子请求也可以为了输出数据之外的目的而创建。例如,ngx_http_auth_request_module在NGX_HTTP_ACCESS_PHASE阶段创建了一个子请求。
为了在这个阶段禁止任何输出,子请求的header_only标志被设置。这可以避免子请求的body被发送到客户端。子请求的header无论如何都是被忽略的。子请求的结果可以通过回调handler来分析处理。
请求结束
一个HTTP请求通过调用ngx_http_finalize_request(r, rc)来完成其生命周期。这通常是content handler在向过滤链发送完全部输出数据后执行的。在这个时候,数据有可能还没有全部发送到客户端,而是其中一部分依然缓存在过滤链的某处。
如果是这样,ngx_http_finalize_request(r, rc)函数会自动注册一个特殊的handlerngx_http_writer(r)来完成数据的发送。一个请求也可能是因为产生了某种错误或者因为标准的HTTP响应码需要被返回给客户端,而被终结。
ngx_http_finalize_request(r, rc)函数接受如下的rc参数值:
NGX_DONE - 快速结束。减少请求引用计数并且如果为0的话就销毁请求。和客户端的连接可能会被继续复用。
NGX_ERROR, NGX_HTTP_REQUEST_TIME_OUT (408), NGX_HTTP_CLIENT_CLOSED_REQUEST (499) - 错误结束。尽可能快结束请求并关闭客户端连接。
NGX_HTTP_CREATED (201), NGX_HTTP_NO_CONTENT (204), 大于或等于 NGX_HTTP_SPECIAL_RESPONSE (300) - 特殊响应结束。对这些值nginx要么发送默认代号响应页面给客户端,要么根据error_page location执行内部重定向(如果配置了的话)。
其它值被认为是成功结束,并且可能激活请求writer完成发送响应体。一旦body发送完毕,请求计数就会递减。如果到达0,则该请求会被销毁,但是客户端可能因为其它请求继续被使用着。如果计数大于0, 则该请求内还有未完成的活动,它们将在后面被继续完成。
请求体
为处理客户端请求体,nginx提供了两个函数:ngx_http_read_client_request_body(r, post_handler) 和 ngx_http_discard_request_body(r)。每一个函数读请求体并且设到 request_body 字段。第二个函数指示nginx丢弃(读和忽略)请求体。每个请求必须调用它们其中的一个。通常,这个在content阶段完成。
读或丢弃客户端请求体不能在子请求里。这个需要在主请求里完成。当一个子请求创建时,如果父请求已经在前面读了请求体,则子请求会继承父的request_body以便使用。
函数 ngx_http_read_client_request_body(r, post_handler) 开始读请求体的处理。当请求体完全读取后,post_handler 回调函数会被调用以继续处理请求。如果没有请求体或已读,则回调函数会立即被调用。
函数 ngx_http_read_client_request_body(r, post_handler) 分配类型为ngx_http_request_body_t的request_body字段。该对象的bufs字段将结果保留为buffer chain。请求体可以保存在内存buffer,如果client_body_buffer_size不足于容纳整个在内存的body时,则保存在文件buffer。
以下例子读客户端请求体并返回它的大小。
以下请求的字段会影响请求体的读取方式。
request_body_in_single_buf - 将请求体读到单一内存buffer。
request_body_in_file_only - 始终将请求体读到文件,即使适合内存缓冲区。
request_body_in_persistent_file - 创建后不删除该文件。这样的文件可以被移到其它目录。
request_body_in_clean_file - 当请求结束时删除该文件。当文件希望被移到其它目录,但由于某种原因没移走,这时该字段就派上用场了。
request_body_file_group_access - 启用文件组权限。默认情况文件以0600权限被创建。当该标记设置时,0660权限就被用上了。
request_body_file_log_level - 记录文件错误的日志级别。
request_body_no_buffering - 不缓冲的读请求体。
当设置request_body_no_buffering这个标记,读请求体的非缓冲模式就开启了。这种模式下,调用完 ngx_http_read_client_request_body()之后,bufs链可能只保留请求体的一部份。
要继续读下个部分,应该调用ngx_http_read_unbuffered_request_body(r) 函数。返回值为 NGX_AGAIN 并且设置了标记reading_body表明还有更多的数据可读。如果调用该函数后 bufs 是 NULL,则说明此该没有数据可读。
当请求体下个部份可用时,请求回调用函数 read_event_handler 回被调用。
响应
nginx里的HTTP响应是通过发送响应头和接着可选的响应体产生的。两者被传进filter链里并且最终写到客户端socket。一个nginx模块可以安装它的handler到header或body filter里,并且处理来自上一个handler的输出。
响应头
通过函数 ngx_http_send_header(r) 发送输出头。在调用这个函数之前,r->headers_out 必须包含所有被用来发送HTTP响应头的数据。r->headers_out的status字段通常是需要设置的。
如果该响应状态码指示响应体应该接着头部,content_length_n 也可以设置。该值默认是-1,表示响应体大小是未知的。这种情况下,就会用到chunked传输。想输出任意的头部,需要加到头部列表里。
头部过滤
函数 ngx_http_send_header(r) 通过调用首个头部filter handler ngx_http_top_header_filter 执行头部filter链。它假设所有的header heandle会调用链里的下一个hanndler直到最后一个handler ngx_http_header_filter(r)。 这个最后的handler构造了基于 r->headers_out 的 HTTP 响应并且将它传给 ngx_http_writer_filter 以作输出。
要将一个handler添加到 header filter 链, 需要在配置阶段将它的地址保存在 ngx_http_top_header_filter 这个全局变量。前一个handler的地址通常保存在模块里的一个静态变量,并且在退出前由新加入的handler调用。
以下是个header filter模块的例子,对每个状态是200的输出都加个 "X-Foo: foo" 头部信息。
响应体
通过函数 ngx_http_output_filter(r, cl) 发响应体。该函数能被调用多次。每次它会发送作为buffer链的响应体的一部份。最后的body buffer应该有设置last_buf标记。
以下例子产生一个完整的HTTP输出 "foo" 作为响应体。为了让这个例子不止能在主请求运行,也在子请求能运行。输出的最后buffer会设置 last_in_chain 标记。标记 last_buf 只会对主请求设置,因为子请求的最后buffer不会作为整个输出的结束
响应体过滤
函数 ngx_http_output_filter(r, cl) 通过调用首个body filter handler ngx_http_top_body_filter 执行响应体过滤链。它假定每个body handler会调用链里的下一个handler直到最后的handler ngx_http_write_filter(r, cl) 被调用。
body filter handler会接收一个buffer链。这个handler会处理 buffers 并且传可能新的chain给下个handler。值得注意的是,传入的ngx_chain_t链接属于调用者。它们不用被复用或者改变。当handler完成后,调用者可以用它的输出链来跟踪其发送的buffer。如果想保存buffer chain或替换一些继续要发送的buffer,该handler应该分配它自己的链。
以下是一个简单的计算响应体大小的body模块。结果作为 $counter 变量可以被用在 access 日志。
构建过滤模块
当写一个body或header过滤模块是,要特别注意filter的顺序。已经有一些已经注册的标准nginx模块。注册一个filter模块到相对其它模块的正确位置是很重要的。通常filter会在模块自己的postconfiguration handler里注册。filter的调用顺序跟它们的注册时的顺序刚好相反。
nginx给第三方模块提供了个特殊的槽口 HTTP_AUX_FILTER_MODULES。想在这个插槽注册一个filter模块,模块的配置里应该将 ngx_module_type 变量设置值为 HTTP_AUX_FILTER。
以下例子显示一个filter模块的配置文件,并且假设只有一个源文件 ngx_http_foo_filter_module.c。
缓冲复用
当处理或更改缓冲区流时,经常需要复用已分配的buffer。nginx代码里比较标准通用的处理方式是保留两个buffer链:free and busy。 free 链保留所有空闲的 buffer。这些buffer可以拿来复用。busy 链保存所有当前模块发送的buffer,但仍然被其它的filter handler使用。
如果它的大小大于0,则认为该buffer还在使用。通常一个buffer被一个filter消费时,它的pos(或file_pos对文件buffer而言)会移向last (或file_pos对文件buffer而言)。一旦整个buffer被完全消费完,它就可以复用了。为将新空闲的buffer更新到空闲chain,需要完整的遍历busy链,并将大小为0的buffer移到free的首部。
这种操作很常见,所以有个特殊的函数 ngx_chain_update_chains(free, busy, out, tag) 专门处理这个。这个函数追加output chain到busy,并且将空闲的buffer从busy移到free。只有匹配tag的buffer才能复用。这样就让一个模块只能复用它自己分配的buffer。
以下例子为每个新进的buffer加入字符串 "foo"。该模块尽可能的复用这些新分配的buffer。注意:为了让该例子运行的没问题,需要安装header filter,并且将 content_length_n 设置为-1 (这节上面有提到)。
负载均衡
ngx_http_upstream_module提供了向远程服务器发送HTTP请求的基本功能。其他具体的协议模块,例如HTTP或FastCDI,都会使用这个功能。该模块同时还提供了可以定制负载均衡算法的接口并默认实现了round-robin(轮询)算法。
例如,提供其他的负载均衡算法的模块有least_conn和hash这些。需要注意的是,这些模块实际上是作为upstream模块的扩展而实现的,他们之间共享了大量的代码,比如对于服务器组的表示。keepalive模块是另外一个例子,这是一个独立的模块,扩展了upstream的功能。
ngx_http_upstream_module可以通过在配置文件中配置upstream块来显式配置,或者通过使用可以接受URL作为参数的指令来隐式开启,比如proxy_pass这种指令。只有显示的配置才能选择负载均衡算法。upstream模块有自己的指令上下文NGX_HTTP_UPS_CONF。相关结构体定义如下:
srv_conf — upstream模块的配置上下文
servers — ngx_http_upstream_server_t的数组,存放的是对upstream块中一组server指令解析的配置
flags — 指定特定负载均衡算法支持哪些特性(通过server指令的参数配置)的标记位。
NGX_HTTP_UPSTREAM_CREATE — 用来区分显式定义的upstream和通过proxy_pass类型指令(FastCGI, SCGI等)隐式创建的upstream
NGX_HTTP_UPSTREAM_WEIGHT — 支持“weight”
NGX_HTTP_UPSTREAM_MAX_FAILS — 支持“max_fails”
NGX_HTTP_UPSTREAM_FAIL_TIMEOUT — 支持“fail_timeout”
NGX_HTTP_UPSTREAM_DOWN — 支持“down”
NGX_HTTP_UPSTREAM_BACKUP — 支持“backup”
NGX_HTTP_UPSTREAM_MAX_CONNS — 支持“max_conns”
host — upstream的名字
file_name, line — 配置文件名字以及upstream块所在行
port and no_port — 显式upstream未使用
shm_zone — 此upstream使用的共享内存
peer — 存放用来初始化upstream配置通用方法的对象:
实现负载均衡算法的模块必须设置这些方法并初始化私有数据。 如果init_upstream在配置阶段没有初始化,ngx_http_upstream_module会将其默认设置成ngx_http_upstream_init_round_robin。
init_upstream(cf, us) — 配置阶段方法,用于初始化一组服务器并初始化init()方法。一个典型的负载均衡模块使用upstream块中的一组服务器来创建某种有效的数据结构并在data成员中存放自身的配置。
init(r, us) — 初始化用于每个请求的ngx_http_upstream_t.peer (不要和之前用于每个upstream的ngx_http_upstream_srv_conf_t.peer搞混了)结构,该结构用于进行负载均衡。该结构会作为所有处理服务器选择的回调函数的data参数传递。
当nginx需要将请求转给其他服务器进行处理时,它会调用配置好的负载均衡算法来选择一个地址,并发起连接。选择算法是从ngx_http_upstream_t.peer对象中获取的,该对象的类型是ngx_peer_connection_t:
这个结构体有如下成员:
sockaddr, socklen, name — 待连接的upstream服务器的地址;此为负载均衡算法的输出参数
data — 每请求的负载均衡算法所需数据;记录选择算法的状态并且通常会含有指向upstream配置的指针。此data会被作为参数传递给所有处理服务器选择的函数(见下文)
tries — 连接upstream服务器的重试次数
get, free, notify, set_session, and save_session - 负载均衡算法模块的方法,详细见下文
所有的方法至少接受两个参数:peer连接对象pc以及由ngx_http_upstream_srv_conf_t.peer.init()创建的data参数。注意,一般来说,由于负载均衡算法的”chaining”,这个data和pc.data是不同的,
get(pc, data) — 当upstream模块需要将请求发送给一个服务器而需要知道服务器地址的时候,该方法会被调用。该方法负责填写ngx_peer_connection_t结构的sockaddr,socklen和name成员。返回值有如下几种:
NGX_OK — 服务器已选择
NGX_ERROR — 发生了内部错误
NGX_BUSY — 当前没有可用服务器。有多种原因会导致这个情况的发生,例如:动态服务器组为空,全部服务器均为失败状态,全部服务器已经达到最大连接数或者其他类似情况。
NGX_DONE — keepalive模块用这个返回值来说明底层连接进行了复用,因此不需要和upstream服务器间创建一条新连接。
free(pc, data, state) — 当upstream模块同某个upstream服务器通信结束后,调用此方法。state参数指示了upstream连接的完成状态,是一个bitmask,可以被设置成这些值:NGX_PEER_FAILED - 失败,NGX_PEER_NEXT - 403和404的特殊情况,不作为失败对待,NGX_PEER_KEEPALIVE。此外,尝试次数也在这个方法递减。
notify(pc, data, type) — 开源版本中未使用。
set_session(pc, data)和save_session(pc, data) — SSL相关方法,用于缓存同upstream服务器间的SSL会话,由round-robin负载均衡算法实现。
https://mp.weixin.qq.com/s/AVW0lvmwrn9L5riwZNySYg