polarphp:一个新的 PHP 语言运行时环境

by admin on 2020年3月5日

polarphp 大致架构

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

  1. polarvm

  2. zendAPI

  3. libpdk

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

polarvm <=> zendAPI <=> libpdk

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

正文

polarphp 优先支持的操作系统

  • debain

  • centos

  • ubuntu

  • openSUSE

  • macOS

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

图片 1

事件驱动机制

还是跟浏览器的差不多。总的来说就是,浏览器端把鼠标点击、键盘按键等定义为事件,而node把网络请求、I/O操作等也看作事件,严格来说,一切动作都是事件,这就是事件驱动的思想。在程序启动时,便进入事件循环,不断遍历执行事件队列中产生的事件,而在执行过程中,又会产生新的事件,因此称为事件循环。主线程执行事件时,遇到麻烦的I/O请求会交给libuv来调度其他工作线程来帮忙,忙完后就会形成事件返回结果到事件队列等待主线程处理。在此期间,主线程会继续执行其他任务。

mbp
曾经做过一个巧妙的比喻,把
Node.js 看成一家餐厅。我在此借用下他的例子,稍作修改来阐述下 Node.js
的执行情况:

把 Node.js
应用程序想象成一家星巴克,一个训练有素的前台服务生(唯一的主线程)在柜台前接受订单。当很多顾客同时光临的时候,他们排队(进入事件队列)等候接待;每当服务生接待一位顾客,服务生会把订单告知给经理(libuv),经理安排相应的专职人员去烹制咖啡(工作线程或者系统特性)。这个专职人员会使用不同的原料和咖啡机(底层
C/C++
组件)按订单要求制作咖啡或甜点,通常会有四个这样的专职人员保持在岗待命(线程池),高峰期的时候也可以安排更多(不过需要在一早就安排人员来上班,而不能中午临时通知)。服务生把订单转交给经理之后不需要等着咖啡制作完成,而是直接开始接待下一位顾客(事件循环放进调用堆栈的另一个事件),你可以把当前调用堆栈里的事件看成是站在柜台前正在接受服务的顾客。

当咖啡完成时,会被发送到顾客队列的最后位置,等它移动到柜台前服务生会叫相应顾客的名字,顾客就来取走咖啡(最后这部分在真实生活中听起来有点怪,不过你从程序执行的角度理解就比较合乎情理了)。

​ ——By
Amio

如果你想进一步了解javascript 事件驱动机制,推荐深入理解 javascript
事件循环机制

为什么发起 polarphp 项目

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

acl github:

node 架构

node架构分为三层(参考链接):

图片来源

图片 2

  • Node standard
    library:node标准库,也就是node模块提供各种接口的javascript实现,任何javascript代码、npm
    install 或者你写的模块都在这里
  • Node bindings:包括C/C++ bindings(胶水代码)和Add
    on(添加其他C/C++库时需要自己写的Bindings),这一层向下封装了V8和libuv接口,向上提供了基础API接口,是连接javascript和C++的桥梁
  • 第三层是支撑 Node.js 运行的关键,由 C/C++ 实现。
    • V8
      是Google开发的JavaScript引擎,提供JavaScript运行环境,可以说它就是
      Node.js 的发动机,负责解释javascript,与chrome浏览器相同。
    • Libuv
      是专门为Node.js开发的一个封装库,提供跨平台的异步I/O能力,负责node运行时的线程池调度。
    • C-ares:提供了异步处理 DNS 相关的能力。
    • http_parser、OpenSSL、zlib 等:提供包括 http
      解析、SSL、数据压缩等系统底层的访问。

平常我们用到的也就是第一层node各个模块实现的接口。

那他们之间时如何协作的呢

图片 3

javascript主线程

程序启动,V8引擎会首先解析javascript代码,通过Node
bindings来调用C/C++库。执行到当前事件时,会把事件放在调用堆栈(stack和heap)处理(可以理解为放进一个工作空间,如上图),在堆栈中的任何I/O请求都会交给libuv来处理,libuv维持一个线程池,里面是一些工作线程(如下图),请求会调用这些线程来完成任务,这些线程则调用底层的C/C++库。完成时,libuv再把结果返回事件队列等待主线程执行。在此期间,主线程继续执行其他任务。

图片 4

polarphp 目前的现状

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

如果降低多线程环境内存动态管理时的锁竞争呢?一般有两种方式,其一:使用建立在线程局部变量上的内存池,其二:使用会话内存管理策略。

简单应用

有了node
自带核心模块的基础功能,就可以进一步封装更强大、容易操作的功能了,就像jQuery
对于javascript 基础API 一样,node 社区也诞生了像
Express、KOA等框架来构建node.js程序

图片 5

node.js开发框架

这些框架的详情移步2017 Node.js
开发框架比较

另外,node 还可以连接MySQL,MangoDB进行数据库操作。

下面是使用express 脚手架生成的基本 node应用结构:

.
├── app.js            //程序入口
├── bin
│   └── www           //二机制文件
├── package.json      //项目配置文件
├── public
│   ├── images        
│   ├── javascripts
│   └── stylesheets
│       └── style.css
├── routes
│   ├── index.js      //路由文件入口
│   └── users.js
└── views
    ├── error.jade    //界面模板
    ├── index.jade
    └── layout.jade

现在使用 node作为后端语言通常都要配合类库和框架使用。

node
的单线程、非阻塞型特点让它非常适合高并发的应用,适合处理大量重复的、简单的逻辑,适合构建Rest/JSON
API服务;同时,也正是因为这些特性,node
不适合CPU使用率较重、IO使用率较轻的偏计算应用。缺点是因为单线程,一个进程挂就全挂了,可靠性低,但这是可以避免的。node
更多的应用是在前端、中间件、前后端分离等。

由于 node 的诸多优点,现在越来越多大公司开始使用node、深度使用node。

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

总结

node
的核心概念、思想远不止这么多,应用更是多了去了,无奈本人水平有限,只能说个浅层,还有很多像进程管理、异步编程、异常调试、部署、性能调优、与集群、CDN协调等都值得深入探索一下。无论如何,node
是让javascript
迈向企业级开发语言重要的一步(也许已经是了),前端工程师从未像现在这样的powerful,能做的事情越来越多,所能涉及的领域也越来越多。前端这行越来越令人兴奋了。

polarphp 提供的基础设施

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

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

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

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

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

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

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

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

概述

本文主要介绍了我对node的一些核心特性的理解,包括node架构、特点、机制、核心模块与简单应用。

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. 实现语言配套小工具,比如文档生成工具等等。

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

node 执行特性

polarvm 介绍

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

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

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

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

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

图片 6
3.2、acl 服务器框架与 Postfix 服务器框架的异同

从浏览器到node

首先,node是一个平台,使用javascript作为编程语言,运行在服务端。服务端语言能做的,node一般都能做,而且有些情况下做的更好,因为它具有自己的特色。

node是javascript运行环境(runtime),就像浏览器一样,是一个平台。在浏览器中,V8引擎负责解释javascript,你在javascript调用的接口都是浏览器实现并提供的,浏览器会调用底层的、由其他语言(C++)实现并封装好的接口来完成任务;同样,在node中,也是V8引擎负责解释javascript,而你在javascript调用的浏览器提供的接口就不能用了,因为它脱离了浏览器的环境,但是因为你在node环境中,你就可以使用node提供的由C++语言实现的、由javascript封装好的各种接口来完成后端任务。浏览器提供的API用于处理前端任务,比如弹个窗,换个主题,处理用户操作等,而node因为服务后端,因此提供的API则用来处理后端任务,比如响应请求,读取文件等,这些API由不同的模块提供。因为关注领域不一样,因此所做的任务就不一样,提供的API就不一样,但是原理、相关实现大致与浏览器端相同。

从浏览器到node这一块如果想了解更多,推荐IBM的文章node.js到底是什么?

如何参与

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

图片 7 
 图片 8

7、安全稳定性原则

单线程、非阻塞型I/O

单线程的意思就是只在一个线程上运行javascript。首先,javascript
在浏览器端是单线程的,这是为了避免多线程产生任务冲突的情况;其次,java和PHP这类多线程后端语言,为避免同步I/O阻塞,每处理一个连接都会产生一个新线程,这样的话在遇到大量并发请求时就会受到物理内存的限制。node
延续了浏览器端单线程javascript,只用一个主线程执行javascript,不断循环遍历事件队列,执行事件。事实上,主线程发出的I/O请求,都会交给其他线程去完成,其他线程完成后悔返回结果放到事件队列。在此期间,主线程会继续执行其他任务,也就是在交给libuv后直接返回,继续执行下面的任务,主线程只负责循环执行事件队列,因此这种模式称为非阻塞型I/O,性能很好,适用于处理大量并发请求,还能简化开发。

zendAPI 提供如下的特性:

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

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

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

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

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

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

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

node是在前端领域经常看到的词。node对于前端的重要性已经不言而喻,掌握node也是作为合格的前端工程师一项基本功了。知道node、知道后端的一些东西,才能更好的与别人合作,发挥更大的价值。

zendAPI 介绍

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

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

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

node 模块

node 模块机制是CommonJs 的实现。起初,javascript
标准一片混沌,并没有其他成熟语言(例如C++)的模块机制、标准库、接口等,为了让javascript
具备开发大型应用的能力,为了让 javascript 能在后端运行,CommonJS
就制定了javascript 模块规范。node 借鉴了这个规范,让javascript
以模块形式组织起来。模块机制是一个成熟语言必备的,一个模块代表一个功能的封装,它就像搭积木一样,不同模块可以衔接在一块,使语言具有极强的可扩展型。node
模块机制同时制定了模块规范,能让全球的开发者都可以在node官网上传自己的包。此外,node
社区又实现了node 包管理器npm,使用npm可以轻松管理各种包。

node 的模块分为核心模块和用户模块,前者是底层的、自带的,后者是第三方。

核心模块有Global(全局对象)、Http、fs(文件系统)、Buffer、Stream、Events、URL、path等,这些模块提供了后端服务的基本功能,都提供自己关注功能的API。

在使用模块时,require 即可。但在require背后,node 有一套寻找模块的机制:

图片 9

node require机制

从上图可以看到,node
优先从缓存区读取,缓存区有直接读取,没有则加载并缓存,这样做不用一遍一遍去找了,非常高效。node
在缓存区没有发现模块时,会分析require 的路径和文件后缀,node
有个模块路径的查找策略,我们可以在名为module_paths
的js文件里console.log(module.paths)然后node module_paths.js运行来间接查看node
寻找文件模块的具体文件的方式:

[ '/home/username/nodeProject/node_modules',
 '/home/username/node_modules',
 '/home/node_modules',
 '/node_modules' ] //Linux下的数组输出(/home/username因电脑不同而异)

[ 'c:\nodeProject\node_modules', 'c:\node_modules' ] //Windows

也就是按照下面的顺序:

  • 当前文件目录下的node_modules目录。
  • 父目录下的node_modules目录。
  • 父目录的父目录下的node_modules目录。
  • 沿路径向上逐级递归,直到根目录下的node_modules目录。

这些顺序都是在查找缓存之后的。

在找到模块后,node
将在引入之前对这个模块进行编译执行,编译成功后会缓存,执行的结果会返回给调用者。

图片 10

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

原文地址在我的博客,转载请注明来源,谢谢!

目前有以下工作组

  1. 语言核心团队

  2. 标准库团队

  3. 生态链项目团队

  4. 文档团队

  5. 官方网站维护团队

(文/开源中国)    

一般来讲,目前常见的网络服务器内部都会封装系统的 IO
事件引擎(如:select/poll/epool/kquque/devpoll/iocp/win32
message),以此作为网络 IO 的消息驱动引擎,acl 库内部也封装了这些 IO
事件引擎,为了适应不同的网络服务框架模型,acl 库封装的 IO
事件引擎分为单线程事件引擎以及多线程事件引擎(目前 iocp/win32 message
除外)。其中单线程 IO
事件引擎主要用在高并发非阻塞网络服务模型中,而多线程 IO
事件引擎则用在多线程服务器模型中。

polarphp 项目介绍

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

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

项目官网库:

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

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

8、模块化原则

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

图片 11
阻塞 IO 继承关系图
图片 12
非阻塞 IO 类继承关系图

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

五、参考资源

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

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

所谓”建立在线程局部变量上的内存池“,其主要思想是使用每个线程上的线程局部变量给其分配一个内存池,这样当线程需要分配/释放内存时只需引用自己的线
程局部内存池即可,不会发生与其它线程产生内存分配的冲突问题;但对于这样一个应用场景:内存在一个线程中分配而在另一个线程释放时,这种分配机制就不会
有效减少锁冲突,尤其是线程局部内存池还进行了内存分片时锁冲突问题就会更为严重,因为当某个线程获得了其它线程分配的内存后需要释放时,并不能立即释
放,而是要先归还给该内存片的”属主“线程,由”属主“线程负责释放。因此,这种分配机制主要用在内存的跨线程操作相对不”频繁“的应用场景中。在
acl 库中也提供了此类内存管理模块,参见 acl_slice.h
头文件的函数说明。当然,大家比较熟悉应该是 google 开源的 tcmalloc 库。

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

功能点 Postfix master acl_master
半驻留服务模式 支持 支持
安全控制 严格的用户权限控制 严格的用户权限控制
配置方式 所有服务配置在同一个配置文件中 一个服务一个配置文件
进程池模式 支持 支持
触发器模式 支持 支持
非阻塞模式 功能一般 功能强大
线程池模式 不支持 支持
在线升级 支持 支持
预启动 不支持 支持
最小进程数控制 不支持 支持
最大进程数控制 支持 支持
监控子进程报警机制 不支持 支持
开发过程调试功能 不太方便 方便(很容易使用 valgrind 检查)
客户端连接访问控制 应用自己保证 框架自动支持
单一进程监听多个地址  受限  支持
单一进程同时监听TCP及域套接口 不支持 支持
子进程运行身份控制 支持 支持
日志记录方式 支持 syslog 支持syslog-ng;允许用户注册自己的日志处理过程;允许同时写入多个目标日志对象中
子进程崩溃是否允许产生 core 文件 通过配置项控制,便于快速消除错误
是否支持UDP通信模式 不支持 支持
是否支持多进程TCP连接均匀化 不支持 支持

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

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

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

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

一、概述

1、网络通信功能的重要作用

在多线程运行环境中,内存的频繁动态分配及释放往往会影响整体运行性能,原因是程序在在堆上动态分配与释放内存时,需要不断地使用线程锁进行互斥,所以当
线程数非常多时,如果每个线程都有大量的内存分配/释放操作,则锁竞争非常严重,象
malloc/free 标准 C 函数内部的线程锁往往使用自旋锁,所以会发现进程的 CPU
占用非常高(在 RHL6/Centos6 上可以使用 perf top -p pid
监控进程运行状态,发会 spin_lock
调用频率非常高,这也说明了多线程进行内存分配时的竞争是非常严重的)。

关于作者:郑树新

图片 13

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

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

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

1 赞 1 收藏
评论

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

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

2、IO 事件引擎的关键作用

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

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

下图是父进程的流程图:
图片 14

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

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

而何为”使用会话内存管理策略“呢?其主要方式是:在一个任务会话开始时创建一个内存分配器(其管理着一个内存池),在下面的所有操作步骤中都将该分配器
传递,在所有处理过程中的内存分配在该分配器上进行,当该任务会话结束时释放内存分配器,从而统一释放了在该内存分配器上的内存池。这样做的好处很明显,
就是大大降低了 malloc/free
的次数。缺点也是很明显的,就是在每的个操作过程都得“带”着这个内存分配器。使用此方式的经典的例子就是
apache;当然在 acl 库也存在类似的一个简单的内存分配器(参见
acl_dbuf_pool.h ),在 acl 的 redis
客户端库中大量使用了该内存分配器,从而使之在多线程环境依赖具有很高的性能。

在 acl 库中封装的事件模型中 select
是一个通用的事件引擎(可以支持WIN32/LINUX/UNIX);epoll 是 LINUX
下内核级的高效事件引擎(尤其是在高并发环境下存在大量空闲连接时性能尤佳);iocp
是 WIN32 下的高效事件引擎,acl 中的封装与互联网上大多数使用方式不同,在
acl 中采用了单线程封装方式;win32 message 是 acl 库中专门针对基于 win32
界面消息而封装的事件 IO 引擎。

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

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

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

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

3、线程池设计中的注意要点

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

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

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

5、内存管理应如何设计

9、配置管理性要求

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

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

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

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

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

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图