0%

Nginx源码简析(一)

最近在做网关相关工作,和Nginx关系比较密切,故了解下它的具体实现。用cloc简单看了下,Nginx总共的C代码不到10W行,基本和Redis差不多,可以说是非常少了,而且Nginx的代码非常经典,以快速和高效著称,值得一试。考虑到Nginx的功能太基础,大多数国内互联网公司都使用的是淘宝开发的基于Nginx扩展出来的Tengine,其中包括负载均衡算法支持vnswrr(虚拟节点平滑权重轮询),以及主动healthcheck功能(Nginx的healthcheck功能需要购买Nginx Plus才可以使用),因此以Tengine作为分析对象。

一、环境准备

先运行下面的命令,下载和配置编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 系统环境 macOS Big Sur

# 安装openssl依赖
cd ~/software
wget https://github.com/openssl/openssl/archive/refs/heads/master.zip
unzip master.zip
cd openssl-master
./config -d
make

# 下载阶段
cd ~/software
# 如果是nginx可以下载 https://nginx.org/download/nginx-1.8.1.tar.gz
wget https://github.com/alibaba/tengine/archive/refs/heads/master.zip
unzip master.zip
cd tengine
mkdir -p workspace/logs
# html 目录里面有一些静态文件例如: index.html
cp -rf html workspace/

# 编译配置阶段
brew install pcre -y #Nginx rewrite功能的一个依赖,正则表达式相关
./configure --prefix=./workspace --with-debug --add-module=./modules/ngx_http_upstream_vnswrr_module --add-module=./modules/ngx_http_upstream_check_module --with-openssl=/Users/danwu/software/openssl-master
# 上面的命令会在obj目录下生成Makefile文件

把生成的Makefile中default改成all(或者修改Clion的preference中Makefile的build target从all改成default)
然后,使用Clion打开Makefile文件,选择Open as Project,注意要把弹出的Clean命令勾选去掉,不然执行clean就把Makefile删掉了。
点击左上角绿色的锤子执行编译,通过编译日志中的-g选项可以确认是带debug信息的编译。

在conf/nginx.conf配置文件如下,其中master_process off能确保单线程运行(不添加worker),这个是可以debug的关键。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#user  nobody;
worker_processes auto;
# 关闭后台进程模式,运行在前台
daemon off;
# 单进程模式,只在debug的时候关闭
master_process off;

error_log logs/error.log;
error_log logs/error.log notice;
error_log logs/error.log debug;
error_log "pipe:rollback logs/error_log interval=1d baknum=7 maxsize=2G";

pid logs/nginx.pid;

events {
worker_connections 1024;
}

http {
include mime.types;
default_type application/octet-stream;

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log logs/access.log main;
access_log "pipe:rollback logs/access_log interval=1d baknum=7 maxsize=2G" main;

sendfile on;
#tcp_nopush on;

#keepalive_timeout 0;
keepalive_timeout 65;

#gzip on;

upstream hello {
vnswrr;
server 127.0.0.1:9090 weight=5;
server 127.0.0.1:9090 weight=2;
check interval=3000 rise=2 fall=5 timeout=1000 type=http;
check_keepalive_requests 100;
check_http_send "HEAD /healthcheck HTTP/1.1\r\nHost: localhost:9090\r\n\r\n";
check_http_expect_alive http_2xx http_3xx;
}

server {
listen 9999;
server_name localhost;

location / {
root html;
index index.html index.htm;
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}

location /hello {
proxy_pass http://hello;
}
}
}

编辑Run -> Edit Configurations,选择target为all或者default/build,然后Executable为obj/nginx,添加启动参数

1
-c /Users/wudan03/software/tengine/conf/nginx.conf

为了让upstream生效,我们起一个golang的http server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 通过 go run main.go 运行即可
package main

import (
"fmt"
"log"
"time"
"net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
time.Sleep(100 * time.Millisecond)
fmt.Fprintf(w, "Hello World!")
}

func healthcheckHandler(w http.ResponseWriter, r *http.Request) {
time.Sleep(100 * time.Millisecond)
fmt.Fprintf(w, "Ok!")
}

func main() {
fileServer := http.FileServer(http.Dir("./static"))
http.Handle("/", fileServer)
http.HandleFunc("/hello", helloHandler)
http.HandleFunc("/healthcheck", healthcheckHandler)

fmt.Printf("Starting server at port 9090\n")
if err := http.ListenAndServe(":9090", nil); err != nil {
log.Fatal(err)
}
}

然后在ngx_http_request.c:ngx_http_process_request方法中打断点,并点击绿色甲壳虫开始debug,使用下面的curl触发调用

1
curl "http://127.0.0.1:9999/index.html"

通过上述curl请求验证程序在断点处停止执行

二、http请求处理流程

首先需要明确几个基本概念

a. 多进程/多线程

Nginx使用1个Master + N个Worker的方式运行,Worker数量一般为CPU数量,Worker是通过fork创建出来的(线程是pthread_create创建出来的),这里使用进程而不是线程的原因是,地址空间是隔离的,不会互相影响,不存在竞争,因为线程之间是可以互相共享数据的,典型的例子是负载均衡如下一篇讲到的round robin算法,每次都要记录轮询到了哪个位置,如果是并发的就需要加锁,性能就会很差。
问:Redis为什么不能像Nginx一样多线程?
答:Nginx本身是stateless的,不存储实际的数据,数据都来自配置文件,可以每个进程维护一份,而Redis本身需要读写数据,如果多进程,实际是需要加锁的。我们可以通过在一台机器上部署多个Redis实例来充分利用CPU

b. NIO(epoll/kqueue)

Nginx使用的是经典的事件驱动的NIO模型,Linux下epoll/macOS下kqueue来处理外来请求,并通过hander来绑定事件触发之后的回调函数。

c. socket

bio socket server的典型流程是:

  1. 调用socket系统调用,创建一个fd(File Descriptor)
  2. 调用bind系统调用,将第一步创建的socket绑定到本地的端口
  3. 调用listen系统调用,开始监听端口上新来的连接
  4. 在死循环中调用accept系统调用,会阻塞知道有新的连接过来,会返回一个新的fd,程序创建新的线程或者fork新的进程,在里面处理这个fd上的数据读写
    这个过程只是要说明一件事,监听端口的fd和新进来的请求的fd不是同一个,因此在监听端口的fd的读操作的handler中处理新连接即可,然后在新请求的fd的读写操作的handler中分别做读写数据的处理

Nginx处理upstream请求的大致流程如下图
nginx process overview
整个流程通过NIO eventloop连接起来,不是太好串联,其中Step8应该是从eventloop中出来的,图里画不下了。
其中Step7主要是发请求给proxy,而Step8主要是将proxy发回的数据写回Downstream(即连到nginx的client)

三、总结

整理的流程非常复杂,但是我们只要抓到设置handler的地方就能找到下个事件的处理函数,同时要紧跟添加事件的地方。整个代码还包含了缓存、过滤等逻辑,以及处理静态文件和其他协议、以及https的请求的过程,这里只介绍了个大概,按需处理,后面两期分别介绍目前比较关注的负载均衡和healthcheck机制。

四、Reference

  1. Tengine
  2. linux下gcc编程10-clion编译调试nginx
  3. Nginx源码分析 - HTTP模块篇 - HTTP Request解析过程
  4. Nginx源码分析
如果您觉得这些内容对您有帮助,你可以赞助我以提高站点的文章质量