升級/安裝 PHP (Ubuntu)
刪除舊版本 5.0
sudo apt-get autoremove --purge php5-*
安裝 7.0
sudo add-apt-repository ppa:ondrej/php
sudo apt-get -y update
sudo apt-get install php7.0 php7.0-fpm php7.0-mysql php7.0-mcrypt php7.0-curl -y
安裝完 php 的 extension (e.g. mcrypt) 會自動幫你建立 /etc/php/7.0/mods-available/mcrypt.ini
並 link 到 /etc/php/7.0/fpm/conf.d/20-mcrypt.ini
Optional:
- php7.0-bz2 (壓縮)
- php7.0-dom (xml)
- php7.0-mbstring (處理編碼)
檢查語法錯誤
$ php -l test.php
No syntax errors detected in test.php
啟動php shell模式
$ php -a
Interactive shell
php > echo "Hello World!!";
Hello World!!
php > exit #離開shell模式
開一個SERVER
php -S test.google.com:8002
直接執行php指令
$ php -r "echo 'hello';"
hello
環境設定
顯示已載入的 extension
echo "<pre>";
print_r(get_loaded_extensions());
判斷某個 extension 是否載入,未載入時載入
if ( ! extension_loaded('Memcached'))
{
dl('memcached.so');
}
PHP 設定檔路徑
- php.ini :
/etc/php5/apache2/php.ini
- session 路徑 :
/var/lib/php5
- pecl (php 5.4) 預設安裝路徑
/usr/lib/php5/20100525/
server variable
.htaccess :
setEnv STATIC_DEBUG 1
php :
$_SERVER['STATIC_DEBUG']
PHP.ini/PHP-FPM 設定
/etc/php/7.0/fpm/php.ini
: cgi.fix_pathinfo 安全性設定
預設為1, 可能會導致安全性問題: www.example.com/1.jpg, 改用www.example.com/1.jpg/xxx.php 會被執行 (此段未試過, 從此篇抄過來)
解法有兩種
-
將 cgi.fix_pathinfo = 0
-
調整 nginx 設定
if ( $fastcgi_script_name ~ ..*/.*php ) {
return 403;
}
/etc/php/7.0/fpm/pool.d/www.conf
: process manager 相關設定
pm = dynamic
pm.max_children = 15
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 15
pm.max_requests = 500
access.log = /var/log/php-fpm/$pool.access.log
slowlog = /var/log/php-fpm/$pool.log.slow
php_admin_value[error_log] = /var/log/php-fpm/www-error.log
php_admin_flag[log_errors] = on
/etc/php/7.0/fpm/php-fpm.conf
:
# 修改 log path
error_log = /var/log/php-fpm/php7.0-fpm.log`
php-fpm 執行失敗
查 /var/log/nginx/error.log
顯示 /run/php/php7.0-fpm.sock
不存在 :
2017/08/07 09:03:09 [crit] 4711#0: *1 connect() to unix:/run/php/php7.0-fpm.sock failed (2: No such file or directory) while connecting to upstream, client: 127.0.0.1, server: astra.cloud, request: "GET /api/ HTTP/1.1", upstream: "fastcgi://unix:/run/php/php7.0-fpm.sock:", host: "127.0.0.1:8080"
表示可能 php-fpm 沒執行成功, 發現重新執行 php-fpm 雖然成功, 但是 process 啟動後就被 kill 了
$ sudo /etc/init.d/php7.0-fpm start
php7.0-fpm start/running, process 7801
檢查 /var/log/php7.0-fpm.log
有沒有 error message
如果上面的 log 沒有顯示 error message, 看看 /var/log/syslog
有沒有相關線索
Aug 7 09:47:45 ip-10-0-13-120 kernel: [23129.971713] init: php7.0-fpm main process ended, respawning
Aug 7 09:47:45 ip-10-0-13-120 kernel: [23129.997879] init: php7.0-fpm main process (14519) terminated with status 78`
或直接使用 test 參數看 php-fpm 問題出在哪
$ sudo php-fpm7.0 -t
[07-Aug-2017 09:56:26] ERROR: failed to open access log (/var/log/php-fpm/www.access.log): No such file or directory (2)
[07-Aug-2017 09:56:26] ERROR: failed to post process the configuration
[07-Aug-2017 09:56:26] ERROR: FPM initialization failed
php-fpm 沒有權限建立我指定的 log path, 當我建立好之後就正常了
$ sudo php-fpm7.0 -t
[07-Aug-2017 09:58:19] NOTICE: configuration file /etc/php/7.0/fpm/php-fpm.conf test is successful
安裝其他 extension
redis
1) sudo vim /etc/php/7.0/mods-available/redis.ini
```
extension=redis.so
```
2)
sudo ln -s /etc/php/7.0/mods-available/redis.ini /etc/php/7.0/cli/conf.d/20-redis.ini
sudo ln -s /etc/php/7.0/mods-available/redis.ini /etc/php/7.0/fpm/conf.d/20-redis.ini
3) restart php-fpm
4) 如果依然顯示 `Exception: Class 'Redis' not found` 就執行 `sudo apt-get install php-redis`, 再 restart php-fpm
將 session 改使用 redis, /etc/php/7.0/fpm/pool.d/www.conf
php_value[session.save_path] = tcp://redis.astra.ap-northeast-1.local:6379
Session 存活時間
php.ini :
session.cookie_lifetime = 0 // The value 0 means "until the browser is closed."
session.gc_maxlifetime = 1440 // session 最多存活時間 (seconds)
語法
include vs require & require vs require_once
- The require() function is identical to include(), except that it handles errors differently. If an error occurs, the include() function generates a warning, but the script will continue execution. The require() generates a fatal error, and the script will stop.
- The require_once() statement is identical to require() except PHP will check if the file has already been included, and if so, not include (require) it again.
json_encode 中文亂碼
預設轉為json時, 如果裡面包含中文, 會被轉成編碼, 如:\u53f0\u6e7e \u53f0\u5317\u5e02
如果加上第二個參數 JSON_UNESCAPED_UNICODE
:
json_encode($data, JSON_UNESCAPED_UNICODE)
出來json就會是中文了 : 台湾 台北市
將字串切割成陣列
$row_arr = preg_split('/\s+/', $row, $limit = 5);
第三個參數指定要切幾次
array preg_split ( string $pattern , string $subject [, int $limit = -1 [, int $flags = 0 ]] )
字串也能用陣列來取字元
$a = "238dsfjkh2378afhkj234d";
php > echo $a[0];
2
php > echo $a[1];
3
php multiple replace
$patterns = array("this", "that", "other");
$regex = '/(' .implode('|', $patterns) .')/i';
if (preg_replace($regex, "me")) { ...
rawurlencode 與 urlencode 的差異
編碼後都一樣的符號:
`
!
@
#
$(錢號)
%
^
&
*
(
)
_
-
+
=
<
>
,
.
/
?
|
\
'
"
:
;
{
}
[
]
數字
英文
中文
不同的:
~
urlencode('~') : %7E
rawurlencode('~') : ~
(空格)
urlencode(' ') : +
rawurlencode(' ') : %20
php 傳json物件給javascript
php :
<input type="hidden" id="json-uids" value='<?php if (isset($uids)) echo json_encode($uids); ?>'/>
javascript :
var j = JSON.parse($('#json-uids').val());
不顯示錯誤訊息 : @
@ rename($old_name, $new_name)
加上@
後,發生錯誤時頁面上不顯示錯誤訊息,但還是會記log (codeigniter測試)
脫逸
只有單引號裡的\
需要特別脫逸 : '\\'
,其他符號都不需脫逸
php debug
echo "<pre>" . print_r($user_list) . "</pre>";
browser 顯示結果 :
Array
(
[0] => Array
(
[id] => 1
[name] => test_user
)
)
隱藏錯誤
error_reporting(E_ALL ^ E_STRICT);
PHP 內部常數
DIRECTORY_SEPARATOR
是 php 內部變數, 在 windows 為 \
, linux 為 /
php script 宣告
#!/usr/bin/php
<?php
// code..
?>
php script 接收指令參數
執行 php script 並接收入進來的參數, 指令 : php t.php Hello-World
t.php :
echo $argv[1]; // Hello-World
簡寫
if else :
php > ($a > 0) ? $b = 2 : $b = 3; echo $b;
2
另種寫法 :
php > $a = 1;
// 成立
php > ($a > 0) && $b = 2; echo $b;
2
// 不成立
php > ($a < 0) && $b = 3; echo $b;
2
檢查及引入某個 module (ex: mcrypt)
php -m | grep mcrypt
: 檢查 mcrypt 有沒有引入
沒有則執行以下
sudo apt-get install php5-mcrypt
sudo php5enmod mcrypt
使用 nginx 記得重啟 fpm : sudo service php5-fpm restart
Regex
preg_match
return preg_match('/pattern/flag', $str); //返回TRUE, FALSE
preg_replace
Example 1
$str = 'aabbccddeeffccggii';
$str = preg_replace('/cc/', '\\1(cc)被取代了', $str);
結果為 : aabb(cc)被取代了ddeeff(cc)被取代了ggii
Example 2
$str = 'qq-qq-QQ';
$reg = '/qq/';
echo preg_replace($reg,'哈',$str); //參數分別為 (pattern , 要取代的字串 , 原字串)
結果為 : 顯示結果為 : 哈-哈-QQ
取得比對結果存進正則變數
Example 1
$str = 'aabbccddeeffccggiiCC';
$str = preg_replace('/cc/i', "[\\0]", $str); //忽略大小寫 , 結果為 : aabb[cc]ddeeff[cc]ggii[CC]
echo $str;
\\0
是取得比對到的字串 , 我們將比對到的字串加上中括號。 似乎pattern未加上括號只能用\0去取變數
Example 2
$str = 'aabbccddeeffccggiiCC';
$str = preg_replace('/(cc)/i', "[\\1]", $str); //忽略大小寫 , 結果為 : aabb[cc]ddeeff[cc]ggii[CC]
echo $str.'<br>';
註 : pattern 對要比對的字串加上括號 , \\1
是取得比對到的字串 , 最後將比對到的字串加上中括號
其他
pattern escape
/
: \\/
(
: \(
)
: \)
.
: \.
擲出第一張長寬各大於100的圖, 如果都沒有就擲第一張圖
$url = $this->input->get_post('url');
if ( ! $html_content = @file_get_contents($url))
{
return $this->_outputJSON(array('status' => 'fail', 'error' => 'Wrong url'));
}
$patten = "/<img.*src=\"(((http[s]?):\/\/|www\.)([^\s\[\\\"'])+)\".*>/";
if (preg_match_all($patten, $html_content, $matches, PREG_SET_ORDER))
{
foreach ($matches as $match)
{
list($width, $height) = getimagesize($match[1]);
if ($width >= 100 OR $height >= 100)
{
return $this->_outputJSON(array('status' => 'ok', 'url' => $match[1]));
}
}
return $this->_outputJSON(array('status' => 'ok', 'url' => $matches[0][1]));
}
return $this->_outputJSON(array('status' => 'fail', 'error' => 'Image not found'));
如果只要單一match的話使用
if (preg_match($patten, $html_content, $matches, PREG_OFFSET_CAPTURE))
Difference between htmlentities
and htmlspecialchars
相同處
兩者都會轉換以下符號 :
&
(ampersand) : &
"
(double quote) : "
'
(single quote) : ''
or '
<
(less than) : <
>
(greater than) : >
兩者都不會轉換英文
、數字
、括號
及分號
不同處
- htmlspecialchars 只轉換以上的 HTML 符號
- htmlentities 除了轉換以上的 HTML 符號, 也轉換中文
Example
不包含中文, 程式碼 :
echo htmlentities("<script type='text/javascript'>alert(1);</script>");
echo htmlspecialchars("<script type='text/javascript'>alert(1);</script>");
轉換後的原始碼 :
<script type='text/javascript'>alert(1);</script>
<script type='text/javascript'>alert(1);</script>
顯示結果 :
<script type='text/javascript'>alert(1);</script>
<script type='text/javascript'>alert(1);</script>
兩者結果相同
包含中文, 程式碼 :
echo htmlentities("<script type='text/javascript'>alert(1);</script>測試");
echo htmlspecialchars("<script type='text/javascript'>alert(1);</script>測試");
轉換後的原始碼 :
<script type='text/javascript'>alert(1);</script>測試
<script type='text/javascript'>alert(1);</script>測試
顯示結果 :
<script type='text/javascript'>alert(1);</script>a﹐?ec|
<script type='text/javascript'>alert(1);</script>測試
兩者結果不同
結論 :
- 如果只有數字、英文、符號, 這兩者轉換後的結果完全沒差
- 但如果有包含中文, 結果就會不同。 htmlentities 會轉換中文
- htmlspecialchars 速度會比 htmlentities 快
參考資料 :
exec() 執行外部指令
exec() 是用來執行外部指令,並且可以透過它取得執行結果
分析 exec('ls -l 2>&1', $result, $is_fail)
[1] 首先第一個參數是 command line,執行的是ls -l
指令
[2] 第二個參數是當執行成功才會有值,失敗則不會有值
原本執行錯誤時 $result 是不會有值的,但當 command line 加上2>&1
就可以將錯誤訊息回傳給 $result
[3] 第三個參數為狀態碼,簡單來說就是執行成功為 0,非 0 則是失敗
- 1 : Catchall for general errors
- 2 : Misuse of shell builtins (according to Bash documentation)
- 126 : Command invoked cannot execute
- 127 : “command not found”
- 128 : Invalid argument to exit
不要完全相信以上表的錯誤訊息, 建議要取得準確的錯誤訊息還是加上2>&1
比較好
exec(‘sudo …..’), 使用 sudo 執行指令
[1] apache 預設執行身份在 /etc/apache2/envvars
export APACHE_RUN_USER=www-data
export APACHE_RUN_GROUP=www-data
[2] 原本是在 /etc/apache2/apache2.conf
:
有些 linux 的 apache 的預設執行身份不像 ubuntu 是用變數,而是直接寫死在這的, 所以 ubuntu 的 apache2.conf 會再去 load envvars,取得 user 身份 :
User ${APACHE_RUN_USER}
Group ${APACHE_RUN_GROUP}
[3] 執行權限設定在 /etc/sudoers
, apache 預設的 http user 是 www-data
如果要用 sudo
讓 exec
執行特定指令
www-data ALL=(ALL) NOPASSWD: /usr/sbin/smartctl,/usr/bin/php
允許執行所有指令:
www-data ALL=(ALL) NOPASSWD:ALL
為了讓 exec()
執行 sudo
時不需要輸入密碼,否則 exec()
沒辦法執行
[4] 例如執行 smartctl
指令:
echo exec("sudo -u www-data sudo smartctl -i /dev/sda1");
如果 apache 預設的執行 user 是 www-data (例如: 步驟 1 預設執行 user 是 www-data),則可以省略寫成:
echo exec("sudo smartctl -i /dev/sda1");
參考來源:
switch(0) problem
當switch帶0進去後會發生一些問題,先來看以下的範例:
function result($num)
{
switch ($num)
{
case $num < 60 :
echo '<60';
break;
case $num > 60 :
echo '>60';
break;
default:
echo "I don't know!";
break;
}
}
echo result(0); // 結果:>60
echo result(1); // 結果:<60
Why ?
答案就在 官方手冊這一頁 的 Loose comparisons with ==
這邊,意思是說當帶0進去時它的判斷會是 0 == (0<60)
,結果就不會是你預期的,因為0
在模糊比對裡也就是代表著false
。
那什麼情況要用if else
什麼情況用switch
?
當你要比對的是一個值而不是一個判斷句則使用switch就沒有問題 ex :
case 3 :
echo '這是3';
break;
如果要比對的是一個判斷句,代入>0的數值
不會出問題,但當代入0
時就會出問題,這不是php的bug,只能算是使用上的認知錯誤,因為我們以為它的判斷為0 > 60
,但實際上是0 == (0>60)
,所以比對判斷句就使用if else
吧!
case $num > 60 :
echo '大於60';
break;
ref:
夯哥
Interface & Abstract
程式碼 :
header('Content-Type: text/html; charset=utf-8');
/**
* 題目 :
* (1)有一台紅色休旅車跟藍色法拉利
* (2)有前進、後退、左轉、右轉,基本功能
* (3)有加速跟減速,法拉利性能較優異(法拉利加速跟減速單位都是20公里, 休旅車是10公里)
* (4)紅色休旅車是自排,極速是180公里(打檔的方法我省略它沒有做)
* (5)藍色法拉利是手排,極速是320公里
*
* 介面跟抽象的差異 :
* (1)介面不可以定義屬性,只可以定義方法,而且只能是public,否則會得到錯誤,
* 如: "Fatal error: Access type for interface method Car::accelerate() must be omitted in"
* (2)抽象可以宣告public/protected/private方法及屬性
* (3)只要有一個方法宣告為abstract,類別就要宣告為abstract
* (4)抽象跟類別都不能實體化,代表著 :
* 1. 抽象只能被繼承(一個類別只能繼承一個父類別)
* 2. 介面只能被實作(一個類別可以實作很多介面)
*
* 做法 :
* (1)如果大家都會做一模一樣的事情,可以用抽象定義
* (例如 : 因為前進、後退、左轉、右轉,動作大家都是一樣所以我在抽象定義)
* (2)如果大家是做同一件事情,但做法有差異用介面定義
* (例如 : 加速跟減速因為車種的性能不一樣,所以我在介面定義)
* (3)封裝速度,設定上下限
*/
// 介面 - 車子
interface Car {
public function accelerate($times); // 加速
public function decelerate($times); // 減速
}
// 抽像 - 操作動作
abstract class Operate {
public $name;
public $color;
protected $_transmission; // 排檔(手、自排)
private $_direction; // 方向(左、右轉)
private $_movement; // 移動(前、後)
abstract protected function _set_speed($speed);
abstract public function get_speed();
public function __construct()
{
echo "$this->name [$this->color] [$this->_transmission] 生產出來了! <br>";
}
/**
* 前進
*
* @access public
* @return null
*/
public function move_forward()
{
$this->_movement = '前進';
echo $this->name."前進了! <br>";
}
/**
* 後退
*
* @access public
* @return null
*/
public function move_back()
{
$this->_movement = '後退';
echo $this->name."後退了! <br>";
}
/**
* 左轉
*
* @access public
* @return null
*/
public function turn_left()
{
$this->_direction = '左轉';
echo $this->name."左轉了! <br>";
}
/**
* 右轉
*
* @access public
* @return null
*/
public function turn_right()
{
$this->_direction = '右轉';
echo $this->name."右轉了! <br>";
}
/**
* 取得車子目前狀態
*
* @access public
* @return null
*/
public function get_status()
{
echo $this->name."目前狀態是 : $this->_movement $this->_direction";
}
}
// 物件 - 休旅車
class Suv extends Operate implements Car {
public $name = '<span style="color:red;">休旅車</span>';
public $color = '紅';
protected $_transmission = '自排';
private $_speed = 0;
/**
* set速度變數(極速180)
*
* @access protected
* @param int
* @return null
*/
protected function _set_speed($speed)
{
$this->_speed += $speed;
if ($this->_speed >= 180)
{
$this->_speed = 180;
}
elseif ($this->_speed <= 0)
{
$this->_speed = 0;
}
}
/**
* get速度變數
*
* @access public
* @return int
*/
public function get_speed()
{
echo ",目前時速是 : ".$this->_speed." km/h <br>";
return $this->_speed;
}
/**
* 加速
*
* @access public
* @param int $times 加速的次數
* @return null
*/
public function accelerate($times = 1)
{
$this->_set_speed(10 * $times);
echo $this->name."加速".(10 * $times)." km/h,目前時速是 : $this->_speed<br>";
}
/**
* 減速
*
* @access public
* @param int $times 減速次數
* @return null
*/
public function decelerate($times = 1)
{
$this->_set_speed(-10 * $times);
echo $this->name."減速".(-10 * $times)." km/h,目前時速是 : $this->_speed<br>";
}
}
// 物件 - 法拉利
class Ferrari extends Operate implements Car {
public $name = '<span style="color:blue;">法拉利</span>';
public $color = '藍';
protected $_transmission = '手排';
private $_speed = 0;
/**
* set速度變數(極速320)
*
* @access protected
* @param int
* @return null
*/
protected function _set_speed($speed)
{
$this->_speed += $speed;
if ($this->_speed >= 320)
{
$this->_speed = 320;
}
elseif ($this->_speed <= 0)
{
$this->_speed = 0;
}
}
/**
* get速度變數
*
* @access public
* @return int
*/
public function get_speed()
{
echo ",目前時速是 : ".$this->_speed." km/h <br>";
return $this->_speed;
}
/**
* 加速
*
* @access public
* @param int $times 加速的次數
* @return null
*/
public function accelerate($times = 1) // 法拉利性能好, 所以加20
{
$this->_set_speed(20 * $times);
echo $this->name."加速".(20 * $times)." km/h,目前時速是 : $this->_speed<br>";
}
/**
* 減速
*
* @access public
* @param int $times 減速次數
* @return null
*/
public function decelerate($times = 1) // 法拉利性能好, 所以減20
{
$this->_set_speed(-20 * $times);
echo $this->name."減速".(-20 * $times)." km/h,目前時速是 : $this->_speed<br>";
}
}
// 建立物件
$ferrari = new Ferrari();
$suv = new Suv();
// 執行動作
$ferrari->move_back();
$suv->move_back();
$ferrari->accelerate(5);
$ferrari->accelerate();
$ferrari->move_forward();
$suv->accelerate(3);
$ferrari->decelerate(30);
$suv->decelerate(5);
$ferrari->turn_left();
$suv->turn_right();
$suv->accelerate(20);
$ferrari->accelerate(20);
$suv->decelerate();
$ferrari->decelerate(4);
$suv->decelerate(8);
$suv->turn_left();
$ferrari->turn_right();
// 顯示最後結果
$suv->get_status().$suv->get_speed();
$ferrari->get_status().$ferrari->get_speed();
結果 :

self::
vs static::
class Parent_{
protected static $x = "parent";
public static function makeTest(){
echo "self => ".self::$x."\n";
echo "static => ".static::$x."\n\n";
}
}
class Child_ extends Parent_{
protected static $x = "child";
}
echo "using the Parent_ class\n";
Parent_::makeTest();
echo "using the Child_ class\n";
Child_::makeTest();
Output:
using the Parent_ class
self => parent
static => parent
using the Child_ class
self => parent
static => child
self only use Parent’s variable, static can override the parent’s variable
Troubleshootings
Fix for upgrading php 7.2 to 7.4
PHP7.2 treating a non array as an array worked within isset, but in 7.4 it errors with this message:
find . -name "*.php" -exec sed -i '' 's/\(isset(\($[^\[]*\)\[['"'"'|"][^'"'"'"]*['"'"'|"]\])\)/is_array(\2) \&\& \1/' {} \;
will change
$error = isset($body['error']) ? $body['error'] : '';
To
$error = is_array($body) && isset($body['error']) ? $body['error'] : '';
MySQL 回覆 Too many connections 錯誤
發生原因是 request 量太大,並且操作 MySQL 造成 MySQL server 無法處理他同時最大能處理的連線數所導致的
可以查看目前與 MySQL 建立的 connections 數量
SHOW STATUS WHERE `variable_name` = 'Threads_connected';
最多支援的數量
SHOW VARIABLES LIKE 'max_connections';
或查看哪個 process 有建立連線但沒有用到 (Sleep)
SHOW FULL PROCESSLIST;
如何確認並找出 memory leak 問題
用另一個程式一直戳你覺得有問題的 api,邊用 ps 去查看 php-fpm process memory 成長的狀況,
PID %CPU %MEM RSS COMMAND
19269 0.0 1.1 11872 php-fpm-7.0
19271 2.2 2.8 28696 php-fpm-7.0
當 php-fpm 執行時,mem 有增長是很正常的,但執行同一個 api 幾次到一個量後就不太會變動了,如果它一直保持穩定增長,
就表示程式有可能存在 memory leak 問題,這時候我們就要細看問題出在哪裡了。
可以把出問題的 api 分成幾個部份,在不同地方結束,邊戳邊看 memory 的變化,看哪一段會讓 memory 突然的增加,
如果找到在有問題的地方,可以在外層包一個迴圈印出記憶體使用量,如果 GC 後記憶體用量一直增加就能確定有 memory leak 的問題了
while (true) {
// do
gc_collect_cycles();
echo memory_get_usage() . "\n";
usleep(2000000); // wait for 2 seconds
}
Out of memory 錯誤
發生原因
Out of memory (allocated 2097152) (tried to allocate 1052672 bytes) ...略
不知道為什麼明明流量很小但 php-fpm 記憶體卻吃這麼多,後來發現 API 在結束之後,記憶體都不會自動釋放,有可能程式中有 memory leak 的問題,
造成越積越多在 php-fpm 的 process,最後沒記憶體用就 crash 了
當時總記憶體吃到 900 M+,過很長的時間 memory 都沒下降的趨勢
total used free shared buffers cached
Mem: 995 921 73 0 103 123
Restart php-fpm 強迫釋放總記憶體後剩 426M
$ sudo /etc/init.d/php-fpm restart
Stopping php-fpm-7.0: [ OK ]
Starting php-fpm-7.0: [ OK ]
total used free shared buffers cached
Mem: 995 426 568 0 103 123
可以看的出來 php-fpm 吃掉很多記憶體,雖然 restart php-fpm 可以暫時解決問題,但最好的辦法還是讓 php-fpm 自已管理好記憶體
如何解決
方法相當簡單,只需修改幾個 php-fpm 的設定 (/etc/php-fpm-7.0.d/www.conf
) 就可以解決了,詳細可以參考這篇教學,
改成 dynamic (固定數量+彈性增減)
pm = dynamic
pm.max_children = 15
pm.start_servers = 5 // php-fpm 一啟動就會開5個 child process 起來
pm.min_spare_servers = 5 // 最少 5 個
pm.max_spare_servers = 15 // 最多到 15 個
pm.max_requests = 500
或改成 ondemand (有需要才服務)
pm = ondemand // ondemand 是最少 0 個,最多到 max_children 設定的數量
pm.max_children = 15
pm.process_idle_timeout = 10s // 當 child process 閒置 10 秒自動 kill 掉
pm.max_requests = 500
補充說明 :
max_children
要設定幾個取決於你的 php-fpm child process 吃多少記憶體,假如一個 process 吃 20MB,總記憶體有 1G,
假設扣掉系統等等雜7雜8的程序記憶體剩 300,那你的 max_children 就是 300MB/20MB = 15 個,可以設定保守點避免 php-fpm 把記憶體吃光光
max_requests
建議一定要設定這個,它意思是說當 php-fpm 的 child process 處理一個特定數量的 request 後就會把它 kill 再重新產生新的 php-fpm process,
它可以避免你因為 memory leak 問題造成整個 server crash,尤其你引入很多第三方的話你根本不知道什麼時候會出現 memory leak,即使知道了可能當下也沒辦法解,
最保險的方法就是設定這個值,即時有 leak 但到一定 request 數量就會立即釋放讓 process 及記憶體重新來過,而且它只會影響單個 process 不用擔心會有 downtime 問題
後記
原本以為 memory leak 發生在使用 aws sqs.sendMessage,因為除了更新 sdk,也另外單獨把 sqs 拉成一個獨立且單純的 api,並且一直戳它,
memory 如預期一直緩慢增加且不會釋放,以為可以確認是 sqs 導致 memory leak,但是連續做 執行 sqs ->GC->看記億體使用量
,卻又很正常,
判斷應該不是 sqs 造成的 leak,但確實是執行 sqs 會發生 leak 的狀況,或許是 php-fpm 底層有問題?最後實在找不出原因,也沒辦法解決,所以只好繞過這個問題,
得知一個 request 大概會有 20k 的 memory leak,根據前面設定的 pm.max_requests = 500 以及 pm.max_children = 15,
所以至少需要保留 500 * 20k * 15 = 150M 以上的記憶體做緩衝以確保 php-fpm 不會沒記憶體可用