一、概述
软件技术发展至今,存在着很多成熟的开发框架(如广大 Java 程序员所熟知的 SSH 框架),这些开发框架或面向数据库,或面向网络通信,或面向应用服务器,或面向界面设计,甚至面向某类业务模型。这些开框架的存在,大大提高了程序员的开发效率,这样使技术人员将精力更多地集中于业务本身,而不必拘泥于技术的底层实现细节,但也造成了众多知其然不知其所以然的所谓“码农”,尤其对于那些使用 Java、PHP、.NET 等高级语言进行业务开发的程序员而言,更是如此。
acl 网络通信与服务器编程框架是一个开源的 C/C++库,提供了丰富的多种网络服务器编程模型,同时提供了大量的常见网络应用协议,有利于技术人员快速地编写出安全、稳定、高效的服务端程序。
二、常见的几种网络服务器模型
程序员应该去关注一下底层的实现原理,甚至需要去研究其实现细节。有很多著名的开源服务器程序值得我们去研究学习,比如 postfix,nginx,mysql,redis,varnish,squid,ircd,apache 等,通过研究这些开源服务软件,可以使我们懂得真实运行环境中的服务器软件设计法则。下面的表格列出了常见的服务器设计模型:
服务器模型 | 描述 | 优点 | 缺点 | 举例 |
多进程方式 | 一个连接一个进程 | 安全、稳定 | 并发度低 | Postfix、Apache1.3.x |
多线程阻塞方式 | 一个连接一个线程 | 并发度略有提升、资源占用稍低 | 并发底较低 | Mysql、Mongodb、Apache2.0.x |
单线程非阻塞方式 | 单一线程采用事件触发支撑大量连接 | 并发度高、资源占用低 | 编程复杂度高、需多个进程实例才可使用多核 | Nginx、lighttpd、Redis、Squid、ircd |
多线程事件触发方式 | 多个线程采用事件触发支撑大量连接 | 并发度高、资源占用低、有效使用多核、编程复杂度低 | 资源共享需要互斥 | Memcached、Varnish、Apache2.2.x |
UDP无连接方式 | 采用UDP的无连接通信模式 | 并发度高、资源占用低 | 通信可靠性差 | bind |
三、acl 网络通信与服务器编程框架介绍
对 C/C++ 程序员而言,虽然存在着如此众多的开源服务器应用软件,但想要直接应用于自己的业务上是不太可能的,毕竟业务类型是千变万化,私有应用协议也是五花八门。是否存在一些能适应多种业务类型的服务器编程框架呢?答案是肯定的,其中 ACE 就是一个非常著名的开源网络通信与服务器开发框架库,这是由 Douglas C. Schmidt 在做博士论文期间用 C++ 编写的网络通信与服务器开发框架,该框架出现的比较早,应用范围也比较广泛,但是编程复杂度很高,里面充斥着大量的设计模式,有人形容其学术味未免太浓。acl 网络通信与服务器框架是另一个选择,该框架至今也有近十年的历史,最初来源于著名的邮件服务器软件 Postfix,从中借鉴了大量服务器设计思想及代码,后来逐渐演变成一个通用的服务器开发框架。在介绍 acl 服务器框架前,不妨先介绍一下 Postfix 的服务器设计模式以及 acl 服务器框架与 Postfix 的服务器的异同点。
3.1、Postfix 服务器框架的设计特点
1)、父子进程协作:父进程(master)复杂调度及监控服务子进程,服务子进程负责接收处理具体的业务类型
2)、稳定:主控进程(master)监控所有子进程的运行状态,子进程异常行为可控
3)、安全:子进程以普通用户身份运行
4)、资源可控:子进程为半驻留服务方式,可在完成一定任务量或空闲一定时间后主动退出
5)、模块化:每种服务为独立程序,有多个服务器模型根据需要选择
6)、并发度:因为采用进程池方式,每个连接一个进程,所以并发度很低
下图是父进程的流程图:
下图为服务子进程的流程图:
关于更多 Postfix 服务器框架的设计,请参考:协作半驻留式服务器程序开发框架 --- 基于 Postfix 服务器框架改造
3.2、acl 服务器框架与 Postfix 服务器框架的异同
虽然 acl 中的服务器框架设计源于 Postfix,但 acl 的设计目标与 Postfix 并不相同,Postfix 的作者Wietse Venema 在设计 Postfix 之初主要是为了设计一个比 sendmail 更为安全、稳定、扩展性更好的邮件 MTA软件,而 acl 服务器框架的主要目标是希望该框架能够适应更多的应用业务场景,下表是二者一些主要异同点:
功能点 | Postfix master | acl_master |
半驻留服务模式 | 支持 | 支持 |
安全控制 | 严格的用户权限控制 | 严格的用户权限控制 |
配置方式 | 所有服务配置在同一个配置文件中 | 一个服务一个配置文件 |
进程池模式 | 支持 | 支持 |
触发器模式 | 支持 | 支持 |
非阻塞模式 | 功能一般 | 功能强大 |
线程池模式 | 不支持 | 支持 |
在线升级 | 支持 | 支持 |
预启动 | 不支持 | 支持 |
最小进程数控制 | 不支持 | 支持 |
最大进程数控制 | 支持 | 支持 |
监控子进程报警机制 | 不支持 | 支持 |
开发过程调试功能 | 不太方便 | 方便(很容易使用 valgrind 检查) |
客户端连接访问控制 | 应用自己保证 | 框架自动支持 |
单一进程监听多个地址 | 受限 | 支持 |
单一进程同时监听TCP及域套接口 | 不支持 | 支持 |
子进程运行身份控制 | 支持 | 支持 |
日志记录方式 | 支持 syslog | 支持syslog-ng;允许用户注册自己的日志处理过程;允许同时写入多个目标日志对象中 |
子进程崩溃是否允许产生 core 文件 | ? | 通过配置项控制,便于快速消除错误 |
是否支持UDP通信模式 | 不支持 | 支持 |
是否支持多进程TCP连接均匀化 | 不支持 | 支持 |
以上为 Postfix 的 master 服务器模块与 acl 中的 acl_master 服务器模块的主要区别,当然这个对比并不是说明 acl 的 acl_master 服务器模块优于 Postfix 的 master(毕竟 acl 的服务器模块是来源于 Postfix),而是为了说明 acl 的 acl_master 服务模块可能更方便技术人员开发自己的服务应用。
四、acl 服务器编程框架设计要点
从上面的表格可以看出,设计一个高效实用的服务器框架需要考虑的层面还是不少,下面从几个角度列出了 acl 网络通信与服务器开发框架的设计要点。
1、网络通信功能的重要作用
在网络服务器架构设计中,网络通信作为基础模块是不可或缺的,在 acl 库中有丰富的网络通信功能模块,虽然该模块是对底层系统 API 的封装,但却提供了丰富的高级功能,同时屏蔽了在使用底层系统 API 容易出错的地方,因而可以方便程序员快速地开发出高效、稳定、安全的网络通信应用。在将系统 IO API 封装成流时,其中一个重要的作法就是数据缓存,数据缓存可以降低对系统 API 的调用次数(这可以减少系统的上下文切换,从而减少系统 CPU 负载),acl 库的网络流的设计也存在着数据缓存层,可以支持网络流和文件流,同时提供了丰富的读操作接口:读指定字节长度数据,按行读数据(可以兼容 \r\n 及 \n 两种情况),以及其它大量的读操作函数。下图分别是阻塞 IO 和非阻塞 IO 的类继承关系:
阻塞 IO 继承关系图
非阻塞 IO 类继承关系图
acl 库中的网络通信模块除了大量的 IO 读写接口外,还有域名解析、网络监听、网络连接等接口,基本上涵盖了常见的网络操作;此外,acl 中的网络模块支持阻塞网络 IO 以及非阻塞 IO 两种 IO 模型,其中非阻塞 IO 又支持 reactor 和 proactor 两种非阻塞 IO 模型;acl 网络模块本身并不支持 SSL/TLS 功能(这毕竟是另一个重要领域),但却对外提供了 IO 操作注册接口,目前通过封装著名的嵌入式 SSL/TLS 库(polarssl,据说最近因并入 arm 而改名了)而具备了 SSL/TLS 的通信能力(阻塞及非阻塞 IO 均已支持 SSL/TLS 通信功能)。
2、IO 事件引擎的关键作用
一般来讲,目前常见的网络服务器内部都会封装系统的 IO 事件引擎(如:select/poll/epool/kquque/devpoll/iocp/win32 message),以此作为网络 IO 的消息驱动引擎,acl 库内部也封装了这些 IO 事件引擎,为了适应不同的网络服务框架模型,acl 库封装的 IO 事件引擎分为单线程事件引擎以及多线程事件引擎(目前 iocp/win32 message 除外)。其中单线程 IO 事件引擎主要用在高并发非阻塞网络服务模型中,而多线程 IO 事件引擎则用在多线程服务器模型中。
在 acl 库中封装的事件模型中 select 是一个通用的事件引擎(可以支持WIN32/LINUX/UNIX);epoll 是 LINUX 下内核级的高效事件引擎(尤其是在高并发环境下存在大量空闲连接时性能尤佳);iocp 是 WIN32 下的高效事件引擎,acl 中的封装与互联网上大多数使用方式不同,在 acl 中采用了单线程封装方式;win32 message 是 acl 库中专门针对基于 win32 界面消息而封装的事件 IO 引擎。
3、线程池设计中的注意要点
多线程服务器模型也许是很多公司使用最多的服务器模型,因为此服务器型的开发效率较高,容易实现一些复杂的业务逻辑(例如,现在多数数据库驱动也是阻塞的,为了与之结合,应用服务器程序只能采用阻塞模型)。为了提高任务执行效率,设计一个高效的线程池是非常有必要的,网上一些经典的线程池设计方式大同小异,基本都是通过组合使用线程锁(pthread_mutex_lock/unlock)与线程条件变量(pthread_cond_signal) 等系统 API 实现任务入队、出队的过程,这些设计中基本都是一个线程池共享一把线程锁和一个线程条件变量,在添加任务时先加锁,然后解锁并通知线程条件变量来唤醒一个或几个工作线程,这些工作线程在加锁后从任务队列中取出任务后立即解锁,然后开始执行取得的任务。这种线程池设计模型看起来并没有什么问题,但在线程数较多(过百)且任务通知非常频繁时却存在着 CPU 占用较多的问题,即所谓线程池惊群现象。出现此类问题的原因主要在线程条件变量通知的系统 API (pthread_cond_signal) 上,通过查看该 API 的在线帮助,可以看到这么一段话:pthread_cond_signal 将会唤醒一个或者多个等待在线程条件变量上的线程,也正是这其中的”多个“关键词造成了高压力下线程池使用中出现的惊群现象。
那该如何避免线程池设计中的惊群现象呢?在 acl 的线程池设计是这样的:在仍然共用一个线程互斥锁的条件下,给每一个消费者线程分配一个独立的线程条件变量和一个独立的任务队列,生产者线程在添加任务时,找到空闲的消费者线程,将任务置入该消费者的任务队列中同时只通知 (pthread_cond_signal) 该消费者的线程条件变量,消费者线程与生产者线程虽然共用相同的线程互斥锁(因为有全局资源及调用 pthread_cond_wait 所需),但线程条件变量的通知过程却是定向通知的,未被通知的消费者线程不会被唤醒,这样惊群现象也就不会产生了。
4、通过 IO 事件引擎将网络连接池与线程池隔离
通常的多线程服务器设计是这样的:给每一个网络连接分配一个独立的线程,连接不关闭,则该线程一直被该连接所占用。这样设计的好处是实现该服务模型非常简单,但缺点也是显而易见的,那就是:实际应用中,客户端为了提高网络传输效率,大量采用连接池方式,每次处理任务时从连接池取得一个空闲连接与服务端进行通信,获得服务端的处理结果后再将该连接放回空闲连接池中,此时服务端却被这个空闲连接占用着,这样就造成了此类服务器程序并发度较低的问题。
而在 acl 多线程服务器模型中网络连接池与线程池是通过 IO 事件引擎隔离的,如何理解”隔离“二字?首先得需要理解 acl 多线程服务器模型的工作机制:
服务端接收到客户端连接 ---> 将该连接置入 IO 事件引擎中,等待该连接可读或出错 ---> IO 事件引擎中的某个连接有数据可读时 ---> 该连接被交给线程池中的一个空闲线程去处理 IO 过程 ---> 线程处理完本次 IO 过程,则重将该连接归还给 IO 事件引擎 ---> 该工作线程也重新被置为空闲状态归还给线程池。
通过 IO 事件引擎就做到了当客户端连接有数据可读时其与线程池中的某个空闲线程绑定,当该连接空闲时便与该线程解绑。acl 中的这种多线程服务设计模型适用了真实生产环境大多数的应用场景,做到了仅需创建几十至几百个线程便可与成千上万个客户端保持长连接。
5、内存管理应如何设计
在多线程运行环境中,内存的频繁动态分配及释放往往会影响整体运行性能,原因是程序在在堆上动态分配与释放内存时,需要不断地使用线程锁进行互斥,所以当线程数非常多时,如果每个线程都有大量的内存分配/释放操作,则锁竞争非常严重,象 malloc/free 标准 C 函数内部的线程锁往往使用自旋锁,所以会发现进程的 CPU 占用非常高(在 RHL6/Centos6 上可以使用 perf top -p pid 监控进程运行状态,发会 spin_lock 调用频率非常高,这也说明了多线程进行内存分配时的竞争是非常严重的)。
如果降低多线程环境内存动态管理时的锁竞争呢?一般有两种方式,其一:使用建立在线程局部变量上的内存池,其二:使用会话内存管理策略。
所谓”建立在线程局部变量上的内存池“,其主要思想是使用每个线程上的线程局部变量给其分配一个内存池,这样当线程需要分配/释放内存时只需引用自己的线程局部内存池即可,不会发生与其它线程产生内存分配的冲突问题;但对于这样一个应用场景:内存在一个线程中分配而在另一个线程释放时,这种分配机制就不会有效减少锁冲突,尤其是线程局部内存池还进行了内存分片时锁冲突问题就会更为严重,因为当某个线程获得了其它线程分配的内存后需要释放时,并不能立即释放,而是要先归还给该内存片的”属主“线程,由”属主“线程负责释放。因此,这种分配机制主要用在内存的跨线程操作相对不”频繁“的应用场景中。在 acl 库中也提供了此类内存管理模块,参见 acl_slice.h 头文件的函数说明。当然,大家比较熟悉应该是 google 开源的 tcmalloc 库。
而何为”使用会话内存管理策略“呢?其主要方式是:在一个任务会话开始时创建一个内存分配器(其管理着一个内存池),在下面的所有操作步骤中都将该分配器传递,在所有处理过程中的内存分配在该分配器上进行,当该任务会话结束时释放内存分配器,从而统一释放了在该内存分配器上的内存池。这样做的好处很明显,就是大大降低了 malloc/free 的次数。缺点也是很明显的,就是在每的个操作过程都得“带”着这个内存分配器。使用此方式的经典的例子就是 apache;当然在 acl 库也存在类似的一个简单的内存分配器(参见 acl_dbuf_pool.h ),在 acl 的 redis 客户端库中大量使用了该内存分配器,从而使之在多线程环境依赖具有很高的性能。
6、更好地使用多进程实例
acl 中的服务器框架有一个是多线程服务器模型,但其仍然可以被启动多个进程实例,每个进程实例内采用线程池方式,大家也许会问:既然多线程已经可以使用多核且性能也不错,那为何还要启动多个进程实例呢?好处是什么?当然,只启动一个进程是可以有效地使用多核的,只所以要用启动多个进程实例,原因主要是两个:
第一:安全稳定性,多进程具备更好的安全隔离机制,当一个进程因为某种原因”意外“停止响应而崩溃了,其它进程还能继续对外提供服务,尽量保证业务不中断;
第二:还是内存管理的高效性,虽然使用了一些高效的内存管理库(如:tcmalloc),但线程锁的竞争依然存在,尤其是当线程数增大时。而使用多进程方式,则可以大大降低这种锁冲突,有时甚至不再需要诸如 tcmalloc 之类的内存管理器(当每个进程内线程数并不太多时)。例如:希望某个服务最多启动 512个线程,如果启动 8 个进程内则每个进程最大只需启动 64 个线程即可,在这种情况下即使用 malloc/free 标准 API,内存的锁冲突仍然是很低的。
当然,采用多进程方式也存在一个问题,就是客户端连接分配的不均匀,有的子进程得到的客户端连接多,有的得到少,因为操作系统并不能保证这种分配的均匀性。采用多进程的一些服务(如 nginx)有时会采用一种进程间锁的方式来保证各个服务子进程得到客户端连接数均衡,但在 acl 的服务器框架中采用了另外一种方式:提供了一个连接分配器子进程,应用服务子进程与这个分配器之间建立了 UNIX 域套接口,所有前端客户端在 TCP 握手时首先连接该分配器,分配器会根据应用服务的各个子进程的负载情况将获得的 TCP 连接通过 UNIX 域套接口传递给后端的服务子进程,这样就保证了各个服务子进程获得的客户端连接是均匀的,可参考:使用 acl 服务器框架编写负载均衡的应用服务。目前,该分配器还定期汇总各个服务子进行的运行状态,这样,我们就可以写一些前端 WEB 程序,查询各台机器上的分配器来查看所有机器上的客户端连接及负载状态。
7、安全稳定性原则
作为一个需要长时间运行的服务器程序,安全稳定性是至关重要的。
在安全性方面,acl 的服务器框架在启动服务子进程后会首先修改子进程的运行身份,将其降为普通用户身份,同时限制该子进程的运行目录,这样即使因程序存在一些 BUG 而被黑客攻破,其获得的身份也只能拥有最低的普通用户权限;
为了保证稳定性,acl 的服务器模型支持服务子进程服务次数退出机制,即当一个子进程处理的客户端连接数达到配制文件中设定的值后会自动退出(在处理完所有的连接后),服务框架会自动启动新的子进程处理新到的连接,这样做的好处是:对于一个新上线的服务程序,有可能存在一些轻微的内存泄露,通过此自动退出与自动启动机制,就可以有效地减少这种内存泄露所带来的危害;另外,如果服务子进程异常退出,acl 的服务主进程会将该子进程退出的消息通知一个报警子进程,由报警子进程以邮件或短信方式通知技术人员进行处理。
8、模块化原则
使用 acl 服务器框架编写服务器程序,建议将不同功能的功能模块写成独立的应用服务程序,由主控进程(acl_master)统一进行管理,这样既便于各个功能模块的分布式部署以及将来进行各自的功能扩展,同时还将不同的功能模块进行有效隔离,避免产生过多的耦合性问题。
9、配置管理性要求
在 acl 的服务器框架设计中,有一个主控制进程(acl_master),这个主控制扫描应用服务配置目录下的配置文件,启动多个服务子进程,这样,每个应用服务程序有一个自己的配置文件,配置项中有:监听端口、进程数、线程数、运行身份、日志输出、访问控制 等等;另外,acl 服务器框架还支持软件在线升级,可以做到不中断当前业务的前提下更新服务器程序。
10、快速开发部署原则
为了方便技术员快速入门,acl 库中还提供了服务器程序生成向导,只需几步便可以搭建一个基于 acl 的服务器编程框架。参考: 使用 acl 生成向导快速创建服务器程序;同时 acl 中还提供了用于快速安装部署的脚本程序,方便实施人员一键式安装部署 acl 服务器应用程序
11、大量实用功能库
在 acl 网络通信与服务器框架库中,不仅提供了一套完整的服务器框架,而且还提供了大量的常见应用库:比如常见的编码库(XML/JSON/HEX/URL CODE/MIME/BASE64/UUCODE/QPCODE/RFC2047/RFC822 等),常见的网络协议库(http、smtp、icmp、redis、memcache、beanstalk、mysql、handler socket 等),常见的数据结构算法(哈希表、动态数组、先进先出队列、二叉树、二分块查找、平衡二叉树、256叉匹配树等)。正所谓独木不成林,结合这些常见应用库以及常见的开源服务器软件,技术人员就可以非常快速地开发出服务应用程序。下图列出了 acl 中 lib_acl_cpp 库中包含的绝大部分功能类索引:
五、参考资源
acl 库下载:http://sourceforge.net/projects/acl/
acl github: https://github.com/acl-dev/acl