存档

‘PHP’ 分类的存档

关于PHP程序员技术职业生涯规划

2017年3月5日 评论已被关闭

看到很多PHP程序员职业规划的文章,都是直接上来就提Linux、PHP、MySQL、Nginx、Redis、Memcache、jQuery这些,然后就直接上手搭环境、做项目,中级就是学习各种PHP框架和类库,高级阶段就是MySQL优化、PHP内核与扩展、架构设计这些了。

这些文章都存在一个严重的缺陷,不重视基础。就好比练武功,只求速成,不修炼内功和心法,只练各种招式,这样能高到哪里去?我所见过的PHP大牛每一个都是具备非常扎实的基础,他们之所以能成为大牛,是因为基础足够好。基础不稳,面对技术复杂的系统,如同盲人摸象、管中窥豹,只得其门不得其法。而且如果基础不扎实,也没办法进入大公司。国外的Google、Facebook,国内的腾讯、阿里、百度、滴滴、京东、新浪等知名互联网企业,无论哪一家公司面试必然会考验应聘者的技术功底。无法进入一个拥有大规模并发请求的项目中得到历练,不坚持提升自己,那也只能在小公司混日子了。

我最开始工作也是在2家小公司,后来加入腾讯阿里,主要原因还是我坚持学习基础知识,从而得倒了这个机会。有几个方面的基础知识,我建议每一位PHP程序员都应该好好学习一下。我推荐几本书给大家,包括深入理解计算机系统、现代操作系统、C程序设计语言、C语言数据结构和算法、Unix环境高级编程、TCP/IP网络通信详解。另外我建议大家学习一下面向对象方面知识,PHP这方面的书不太多,建议看Java面向对象编程、Java编程思想、J2EE这些书。PHP语言基础方面,建议认真地把PHP5权威编程这本书好好读完。另外不光要读,还要照着书中的讲解动手去编程实践。

总之有一个好的基础,再去学LAMP、Redis、PHP框架、前端,这样取得的成就更大。这与年龄无关、与学历无关、与智力无关,与天赋也无关。只要肯努力学习,人人可以成为技术大牛。

分类: PHP, 人生, 编程 标签:

PHP+Swoole的闭包写法

2016年10月25日 评论已被关闭

JS程序员总是嘲笑PHP没有闭包,今天抽空写一篇文章来专门介绍一下PHP的闭包。从5.3版本开始PHP就增加了匿名函数支持,经过数个版本迭代到现在的PHP5.6、PHP7,PHP语言的闭包已经非常完善了。再结合Swoole提供的事件驱动支持,PHP的闭包功能非常强大而且很优雅。

匿名函数

匿名函数是闭包的核心,匿名函数在PHP里实际上是一个Closure类的对象(请注意是对象)。与普通的面向对象编程方式不同,匿名函数的代码是直接写在调用处的,不需要额外写一个类,编写方法的代码。这样的好处就是更直接。下面的示例是设置一个定时器,每2秒输出hello world。

传统写法

function timer () {
    echo "hello world";
}
Swoole\Timer::tick(2000, 'timer');

闭包写法

Swoole\Timer::tick(2000, function () {
    echo "hello world";
});

非闭包的传统写法,先要声明一个函数,再转入函数名称字符串。两段代码是分离的,不够直观。而闭包的写法把定时器的声明和定时器要执行的代码写在了一起,逻辑非常清晰直观。使用闭包语法可以很方便编写回调函数。在事件驱动编程、排序、array_walk等需要用户传入一段执行代码的场景中,闭包的写法非常优雅。

闭包更强大的地方在于它可以直接在调用处引入外部变量。PHP中实现的方法就是use关键词。

Use语法

如果刚才的定时器需要传入一个变量,传统的写法只能通过全局变量来实现。与JS不同,PHP的变量引入是显式的,如果要引用外部变量必须使用use来声明。而JS是隐式的,匿名函数内部可以随意操作外部变量,无需声明。这样好处是少写了一点代码,缺点是存在风险和混乱。

传统写法

$str = "hello world";
function timer () {
    global $str;
    echo $str;
}
Swoole\Timer::tick(2000, 'timer');

闭包写法

$str = "hello world";
Swoole\Timer::tick(2000, function () use ($str) {
    echo $str;
});

闭包写法使用use直接引入了当前的$str变量,而不需要使用global全局变量。另外如果是在swoole的事件驱动编程模式,使用global就无法实现异步并发了,因为global全局变量只有1个,如果同时有多个客户端请求,每个请求要查询数据库,输出不同的内容,传统的编程方法就不太容易实现,需要使用全局变量数组,以客户端的ID为KEY保存各自的数据。

传统写法

$requestArray = array();
$dbResultArray = array();

function my_request($request, $response) {
    global $dbResultArray, $requestArray;
    $queryId = $db->query($sql, 'get_result');
    $requestArray[$request->fd] = array($request, $response);
    $dbResultArray[$queryId] = $request->fd;
}

function get_result($queryId, $queryResult) {
    global $dbResultArray, $requestArray;
    list($request, $response) = $requestArray[$dbResultArray[$queryId]];
    $response->end($queryResult);
}

$server->on('request', 'my_request');

闭包写法

$server->on('request', function ($request, $response) {
    $queryId = $db->query($sql, function ($queryId, $queryResult) use ($request, $response) {
        $response->end($queryResult);
    });
});

传统的写法非常复杂,需要反复多次从全局数组保存/提取数据。而闭包的写法非常简洁优雅,只用了几行代码就实现了同样的功能。闭包写法非常适合用来编写异步非阻塞回调模式的服务器程序。目前热门的编程语言中只有PHP和JS具备这种能力。

闭包更多特性

在类的方法中使用匿名函数,5.4以上的版本无需使用use引入$this,直接可以在匿名函数中使用$this来调用当前对象的方法。在swoole编程中,可以利用此特性减少$serv对象的use引入传递。

class Server extends Swoole\Server {
    function onReceive($serv, $fd, $reactorId, $data) {
        $db->query($sql, function ($queryId, $queryResult) use ($fd) {
            $this->send($fd, $queryResult);
        }
    }
}

另外如果希望在闭包函数中修改外部变量,可以在use时为变量增加&引用符号即可。注意对象类型不需要加&,因为在PHP中对象默认就是传引用而非传值。

分类: Node.js, PHP, Swoole扩展 标签:

基于swoole的task功能实现程序内的map-reduce

2016年3月31日 评论已被关闭

Swoole扩展自带的Task进程功能非常强大,可以用来实现各种复杂的业务逻辑。本文主要介绍使用task/finish功能实现程序内的Map-Reduce并发任务处理。一个聊天服务经常会有群聊需求,我的群组和群组内成员,另外群组内成员需要按照积分排序,类似与这样的功能就可以使用Swoole简单实现。

传统多线程方案

创建2个全局变量Map,group_map以group_id为Key,存储成员set。user_map以uid为Key存储当前用户加入的所有group。

多线程环境下实际上不能直接操作这2个Map,必须要加锁。当添加用户到一个组或者用户退出一个组时需要操作这2个map,必须要加锁。如果操作很频繁,实际上锁的碰撞是很严重的,这部分操作就会变成串行的。同时只有一个线程可以对map进行操作。锁的争抢也会带来大量线程切换浪费很多CPU资源。

lock.lock();
group_map[group_id].append([uid, score]);
user_map[uid].append(group_id);
group_map.sortByScore();
lock.unlock();

基于Swoole的Task功能

基于Swoole的Task功能,可以将任务切片,然后hash投递到不同的Task进程,完成任务。排序功能可以直接使用PHP提供的SplHeap实现,时间复杂度为O(logn),如果要实现查询功能,如根据UID查询用户加入的所有群组,根据GroupId查询有哪些成员。可以先计算Hash找到对应Task进程,然后通过task/taskwait发送指令,直接读取进程的变量查找到信息。

$serv->set(array("task_worker_num" => 24));

$serv->task(array("cmd" => "user", "uid" => $uid, "gid" => $gid, "score" => $score), $gid % $task_worker_num);
$serv->task(array("cmd" => "group", "uid" => $uid, "gid" => $gid), $uid % $task_worker_num);

class MyMaxHeap extends SplHeap
{
    public function compare($value1, $value2)
    {
        return ($value1['score'] - $value2['score']);
    }
}

function onTask($serv, $taskId, $srcWorkerId, $data) {
    static $userMap = array();
    static $groupMap = array();
    
    if ($data['cmd'] == 'group')
    {
        if (!isset($groupMap[$data['gid']]))
        {
            $groupMap[$data['gid']] = new MyMaxHeap();
        }
        $heap = $groupMap[$data['gid']];
        $heap->insert(array("uid" => $data['uid'], "score" => $data['score']));
    }
    elseif ($data['cmd'] == 'user')
    {
        $userMap[$data['uid']][] = $data['gid'];
    }
}

由于Task进程只有数组操作,所以是非阻塞的,只需要开启与CPU核数相同的进程数量即可。进程间无任何加锁争抢,性能非常好。Swoole的Task进程通信使用UnixSocket,是内核提供的全内存通信方式无任何IO,一写一读单进程可达100万/秒。虽然没有直接读变量的速度快,但性能也足够了。

————–伟大的分割线—————
PHP饭米粒(phpfamily) 由一群靠谱的人建立,愿为PHPer带来一些值得细细品味的精神食粮!
本文由 rango 独家授权 php饭米粒发布,转载请注明本来源信息和以下的二维码(长按可识别二维码关注):

分类: PHP, Swoole扩展 标签:

PHP并发IO编程之路

2016年3月21日 评论已被关闭

并发 IO 问题一直是服务器端编程中的技术难题,从最早的同步阻塞直接 Fork 进程,到 Worker 进程池/线程池,到现在的异步IO、协程。PHP 程序员因为有强大的 LAMP 框架,对这类底层方面的知识知之甚少,本文目的就是详细介绍 PHP 进行并发 IO 编程的各种尝试,最后再介绍 Swoole 的使用,深入浅出全面解析并发 IO 问题。

多进程/多线程同步阻塞

最早的服务器端程序都是通过多进程、多线程来解决并发IO的问题。进程模型出现的最早,从 Unix 系统诞生就开始有了进程的概念。最早的服务器端程序一般都是 Accept 一个客户端连接就创建一个进程,然后子进程进入循环同步阻塞地与客户端连接进行交互,收发处理数据。

多线程模式出现要晚一些,线程与进程相比更轻量,而且线程之间是共享内存堆栈的,所以不同的线程之间交互非常容易实现。比如聊天室这样的程序,客户端连接之间可以交互,比聊天室中的玩家可以任意的其他人发消息。用多线程模式实现非常简单,线程中可以直接向某一个客户端连接发送数据。而多进程模式就要用到管道、消息队列、共享内存,统称进程间通信(IPC)复杂的技术才能实现。

代码实例:

多进程/线程模型的流程是

  1. 创建一个 socket,绑定服务器端口(bind),监听端口(listen),在PHP中用stream_socket_server一个函数就能完成上面3个步骤,当然也可以使用更底层的sockets扩展分别实现。
  2. 进入while循环,阻塞在accept操作上,等待客户端连接进入。此时程序会进入睡眠状态,直到有新的客户端发起connect到服务器,操作系统会唤醒此进程。accept函数返回客户端连接的socket
  3. 主进程在多进程模型下通过forkphp: pcntl_fork)创建子进程,多线程模型下使用pthread_createphp: new Thread)创建子线程。下文如无特殊声明将使用进程同时表示进程/线程。
  4. 子进程创建成功后进入while循环,阻塞在recvphp: fread)调用上,等待客户端向服务器发送数据。收到数据后服务器程序进行处理然后使用sendphp: fwrite)向客户端发送响应。长连接的服务会持续与客户端交互,而短连接服务一般收到响应就会close
  5. 当客户端连接关闭时,子进程退出并销毁所有资源。主进程会回收掉此子进程。

 

这种模式最大的问题是,进程/线程创建和销毁的开销很大。所以上面的模式没办法应用于非常繁忙的服务器程序。对应的改进版解决了此问题,这就是经典的 Leader-Follower 模型。

代码实例:

它的特点是程序启动后就会创建N个进程。每个子进程进入 Accept,等待新的连接进入。当客户端连接到服务器时,其中一个子进程会被唤醒,开始处理客户端请求,并且不再接受新的TCP连接。当此连接关闭时,子进程会释放,重新进入 Accept ,参与处理新的连接。

这个模型的优势是完全可以复用进程,没有额外消耗,性能非常好。很多常见的服务器程序都是基于此模型的,比如 Apache PHP-FPM

多进程模型也有一些缺点。

  1. 这种模型严重依赖进程的数量解决并发问题,一个客户端连接就需要占用一个进程,工作进程的数量有多少,并发处理能力就有多少。操作系统可以创建的进程数量是有限的。
  2. 启动大量进程会带来额外的进程调度消耗。数百个进程时可能进程上下文切换调度消耗占CPU不到1%可以忽略不接,如果启动数千甚至数万个进程,消耗就会直线上升。调度消耗可能占到 CPU 的百分之几十甚至 100%

另外有一些场景多进程模型无法解决,比如即时聊天程序(IM),一台服务器要同时维持上万甚至几十万上百万的连接(经典的C10K问题),多进程模型就力不从心了。

还有一种场景也是多进程模型的软肋。通常Web服务器启动100个进程,如果一个请求消耗100ms100个进程可以提供1000qps,这样的处理能力还是不错的。但是如果请求内要调用外网Http接口,像QQ、微博登录,耗时会很长,一个请求需要10s。那一个进程1秒只能处理0.1个请求,100个进程只能达到10qps,这样的处理能力就太差了。

有没有一种技术可以在一个进程内处理所有并发IO呢?答案是有,这就是IO复用技术。

IO复用/事件循环/异步非阻塞

其实IO复用的历史和多进程一样长,Linux很早就提供了 select 系统调用,可以在一个进程内维持1024个连接。后来又加入了poll系统调用,poll做了一些改进,解决了 1024 限制的问题,可以维持任意数量的连接。但select/poll还有一个问题就是,它需要循环检测连接是否有事件。这样问题就来了,如果服务器有100万个连接,在某一时间只有一个连接向服务器发送了数据,select/poll需要做循环100万次,其中只有1次是命中的,剩下的999999次都是无效的,白白浪费了CPU资源

直到Linux 2.6内核提供了新的epoll系统调用,可以维持无限数量的连接,而且无需轮询,这才真正解决了 C10K 问题。现在各种高并发异步IO的服务器程序都是基于epoll实现的,比如NginxNode.jsErlangGolang。像 Node.js 这样单进程单线程的程序,都可以维持超过1百万TCP连接,全部归功于epoll技术。

IO复用异步非阻塞程序使用经典的Reactor模型,Reactor顾名思义就是反应堆的意思,它本身不处理任何数据收发。只是可以监视一个socket句柄的事件变化。

Reactor4个核心的操作:

  1. add添加socket监听到reactor,可以是listen socket也可以使客户端socket,也可以是管道、eventfd、信号等
  2. set修改事件监听,可以设置监听的类型,如可读、可写。可读很好理解,对于listen socket就是有新客户端连接到来了需要accept。对于客户端连接就是收到数据,需要recv。可写事件比较难理解一些。一个SOCKET是有缓存区的,如果要向客户端连接发送2M的数据,一次性是发不出去的,操作系统默认TCP缓存区只有256K。一次性只能发256K,缓存区满了之后send就会返回EAGAIN错误。这时候就要监听可写事件,在纯异步的编程中,必须去监听可写才能保证send操作是完全非阻塞的。
  3. delreactor中移除,不再监听事件
  4. callback就是事件发生后对应的处理逻辑,一般在add/set时制定。C语言用函数指针实现,JS可以用匿名函数,PHP可以用匿名函数、对象方法数组、字符串函数名。

Reactor只是一个事件发生器,实际对socket句柄的操作,如connect/acceptsend/recvclose是在callback中完成的。具体编码可参考下面的伪代码:

Reactor模型还可以与多进程、多线程结合起来用,既实现异步非阻塞IO,又利用到多核。目前流行的异步服务器程序都是这样的方式:如

  • Nginx:多进程Reactor
  • Nginx+Lua:多进程Reactor+协程
  • Golang:单线程Reactor+多线程协程
  • Swoole:多线程Reactor+多进程Worker

协程是什么

协程从底层技术角度看实际上还是异步IO Reactor模型,应用层自行实现了任务调度,借助Reactor切换各个当前执行的用户态线程,但用户代码中完全感知不到Reactor的存在。

PHP并发IO编程实践

PHP相关扩展

  • StreamPHP内核提供的socket封装
  • Sockets:对底层Socket API的封装
  • Libevent:对libevent库的封装
  • Event:基于Libevent更高级的封装,提供了面向对象接口、定时器、信号处理的支持
  • Pcntl/Posix:多进程、信号、进程管理的支持
  • Pthread:多线程、线程管理、锁的支持
  • PHP还有共享内存、信号量、消息队列的相关扩展
  • PECLPHP的扩展库,包括系统底层、数据分析、算法、驱动、科学计算、图形等都有。如果PHP标准库中没有找到,可以在PECL寻找想要的功能。

PHP语言的优劣势

PHP的优点:

  1. 第一个是简单,PHP比其他任何的语言都要简单,入门的话PHP真的是可以一周就入门。C++有一本书叫做《21天深入学习C++》,其实21天根本不可能学会,甚至可以说C++没有3-5年不可能深入掌握。但是PHP绝对可以7天入门。所以PHP程序员的数量非常多,招聘比其他语言更容易。
  2. PHP的功能非常强大,因为PHP官方的标准库和扩展库里提供了做服务器编程能用到的99%的东西。PHPPECL扩展库里你想要的任何的功能。

另外PHP有超过20年的历史,生态圈是非常大的,在Github可以找到很多代码。

PHP的缺点:

  1. 性能比较差,因为毕竟是动态脚本,不适合做密集运算,如果同样的 PHP 程序使用 C/C++ 来写,PHP 版本要比它差一百倍。
  2. 函数命名规范差,这一点大家都是了解的,PHP更讲究实用性,没有一些规范。一些函数的命名是很混乱的,所以每次你必须去翻PHP的手册。
  3. 提供的数据结构和函数的接口粒度比较粗。PHP只有一个Array数据结构,底层基于HashTablePHPArray集合了MapSetVectorQueueStackHeap等数据结构的功能。另外PHP有一个SPL提供了其他数据结构的类封装。

所以PHP

  1. PHP更适合偏实际应用层面的程序,业务开发、快速实现的利器
  2. PHP不适合开发底层软件
  3. 使用C/C++JAVAGolang等静态编译语言作为PHP的补充,动静结合
  4. 借助IDE工具实现自动补全、语法提示  

PHPSwoole扩展

基于上面的扩展使用纯PHP就可以完全实现异步网络服务器和客户端程序。但是想实现一个类似于多IO线程,还是有很多繁琐的编程工作要做,包括如何来管理连接,如何来保证数据的收发原子性,网络协议的处理。另外PHP代码在协议处理部分性能是比较差的,所以我启动了一个新的开源项目Swoole,使用C语言和PHP结合来完成了这项工作。灵活多变的业务模块使用PHP开发效率高,基础的底层和协议处理部分用C语言实现,保证了高性能。它以扩展的方式加载到了PHP中,提供了一个完整的网络通信的框架,然后PHP的代码去写一些业务。它的模型是基于多线程Reactor+多进程Worker,既支持全异步,也支持半异步半同步。

Swoole的一些特点:

  • Accept线程,解决Accept性能瓶颈和惊群问题
  • IO线程,可以更好地利用多核
  • 提供了全异步和半同步半异步2种模式
  • 处理高并发IO的部分用异步模式
  • 复杂的业务逻辑部分用同步模式
  • 底层支持了遍历所有连接、互发数据、自动合并拆分数据包、数据发送原子性。

Swoole的进程/线程模型:

Swoole程序的执行流程:

使用PHP+Swoole扩展实现异步通信编程

实例代码在https://github.com/swoole/swoole-src 主页查看。

TCP服务器与客户端

异步TCP服务器:

 

在这里new swoole_server对象,然后参数传入监听的HOSTPORT,然后设置了3个回调函数,分别是onConnect有新的连接进入、onReceive收到了某一个客户端的数据、onClose某个客户端关闭了连接。最后调用start启动服务器程序。swoole底层会根据当前机器有多少CPU核数,启动对应数量的Reactor线程和Worker进程。

异步客户端:

客户端的使用方法和服务器类似只是回调事件有4个,onConnect成功连接到服务器,这时可以去发送数据到服务器。onError连接服务器失败。onReceive服务器向客户端连接发送了数据。onClose连接关闭。

设置完事件回调后,发起connect到服务器,参数是服务器的IP,PORT和超时时间。

 

同步客户端:

同步客户端不需要设置任何事件回调,它没有Reactor监听,是阻塞串行的。等待IO完成才会进入下一步。

异步任务:

异步任务功能用于在一个纯异步的Server程序中去执行一个耗时的或者阻塞的函数。底层实现使用进程池,任务完成后会触发onFinish,程序中可以得到任务处理的结果。比如一个IM需要广播,如果直接在异步代码中广播可能会影响其他事件的处理。另外文件读写也可以使用异步任务实现,因为文件句柄没办法像socket一样使用Reactor监听。因为文件句柄总是可读的,直接读取文件可能会使服务器程序阻塞,使用异步任务是非常好的选择。

异步毫秒定时器

2个接口实现了类似JSsetIntervalsetTimeout函数功能,可以设置在n毫秒间隔实现一个函数或 n毫秒后执行一个函数。

异步MySQL客户端

swoole还提供一个内置连接池的MySQL异步客户端,可以设定最大使用MySQL连接数。并发SQL请求可以复用这些连接,而不是重复创建,这样可以保护MySQL避免连接资源被耗尽。

异步Redis客户端

异步的Web程序

程序的逻辑是从Redis中读取一个数据,然后显示HTML页面。使用ab压测性能如下:

同样的逻辑在php-fpm下的性能测试结果如下:

WebSocket程序

swoole内置了websocket服务器,可以基于此实现Web页面主动推送的功能,比如WebIM。有一个开源项目可以作为参考。https://github.com/matyhtf/php-webim

PHP+Swoole协程

异步编程一般使用回调方式,如果遇到非常复杂的逻辑,可能会层层嵌套回调函数。协程就可以解决此问题,可以顺序编写代码,但运行时是异步非阻塞的。腾讯的工程师基于Swoole扩展和PHP5.5Yield/Generator语法实现类似于Golang的协程,项目名称为TSFTencent Server Framework),开源项目地址:https://github.com/tencent-php/tsf。目前在腾讯公司的企业QQQQ公众号项目以及车轮忽略的查违章项目有大规模应用 。

TSF使用也非常简单,下面调用了3IO操作,完全是串行的写法。但实际上是异步非阻塞执行的。TSF底层调度器接管了程序的执行,在对应的IO完成后才会向下继续执行。

在树莓派上使用PHP+Swoole

PHPSwoole都可以在ARM平台上编译运行,所以在树莓派系统上也可以使用PHP+Swoole来开发网络通信的程序。

 

分类: PHP 标签:

关于C++、PHP和Swoole

2015年8月25日 评论已被关闭

昨天和一个前同事聊天,各种吐槽PHP,吐槽Swoole,他认为PHP到处是坑,PHP局限很大。PHP+Swoole不适合做高并发服务器,C+Swoole才是最好的方案。C++有各种数据结构,C++可以开线程,C++可以共享对象。看来有必要好好得说明一下了。

PHP比C/C++或Java少了什么?多线程,多线程,多线程……

是的。PHP比C/C++、Java少了多线程。PHP只有多进程的方案,所以PHP里的全局变量和对象不是共享的、数据结构也不能跨进程操作、Socket文件描述符不能共享等等。所以PHP有局限?

多线程看似比多进程要强大很多,实际上我可以负责任的告诉你,多线程带来的坑更多

  • 数据同步问题会让你崩溃的。要么就牺牲性能到处加锁,要么就用地狱难度的无锁并发编程,据我所知目前国内能掌握此项技能的人凤毛麟角。
  • 不要以为加锁就万事大吉了,你会在死锁问题上栽个大跟头。当你的程序逻辑复杂后,锁越来越难控制了,一旦死锁你的程序基本上就完了。
  • 某个线程挂了那所有线程都会退出
反而在看多进程,其实就简单的多了。
  • 配合进程间通信,基本上你可以实现任意的数据共享。比如利用一个进程专门存数据结构和对象,其他进程的数据操作全部投递到此进程来
  • 多进程不需要锁
  • 多进程可以使用共享内存的数据结构实现一些多线程的功能。如Swoole提供的Table、Atomic可以实现数据共享,但成本很低。未来还会加入共享内存队列

所谓PHP限制了Swoole,这完全是无稽之谈。合理利用Swoole提供的Table、Atomic、SendMessage/PipeMessage、Task完全可以实现异步非阻塞的代码逻辑。

C++写出来的程序性能更好?

这完全是盲目的迷信,密集计算的程序C++确实是有优势的。而并发服务器核心是IO,并非大规模密集运算。C++从语言层面来看并没有什么优势。另外C++中的大部分数据结构在PHP中都有对应的实现,实在不行自己写个专门的扩展也能解决之。

高并发的服务器单机能维持10W连接、每秒可处理3-5W笔消息收发。这种性能水准已经可以应用在BAT的核心系统上了。

开发效率快的意义是什么?

这位同事还说PHP开发Server虽然比C++快了,但是追求性能的极致还是要用C++。我要告诉你效率高了究竟意义何在。开发一套好程序不是一件容易的事情,需要程序员投入大量时间和精力。开发效率提升的意义并不是简单的我可以更少时间完工,而是剩下的时间你可以增加单元测试、修复BUG、提升用户体验、完善细节、提供配套工具、优化性能、增加关键日志、增加监控报警、增加容灾方案。

 

分类: C/C++, PHP, Swoole扩展 标签:

关于TCP网络通信

2015年8月10日 评论已被关闭

TCP协议在底层机制上解决了UDP协议的顺序和丢包重传问题。但相比UDP又带来了新的问题,TCP协议是流式的,数据包没有边界。应用程序使用TCP通信就会面临这些难题。一些程序在本机测试是正确的,上线后就出现各种奇怪的BUG。如下面的伪代码,客户端向服务器端发送一个json字符串,服务器端接收此字符串。在慢速网络中Server无法正确接收完整的JSON字符串。


$client->send(json_encode('a' => $data_10k, 'b' => $data_5k));
$pkg = $server->recv(); //Server收到的数据只有一小部分

$client->send("hello1");
$client->send("hello2");
$client->send("hello3");
$pkg = $server->recv(); //Server会一次性收到3个数据包

因为TCP通信是流式的,在接收1个大数据包时,可能会被拆分成多个数据包发送。多次Send底层也可能会合并成一次进行发送。这里就需要2个操作来解决:

  • 分包:Server收到了多个数据包,需要拆分数据包
  • 合包:Server收到的数据只是包的一部分,需要缓存数据,合并成完整的包

具体编码实现这里就不讲了,这是一个比较复杂的编程过程,稍有不慎就会出现严重的BUG

Swoole如何解决此问题

swoole提供了通用协议的支持,如Http和WebSocket。自定义协议可以使用Length/EOF 2种协议解析方式来完美解决此问题。从1.7.18版本开始,Swoole的Server/Client都支持了Length/EOF的协议处理方式,应用层代码只需要配置一下参数,就无需关注底层分包合包了。每次onReceive收到的数据包总是完整的。

Server/Client的配置是相同的

Http/WebSocket

swoole内置对http/websocket 2种协议的支持,如果要实现一个http服务或者websocket服务,直接用swoole_http_server和swoole_websocket_server即可。

EOF

$server->set(array('open_eof_split' => true, 'package_eof' => "\r\n"));

EOF协议处理的原理是每个数据包结尾加一串特殊字符表示包已结束。如memcache、ftp、stmp都使用\r\n作为结束符。发送数据时只需要在包末尾增加\r\n即可。使用EOF协议处理,一定要确保数据包中间不会出现EOF,否则会造成分包错误。

Length

$server->set(array(
'open_length_check' => true,
'package_max_length' => 81920,
'package_length_type' => 'n', //see php pack()
'package_length_offset' => 0,
'package_body_offset' => 2,
));

固定包头的协议非常通用,在BAT的服务器程序中经常能看到。这种协议的特点是一个数据包总是由包头+包体2部分组成。包头由一个字段指定了包体或整个包的长度,长度一般是使用2字节/4字节整数来表示。服务器收到包头后,可以根据长度值来精确控制需要再接收多少数据就时完整的数据包。Swoole的配置可以很好的支持这种协议,可以灵活地设置4项参数应对所有情况。

Swoole的Server和异步Client都是在onReceive回调函数中处理数据包,当设置了协议处理后,只有收到一个完整数据包时才会触发onReceive事件。同步客户端在设置了协议处理后,调用 $client->recv() 不再需要传入长度,recv函数在收到完整数据包或发生错误后返回。

 

分类: PHP, Swoole扩展 标签:

PHP7与Swoole

2015年6月19日 评论已被关闭

最近PHP官方终于发布了传说中的PHP7,虽然只是alpha版。PHP7号称是新一代的PHP,官方开发组对Zend引擎底层做了大量修改来优化PHP的性能。可以说PHP7这个版本的主题就是性能优化

在过去PHP一直以开发效率快著称,而语言本身的性能较差(当然比Python,Ruby还是要快一些的)。普通的Web网站都是IO密集型的程序,瓶颈在MySQL上,所以体现不出PHP的性能劣势。但在密集计算方面比C/C++、Java等静态编译语言差几十倍甚至上百倍。另外使用设计非常复杂的开发框架,如Symfony、Laravel等,程序性能也会明显下降。

现在随着PHP越来越流行,像Facebook、新浪微博这样超大型规模的网站都在使用PHP。PHP语言性能问题就越来越严重了。Facebook有几十万台服务器,如果现有的PHP程序可以提升一部分性能,将会节约大量的服务器资源。所以就有了HHVM、Hack。Hack为PHP增加了类型,HHVM是一个重新设计的PHP引擎,实际项目中使用HHVM可以提近70%的性能。实际项目70%性能提升这是一个什么概念?腾讯QQ农场最初使用PHP开发,后因为性能问题使用C语言重构,完成后性能提升了100%。

PHP官方也注意到了这个问题,所以就有了PHP7的开发计划。最新公布的PHP7-alpha在WordPress项目中测试的表现已经超越了HHVM。未来PHP将会同时具备极高的开发效率和极高的性能,再结合Swoole做异步编程,PHP势必会更加流行。

本文简单介绍一下PHP7做了哪些优化,可以提升如此多性能。

一  zval使用栈内存

在Zend引擎和扩展中,经常要创建一个PHP的变量,底层就是一个zval指针。之前的版本都是通过MAKE_STD_ZVAL动态的从堆上分配一个zval内存。而PHP7可以直接使用栈内存。PHP代码中创建的变量也进行了优化,PHP7直接在栈内存上预分配zval。这样节约了大量内存分配和内存管理的操作。

PHP5

zval *val; MAKE_STD_ZVAL(val);

PHP7

zval val;

二 zend_string存储hash值,array查询不再需要重复计算hash

PHP7为字符串单独创建了新类型叫做zend_string,除了char *指针和长度之外,增加了一个hash字段,用于保存字符串的hash值。PHP中array是核心数据结构,PHP程序中往往都有大量的$array[$key]操作,虽然hashtable查找的时间复杂度是O(1),但$key要转为hash值是要经过计算的。不仅仅是array操作,实际上PHP底层对于类属性、类方法、函数,访问时都要先通过hashtable查找到对应的指针,再执行对应的操作。PHP7之前Zend引擎会有大量的CPU时间用于计算hash值。

实际上PHP程序运行起来之后,大部分情况下$key的值都是不变的。PHP7干脆将这个hash值保存起来,下次直接使用,这样就节省了大量的hash计算操作,PHP的hashtable与C数组的性能一致。

从实际项目进行callgrind性能分析,会发现alloc和hash 2项操作就占用了相当大比例的CPU时间。PHP7优化之后这2项操作占用的CPU时间降低了非常多。(注:zend_hash仍然占12%,因为整体CPU降低了,所以总的耗时降低了不少)

三 hashtable桶内直接存数据

PHP5的hashtable每个元素都是一个 Bucket *,而PHP7直接存Bucket,减少了内存申请次数,提升了Cache命中率和内存访问速度。

四 zend_parse_parameters改为宏实现

PHP的C扩展函数与PHP中的变量进行参数输入时,要使用zend_parse_parameters()函数,这个函数根据一个字符串参数找到对应PHP的zval指针,然后进行赋值。 这个函数实际上有一定的性能消耗。PHP7直接使用宏替换了zend_parse_parameters函数,C扩展中不再需要使用zend_parse_parameters进行逐个参数的查找,宏展开后自动会实现参数赋值。仅此一项就提升了5%的性能。

五 新增加4种OPCODE

很多PHP程序中会大量使用call_user_function, is_int/string/array,  strlen , defined 函数。PHP5 都是以扩展函数的方式提供,PHP7中这4类函数改成ZendVM的OPCODE指令,执行更快。

六 其他更多优化

除了上面5个主要优化点之外,PHP7还有其他更多的细节性能优化。如基础类型int、float、bool等改为直接进行值拷贝,排序算法改进,PCRE with JIT,execute_data和opline使用全局寄存器等等。PHP7对性能的优化会继续进行下去。

PHP7-alpha相比PHP5.6性能提升了近3倍。下面是WordPress在PHP7上的表现:

 

PHP7的新特性

除了性能优化外,PHP7新增加了2项重要的新特性。

1. 变量类型

PHP7版本函数的参数和返回值增加了类型限定。为什么PHP要加入类型,实际上此项特性是为了PHP7.1版本的JIT特性做准备,增加类型后PHP JIT可以准确判断变量类型,生成最佳的机器指令。

function test(int $a, string $b, array $c) : int {
//code
}

2. 错误异常

PHP程序出错后过去Zend引擎会发生致命错误并终止程序运行,PHP7可以使用try/catch捕获错误。底层使用Exeception代替了Fatal Error。这个特性表示PHP语言正在向一个更加规范的方向发展。应用层与底层在错误抛出的方式全部统一为异常。

try {
non_exists_func();
} catch (EngineException $e) {
echo "Exception: {$e->getMessage()}\n";
}

3. 匿名类

$test = new class("Hello World") {
public function __construct($greeting) {
$this->greeting = $greeting;
}
};

PHP7与JIT

最初PHP7性能优化的方向并不是以上所讲的,而是JIT。JIT是just in time的缩写,表示运行时将指令转为二进制机器码。Java语言的JVM引擎底层就是使用JIT将Java字节码编译为二进制机器码执行。PHP7开发过程中有一个中间版本是基于JIT,后来开发组发现使用JIT后,对于实际项目并没有有太大的性能提升,所以PHP7最终放弃了JIT方案,PHP7.0-final版本不会携带JIT特性。

但如果是密集计算类程序就不同了,使用JIT将PHP OpCode编译为机器码,运算的性能会大幅提升。PHP官方开发组在2014年底重启了JIT的开发工作。

PHP的异步网络通信扩展Swoole

PHP在大部分程序员印象中都是用来做Web网站的。PHP没有像Python的Twisted、Tornado,Java的Netty、Mina,JavaScript的Node.js等框架,无法实现异步网络通信程序。PHP的Swoole扩展就是为了弥补此项缺陷而诞生的开源项目。Swoole是一个标准的PHP扩展,为PHP提供了一系列异步IO、事件驱动、并行数据结构功能。

Swoole与Node.js非常相似,不同之处是Swoole在并行提供了底层支持。Node.js是一个单进程单线程的程序,在多核服务器上无法发挥全部CPU核的计算能力。需要程序员自行使用child_process/cluster扩展或者启动多实例,使程序能够利用到多核优势。而Swoole在底层就支持了多线程/多进程,程序启动后就会创建好多个IO线程和多个Worker进程。程序员仅需配置线程/进程数量即可。

使用Swoole开发的TCP服务器程序:

$serv = new swoole_server("127.0.0.1", 9501);

$serv->on('connect', function ($serv, $fd){
echo "Client:Connect.\n";
});

$serv->on('receive', function ($serv, $fd, $from_id, $data) {
$serv->send($fd, $data);
});

$serv->on('close', function ($serv, $fd) {
echo "Client: Close.\n";
});

$serv->start();

Swoole同样也内置了http_server和WebSocket服务器的支持。swoole_http_server与传统的php-fpm不同,它是在PHP内进行事件循环的,基于swoole_http_server完全可以开发出类似Java应用服务器一样,可以控制完整对象生命周期的程序。swoole_http_server天然支持异步IO,可以很方便的实现支持大量TCP连接的Comet服务。swoole_websocket_server可以用来实现支持Web实时推送的程序。

使用Swoole的Web服务器程序:


$http = new swoole_http_server("0.0.0.0", 9501);

$http->on('request', function ($request, $response) {
$response->header("Content-Type", "text/html; charset=utf-8");
$response->end("

Hello Swoole. #".rand(1000, 9999)."

"); }); $http->start();

PHP的未来

可以预见PHP语言未来会在性能方面有明显的提升,越来越接近C/C++、Java等静态编译语言。再加上Swoole扩展,PHP的使用范围可以扩展到移动通信、云计算、网络游戏、物联网、车联网、智能家居等领域。

PHP虽然未必是最好的编程语言,但PHP在向着这个方向在发展。

 

分类: PHP 标签:

关于编程语言

2015年1月8日 没有评论

最近群里很多朋友询问我是如何学习编程的,今天抽个时间大致讲讲。

C语言

和所有人一样,上学学的第一门编程语言是C语言。我记得我当时学的很认真,非常认真。和别人最大的不同是我买了一本老外写的C语言编程书,没被谭浩强误导。另外还买了一本《现代操作系统》一书看了很久。学习期间写了不少读书笔记。真正要说学到什么了,这个还真不好说,到最后我对C语言也是似懂非懂的状态。

用学物理、数学的方法学编程语言是行不通的,编程需要的是实践。

Java

学Java的目的很明确,为了找一份好工作。面向对象、J2EE、Java设计模式、SSH的书买了好几本。最后还是没能够入门。原因我后来分析主要是2点,1、Java太复杂了,根本就不适合初学者。2、犯了和学习C语言同样的错误,读书太多,实践太少(主要是不会搭建Java的开发环境)。

PHP

偶然的机会了解到了PHP语言,按照网上的教程一搞,写了一个MySQL的网页程序,竟然运行成功了。这实在太神奇了。你们无法理解当时我的心情,可以说到了 高兴地3天不用吃饭的程度。呵呵,当时我就下定决心以后就是它了。PHP让我进入了一个新的境界,我可以大量地实践,写出各种程序。而且也顺利找到了工作,干的风生水起,一个接一个地做了很多项目。后来又进入腾讯,在腾讯有机会去开发维护一个超过2亿用户,日活超过8千万,机器规模超过1000多台的超大系统。我的人生的各种成果都归功于PHP这门伟大的编程语言。

重新回到C语言

当然后面也逐渐认识到PHP的不足,不过很快我就发现可以写C扩展来弥补。重新学起C语言,我发现竟然很顺利。原因应该是我在写PHP程序的过程中,工程实践能力已经磨练到非同一般。其实不只是C语言,只要我愿意,C++,Java,Python,Golang任何语言都可以快速掌握。然后就开始搞Swoole这个项目,边学边做。经过这2年的时间,现在可以说我已经掌握C语言了。

为什么会回到C语言,原因很简单:所有底层软件都是C语言写的,包括操作系统、底层库、驱动、其他编程语言本身。C语言一切现代软件技术的根源。

我的个人感悟

我的编程之路轨迹就是 C语言(起始) -> Java(过客)-> PHP(主)-> C语言(终点)。

  1. 编程语言专注于一个就行了,Java、PHP、Python、Ruby 任意一个都行。只要能把一门编程语言学精通,其他语言学起来也很容易。
  2. 工作时选择的编程语言,应该是一主多辅。做Web得了解Javascript,做APP需要掌握Android、Object-C,写桌面程序需要学习C++。带着问题去学就好了。
  3. 不要选择难或者复杂的编程语言作为入门,易于上手的编程语言更容易成功。公司有一个搞Java 9年的人,技术等级很低。很好的例子。另外一个只工作了5年的PHPer现在已经是T3-2的技术Leader了。
  4. 编程最核心的是动手实践
  5. 多看一看别的编程语言和技术,开阔一下视野
  6. 编程做到最后还是会回到C语言的,当然很多人中途就已经离开编程岗位了

 

分类: C/C++, Java, PHP, 人生 标签:

关于PHP程序员解决问题的能力

2014年6月24日 3 条评论

这个话题老生长谈了,在面试中必然考核的能力中,我个人认为解决问题能力是排第一位的,比学习能力优先级更高。解决问题的能力既能看出程序员的思维能力,应变能力,探索能力等,又可以看出他的经验。如果解决问题能力不佳是无法通过面试的。

这里举个例子,假如我执行了一个PHP的脚本,如php test.php,预期是可以返回一个字符串。但执行后没有任何信息输出,这时候通过什么方法能知道程序错在哪里?这里可以将解决问题能力分为8个等级,越到后面的表示能力越强。

Lv0 查看PHP错误信息

程序没有达到预期效果,证明代码出错了,看PHP的错误信息是第一步。如果直接忽略错误信息,表明这个人不适合担任专业的程序员岗位。有些情况下php.ini配置中关闭了错误显示,需要修改php.ini打开错误信息,或者错误信息被导出到了日志文件,这种情况可以直接tailf php_error.log来看错误信息。

拿到错误信息后直接定位到程序代码问题,或者到Google/百度搜索,即可解决问题。

注:打开错误显示的方法是

  • php.ini中display_errors / display_startup_errors 设置为On
  • php.ini中error_reporting 设置为E_ALL
  • PHP代码中设置error_reporting(E_ALL)

Lv1 存在多个版本的php或php-cli与php-fpm加载不同的配置

存在多个版本的php,懂得通过which php来看是哪个PHP,或者加绝对路径制定php版本。表示此PHPer通过了此层级的50%考验。

另外一个情况就是php-cli与php-fpm得到的执行情况不一样,如在web浏览器中执行是对的,cli下执行是错的。这时候可能是2个环境加载的php.ini不同所致。cli下通过php -i |grep php.ini得到加载了哪个php.ini。而fpm下通过phpinfo()函数可以得到php.ini的绝对路径。

 

Lv2 var_dump/die打印变量值信息单步调试

这是惯用的程序调试手段,也是最简单粗暴有效的解决问题方法。高级一点的手段是使用PHP的Trace类/日志类,花哨一点的可以借助phpstorm+xdebug在IDE工具里进行Debug。

Trace工具还可以分析脚本的耗时,进行PHP程序的性能优化。

这3个考验全部通过,表明此程序员已经具备了专业PHP程序员应该有的解决问题能力了。PHP程序员只要过了这个等级,就足以应多大部分情况,在中小型网站中毫无压力。

 

Lv3 使用strace工具跟踪程序执行

strace可以用来查看系统调用的执行,使用strace php test.php,或者strace -p 进程ID。strace就可以帮助你透过现象看本质,掌握程序执行的过程。这个手段是在大型网站,大公司里最常用的。如果没掌握strace,这里只能说抱歉了,我们不接受不会strace的PHPer。

strace其实也是对程序员基础的考验,如果不懂操作操作系统,完全不懂底层,肯定也达不到会用strace的程度。当然strace对于PHP代码里的死循环是解决不了的。比如你发现一个php-fpm进程CPU100%了,strace恐怕是解决不了的。因为strace是看系统调用,一般都是IO类操作,既然是IO密集,那CPU一定不可能是100%。

 

Lv4 使用tcpdump工具分析网络通信过程

tcpdump可以抓到网卡的数据通信过程,甚至数据内容也可以抓到。使用tcpdump可以看到网络通信过程是什么样的,如何时发起了TCP SYN3次握手,何时发送FIN包,何时发送RST包。这是一个基本功,如果不懂tcpdump,证明不具备网络问题解决能力。

 

Lv5 统计函数调用的耗时和成功率

使用xhporf/xdebug导出PHP请求的调用过程,然后分析每个函数调用的过程和耗时。能够分析PHP程序的性能瓶颈,找出可以优化的点。

另外一个对于网络服务的调用,如mysql查询,curl,其他API调用等,通过记录起始和结束时microtime,返回的是不是false,可以得到调用是否成功,耗时多少。如果可以汇总数据,整理出调用的成功率,失败率,平均延时,证明此程序员对接口质量敏感,有大型网站项目经验。

 

Lv6 gdb使用

gdb是C/C++调试程序的利器,需要具备一定C/C++功底的程序员才会能熟练使用gdb。上面说的strace无法跟踪php程序CPU100%,而gdb是可以跟踪的。另外gdb也可以解决php程序core dump的问题。

通过gdb -p 进程ID,再配合php-src的.gdbinit zbacktrace等工具,可以很方便地跟踪PHP程序的执行。像上面的CPU100%往往是PHP程序中发生死循环了,gdb进行多次查看,就大致可以得到死循环的位置。具备gdb解决问题能力的PHP程序员少之又少。如果能使用gdb解决PHP问题,这个PHPer百分之百可以通过面试,并且可以拿到较高的技术评级。

 

Lv7 查看PHP内核和扩展源码

如果能熟悉PHP内核和扩展的源码,遇到PHP程序中最复杂的内存错误,也可以有解决的能力。这类PHP程序员就是凤毛麟角了。配合gdb工具和对PHP源码的熟悉,可以查看opcode的信息,execute_data的内存,全局变量的状态等。

 

分类: PHP 标签:

node.js与swoole相比优势与劣势分析

2014年5月7日 没有评论

这几天微博上好多网友在询问node.js和swoole对比的问题。这里做一个详细的解答。

多核并行

node.js的event loop是单进程单线程的,只有一个epoll/kqueue事件轮询被执行。所以无法利用到多核的计算优势。

swoole的event loop是多线程的,是基于epoll/kqueue的Multi-Reactor模型。这点与nginx/golang相同。另外swoole的多线程Reactor之间不存在锁,这点与nginx不同。它启用了专门的accept线程,类似与JAVA的netty。

所以在多核CPU的机器上,node.js对网络IO事件的处理能力绝对是要差swoole数倍的,在4核的机器上至少要查2-3倍。

虽然node.js也提供了多线程的扩展,但对于event_loop来说必须是内核提供,扩展不行的。

另外node.js未来是否会改造成为多线程Reactor,我估计不会,这不是技术上的难题。而是一旦改成多线程Reactor,node.js恐怕就不是node.js了。失去了原来单线程的各种优势后,反而会一无是处。当然可能node.js官方开发组会思考出解决问题的巧妙办法,一切都是后话了。

不只是event_loop,执行用户层代码是也是单线程的,不能利用多核计算优势。需要用户自己去fork多进程或者创建线程池。使用难度增加了很多。不像swoole,配置一下参数即可,是天然多进程。

异步网络IO

node.js和swoole都是基于epoll/kqueue实现的全异步非阻塞IO,所以这方面大同小异,没有差别。维持TCP长连接的能力是一样的。

node.js在这里有一个优势就是它支持windows的IOCP。swoole仅支持Linux/FreeBSD/MacOS的epoll/kqueue.

异步文件读写

node.js和swoole都提供了基于线程池的异步文件读写,DNS查询功能。node.js最早基于libeio实现,后来才自行实现线程池。swoole一开始就基于线程池设计的。

实际上这里都是通过多线程阻塞来模拟的,并非真正的异步读写文件。比如同时读写数百个文件时,性能远不如普通的多进程PHP程序。

swoole中还提供了Linux Native AIO的支持,是真正的内核层异步并行文件读写,不过需要通过修改宏开关,重新编译才可以使用。

最后结论

node.js和swoole比没有明显优势,仅在Windows支持方面比swoole要好。node.js中有的特性swoole中都有。个人认为Node.js最大的优势在于:

  • node.js使用JavaScript语言,与浏览器、HTML之间的融合度非常高,使用同一种语言既写前端又写后端
  • 支持Windows平台,利用node-webkit,可以开发PC客户端软件

node.js的定位应该是前端与后端结合非常紧密的应用场景。如websocket推送,JSON-RPC,轻量级HTTP接口。

而对于真正专业的后端领域,分布式系统,node.js不适合。

 

分类: Node.js, PHP, Swoole扩展 标签: