Jex’s Note

PHP

Install php package

sudo apt-get install php5 php5-curl php5-gd php5-imagick imagemagick php5-intl php5-mcrypt php5-memcached php5-mysql php5-json

顯示PHP版本資訊

$ php -v
PHP 5.3.10-1ubuntu3.5 with Suhosin-Patch (cli) (built: Jan 18 2013 23:40:19)
Copyright (c) 1997-2012 The PHP Group
Zend Engine v2.3.0, Copyright (c) 1998-2012 Zend Technologies

檢查語法錯誤

$ 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');
}

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 設定檔路徑

  • 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 傳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] => jex_lin
        )
 )

隱藏錯誤

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

Session 存活時間

php.ini :

session.cookie_lifetime = 0             // The value 0 means "until the browser is closed."
session.gc_maxlifetime = 1440           // session 最多存活時間 (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 不會沒記憶體可用

如何確認並找出 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
}

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;

升級/安裝 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.ini/PHP-FPM 設定

(updated at 2017-08-07)

/etc/php/7.0/fpm/php.ini: cgi.fix_pathinfo 安全性設定

預設為1, 可能會導致安全性問題: www.example.com/1.jpg, 改用www.example.com/1.jpg/xxx.php 會被執行 (此段未試過, 從此篇抄過來)

解法有兩種

1) 將 cgi.fix_pathinfo = 0

2) 調整 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

Comments