苏苏的博客

简约至极

PHP最佳实践

性能优化

数组相关

in_array在大数据量(数万以上元素)下效率低下

在分析nginx log得出所有不重复IP时,采用in_array判断,50多万的数据耗时需要五六分钟,而采用存键的方法,用isset判断仅需要十几秒,效率差别非常大. [php7环境下]

判断一个数组是否存在某个元素,远比查找一个键消耗的要多.前者时间复杂度O(n),而后者O(1)

即时使用in_array,也需要加上第三个参数,设置为严格模式,省略数据类型转化的开销,设置为true比设置为false性能要提升好几倍.

这种情况下使用array_search情况比in_array更加糟糕

如果条件允许使用array_flip交换键值,在用isset来判断要比in_array好得多

同时,使用isset来判断数组的键,也比使用array_key_exists要好

而对于本题,还可以使用array_unique最后去重,用空间换时间,效率仅次于用isset

php pdo MySQL server has gone away

在使用PDO长连接时,执行过一次PDO初始化后,php-fpm进程会与mysql server开启一条TCP长连接,下次连接数据库就能加快速度.

但是却存在一定问题,PDO维持了长连接并没有较好的检测其可用性,如果mysql server kill 掉这个连接,或者mysql重启,都会造成

重新实例化PDO时得到旧的链接,导致出现MySQL server has gone away,更让人郁闷的是这个错误并不是一个Exception,无法被catch捕获,即使设置PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,也不行,还是直接在页面上提示.

如果你使用了set_error_handler,那么这个Warning将会被捕捉,不会直接显示在页面上,但也改变了程序的原有执行逻辑.

如果你不使用set_error_handler,页面上报出Warning,但其实PDO已经返回一个可用的链接了.

猜测可能是PDO首先得到了不可用的然后报警告,然后又创建了一个新的.

较好的解决方法是设置set_error_handler若捕获了这个MySQL server has gone away,则返回null,程序继续按原有逻辑执行.

或者不使用PDO长连接.

注意,set_error_handler返回false的话,这个错误还是会被交到上一级错误程序处理的.

PDO int str

使用 mysqlnd 扩展而不是 mysql扩展

编码转换中的若干问题

我们经常会遇到将utf8编码字符转化为gbk编码的字符,例如生成csv表格,在windows上的cmd窗口输出文字,以及操作windows上的有关文件路径的操作都需要使用gbk编码.

常使用iconv('utf-8', 'gbk', $str);将utf8字符转为gbk

这里 utf gbk 不区分大小写,也可以混写,不区分是utf8还是utf-8,都能都正常使用

iconv('utf-8', 'gbk//ignore', $str); 加上//ignore使有些字符无法装换时略过.

但是//ignore在php5.4及以下,和个别php5.6版本上无效,任然是报NOTICE错误.

<?php
	echo iconv('UTF8', 'GBK//IGNORE', 'l l l');
?>

https://3v4l.org/7vCFWhttp://www.php.net/manual/en/function.iconv.php第一条评论

可以考虑使用$content = mb_convert_encoding($content, "GBK","UTF-8"); 从UTF8转为GBK

这样不会报错,不能装换的被替换为?,参数也是不区分大小写,utf8和utf-8

filter_var FILTER_VALIDATE_URL

filter_var FILTER_VALIDATE_URL 中不能包含中文,包含中文被判定为false

内存是拷贝还是内存引用

例1

$a=str_repeat('hello world',81920);
echo intval(memory_get_usage()/1024);

例2

$a=str_repeat('hello world',81920);
$b=$a;
$c=$a;
$d=$a;
echo intval(memory_get_usage()/1024);

例3

$a=str_repeat('hello world',81920);
$b=$a.'';
$c=$a.'';
$d=$a.'';
echo intval(memory_get_usage()/1024);

例1与例2,内存差距基本不大,例2与例3有较大差距,例3约为例2的3倍,但小于3倍

一般数据类型

php在内存上是写时拷贝,一个变量复制多份内存不会占用多份,只有变量被改变时才会新申请一份内存给此变量.

Memcached 中的问题

如果你安装了php的memcached扩展,但是igbinary扩展没有被启用,

会使得memcache存储简单数据类型没有问题,但是存储复杂数据类型,如数组等,便会出现问题,

对一个复杂数据类型的set将会导致php worker进程崩溃,类似[pool www] child 4573 exited on signal 5 (SIGTRAP) after 20.831438 seconds from start

nginx将会收到类似upstream prematurely closed connection while reading response header from upstream,用户看到的将是502错误.

故启用memcached时最好启用igbinary

php worker

python 简单的空worker 消耗内存比php空worker更少,但是随着pythonimport的模块增加,消耗的内存也随之上升,

import大多数常用模块后,内存消耗较php稍大,php进程只有业务处理会波动内存,python的CPU占用明显比php高.

但python有多线程,多线程下内存消耗也较少,都适合写一些worker,相比之下,node的worker占用较多内存,引入常用模块后内存更是占用更大,CPU占用也比PHP多,优点是异步可以媲美多线程

DOMDocument loadHTML 内存泄漏

在大量使用DOMDocument的loadHTML后,php的进程占用内存不断增长,最终内存泄漏被强行杀死.

其实这是libxml_use_internal_errors所引起的问题,loadHTML产生的警告被内部收集,但没有清除,所以一直堆积.

http://stackoverflow.com/questions/8379829/domdocument-php-memory-leak

php gzip 压缩

压缩函数:gzcompress gzdeflate gzencode

解压函数:gzuncompress gzinflate gzdecode

他们都有第三个参数,并且第三个参数相同时,产生的结果也相同,其实他们都是使用了DEFLATE压缩算法,第三个参数控制他们压缩后添加的一些其他信息,只不过默认参数不同.

ZLIB_ENCODING_RAW 对应于纯DEFLATE格式; ZLIB_ENCODING_GZIP 对应于GZIP格式; ZLIB_ENCODING_DEFLATE 对应于ZLIB格式(注意不是纯DEFLATE格式)

默认配置下压缩后大小 gzdeflate < gzcompress < gzencode

大小差别是6字节,12字节,18字节

函数 readgzfile类似于readfile但是可以在输出之前解压然后输出

Content-Encoding:deflate 对应于 gzdeflate

foreach vs array_filter

大数据量下,使用foreach过滤数组控制不如用array_filter,array_filter不加第二个参数,默认就是返回值,可用作去除非true值.

array_filter加上array_values也要比foreach快,在php7下更加明显

在php7之前的foreach其实是复制出一个副本出来,然后进行循环的。这个时候建议使用array_map(), array_walk(), array_filter()等函数来处理你的问题

php5 与 php7

php5下关键字list既不能作为一个函数也不能作为一个方法,也不能作为一个类.

在php7下有了改善,能作为一个类中的方法了.

array_merge vs array+array

http://stackoverflow.com/questions/7059721/array-merge-versus

正则\w匹不匹配中文

\w在ASCII下等价于[A-Za-z0-9_],在Unicode下表示字符(包括汉字)和数字和下划线.

/\w+/u 不添加u修饰符是不能匹配汉字的.

匹配全是汉字的正则/^[\x{4e00}-\x{9fa5}]+$/u

Unicode的中文字范围是u4e00-u9fa5,4e00对应的字是”一”,9fa5对应的汉字是”龥”

/[\x{4e00}-\x{9fa5}]/u 等同于 /[一-龥]/u

匹配汉字

http://php.net/manual/en/regexp.reference.unicode.php

<?php
preg_match_all('/\p{Han}+/u','你我他沵莪彵;1234',$m);
print_r($m);

readfile stream_copy_to_stream

大文件操作

发送 http请求的时候使用指定的host域名解析

curl -H "Host:www.studygolang.com" http://192.168.1.102/testhost.txt
$ch = curl_init();
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Host:www.studygolang.com'));
curl_setopt($ch, CURLOPT_URL, 'http://192.168.1.102/testhost.txt');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$ret = curl_exec($ch);
var_dump($ret);
// Create a stream
$opts = array(
    'http'=>array(
        'method'=>"GET",
        'header'=>"Host:www.studygolang.com"
    )
);

$context = stream_context_create($opts);

// Open the file using the HTTP headers set above
$ret = file_get_contents('http://192.168.1.102/testhost.txt', false, $context);
var_dump($ret);
package main

import (
    "net/http"
    "io/ioutil"
    "fmt"
)

func main() {
    req, err := http.NewRequest("GET", "http://192.168.1.102/testhost.txt", nil)
    if err != nil {
        panic(err)
    }
    req.Host = "www.studygolang.com"
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(body))
}
curl --resolve test.test.com:80:127.0.0.1 "http://test.test.com/"

--resolve HOST:PORT:ADDRESS Force resolve of HOST:PORT to ADDRESS

https://ec.haxx.se/usingcurl-connections.html

curl 新版已可以使用通配符 https://github.com/curl/curl/pull/2320

http://blog.studygolang.com/2014/02/golang_host/

print_r 打印大型Object慢

object vs array

内存占用上object优于array

谁的ipc最小

https://blog.litespeedtech.com/2015/07/16/php7-vs-hhvm-benchmark-series-1-hello-world/

https://stackoverflow.com/questions/4191385/php-buffer-ob-flush-vs-flush

string to hex and hex to string

string to hex

implode(unpack("H*", $string));

or

function strhex($string) {
  $hexstr = unpack('H*', $string);
  return array_shift($hexstr);
}

hex to string

pack("H*", $hex);

or

function str_to_hex($string) {
	$hexstr = unpack('H*', $string);
	return array_shift($hexstr);
}
function hex_to_str($string) {
	return hex2bin("$string");
}

or

bin2hex convert string to hex hex2bin convert hex to string

session save path

优化php性能最首要做的就是 修改session 存储为内存存储

使用redis (需要安装redis扩展)

session.save_handler = redis
session.save_path = "tcp://127.0.0.1:6379"

redis 如果有密码 tcp://127.0.0.1:6379?auth=authpwd

也可以在程序最开始配置动态

ini_set("session.save_handler", "redis");
ini_set("session.save_path", "tcp://127.0.0.1:6379");

存储到redis的键是session_id()作为键的,以string形式存储.

path 也可以是unix socket 形式

unix:///var/run/redis/redis.sock?persistent=1&weight=1&database=0

使用memcache (需要安装memcache/memcached扩展)

session.save_handler = memcache   ## 如果使用的是memcached扩展,这里就是 memcached
session.save_path = "tcp://127.0.0.1:11211"

https://segmentfault.com/q/1010000000177919

改善性能的配置和建议

  1. 使用redis模块,或者memcached模块存储session

  2. session.save_path 使用可以使用 unix socket 模式链接redis或memcached获得最佳性能

  3. 使用igbinary模块,使用igbinary模块提供的序列化方式序列化session

sed -i '/^session.serialize_handler.*/csession.serialize_handler = igbinary' /etc/php.ini
  1. nginx 链接 php-fpm 使用 unix socket 模式

  2. 开启 opcache

外部nginx链接docker内部php-fpm

  1. 使用opcache
  2. 使用igbinary序列化session
  3. 修改php.ini max_execution_time 10s
  4. 修改php.ini memory_limit 64M
  5. 开启opcache持久缓存
  6. 使用apc缓存,yac进程内缓存等
  7. 记录php-fpm slow log
  8. 使用pdo链接数据库

redis igbinary msgpack

opcache swoole apcu memcached imagick

相比golang , nodejs , python 优势 开启非常多的微服务也不会过分消耗内存,只有真正执行时才会消耗资源

php-fpm slow log

slowlog = /var/log/php-fpm/$pool.log.slow
request_slowlog_timeout = 5
request_terminate_timeout = 30

命令行参数

零时开启允许创建phar

php -d phar.readonly=0 index.php

自带server配置memory_limit大小。

php -d memory_limit=1024M -S 0.0.0.0:9098

php build-in server

发送文件到php自带的server

curl -i -v -noproxy -F "file=@/data/tmp/f.exe" http://host:18123

php server 需要接受完这个文件 ( read body eof ) 然后才会判断 body size 是不是大于设定的 max_upload_size max_post_size 等.

php 接收文件的时候,文件是存储到内存中的,不会产生零时文件.这会造成上传一个400MB的文件,php server 接受文件,内存也会慢慢增加到400MB

接收文件的过程中,php server 仍然可以正常处理请求,不会阻塞请求.

php build-in server 不支持range

编码规范

文件夹、文件名、类名 首字母大写驼峰 方法名 函数名 首字母小写驼峰 常量 大写下划线链接

php password verify

还在考虑密码加密算法?用php自带的就够了,还能防范多种密码学攻击。

https://glot.io/snippets/f0pupn3wyb

PHP 正则的效率

https://github.com/mariomka/regex-benchmark

PHP Curl 多线程的错误捕获

对于单线程curl_errno 是可靠的,多线程是不可靠的.

https://stackoverflow.com/questions/23612881/detect-curl-timeout-in-php

imagick

php7里下面两个函数已被关闭

Imagick::roundCorners
Imagick::roundCornersImage

一些有用的命令行

rg --files -t php | awk '{print "php -l " $0}' | sh

php-fpm 自定义systemd的status msg

systemctl status php-fpm

Status: "Processes active: 0, idle: 8, Requests: 274, slow: 0, Traffic: 0req/sec"

那么 systemd 怎么得到这个消息的呢, 其实是编译php时,有选项的

http://php.net/manual/zh/install.fpm.install.php

开启--with-fpm-systemd, php-fpm 会按照一定时间间隔,向/run/systemd/notifysocket文件发送数据.

可以使用strace -p php-fpm的master进程PID 跟踪这一过程.

更多关于systemd自定义status 可参考 https://zerokspot.com/weblog/2016/11/06/systemd-service-startup/

三目运算

$a = getData() ? getData() : 1;

getData 如果函数求职为真,会执行两次

php5.3可以使用?:

$a = getData() ?: 1

php7 可以使用 ??

$a = $_COOKIE['id'] ?? 0;

?:等价,但是 Cookie中没有找到键时不会抛出错误.

Type Hinting

Class/interface name (>=PHP 5.0.0); self (>=PHP 5.0.0); array (>=PHP 5.1.0); callable (>=PHP 5.4.0); bool, float, int, string (>=PHP 7.0.0); iterable - either an array or an instanceof Traversable (>=PHP 7.1.0).

PHP 中如何实现锁

  1. 不要判断文件是否存在作为锁,因为会存在TOCTOU问题

  2. fclock

  3. ftok

静态分析

https://phpmd.org/

https://github.com/ovr/phpsa

https://github.com/squizlabs/PHP_CodeSniffer/wiki

https://github.com/vimeo/psalm

https://github.com/phpmetrics/PhpMetrics

https://github.com/mmoreram/php-formatter

https://github.com/friendsofphp/PHP-CS-Fixer