Nginx性能优化总结 (2019-03-28)

 目录

1.Gzip 压缩

2.Expires缓存时间

3.事件处理模型优化use epoll

4.高效文件传输模式sendfile on

5.优化服务器域名的散列表大小

6.优化worker服务进程数

7.FastCGI参数调优

8.Log日志优化

9.资源防盗链

10.限制HTTP的请求方法

11.控制客户端请求速率

12.HTTPS配置优化

13.优化配置及详细注释事例

 


 

Gzip压缩:

#修改配置为 gzip on; #开启gzip压缩功能 gzip_min_length 10k; #设置允许压缩的页面最小字节数; 这里表示如果文件小于10个字节,就不用压缩,因为没有意义,本来就很小. gzip_buffers 4 16k; #设置压缩缓冲区大小,此处设置为4个16K内存作为压缩结果流缓存 gzip_http_version 1.1; #压缩版本 gzip_comp_level 2; #设置压缩比率,最小为1,处理速度快,传输速度慢;9为最大压缩比,处理速度慢,传输速度快; 这里表示压缩级别,可以是0到9中的任一个,级别越高,压缩就越小,节省了带宽资源,但同时也消耗CPU资源,所以一般折中为6 gzip types text/css text/xml application/javascript; #制定压缩的类型,线上配置时尽可能配置多的压缩类型! gzip_disable "MSIE [1-6]\."; #配置禁用gzip条件,支持正则。此处表示ie6及以下不启用gzip(因为ie低版本不支持) gzip vary on; #选择支持vary header;改选项可以让前端的缓存服务器缓存经过gzip压缩的页面; 这个可以不写,表示在传送数据时,给客户端说明我使用了gzip压缩

 

Nginx设置expires设定页面缓存时间

配置expires起到控制页面缓存的作用,合理的配置expires可以减少很多服务器的请求要配置expires,
官方文档:http://nginx.org/en/docs/http/ngx_http_headers_module.html
可以在http段中或者server段中或者location段中加入

location ~ \.(gif|jpg|jpeg|png|bmp|ico)$ {    root /var/www/img/;    expires 30d; # 30m:30 分钟,2h:2 小时,30d:30 天 }

控制图片等过期时间为30天,当然这个时间可以设置的更长。具体视情况而定

 

事件处理模型优化use epoll

epoll用在linux上, kqueue用在bsd上, 不能物理上共存。如果你的服务器cpu较好,linux内核新,可考虑用epoll.

events {     #单个进程允许的客户端最大连接数     worker_connections  20480;     #收到一个新连接通知后接受尽可能多的连接。     multi_accept on;      #使用epoll模型     use epoll; }

 

高效文件传输模式sendfile on

参数sendfile on 用于开启文件高效传输模式,同时将tcp_nopush on 和tcp_nodelay on 两个指令设置为on,可防止网络及磁盘I/O阻塞,提升Nginx工作效率

#开启高效文件传输模式 sendfile on; #减少网络报文段数量 tcp_nopush on; #提高I/O性能 tcp_nodelay on;

 

优化服务器域名的散列表大小

如果在 server_name 中配置了一个很长的域名,那么重载 Nginx 时会报错,因此需要使用 server_names_hash_max_size 来解决域名过长的问题

#域名散列表大小  server_names_hash_bucket_size 64; server_names_hash_max_size 2048;

 

优化worker服务进程数

worker进程数最开始的设置可以等于CPU的核数,高流量高并发场合也可以考虑将进程数提高至CPU核数*2, 设置太多没有意义。

#查看CPU总颗数: [root@nginx conf]# grep 'physical id' /proc/cpuinfo|sort|uniq|wc -l 1 #查看CPU总核数: [root@nginx conf]# grep processor /proc/cpuinfo |wc -l 4
#CPU的总核数4 worker_processes  4; 

 

FastCGI参数调优

如果是动态请求(如 PHP),那么 Nginx 就会把它通过 FastCGI 接口发送给 PHP 引擎服务, (即 php-fpm)进行解析.

fastcgi_connect_timeout 240; # Nginx服务器和后端FastCGI服务器连接的超时时间  fastcgi_send_timeout 240; # Nginx允许FastCGI服务器返回数据的超时时间,即在规定时间内后端服务器必须传完所有的数据,否则Nginx将断开这个连接  fastcgi_read_timeout 240; # Nginx从FastCGI服务器读取响应信息的超时时间,表示连接建立成功后,Nginx等待后端服务器的响应时间  fastcgi_buffer_size 64k; # Nginx FastCGI 的缓冲区大小,用来读取从FastCGI服务器端收到的第一部分响应信息的缓冲区大小  fastcgi_buffers 4 64k; # 设定用来读取从FastCGI服务器端收到的响应信息的缓冲区大小和缓冲区数量  fastcgi_busy_buffers_size 128k; # 用于设置系统很忙时可以使用的 proxy_buffers 大小  fastcgi_temp_file_write_size 128k; # FastCGI 临时文件的大小  # fastcti_temp_path /data/ngx_fcgi_tmp; # FastCGI 临时文件的存放路径  fastcgi_cache_path /data/ngx_fcgi_cache levels=2:2 keys_zone=ngx_fcgi_cache:512m inactive=1d max_size=40g; # 缓存目录

 

fastcgi_cache ngx_fcgi_cache; # 缓存FastCGI生成的内容,比如PHP生成的动态内容  fastcgi_cache_valid 200 302 1h; # 指定http状态码的缓存时间,这里表示将200和302缓存1小时  fastcgi_cache_valid 301 1d; # 指定http状态码的缓存时间,这里表示将301缓存1天  fastcgi_cache_valid any 1m; # 指定http状态码的缓存时间,这里表示将其他状态码缓存1分钟  fastcgi_cache_min_uses 1; # 设置请求几次之后响应被缓存,1表示一次即被缓存  fastcgi_cache_use_stale error timeout invalid_header http_500; # 定义在哪些情况下使用过期缓存  fastcgi_cache_key http://$host$request_uri; # 定义 fastcgi_cache 的 key

 

Log日志优化

1. 排除不需要的日志。

location ~ .*\.(js|jpg|JPG|jpeg|JPEG|css|bmp|gif|GIF)$ {     access_log off; }

2. 日志切割:nginx日志默认不做处理,都会存放到access.log,error.log, 导致越积越多。 可写个定时脚本按天存储,每天凌晨00:00执行

#!/bin/bash YESTERDAY=$(date -d "yesterday" +"%Y-%m-%d")   LOGPATH=/usr/local/openresty/nginx/logs/   PID=${LOGPATH}nginx.pid   mv ${LOGPATH}access.log ${LOGPATH}access-${YESTERDAY}.log   mv ${LOGPATH}error.log ${LOGPATH}error-${YESTERDAY}.log     kill -USR1 `cat ${PID}`

 

资源防盗链

大量的盗链对于服务器也是很大的消耗, 防盗链主要两种方法:

1. 根据 HTTP referer 实现防盗链

#第一种,匹配后缀 location ~ .*\.(gif|jpg|jpeg|png|bm|swf|flv|rar|zip|gz|bz2)$ {    # 指定需要使用防盗链的媒体资源     access_log  off;                                              # 不记录防盗链的日志     expires  15d;                                                 # 设置缓存时间     valid_referers  none  blocked  *.test.com  *.abc.com;         # 表示这些地址可以访问上面的媒体资源     if ($invalid_referer) {                                       # 如果地址不如上面指定的地址就返回403         return 403     } }

 2. 根据 cookie 实现防盗链:cookie 是服务器贴在客户端身上的 "标签" ,服务器用它来识别客户端

#第二种,绑定目录 location /images {       root /web/www/img;     vaild_referers nono blocked *.spdir.com *.spdir.top;     if ($invalid_referer) {         return 403;     } }

 

限制HTTP的请求方法

HTTP1.1定义了八种主要的方法,其中OPTIONS、DELETE等方法在生产环境可以被认为是不安全的,因此需要配置Nginx实现限制指定某些HTTP请求的方法来达到提升服务器安全的目的。

if ($request_method !~ ^(GET|HEAD|POST)$ ) {     return 501; }

 通过限制上传服务器的Web服务(可以具体到文件)使用GET方法,防止用户通过上传服务器访问存储内容

if ($request_method !~ ^(GET)$ ) {     return 501; }

 

控制客户端请求速率

# 以请求的客户端IP作为key值,内存区域命名为one,分配10m内存空间,访问速率限制为1秒1次请求 limit_req_zone $binary_remove_addr zone=one:10m rate=1r/s; # 使用前面定义的为one的内存空间,队列值为5,即可以有5个请求排队等候 limit_req  zone=one burst=5; 

 

HTTPS配置优化

NGINX配置HTTPS性能优化方案一则:

1) HSTS的合理使用
2) 会话恢复的合理使用
3) Ocsp stapling的合理使用
4) TLS协议的合理配置
5) False Start的合理使用
6) SNI功能的合理使用,
7) HTTP 2.0的合理使用(Nginx在1.9.x版本就开始支持http2协议)
8) SSL硬件加速卡合理使用

#以下是一个nginx优化https的配置模板: server {     # 把ssl on;这行去掉,ssl写在443端口后面。这样http和https的链接都可以用     listen 443 ssl http2 default_server;     server_name  site.xxx.com;       # HSTS的合理使用,max-age表明HSTS在浏览器中的缓存时间,includeSubdomainscam参数指定应该在所有子域上启用HSTS,preload参数表示预加载,通过Strict-Transport-Security: max-age=0将缓存设置为0可以撤销HSTS     add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";       ssl_certificate /usr/local/nginx/cert/server.pem;      ssl_certificate_key /usr/local/nginx/cert/server.key;        # 分配10MB的共享内存缓存,不同工作进程共享TLS会话信息     ssl_session_cache shared:SSL:10m;     # 设置会话缓存过期时间24h     ssl_session_timeout 1440m;       # TLS协议的合理配置     # 指定TLS协议的版本,不安全的SSL2和SSL3要废弃掉     ssl_protocols TLSv1 TLSv1.1 TLSv1.2;     # 启用ssl_prefer_server_ciphers,用来告诉Nginx在TLS握手时启用服务器算法优先,由服务器选择适配算法而不是客户端     ssl_prefer_server_ciphers on;     # 优先选择支持前向加密的算法,且按照性能的优先顺序排列     ssl_ciphers ssl_ciphers "ECDHE-ECDSA-CHACHA20-POLY1305 ECDHE-RSA-CHACHA20-POLY1305 ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-SHA256 ECDHE-RSA-AES128-SHA256 ECDHE-ECDSA-AES128-SHA ECDHE-RSA-AES256-SHA384 ECDHE-RSA-AES128-SHA ECDHE-ECDSA-AES256-SHA384 ECDHE-ECDSA-AES256-SHA ECDHE-RSA-AES256-SHA DHE-RSA-AES128-SHA256 DHE-RSA-AES128-SHA DHE-RSA-AES256-SHA256 DHE-RSA-AES256-SHA ECDHE-ECDSA-DES-CBC3-SHA ECDHE-RSA-DES-CBC3-SHA EDH-RSA-DES-CBC3-SHA AES128-GCM-SHA256 AES256-GCM-SHA384 AES128-SHA256 AES256-SHA256 AES128-SHA AES256-SHA DES-CBC3-SHA !DSS";       # 会话恢复的合理使用     # 配置会话票证,减少了TLS握手的开销     ssl_session_tickets on;     # 生产key的命令通过openssl生成:openssl rand –out session_ticket.key 48     ssl_session_ticket_key /usr/local/nginx/ssl_cert/session_ticket.key;       #设置TLS日志格式     log_format ssl "$time_local $server_name $remote_addr $connection $connnection_requests $ssl_protocol $ssl_cipher $ssl_session_id $ssl_session_reused";     access_log /usr/local/nginx/logs/access.log ssl;       # Ocsp stapling的合理使用     # 启用OCSP stapling,指定更新文件内容,无需从服务商拉取     ssl_stapling on;     ssl_stapling_file /usr/local/nginx/oscp/stapling_file.ocsp;     # 或者不指定更新文件内容,在线获取,valid表示缓存5分钟,resolver_timeout表示网络超时时间     #resolver 8.8.8.8 8.8.4.4 223.5.5.5 valid=300s;     #resolver_timeout 5s;      # 启用OCSP响应验证,OCSP信息响应适用的证书                ssl_stapling_verify on;      ssl_trusted_certificate /usr/local/nginx/ssl_cert/trustchain.crt;            root   html;     index  index.html index.htm;       location / {         ...     }       error_page 403 /403.html;     location = /403.html {         root /usr/local/nginx/waf/403/default;     }     error_page 500 502 503 504 /502.html;     location = /502.html {         root /usr/local/nginx/waf/403/default;     } }

  

优化配置及详细注释事例

#普通配置 #==性能配置   #运行用户 user nobody; #pid文件 pid logs/nginx.pid;  #Nginx基于事件的非阻塞多路复用模型(epoll或kquene) #一个进程在短时间内可以响应大量请求,工作进程设置与cpu数相同,避免cpu在多个进程间切换增加开销 #==worker进程数,通常设置<=CPU数量,auto为自动检测,一般设置最大8个即可,再大性能提升较小或不稳定 worker_processes auto;  #==将每个进程绑定到特定cpu上,避免进程在cpu间切换的开销 worker_cpu_affinity 00000001 00000010 00000100 00001000 00010000 00100000 01000000 10000000;  #==worker进程打开最大文件数,可CPU*10000设置,或设置系统最大数量655350 worker_rlimit_nofile 102400; #全局错误日志 error_log  logs/error.log;   #events模块中包含nginx中所有处理连接的设置,并发响应能力的关键配置 events {     #==每个进程同时打开的最大连接数(最大并发数)     worker_connections 102400;          #==告诉nginx收到一个新链接通知后接受尽可能多的链接     #multi_accept on;          #一般http 1.1协议下,浏览器默认使用两个并发链接     #如果是反向代理,nginx需要和客户端保持连接,还需要和后端服务器保持连接     #Http服务器时,设置max_client=worker_processes*worker_connections/2     #反向代理时,设置max_client=worker_processes*worker_connections/4         #==最大可用客户端数     #max_client           #==使用非阻塞模型,设置复用客户端线程的轮训方法     use epoll; }   #http模块控制着nginx http处理的所有核心特性 http {     #打开或关闭错误页面中的nginx版本号等信息     server_tokens on;     #!server_tag on;     #!server_info on;     #==优化磁盘IO设置,指定nginx是否调用sendfile函数来输出文件,普通应用设为on,下载等磁盘IO高的应用,可设为off     sendfile on;          #缓存发送请求,启用如下两个配置,会在数据包达到一定大小后再发送数据     #这样会减少网络通信次数,降低阻塞概率,但也会影响响应的及时性     #比较适合于文件下载这类的大数据包通信场景     #tcp_nopush on;     #tcp_nodelay on;      #==设置nginx是否存储访问日志,关闭这个可以让读取磁盘IO操作更快     access_log on;     #设置nginx只记录严重错误,可减少IO压力     #error_log logs/error.log crit;      #Http1.1支持长连接     #降低每个链接的alive时间可在一定程度上提高响应连接数量     #==给客户端分配keep-alive链接超时时间     keepalive_timeout 30;      #设置用户保存各种key的共享内存的参数,5m指的是5兆     limit_conn_zone $binary_remote_addr zone=addr:5m;     #为给定的key设置最大的连接数,这里的key是addr,设定的值是100,就是说允许每一个IP地址最多同时打开100个连接     limit_conn addr 100;      #include指在当前文件中包含另一个文件内容     include mime.types;     #设置文件使用默认的mine-type     default_type text/html;     #设置默认字符集     charset UTF-8;      #==设置nginx采用gzip压缩的形式发送数据,减少发送数据量,但会增加请求处理时间及CPU处理时间,需要权衡     gzip on;     #==加vary给代理服务器使用,针对有的浏览器支持压缩,有个不支持,根据客户端的HTTP头来判断是否需要压缩     gzip_vary on;     #nginx在压缩资源之前,先查找是否有预先gzip处理过的资源     #!gzip_static on;     #为指定的客户端禁用gzip功能     gzip_disable "MSIE[1-6]\.";     #允许或禁止压缩基于请求和相应的响应流,any代表压缩所有请求     gzip_proxied any;     #==启用压缩的最少字节数,如果请求小于1024字节则不压缩,压缩过程会消耗系统资源     gzip_min_length 1024;     #==数据压缩等级,1-9之间,9最慢压缩比最大,压缩比越大对系统性能要求越高     gzip_comp_level 2;     #需要压缩的数据格式     gzip_types text/plain text/css text/xml text/javascript  application/json application/x-javascript application/xml application/xml+rss;       #静态文件缓存     #==开启缓存的同时也指定了缓存文件的最大数量,20s如果文件没有被请求则删除缓存     open_file_cache max=100000 inactive=20s;     #==多长时间检查一次缓存的有效期     open_file_cache_valid 30s;     #==有效期内缓存文件最小的访问次数,只有访问超过2次的才会被缓存     open_file_cache_min_uses 2;     #当搜索一个文件时是否缓存错误信息     open_file_cache_errors on;      #==允许客户端请求的最大单文件字节数     client_max_body_size 4m;     #==客户端请求头缓冲区大小     client_header_buffer_size 4k;      #是否启用对发送给客户端的URL进行修改     proxy_redirect off;     #后端的Web服务器可以通过X-Forwarded-For获取用户真实IP     proxy_set_header Host $host;     proxy_set_header X-Real-IP $remote_addr;     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;     #==nginx跟后端服务器连接超时时间(代理连接超时)     proxy_connect_timeout 60;     #==连接成功后,后端服务器响应时间(代理接收超时)     proxy_read_timeout 120;     #==后端服务器数据回传时间(代理发送超时)     proxy_send_timeout 20;     #==设置代理服务器(nginx)保存用户头信息的缓冲区大小     proxy_buffer_size 32k;     #==proxy_buffers缓冲区,网页平均在32k以下的设置     proxy_buffers 4 128k;     #==高负荷下缓冲大小(proxy_buffers*2)     proxy_busy_buffers_size 256k;     #==设定缓存文件夹大小,大于这个值,将从upstream服务器传     proxy_temp_file_write_size 256k;     #==1G内存缓冲空间,3天不用删除,最大磁盘缓冲空间2G     proxy_cache_path /home/cache levels=1:2 keys_zone=cache_one:1024m inactive=3d max_size=2g;       #设定负载均衡服务器列表     upstream nginx.test.com{         #后端服务器访问规则         #ip_hash;         #weight参数表示权重值,权值越高被分配到的几率越大         #server 10.11.12.116:80 weight=5;         #PC_Local         server 10.11.12.116:80;         #PC_Server         server 10.11.12.112:80;         #Notebook         #server 10.11.12.106:80;     }      #server代表虚拟主机,可以理解为站点(挂载多个站点,只需要配置多个server及upstream节点即可)     server {         #监听80端口         listen 80;         #识别的域名,定义使用nginx.test.com访问         server_name nginx.test.com;         #设定本虚拟主机的访问日志         access_log logs/nginx.test.com.access.log;                  #一个域名下匹配多个URI的访问,使用location进行区分,后面紧跟着的/代表匹配规则         #如动态资源访问和静态资源访问会分别指向不同的位置的应用场景         #         # 基本语法规则:location [=|~|~*|^~] /uri/ {...}          # = 开头表示精确匹配         # ^~ 开头表示uri以某个常规字符串开头,匹配成功后不再进行正则匹配         # ~ 开头表示区分大小写的正则匹配         # ~* 开头表示不区分大小写的正则匹配         # !~ 开头表示区分大小写的不匹配的正则         # !~* 开头表示不区分大小写的不匹配的正则         # / 通用匹配,任何请求都会被匹配到         #         # 理解如下:         # 有两种匹配模式:普通字符串匹配,正则匹配         # 无开头引导字符或以=开头表示普通字符串匹配         # 以~或~*开头表示正则匹配,~*表示不区分大小写         # 【多个location时,先匹配普通字符串location,再匹配正则location】         # 只识别URI部分,例如请求为“/test/1/abc.do?arg=xxx”         # (1)先查找是否有=开头的精确匹配,即“location=/test/1/abc.do {...}”         # (2)再查找普通匹配,以“最大前缀”为规则,如有以下两个location         #    location /test/ {...}         #    location /test/1/ {...}         #    则匹配后一项         # (3)匹配到一个普通location后,搜索并未结束,而是暂存当前结果,并继续进行正则搜索         # (4)在所有正则location中找到第一个匹配项后,以此匹配项为最终结果         # 【所以正则匹配项,匹配规则受定义前后顺序影响,但普通匹配不会】         # (5)如果未找到正则匹配项,则以(3)中缓存的结果为最终结果         # (6)如果一个匹配都没有,则返回404         # location =/ {...}与location / {...}的差别         # 前一个是精确匹配,只响应“/”的请求,所有“/xxx”形式的请求不会以“前缀匹配形式”匹配到它         # 后一个正相反,所有请求必然都是以“/”开头,所以没有其他匹配结果时一定会执行到它         # location ^~ / {...} ^~的意思是禁止正则匹配,表示匹配到此项后不再进行后续的正则搜索         # 相当于普通匹配模式匹配成功后就以此结果为最终结果,停止进行后续的正则匹配         location / {             #定义服务器的默认网站根目录位置,可以写相对路径,也可以写绝对路径             root html;             #定义首页索引文件的名称             index index.html index.htm;             #定义转发后端负载服务器组             proxy_pass http://nginx.test.com;         }          #定义错误提示页面         error_page 500 502 503 504 /50x.html;         location = /50x.html {             root html;         }         #静态文件,nginx自己处理         location ~ ^/(images|javascript|js|css|flash|media|static)/{             root /var/www/virtual/htdocs;             #过期时间1天             expires 1d;             #关闭媒体文件日志             access_log off;             log_not_found off;         }         #设定查看Nginx状态的地址         location /NginxStatus {             #!stub_status on; #无此关键字             access_log off;             auth_basic "NginxStatus";             auth_basic_user_file conf/htpasswd;         }         #禁止访问的文件.htxxx         location ~ /\.ht {             #deny all;禁止访问,返回403             deny all;             #allow all;允许访问         }     }     #网站较多的情况下ngxin又不会请求瓶颈可以考虑挂多个站点,并把虚拟主机配置单独放在一个文件内,引入进来     #include website.conf; }

 

 

 

点击查看原文阅读(10) | 评论(0) | 分类:Linux/架构,部署

NodeJS源码解析 - HTTP Server模块 (2018-03-04)

 

NodeJS源码解析 - HTTP Server模块

http是nodejs中重要的模块之一,有必要了解它的运行原理

回到helloworld ,当node在收到一个http请求,会创建一个http.Server,注册并监听request。

var http = require('http');
http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello World\n');
}).listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

HTTP模块

  1. 打开node-v8.9.3/lib/http.js

首先引入的是http模块,模块抛出公共方法调用createServer实际上是返回Server实例,

createServer里面的回调函数(参数requestListener)

直接作为了Server的参数requestListener,而这个Server实际上是require('_http_server')

'use strict';

const agent = require('_http_agent');
const { ClientRequest } = require('_http_client');
const common = require('_http_common');
const incoming = require('_http_incoming');
const outgoing = require('_http_outgoing');
//引入私有_http_server模块
const server = require('_http_server');

const { Server } = server;
//创建server, 将回调函数作为参数
function createServer(requestListener) {
  return new Server(requestListener);
}

function request(options, cb) {
  return new ClientRequest(options, cb);
}

function get(options, cb) {
  var req = request(options, cb);
  req.end();
  return req;
}
//http模块暴露的所有公共方法
module.exports = { 
  _connectionListener: server._connectionListener,
  METHODS: common.methods.slice().sort(),
  STATUS_CODES: server.STATUS_CODES,
  Agent: agent.Agent,
  ClientRequest,
  globalAgent: agent.globalAgent,
  IncomingMessage: incoming.IncomingMessage,
  OutgoingMessage: outgoing.OutgoingMessage,
  Server,
  ServerResponse: server.ServerResponse,
  createServer, 
  get,
  request
};

打开文件node-v8.9.3/lib/_http_server.js 260行

实际上是为这个requestListener函数与'request'事件绑定到了一起,而'request '是方法parserOnIncoming里面抛出的一个事件

function Server(requestListener) {
  if (!(this instanceof Server)) return new Server(requestListener);
  net.Server.call(this, { allowHalfOpen: true }); 
 
  //如果有回调函数,对当前实例进行监听,若request有事件触发则调用回调
  if (requestListener) {
    this.on('request', requestListener);
  }

  // Similar option to this. Too lazy to write my own docs.
  // http://www.squid-cache.org/Doc/config/half_closed_clients/
  // http://wiki.squid-cache.org/SquidFaq/InnerWorkings#What_is_a_half-closed_filedescriptor.3F
  this.httpAllowHalfOpen = false;
  //当启动server实例时,观察者建立connect事件
  this.on('connection', connectionListener);

  this.timeout = 2 * 60 * 1000;
  this.keepAliveTimeout = 5000;
  this._pendingResponseData = 0;
  this.maxHeadersCount = null;
}

//net.Server继承Server
util.inherits(Server, net.Server);

res过程?

调用emit方法,将request事件发送给每一个监听的实例,并且传入req,res

server.emit('request', req, res); 这个事件也会同时抛出req和res两个对象

req变量与另一个叫做shouldKeepAlive的变量作参同时传入此函数parserOnIncoming

_http_server.js 592行 602行

//处理具体解析完毕的请求
function parserOnIncoming(server, socket, state, req, keepAlive) {
  resetSocketTimeout(server, socket, state);

  state.incoming.push(req);

  // If the writable end isn't consuming, then stop reading
  // so that we don't become overwhelmed by a flood of
  // pipelined requests that may never be resolved.
  if (!socket._paused) {
    var ws = socket._writableState;
    if (ws.needDrain || state.outgoingData >= ws.highWaterMark) {
      socket._paused = true;
      // We also need to pause the parser, but don't do that until after
      // the call to execute, because we may still be processing the last
      // chunk.
      socket.pause();
    }
  }
  //服务器通过ServerResponse实例,来个请求方发送数据。包括发送响应表头,发送响应主体
  var res = new ServerResponse(req);
  res._onPendingData = updateOutgoingData.bind(undefined, socket, state);

  res.shouldKeepAlive = keepAlive;
  DTRACE_HTTP_SERVER_REQUEST(req, socket);
  LTTNG_HTTP_SERVER_REQUEST(req, socket);
  COUNTER_HTTP_SERVER_REQUEST();

  if (socket._httpMessage) {
    // There are already pending outgoing res, append.
    state.outgoing.push(res);
  } else {
    res.assignSocket(socket);
  }

  // When we're finished writing the response, check if this is the last
  // response, if so destroy the socket.
  res.on('finish',
         resOnFinish.bind(undefined, req, res, socket, state, server));

  if (req.headers.expect !== undefined &&
      (req.httpVersionMajor === 1 && req.httpVersionMinor === 1)) {
    if (continueExpression.test(req.headers.expect)) {
      res._expect_continue = true;

      if (server.listenerCount('checkContinue') > 0) {
        server.emit('checkContinue', req, res);
      } else {
        res.writeContinue();
        //送给每一个监听器的实例并传入req&res
        server.emit('request', req, res);
      }
    } else if (server.listenerCount('checkExpectation') > 0) {
      server.emit('checkExpectation', req, res);
    } else {
      res.writeHead(417);
      res.end();
    }
  } else {
    //送给每一个监听器的实例并传入req&res
    // res实际上是ServerResponse的实例
    // var res = new ServerResponse(req);
    server.emit('request', req, res);
  }
  return false; // Not a HEAD response. (Not even a response!)
}

ServerResponse 实现了 Writable Stream interface,内部也是通过socket来发送信息。 res,发现为ServerResponse()的实例并传入req

function ServerResponse(req) {
  OutgoingMessage.call(this);

  if (req.method === 'HEAD') this._hasBody = false;

  this.sendDate = true;
  this._sent100 = false;
  this._expect_continue = false;

  if (req.httpVersionMajor < 1 || req.httpVersionMinor < 1) {
    this.useChunkedEncodingByDefault = chunkExpression.test(req.headers.te);
    this.shouldKeepAlive = false;
  }
}
//继承自OutgoingMessage,为OM的一个子类,所以回调函数里的res也是OM的一个实例
//来自_http_outgoing私有模块  
//const OutgoingMessage = require('_http_outgoing').OutgoingMessage;
util.inherits(ServerResponse, OutgoingMessage);

到此res线找到,res为ServerMessage的实例,也是OutgoingMessage的实例

function OutgoingMessage() {
  Stream.call(this);
  
  //返回一些与服务器有关的属性
  // Queue that holds all currently pending data, until the response will be
  // assigned to the socket (until it will its turn in the HTTP pipeline).
  this.output = []; 
  this.outputEncodings = []; 
  this.outputCallbacks = []; 

  // `outputSize` is an approximate measure of how much data is queued on this
  // response. `_onPendingData` will be invoked to update similar global
  // per-connection counter. That counter will be used to pause/unpause the
  // TCP socket and HTTP Parser and thus handle the backpressure.
  this.outputSize = 0;

  this.writable = true;

  this._last = false;
  this.upgrading = false;
  this.chunkedEncoding = false;
  this.shouldKeepAlive = true;
  this.useChunkedEncodingByDefault = true;
  this.sendDate = false;
  this._removedConnection = false;
  this._removedContLen = false;
  this._removedTE = false;

  this._contentLength = null;
  this._hasBody = true;
  this._trailer = ''; 

  this.finished = false;
  this._headerSent = false;

  this.socket = null;
  this.connection = null;
  this._header = null;
  this[outHeadersKey] = null;

  this._onPendingData = noopPendingOutput;
}
util.inherits(OutgoingMessage, Stream); //继承自Stream  

流程图演示:

image

req 过程

req,在parserOnIncoming()作为参数传入

parserOnIncoming()在哪里被调用?

// _http_server.js 345行
function connectionListener(socket) {
    ...
    var parser = parsers.alloc();
    parser.onIncoming = parserOnIncoming.bind(undefined, this, socket, state);
    ...
}

parsers在_http_common.js抛出 onIncoming在skipBody = parser.onIncoming(parser.incoming, shouldKeepAlive)中调用

 function parserOnHeadersComplete(...) {      ...      //IncomingMessage的实例并将套接字作为参数传入 ,来自_http_common.js模块      parser.incoming = new IncomingMessage(parser.socket);      parser.incoming.httpVersionMajor = versionMajor;      parser.incoming.httpVersionMinor = versionMinor;      parser.incoming.httpVersion = `${versionMajor}.${versionMinor}`;      parser.incoming.url = url;      ...      //onIncoming 这里被调用 parser.incoming相当于req      skipBody = parser.onIncoming(parser.incoming, shouldKeepAlive);      ...  } 

流程图演示: image

Listen 过程

基于ner.js模块 Server Connection事件在net.Server.call(this, { allowHalfOpen: true })触发

connection会在onconnection中触发handle

function onconnection(err, clientHandle) {
  var handle = this;
  var self = handle.owner;

  debug('onconnection');

  if (err) {
    self.emit('error', errnoException(err, 'accept'));
    return;
  }

  if (self.maxConnections && self._connections >= self.maxConnections) {
    clientHandle.close();
    return;
  }

  var socket = new Socket({
    handle: clientHandle,
    allowHalfOpen: self.allowHalfOpen,
    pauseOnCreate: self.pauseOnConnect
  });  
  socket.readable = socket.writable = true;


  self._connections++;
  socket.server = self;
  socket._server = self;

  DTRACE_NET_SERVER_CONNECTION(socket);
  LTTNG_NET_SERVER_CONNECTION(socket);
  COUNTER_NET_SERVER_CONNECTION(socket);
  
  self.emit('connection', socket);
}

listen2调用setupListenHandle方法,注册onconnection

function setupListenHandle(address, port, addressType, backlog, fd) {
    ...
    this._handle.onconnection = onconnection
    ...
}

_listen2注册handle, 在listen里被调用

Server.prototype._listen2 = setupListenHandle;
server._listen2(address, port, addressType, backlog, fd);

listen在Server原型上,所以在代码里的http.createServer()实例上有listen()方法

Server.prototype.listen = function(...args) {
    ...
    if (options instanceof TCP) {
      this._handle = options;
      this[async_id_symbol] = this._handle.getAsyncId();
      listenInCluster(this, null, -1, -1, backlogFromArgs);
      return this;
    }
    ...
Socket.prototype.listen = function() {
  debug('socket.listen');
  this.on('connection', arguments[0]);
  listenInCluster(this, null, null, null);
};

Listen流程图:

 image

我的github地址:https://github.com/fzxa/NodeJS-Nucleus-Plus-Internals/blob/master/chapter1/chapter1-1.md

参考链接: https://yjhjstz.gitbooks.io/deep-into-node/chapter10/chapter10-1.html https://www.cnblogs.com/chyingp/p/node-learning-guide-http.html http://blog.csdn.net/sinat_22996989/article/details/51496010

点击查看原文阅读(966) | 评论(2) | 分类:Node.js

NodeJS源码分析-1 Hello world (2018-03-04)

 

NodeJS源码分析-1 Hello world

简要

Node已经如今发展很快,已经相对稳定和成熟,在某些时候有必要知道其内部运行原理以及运行处理过程。 种一棵树最好的时间是十年前 其次是现在。希望能坚持下去。

Nodejs当前最新版本 8.9.4

NodeJS官方网站下载源码 image

Node.js主要分为四大部分,Node Standard Library,Node Bindings,V8,Libuv

大体流程是这样的:

  1. 初始化 V8 、LibUV , OpenSSL

  2. 创建 Environment 环境

  3. 设置 Process 进程对象

  4. 执行 node.js 文件

解压包后代码结构如下:

├── AUTHORS ├── BSDmakefile   # bsd平台makefile文件 ├── BUILDING.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── COLLABORATOR_GUIDE.md ├── CONTRIBUTING.md ├── CPP_STYLE_GUIDE.md ├── GOVERNANCE.md ├── LICENSE ├── Makefile     # Linux平台makefile文件 ├── README.md ├── android-configure ├── benchmark ├── common.gypi ├── configure ├── deps          # Node底层核心依赖; 最核心的两块V8 Engine和libuv事件驱动的异步I/O模型库 ├── doc            ├── lib           # Node后端核心库 ├── node.gyp      # Node编译任务配置文件  ├── node.gypi ├── src           # C++内建模块 ├── test          # 测试代码 ├── tools         # 编译时用到的工具 └── vcbuild.bat   # Windows跨平台makefile文件 

Hello World 底层运行过程

官方Hello world代码

#app.js
const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

一个简单的Helloworld涉及到多个模块:

  • global
  • module
  • http
  • event
  • net

1.0 从main执行到js

入口 src/node_main.cc 106行 通过 src/node.cc 调用 node::Start(argc, argv); node_main.cc

namespace node {
  extern bool linux_at_secure;
}  // namespace node

int main(int argc, char *argv[]) {
#if defined(__linux__)
  char** envp = environ;
  while (*envp++ != nullptr) {}
  Elf_auxv_t* auxv = reinterpret_cast<Elf_auxv_t*>(envp);
  for (; auxv->a_type != AT_NULL; auxv++) {
    if (auxv->a_type == AT_SECURE) {
      node::linux_at_secure = auxv->a_un.a_val;
      break;
    }   
  }
#endif
  // Disable stdio buffering, it interacts poorly with printf()
  // calls elsewhere in the program (e.g., any logging from V8.)
  setvbuf(stdout, nullptr, _IONBF, 0); 
  setvbuf(stderr, nullptr, _IONBF, 0); 
  // main作为入口调用node::Start
  return node::Start(argc, argv);
}
#endif

1.1 node::Start 加载js

调用顺序:

Start() -> LoadEnviroment() -> ExecuteString()

最终在LoadEnvrioment()里面加载node.js文件,调用ExecuteString() 解析执行node.js文件,返回值是一个f_value

并且在ExecuteString()调用V8的 Script::Compile() 和 Script::Run()两个接口去解析执行js代码。

node.cc

# Nodejs启动入口, 
inline int Start(Isolate* isolate, IsolateData* isolate_data,
                 int argc, const char* const* argv,
                 int exec_argc, const char* const* exec_argv) {
  HandleScope handle_scope(isolate);
  Local<Context> context = Context::New(isolate);
  Context::Scope context_scope(context);
  Environment env(isolate_data, context);
  CHECK_EQ(0, uv_key_create(&thread_local_env));
  uv_key_set(&thread_local_env, &env);
  env.Start(argc, argv, exec_argc, exec_argv, v8_is_profiling);

  const char* path = argc > 1 ? argv[1] : nullptr;
  StartInspector(&env, path, debug_options);

  if (debug_options.inspector_enabled() && !v8_platform.InspectorStarted(&env))
    return 12;  // Signal internal error.

  env.set_abort_on_uncaught_exception(abort_on_uncaught_exception);

  if (force_async_hooks_checks) {
    env.async_hooks()->force_checks();
  }

  {
    Environment::AsyncCallbackScope callback_scope(&env);
    env.async_hooks()->push_async_ids(1, 0);
    
    //加载nodejs文件后调用ExecuteString()
    LoadEnvironment(&env); 
    env.async_hooks()->pop_async_id(1);
  }

  env.set_trace_sync_io(trace_sync_io);
  //事件循环池
  {
    SealHandleScope seal(isolate);
    bool more;
    PERFORMANCE_MARK(&env, LOOP_START);
    do {
      uv_run(env.event_loop(), UV_RUN_DEFAULT);

      v8_platform.DrainVMTasks();

      more = uv_loop_alive(env.event_loop());
      if (more)
        continue;

      EmitBeforeExit(&env);

      // Emit `beforeExit` if the loop became alive either after emitting
      // event, or after running some callbacks.
      more = uv_loop_alive(env.event_loop());
    } while (more == true);
    PERFORMANCE_MARK(&env, LOOP_EXIT);
  }

  env.set_trace_sync_io(false);

  const int exit_code = EmitExit(&env);
  RunAtExit(&env);
  uv_key_delete(&thread_local_env);

  v8_platform.DrainVMTasks();
  WaitForInspectorDisconnect(&env);
#if defined(LEAK_SANITIZER)
  __lsan_do_leak_check();
#endif

  return exit_code;
}

核心运行流程

整体运行流程图 image

  1. 核心数据结构 default_loop_struct 结构体为struct uv_loop_s 当加载js文件时,如果代码有io操作,调用lib模块->底层C++模块->LibUV(deps uv)->拿到系统返回的一个fd(文件描述符),和 js代码传进来的回调函数callback,封装成一个io观察者(一个uv__io_s类型的对象),保存到default_loop_struct.

  2. 进入事件池, default_loop_struct保存对应io观察着,V8 Engine处理js代码, main函数调用libuv进入uv_run(), node进入事件循环 ,判断是否有存活的观察者

  • 如果也没有io, Node进程退出
  • 如果有io观察者, 执行uv_run()进入epoll_wait()线程挂起,io观察者检测是否有数据返回callback, 没有数据则会一直在epoll_wait()等待执行 server.listen(3000)会挂起一直等待。

Module对象

根据CommonJS规范,每一个文件就是一个模块,在每个模块中,都会有一个module对象,这个对象就指向当前的模块。 module对象具有以下属性:

  • id:当前模块的bi
  • exports:表示当前模块暴露给外部的值
  • parent: 是一个对象,表示调用当前模块的模块
  • children:是一个对象,表示当前模块调用的模块
  • filename:模块的绝对路径
  • paths:从当前文件目录开始查找node_modules目录;然后依次进入父目录,查找父目录下的node_modules目录;依次迭代,直到根目录下的node_modules目录
  • loaded:一个布尔值,表示当前模块是否已经被完全加载

示例:

module.exports = { 
    name: 'fzxa',
    getAge: function(age){
            console.log(age)
    }   
}
console.log(module)

执行node module.js 返回如下

Module {
  id: '.',
  exports: { name: 'fzxa', getAge: [Function: getAge] },
  parent: null,
  filename: '/Users/fzxa/Documents/study/module.js',
  loaded: false,
  children: [],
  paths: 
   [ '/Users/fzxa/Documents/study/node_modules',
     '/Users/fzxa/Documents/node_modules',
     '/Users/fzxa/node_modules',
     '/Users/node_modules',
     '/node_modules' ] }

module对象具有一个exports属性,该属性就是用来对外暴露变量、方法或整个模块的。当其他的文件require进来该模块的时候,实际上就是读取了该模块module对象的exports属性

exports对象

exports和module.exports都是引用类型的变量,而且这两个对象指向同一块内存地址

exports = module.exports = {}; 

例子:

var module = {
    exports: {}
}

var exports = module.exports

function change(exports) {
    //为形参添加属性,是会同步到外部的module.exports对象的
    exports.name = "fzxa"
    //在这里修改了exports的引用,并不会影响到module.exports
    exports = {
        age: 24
    }
    console.log(exports) //{ age: 24 }
}

change(exports)
console.log(module.exports) //{exports: {name: "fzxa"}}

直接给exports赋值,会改变当前模块内部的形参exports对象的引用,也就是说当前的exports已经跟外部的module.exports对象没有任何关系了,所以这个改变是不会影响到module.exports的

module.exports就是为了解决上述exports直接赋值,会导致抛出不成功的问题而产生的。有了它,我们就可以这样来抛出一个模块了.

require方法

Node中引入模块的机制步骤

  1. 路径分析
  2. 文件定位
  3. 编译执行 Node对引入过的模块也会进行缓存。不同的地方是,node缓存的是编译执行之后的对象而不是静态文件

Module._load的源码:

Module._load = function(request, parent, isMain) {

  //  计算绝对路径
  var filename = Module._resolveFilename(request, parent);

  //  第一步:如果有缓存,取出缓存
  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    return cachedModule.exports;

  // 第二步:是否为内置模块
  if (NativeModule.exists(filename)) {
    return NativeModule.require(filename);
  }

  // 第三步:生成模块实例,存入缓存
  var module = new Module(filename, parent);
  Module._cache[filename] = module;

  // 第四步:加载模块
  try {
    module.load(filename);
    hadException = false;
  } finally {
    if (hadException) {
      delete Module._cache[filename];
    }
  }

  // 第五步:输出模块的exports属性
  return module.exports;
};

在Module._load方法的内部调用了Module._findPath这个方法,这个方法是用来返回模块的绝对路径的,源码如下:

Module._findPath = function(request, paths) {

  // 列出所有可能的后缀名:.js,.json, .node
  var exts = Object.keys(Module._extensions);

  // 如果是绝对路径,就不再搜索
  if (request.charAt(0) === '/') {
    paths = [''];
  }

  // 是否有后缀的目录斜杠
  var trailingSlash = (request.slice(-1) === '/');

  // 第一步:如果当前路径已在缓存中,就直接返回缓存
  var cacheKey = JSON.stringify({request: request, paths: paths});
  if (Module._pathCache[cacheKey]) {
    return Module._pathCache[cacheKey];
  }

  // 第二步:依次遍历所有路径
  for (var i = 0, PL = paths.length; i < PL; i++) {
    var basePath = path.resolve(paths[i], request);
    var filename;

    if (!trailingSlash) {
      // 第三步:是否存在该模块文件
      filename = tryFile(basePath);

      if (!filename && !trailingSlash) {
        // 第四步:该模块文件加上后缀名,是否存在
        filename = tryExtensions(basePath, exts);
      }
    }

    // 第五步:目录中是否存在 package.json 
    if (!filename) {
      filename = tryPackage(basePath, exts);
    }

    if (!filename) {
      // 第六步:是否存在目录名 + index + 后缀名 
      filename = tryExtensions(path.resolve(basePath, 'index'), exts);
    }

    // 第七步:将找到的文件路径存入返回缓存,然后返回
    if (filename) {
      Module._pathCache[cacheKey] = filename;
      return filename;
    }
 }

  // 第八步:没有找到文件,返回false 
  return false;
};

当我们第一次引入一个模块的时候,require的缓存机制会将我们引入的模块加入到内存中,以提升二次加载的性能。但是,如果我们修改了被引入模块的代码之后,当再次引入该模块的时候,就会发现那并不是我们最新的代码,这是一个麻烦的事情。如何解决呢 require有如下方法: require(): 加载外部模块 require.resolve():将模块名解析到一个绝对路径 require.main:指向主模块 require.cache:指向所有缓存的模块 require.extensions:根据文件的后缀名,调用不同的执行函数

//删除指定模块的缓存 delete require.cache[require.resolve('/*被缓存的模块名称*/')]  // 删除所有模块的缓存 Object.keys(require.cache).forEach(function(key) {      delete require.cache[key]; }) 

HTTP_Server

首先需要创建一个 http.Server 类的实例,然后监听它的 request 事件

requestListener 回调函数作为观察者,监听了 request 事件, 默认超时时间为2分

lib/_http_server.js

function Server(requestListener) {
  if (!(this instanceof Server)) return new Server(requestListener);
  net.Server.call(this, { allowHalfOpen: true }); 

  if (requestListener) {
    this.on('request', requestListener);
  }

  // Similar option to this. Too lazy to write my own docs.
  // http://www.squid-cache.org/Doc/config/half_closed_clients/
  // http://wiki.squid-cache.org/SquidFaq/InnerWorkings#What_is_a_half-closed_filedescriptor.3F
  this.httpAllowHalfOpen = false;

  this.on('connection', connectionListener);

  this.timeout = 2 * 60 * 1000;
  this.keepAliveTimeout = 5000;
  this._pendingResponseData = 0;
  this.maxHeadersCount = null;
}

观察者 connectionListener 处理 connection 事件。 这时,则需要一个 HTTP parser 来解析通过 TCP 传输过来的数据:

lib/_http_server.js

function connectionListener(socket) {
  debug('SERVER new http connection');

  httpSocketSetup(socket);

  // Ensure that the server property of the socket is correctly set.
  // See https://github.com/nodejs/node/issues/13435
  if (socket.server === null)
    socket.server = this;

  // If the user has added a listener to the server,
  // request, or response, then it's their responsibility.
  // otherwise, destroy on timeout by default
  if (this.timeout)
    socket.setTimeout(this.timeout);
  socket.on('timeout', socketOnTimeout);

  var parser = parsers.alloc();
  parser.reinitialize(HTTPParser.REQUEST);
  parser.socket = socket;
  socket.parser = parser;
  parser.incoming = null;

  // Propagate headers limit from server instance to parser
  if (typeof this.maxHeadersCount === 'number') {
    parser.maxHeaderPairs = this.maxHeadersCount << 1;
  } else {
    // Set default value because parser may be reused from FreeList
    parser.maxHeaderPairs = 2000;
  }

  var state = { 
    onData: null,
    onEnd: null,
    onClose: null,
    onDrain: null,
    outgoing: [], 
    incoming: [], 
    // `outgoingData` is an approximate amount of bytes queued through all
    // inactive responses. If more data than the high watermark is queued - we
    // need to pause TCP socket/HTTP parser, and wait until the data will be
    // sent to the client.
    outgoingData: 0,
    keepAliveTimeoutSet: false
  };  
  state.onData = socketOnData.bind(undefined, this, socket, parser, state);
  state.onEnd = socketOnEnd.bind(undefined, this, socket, parser, state);
  state.onClose = socketOnClose.bind(undefined, socket, state);
  state.onDrain = socketOnDrain.bind(undefined, socket, state);
  socket.on('data', state.onData);
  socket.on('error', socketOnError);
  socket.on('end', state.onEnd);
  socket.on('close', state.onClose);
  socket.on('drain', state.onDrain);
  parser.onIncoming = parserOnIncoming.bind(undefined, this, socket, state);

  // We are consuming socket, so it won't get any actual data
  socket.on('resume', onSocketResume);
  socket.on('pause', onSocketPause);

  // Override on to unconsume on `data`, `readable` listeners
  socket.on = socketOnWrap;

  // We only consume the socket if it has never been consumed before.
  var external = socket._handle._externalStream;
  if (!socket._handle._consumed && external) {
    parser._consumed = true;
    socket._handle._consumed = true;
    parser.consume(external);
  }
  parser[kOnExecute] =
    onParserExecute.bind(undefined, this, socket, parser, state);

  socket._paused = false;
}

github: https://github.com/fzxa/NodeJS-Nucleus-Plus-Internals/blob/master/chapter1/chapter1-0.md

参考链接:

https://yjhjstz.gitbooks.io/deep-into-node/chapter1/ http://blog.csdn.net/wuji3390/article/details/71276849 https://feclub.cn/post/content/wq_node

 

点击查看原文阅读(728) | 评论(0) | 分类:Node.js

Consul 使用手册 (2017-09-06)

 

介绍

Consul包含多个组件,但是作为一个整体,为你的基础设施提供服务发现和服务配置的工具.他提供以下关键特性:

  • 服务发现 Consul的客户端可用提供一个服务,比如 api 或者mysql ,另外一些客户端可用使用Consul去发现一个指定服务的提供者.通过DNS或者HTTP应用程序可用很容易的找到他所依赖的服务.
  • 健康检查 Consul客户端可用提供任意数量的健康检查,指定一个服务(比如:webserver是否返回了200 OK 状态码)或者使用本地节点(比如:内存使用是否大于90%). 这个信息可由operator用来监视集群的健康.被服务发现组件用来避免将流量发送到不健康的主机.
  • Key/Value存储 应用程序可用根据自己的需要使用Consul的层级的Key/Value存储.比如动态配置,功能标记,协调,领袖选举等等,简单的HTTP API让他更易于使用.
  • 多数据中心: Consul支持开箱即用的多数据中心.这意味着用户不需要担心需要建立额外的抽象层让业务扩展到多个区域.

Consul面向DevOps和应用开发者友好.是他适合现代的弹性的基础设施.

consul-cluster

基础架构

Consul是一个分布式高可用的系统. 这节将包含一些基础,我们忽略掉一些细节这样你可以快速了解Consul是如何工作的.如果要了解更多细节,请参考深入的架构描述.

每个提供服务给Consul的阶段都运行了一个Consul agent . 发现服务或者设置和获取 key/value存储的数据不是必须运行agent.这个agent是负责对节点自身和节点上的服务进行健康检查的.

Agent与一个和多个Consul Server 进行交互.Consul Server 用于存放和复制数据.server自行选举一个领袖.虽然Consul可以运行在一台server , 但是建议使用3到5台来避免失败情况下数据的丢失.每个数据中心建议配置一个server集群.

你基础设施中需要发现其他服务的组件可以查询任何一个Consul 的server或者 agent.Agent会自动转发请求到server .

每个数据中运行了一个Consul server集群.当一个跨数据中心的服务发现和配置请求创建时.本地Consul Server转发请求到远程的数据中心并返回结果.

更多介绍查看官网点击前往

安装Consul

安装Consul,找到适合你系统的包下载他.Consul打包为一个’Zip’文件.前往下载

下载后解开压缩包.拷贝Consul到你的PATH路径中,在Unix系统中~/bin/usr/local/bin是通常的安装目录.根据你是想为单个用户安装还是给整个系统安装来选择.在Windows系统中有可以安装到%PATH%的路径中.

验证安装

完成安装后,通过打开一个新终端窗口检查consul安装是否成功.通过执行 consul你应该看到类似下面的输出

  1. [root@dhcp-10-201-102-248 ~]# consul
  2. usage: consul [--version] [--help] <command> [<args>]
  3.  
  4. Available commands are:
  5. agent Runs a Consul agent
  6. configtest Validate config file
  7. event Fire a new event
  8. exec Executes a command on Consul nodes
  9. force-leave Forces a member of the cluster to enter the "left" state
  10. info Provides debugging information for operators
  11. join Tell Consul agent to join cluster
  12. keygen Generates a new encryption key
  13. keyring Manages gossip layer encryption keys
  14. kv Interact with the key-value store
  15. leave Gracefully leaves the Consul cluster and shuts down
  16. lock Execute a command holding a lock
  17. maint Controls node or service maintenance mode
  18. members Lists the members of a Consul cluster
  19. monitor Stream logs from a Consul agent
  20. operator Provides cluster-level tools for Consul operators
  21. reload Triggers the agent to reload configuration files
  22. rtt Estimates network round trip time between nodes
  23. snapshot Saves, restores and inspects snapshots of Consul server state
  24. version Prints the Consul version
  25. watch Watch for changes in Consul

如果你得到一个consul not be found的错误,你的PATH可能没有正确设置.请返回检查你的consul的安装路径是否包含在PATH中.

运行Agent

完成Consul的安装后,必须运行agent. agent可以运行为serverclient模式.每个数据中心至少必须拥有一台server . 建议在一个集群中有3或者5个server.部署单一的server,在出现失败时会不可避免的造成数据丢失.

其他的agent运行为client模式.一个client是一个非常轻量级的进程.用于注册服务,运行健康检查和转发对server的查询.agent必须在集群中的每个主机上运行.

查看启动数据中心的细节请查看这里.

启动 Consul Server

  1. consul agent -server -bootstrap-expect 3 -data-dir /tmp/consul -node=s1 -bind=10.201.102.198 -ui-dir ./consul_ui/ -rejoin -config-dir=/etc/consul.d/ -client 0.0.0.0

运行cosnul agent以server模式,

  • -server : 定义agent运行在server模式
  • -bootstrap-expect :在一个datacenter中期望提供的server节点数目,当该值提供的时候,consul一直等到达到指定sever数目的时候才会引导整个集群,该标记不能和bootstrap共用
  • -bind:该地址用来在集群内部的通讯,集群内的所有节点到地址都必须是可达的,默认是0.0.0.0
  • -node:节点在集群中的名称,在一个集群中必须是唯一的,默认是该节点的主机名
  • -ui-dir: 提供存放web ui资源的路径,该目录必须是可读的
  • -rejoin:使consul忽略先前的离开,在再次启动后仍旧尝试加入集群中。
  • -config-dir::配置文件目录,里面所有以.json结尾的文件都会被加载
  • -client:consul服务侦听地址,这个地址提供HTTP、DNS、RPC等服务,默认是127.0.0.1所以不对外提供服务,如果你要对外提供服务改成0.0.0.0
  1. [root@dhcp-10-201-102-198 consul]# consul agent -server -bootstrap-expect 1 -data-dir /tmp/consul -node=s1 -bind=10.201.102.198 -ui-dir ./consul_ui/ -rejoin -config-dir=/etc/consul.d/ -client 0.0.0.0
  2. ==> WARNING: Expect Mode enabled, expecting 3 servers
  3. ==> Starting Consul agent...
  4. ==> Starting Consul agent RPC...
  5. ==> Consul agent running!
  6. Version: 'v0.7.4'
  7. Node ID: '422ec677-74ef-8f29-2f22-01effeed6334'
  8. Node name: 's1'
  9. Datacenter: 'dc1'
  10. Server: true (bootstrap: false)
  11. Client Addr: 0.0.0.0 (HTTP: 8500, HTTPS: -1, DNS: 8600, RPC: 8400)
  12. Cluster Addr: 10.201.102.198 (LAN: 8301, WAN: 8302)
  13. Gossip encrypt: false, RPC-TLS: false, TLS-Incoming: false
  14. Atlas: <disabled>
  15.  
  16. ==> Log data will now stream in as it occurs:
  17.  
  18. 2017/03/17 18:03:08 [INFO] raft: Restored from snapshot 139-352267-1489707086023
  19. 2017/03/17 18:03:08 [INFO] raft: Initial configuration (index=6982): [{Suffrage:Voter ID:10.201.102.199:8300 Address:10.201.102.199:8300} {Suffrage:Voter ID:10.201.102.200:8300 Address:10.201.102.200:8300} {Suffrage:Voter ID:10.201.102.198:8300 Address:10.201.102.198:8300}]
  20. 2017/03/17 18:03:08 [INFO] raft: Node at 10.201.102.198:8300 [Follower] entering Follower state (Leader: "")
  21. 2017/03/17 18:03:08 [INFO] serf: EventMemberJoin: s1 10.201.102.198
  22. 2017/03/17 18:03:08 [INFO] serf: Attempting re-join to previously known node: s2: 10.201.102.199:8301
  23. 2017/03/17 18:03:08 [INFO] consul: Adding LAN server s1 (Addr: tcp/10.201.102.198:8300) (DC: dc1)
  24. 2017/03/17 18:03:08 [INFO] consul: Raft data found, disabling bootstrap mode
  25. 2017/03/17 18:03:08 [INFO] serf: EventMemberJoin: s2 10.201.102.199
  26. 2017/03/17 18:03:08 [INFO] serf: EventMemberJoin: s3 10.201.102.200
  27. 2017/03/17 18:03:08 [INFO] serf: Re-joined to previously known node: s2: 10.201.102.199:8301
  28. 2017/03/17 18:03:08 [INFO] consul: Adding LAN server s2 (Addr: tcp/10.201.102.199:8300) (DC: dc1)
  29. 2017/03/17 18:03:08 [INFO] consul: Adding LAN server s3 (Addr: tcp/10.201.102.200:8300) (DC: dc1)
  30. 2017/03/17 18:03:08 [INFO] serf: EventMemberJoin: s1.dc1 10.201.102.198
  31. 2017/03/17 18:03:08 [INFO] consul: Adding WAN server s1.dc1 (Addr: tcp/10.201.102.198:8300) (DC: dc1)
  32. 2017/03/17 18:03:08 [WARN] serf: Failed to re-join any previously known node
  33. 2017/03/17 18:03:14 [INFO] agent: Synced service 'consul'
  34. 2017/03/17 18:03:14 [INFO] agent: Deregistered service 'consul01'
  35. 2017/03/17 18:03:14 [INFO] agent: Deregistered service 'consul02'
  36. 2017/03/17 18:03:14 [INFO] agent: Deregistered service 'consul03'
  • 查看集群成员

新开一个终端窗口运行consul members, 你可以看到Consul集群的成员.

  1. [root@dhcp-10-201-102-198 ~]# consul members
  2. Node Address Status Type Build Protocol DC
  3. s1 10.201.102.198:8301 alive server 0.7.4 2 dc1
  4. s2 10.201.102.199:8301 alive server 0.7.4 2 dc1
  5. s3 10.201.102.200:8301 alive server 0.7.4 2 dc1

启动 Consul Client

  1. consul agent -data-dir /tmp/consul -node=c1 -bind=10.201.102.248 -config-dir=/etc/consul.d/ -join 10.201.102.198

运行cosnul agent以client模式,-join 加入到已有的集群中去。

  1. [root@dhcp-10-201-102-248 ~]# consul agent -data-dir /tmp/consul -node=c1 -bind=10.201.102.248 -config-dir=/etc/consul.d/ -join 10.201.102.198
  2. ==> Starting Consul agent...
  3. ==> Starting Consul agent RPC...
  4. ==> Joining cluster...
  5. Join completed. Synced with 1 initial agents
  6. ==> Consul agent running!
  7. Version: 'v0.7.4'
  8. Node ID: '564dc0c7-7f4f-7402-a301-cebe7f024294'
  9. Node name: 'c1'
  10. Datacenter: 'dc1'
  11. Server: false (bootstrap: false)
  12. Client Addr: 127.0.0.1 (HTTP: 8500, HTTPS: -1, DNS: 8600, RPC: 8400)
  13. Cluster Addr: 10.201.102.248 (LAN: 8301, WAN: 8302)
  14. Gossip encrypt: false, RPC-TLS: false, TLS-Incoming: false
  15. Atlas: <disabled>
  16.  
  17. ==> Log data will now stream in as it occurs:
  18.  
  19. 2017/03/17 15:35:16 [INFO] serf: EventMemberJoin: c1 10.201.102.248
  20. 2017/03/17 15:35:16 [INFO] agent: (LAN) joining: [10.201.102.198]
  21. 2017/03/17 15:35:16 [INFO] serf: EventMemberJoin: s2 10.201.102.199
  22. 2017/03/17 15:35:16 [INFO] serf: EventMemberJoin: s3 10.201.102.200
  23. 2017/03/17 15:35:16 [INFO] serf: EventMemberJoin: s1 10.201.102.198
  24. 2017/03/17 15:35:16 [INFO] agent: (LAN) joined: 1 Err: <nil>
  25. 2017/03/17 15:35:16 [INFO] consul: adding server s2 (Addr: tcp/10.201.102.199:8300) (DC: dc1)
  26. 2017/03/17 15:35:16 [INFO] consul: adding server s3 (Addr: tcp/10.201.102.200:8300) (DC: dc1)
  27. 2017/03/17 15:35:16 [INFO] consul: adding server s1 (Addr: tcp/10.201.102.198:8300) (DC: dc1)
  28. 2017/03/17 15:35:16 [INFO] agent: Synced node info
  • 查看集群成员

新开一个终端窗口运行consul members, 你可以看到Consul集群的成员.

  1. [root@dhcp-10-201-102-248 ~]# consul members
  2. Node Address Status Type Build Protocol DC
  3. c1 10.201.102.248:8301 alive client 0.7.4 2 dc1
  4. s1 10.201.102.198:8301 alive server 0.7.4 2 dc1
  5. s2 10.201.102.199:8301 alive server 0.7.4 2 dc1
  6. s3 10.201.102.200:8301 alive server 0.7.4 2 dc1
  • 加入集群
  1. [root@dhcp-10-201-102-248 ~]# consul join 10.201.102.198
  2. Node Address Status Type Build Protocol DC
  3. c1 10.201.102.248:8301 alive client 0.7.4 2 dc1
  4. s1 10.201.102.198:8301 alive server 0.7.4 2 dc1
  5. s2 10.201.102.199:8301 alive server 0.7.4 2 dc1
  6. s3 10.201.102.200:8301 alive server 0.7.4 2 dc1

停止Agent

你可以使用Ctrl-C 优雅的关闭Agent. 中断Agent之后你可以看到他离开了集群并关闭.

在退出中,Consul提醒其他集群成员,这个节点离开了.如果你强行杀掉进程.集群的其他成员应该能检测到这个节点失效了.当一个成员离开,他的服务和检测也会从目录中移除.当一个成员失效了,他的健康状况被简单的标记为危险,但是不会从目录中移除.Consul会自动尝试对失效的节点进行重连.允许他从某些网络条件下恢复过来.离开的节点则不会再继续联系.

此外,如果一个agent作为一个服务器,一个优雅的离开是很重要的,可以避免引起潜在的可用性故障影响达成一致性协议.

查看这里了解添加和移除server.

更新服务

服务定义可以通过配置文件并发送SIGHUP给agent来进行更新.这样你可以让你在不关闭服务或者保持服务请求可用的情况下进行更新.

  1. consul reload

另外 HTTP API可以用来动态的添加,移除和修改服务.

注册服务

搭建好conusl集群后,用户或者程序就能到consul中去查询或者注册服务。可以通过提供服务定义文件或者调用HTTP API来注册一个服务.

首先,为Consul配置创建一个目录.Consul会载入配置文件夹里的所有配置文件.在Unix系统中通常类似 /etc/consul.d (.d 后缀意思是这个路径包含了一组配置文件).

  1. mkdir /etc/consul.d

然后,我们将编写服务定义配置文件.假设我们有一个名叫web的服务运行在 80端口.另外,我们将给他设置一个标签.这样我们可以使用他作为额外的查询方式:

  1. echo '{"service": {"name": "web", "tags": ["rails"], "port": 80}}' >/etc/consul.d/web.json

现在重启agent , 设置配置目录:

  1. $ consul agent -server -bootstrap-expect 1 -data-dir /tmp/consul -node=s1 -bind=10.201.102.198 -rejoin -config-dir=/etc/consul.d/ -client 0.0.0.0
  2.  
  3. ...
  4. [INFO] agent: Synced service 'web'
  5. ...
  • -data-dir:提供一个目录用来存放agent的状态,所有的agent允许都需要该目录,该目录必须是稳定的,系统重启后都继续存在

你可能注意到了输出了 “synced” 了 web这个服务.意思是这个agent从配置文件中载入了服务定义,并且成功注册到服务目录.

如果你想注册多个服务,你应该在Consul配置目录创建多个服务定义文件.

HTTP API注册服务,curl命令或者postman 以PUT方式请求consul HTTP API更多细节点击查看

  1. curl -X PUT -d '{"Datacenter": "dc1", "Node": "c2", "Address": "10.155.0.106", "Service": {"Service": "MAC", "tags": ["lianglian", "Mac"], "Port": 22}}' http://127.0.0.1:8500/v1/catalog/register

查询服务

一旦agent启动并且服务同步了.我们可以通过DNS或者HTTP的API来查询服务.

  • DNS API

让我们首先使用DNS API来查询.在DNS API中,服务的DNS名字是 NAME.service.consul. 虽然是可配置的,但默认的所有DNS名字会都在consul命名空间下.这个子域告诉Consul,我们在查询服务,NAME则是服务的名称.

对于我们上面注册的Web服务.它的域名是 web.service.consul :

  1. [root@dhcp-10-201-102-198 ~]# dig @127.0.0.1 -p 8600 web.service.consul
  2.  
  3. ; <<>> DiG 9.8.2rc1-RedHat-9.8.2-0.17.rc1.el6 <<>> @127.0.0.1 -p 8600 web.service.consul
  4. ; (1 server found)
  5. ;; global options: +cmd
  6. ;; Got answer:
  7. ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 39468
  8. ;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
  9. ;; WARNING: recursion requested but not available
  10.  
  11. ;; QUESTION SECTION:
  12. ;web.service.consul. IN A
  13.  
  14. ;; ANSWER SECTION:
  15. web.service.consul. 0 IN A 10.201.102.198
  16.  
  17. ;; Query time: 0 msec
  18. ;; SERVER: 127.0.0.1#8600(127.0.0.1)
  19. ;; WHEN: Tue Mar 28 16:10:24 2017
  20. ;; MSG SIZE rcvd: 52
  21.  
  22. [root@dhcp-10-201-102-198 ~]#

如你所见,一个A记录返回了一个可用的服务所在的节点的IP地址.`A记录只能设置为IP地址. 有也可用使用 DNS API 来接收包含 地址和端口的 SRV记录:

  1. [root@dhcp-10-201-102-198 ~]# dig @127.0.0.1 -p 8600 web.service.consul SRV
  2.  
  3. ; <<>> DiG 9.8.2rc1-RedHat-9.8.2-0.17.rc1.el6 <<>> @127.0.0.1 -p 8600 web.service.consul SRV
  4. ; (1 server found)
  5. ;; global options: +cmd
  6. ;; Got answer:
  7. ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 13331
  8. ;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
  9. ;; WARNING: recursion requested but not available
  10.  
  11. ;; QUESTION SECTION:
  12. ;web.service.consul. IN SRV
  13.  
  14. ;; ANSWER SECTION:
  15. web.service.consul. 0 IN SRV 1 1 80 s1.node.dc1.consul.
  16.  
  17. ;; ADDITIONAL SECTION:
  18. s1.node.dc1.consul. 0 IN A 10.201.102.198
  19.  
  20. ;; Query time: 0 msec
  21. ;; SERVER: 127.0.0.1#8600(127.0.0.1)
  22. ;; WHEN: Tue Mar 28 16:10:56 2017
  23. ;; MSG SIZE rcvd: 84
  24.  
  25. [root@dhcp-10-201-102-198 ~]#

SRV记录告诉我们 web 这个服务运行于节点dhcp-10-201-102-198 的80端口. DNS额外返回了节点的A记录.

最后,我们也可以用 DNS API 通过标签来过滤服务.基于标签的服务查询格式为TAG.NAME.service.consul. 在下面的例子中,我们请求Consul返回有 rails标签的 web服务.我们成功获取了我们注册为这个标签的服务:

  1. [root@dhcp-10-201-102-198 ~]# dig @127.0.0.1 -p 8600 rails.web.service.consul SRV
  2.  
  3. ; <<>> DiG 9.8.2rc1-RedHat-9.8.2-0.17.rc1.el6 <<>> @127.0.0.1 -p 8600 rails.web.service.consul SRV
  4. ; (1 server found)
  5. ;; global options: +cmd
  6. ;; Got answer:
  7. ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 37307
  8. ;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
  9. ;; WARNING: recursion requested but not available
  10.  
  11. ;; QUESTION SECTION:
  12. ;rails.web.service.consul. IN SRV
  13.  
  14. ;; ANSWER SECTION:
  15. rails.web.service.consul. 0 IN SRV 1 1 80 s1.node.dc1.consul.
  16.  
  17. ;; ADDITIONAL SECTION:
  18. s1.node.dc1.consul. 0 IN A 10.201.102.198
  19.  
  20. ;; Query time: 0 msec
  21. ;; SERVER: 127.0.0.1#8600(127.0.0.1)
  22. ;; WHEN: Tue Mar 28 16:11:45 2017
  23. ;; MSG SIZE rcvd: 90
  24.  
  25. [root@dhcp-10-201-102-198 ~]#
  • HTTP API

除了DNS API之外,HTTP API也可以用来进行服务查询:

  1. [root@dhcp-10-201-102-198 ~]# curl -s 127.0.0.1:8500/v1/catalog/service/web | python -m json.tool
  2. [
  3. {
  4. "Address": "10.201.102.198",
  5. "CreateIndex": 492843,
  6. "ID": "422ec677-74ef-8f29-2f22-01effeed6334",
  7. "ModifyIndex": 492843,
  8. "Node": "s1",
  9. "NodeMeta": {},
  10. "ServiceAddress": "",
  11. "ServiceEnableTagOverride": false,
  12. "ServiceID": "web",
  13. "ServiceName": "web",
  14. "ServicePort": 80,
  15. "ServiceTags": [
  16. "rails"
  17. ],
  18. "TaggedAddresses": {
  19. "lan": "10.201.102.198",
  20. "wan": "10.201.102.198"
  21. }
  22. }