nginx开发相关笔记

对服务server模式的理解和把控

在redis里面你对各种connection其实也就是redisClient结构体的成员都能熟悉,也就能够更好滴知道每个连接的状态,在nginx里面也要知道熟悉这就是ngx_connection_t,这个连接记录每个客户端处理的中间过程,需要对这个结构体非常熟悉,几乎所有的活动都是围绕这个结构体,就如同redis中几乎所有行为都围绕着redisClient展开。

从某种意义来看 nginx已经成为和tcp实现一样的七层标准呢。

nginx相比redis更多一层是对upstream的处理,那么一个客户端c如何关联upstream,也就需要有一个c与之对应上。

另外可以对比下两者(nginx/redis)在c上属性的差异,比如redis里面有对某个c连接建立的事件 最后一次互动时间。

看一个事物,既要看到他的全貌,也要看到他的局部,局部为全貌服务负责,全貌让各个局部互动起来有意义,人对一个事物的认知一定先看到全貌,再逐步深入 否则遇到问题就会抓瞎, 不知道从哪里入手,在深入细节后不至于迷失方向。

另外不能只看一个事物,要看多个同类事物有比较厚能够知道始终差异,其实核心框架都大同小异。

在redis里面几乎所有动作都围绕c(redisClient)进行,在nginx里面其实也是同样 都是围绕c来进行,毕竟都是为了处理服务连接。在nginx中其实c和r是一个方向,

redisServer对应nginx里面的ngx_cycle_t。

ngx_event是为了更好滴封装事件的抽象。

在redis中

 每次来一个连接就生产一个redisClient链在这里。

在nginx:

在redis中,redisClient:

nginx ngx_connection_t

 在nginx中,由于每个连接处理的handler实在太多,不像redis里面的handler比较固定。另外nginx需要处理的是http协议,对于请求的处理就移动到七层上,用r来实例化,其中data指向r。

其实完全可以类似于redis将收发处理也放在c中,但为了让界面跟清晰,nginx抽象出了r来做处理。

另外每次处理过程可能不会一次性完成,比如要存放一些数据,这样就会有缓冲区存放已有的请求/响应。

nginx里面的结构体成员较多,但只要抓住核心的ngx_cycle_t,就能找到所有其他的成员。不必陷入泥潭。

nginx开始打开监听:在ngx_init_cycle中,类似于redis的初始化 (initServer) 

nginx的listen由于可以监听多个ip端口,使用了循环,不过本质都是一样的。

从上面可以看到 redis的listen fd是放在redisServer上的,而nginx的listen fd是放在ngx_cycle->listening数组中的。(nginx在封装抽象上做的多一些)

对于每个listen fd首先需要有个回调handler,用于出发accept后的建立c的动作,redis是在initServer里面

 而nginx的每个ls的handler的赋值是在解析配置的时候就完成了(实际上每个ls的创建(因为nginx对监听也做了抽象)都是在解析配置时候 ngx_http_add_listening ),而不是在open的时候才去做:因为nginx认为每个ls的handler应该由其配置决定,比如如果配置的是http协议的上下文则会赋值为ngx_http_init_connection,而如果是mail场景则为ngx_mail_init_connection。 

值得注意的是,对于每个listen fd,也需要当作一个client来对待,将其统一化处理,所以listen fd也需要占用一个c: 如果某个c对应的是listen fd,则其c->listen指向listen_t。

创建事件:在redis中,这些事件被抽象化为aeFileEvent,在nginx则是ngx_event。 

可以看到redis/nginx中都是,事件和connectin(也就是fd,也就是redisClient,也就是ngx_connectiont_t)是有着对应关系的。在redis中读/写事件用了一个event,而nginx中将其分为两个event。

对于fd对应的事件的分配在 ngx_event_process_init (值得注意的是所有的c/event也是在这里提前分配好空间) 

这样listen fd对应的事件就有了:ngx_event_accept。(注意ngx_event_accept和ls->handler的区分:事件入口是ngx_event_accept,而对事件进一步的处理是在ls->handler)

而在redis中,直接就是一个:acceptHandler

进入创建连接的过程: 在一开始 eventloop池子中只加入了listenfd,进入到epoll_wait时触发accept动作:ngx_process_events_and_timers(ngx_cycle_t* cycle) (看到没,核心的结构体cycle,相当于redis中的redisServer,而为了处理方便 redis更是将其设置为了全局变量。

(实际上,nginx里面也有把cycle设置为了全局变量:任何时候想要获取cycle,即可使用ngx_cycle,找到它就找到了根)

 

(后续如果想扩展全局的配置,就可以放到这里面)

说到全局变量这里扩展一点:nginx里面的常用的全局变量(非static)的,主要是一些经常使用的

1 几个time的变量,可以随时获取到当前时间

2 ngx_cycle:nginx核心

3 ngx_event_timer_rbtree:时间事件红黑树,所有的timer事件都挂在上面(redis里面其实就是redisServer成员eventloop里面 用链表组织(其实目前只有一个时间事件)

4  ngx_posted_accept_events: 置后处理accept事件队列

5 ngx_posted_events: 置后处理普通事件队列

6 ngx_threads: 多线程支持

(为何要加volatile:http://c.biancheng.net/view/2iie2p.html

就是明确告诉编译器,不要对这些变量进行优化,这些变量的值是随时都在变化的。(c编译器可能会对某些持久不变的变量加载到寄存器中做优化,避免频繁从内存读取),而这里就告诉编译器对这些变量不要这样做,一直放在内存里就行。 但不要滥用该关键字,因为会有性能影响。

而这些全局变量随时都在被修改中。

4 ngx_process_events_and_timers(cycle)是工作启动的地方

-> ngx_epoll_process_events(epoll模型选中的话就是这个)

epoll出来,将触发的事件都导出放到了 static struct epoll_event  *event_list; (是个全局static变量

(

epoll_wait: 

然后逐步循环处理:

这里在先前加入事件add_conn(c)的时候将data.ptr与c关联上了.

然后开始对这个c进行处理:是个listen fd对应的accept事件,触发:

ngx_event_accept -> (ls->handler) 

在这里就生产了一个新的redisClient(ngx_conneciont_t),以后就围绕着c开始进行处理了. (不过也要把这个加入到epoll中去)

第一:ls->handler(c)  也即是ngx_http_init_connection(c)

以后所有的内存空间都从c上去分配(其实后面的r上的pool也看作类似的)

然后将该c的rev读事件handler设置上:ngx_http_init_request. 同时rev读事件还需要加入timer树中。这样一个accept事件就处理完了

然后再次for循环处理event_list中的事件,如果都处理完了,则本轮事件处理完了,然后进入到对timer的处理中: 如果timer有超时的事件,则先设置event的timed_out字段表示此处是超时触发,然后立即调用ev->handler进入处理。所以每次在进行event->handler除理之前总要判断是否是超时触发的,则有不同的逻辑.

然后循环epoll,导出事件,此次上面的连接上如果有发送数据过来,则会触发该c的读事件,进而调用到 ngx_http_init_request. (就是开始对client的request内容,有点类似于redis里面的readQueryFromClient,这种命令方式很直接,直接将数据流的方向指明白更容易形成架构)

(要注意,这些c可能是长期存在的,比如redis里面的redisClient长期存在的概率很大,所以会有定期清理)

在ngx_http_init_request中注意是对c的进一步处理,包括构建r,用于对http协议的处理。

同时进一步将rev->hander替换rev->handler = ngx_http_process_request_line; 即开始读取请求解析协议

  signature = 1347703880, 
  connection = 0x7ffff7fb8180,     r对应的c
  ctx = 0x6d8000,                  对各个模块预留的上下文来处理这个r
  main_conf = 0x6ce300,            该连接/该请求对应的配置上下文
  srv_conf = 0x6df4c8, 
  loc_conf = 0x6df5e8, 
  read_event_handler = 0x442bd5 <ngx_http_block_reading>, 
  write_event_handler = 0x0, 
  cache = 0x0, 
  upstream = 0x0,                  如果有upstream的处理,则一个c/r对应一个u
  upstream_states = 0x0, 
  pool = 0x6d7bf0, 
  header_in = 0x6cc650,            客户端发送过来的请求数据
  headers_in = {                   解析过的各个头部,
    headers = {
      last = 0x0, 
      part = {
        elts = 0x0, 
        nelts = 0, 
        next = 0x0
      }, 
      size = 0, 
      nalloc = 0, 
      pool = 0x0
    }, 
    host = 0x0,                    host头部,                 
    connection = 0x0,              connection: keepalive头部
    if_modified_since = 0x0, 
    if_unmodified_since = 0x0, 
    user_agent = 0x0, 
    referer = 0x0, 
    content_length = 0x0, 
    content_type = 0x0, 
    range = 0x0, 
    if_range = 0x0, 
    transfer_encoding = 0x0, 
    expect = 0x0, 
    accept_encoding = 0x0, 
    via = 0x0, 
    authorization = 0x0, 
    keep_alive = 0x0, 
    x_forwarded_for = 0x0, 
    user = {
      len = 0, 
      data = 0x0
    }, 
    passwd = {
      len = 0, 
      data = 0x0
    }, 
    cookies = {
      elts = 0x0, 
      nelts = 0, 
      size = 0, 
      nalloc = 0, 
      pool = 0x0
    }, 
    server = {
      len = 0, 
      data = 0x0
    }, 
    content_length_n = -1, 
    keep_alive_n = -1, 
    connection_type = 0, 
    msie = 0, 
    msie6 = 0, 
    opera = 0, 
    gecko = 0, 
    chrome = 0, 
    safari = 0, 
    konqueror = 0
  }, 
  headers_out = {                    响应内容存放
    headers = {
      last = 0x6d7490, 
      part = {
        elts = 0x6d7c40, 
        nelts = 0, 
        next = 0x0
      }, 
      size = 48, 
      nalloc = 20, 
      pool = 0x6d7bf0
    }, 
    status = 0, 
    status_line = {
      len = 0, 
      data = 0x0
    }, 
    server = 0x0, 
    date = 0x0, 
    content_length = 0x0, 
    content_encoding = 0x0, 
    location = 0x0, 
    refresh = 0x0, 
    last_modified = 0x0, 
    content_range = 0x0, 
    accept_ranges = 0x0, 
    www_authenticate = 0x0, 
    expires = 0x0, 
    etag = 0x0, 
    override_charset = 0x0, 
    content_type_len = 0, 
    content_type = {
      len = 0, 
      data = 0x0
    }, 
    charset = {
      len = 0, 
      data = 0x0
    }, 
    content_type_lowcase = 0x0, 
    content_type_hash = 0, 
    cache_control = {
      elts = 0x0, 
      nelts = 0, 
      size = 0, 
      nalloc = 0, 
      pool = 0x0
    }, 
    content_length_n = -1, 
    date_time = 0, 
    last_modified_time = -1
  }, 
  request_body = 0x0, 
  lingering_time = 0, 
  start_sec = 1691932772, 
  start_msec = 15, 
  method = 1, 
  http_version = 0, 
  request_line = {
    len = 0, 
    data = 0x0
  }, 
  uri = {
    len = 0, 
    data = 0x0
  }, 
  args = {
    len = 0, 
    data = 0x0
  }, 
}
  lingering_time = 0, 
  start_sec = 1691932772, 
  start_msec = 15, 
  method = 1, 
  http_version = 0, 
  request_line = {
    len = 0, 
    data = 0x0
  }, 
  uri = {
    len = 0, 
    data = 0x0
  }, 
  args = {
    len = 0, 
    data = 0x0
  }, 
  exten = {
    len = 0, 
    data = 0x0
  }, 
  unparsed_uri = {
    len = 0, 
    data = 0x0
  }, 
  method_name = {
    len = 0, 
    data = 0x0
  }, 
  http_protocol = {
    len = 0, 
    data = 0x0
  }, 
  out = 0x0, 
  main = 0x6d72f0, 
  parent = 0x0, 
  postponed = 0x0, 
  post_subrequest = 0x0, 
  posted_requests = 0x0, 
  virtual_names = 0x0, 
  phase_handler = 0, 
  content_handler = 0x0, 
  access_code = 0, 
  variables = 0x6d8120, 
  ncaptures = 0, 
  captures = 0x0, 
  captures_data = 0x0, 
  limit_rate = 0, 
  header_size = 0, 
  request_length = 0, 
  err_status = 0, 
  http_connection = 0x6cc4d8, 
  log_handler = 0x4440e1 <ngx_http_log_error_handler>, 
  cleanup = 0x0, 
  subrequests = 51, 
  count = 1, 
  blocked = 0, 
  aio = 0, 
  http_state = 1, 
  complex_uri = 0, 
  quoted_uri = 0, 
  plus_in_uri = 0, 
  space_in_uri = 0, 
  invalid_header = 0, 
  add_uri_to_alias = 0, 
  valid_location = 0, 
  valid_unparsed_uri = 0, 
  uri_changed = 0, 
  uri_changes = 11, 
  request_body_in_clean_file = 0, 
  request_body_file_group_access = 0, 
  request_body_file_log_level = 0, 
  subrequest_in_memory = 0, 
  waited = 0, 
  cached = 0, 
  gzip_tested = 0, 
  gzip_ok = 0, 
  gzip_vary = 0, 
  proxy = 0, 
  bypass_cache = 0, 
  no_cache = 0, 
  limit_zone_set = 0, 
  limit_req_set = 0, 
  pipeline = 0, 
  plain_http = 0, 
  chunked = 0, 
  header_only = 0, 
  keepalive = 0, 
  lingering_close = 0, 
  discard_body = 0, 
  internal = 0, 
  error_page = 0, 
  ignore_content_encoding = 0, 
  filter_finalize = 0, 
  post_action = 0, 
  request_complete = 0, 
  request_output = 0, 
  header_sent = 0, 
  expect_tested = 0, 
  root_tested = 0, 
  done = 0, 
  logged = 0, 
  buffered = 0, 
  main_filter_need_in_memory = 0, 
  filter_need_in_memory = 0, 
  filter_need_temporary = 0, 
  allow_ranges = 0, 
  state = 0, 
  header_hash = 0, 
  lowcase_index = 0, 
  lowcase_header = '\000' <repeats 31 times>, 
  header_name_start = 0x0, 
  header_name_end = 0x0, 
  header_start = 0x0, 
  header_end = 0x0, 
  uri_start = 0x0, 
  uri_end = 0x0, 
  uri_ext = 0x0, 
  args_start = 0x0, 
  request_start = 0x0, 
  request_end = 0x0, 
  method_end = 0x0, 
  schema_start = 0x0, 
  schema_end = 0x0, 
  host_start = 0x0, 
  host_end = 0x0, 
  port_start = 0x0, 
  port_end = 0x0, 
  http_minor = 0, 
  http_major = 0
}

c->r,r的核心结构其实没有多少:r->headers_in, r->headers_out等等

然后就是ngx_http_process_request_line 处理解析,根据解析出来的host头部选择对应的server进行处理,然后就进入生产响应阶段:

--> ngx_http_core_content_phase 

-> 

至于响应的内容是从本地拿到后给client(纯server模式),还是自己作为client请求外面拿到响应后给client(代理模式),就是另外的事情了,作为代理模式场景,nginx即作为了server又要作为client。

即然知道了收到的请求数据放到了c->buffer上,那么给客户的响应放到哪里的:响应分为响应行响应头响应体,前两个直接放在了r->headers_out上。响应体由于不确定会放到,会以chain的方式挂在r->out上。最终都是通过ngx_http_write_filter发送出去。(在后面会讲到)

==============

从redis的复制主从同步说到nginx的代理

最近在看redis的多机数据库复制部分,作为一个服务器,如果不能与其他的服务器交互肯定不能够扩展自己的能力,我们通常使用redis只看到了它的server的能力,即接受命令处理后反应响应,但是,redis还有和其他redis交互的能力,即它作为一个client的能力,下面可以通过redis的复制来展开说明:(注意这里说redis作为client的能力是为了下面展开对nginx的proxy能力做铺垫)

redis如何做主从同步:比如A与B,B从属于A,那么如何将A的状态同步给B。

首先B发送sync命令给A,其实这里就看到了B作为client的角色。然后A收到sync后将执行bgsave生成自己的rdb,发送给B,B收到这个rdb后将载入。这就完成了第一步

然后每次A接收到写命令后,都将同步发送给B,这样可以看到A也作为了client的角色。B收到后执行。这样就保持了和A一个状态。

从这里可以看到不管是主还是从(A or B),都既有充当server的角色也有充当client的角色。双方互为对方的client。

(其实后面的分析可以看到并不是如此,做为主只是把B当作一个特殊的client而已进行处理,并没有将其当作真正的server,不会出现AconnectB的行为,从A眼中,B和client 1并没有本质区别,都是给你们吐数据而已,只不过一个是吐我处理过的响应,一个是直接吐client 1发送过来的请求给B。(当然 这个过程B和A的连接是保持的,如果连阶断了,B会主动再次SYNC A。)。

至于后面的PSYNC(有兴趣可以了解)复制提高了SYNC的效率,但同步本质上还是一样的。

注意看颜色的传导,其实有点类似nginx里面的反向代理模式,本质上都是一个:即要做server接受,也要做client请求。

提问:A如何知道B的服务ip/端口?保存在哪里的,(nginx将upstream的信息保存在哪里的)

可以通过跟进A处理命令的流程:A处理了set命令后比如要把set发给B,看A是怎么发的:

跟进redis处理命令过程中学到一个技巧:比如redis对每个命令都做了一个cmd的handler进行调用,如果我要在这些处理流程里面加入一个逻辑,怎么搞?是对每个handler都添加一个吗?redis的做法是:将调用逻辑放入了一个函数里面,而不是直接cmd->proc(c)

 在call它做了这些通用逻辑,而我们追踪的redis如何将命令发送给从的地方就在此:replicationFeedSlaves.

redis将slaves(也就是upstream的信息)都放在了redis.slaves此处。(链表上)

那么其成员是什么?这就要看A是如何填充它的slaves list的:从给B发slaveof命令后,B此时并不是立即成为了A的slave,而是先返回给客户OK,然后异步地发送了SYNC命令给A,此时A就将B这个client 收入slaves list中,事实也是如此:A在处理sync命令时候填充了自己的slaves链表:

那么可以看到其实A的slaves链表里面 还是redisClient成员,也就是说A将对upstream的连接也用redisClient表示了(update补充:这里并不是upstream,其实也是client,正如下面你要看到的一样:不过结论没有错,redis将对upstream的连接也是用redisClient表示的,即server.master成员)。(其实和nginx的类似:也是用ngx_connection_t表示,不过包装了一下用的ngx_peer_connection_t,是指也是ngx_connection_t , pc->c = c ) (另外这里多补充一下:nginx的c里面的信息,包含的是client方的信息,比如他的remote_ip/remote_port就是放在c->sockaddr(通过accept系统调用获取的),而本身server的信息放在的是c->local_addr.。而在进行connect upstream时候,应该要获取到upstream的serverip/serverport,这是存放在哪里的?就是上面的pc,pc的pc->sockaddr里面,而这是从解析upstream配置里面获取到的(选择upstream的地址又是一个课题了这里又可以延伸一些策略,包括wrr、chash等,就是来填充这个pc->sockaddr信息,所以叫做peer>init),

回到上面:如何replicationFeedSlaves,肯定是先建立连接,然后发送数据然后等响应。(update:这是我开始的猜测,其实后面并非如此,FeedSlaves就可以看出,他将Slaves是看做和普通client一样的client统一处理的逻辑的,并不会去建立连接,这些连接是之前slave主动建立好的)

那么redisClient里面有保存B的ip/port吗?其实没有那么看redis是如何connect的:事实上,作为主的A根本就没有主动去connect B,而此时还只是将B当作一个client处理,之前B发送给A的SYNC后,A就当B为一个client而已,只不过是一个特殊的client(后续所有命令都给B一份,而命令和响应其实在A看来本质上并没有区别就是一串字符而已),而且B之前主动连接上A的连接是没有端口的,是一个长连接,只不过此时是A主动推送而已,和接受普通client的命令处理后反应响应并没有差异,都是将一堆字符放入到发送连接上而已。

 这里的处理和nginx完全不通:nginx在和upstream交互时候并没有upstream主动给nginx连接过,而需要nginx主动连接upstream(事实上,如果upstream主动给nginx注册过,那么nginx完全可以把upstream当作一个特殊的client对待了)

所以这里可以看到,做为主的redis,其实并没有真正做过connect,只不过是将之前发送过SYNC的“连接”当作一个特殊的连接而已,仅此而已这样就保持处理的统一。(有点类似于注册的机制)

所以之前所说的“主从之间互相为client/server其实并不准确)。

那么redis里面就没有connect把自己当作真正client的场景吗,很显然有,B就是:那么看B这个场景是在哪里发生的,可以看到是在client 2发送了slaveof给B后,B进而触发的,此时看B在什么场景会触发:

在收到slaveof后其实B的处理比较简单只是标记了一下自己的角色并将A的ip/port存了下来,然后直接返回client2了。B后续的处理才会真正去connect A然后发送SYNC,只需要看它是如何使用server.masterhost,server.masterport即可:

显然B是将其放在定时任务serverCron里面的:

redis里面有一个简单的地方就是他主动外连的connect很少,比如只有这种sync master的场景或者其他少量的sentinel/cluster场景,这样他的连接处理相对简单很多。

另外还有一个地方就是,他的主动连接和处理客户端的连接之间没有任何的同步逻辑,如连接master和处理client 2发送来的请求之间没有任何联系。而nginx中一般都是一对一的场景。是什么造就了这个差异:归根结底就是产生响应是否能够本身就处理完成,如果不能就需要处理这种同步关系。而redis很显然没有什么命令需要和外界沟通才能处理的,这样就让他的连接处理逻辑变得很简单了。而且他的connect操作还是在定时器里进行的就更加简单了。

可以跟进去syncWithMaster的处理流程非常直观,没有太多的重入逻辑(而nginx里面由于HTTP协议的特殊性以及需要和同步客户端连接的逻辑会有较多的重入与超时场景)。

还有一点,redis将他与master的被动连接也作为redisClient实例来统一处理了:

 看下redis对这个master连接会有哪些处理:即redis是如何当好一个client的:

可以看到在至少在2.0.5版本中并没有过多的处理。

就是说:如果我要发送数据给对方,我就创建一个redisClient,不管我要发送什么,是请求也好,响应也好。这个redisClient可能更合适地叫法叫做 peer. redisPeer. 

在后续的版本中,从redis主动发送数据的场景还很多了:集中对复制的一些优化都是需要如此。

可以想见当时的场景应该是:

小结一下

1 redis的复制中产生的主从互为client的说法其实不准确,主A其实没有做为从B的client,而是主把从当作和普通的client一样的client看待了,有点类似B注册到A上的例子。

2 redis也有主动connect的场景,即从B主动的场景,就是注册嘛类似。但是redis处理这种被动连接的逻辑非常简单,因为没有需要和主动连接同步的逻辑。

3 而在nginx中,当作为代理的角色nginx的connect是非常多的而且是需要和client connect进行同步的,这样nginx的网络处理就会比redis逻辑会xx。说到底还是因为nginx作为代理不能直接响应而是需要和upstream交流后才能给出响应,而redis对client的每个处理都能够本地完成不需要到外界。

从某种场景来说,nginx的覆盖是>redis场景的覆盖的,而且nginx的扩展场景非常大,redis可以做缓存,nginx也可以实现。

4 另外redis中这种注册的思想(即从B主动注册到A上)其实不仅可以用于复制同步场景,还可以扩展到消息队列场景(即client订阅的场景),无非就是client 1推送的消息 A将其广播给他所有的从,而且这里还可以精细一些,比如将这些订阅的从分为几类,每个从订阅不同的通道,完全可以实现。然后A根据不同的通道推送。(消息队列其实就是复制的扩展思路)

 我在想redis有没有代理的使用场景:即每次都connect外界的场景 ,从5.0版本搜索源码的Connect关键字,也只是出现在了复制、sentinel、cluster场景中,所以,可以看到并不会有。

结合当前从事的工作其实有一些启发点,cdn其实很好的例子,client -> proxy -> cache -> origincache作为server如果有内容就命中返回如果没有就向后端请求。nginx也是可以作为这个逻辑的(代理+缓存角色)

nginx对downstream 连接的处理把控

就是说需要对connectin的处理过程有哪些关注,会发生哪些事件,nginx会如何异常处理这些事件以及原因。

nginx的proxy(upstream)模式把控

其实就是如何协调upstream连接和downstream连接

如何灵活地把nginx当作客户端操作

必然是location.capture+proxy_pass

在一些场景下对于客户的请求处理,需要从外面获取到结果后再试情况来进行处理的场景也是有的,比如远程鉴权的场景(但远程鉴权场景其实可以有缓存,不用每次去connect)。如果面临每次都需要connect的长,你怎么写?通过coscoket裸写吗?在qps高的场景下你确认你的代码能够保障socket不泄漏吗?

最好的办法还是通过子请求的方式+proxy_pass的C处理方式,这样不用担心qps的问题而且非常简单不用担心性能问题。例子:

Lua如何与redis交互

Lua如何与nginx交互

update: 2023.12.31

我对openresty的理解还是比较肤浅,没有从本质上去实际了解lua是如何被嵌入到nginx里面的,但是我知道大概:通过lua的协程方式将nginx里面的一些需要维护调用上下文的api(比如epoll回掉)的给hook掉,然后通过同步的方式进行,这样达到了同步又不会有阻塞的情况,其核心是在ngx_lua中实现了对多个协程的调度的调度维护:每个请求通过协程来承载,当每次事件(网络事件/timer事件)通知到epoll则通过唤醒对应的协程(resume),每次协程进行需要“同步操作”时则通过yelied操作注册相关事件到epoll。这样达到了不阻塞进程又能够方便开发者的目的。但是具体是如何实现的 还需要从代码层面进行展开

在ngx_lua中实现了一些co-socket的api,这部分api通过协程+调度器的方式与epoll系统交互:每次在进行co-socket操作时,调度器将器协程挂起,并注册可读/可写事件到cosocket,当就绪后,则调度器被触发后把协程再次唤醒。在过程中从lua到c,会将控制权交换给c。整个进程没有阻塞。

比如ngx.sleep中,通过注册timer事件,将协程挂起,然后timer到期后触发调度,进而resume协程。

所以在lua中不能使用任何lua原生的阻塞api,由于没有对相关底层做hook直接走的glibc(lua实现),会直接将进程阻塞住。

更形象一点说,ngx_lua的意义在于,搭建起来从nginx世界到Lua世界的穿梭的桥梁:

由于通过C来写业务逻辑实在太复杂晦涩,而lua的强表达力刚好可以满足,但lua的核心能力可能没有那么强,还是需要C来辅助:简而言之,执行流是从C出发,然后到lua中,如果lua有任何不能够完成或者不适合完成的任务,再回到C中来。

 其实一个请求的处理执行流,可能在lua和c中会来来回回好多轮次,最终才完成。

这里将业务逻辑都统一放到lua world进程,好处就是利用lua实现的corotinue机制,可以不用手动去维护上下文,而如果在c中你需要自己手动维护各个请求的上下文 一遍再次恢复运行。

执行流:从c出发,到达lua,最终回到c然后返回给客户(但lua的协程执行结束了最终是要返回到c的)。里面涉及到c对lua的调用,也有lua对c的调用。

请求的入口处只能是从c开始,然后从c world结束,lua是没有权利直接处理请求的。(这有点类似os内核的内核态与用户态。)

那么这里看下协程:

* 其实lua的执行流就是协程,你执行一个lua main脚本就是执行一个lua协程。

* A coroutine is similar to a thread (in the sense of multithreading): a line of execution,  with its own stack, its own local variables, and its own instruction pointer; but sharing global variables and mostly anything else with other coroutines. 

* 实际上,协程和进程/线程没有太多本质区别,都是执行流单元,只不过操作系统OS实现了进程/线程的抢占调度,你不需要去维护调度器而已:

进程场景:

1 每次进程做处系统调用陷入内核,如果是阻塞的systemcall ,内核则会调度出去。

2 如果进程时间片到期了,则内核会直接切换到另一个进程上

只不过这些在用户态都没有感知。

而到协程场景下,lua的协程是非抢占的,除非执行流自己本身将执行权交出去(调用yiled)否则没有被强制交出去的可能。

lua协程的实现:

1 如果看了lua虚拟机的实现(虚拟机实现打造了一个虚拟的cpu,包括pc、寄存器等实体) ,就会知道协程的实现和切换会很简单,详见:对Lua的理解-CSDN博客

另外补充一点,其实进程/线程等这些都是应用层的概念,在cpu层面完全没有这些,cpu就是一条一条地取出指令执行指令(bindly)的执行流而已,而这些概念都是为了更好地利用cpu(更充分)而创造出来的执行单元(不然cpu因为io等慢设备而闲着),是操作系统提供的概念。

而协程也类似,只不过不是通用操作系统提供的,而是应用层的编程语言提供的一套机制。比如lua就是lua vm模拟出虚拟的cpu等实体。

(其实类似多路复用的epoll机制就有点类似os提供的调度机制,只不过两个属于不同的维度而已,中断机制就是os提供的epollwait的调度机制) 

另外,虽然我们为了让cpu不停歇地工作,争取到更多的load,但是如果cpu只是在不停地忙循环,则其实cpu的消耗会比较大。最好的状态是:一旦有真实的任务可以做且就绪,cpu就应该立即起来,一旦没有任何可以就绪的任务,cpu还是应该睡着。这样才是真正的高效率cpu)

回过头来看,很多机制其实都是通用的思想,都是简单的思想,只是在不同维度上的实践应用。

对nginx同类产品的比较

能够作为大面积使用的真正的竞争者应该就是近期的envoy

对多机redis的继续认识和熟悉

在上面讲到了redis的主从同步复制的实现细节,其实说白了就是redis服务之间是可以交互的,而且是有逻辑联系的,这点nginx这种无状态的代理就比不了。那么除了这种主从同步的交互之外,还有哪些是redis服务之间进行的?换言之就是还有哪些场景是redis服务会主动connect的场景呢?主要还有两类:sentinel场景和cluster场景

sentinel场景:

sentinel场景其实是redis的高可用解决方案:比如在一个主多从的架构场景下,如果这个主如果挂了怎么办的问题?sentinel的解决核心思路就是:从多从里面选择一个作为主,其余的从成为这个新主的从。而sentinel其实是一个运行在sentinel模式下的redis客户端。主,这个sentinel会与所有的主/从都connect上作为客户端,对这些主/从发送执行命令,而sentinel根据这些返回的信息,将进行决定怎么样做。

其实可以理解为:sentinel是一个进行调度的客户端。

还有一个点:如果这个sentinel也挂了?其实在一般场景下会有多个sentinel同时运行,那么这时候就出现一个问题,是谁来最终决定在主挂的场景下做决定做地调配:就是这些多个sentinel进行商量并且做出选举,最终选择一个sentinel进行,这是领头的sentinel。而选举过程其实是raft算法的实现:过半当选。

在sentinel场景下,其实各个主从之间并没有除了同步之外的交互逻辑,而主要是sentinel这个“调配客户端”对他监管下的所有主从进行获取信息(也是通过发送命令)并发送命令的方式完成的。

cluster场景:

cluster场景是redis的分布式数据库解决方案:他是水平扩展的思路,主要是为了解决单机redis容量扩展的:比如一个redis提供50G,如果我需要2T的缓存,可以组件2048/50=42个redis实例构建成集群。而这里的就设计到redis之间的交互了。每个集群中的redis都需要对集群内的所有redis的状态都需要清楚,所以就需要相互交互了。

集群其实就是每个redis负责存储一部分数据,在redis场景下将分片出了16384个slot,然后每个redis节点都负责某slot范围,覆盖全部的slot。在实际进行使用时候,根据操作的key的crc32值进行hash到某个slot上,该哪个节点负责就哪个节点处理。

集群是逐步扩大的,开始时A与B组件集群,其实需要A与B进行握手,其实有点类似tcp的三次握手。(MEET、PONG、PING)

此时B的信息A都知道了,A的信息B也都知道了,然后如果A之前还有其他的节点与之构建了集群,这时候A将通过gossip协议将B的信息广播出去,这样逐渐地所有的集群内的节点都知道了B的信息,B也知道了其他所有节点的信息。(有点类似ARP??)

关于集群的场景细节实现其实还是挺多的,引入了一些新的数据结构来表示集群,包括和节点之间的连接实例在集群节点之间其实不是用redisClient表示了而是用的redisClusterLink表示,但其实他们本质上都是一样的抽象表示,而且结构体成员也大同小异,如果到时候有必要了解细节再去深入研究。

从多机场景可以看到,redis即使再多机场景下 对连接的管理还是比较简单的,即使在集群场景下有各自的connect但也只是一种非同步方式、而且主动connect连接数量较少。不过话又说回来,虽然nginx对upstream的处理复杂,但是他不用关注状态,这点其实redis里面关注的较多。

redis实现事务

执行过程:multi  xx yy zz ... exec. 就是说在multi和exec之间的这些命令会被一股脑执行,中间不会去处理其他客户端的命令。这个其实看着还是很好实现的:看到mulit后,则将后续输入的命令存放起来队列,等exec来了后将这些命令从队列取出执行就可以了。其实就是多增加了一个缓存队列。

但接下来对watch的实现:如果watch了某个key,在multi -- exec之间如果这个key有被修改过,则exec则执行失败:这个是怎么实现?想一下。。。

实际上redis是将所有watch的key放入了一个dict中watch_dict,key为这些key名,value为wach这些写key的客户端redisClient列表。在任何执行了修改命令的操作比如set/mset等这些命令执行完后,都需要检查一些这个watch_dict中的key是否存在,然后将这些key后面挂着的redisClient的flag标记打上DIRTY的标记,这样后面再执行exec的时候,就首先去检查下redisClient的这个dirty标记,如果打上了则直接退出失败。

其实想想这个设计还是挺有想法:引入了对连接的管理。其实就是说redis如何对这些连接进行管理。而在nginx中,由于没有连接去共同操作一个对象可能性,所以这些连接之间是无状态的存在也就不会有刚上面说的这种场景。

==========================================================

nginx的生命力,首先要清楚他在整个软件体系中的位置 在网络层次中的位置。

nginx作为一个webserver与七层代理(主要是HTTP),其实七层代理更为官方现如今,webserver一般都用golang/ptyhon/java做后段专门写业务逻辑了。而nginx是主力角色其实就是代理。 

其实HTTP的生命力就决定了nginx的生命力,几乎现在网上的很多流量都是HTTP的(由于其无状态简单、高可扩展性,几乎每种业务都会优先选择HTTP(服务治理选择grpc/thrift不在其中考虑,也就是效率与‘成本’的平衡),那不可避免地就会用到nginx。

既然是代理,可以认为是网关,位置就在业务流量入口处,那么理所应当地可以延伸处 包括 

安全  -- 七层安全几乎都在这上面,包括对流量的清洗啥的,这里更多的是安全业务了。

负载均衡-- 几乎每个业务都会用到(尤其是在云原声场景下,大量后段服务开始容器化部署),包括对负载均衡的各种玩法(其实这个部分没有太多新花样,wrr/hash/ip-hash/chash/least_conn就这几种,负载均衡层面最近玩的比较多的就是服务发现。这方面其实也很成熟了,动态的upstream。 

ssl / http2/quic/等网关 -- nginx可能是现如今跑https流量的主力军吧,配置很方便。其实这种新协议 最好的实现就是在网关,后面的服务不用任何升级就可以享受到这个好处了。

(对HTTP/2的优化真的还要继续。。。/顺便熟悉quic)

然后就是一些业务逻辑需要在网关实现的,类似于apigateway之类(限流/限速/鉴权/流量打标/防护拦截ip等各种你能够想到的需要在网关处实现的业务. (api调用进入的入口网关,主要处理南北流量)

监控/统计/日志:最好莫过于在网关统计这个了。。

上面是从应用层来看到的,7层业务。其实四层和七层的逻辑是类似的(都是运行在端到端)。

而应用层可以说是必须会长期存在下去的,没有应用层,网络的意义也就没有了,所以从这个角度来看,七层网关是会长期存在的一个基础施舍。

要把周边的东西串起来,虽然有你深入的点,但周围的知识框架 你需要串起来,形成一张有逻辑的网,不然只是都是零碎的不好搞。

(如果要深入掌握好网关,还是要深入去玩转nginx,但如果只是看nginx其实你还是缺少实践的,他的代码成本还是很高的,但openresty将其入门门槛从100降低到了30,让其可以通过同步的方式去编程这个nginx性能怪兽,让nginx也变得可爱起来。 所以对luajit/lua-nginx-module的深入,包括对nginx和lua-nginx-module的深入)(彻底玩转nginx 肯定是要掌握这些) (把 lua-nginx-module当作一个or几个nginx的C模块来看待就不容易迷失了)https://github.com/openresty/lua-nginx-module.

另外 随着云原生的浪潮 Kubernetes Ingress的选择也越成为关键。(如今envoy+istion占据了上风)

看到这个讨论很有意思。https://github.com/envoyproxy/envoy/issues/12724

这里也谈到了envoy的扩展也很难上手要写c++,他们也期望能够通过lua来简化envoy的扩展(类似于openresty)。【看他们的讨论 发现外国技术人都好礼貌,每次都是用 could we、if、sorry to这种语气。。】

另外在可编程领域,还是多关注下wasm:。。

Yes, actually there is an internally implementation(we name it EnvoyResty) following the OpenResty design, and we have run it on production for more than half year. Now we hope to contribute this feature to upstream.

FWIW, If we want to have more "extensibility points" to Envoy via Lua, I think we can leverage the proxy-wasm interface here: spec/abi-versions/vNEXT at master · proxy-wasm/spec · GitHub to extend the current Lua support on extending Envoy. Hence it will be consistent with the other efforts for Envoy extensibility.

wasm is definitely the future, but from our experience, it's still a while from production availability. I think the two can go together. Many users want to migrate to envoy-based cloud native api gateways, but they have many Lua plugins based on OpenResty. At NetEase, we use EnvoyResty to migrate Lua plugins. At the same time we track the development of wasm filter, hopefully we can contribute to it someday.

envoy-apisix/table.lua at master · api7/envoy-apisix · GitHub

另外,其实在整个网路图谱中,网管只是作为一个网元存在,还是要深入网络里面去(包括四层及以下,将整个网络掌握,包括vpc等)

1 pool 

most nginx allocations are done in pools, memory allocated in an nginx pool is freed autonmatically when the pool is destryed. Htis porovieds good allocation perfomanaceand makes meory control easy.

a pool internally allocates objects in continous  blocks ofmemory. Once a block is full, a new one is allocated and added to the pool memory block list. Wthen the requested allocation is too large to fit into a block., the request is forwared to the system allocator and the returned pointer is stored in the pool for further deallocation. 

从第二段描述中其实可以很简略地看出pool的架构:就是一个个的block,分配完了这个block就分派下一个block,如果实在有太大的size需要超出了一个block,就直接向system申请了,然后

void* 
ngx_pnalloc(ngx_pool_t *pool, size_t size)
{
    u_char    *m;
    ngx_pool_t    *p;

    if (size <= pool->max) { //即每个block的size
        p = pool->current; //当前的block 

        do {
            m = p->d.last;
            if ((size_t) (p->d.end - m ) >= size) { //该block剩余空间足够分
                p->d.last = m + size;
                return m;
            }
            p = p->d.next;// 当前block剩余空间不够,需要到下个block看看// TODO 这里不移动current么
        } while (p);

        return ngx_palloc_block_pool(pool, size); // 所有block都不够,需要新增加block了
    }  

    return ngx_palloc_large(pool, size); // 超过了block的最大size 需要向system要了直接分配大内存块,不存储在block里面了。
}


//增加一个新block
static void*
ngx_palloc_block(ngx_pool_t *pool, size_t size) 
{

    u_char        *m;
    size_t         psize;
    ngx_pool_t    *p, *new, *current;

    psize    = (size_t)(pool->d.end - (u_char*)pool); //一个block的真实大小(包括头部的overheader) (其实pool->max只是实际能使用的空间)
    m = ngx_memalign(NGX_POOL_ALGINMENT, psize, pool->log); //申请一个block
    if (m == NULL) {
        return NULL;
    }
    
    new = (ngx_pool_t*)m;     //new block

    new->d.end = m+psize;
    new->d.next = NULL;
    new->d.failed = 0;

    m += sizeof(ngx_pool_data_t); //block的头部开销
    m = ngx_algin_ptrm(m, ALGINMENT);
    new->d.last = m + size;

    current = pool->current;
    for (p = current; p->d.next; p = p->d.next) {
        if (p->d.failed++ > 4) {
            curent = p->d.next;
        }
    }

    p->d.next = new;     //挂载new block
    pool->current = current ? current : new; 
    return m;
}
//可以看到current指向的block其实是首次可以被遍历的block(每次分配时)所以在这current之前的block都将不予考虑了(说明几乎已经满了,失败次数每次达到4次)。



static void*
ngx_palloc_large(ngx_pool_t *pool, size_t size)
{
    void        *p;
    ngx_uint_t    n;
    ngx_pool_large_t        *large;

    p = ngx_alloc(size, pool->log); //直接从heap上要了。
    if (p == NULL) {
        retrun NULL;
    }

    n = 0;
    for (large = pool->large; large; large=large->next) {//大内存块看来是链式结构
        if (large->alloc == NULL) {
            large->alloc = p;
            return p;
        }
        if (n++>3) {
            break;
        }
    }
    large = ngx_palloc(pool, sizeof(ngx_pool_large_t));// 这个不会陷入到死循环,large_t的size肯定小于一个block了
    if (large == NULL) {
        ngx_free(p);
        return NULL;
    }

    large->alloc=p;
    large->next=pool->large;
    pool->large = large;  //头插入

    return p;
}
可以看出其实对大内存的分配并没有什么优化,只是将其获取后放入到large链上而已。而且也没有多余空间。











    

而destory其实刚好可以看出pool是如何管理的:

void
ngx_destroy_pool(ngx_pool_t *pool)
{
    ngx_pool_t        *p, *n;
    ngx_pool_large_t    *l;
    ngx_pool_cleaup_t    *c;

    //这是后面要讲到的:在回收pool之前,对挂在该pool上的cleanup钩子做回掉清理
    //在释放内存之际可能有一些需要善后的工作
    for(c=pool->cleanup; c; c=c->next) {
        if (c->handler) {
            c->handler(c->data);
         }
    }

    //释放大块内存:很直观
    for (l=pool->large; l;l=l->next) {
        if(l->alloc) {
            ngx_free(l->alloc);
        }
    }

    //释放各个block: 也很直接
    for (p=pool,n=pool->d.next;/*void*/;p=n,n=n->d.next) {
        ngx_free(p); //释放block
        if (n==NULL) {
            break;
        }
    }

    另外注意到其实large结构体也是分配在block里面的,释放block后也理所当然地被释放了。
}

api:

ngx_palloc(pool, size); -- allocate aligned memory from the specified pool

ngx_pcalloc(pool, size); -- allocate aligned memory from the specified pool with fill it with zeros.

ngx_pnalloc(pool, size); -- allocate unglinged memory from the speicfied pool, mostly used for allocating strings.

ngx_pfree(pool, p) -- free memory that was previously allocated in the specified pool. only allocations that result from requests forwared to the system allocator can be freeed. (从大内存申请出来的才能被这么释放掉)所以可以理解为任何时候都可以调用pfree,但不一定能真正释放掉罢了。

===

chain links(ngx_chain_t) are activley used in nginx, so the nginx pool implementation provides a way to reuse them. The chain filed of ngx_pool_t keeps a list of previoulsy allocated links ready fore reuse. For effecient allocation of a chain link in a pool, use the ngx_alloc_chain_link(pool) function. This function looks up a free chain link in the pool list and allocates a new chain link if the pool list is empty. To free a link, call the ngx_free_chain(pool, cl) function 

为何单做了chain的这个复用而其他结构体没有这个待遇?因为这个结构体使用的太频繁了,故而为了提高效率,将其做了一个复用.

所以搜索nginx里面对cl的分配,几乎都是通过 ngx_alloc_chain_link 申请,通过ngx_free_chain释放:

$grep -rn 'cl = ngx' src/ | wc -l
52

ngx_chain_t*
ngx_alloc_chain_link(ngx_pool_t*pool)
{
    ngx_chain_t        *cl;

    cl =  pool->chain;

    if (cl) {
        pool->chain = cl->next;
        return cl;
    }

    cl = ngx_palloc(pool, sizeof(ngx_chain_t));
    if (cl == NULL) {
        return NULL;
    }

    return cl;
}

#define ngx_free_chain(pool, cl)   //头插
    cl->next = pool->chain;    \
    pool->chain = cl;  


这里有个注意点:chain并没有标记指向哪个pool。

cleanup handlers can be greigstered in a pool. A cleanup handler is a callback with an argument which is called when pool is destroyed. (为何要涉及这个策略). =>  A pool is usually tied to a speicifc nginx object (like an HTTP request) and is destroyed when the object reacheds the end of its lifetime. Registering a pool cleanup is a converient way to release resoures, close file descriptors or make final adjustments to the shared data assocated with the man object(说的很全面概括了) 

To register a pool cleanup, call ngx_pool_cleanup_add(pool, size) which retruns a ngx_pool_cleanup_t pointer to be filed in by the calleer, use the size argument to allocate context of the cleanup handler. 

ngx_pool_cleanup_t *cln;

cln = ngx_pool_cleanup_add(pool, 0);
if (cln == NULL) {/*error*/}

cln->handler = ngx_my_cleanup;
cln->data = "foo"

...

static void
ngx_my_cleanup(void *data)
{
    u_char    *msg = data;
    ngx_do_smth(msg);
}

2 shared memory

Shared memory is used by nginx to share  comman data between processes. The ngx_shared_memory_add(cf, name, size, tag) function adds a new shared memory entry ngx_shm_zone_t to a cycle. The function receives the name and size of the zone. Each shared zone must hava a unique name. If a shared zone entry with the provided name an tag already exsits, the existing zone entry is reused. The function fails with an error if an existing entry with the same name has a diffrerent tag. Usually, the address of the module struct is passed as tag ,making it possible to reuse shared zone by name within one nginx module. 

For allocating in shared memory, nignx provides the slab pool ngx_slab_pool_t 机制

a slab pool for allocating memory is automatically created in each nginx shared zone. The pool is located in the beginnning of the shared zone and be accessed by the expresision( ngx_slab_pool_t*)shm_zone->shm.addr (和常规的pool有类似的构造)

To protect data in shared memory from current access, use the mutex avaialbe in the mutex field of ngx_slab_pool_t. A mutex is most commonly used by the slab pool while allocating and freeing memory, but it can be used to protect any other user data structes allocated in the shared zone. (slab上有配置锁)

3 logging

Normally, loggers are created by existing nginx code from error_log directives and are availiabe at nearly every stage of processing in cycle, configuration, client connection and other objects.

A log message is formatted in a buffer of size NGX_MAX_ERROR_STR(currently 2048 bytes) on stack. The message is prepended with the severity level, process ID(pid), connection ID(stored in log->connection), and the system error text.  For non-debug messages log->handler is called as well to prepend more specific information to the log message. HTTP module sets ngx_http_log_error() function as log handler to log client and server address, current action(stored in log->action) ,client request line, server name etc. 

void 
ngx_http_init_connection(ngx_connection_t *c)
{
    ...;
    c->log->connection = c->number ; //connection id
    
    c->log->handler = ngx_http_log_error;
    c->log->action = "rading client request line";
    ...;
}

static u_char*
ngx_http_log_error(ngx_log_t* log, u_char *buf, size_t len)
{
    u_char            *p;
    ngx_http_request_t *r;
    ngx_http_log_ctx_t    *ctx;

    //pre-append action ;
    if (log->action) {
        p = ngx_snprintf(buf, len, " while %s", log->action);
        len -= p - buf;
        buf = p;
    }

    ctx = log->data;
    // pre-append client address
    p = ngx_snprintf(buf, len, ", client: %V", &ctx->connection->addr_text);
    len -= p-buf;

    // pre-append server address (listening addres)
    r = ctx->request;
    if (r) {
        return r->log_handler(r, ctx->current_request, p, len);
    } else {
        p = ngx_snprintf(p, len, " , server: %V", &ctx->connection->listening->addr_text);
    }
    return p;
}

这里要注意区分:并不是只有在请求结束时才会logging(针对http场景下),而在任意时候都可以logging. 只不过在http的log阶段他在logging请求时也是调用的通用的写log api : ngx_log_error之类的底层API。 

4 cycle

A cycle object stores the nnginx  runtime context created from a specific configuration . The current cyclee is referenced by the ngx_cycle global variable and inherited by nginx worker as the start. Each time the nginx configuration is reloaded, a new cycle is created from the new nginx configuration, the old cycle is usually deleted after the new one successfully created.

A cycle is created by the nginx_init_cycle function, which takes the previous cycle as its argument. (为啥需要这样) ==> inherits as many resources as possiable from the previous cycle.

5 buffer

For input/output opertaions, nginx provides the buffer type ngx_buf_t . Normally it's used to hold data to be written to a destination or read from a source, Memory for the buffer is allocated separately and is not related to the buffer structuce. 

* start, end -- The boundaries of the memory block allocated for the buffer.

* pos, last -- The boundaries of the memory buffer, norlay a subrage of start .. end

* tag -- Uniqu value used to distinguish buffers, created by different nginx modules, usually for the purpose of buffer reuse.

* temporary -- Flag indicating that the buffer refererences writeable memory ( 这个buffer是writeable的)

* memory -- Flag indicating that the buffer refreneces read-only memory (和上面的对比:该buffer只能读取)

* flush -- Flag indicating that all data priror to the buffer need to be flushed (有点类似于tcp里的push)

* recycled -- Flag indicating that the buffer can be reused and needs to be consumed as soon as possible. 

* last_buf -- Flag indicating thaht the bufer is the last in outpout 

* last_in_chain -- Flag indicating that there are no more data buffers in a request or subrequest. 

注意区别 last_buf / last_in_chain: last_in_chain表示的是在当前的chain中是最后一个buf,而last_buf表示是整体的最后一个buf。即last_in_chain不一定是last_buf,但last_buf肯定是last_in_chain。(因为buf可能分了多个chain)

For input and output operations buffers are linked in chains (在输入输出场景下,buffer总是搭配chain一起使用),为何:因为单独的buf可以看到没有回收和管理机制,而搭配了chain就方便做相关管理了:chain在前面说过一般都是从pool中进行分配和挂载重复利用。

buf的使用很灵活感觉,还是需要结合实际场景。(任何input/outpout的地方都会看到buf的使用) 

6 Networking

Connection

The connection type (ngx_connection_t) is a wrapper around a socket descriptor. 

* fd -- Socket descriptor

* data -- Aribtary connection conetxt. Normally it is a pointer to a higher-levle object built on top of the connection, such as an HTTP request or a Stream session.

* read,write -- Read and write events for the connection

* recv send recv_chain send_chain -- I/O operation handler for the connection

* ssl -- SSL context for the connection 

* reuseable -- Flag indicating the connection is in a state that make it eligible for reuse (就是说该c可以被回收了)

* close -- Flag indicating that the connection is being reused and needs to be closed (正在被回收中,而且需要关闭了)这个标记通常在c的read_handler中被判断【在真正drain回收之前做善后)

All connection structures are precreated when a worker starts and store in the (connections) field of the cycle object.

connection的回收与重用:

Because the number of connections per worker is limited, nginx provides a way to grab connections that are currently in use. ( 就是说正在被使用的这些c,可以被grab过来重用,比如一些空闲的长连接)

ngx_reusable_connection(c,1) sets the reuse flag in the connection strucutre and inserts the connection into reuseable_connection_queue of the cycle. Whenever ngx_get_connection find out there are no available connections in cycle's free_connections list, it calls ngx_drain_connections() to release a specific number of reusable connections. (就是说,这些被reusalbe的c,其实还是没有断开的,而且放在了 reuseable_connection_queue队列上,此时调用drain,就会将其释放掉并放入free队列以便使用,俗称挤破),for each sunc connection, the close flag is set and its read handler is called which is supposed to free the connection by calling ngx_close_connection(c) and make it available for reuse.  (在close前,这些被drain的connection还会去尝试read_handler 用于善后,看是否有事件需要处理)

//connection为何需要复用:因为c是有限的,如果一些没有正在流数据的c(比如keepalive下,比如刚建立链接但没有收到数据下), 其实可以挤破掉拿来做实际的工作

ngx_connection_t*
ngx_get_connection(s,log)
{
    c=ngx_cycle->free_connections;
    if(c==NULL){
        ngx_drain_connections(); //注意看这里,如果发现free链上没有了,那么是不是就没办法?nginx会尝试从目前在keepalive/or 没有数据的但已经建立链接的c上的一些连接上挤破一批(32)连接来用,因为keepalive的连接目前没有数据,挤破的代价是最小的
        c=ngx_cycle->free_connections;
    }
    if(c==NULL){
        ngx_log_error(ALERT,0, "connection not enoguth");
        return NULL;
    }
    ...;
    ngx_cycle->free_connections=c->data; //摘下 ,每次都从队头拿
    ngx_cycle->free_connections_n--;
    return c;
}

ngx_free_connceciont(c)
{
    c->data=ngx_cycle->free_connections; //每次都放在队头
    ngx_cycle->free_connections=c;
    ..
}


//至于drain机制是如何工作的?
//这里有ngx_cycle->reuseable_connection_queue这个链上面就是放着这些keepalive的c,是可以从这上面挤破的
static void
ngx_drain_connections(void)
{
    for(i=0;i<32;i++){
        if(ngx_queue_empty(&ngx_cycle->reuseable_connections_queue)){//没有
            break;
        }

        //从最末尾开始:可以猜测这个链表每次加入都是加载表头,最末尾的是最最长时间keepalive的连接【占坑最长】
        q=ngx_queue_last(&ngx_cycle->reuse_connection_queue);
        c=ngx_queue_data(q,ngx_connection_t,queue);

        //这里是挤破的关键:将close标记设置为1,同时调用c->read->handler
        //目前来看该链上所有c的read->handler都被设置为了ngx_http_keepalive_handler(http框架内,四层的不讨论)
        c->close=1;
        c->read->handler(c->read);
    }
}

//c的指针函数实现的多态:在七层和四层的函数指着可以不同,且设置为static
static void
ngx_http_keepalive_handler(rev)
{
    c=rev->data;
    if(rev->timedout || c->close) {                //这里:从drain调过来就可以知道,该连接会被clsoe掉
        ngx_http_close_connection(c);
        return;
    }

    ...;
}

//将正http keepalive的c掐掉
ngx_http_close_connection(c)
{
    c->destroyed=1;
    pool=c->pool;
    ngx_close_connection(c);
    ngx_destroy_pool(pool);
}

//这里实现c的回收:回收需要做的事情,1 从ngx_cycle->reuse_connection_queue队列中删掉,因为不再是ka c了,2 调用free 归还到ngx_cycle->free_connections
ngx_close_connection(c)
{
    //释放c前的一些前置工作:将对应的事件从timer树/posted队列/epoll拿掉防止无效事件被踩到 

    if(c->read->timer_set) ngx_del_timer(c->read);
    if(ngx_del_conn) ngx_del_conn(c,);
    if(c->read->posted)ngx_delete_event(c->read);
    if(c->write->posted)ngx_delete_event(c->write);

    c->read->closed=1;c->write->closed=1;

    //从ngx_cycle->reuse_connection_queue队列中删掉 (防止被再次回收)
    ngx_reuseable_connection(c,0);


    //归还到ngx_cycle->free_connections
    ngx_free_connceciont(c);
}


回头看 c是如何被加入ngx_cycle->reuse_connection_queue链的:每次被keepalive时加入:
ngx_http_finalize_connection(r)
{
    if(r->keepalive&&clcf->keealive_timeout>0){
        ngx_http_set_keepalive(r);
        return;
    }
    ngx_http_set_keepalive(r)
    {
        c=r->connection;
        rev=c->read;
        rev->handler=ngx_http_keepalive_handler;

        ....;

        c->idle=1;
        ngx_reuseab_connection(c,1);     //加入ngx_cycle->reuse_connection_queue队列
        ngx_add_timer(rev,clcf->keepalive_timeout);
        ...
    }
}


ngx_reuseable_connection(c, reusable)
{
    //这里将 0/1的逻辑放在一起了,并不是那么直观,其实更直观的方法应该是拆为两个函数:
    //ngx_reusable_connection(c, 1) ==> ngx_make_connection_be_reuseable    (可以被drain挤破)    (一般发生在连接上没有数据了 需要把连接keepalive情况下)
    //ngx_reusable_connection(c, 0) ==> ngx_make_connection_not_be_resuable (还需要继续自己使用) (一般发生在keepalive的连接上突然收到收据了,需要继续处理的场景下, or从reuseable队列中已经摘下来了))

    //先不管如何处理这个c, 首先check他的状态,决定继续可以挤破/不可挤破之前都需要恢复
    if (c->reusable) {
        ngx_queue_remove(&c->queue);
        ngx_cycle->resuable_connections_n--;
    }

    c->reusable=reusable;

    if(reusble) {
        ngx_queue_insert_header((ngx_queue_t*)&ngx_cycle->resuable_connections_queue, &c->queue);
        ngx_cycle->resuable_connections_n++;
    }
}


//至此:一个连接尽力了从空闲池->被使用->被keepalive->被挤破->空闲池的整个生命周期

HTTP client connections are an example of reuselabe connections in nginx; they are makred as reusalbe until the first request byte is received from the client (也就是说,刚建立的链接,如果在没有收到客户端数据时候,他是被marked 为reusable的:

```

//建立tcp链接后被调用初始化该c
ngx_http_init_connection(ngx_connection_t *c)
{
    
    ....;

    ngx_add_timer(rev, c->listening->post_accept_timeout);

    ngx_reusable_connection(c, 1) ;// 此处marked 为reusable

    ngx_handler_read_event(rev, 0); //注册可读等待
}
    
当数据到来了,就要摘除了:
static void
ngx_http_wait_request_handler( ngx_event_t *rev)
{
    ...;

    n = c->recv(c, b->last, size);


    if ( n== NGX_AGIN) {
            
        if (!rev->timer_set) {
            ...;
        }
        ngx_handle_read_event(rev,0);//再次加入epoll

        /* 一般调用pfree是为了释放大块内存归还给system(当然如果确认是大内存的话
        所以pfree不一定能够返回oK(如果不是大内存的话),要看实际情况
        // we are trying not hold c->buffer's memory for an idle connection 
        if(ngx_pfree(c->pool, b->start) == NGX_OK) {
            b->start == NULL;
        }

        return;
    }

    // n > 0
    ngx_reusable_connection(c, 0) ;// 读到数据了,不能被挤破了需要摘下来

    ...;
}
    

Events

event

Debugging memory issues

To debug memory issues such as buffer overruns or use-after-free errors, you can use the AddressSanitizer(Asan) surrported by some modern compilers. To enable Asan with gcc and clang, use the -fsanitize=address compiler and linker option. When building nginx, this can be done by adding the option to --with-cc-opt and --with-ld-opt parameters of the configigure script.

Since most allocations in nginx are made from nginx internal Pool, enabling Asan may not always be engouh to debug memory issues. THe internal pool allocates a big chunk of memory form the system and cuts smaller allocations from it. However this mechanism can be disabled by setting the NGX_DEBUG_PALLOC macro to 1, in this case, allocations are passed directly to the system allocatir giving it full control over the buffers boundaries. 

void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
#if !(NGX_DEBUG_PALLOC)
    if (size <= pool->max) {
        return ngx_palloc_small(pool, size, 1);
    }
#endif

    return ngx_palloc_large(pool, size);
}

这里如果NGX_DEBUG_PALLOC置位,后续所有的分配都当作是large分配机制(即直接从system分配了)

Common pitalls 

Writing a C modules

In most cases your task can be accomplished by creating a proper configuration. (通过配置能够搞定。。。) i

Lua让nginx可编程了,确实给nginx续了很长一命。

Global Variables

Avoid using global variables in your modules. Most likely this is an error to have a global viarable. Any global data should be tied to a configuration cycle and be allocated from the corresponding memory pool  . This allows nignx to perform graceful configuration reloads. An attemp to use global variables will likey break this feature, (就是说尽可能避免在堆上分配全局变量,尽量将全局变量分配到cycle->pool上去,这样能够保证在reload的时候能保持一致性)

Manual Memory Managment

instead of dealing with malloc/free approach which is error prone, learn how to use nginx pool. A pool is created and tied to an object -- configuration, cycle, connection or http request . When the object is destroyed, the associated pool is destroyed too. So when work with an object, it is possible to allocate the amount needed from the correspoding pool and don't crea about freeing memory even in case of errors. (就是说,无论何时需要分配内存了都应该从pool中分配,前提是你要知道你的数据的生命周期随着哪个object,比如提到的cycle,connection ,request等)

Therads

不要尝试在nginx中使用threads 

1 most nginx functions are not thread-safe.   It is expected that a thread will be executing only system calls and thread-safe library functions. 

如果你想要使用threads,尽可能地使用timer (if you want to run some code that is not related to client request processing, the proper way is to shcedule a timer in the init_process module handler and perform required actions in  timer handler. 

那为何nginx要实现threads:nginx makes use of threads to boost IO-realated operations, but this is a special case with a lot of limitations( 只是为了提高IO能力而做的一种有限制的尝试)

HTTP Requets to Extenal service 

如果想要在nginx发起请求到第三方,如果自己手写io其实是比较棘手的,如果你还用了类似libcurl这种阻塞库就更加错误了。其实这种任务nginx自己就可以完成,答案是 subrequest。 (当然lua出来后就很方便了)

There are two basic usage scenarious when a an external request is needed.

* in the context of processing a client request

* in the context of a worker process (eg: timer)

in the first case, the best is to use subrequest API. 其实很直接就是调用subrequest的api到一个location,然后在这个location里面做配置比如proxy_pass让nginx来完成任务(其实这个也很经常用,比如在异步限速模块里面,用cosocket发起请求到locatio,再有location proxy到远程限速中心去)。 如果用C来实现可以参考的例子就是 auth模块( ngx_http_auth_request_module) (这个也是个鉴权模块。。)

location /private/ {
    auth_request /auth;
    ...
}

location = /auth {
    proxy_pass ...
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
    proxy_set_header X-Original-URI $request_uri;
}

看了下auth模块的代码,关键点在: 

static ngx_int_t 
ngx_http_auth_request_handler(ngx_http_request_t *r) //在access阶段插了一个handler
{ 
 .....;

 if (ctx != NULL) {
   ///对子请求来的响应结果进行处理决定主请求是否通行
        if (ctx->status == NGX_HTTP_FORBIDDEN) {
            return ctx->status;
        }
        if (ctx->status >= NGX_HTTP_OK
            && ctx->status < NGX_HTTP_SPECIAL_RESPONSE)
        {
            return NGX_OK;
        }
        。。。
  }

 ps->handler = ngx_http_auth_request_done;
 ps->data = ctx;

 if (ngx_http_subrequest(r, &arcf->uri, NULL, &sr, ps,        //发起子请求
                            NGX_HTTP_SUBREQUEST_WAITED)
        != NGX_OK)
    {   
        return NGX_ERROR;
    }   

 } 
} 

For the second case, it is possible to use basic HTTP client functionlity available in nginx. For example OCSP module impletns simple HTTP client. (话说,用Lua真的解决了好多问题) [这个太复杂了] 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值