AirJD 焦点
AirJD

没有录音文件
00:00/00:00
加收藏

高性能并发Web服务器实现核心内幕

发布者 architecture
发布于 1429504344511  浏览 8831 关键词 架构, 并发 
分享到

第1页

高性能并发Web服务器实现核心内幕
ideawu
百度服务器研发高级工程师
http://www.ideawu.net/

第2页

内容简介
Not Apache/Lighttpd/Nginx source code
理论, 基础, 通用代码(核心内幕)
如何进化

高性能Web服务器实现核心内幕
高性能网络服务器的实现原理
Web服务器的实现

socket基础, 先学会走再学会飞

第3页

理论结合实践, 实践结合理论
“理论要结合实践”, 是对理论的贬低吗?
Linus不喜欢低级的试错
别告诉我哪个对(错), 告诉我那一个为什么对(错)

理论和实践
理论不结合实践 - 书呆子
实践不结合理论 - 业余者
理论结合实践 - 科学家
实践结合理论 - 专业者

第4页

最原始的网络服务器
网络IO的基础
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
特点: 阻塞

第5页

网络协议
协议包含两个部分
语法(报文格式)
语义(指令的处理, 交互时序等)
最重要的TCP协议是流式协议,                                但几乎所有的应用协议都是基于报文的协议
TCP的”粘包”和”分包”
报文分隔
用连接关闭来表示报文结束. 如, HTTP/1.0的响应
固定长度的报文. 如, TFTP的数据报文.
带自描述长度的固定长度首部的变长报文. 如IP包, TCP分段, nshead(是协议吗?).
带结束符. 如, 行协议, HTTP协议. 逐字节解析和数据转义的影响.

第6页

带有协议的网络服务器
如何读取报文?
尽可能多地读取(read)数据到用户缓冲区中, 即使是固定长度报文, 也不要读取指定长度.
判断用户缓冲区中的数据是否包含至少一个报文

第7页

单个连接的连续服务(长连接)
在一个循环里不断得读取请求, 处理, 然后发送响应.

serv = tcp_socket();
listen(serv);
sock = accept(serv);
while(1){
packet_read(sock, request);
if(request == EXIT){
break;
}
response = handle_packet(request);
packet_write(sock, response);
}
close(sock)

第8页

可以处理多个连接的网络服务器
在外层加一个循环

while(1){
sock = accept(serv);
while(1){
packet_read(sock, request);
if(request == EXIT){
break;
}
response = handle_packet(request);
packet_write(sock, response);
// close(sock); // 短连接
}
close(sock); // 长连接
}

缺点: 必须等一个连接关闭或者退出后, 才能处理下一个连接, 不是并发服务器.

第9页

并发网络服务器
并发服务器是指, 同时处理多个请求的服务器. 并发的原理:
多核(多线程, 多进程)
分片(请求处理的切分)
并发的基本实现 – 避免阻塞(解阻塞)!
使用非阻塞的接口来替代
IO多路复用
找出阻塞的地方, 委托出去.
委托给操作系统内核 sendfile()
委托给多线程/多进程(后面不讨论多进程)
委托给网络服务
委托有时候也叫做"异步".

第10页

阻塞
while(1){
// 可能阻塞
sock = accept(serv);
while(1){
// 可能阻塞
packet_read(sock, request);
if(request == EXIT){
break;
}
// 可能阻塞
response = handle_packet(request);
// 可能阻塞
packet_write(sock, response);
}
close(sock);
}

至少要有一个阻塞, 所以可以在accept()之后进行“解阻塞”.
奇迹 => ...

第11页

原始多线程并发网络服务器
while(1){
// 可能阻塞
sock = accept(serv);

RUN_IN_NEW_THREAD{
while(1){
// 可能阻塞
packet_read(sock, packet);
if(packet == EXIT){
break;
}
// 可能阻塞
response = handle_packet(packet);
// 可能阻塞
packet_write(sock, response);
}
close(sock);
}
}

"RUN_IN_NEW_THREAD"表示创建线程, 这个线程叫做"工作线程".

第12页

原始多线程并发网络服务器(续)
缺点:
线程的数量无法得到控制.
如果是短连接, 创建线程的成本可能相对请求处理的成本更大

要解决的问题:
如何控制线程的数量?
如何避免创建线程对性能的影响

第13页

线程池并发网络服务器
初始化时创建线程池
主进程中accept()之后, 把socket传给工作线程

但又带来了一个问题: 虽然可以不断地接受连接, 但毕竟工作线程有限, 还是会出现连接排队等线程的情况. 当连接数少时是线程等连接, 但当连接数多时是连接等线程.
怎么解决?
调优工作线程的数量.
硬件问题, 不是软件所能解决的, 增加机器.
改变服务器架构, to be continued...

第14页

IO多路复用(IO Multiplex)
前面的架构瓶颈在哪?
把IO委托给操作系统内核
操作系统告知是否可读或者可写
轮询等通知(select, epoll, kqueue)
可读/写表示只能最多成功调用一次read/write而不阻塞

IO多路复用只能解决IO阻塞, 阻塞的类型还有很多种!

第15页

IO多路复用函数介绍
前面的架构瓶颈在哪?
基本IO多路复用函数:
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
简化:
 (rfds_out, wfds_out) = select(rfds_in, wfds_in, timeout);

功能: 判断rfds_in和wfds_in两个列表中的socket连接, 只要有至少一个可读或者可写, 就返回. 或者超时返回.
rfds_in: 要测试的是否可读的socket列表
wfds_in: 要测试的是否可写的socket列表
rfds_out: 返回可读的socket列表
wfds_out: 返回可写的socket列表
timeout: 超时时间, -1表示不永超时

第16页

委托给网络服务
回顾避免阻塞(解阻塞)的方法:
 使用非阻塞的接口来替代
IO多路复用
找出阻塞的地方, 委托出去.
委托给操作系统内核 sendfile()
委托给多线程/多进程(后面不讨论多进程)
委托给网络服务
如Apache/Lighttpd/Niginx把请求通过fastcgi(网络)委托给php-cgi进程(网络服务器).
委托给网络服务, 这是一个递归过程

第17页

HTTP服务器(Web服务器)
报文解析: 实现 packet_read()
用抓包工具抓一个HTTP请求报文和一个HTTP响应报文
对照着RFC
上面两步就是理论结合实践, 实践结合理论
语义实现: 实现 handle_packet()
静态文件
大文件
小文件
脚本处理, 以php为例
CGI
FastCGI
Apache mod_php

相对来说, 报文的发送比较通用.

第18页

Web服务器的一般架构
Web服务器将客户端的请求委托给PHP FastCGI进程(是一个独立的网络服务)处理
Web服务器从FastCGI进程读取数据后, 返回给浏览器
如果不是独立的FastCGI服务, 也可以是嵌入到Web服务器内的线程/进程(如Apache mod_php).

第19页

报文解析
http://www.ideawu.net/person/pyhttp/

使用Python的基本socket接口和字符串处理能力, 实现了基本的HTTP协议报文的解析和协议实现.
为IO复用预留了接口 

第20页

静态文件请求的处理
文件IO会阻塞
委托给线程
避免文件IO - 内存缓存
委托给操作系统 – sendfile()

第21页

CGI
多进程
用环境变量来传递请求的HTTP报头信息和服务器信息
用stdin传递请求的HTTP报体
用stdout发送响应报头(部分)和报体

缺点:
由于使用环境变量来通信, 扩展性受限
一个进程的生命周期只处理一个请求

第22页

FastCGI
委托给网络

第23页

补充话题
IO多路复用模型中, 为什么不能用标准IO库的行读取函数fgets()来读取HTTP的首部.
因为fgets()调用可能多于一次read(), 是可阻塞的

文本协议和二进制协议如何取舍
报文的格式只是协议的其中一项内容, 语义是另一项更重要的内容.
文本协议总是优于二进制协议(除了少数情况)
应该更关注的是, 报文是定长报文还是变长报文!
参考HTTP, 报头(元数据部分)是文本, 报体可以是二进制数据.

另外, 冒号分隔的key-value行文本报头格式, 是最简单最通用的报文格式.

把"TCP/IP协议详解-卷1", "Unix网络编程-卷1", "计算机网络"这几本书好好看一遍!

第24页

FAQ
IT牛人http://www.udpwork.com/

第25页

FIN
Thanks
支持文件格式:*.pdf
上传最后阶段需要进行在线转换,可能需要1~2分钟,请耐心等待。