cxin 发表于 2018-11-18 07:58:11

网络编程的演进——从 Apache 到 Nginx

Apache
  Apache HTTP 服务器是 Robert McCool 在 1995 年写成,并在 1999 年开始在 Apache 软件基金会的 框架下进行开发。由于 Apache HTTP 服务器是基金会最开始的一个项目也是最为有名的一个项目, 所以通常大家提到 Apache 这个词都是说的 Apache HTTP Server。
  Apache web 服务器从 1996 年开始就是互联网上最为流行的 HTTP 服务器。Apache 之所以这么流行 很大程度上是由于相比其他的软件项目,在 Apache 基金会的精心维护下他的文档十分的详尽还有 集成的支持服务。
  Apache 由于其可变性、高性能和广泛的支持,经常是系统管理员的首选。他可以通过一系列 的语言相关的扩展模块支持很多解释型语言的后端,而不需要连接一个独立的后端程序。
  Apache 软件基金会也是利用开源软件盈利的一个范本。时至今日,Apache 软件基金会 已经枝繁叶茂,在基金会名下的开源项目我们耳熟能详的有:


[*]Apache HTTP Server
[*]Ant( Java 的编译工具)
[*]ActiveMQ( MQ 集群)
[*]Cassandra(强一致的分布式 KV 数据库)
[*]CloudStack( OpenStack 的劲敌)
[*]CouchDB( KV 数据库)
[*]Flume(日志收集工具)
[*]Hadoop、Hbase、Hive
[*]Kafka(流式计算)
[*]Lucene(开源搜索引擎)
[*]Maven(Java编译&依赖管理工具)
[*]Mesos(分布式协调)
[*]OpenNLP(开源自然语言处理库)
[*]OpenOffice(开源的类 Office 工具)
[*]Perl( Perl 语言)
[*]Spark(分布式计算集群)
[*]Storm(流式计算)
[*]Structs(Java SSH 框架的第二个 S)
[*]Subversion( SVN,你懂的)
[*]Tcl( Tcl 语言)
[*]Thrift( Java 网络框架)
[*]Tomcat(大名鼎鼎的 Java 容器)
[*]ZooKeeper(分布式协调集群)
  完整的 Apache 基金会的项目列表参见:Welcome to The Apache Software Foundation!

Nginx
  2002年,一个叫 Igor Sysoev 的俄罗斯哥们儿(貌似俄罗斯叫 Igor 的人挺多的) 写出了一个叫 Nginx(和 Engine X 谐音,取引擎之义)。 那时候有一个时代背景,当时 C10K( Concurrency 10K,1万并发)问题还是困扰绝大多数 web 服务器的一个难题。Nginx 利用异步事件驱动的架构写成,是 C10K 问题的一个很好的答卷。 Nginx 的第一个公开发行版是在 2004 年发布的,之前都是作为俄罗斯访问量第二的网站 Rambler 的内部使用。
  Nginx 的主要优势在于“轻、快、活”:


  很低的资源占用,甚至能在很多嵌入式设备上运行。


  响应速度超快,几乎不会由于高并发影响响应速度。


  配置灵活,广泛的模块支持。
  网上关于 Apache 和 Nginx 性能比较的文章非常多,基本上有如下的定论:


[*]Nginx 在并发性能上比 Apache 强很多,如果是纯静态资源(图片、JS、CSS)那么 Nginx 是不二之选。
[*]Apache 有 mod_php、在 PHP 类的应用场景下比 Nginx 部署起来简单很多。一些老的 PHP 项目用 Apache 来配置运行非常的简单,例如 Wordpress。
[*]对于初学者来说 Apache 配置起来非常复杂冗长的类 XML 语法,甚至支持在子目录放置.htaccess 文件来配置子目录的属性。Nginx 的配置文件相对简单一点。
[*]Nginx 的模块比较容易写,可以通过写 C 的 mod 实现接口性质的服务,并且拥有惊人的性能。 分支OpenResty,可以配合 lua 来实现很多自定义功能,兼顾扩展性和性能。
  这里我们要着重讨论的是:为什么 Nginx 在并发性能上比 Apache 要好很多?
  想要了解这个问题,不得不先做一些铺垫,讲讲并发网络编程的一些历史:

壹 ——最原始
  最原始的网络编程的伪代码大致是这样:
  

00 listen(port)# 监听在接收服务的端口上  
01 while True:   # 一直循环
  
02conn = accept()    # 接收连接
  
03if fork() == 0:
  
04    # 子进程
  
05    read_content = read(conn) # 读取连接发送过来的请求
  
06    response = process(conn) # 执行业务逻辑,并得到给客户端回应的内容
  
07    conn.write(response) # 将回应写回给连接
  

贰—— 每个连接开一个进程
  后来,大家想到了办法:
  用子进程来处理连接,父进程继续等待连接进来。但这种方式有如下两个明显的缺陷:


[*]fork() 调用比较费时,需要对进程进行内存拷贝。即使现在的 Linux 普遍 引入了 COW(Copy On Write)技术(fork 的时候不做内存拷贝,只有其中一个 副本发生了 write 的时候才进行 copy)加速了 fork 的效率,但 fork 依旧是个 比较“重”的系统调用。
[*]较多的内存占用,也是由于上述的内存复制造成的。
叁——引入线程
  得益于之前提过的 Linux 对于线程的引入,上面例子的开进程,被换成了开线程, 这样,上一小节说的两个缺陷都大大的被缓解了。

肆—— 进程/线程池
  计算机领域有很多算法或者是方法都会用到一种智慧:“空间换时间”。 即用使用更多内存的方式换取更快的运行速度:事先创建出很多进程/线程 ,就像一个池子,这样虽然会浪费一部分的内存,但连接过来的时候就省去了 开启进程/线程的时间。
  但这种方式会有一个比较显著的缺陷:当并发数大于进程/线程池的大小的时候 性能就会发生很大的下滑,退化成“贰”的情况。

伍 ——非阻塞&事件驱动
  那么,是不是想要达到高性能就一定要付出高系统资源占用呢? 答案是否定的,如果我们注意观察生活中的一个细节,肯德基和麦当劳的不同 服务方式:

肯德基
  服务员在前台问:“先生/×××,有什么可以帮你?”
  顾客,思考一下点什么比较好:“我要,xxxxx”
  服务员去后台配餐、取餐,3分钟过去了:“您的餐齐了,下一位”

麦当劳
  服务员在前台问:“先生/×××,有什么可以帮你?”
  顾客:“我要,xxxxx”。如果顾客思考超过5秒:“后面的顾客请先点”; 点完餐,前台服务员继续为下一位顾客点餐。后台有别的服务员完成配餐。
  可以思考一下,这两种运作方式那种比较好:


[*]在肯德基,如果遇到需要纠结半天吃什么的客户。服务员和后面的顾客 都会陷入较长时间的等候。原因就是如果最前面的客户先让后面的顾客点餐 ,他想好了还需要较长时间的等候。相比之下,麦当劳就更胜一筹。
[*]在麦当劳,后面配餐的服务员如果发现有两个订单都要了可乐。他可以 智能地把两个订单的可乐一次性灌好,这样会大大的提高效率。各个岗位上 的服务员可以灵活的采用各种方式优化自己的工作效率。  这里,肯德基的服务方式就是古老的进程/线程池;麦当劳的服务方式 就是一个简单的非阻塞&事件驱动。

  那么,非阻塞&事件驱动这么好,为什么大家没有一开始就采用这种方式呢? 原因有二:


[*]非阻塞&事件驱动需要系统的支持,提供non-blocking版的整套 系统调用。
[*]非阻塞&事件驱动编程难度较大,需要很高的抽象思维能力, 把整个任务拆解;采用有限状态机编程才能实现。


页: [1]
查看完整版本: 网络编程的演进——从 Apache 到 Nginx