做为一个后端开发、运维人员,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()
}
上面的例子来自某开源软件的启动部份,可以看到开发流程基本都是
- flag parse 获取参数,比如 config 文件路径
- 初始化日志库
- 初始化主程序,根据 config 初始化外围 IO 等等
- 启动主程序,开启一个死循环
- wait 等待退出信号
这有什么问题么?大家平时都是这么写的,程序员拿着老板的工资,代码都读懂能上手也是份内的事情。但是对于开源软件,尤其是 nginx 现在 19w 行代码,太过于庞大,如果我只想开发个 auth_basic 功能还需要了解所有代码,代价太大,pr 的水平不敢想象,敢提官方也不敢接收啊,要求每个参差不齐的人都是 Igor Sysoev 也不现实。另外,c 语言真恶心,字符串复制都能写好几行):
言规正传,nginx 在三个层面做了模块化处理,使得开发者,只需要写各个层面的函数就可以。
- 配置文件解析。
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 多态类型用来控制生成配置。
- 资源的生成和释放。
这一部份很有意思,不同模块的作用域是不同的,需要依赖的资源也是不同。平时写代码,只要在 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 生成连接。
- 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 边调试边分析更深刻。
网友评论