Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

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

16,012 views

Published on

Published in: Technology
  • Login to see the comments

高性能并发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

×