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

15,284 views

Published on

Published in: Technology
2 Comments
64 Likes
Statistics
Notes
No Downloads
Views
Total views
15,284
On SlideShare
0
From Embeds
0
Number of Embeds
6,199
Actions
Shares
0
Downloads
537
Comments
2
Likes
64
Embeds 0
No embeds

No notes for slide

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

  1. 1. 高性能并发 Web 服务器实现核心内幕 ideawu 百度服务器研发高级工程师 http://www.ideawu.net/
  2. 2. 内容简介 <ul><li>Not Apache/Lighttpd/Nginx source code </li></ul><ul><li>理论 , 基础 , 通用代码 ( 核心内幕 ) </li></ul><ul><li>如何进化 </li></ul><ul><li>高性能 Web 服务器实现核心内幕 </li></ul><ul><ul><li>高性能网络服务器的实现原理 </li></ul></ul><ul><ul><li>Web 服务器的实现 </li></ul></ul><ul><li>socket 基础 , 先学会走再学会飞 </li></ul>
  3. 3. 理论结合实践 , 实践结合理论 <ul><li>“ 理论要结合实践” , 是对理论的贬低吗 ? </li></ul><ul><li>Linus 不喜欢低级的试错 </li></ul><ul><ul><li>别告诉我哪个对 ( 错 ), 告诉我那一个为什么对 ( 错 ) </li></ul></ul><ul><li>理论和实践 </li></ul><ul><ul><li>理论不结合实践 - 书呆子 </li></ul></ul><ul><ul><li>实践不结合理论 - 业余者 </li></ul></ul><ul><ul><li>理论结合实践 - 科学家 </li></ul></ul><ul><ul><li>实践结合理论 - 专业者 </li></ul></ul>阅读了 XX 源码的人 普通码农 一楼 掌握了理论的人 二楼 能进行理论创新的人 三楼
  4. 4. 最原始的网络服务器 <ul><li>网络 IO 的基础 </li></ul><ul><ul><li>ssize_t read(int fd, void *buf, size_t count); </li></ul></ul><ul><ul><li>ssize_t write(int fd, const void *buf, size_t count); </li></ul></ul><ul><li>特点 : 阻塞 </li></ul><ul><li>一次性服务 </li></ul><ul><li>无协议 </li></ul><ul><li>短连接 </li></ul><ul><li>如何重复服务 ? </li></ul>serv = tcp_socket(); listen(serv); sock = accept(serv); read(sock, data); write(sock, data); close(sock)
  5. 5. 网络协议 <ul><li>协议包含两个部分 </li></ul><ul><ul><li>语法 ( 报文格式 ) </li></ul></ul><ul><ul><li>语义 ( 指令的处理 , 交互时序等 ) </li></ul></ul><ul><li>最重要的 TCP 协议是流式协议 , 但几乎所有的应用协议都是基于报文的协议 </li></ul><ul><ul><li>TCP 的”粘包”和”分包” </li></ul></ul><ul><ul><li>报文分隔 </li></ul></ul><ul><ul><ul><li>用连接关闭来表示报文结束 . 如 , HTTP/1.0 的响应 </li></ul></ul></ul><ul><ul><ul><li>固定长度的报文 . 如 , TFTP 的数据报文 . </li></ul></ul></ul><ul><ul><ul><li>带自描述长度的固定长度首部的变长报文 . 如 IP 包 , TCP 分段 , nshead( 是协议吗 ? ). </li></ul></ul></ul><ul><ul><ul><li>带结束符 . 如 , 行协议 , HTTP 协议 . 逐字节解析和数据转义的影响 . </li></ul></ul></ul>二进制 , 固定长度 底层 文本 , 带结束符 高层
  6. 6. 带有协议的网络服务器 <ul><li>如何读取报文 ? </li></ul><ul><ul><li>尽可能多地读取 (read) 数据到用户缓冲区中 , 即使是固定长度报文 , 也不要读取指定长度 . </li></ul></ul><ul><ul><li>判断用户缓冲区中的数据是否包含至少一个报文 </li></ul></ul><ul><li>Packet 是协议的报文 </li></ul><ul><li>能不能使用 TCP 报文的格式 ? UDP? IP? ICMP? </li></ul>serv = tcp_socket(); listen(serv); sock = accept(serv); packet_read(sock, packet); packet_write(sock, packet); close(sock)
  7. 7. 单个连接的连续服务 ( 长连接 ) <ul><li>在一个循环里不断得读取请求 , 处理 , 然后发送响应 . </li></ul><ul><li>serv = tcp_socket(); </li></ul><ul><li>listen(serv); </li></ul><ul><li>sock = accept(serv); </li></ul><ul><li>while(1){ </li></ul><ul><li>packet_read(sock, request); </li></ul><ul><li>if(request == EXIT){ </li></ul><ul><li>break; </li></ul><ul><li>} </li></ul><ul><li>response = handle_packet(request); </li></ul><ul><li>packet_write(sock, response); </li></ul><ul><li>} </li></ul><ul><li>close(sock) </li></ul>
  8. 8. 可以处理多个连接的网络服务器 <ul><li>在外层加一个循环 </li></ul><ul><li>while(1){ </li></ul><ul><li>sock = accept(serv); </li></ul><ul><li>while(1){ </li></ul><ul><li>packet_read(sock, request); </li></ul><ul><li>if(request == EXIT){ </li></ul><ul><li>break; </li></ul><ul><li>} </li></ul><ul><li>response = handle_packet(request); </li></ul><ul><li>packet_write(sock, response); </li></ul><ul><li>// close(sock); // 短连接 </li></ul><ul><li>} </li></ul><ul><li>close(sock); // 长连接 </li></ul><ul><li>} </li></ul><ul><li>缺点 : 必须等一个连接关闭或者退出后 , 才能处理下一个连接 , 不是并发服务器 . </li></ul>
  9. 9. 并发网络服务器 <ul><li>并发服务器是指 , 同时处理多个请求的服务器 . 并发的原理 : </li></ul><ul><ul><li>多核 ( 多线程 , 多进程 ) </li></ul></ul><ul><ul><li>分片 ( 请求处理的切分 ) </li></ul></ul><ul><li>并发的基本实现 – 避免阻塞 ( 解阻塞 )! </li></ul><ul><ul><li>使用非阻塞的接口来替代 </li></ul></ul><ul><ul><ul><li>IO 多路复用 </li></ul></ul></ul><ul><ul><li>找出阻塞的地方 , 委托出去 . </li></ul></ul><ul><ul><ul><li>委托给操作系统内核 sendfile() </li></ul></ul></ul><ul><ul><ul><li>委托给多线程 / 多进程 ( 后面不讨论多进程 ) </li></ul></ul></ul><ul><ul><ul><li>委托给网络服务 </li></ul></ul></ul><ul><ul><li>委托有时候也叫做 &quot; 异步 &quot;. </li></ul></ul>
  10. 10. 阻塞 <ul><li>while(1){ </li></ul><ul><li>// 可能阻塞 </li></ul><ul><li>sock = accept(serv); </li></ul><ul><li>while(1){ </li></ul><ul><li>// 可能阻塞 </li></ul><ul><li>packet_read(sock, request); </li></ul><ul><li>if(request == EXIT){ </li></ul><ul><li>break; </li></ul><ul><li>} </li></ul><ul><li>// 可能阻塞 </li></ul><ul><li>response = handle_packet(request); </li></ul><ul><li>// 可能阻塞 </li></ul><ul><li>packet_write(sock, response); </li></ul><ul><li>} </li></ul><ul><li>close(sock); </li></ul><ul><li>} </li></ul><ul><li>至少要有一个阻塞 , 所以可以在 accept() 之后进行“解阻塞” . </li></ul><ul><li>奇迹 => ... </li></ul>
  11. 11. 原始多线程并发网络服务器 <ul><li>while(1){ </li></ul><ul><li>// 可能阻塞 </li></ul><ul><li>sock = accept(serv); </li></ul><ul><li>RUN_IN_NEW_THREAD{ </li></ul><ul><li>while(1){ </li></ul><ul><li>// 可能阻塞 </li></ul><ul><li>packet_read(sock, packet); </li></ul><ul><li>if(packet == EXIT){ </li></ul><ul><li>break; </li></ul><ul><li>} </li></ul><ul><li>// 可能阻塞 </li></ul><ul><li>response = handle_packet(packet); </li></ul><ul><li>// 可能阻塞 </li></ul><ul><li>packet_write(sock, response); </li></ul><ul><li>} </li></ul><ul><li>close(sock); </li></ul><ul><li>} </li></ul><ul><li>} </li></ul><ul><li>&quot;RUN_IN_NEW_THREAD&quot; 表示创建线程 , 这个线程叫做 &quot; 工作线程 &quot;. </li></ul>
  12. 12. 原始多线程并发网络服务器 ( 续 ) <ul><li>缺点 : </li></ul><ul><ul><li>线程的数量无法得到控制 . </li></ul></ul><ul><ul><li>如果是短连接 , 创建线程的成本可能相对请求处理的成本更大 </li></ul></ul><ul><li>要解决的问题 : </li></ul><ul><ul><li>如何控制线程的数量 ? </li></ul></ul><ul><ul><li>如何避免创建线程对性能的影响 </li></ul></ul>
  13. 13. 线程池并发网络服务器 <ul><li>初始化时创建线程池 </li></ul><ul><li>主进程中 accept() 之后 , 把 socket 传给工作线程 </li></ul><ul><li>但又带来了一个问题 : 虽然可以不断地接受连接 , 但毕竟工作线程有限 , 还是会出现连接排队等线程的情况 . 当连接数少时是线程等连接 , 但当连接数多时是连接等线程 . </li></ul><ul><li>怎么解决 ? </li></ul><ul><ul><li>调优工作线程的数量 . </li></ul></ul><ul><ul><li>硬件问题 , 不是软件所能解决的 , 增加机器 . </li></ul></ul><ul><ul><li>改变服务器架构 , to be continued... </li></ul></ul>
  14. 14. IO 多路复用 (IO Multiplex) <ul><li>前面的架构瓶颈在哪 ? </li></ul><ul><li>把 IO 委托给操作系统内核 </li></ul><ul><li>操作系统告知是否可读或者可写 </li></ul><ul><ul><li>轮询等通知 (select, epoll, kqueue) </li></ul></ul><ul><li>可读 / 写表示 只 能最多成功调用一次 read/write 而不阻塞 </li></ul><ul><li>IO 多路复用只能解决 IO 阻塞 , 阻塞的类型还有很多种 ! </li></ul>
  15. 15. IO 多路复用函数介绍 <ul><li>前面的架构瓶颈在哪 ? </li></ul><ul><li>基本 IO 多路复用函数 : </li></ul><ul><ul><li>int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); </li></ul></ul><ul><li>简化 : </li></ul><ul><ul><li>(rfds_out, wfds_out) = select(rfds_in, wfds_in, timeout); </li></ul></ul><ul><ul><li>功能 : 判断 rfds_in 和 wfds_in 两个列表中的 socket 连接 , 只要有至少一个可读或者可写 , 就返回 . 或者超时返回 . </li></ul></ul><ul><ul><li>rfds_in: 要测试的是否可读的 socket 列表 </li></ul></ul><ul><ul><li>wfds_in: 要测试的是否可写的 socket 列表 </li></ul></ul><ul><ul><li>rfds_out: 返回可读的 socket 列表 </li></ul></ul><ul><ul><li>wfds_out: 返回可写的 socket 列表 </li></ul></ul><ul><ul><li>timeout: 超时时间 , -1 表示不永超时 </li></ul></ul>
  16. 16. 委托给网络服务 <ul><li>回顾避免阻塞 ( 解阻塞 ) 的方法 : </li></ul><ul><ul><li>使用非阻塞的接口来替代 </li></ul></ul><ul><ul><ul><li>IO 多路复用 </li></ul></ul></ul><ul><ul><li>找出阻塞的地方 , 委托出去 . </li></ul></ul><ul><ul><ul><li>委托给操作系统内核 sendfile() </li></ul></ul></ul><ul><ul><ul><li>委托给多线程 / 多进程 ( 后面不讨论多进程 ) </li></ul></ul></ul><ul><ul><ul><li>委托给网络服务 </li></ul></ul></ul><ul><li>如 Apache/Lighttpd/Niginx 把请求通过 fastcgi( 网络 ) 委托给 php-cgi 进程 ( 网络服务器 ). </li></ul><ul><li>委托给网络服务 , 这是一个递归过程 </li></ul>
  17. 17. HTTP 服务器 (Web 服务器 ) <ul><li>报文解析 : 实现 packet_read() </li></ul><ul><ul><li>用抓包工具抓一个 HTTP 请求报文和一个 HTTP 响应报文 </li></ul></ul><ul><ul><li>对照着 RFC </li></ul></ul><ul><ul><li>上面两步就是理论结合实践 , 实践结合理论 </li></ul></ul><ul><li>语义实现 : 实现 handle_packet() </li></ul><ul><ul><li>静态文件 </li></ul></ul><ul><ul><ul><li>大文件 </li></ul></ul></ul><ul><ul><ul><li>小文件 </li></ul></ul></ul><ul><ul><li>脚本处理 , 以 php 为例 </li></ul></ul><ul><ul><ul><li>CGI </li></ul></ul></ul><ul><ul><ul><li>FastCGI </li></ul></ul></ul><ul><ul><ul><li>Apache mod_php </li></ul></ul></ul><ul><li>相对来说 , 报文的发送比较通用 . </li></ul>
  18. 18. Web 服务器的一般架构 <ul><li>Web 服务器将客户端的请求委托给 PHP FastCGI 进程 ( 是一个独立的网络服务 ) 处理 </li></ul><ul><li>Web 服务器从 FastCGI 进程读取数据后 , 返回给浏览器 </li></ul><ul><li>如果不是独立的 FastCGI 服务 , 也可以是嵌入到 Web 服务器内的线程 / 进程 ( 如 Apache mod_php). </li></ul>
  19. 19. 报文解析 <ul><li>http://www.ideawu.net/person/pyhttp/ </li></ul><ul><li>使用 Python 的基本 socket 接口和字符串处理能力 , 实现了基本的 HTTP 协议报文的解析和协议实现 . </li></ul><ul><li>为 IO 复用预留了接口 </li></ul>
  20. 20. 静态文件请求的处理 <ul><li>文件 IO 会阻塞 </li></ul><ul><li>委托给线程 </li></ul><ul><li>避免文件 IO - 内存缓存 </li></ul><ul><li>委托给操作系统 – sendfile() </li></ul>
  21. 21. CGI <ul><li>多进程 </li></ul><ul><li>用环境变量来传递请求的 HTTP 报头信息和服务器信息 </li></ul><ul><li>用 stdin 传递请求的 HTTP 报体 </li></ul><ul><li>用 stdout 发送响应报头 ( 部分 ) 和报体 </li></ul><ul><li>缺点 : </li></ul><ul><ul><li>由于使用环境变量来通信 , 扩展性受限 </li></ul></ul><ul><ul><li>一个进程的生命周期只处理一个请求 </li></ul></ul>
  22. 22. FastCGI <ul><li>委托给网络 </li></ul>
  23. 23. 补充话题 <ul><li>IO 多路复用模型中 , 为什么不能用标准 IO 库的行读取函数 fgets() 来读取 HTTP 的首部 . </li></ul><ul><ul><li>因为 fgets() 调用可能多于一次 read(), 是可阻塞的 </li></ul></ul><ul><li>文本协议和二进制协议如何取舍 </li></ul><ul><ul><li>报文的格式只是协议的其中一项内容 , 语义是另一项更重要的内容 . </li></ul></ul><ul><ul><li>文本协议总是优于二进制协议 ( 除了少数情况 ) </li></ul></ul><ul><ul><li>应该更关注的是 , 报文是定长报文还是变长报文 ! </li></ul></ul><ul><ul><li>参考 HTTP, 报头 ( 元数据部分 ) 是文本 , 报体可以是二进制数据 . </li></ul></ul><ul><li>另外 , 冒号分隔的 key-value 行文本报头格式 , 是最简单最通用的报文格式 . </li></ul><ul><li>把 &quot;TCP/IP 协议详解 - 卷 1&quot;, &quot;Unix 网络编程 - 卷 1&quot;, &quot; 计算机网络 &quot; 这几本书好好看一遍 ! </li></ul>
  24. 24. FAQ IT 牛人 http://www.udpwork.com/
  25. 25. FIN Thanks

×