澳门新葡萄京所有网站 1

本文作者: 伯乐在线 –
郑树新
。未经作者许可,禁止转载!
欢迎加入伯乐在线 专栏作者。

一、概述

软件技术发展至今,存在着很多成熟的开发框架(如广大 Java 程序员所熟知的
SSH
框架),这些开发框架或面向数据库,或面向网络通信,或面向应用服务器,或面向界面设计,甚至面向某类业务模型。这些开框架的存在,大大提高了程序员的开发效率,这样使技术人员将精力更多地集中于业务本身,而不必拘泥于技术的底层实现细节,但也造成了众多知其然不知其所以然的所谓“码农”,尤其对于那些使
用 Java、PHP、.NET 等高级语言进行业务开发的程序员而言,更是如此。

acl 网络通信与服务器编程框架是一个开源的
C/C++库,提供了丰富的多种网络服务器编程模型,同时提供了大量的常见网络应用协议,有利于技术人员快速地编写出安全、稳定、高效的服务端程序。

polarphp 项目介绍

polarphp是一个全新的PHP语言的运行时环境,基于目前最新的zend virtual machine进行打造,支持最新的语言规范,同时提供了自己的运行时标准库
(libpdk)。

简单来说polarphp之于PHP语言的关系跟NodeJS之于Javascript语言一样,NodeJSv8引擎基础之上进行打造,为Javascript提供了一个在服务端运行的环境。同样polarphp也在zend engine的基础上进行打造,实现了一个除Web开发之外的一个全新的运行环境。

项目官网库:

欢迎小伙伴们多多star ^ _ ^

一、概述

二、常见的几种网络服务器模型

程序员应该去关注一下底层的实现原理,甚至需要去研究其实现细节。有很多著名的开源服务器程序值得我们去研究学习,比如
postfix,nginx,mysql,redis,varnish,squid,ircd,apache
等,通过研究这些开源服务软件,可以使我们懂得真实运行环境中的服务器软件设计法则。下面的表格列出了常见的服务器设计模型:

服务器模型 描述 优点缺点 举例

澳门新葡萄京所有网站 2推荐C/C++编程交流学习群:941636044,群内更多学习资料!

以上表格将一些著名的开源服务器软件进行了归类,同时对比了不同的服务器编程模型的优缺点,究竟该采用何种服务器模型,则需根据实际应用场景进行选择。如
果你非常注重系统安全稳定性但并发度要求不高时则可以选择“多进程方式”;如果你的应用服务要求支持高并发,同时要求非常低的资源消耗可以选择“单线程非
阻塞方式”(当然选择这种方式得需要注意编程的复杂度,毕竟多数情况下,我们的实际应用并不需要象
nginx,redis
那样的高性能、高并发);如果你想要支持一定的高并发,但又不想要非常高的编程复杂度,则“多线程事件触发方式”就是你的选择了(本人在实践中的项目大多
采用此类模型)。

为什么发起 polarphp 项目

随着GoNodeJS的强势崛起,PHP的市场份额逐渐被蚕食,而PHP官方仍然坚守在Web编程领域,有些东西越是想守住就越守不住。polarphp借鉴NodeJSGo的相关特性对zendVM重新封装,去掉PHP一些古老弃用的特性和强Web属性,通过实现一套新的运行时框架libpdk,将PHP语言打造成为一门真正的通用性脚本语言,赋能PHP,让其拥有异步编程,协程,线程,内置的unicode支持,标准的文件IO等等特性,让PHP程序员不仅仅能做web应用,也能从容面对真正的服务端应用。

软件技术发展至今,存在着很多成熟的开发框架(如广大 Java 程序员所熟知的
SSH
框架),这些开发框架或面向数据库,或面向网络通信,或面向应用服务器,或面向界面设计,甚至面向某类业务模型。这些开框架的存在,大大提高了程序员的开发效率,这样使技术人员将精力更多地集中于业务本身,而不必拘泥于技术的底层实现细节,但也造成了众多知其然不知其所以然的所谓“码农”,尤其对于那些使
用 Java、PHP、.NET 等高级语言进行业务开发的程序员而言,更是如此。

三、acl 网络通信与服务器编程框架介绍

对 C/C++
程序员而言,虽然存在着如此众多的开源服务器应用软件,但想要直接应用于自己的业务上是不太可能的,毕竟业务类型是千变万化,私有应用协议也是五花八门。
是否存在一些能适应多种业务类型的服务器编程框架呢?答案是肯定的,其中 ACE
就是一个非常著名的开源网络通信与服务器开发框架库,这是由 Douglas C.
Schmidt 在做博士论文期间用 C++
编写的网络通信与服务器开发框架,该框架出现的比较早,应用范围也比较广泛,但是编程复杂度很高,里面充斥着大量的设计模式,有人形容其学术味未免太浓。
acl
网络通信与服务器框架是另一个选择,该框架至今也有近十年的历史,最初来源于著名的邮件服务器软件
Postfix,从中借鉴了大量服务器设计思想及代码,后来逐渐演变成一个通用的服务器开发框架。在介绍
acl 服务器框架前,不妨先介绍一下 Postfix 的服务器设计模式以及 acl
服务器框架与 Postfix 的服务器的异同点。

1)、父子进程协作:父进程复杂调度及监控服务子进程,服务子进程负责接收处理具体的业务类型

2)、稳定:主控进程监控所有子进程的运行状态,子进程异常行为可控

3)、安全:子进程以普通用户身份运行

4)、资源可控:子进程为半驻留服务方式,可在完成一定任务量或空闲一定时间后主动退出

5)、模块化:每种服务为独立程序,有多个服务器模型根据需要选择

6)、并发度:因为采用进程池方式,每个连接一个进程,所以并发度很低

下图是父进程的流程图:

澳门新葡萄京所有网站 3

下图为服务子进程的流程图:

澳门新葡萄京所有网站 4

虽然 acl 中的服务器框架设计源于 Postfix,但 acl 的设计目标与 Postfix
并不相同,Postfix 的作者Wietse Venema 在设计 Postfix
之初主要是为了设计一个比 sendmail 更为安全、稳定、扩展性更好的邮件
MTA软件,而 acl
服务器框架的主要目标是希望该框架能够适应更多的应用业务场景,下表是二者一些主要异同点:

功能点Postfix masteracl_master

半驻留服务模式支持支持

安全控制严格的用户权限控制严格的用户权限控制

配置方式所有服务配置在同一个配置文件中一个服务一个配置文件

进程池模式支持支持

触发器模式支持支持

非阻塞模式功能一般功能强大

线程池模式不支持支持

在线升级澳门新葡萄京所有网站,支持支持

预启动不支持支持

最小进程数控制不支持支持

最大进程数控制支持支持

监控子进程报警机制不支持支持

开发过程调试功能不太方便方便(很容易使用 valgrind 检查)

客户端连接访问控制应用自己保证框架自动支持

单一进程监听多个地址 受限 支持

单一进程同时监听TCP及域套接口不支持支持

子进程运行身份控制支持支持

日志记录方式支持
syslog支持syslog-ng;允许用户注册自己的日志处理过程;允许同时写入多个目标日志对象中

子进程崩溃是否允许产生 core 文件?通过配置项控制,便于快速消除错误

是否支持UDP通信模式不支持支持

是否支持多进程TCP连接均匀化不支持支持

以上为 Postfix 的 master 服务器模块与 acl 中的 acl_master
服务器模块的主要区别,当然这个对比并不是说明 acl 的 acl_master
服务器模块优于 Postfix 的 master(毕竟 acl 的服务器模块是来源于
Postfix),而是为了说明 acl 的 acl_master
服务模块可能更方便技术人员开发自己的服务应用。

polarphp 提供的基础设施

  1. 直接面向终端,去掉SAPI从而更好的实现服务端环境。

  2. 规范化OPCODE形成规范,从而提供一种类似pyc文件的预编译机制。

  3. 提供原生多线程支持,借鉴Java在多线程方面的编程范式。

  4. 提供原生异步IO支持。

  5. 提供针对字符串的unicode支持。

  6. 提供一种全新的包组织方式,内置包依赖管理工具,类似Cargonpm

  7. 提供内置的API文档生成工具。

acl 网络通信与服务器编程框架是一个开源的
C/C++库,提供了丰富的多种网络服务器编程模型,同时提供了大量的常见网络应用协议,有利于技术人员快速地编写出安全、稳定、高效的服务端程序。

四、acl 服务器编程框架设计要点

从上面的表格可以看出,设计一个高效实用的服务器框架需要考虑的层面还是不少,下面从几个角度列出了
acl 网络通信与服务器开发框架的设计要点。

在网络服务器架构设计中,网络通信作为基础模块是不可或缺的,在 acl
库中有丰富的网络通信功能模块,虽然该模块是对底层系统 API
的封装,但却提供了丰富的高级功能,同时屏蔽了在使用底层系统 API
容易出错的地方,因而可以方便程序员快速地开发出高效、稳定、安全的网络通信应用。在将系统
IO API
封装成流时,其中一个重要的作法就是数据缓存,数据缓存可以降低对系统 API
的调用次数(这可以减少系统的上下文切换,从而减少系统 CPU 负载),acl
库的网络流的设计也存在着数据缓存层,可以支持网络流和文件流,同时提供了丰富的读操作接口:读指定字节长度数据,按行读数据(可以兼容
rn 及 n 两种情况),以及其它大量的读操作函数。下图分别是阻塞 IO
和非阻塞 IO 的类继承关系:

澳门新葡萄京所有网站 5阻塞
IO
继承关系图澳门新葡萄京所有网站 6非阻塞IO继承关系图

acl 库中的网络通信模块除了大量的 IO
读写接口外,还有域名解析、网络监听、网络连接等接口,基本上涵盖了常见的网络操作;此外,acl
中的网络模块支持阻塞网络 IO 以及非阻塞 IO 两种 IO 模型,其中非阻塞 IO
又支持 reactor 和 proactor 两种非阻塞 IO 模型;acl 网络模块本身并不支持
SSL/TLS 功能(这毕竟是另一个重要领域),但却对外提供了 IO
操作注册接口,目前通过封装著名的嵌入式 SSL/TLS
库(polarssl,据说最近因并入 arm 而改名了)而具备了 SSL/TLS
的通信能力(阻塞及非阻塞 IO 均已支持 SSL/TLS 通信功能)。

一般来讲,目前常见的网络服务器内部都会封装系统的 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 引擎。

多线程服务器模型也许是很多公司使用最多的服务器模型,因为此服务器型的开发效率较高,容易实现一些复杂的业务逻辑(例如,现在多数数据库驱动也是阻塞
的,为了与之结合,应用服务器程序只能采用阻塞模型)。为了提高任务执行效率,设计一个高效的线程池是非常有必要的,网上一些经典的线程池设计方式大同小
异,基本都是通过组合使用线程锁(pthread_mutex_lock/unlock)与线程条件变量(pthread_cond_signal)
等系统 API
实现任务入队、出队的过程,这些设计中基本都是一个线程池共享一把线程锁和一个线程条件变量,在添加任务时先加锁,然后解锁并通知线程条件变量来唤醒一个
或几个工作线程,这些工作线程在加锁后从任务队列中取出任务后立即解锁,然后开始执行取得的任务。这种线程池设计模型看起来并没有什么问题,但在线程数较
多且任务通知非常频繁时却存在着 CPU
占用较多的问题,即所谓线程池惊群现象。出现此类问题的原因主要在线程条件变量通知的系统
API (pthread_cond_signal) 上,通过查看该 API
的在线帮助,可以看到这么一段话:pthread_cond_signal
将会唤醒一个或者多个等待在线程条件变量上的线程,也正是这其中的”多个“关键词造成了高压力下线程池使用中出现的惊群现象。

那该如何避免线程池设计中的惊群现象呢?在 acl
的线程池设计是这样的:在仍然共用一个线程互斥锁的条件下,给每一个消费者线程分配一个独立的线程条件变量和一个独立的任务队列,生产者线程在添加任务
时,找到空闲的消费者线程,将任务置入该消费者的任务队列中同时只通知
(pthread_cond_signal)
该消费者的线程条件变量,消费者线程与生产者线程虽然共用相同的线程互斥锁(因为有全局资源及调用
pthread_cond_wait
所需),但线程条件变量的通知过程却是定向通知的,未被通知的消费者线程不会被唤醒,这样惊群现象也就不会产生了。

polarphp 大致架构

项目主要由三部分构成,主要有如下三个子模块

  1. polarvm

  2. zendAPI

  3. libpdk

这个模块大致的关系如下:

polarvm <=> zendAPI <=> libpdk

二、常见的几种网络服务器模型

4、通过 IO 事件引擎将网络连接池与线程池隔离

通常的多线程服务器设计是这样的:给每一个网络连接分配一个独立的线程,连接不关闭,则该线程一直被该连接所占用。这样设计的好处是实现该服务模型非常简
单,但缺点也是显而易见的,那就是:实际应用中,客户端为了提高网络传输效率,大量采用连接池方式,每次处理任务时从连接池取得一个空闲连接与服务端进行
通信,获得服务端的处理结果后再将该连接放回空闲连接池中,此时服务端却被这个空闲连接占用着,这样就造成了此类服务器程序并发度较低的问题。

而在 acl 多线程服务器模型中网络连接池与线程池是通过 IO
事件引擎隔离的,如何理解”隔离“二字?首先得需要理解 acl
多线程服务器模型的工作机制:

服务端接收到客户端连接 —> 将该连接置入 IO
事件引擎中,等待该连接可读或出错 —> IO
事件引擎中的某个连接有数据可读时 —>
该连接被交给线程池中的一个空闲线程去处理 IO 过程 —> 线程处理完本次 IO
过程,则重将该连接归还给 IO 事件引擎 —>
该工作线程也重新被置为空闲状态归还给线程池。

通过 IO
事件引擎就做到了当客户端连接有数据可读时其与线程池中的某个空闲线程绑定,当该连接空闲时便与该线程解绑。acl
中的这种多线程服务设计模型适用了真实生产环境大多数的应用场景,做到了仅需创建几十至几百个线程便可与成千上万个客户端保持长连接。

polarvm 介绍

现阶段实现对zend engine的封装,实现最基本的PHP执行环境,比如实现:

  1. 语言解析,OPCODE的执行。

  2. 实现基础运行环境,实现变量操作,数组操作,类加载机制,语言反射等等。

  3. zend engine的初始化,实现语言引擎与终端的链接,实现语言引擎对标准输入输出的直接控制。

  4. 实现语言引擎与标准库之间的回调机制。

程序员应该去关注一下底层的实现原理,甚至需要去研究其实现细节。有很多著名的开源服务器程序值得我们去研究学习,比如
postfix,nginx,mysql,redis,varnish,squid,ircd,apache
等,通过研究这些开源服务软件,可以使我们懂得真实运行环境中的服务器软件设计法则。下面的表格列出了常见的服务器设计模型:

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
客户端库中大量使用了该内存分配器,从而使之在多线程环境依赖具有很高的性能。

zendAPI 介绍

做过PHP扩展的朋友应该知道,在我们开发扩展的时候,zend engine的很多接口都是通过宏调用的方式提供的,类型不安全,出错了不好调试,而且有些宏还长的特别像,同时操作数组的时候特别繁琐。zend enginegc是通过引用计数实现的,同时C语言又没有什么从语言层面帮我们管理计数的机制,从而我们在写扩展的时候管理内存不仅很繁琐而且一不小心就会造成内存泄露。特别是将写时复制和PHP变量之间的引用一起使用的时候,非常让能头痛。

如果我们的标准库如果直接基于原生的zend engine的接口,势必扩展性,可维护性会受到严重影响,特别是目前polarvm是基于zend engine二次开发的可观情况下。所以在语言引擎和标准库之间实现一个屏蔽层,对下实现对zend engine原生接口的封装,对上提供一套相对稳定且简单的面向对象的CPP编程接口。

 服务器模型  描述  优点 缺点  举例
 多进程方式  一个连接一个进程  安全、稳定  并发度低  Postfix、Apache1.3.x
 多线程阻塞方式  一个连接一个线程  并发度略有提升、资源占用稍低  并发底较低  Mysql、Mongodb、Apache2.0.x
 单线程非阻塞方式  单一线程采用事件触发支撑大量连接  并发度高、资源占用低  编程复杂度高、需多个进程实例才可使用多核  Nginx、lighttpd、Redis、Squid、ircd
 多线程事件触发方式  多个线程采用事件触发支撑大量连接  并发度高、资源占用低、有效使用多核、编程复杂度低  资源共享需要互斥  Memcached、Varnish、Apache2.2.x
 UDP无连接方式  采用UDP的无连接通信模式  并发度高、资源占用低  通信可靠性差  bind

6、更好地使用多进程实例

acl
中的服务器框架有一个是多线程服务器模型,但其仍然可以被启动多个进程实例,每个进程实例内采用线程池方式,大家也许会问:既然多线程已经可以使用多核且
性能也不错,那为何还要启动多个进程实例呢?好处是什么?当然,只启动一个进程是可以有效地使用多核的,只所以要用启动多个进程实例,原因主要是两个:

第一:安全稳定性,多进程具备更好的安全隔离机制,当一个进程因为某种原因”意外“停止响应而崩溃了,其它进程还能继续对外提供服务,尽量保证业务不中断;

第二:还是内存管理的高效性,虽然使用了一些高效的内存管理库(如:tcmalloc),但线程锁的竞争依然存在,尤其是当线程数增大时。而使用多进程方
式,则可以大大降低这种锁冲突,有时甚至不再需要诸如 tcmalloc
之类的内存管理器(当每个进程内线程数并不太多时)。例如:希望某个服务最多启动
512个线程,如果启动 8 个进程内则每个进程最大只需启动 64
个线程即可,在这种情况下即使用 malloc/free 标准
API,内存的锁冲突仍然是很低的。

当然,采用多进程方式也存在一个问题,就是客户端连接分配的不均匀,有的子进程得到的客户端连接多,有的得到少,因为操作系统并不能保证这种分配的均匀
性。采用多进程的一些服务有时会采用一种进程间锁的方式来保证各个服务子进程得到客户端连接数均衡,但在
acl
的服务器框架中采用了另外一种方式:提供了一个连接分配器子进程,应用服务子进程与这个分配器之间建立了
UNIX 域套接口,所有前端客户端在 TCP
握手时首先连接该分配器,分配器会根据应用服务的各个子进程的负载情况将获得的
TCP 连接通过 UNIX
域套接口传递给后端的服务子进程,这样就保证了各个服务子进程获得的客户端连接是均匀的。目前,该分配器还定期汇总各个服务子进行的运行状态,这样,我们就可以写一些前端
WEB 程序,查询各台机器上的分配器来查看所有机器上的客户端连接及负载状态。

zendAPI 提供如下的特性:

  1. 完全面向对象,对Zend Engine API进行二次定义

  2. 使用现代的C++11语法进行开发,便于维护

  3. 最大化屏蔽PHP版本对扩展开发的影响,zendAPI将对Zend Engine API不同版本带来的差异屏蔽掉

  4. 高覆盖的单元测试,保证代码质量

  5. 在封装的时候,尽最大能力保证性能

  6. 致力于项目库的二进制兼容

以上表格将一些著名的开源服务器软件进行了归类,同时对比了不同的服务器编程模型的优缺点,究竟该采用何种服务器模型,则需根据实际应用场景进行选择。如
果你非常注重系统安全稳定性但并发度要求不高时则可以选择“多进程方式”;如果你的应用服务要求支持高并发,同时要求非常低的资源消耗可以选择“单线程非
阻塞方式”(当然选择这种方式得需要注意编程的复杂度,毕竟多数情况下,我们的实际应用并不需要象
nginx,redis
那样的高性能、高并发);如果你想要支持一定的高并发,但又不想要非常高的编程复杂度,则“多线程事件触发方式”就是你的选择了(本人在实践中的项目大多
采用此类模型)。

7、安全稳定性原则

作为一个需要长时间运行的服务器程序,安全稳定性是至关重要的。

在安全性方面,acl
的服务器框架在启动服务子进程后会首先修改子进程的运行身份,将其降为普通用户身份,同时限制该子进程的运行目录,这样即使因程序存在一些
BUG 而被黑客攻破,其获得的身份也只能拥有最低的普通用户权限;

为了保证稳定性,acl
的服务器模型支持服务子进程服务次数退出机制,即当一个子进程处理的客户端连接数达到配制文件中设定的值后会自动退出(在处理完所有的连接后),服务框架
会自动启动新的子进程处理新到的连接,这样做的好处是:对于一个新上线的服务程序,有可能存在一些轻微的内存泄露,通过此自动退出与自动启动机制,就可以
有效地减少这种内存泄露所带来的危害;另外,如果服务子进程异常退出,acl
的服务主进程会将该子进程退出的消息通知一个报警子进程,由报警子进程以邮件或短信方式通知技术人员进行处理。

libpdk 介绍

libpdk 的定位是polarphp语言环境中的标准库,PDKPHP Development Kit几个单词的缩写。在设计上参考JavaJDK的模块组织风格,为PHP提供一套严谨并且功能强大的运行时标准库,让实现服务端高效编程成为可能,比如使用PHP实现类似Netty那样的事件驱动的网络框架,或者CoreDNS那样的应用项目成为可能。同时也可以让开发终端程序比如npmCargoPM2等等类似的程序更加便捷。在Web领域,libpdkpolarphp能够脱离SAPI直接像go那样自己对端口进行监听,从而实现gin那样的轻量级的服务框架更加方便,底层基于事件循环模型和多线程模型。

项目库地址: 

PDK计划了如下几个模块

  • Base module
    (基础模块,实现最基本的功能,比如输入输出,文件系统,进程与线程,事件模型等等)

  • Network
    module(网络模块,在基础模块之上,实现一套高性能的网络框架,让编写服务端系统更加便捷)

  • Web module
    (Web模块,实现常见的Http协议,提供一个类型SerletWeb运行时容器)

  • GUI module
    (用户界面模块,未来实现,让PHP具备编写常见的客户端系统,基于openGL实现)

三、acl 网络通信与服务器编程框架介绍

8、模块化原则

使用 acl
服务器框架编写服务器程序,建议将不同功能的功能模块写成独立的应用服务程序,由主控进程(acl_master)统一进行管理,这样既便于各个功能模块
的分布式部署以及将来进行各自的功能扩展,同时还将不同的功能模块进行有效隔离,避免产生过多的耦合性问题。

polarphp 的开发计划

因为开发资源有限,开发计划暂定如下:

  1. 使用cmakezend VM进行编译,生成polarphp定制版的PHP语言虚拟机。

  2. 语言支持项目,语言测试框架,移植LLVM项目的lit测试框架。

  3. 实现polarphp驱动程序,实现从命令行执行PHP代码。

  4. polarphp虚拟机进行回归测试,暂定跑通PHP的语言虚拟机相关回归测试。

  5. 实现polarphp的内置函数。

  6. 发布核心虚拟机的docker镜像。

  7. 整合libpdk运行时框架。

  8. 实现人性化安装,尽量以最少的步骤进行polarphp的安装。

  9. 实现包管理器。

  10. 实现语言配套小工具,比如文档生成工具等等。

对 C/C++
程序员而言,虽然存在着如此众多的开源服务器应用软件,但想要直接应用于自己的业务上是不太可能的,毕竟业务类型是千变万化,私有应用协议也是五花八门。
是否存在一些能适应多种业务类型的服务器编程框架呢?答案是肯定的,其中 ACE
就是一个非常著名的开源网络通信与服务器开发框架库,这是由 Douglas C.
Schmidt 在做博士论文期间用 C++
编写的网络通信与服务器开发框架,该框架出现的比较早,应用范围也比较广泛,但是编程复杂度很高,里面充斥着大量的设计模式,有人形容其学术味未免太浓。
acl
网络通信与服务器框架是另一个选择,该框架至今也有近十年的历史,最初来源于著名的邮件服务器软件
Postfix,从中借鉴了大量服务器设计思想及代码,后来逐渐演变成一个通用的服务器开发框架。在介绍
acl 服务器框架前,不妨先介绍一下 Postfix 的服务器设计模式以及 acl
服务器框架与 Postfix 的服务器的异同点。

9、配置管理性要求

在 acl
的服务器框架设计中,有一个主控制进程(acl_master),这个主控制扫描应用服务配置目录下的配置文件,启动多个服务子进程,这样,每个应用服务
程序有一个自己的配置文件,配置项中有:监听端口、进程数、线程数、运行身份、日志输出、访问控制
等等;另外,acl
服务器框架还支持软件在线升级,可以做到不中断当前业务的前提下更新服务器程序。

polarphp 优先支持的操作系统

  • debain

  • centos

  • ubuntu

  • openSUSE

  • macOS

未来打算原生支持Windows操作系统,目前正在进行知识储备。

3.1、Postfix 服务器框架的设计特点

10、快速开发部署原则

为了方便技术员快速入门,acl
库中还提供了服务器程序生成向导,只需几步便可以搭建一个基于 acl
的服务器编程框架。同时 acl
中还提供了用于快速安装部署的脚本程序,方便实施人员一键式安装部署 acl
服务器应用程序

polarphp 目前的现状

目前项目处于一个非常前期的阶段,通过docker镜像来实现项目的迭代发布,目前主要是我一个人在业余时间进行开发,欢迎大家一起玩。2019年一个重要的任务就是完善polarphp标准库libpdk,以及实现在主流的Linux操作系统上稳定的运行。

1)、父子进程协作:父进程(master)复杂调度及监控服务子进程,服务子进程负责接收处理具体的业务类型
2)、稳定:主控进程(master)监控所有子进程的运行状态,子进程异常行为可控
3)、安全:子进程以普通用户身份运行
4)、资源可控:子进程为半驻留服务方式,可在完成一定任务量或空闲一定时间后主动退出
5)、模块化:每种服务为独立程序,有多个服务器模型根据需要选择
6)、并发度:因为采用进程池方式,每个连接一个进程,所以并发度很低

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 库中包含的绝大部分功能类索引:

澳门新葡萄京所有网站 7

另外本人从事在线教育多年,将自己的资料整合建了一个QQ群,对于有兴趣一起交流学习c/c++的初学者可以加群:941636044,里面有大神会给予解答,也会有许多的资源可以供大家学习分享,欢迎大家前来一起学习进步!

如何参与

目前我们暂时只针对中国的用户,所以采用了微信和QQ群的交流方式,下面是二维码,有兴趣的同学可以扫码加入:(推荐使用微信^
_ ^)

澳门新葡萄京所有网站 8 
 澳门新葡萄京所有网站 9

下图是父进程的流程图:
澳门新葡萄京所有网站 10

目前有以下工作组

  1. 语言核心团队

  2. 标准库团队

  3. 生态链项目团队

  4. 文档团队

  5. 官方网站维护团队

(文/开源中国)    

下图为服务子进程的流程图:

澳门新葡萄京所有网站 11
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
库的网络流的设计也存在着数据缓存层,可以支持网络流和文件流,同时提供了丰富的读操作接口:读指定字节长度数据,按行读数据(可以兼容
rn 及 n 两种情况),以及其它大量的读操作函数。下图分别是阻塞 IO
和非阻塞 IO 的类继承关系:

澳门新葡萄京所有网站 12
阻塞 IO 继承关系图
澳门新葡萄京所有网站 13
非阻塞 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
域套接口传递给后端的服务子进程,这样就保证了各个服务子进程获得的客户端连接是均匀的。目前,该分配器还定期汇总各个服务子进行的运行状态,这样,我们就可以写一些前端
WEB 程序,查询各台机器上的分配器来查看所有机器上的客户端连接及负载状态。

7、安全稳定性原则

作为一个需要长时间运行的服务器程序,安全稳定性是至关重要的。

在安全性方面,acl
的服务器框架在启动服务子进程后会首先修改子进程的运行身份,将其降为普通用户身份,同时限制该子进程的运行目录,这样即使因程序存在一些
BUG 而被黑客攻破,其获得的身份也只能拥有最低的普通用户权限;

为了保证稳定性,acl
的服务器模型支持服务子进程服务次数退出机制,即当一个子进程处理的客户端连接数达到配制文件中设定的值后会自动退出(在处理完所有的连接后),服务框架
会自动启动新的子进程处理新到的连接,这样做的好处是:对于一个新上线的服务程序,有可能存在一些轻微的内存泄露,通过此自动退出与自动启动机制,就可以
有效地减少这种内存泄露所带来的危害;另外,如果服务子进程异常退出,acl
的服务主进程会将该子进程退出的消息通知一个报警子进程,由报警子进程以邮件或短信方式通知技术人员进行处理。

8、模块化原则

使用 acl
服务器框架编写服务器程序,建议将不同功能的功能模块写成独立的应用服务程序,由主控进程(acl_master)统一进行管理,这样既便于各个功能模块
的分布式部署以及将来进行各自的功能扩展,同时还将不同的功能模块进行有效隔离,避免产生过多的耦合性问题。

9、配置管理性要求

在 acl
的服务器框架设计中,有一个主控制进程(acl_master),这个主控制扫描应用服务配置目录下的配置文件,启动多个服务子进程,这样,每个应用服务
程序有一个自己的配置文件,配置项中有:监听端口、进程数、线程数、运行身份、日志输出、访问控制
等等;另外,acl
服务器框架还支持软件在线升级,可以做到不中断当前业务的前提下更新服务器程序。

10、快速开发部署原则
为了方便技术员快速入门,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 库中包含的绝大部分功能类索引:

澳门新葡萄京所有网站 14

五、参考资源

acl github:

1 赞 1 收藏
评论

关于作者:郑树新

澳门新葡萄京所有网站 15

长期从事技术研究及技术管理工作,有近17年工作经历。有两次创业经历,曾任和讯架构师,二六三企业通信首席架构师,现在爱奇艺从事基础研发。

个人主页 ·
我的文章 ·
6 ·