Node.js vs Amp vs Swoole vs Go helloworld web server performance

server.js

const http = require('http');

const hostname = '0.0.0.0';
const port = 1337;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.end('Hello World!');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

server.php

<?php
require __DIR__ . '/vendor/autoload.php';
error_reporting(-1);
use Amp\Http\Server\RequestHandler\CallableRequestHandler;
use Amp\Http\Server\Server;
use Amp\Http\Server\Request;
use Amp\Http\Server\Response;
use Amp\Http\Status;
use Amp\Socket;
use Psr\Log\NullLogger;

// Run this script, then visit http://localhost:1337/ in your browser.

Amp\Loop::run(function () {
    $sockets = [
        Socket\listen("0.0.0.0:1338"),
    ];
    
    $server = new Server($sockets, new CallableRequestHandler(function (Request $request) {
        return new Response(Status::OK, [
        ], "Hello World!");
    }), new NullLogger());

    yield $server->start();

});

swoole.php

<?php
$http = new swoole_http_server("127.0.0.1", 9501);
$http->on("start", function ($server) {
    echo "Swoole http server is started at http://127.0.0.1:9501\n";
});

$http->on("request", function ($request, $response) {
    $response->end("Hello World");
});

$http->start();

server.go

package main

import (
    "fmt"
    "log"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World")
}

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

测试命令为:ab -c1000 -n30000 http://127.0.0.1:1337/

最后结果对比如下:

结论:AMP异步非阻塞框架实现的web server性能最差,swoole和go性能相当。本测试只供参考。

PHP-PM 执行原理

大体流程如下:主进程监听两个socket,一个接收外部 http 请求(web 服务器),另一个与子进程通信。主进程通过 proc_open 调用创建子进程,每个子进程会监听一个 socket(http 服务器),主进程将 http 请求转发给子进程进行处理。子进程在 bootstrap 阶段会 new 一个 Application (假设是 symfony 框架),后面所有的请求都不会再重复初始化 Application (因此比传统 PHP-FPM 执行速度快)。

画了一个图

PHP7 的高性能来自于哪些改进

  • 变量容器 zval 结构体定义改变,减少内存占用,减少引用计数相关操作。
  • zend_string。字符串复制的时候,采用引用赋值,zend_string可以避免的内存拷贝。
  • zend_array。数组的value默认为zval。
    HashTable的大小从72下降到56字节,减少22%。
    Buckets的大小从72下降到32字节,减少50%。
    数组元素的Buckets的内存空间是一同分配的。
    数组元素的key(Bucket.key)指向zend_string。
    数组元素的value被嵌入到Bucket中。
    降低CPU Cache Miss。
  • 改进函数调用机制。
  • 通过宏定义和内联函数(inline),让编译器提前完成部分工作

参考链接:

  1. http://www.csdn.net/article/2015-09-16/2825720
  2. PHP’s new hashtable implementation
  3. Internal value representation in PHP 7 – Part 2

PHP内存回收原理

每个php变量存在一个叫”zval”的变量容器中。除了包含变量的类型和值,还包括两个字节的额外信息。is_ref 是个bool值,用来标识这个变量是否是属于引用集合(reference set),refcount 用以表示指向这个zval变量容器的变量(也称符号即symbol)个数

refcount 为0时变量从内存中删除。在5.3之前的版本无法处理循环引用的问题。5.3及以后, 在引入新的垃圾回收算法来对付循环引用计数的时候, 作者加入了大量的宏来操作refcount, 为了能让错误更快的显现, 所以改名为refcount__gc, 迫使大家都使用宏来操作refcount。

一个zval在5.3之前版本占用24字节(64位系统,下同),5.3为了解决循环引用的问题,用zval_gc_info劫持了zval的分配,因此5.3到5.6,一个zval实际占用32字节。

从PHP7开始, 对于在zval的value字段中能保存下的值, 就不再对他们进行引用计数了, 而是在拷贝的时候直接赋值, 这样就省掉了大量的引用计数相关的操作, 这部分类型有:IS_LONG、IS_DOUBLE,对于那种根本没有值, 只有类型的类型, 也不需要引用计数了:IS_NULL、IS_FALSE、IS_TRUE。

PHP7的性能,我们并没有引入什么新的技术模式, 不过就是主要来自, 持续不懈的降低内存占用, 提高缓存友好性, 降低执行的指令数的这些原则而来的。

PHP5(v<5.3) zval 结构体定义:

struct _zval_struct {
    union {
        long lval;					/* long value */
        double dval;				/* double value */
        struct {
            char *val;
            int len;
        } str;
        HashTable *ht;				/* hash table value */
        zend_object_value obj;
    } value;
    unsigned int refcount;
    unsigned char type;
    unsigned char is_ref;
};

PHP5(5.3<= v < 7) zval 结构体定义:

struct _zval_struct {
    union {
        long lval;					/* long value */
        double dval;				/* double value */
        struct {
            char *val;
            int len;
        } str;
        HashTable *ht;				/* hash table value */
        zend_object_value obj;
    } value;
    unsigned int refcount__gc;
    unsigned char type;
    unsigned char is_ref__gc;
};

5.3及以上版本虽然 struct  _zval_struct 结构体的内容没有变(除了 refcount 重命名为 refcount__gc,is_ref 重命名为 is_ref__gc),但是新增的头文件 zend_gc.h 重定义了 ALLOC_ZVAL 宏

#undef  ALLOC_ZVAL
#define ALLOC_ZVAL(z) 									\
	do {												\
		(z) = (zval*)emalloc(sizeof(zval_gc_info));		\
		GC_ZVAL_INIT(z);								\
	} while (0)

zval_gc_info 的定义为

typedef struct _zval_gc_info {
	zval z;
	union {
		gc_root_buffer       *buffered;
		struct _zval_gc_info *next;
	} u;
} zval_gc_info;

所以实际上5.3及以上版本zval大小为32字节。

PHP7 zval 结构体定义如下

struct _zval_struct {
    union {
        zend_long lval;                /* long value */
        double dval;                /* double value */
        zend_refcounted *counted;
        zend_string *str;
        zend_array *arr;
        zend_object *obj;
        zend_resource *res;
        zend_reference *ref;
        zend_ast_ref *ast;
        zval *zv;
        void *ptr;
        zend_class_entry *ce;
        zend_function *func;
        struct {
            uint32_t w1;
            uint32_t w2;
        } ww;
    } value;            /* value */
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                    zend_uchar type,            /* active type */
                    zend_uchar type_flags,
                    zend_uchar const_flags,
                    zend_uchar reserved)        /* call info for EX(This) */
        } v;
        uint32_t type_info;
    } u1;
    union {
        uint32_t var_flags;
        uint32_t next;                 /* hash collision chain */
        uint32_t cache_slot;           /* literal cache slot */
        uint32_t lineno;               /* line number (for ast nodes) */
        uint32_t num_args;             /* arguments number for EX(This) */
        uint32_t fe_pos;               /* foreach position */
        uint32_t fe_iter_idx;          /* foreach iterator index */
    } u2;
};

 

关于内存对齐请参考:https://levphy.github.io/2017/03/23/memory-alignment.html

Opcache 开启前后性能对比

测试环境介绍

机器是一台基于 vmware 的虚拟机

CPU: cat /proc/cpuinfo 看到为 8 核 CPU,
    型号是 Intel(R) Xeon(R) CPU E5-2430 v2 @ 2.50GHz
MEM: free 命令看到内存大小为 16G
nginx 版本为 1.11.5, worker_processes 8
php 版本为 7.0.12, fpm 进程 100 个

程序是 yii2-app-basic , HelloController.php 文件内容如下:

<?php
namespace app\controllers;

class HelloController extends \yii\web\Controller
{
    public function actionWorld()
    {
        return 'Hello, world!';
    }
}

测试命令: ab -n10000 -c100 ‘http://yii2.app.com:8090/index.php?r=/hello/world’

测试结果:

 Opcache CPU占用 Requests per second(吞吐率)
未开启 99% 520
开启 99% 5480

用 strace 追踪 php-fpm 的系统调用(strace 会严重影响性能,追踪系统调用时用的测试命令是:ab -n1000 -c100 ‘http://yii2.app.com:8090/index.php?r=/hello/world’,总请求减少为之前的10%),结果如下:

未开启 Opcache php-fpm的系统调用
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
29.99    0.512695           3    174053           fstat
18.60    0.318016           5     59611           munmap
13.98    0.238919           4     58059         3 open
13.56    0.231822           4     59235           mmap
9.70    0.165759           3     59559           close
4.83    0.082623           8     11010         4 lstat
3.46    0.059209           2     28014           read
1.14    0.019432           4      5102       100 stat
0.76    0.012930          13      1000           accept
0.66    0.011317           6      2000           chdir
0.50    0.008500           8      1005           write
0.50    0.008488           8      1001           getcwd
0.44    0.007527           8      1000           shutdown
0.41    0.006957           2      3000           setitimer
0.40    0.006795           7      1000           poll
0.37    0.006369           4      1610           rt_sigaction
0.31    0.005215           3      2000           times
0.19    0.003238           2      2000           recvfrom
0.11    0.001857           2      1001           rt_sigprocmask

开启 Opcache php-fpm的系统调用
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
26.92    0.041058           1     28015           read
15.73    0.023990           5      5160       100 stat
8.44    0.012872           6      2000           chdir
6.78    0.010336           3      3000           setitimer
6.04    0.009209           4      2132           fcntl
5.49    0.008373           8      1000           accept
5.29    0.008068          13       615         4 lstat
3.63    0.005544           3      1618           close
3.48    0.005308           3      2000           times
3.01    0.004586           5      1001           rt_sigprocmask
2.90    0.004427           4      1005           write
2.65    0.004042           4      1001           getcwd
2.39    0.003651           2      1610           rt_sigaction
2.29    0.003491           2      2000           recvfrom
1.93    0.002950           3      1000           shutdown
1.65    0.002516           3      1000           poll
0.81    0.001241           3       397           mmap
0.38    0.000582           6       100           clone

测试结果分析:

Opcache 开启前后 cpu 使用率都达到了 100% 说明系统瓶颈在 cpu。开启 Opcache 后系统调用少了很多,特别是 fstat,mumap,open,mmap,开启后,这几个系统调用可以忽略不计。Opcache 省去了每次加载和解析 PHP 脚本的开销,一次加载解析后后续请求不用去读源码,因此少了这么多系统调用。

结论:提高PHP程序的性能,最重要也最有效的方法就是开启 Opcache。

其它:  最初 fpm 配置是监听端口,吞吐率在开启Opcache前只有340左右;开启Opcache后在900左右,cpu占有80%,无法达到100%;如果请求过多,则会出现超时错误。后来改 fpm 监听 unix sock,性能一下子上来了。Yii2 开启  debug 模式后,吞吐率为 1200  左右。

Is this php’s bug?

I am reading yii2’s source code right now. I found that this method yii\di\Instance::ensure something werid。

I changed the code as following:

public static function ensure($reference, $type = null, $container = null)
{
// I added three lines
     $container = null;
     var_dump(get_class($container));
     die;
// original code
}

I got output as :

string(15) “yii\di\Instance”

Can anyone explain?

#Answer

Just now(2016-04-12 15:20:58),I asked Laruence about this questing in qq group.  He answered me that get_calss(null) will return current scope. Then I read the php documetation , It says:

If object is omitted when inside a class, the name of that class is returned.

So, RTFM si right.

compile php with openssl on mac osx error

从源码手动编译 PHP 时出现如下错误:

Undefined symbols for architecture x86_64:
  "_PKCS5_PBKDF2_HMAC", referenced from:
      _zif_openssl_pbkdf2 in openssl.o
  "_TLSv1_1_client_method", referenced from:
      _php_openssl_setup_crypto in xp_ssl.o
  "_TLSv1_1_server_method", referenced from:
      _php_openssl_setup_crypto in xp_ssl.o
  "_TLSv1_2_client_method", referenced from:
      _php_openssl_setup_crypto in xp_ssl.o
  "_TLSv1_2_server_method", referenced from:
      _php_openssl_setup_crypto in xp_ssl.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make: *** [libs/libphp5.bundle] Error 1

解决办法

MakeFile 里面找到类似下面这一行:

EXTRA_LIBS = -lresolv -lmcrypt -lltdl -liconv-lm -lxml2 -lcurl -lssl -lcrypto

删除所有的 -lssl 和 -lcrypto 然后添加 libssl.dylib 和 libcrypto.dylib 的路径(如果你安装了 brew,那么则是 /usr/local/opt/openssl/lib/),重新运行 make 命令,done。

附上我修改后的 MakeFile EXTRA_LIBS 那一行:

EXTRA_LIBS = -lz -lresolv -lmcrypt -lltdl -lstdc++ -liconv -liconv -lpng -lz -lcurl -lz -lm -lxml2 -lz -licucore -lm -lcurl -lxml2 -lz -licucore -lm -licui18n -licuuc -licudata -licuio -lxml2 -lz -licucore -lm -lxml2 -lz -licucore -lm -lxml2 -lz -licucore -lm -lxml2 -lz -licucore -lm /usr/local/opt/openssl/lib/libssl.dylib /usr/local/opt/openssl/lib/libcrypto.dylib

 

分享一个 MySQL 分库分表类

当一个表数据记录过大时就会出现性能瓶颈,而一般对应的解决办法是要么做分区表,要么分表,分区表就不说了,分表又分为垂直分割和水平分割,具体区别请自行搜索。一般而言,分库分表属于水平分割,按照一定的规则将数据插入到不同的表中去。而分库则可以很方便的转移数据库的压力,比如将一个很大库的分别放在不同的服务器上。

下面是我写的一个分库分表的实现:

<?php
/**
 * User: jing.liu
 * Date: 14-8-12
 * Time: 下午3:16
 */

namespace App\Model\Database;

class Config
{
    public $dsn;
    public $user;
    public $password;
    /**
     * @var string 分库分表后得到的数据库名
     */
    public $dbname;
    /**
     * @var string 分库分表后得到的表名
     */
    public $table;

    /**
     * @var array MySQL 配置数组
     */
    private static $config;

    /**
     * @var string 配置文件路径
     */
    private static $configFile = 'mysql.php';

    public function __construct($dbname, $table, $id = 0)
    {
        if (is_null(static::$config)) {
            $config = include(static::$configFile);
            static::$config = $config;
        }

        $config = static::$config;
        if (isset($config['shared']) && isset($config['shared'][$dbname])) {
            $dbconfig = $config['shared'][$dbname];
            $id = is_numeric($id) ? (int)$id : crc32($id);
            $database_id = ($id / $dbconfig['database_split'][0]) % $dbconfig['database_split'][1];
            $table_id = ($id / $dbconfig['table_split'][0]) % $dbconfig['table_split'][1];

            foreach ($dbconfig['host'] as $key => $conf) {
                list($from, $to) = explode('-', $key);
                if ($from <= $database_id && $database_id <= $to) {
                    $the_config = $conf;
                }
            }

            $this->dbname = $dbname . '_' . $database_id;
            $this->table = $table . '_' . $table_id;
        } else {
            $this->dbname = $dbname;
            $this->table = $table;
            $the_config = $config['db'][$dbname];
        }
        $c = $the_config;
        if (isset($c['unix_socket']) && $c['unix_socket']) {
            $this->dsn = sprintf('mysql:dbname=%s;unix_socket=%s', $this->dbname, $c['unix_socket']);
        } else {
            $this->dsn = sprintf('mysql:dbname=%s;host=%s;port=%s', $this->dbname, $c['host'], $c['port']);
        }
        $this->user = $c['user'];
        $this->password = $c['password'];
    }

}

Config 类就做一个事情,根据配置文件,去拿到对应的库和表的链接配置,然后客户可以根据 dsn 去链接对应的数据库。对应的配置文件如下:

<?php
/**
 * User: jing.liu
 * Date: 14-8-6
 * Time: 上午11:19
 */

$default = array(
    'unix_socket' => null,
    'host' => 'localhost',
    'port' => '3306',
    'user' => 'root',
    'password' => '',
);

$config = array(
    // 不进行分库分表的数据库
    'db' => array(
        'my_site' => $default,
    ),
    // 分库分表
    'shared' => array(
        'user' => array(
            'host' => array(
                /**
                 * 编号为 0 到 10 的库使用的链接配置
                 */
                '0-10' => $default,
                /**
                 * 编号为 11 到 28 的库使用的链接配置
                 */
                '11-28' => $default,
                /**
                 * 编号为 29 到 99 的库使用的链接配置
                 */
                '29-99' => $default,

            ),

            // 分库分表规则
            /**
             * 下面的配置对应百库百表
             * 如果根据 uid 进行分表,假设 uid 为 543234678,对应的库表为:
             *  (543234678 / 1) % 100 = 78 为编号为 78 的库
             *  (543234678 / 100) % 100 = 46 为编号为 46 的表
             */
            'database_split' => array(1, 100),
            'table_split' => array(100, 100),
        ),
    ),
);


return $config;

给出一个使用这个分库分表的例子:

<?php
/**
 * User: jing.liu
 * Date: 14-8-6
 * Time: 上午10:23
 */

namespace App\Model;

use App\Model\Database\Config;
use \PDO;

abstract class Model
{
    /**
     * @var Config
     */
    public $config;

    /**
     * @var PDO
     */
    public $connection;

    protected $dbnamePrefix;
    protected $tablePrefix;

    /**
     * @var string 分库分表后对应的表
     */
    protected $table;

    public function __construct($id)
    {
        $this->config = new Config($this->dbnamePrefix, $this->tablePrefix, $id);
        $this->connection = new Pdo($this->config->dsn, $this->config->user, $this->config->password);
        $this->table = $this->config->table;
    }

    public function update(array $data, array $where = array())
    {

    }

    public function select(array $where)
    {

    }

    public function insert(array $data)
    {

    }

    public function query($sql)
    {
        return $this->connection->query($sql);
    }
}

下面这个例子展示了如何使用上述的 Model 类:

<?php
/**
 * User: jing.liu
 * Date: 14-8-12
 * Time: 下午4:06
 */

require 'Config.php';
require 'Model.php';

use App\Model\Model;

class User extends Model
{
    protected $dbnamePrefix = 'user';
    protected $tablePrefix = 'userinfo';
}

$user = new User(4455345345);

print_r($user);

如果看官您有任何疑问或者有更好的实现,欢迎交流~

PHP 取一个整数的十位上的数值可能出现的 bug

问:如何取到一个整数的十位上的数值?

答:先除以 10 再对 10 取余。代码如下:

echo ($number / 10) % 10;

很简单是不是?但这段代码有 bug。你可以运行下面这段代码:

echo PHP_INT_MAX;
echo PHP_EOL; // 输出换行 
echo (PHP_INT_MAX / 10) % 10;
echo PHP_EOL;

输出如下:

buggggggggggeeeeee

可以看到整数 9223372036854775807 的十位数上是 0 ,而我们确得到了 2 ,这是怎么一回事呢?

我们执行下如下代码:

$a = PHP_INT_MAX;
var_dump(gettype($a), $a);
$b = $a / 10;
var_dump(gettype($b), $b);
$c = $b % 10;
var_dump(gettype($c), $c);

输出为:

RTX截图未命名

可以看到, 9223372036854775807 不能被 10 整除,$a / 10 后得到的是一个浮点数,丢失了精度,这个时候再对 10 取模得到的值就不准了。