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/7vCFW
和 http://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
- Array plus operation treats all array as assoc array.
- When key conflict during plus, left(previous) value will be kept
- null + array() will raise fatal error
array_merge()
- array_merge() works different with index-array and assoc-array.
- If both parameters are index-array, array_merge() concat index-array values.
- If not, the index-array will to convert to values array, and then convert to assoc array.
- Now it got two assoc array and merge them together, when key conflict, right(last) value will be kept.
- 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 vs var_export
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
改善性能的配置和建议
-
使用redis模块,或者memcached模块存储session
-
session.save_path 使用可以使用 unix socket 模式链接redis或memcached获得最佳性能
-
使用igbinary模块,使用igbinary模块提供的序列化方式序列化session
sed -i '/^session.serialize_handler.*/csession.serialize_handler = igbinary' /etc/php.ini
-
nginx 链接 php-fpm 使用 unix socket 模式
-
开启 opcache
外部nginx链接docker内部php-fpm
- 使用opcache
- 使用igbinary序列化session
- 修改php.ini max_execution_time 10s
- 修改php.ini memory_limit 64M
- 开启opcache持久缓存
- 使用apc缓存,yac进程内缓存等
- 记录php-fpm slow log
- 使用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/notify
socket文件发送数据.
可以使用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 中如何实现锁
-
不要判断文件是否存在作为锁,因为会存在
TOCTOU
问题 -
fclock
-
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://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