美文网首页
Nginx 学习笔记1 开篇

Nginx 学习笔记1 开篇

作者: 董泽润 | 来源:发表于2018-06-13 18:27 被阅读601次

    做为一个后端开发、运维人员,nginx 属于标配,每个人都或多或少接触过,甚至是重度依赖。也有例外,比如某滴,微服务都是用 thrift 交互的。一直好奇 Nginx 是如何做到这么高性能,最近粗略的阅读源码,限于功底太差,一头雾水... 先挖这个系列的坑,希望能坚持写下去...

    编程思维

    首先是编程思维跨度较大,最近两年一直从事 go 开发,goroutine 降低了高并发编程的门槛,尤其是对原有 c/c++ 从业者。语法简洁,又没兼容性的历史包袱,让很多以为高大上的 epoll 不再神秘,也不用像 java 开各种 thread pool, 真是广大 IT从业者弯到超车必学的语言,所以很多大拿鄙视 go 就不奇怪了。但是带来编程效率提升最大的,还是这种无阻塞的同步编程模式:

    fd = socket();
    read(fd, buf, len);
    write(fd, buf, len);
    

    对于网络收发数据,上面的模式就是 go 的同步无阻塞编程,非常符合人的直觉,书写流程不被打断。脏活累活都是由 go 的 GMP 调度模型帮你做,原理可以参考 qyuhen 雨痕老师的书。

    对于 c 开发来说,操作系统完全把底层暴露给开发者,需要自己操作 epoll 注册事件。Nginx 代码相比 Redis, 复杂度又上了个等级。同是对一个 fd 读写,如果是 c 就写得很麻烦:

    #filename1.c
    fd = socket();
    epoll_mod(epoll_fd, add_op, fd,epoll_read|epoll_write);
    
    #filename2.c
    epoll_fd = epoll_create(1024);
    events = epoll_wait()
    for(i:=0;i<len;i++){
      process(events[i]);
    }
    
    #filename3.c
    void *process(event) {
      handler = event.data.callback;
      handler(event.data.fd);
    }
    

    打开 socket 后,把 fd 注册到 epoll,等待监听网格事件。然后在程序 main 处会有 epoll_wait 在阻塞等待事件,events 是待处理事件的集合,处理完 read event 后,还要把 write event 注册。怎么区分事件,是由谁产生的呢?一般 event 都是自定义的结构,里面有 (void *) data 数据存放业务信息,比如上面提到的 callback 回调,文件描述符等等,Nginx 保存的是 ngx_connection_t 结构体。还要处理很多 epoll 细节,对于 ET、LT 不同触发模式的原理还要清楚,接下来的笔记再写这块。

    总结来讲,串行的业务流程,打散分布到 N 处源码文件。对于开发者习惯就好了,但是对于业务接手、调试是一个灾难。Nginx 找回调函数看得头都大了,各种叫 handle 的回调,sublime 也蒙逼了

    模块化开发

    Ningx 最受欢迎的一个功能就是模块化,而且 copyright 授权最开放,官方现有的大量模块都是开发者贡献的。值得仔细研究下,但什么是模块化开发呢?先看一个例子:

    func main() {
        flag.Parse()
        log.SetLogger(logger.Sugar())
        if err = n.Run(); err != nil {
            log.Errorf(err.Error())
            return
        }
        log.Infof("cronsun %s service started, Ctrl+C or send kill sign to exit", n.String())
        // 注册退出事件
        event.On(event.EXIT, n.Stop, conf.Exit, cronsun.Exit)
        // 注册监听配置更新事件
        event.On(event.WAIT, cronsun.Reload)
        // 监听退出信号
        event.Wait()
    }
    

    上面的例子来自某开源软件的启动部份,可以看到开发流程基本都是

    1. flag parse 获取参数,比如 config 文件路径
    2. 初始化日志库
    3. 初始化主程序,根据 config 初始化外围 IO 等等
    4. 启动主程序,开启一个死循环
    5. wait 等待退出信号

    这有什么问题么?大家平时都是这么写的,程序员拿着老板的工资,代码都读懂能上手也是份内的事情。但是对于开源软件,尤其是 nginx 现在 19w 行代码,太过于庞大,如果我只想开发个 auth_basic 功能还需要了解所有代码,代价太大,pr 的水平不敢想象,敢提官方也不敢接收啊,要求每个参差不齐的人都是 Igor Sysoev 也不现实。另外,c 语言真恶心,字符串复制都能写好几行):

    言规正传,nginx 在三个层面做了模块化处理,使得开发者,只需要写各个层面的函数就可以。

    1. 配置文件解析。
    worker_processes  1;        #nginx worker 数量
    events {
        worker_connections 1024;
    }
    http {
    server {
            listen 6699;
            location /sleep_1 {
                default_type 'text/plain';
                content_by_lua_block {
                    ngx.sleep(0.01)
                    ngx.say("ok")
                }
            }
    }
    

    nginx conf 蛮有意思的,就像一个自解释的脚本语言。解析时每遇到关键字,比如 listen、location、server, 就会调用相应的 set 回调函数解析,如果是语句块,比如 server, 就会递归的去解析...依次类推

        { ngx_string("server_names_hash_bucket_size"),
          NGX_HTTP_MAIN_CONF|NGX_CONF_TAKE1,
          ngx_conf_set_num_slot,
          NGX_HTTP_MAIN_CONF_OFFSET,
          offsetof(ngx_http_core_main_conf_t, server_names_hash_bucket_size),
          NULL },
    
        { ngx_string("server"),
          NGX_HTTP_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS,
          ngx_http_core_server,
          0,
          0,
          NULL },
    

    上面部份 http core 配置,代码文件见 src/http/ngx_http_core_module.c, 对于 server 的解析会调用 ngx_http_core_server 函数,递归解析。另外,http 模块由于比较复杂,还会有 ngx_http_module_t 多态类型用来控制生成配置。

    1. 资源的生成和释放。

    这一部份很有意思,不同模块的作用域是不同的,需要依赖的资源也是不同。平时写代码,只要在 main 里初始化,然后退出时释放即可。Nginx 不可能让开发者随变写,心智负担也很大,所以有了 ngx_module_t 多态类型的实现

        ngx_int_t           (*init_master)(ngx_log_t *log);
        ngx_int_t           (*init_module)(ngx_cycle_t *cycle); 
        ngx_int_t           (*init_process)(ngx_cycle_t *cycle);  
        ngx_int_t           (*init_thread)(ngx_cycle_t *cycle); 
        void                (*exit_thread)(ngx_cycle_t *cycle);
        void                (*exit_process)(ngx_cycle_t *cycle); 
        void                (*exit_master)(ngx_cycle_t *cycle); 
    

    每个模块如有必须,写相应的回调函数,就会在不同阶段生成模块所需的资源。比如,init_process 函数,http core 模块就会为所有 nx_listening_t 监听设置回调函数 ngx_accpt_event, 这样有网络连接请求 nginx 时,就会 accept 生成连接。

    1. http 执行阶段的回调。
    typedef enum {
        NGX_HTTP_POST_READ_PHASE = 0,
        NGX_HTTP_SERVER_REWRITE_PHASE,
        NGX_HTTP_FIND_CONFIG_PHASE,
        NGX_HTTP_REWRITE_PHASE,
        NGX_HTTP_POST_REWRITE_PHASE,
        NGX_HTTP_PREACCESS_PHASE,
        NGX_HTTP_ACCESS_PHASE,
        NGX_HTTP_POST_ACCESS_PHASE,
        NGX_HTTP_TRY_FILES_PHASE,
        NGX_HTTP_CONTENT_PHASE,
        NGX_HTTP_LOG_PHASE
    } ngx_http_phases;
    

    nginx 即然是服务于 http 请求的,那么第三方模块也必然要在某一特定时刻执行。这里 nginx 划分成了 11 个 phase, 比如大家熟悉的 allow/deny 控制模块注册在 NGX_HTTP_ACCESS_PHASE 阶段。大部分模块都集中在 NGX_HTTP_CONTENT_PHASE 生成内容阶段。

    static ngx_int_t
    ngx_http_access_init(ngx_conf_t *cf)
    {
        ngx_http_handler_pt        *h;
        ngx_http_core_main_conf_t  *cmcf;
    
        cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);
        h = ngx_array_push(&cmcf->phases[NGX_HTTP_ACCESS_PHASE].handlers);
        if (h == NULL) {
            return NGX_ERROR;
        }
        *h = ngx_http_access_handler;
        return NGX_OK;
    }
    

    小结

    先写下感观的想法,还有太多内容要消化,进程间通信、超时机制、线程池、epoll 处理大文件、aio、http 完整流程、热启动、upstream 等等,每一处争取写个总结。当然,最好是结合 gdb 边调试边分析更深刻。

    相关文章

      网友评论

          本文标题:Nginx 学习笔记1 开篇

          本文链接:https://www.haomeiwen.com/subject/stdjeftx.html