苏苏的博客

简约至极

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

对于全是关联数组,

  • 会保留左值 merge 会保留右值

array plus

  1. Array plus operation treats all array as assoc array.
  2. When key conflict during plus, left(previous) value will be kept
  3. null + array() will raise fatal error

array_merge()

  1. array_merge() works different with index-array and assoc-array.
  2. If both parameters are index-array, array_merge() concat index-array values.
  3. If not, the index-array will to convert to values array, and then convert to assoc array.
  4. Now it got two assoc array and merge them together, when key conflict, right(last) value will be kept.
  5. array_merge(null, array()) returns array() and got a warning said, parameter #1 is not an array.

正则\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);

支持http/https

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

https://github.com/golang/go/issues/26623

PHP Curl 多线程的错误捕获

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

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

mysql localhost

在使用localhost时,mysql不使用TCP/IP通行,

而是查找

mysqli.default_socket

使用此socket文件通信,

若是不存在此文件, 将会报错.

在编译PHP时,决定此默认值

/var/lib/mysql/mysql.sock

可修改配置文件,修改此值 pdo_mysql.default_socket

mysqli.default_socket

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

PHP CURL

CURLOPT_RETURNTRANSFER 设置为1 数据被储存到 curl_exec 的返回值里

设置为0, 直接输出到缓存区了, 不要设置该项,就不会造成浪费了.

此时 curl_exec 返回bool值,代表操作成功和失败

下载大文件

$ch=curl_init($url);
curl_setopt_array($ch,[CURLOPT_FILE=>$fp,CURLOPT_HTTPHEADER=>self::$headers,CURLOPT_FOLLOWLOCATION=>1,CURLOPT_SSL_VERIFYPEER=>0,CURLOPT_SSL_VERIFYHOST=>0,CURLOPT_TIMEOUT=>$timeout,CURLOPT_CONNECTTIMEOUT=>$timeout]);
$success=curl_exec($ch);
curl_close($ch);

静态分析

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

6.PHP简单有效的密码策略

用户输入明文,md5后http提交(JS有这样的md5库,可防止网络窃听),服务器进行sha1,之后和数据库对比; cookie中存储[uid,用户提交的md5,ua与uid和加密后的md5的crc32的小写16进制],设置为httponly用于自动登录; 形式:1000263-47bce5c74f589f4867dbd57e9ca9f808-606216b4

包含自校验,取ua.第一段.第二段进行crc32,结果与第三段对比,通过则继续进行验证 根据第一段查找用户,取出sha1过的password,将第二段进行sha1与数据库里的对比.

sendfile

https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile/ https://www.jianshu.com/p/bf5c387830b7

.user.ini

fastcgi_param CI_ENV ‘production’;

.user.ini 可以配置很多有用的配置

short_open_tags
display_errors
error_reporting
max_execution_time
session.save-handler
include_path
file_uploads
upload_tmp_dir
upload_max_filesize
post_max_size

https://www.gearhost.com/documentation/how-to-configure-user-ini

.user.ini 可以使用 chattr 保护起来

lsattr 用来显示一个文件的附加信息

+a

a : 即append,设定该参数后,只能向文件中添加数据,而不能删除,多用于服务器日志文件安全,只有root才能设定这个属性。

+i

i: 设定文件不能被删除、改名、设定链接关系,同时不能写入或新增内容。i参数对于文件 系统的安全设置有很大帮助。

excel

https://github.com/mk-j/PHP_XLSXWriter

https://gitee.com/viest/php-ext-xlswriter

压缩

RFC 1950

https://golang.org/pkg/compress/zlib/

RFC 1951

https://golang.org/pkg/compress/flate/

RFC 1952

https://golang.org/pkg/compress/gzip/

the differences between gzip, zlib, and deflate will be essentially negligible in the space taken. The header and trailers account for 18 bytes, 6 bytes, and 0 bytes respectively for the three formats

https://imququ.com/post/how-to-compress-http-request-body.html