แก้ปัญหา Laravel Error บน Appservhosting

พอดีวันนี้เจอปัญหาพิเศษ ที่เกิดขึ้นบน Share Host บางตัว (ในตัวอย่างนี้คือ Appservhosting แต่เชื่อว่าบาง Host ก็น่าจะเป็นเช่นกัน) จากโพสนี้  ซึ่งผมเห็นว่าเป็นเคสที่น่าสนใจมาก เลยเอามาแบ่งปันกันครับ

ปัญหาที่เกิดขึ้นคือ เมื่ออับโหลดขึ้นไปแล้ว Laravel ไม่ทำงาน โดยการตรวจสอบเบื้องต้นคือ

  • ผมได้ตรวจเวอร์ชัน PHP และ PHP Extension ที่ Laravel ต้องการแล้ว ซุึ่งปกติดีครับ
  • Permission ของ storage และ bootstrap/cache สามารถเขียนได้ปกติ

ต่อไปคือข้อบ่งชี้ว่าเป็นสาเหตุเดียวกัน

  • ผมได้ลองแก้ไขไฟล์ index.php และไฟล์อื่น เพื่อใส่ die(‘a’); เข้าไปเรื่อยๆ เพื่อหาว่ามันพังที่บรรทัดไหนกันแน่ แต่ปรากฏว่า เลื่อนบรรทัดไปมา เหมือนบรรทัดที่พังเปลี่ยนไปเรื่อยๆ (อันนี้เงิบมากๆ เพราะผมขยับไป 1 บรรทัดมันพัง เลยลงไปใส่ใน function ที่พัง แต่ผ่านตลอด แล้ววนกลับมา die ที่เดิมมันไม่พังเฉยเลย)
  • Error Logs ไม่มีเขียนอะไรเลย (อาจจะมีเขียน แต่ผมใช้ .htaccess เปลี่ยนที่อยู่ logs มัน เพราะที่เก่าไปอยู่ในที่ๆผมเข้าถึงไม่ได้ เพราะ share host ไม่เปิด)
  • ผมใช้  curl -v url > /dev/null เพื่อดูว่ามันมีอะไรที่ผมไม่เห็นไหม ประกฏว่ามันไม่ได้่ error 500 แต่มันตัด connection ไปดื้อๆ เลย ข้อนี้เลยค่อนข้างเดาได้ว่าน่าจะเป็นที่ระดับ php ไม่ได้เป็นที่ระดับ Code

สรุปผลการคาดเดาของผมคือ น่าจะเป็นปัญหาที่น่าจะเคยเจอกันบ่อยๆคือ OP Cache ของ PHP น่าจะเกิดปัญหา ซึ่งในที่นี้คือ Zend OPcache v7.0.6-dev ซึ่งวิธีแก้ปัญหาคือปิดมันครับ โดยการเปิด .htaccess ขึ้นมา แล้วใส่บรรทัดนี้ลงไปครับ

php_flag opcache.enable Off

โดยในสมัยก่อนนั้น ผมเคยใช้ XCache ก็มีปัญหาคล้ายๆกัน แต่คนละแบบ ของ XCache เมื่อก่อน มันไป Cache routes.php ของคนอื่นมา มันเลยพัง ซึ่งวิธีปิดคล้ายๆกันคือใส่บรรทัดนี้ลงไป .htaccess

php_flag xcache.cacher Off

วิธีทำให้เข้าสู่ระบบ WordPress ใน Domain และ SubDomain ในครั้งเดียว

เกริ่นปัญหา

ปัญหาของเรื่องนี้มาจากโพสนี้ครับ สรุปคือเค้าอยากได้ WordPress 2 ตัวที่ใช้ฐานข้อมูลผู้ใช้อันเดียวกัน และเข้าสู่ระบบที่นึง อีกที่จะเข้าสู่ระบบด้วย โดยหลังจากผมลองค้นหาดู พบว่าสามารถทำได้ แต่ทำได้ในระดับแค่ Sub Domain หรือ Sub Directory เท่านั้นนะครับ และ Database ต้องใช้ Database เดียวกัน (ผมเข้าใจว่าสามารถทำได้ถึงระดับที่อยู่คนละ Database แต่ต้องเป็นที่เดียวกันและใช้ Username/Password เหมือนกัน)

วิธีทำ

  1. ก่อนอื่นให้ลง WordPress ทั้ง 2 ที่แบบปกติให้เรียบร้อยก่อน (ถ้ามีแล้ว ข้ามไปครับ)
  2. ลง Plugins “WP-Orphanage Extended” ให้ WordPress ทั้ง 2 ตัว แล้วไปตั้งค่าใส่ prefix ให้เรียบร้อยครับ
  3. ทำการแก้ไข wp-config.php ของ WordPress ทุกตัวดังนี้ครับ
    <?php
    // แทน xxx ด้วยค่าเดิมนะครับ แต่ทุกไฟล์ที่เป็น wp-config.php ต้องเหมือนกันหมด
    define('AUTH_KEY', 'xxx');
    define('SECURE_AUTH_KEY', 'xxx');
    define('LOGGED_IN_KEY', 'xxx');
    define('NONCE_KEY', 'xxx');
    define('AUTH_SALT', 'xxx');
    define('SECURE_AUTH_SALT', 'xxx');
    define('LOGGED_IN_SALT', 'xxx');
    define('NONCE_SALT', 'xxx');
    
    // ตั้งค่า Cookie ให้ Login แล้วใช้ได้ทุก sub domain 
    // อย่าลิมแก้ .example.com เป็น domain ตัวเอง (ไม่ต้อง www)
    // อย่าลืมจุดหน้า domain มันสำคัญมาก ผมไม่ได้พิมพ์ผิด 
    define('COOKIE_DOMAIN', '.example.com'); 
    define('COOKIEPATH', '/');
    define('SITECOOKIEPATH', '/');
    define('PLUGINS_COOKIE_PATH', '/');
    define('ADMIN_COOKIE_PATH', '/');
    
    // ตั้งชื่อ Cookie ที่ใช้เวลา Login 
    define('AUTH_COOKIE','wordpress_auth_cookie');
    define('SECURE_AUTH_COOKIE','wordpress_auth_cookie');
    
    // บอกว่าจะใช้ User จากตารางไหน 
    define('CUSTOM_USER_TABLE','wp1_users');
    define('CUSTOM_USER_META_TABLE','wp1_usermeta');
  4. ทดสอบได้เลยครับ

ความเห็นส่วนตัว

จริงๆคิดว่าถ้าจะทำแบบนี้น่าจะทำเว็บเดียวแล้วแยก Category ของบทความน่าจะดีกว่า แล้วปรับ Theme เอา โดยใช้ Template Hierarchy หรือ Condition Tags ช่วยก็จะได้ 2 ส่วนที่หน้าตาไม่เหมือนกัน แต่ข้อมูลเหมือนกันแล้ว (ช่วยได้ระดับนึงนะ) หรือไม่ก็แยก Post Type ไเลยก็ได้ครับ

WordPress ช้าเพราะ Really Simple CAPTCHA

อันนี้เอามาจากโพสนี้ ไม่ได้เจอเอง คิดว่าน่าจะแก้ได้แล้วเพราะเจ้าของโพสมากดไลค์ แต่ไม่แจ้งผล

really_simple_captcha_property

ปัญหาที่เกิดคือเค้าบอกว่าเว็บช้ามาก เพราะ Really Simple CAPTCHA และขนาดของ plugin ใหญ่มาก (พวก cache ภาพที่ gen แล้วต่างๆ) ตอนแรกผมไม่เชื่อเท่าไหร่ เพราะผมคิดว่ามันไม่น่าจะช้าได้จากตัวนี้ จนกระทั่ง เค้าใช้ P3 Profiler มาให้ดูจริงๆ ผมก็เลยไปลองไล่ดู Code

ผมเดาว่าสาเหตุนั้นมาจากตอน clean up พวกภาพที่หมดอายุต่างๆ ทำให้มันกิน resource จากตัวอย่างคือ loop อ่าน meta file 20000 กว่ารอบ เพื่อลบไฟล์ที่ไม่ใช้ (ยิ่งถ้าใช้พวก NFS นะ โคตรช้า)

Solution

  1. เปิดไฟล์ really-simple-captcha.php หา function cleanup ให้แก้ไขเป็นดังนี้
    	public function cleanup( $minutes = 60 ) {
    		return 0;
    	}

    (ปิดการทำงานของ clean up เพื่อไม่ให้เว็บช้า เราจะย้ายไปทำใน background แทน)

  2. ให้สร้างไฟล์ชื่อ cleanup.php ข้างๆ really-simple-captcha.php มีเนื้อความดังนี้
    <?php
    $minutes=60;
    $dir = trailingslashit( path_join( dirname( __FILE__ ), 'tmp' ) );
    $dir = str_replace( '\\', '/', $dir );
    $dir = preg_replace( '|/+|', '/', $dir );
     
    if ( ! @is_dir( $dir ) || ! @is_readable( $dir ) )
            return false;
     
    $is_win = ( 'WIN' === strtoupper( substr( PHP_OS, 0, 3 ) ) );
     
    if ( ! ( $is_win ? win_is_writable( $dir ) : @is_writable( $dir ) ) )
            return false;
     
    $count = 0;
     
    if ( $handle = @opendir( $dir ) ) {
            while ( false !== ( $filename = readdir( $handle ) ) ) {
                    if ( ! preg_match( '/^[0-9]+\.(php|txt|png|gif|jpeg)$/', $filename ) )
                            continue;
     
                    $file =  $dir . $filename ;
                    $file = str_replace( '\\', '/', $dir );
                    $file = preg_replace( '|/+|', '/', $dir );
     
                    $stat = @stat( $file );
                    if ( ( $stat['mtime'] + $minutes * 60 ) < time() ) {
                            @unlink( $file );
                            $count += 1;
                    }
            }
     
            closedir( $handle );
    }
     
    ?>

    (เป็นไฟล์สำหรับ clean up แบบ manual)

  3. สร้าง cronjob เพื่อสั่งให้ cleanup.php ทำงาน (หรือเข้ามาสั่งเองทุกวันก็ได้ แต่ถ้าสั่งช้า มันจะกินพื้นที่ HDD เยอะขึ้นเรื่อยๆ)

จบ เอาไปทดสอบได้เลยครับ

บันทึกการซน Thaiware

อันนี้เป็นบันทึกสมัย QA เปิดใหม่ๆนะครับ ผมแจ้ง bug ไปเมื่อ 13 มกราคม 2013 ครับ ผมถือว่านานแล้วเลยเอาออกมาเล่าสู่กันฟัง หลายอย่างกันไปแล้วครับ แต่เอามาแสดงเป็นอุธาหรณ์สำหรับ Programmer ครับ

อันแรกที่เจอ เป็นผลจากการซนของผมครับ ความซนอย่างหนึ่งของผมคือ ถ้ารู้ว่าเป็น custom web ผมจะลองใส่ ‘ or ‘1’=’1 ลงไปครับ ถ้าเป็นหน้า login แล้วผ่านหรือ error ขึ้น ก็ Jackport ครับ เป็น SQL Injection พื้นฐาน ไม่ขออธิบายนะครับ หาตาม Google ได้ทั่วไป จริงๆทำแบบนี้แล้วเกิด memory limit เพราะเค้าไม่ได้ใส่ limit ไว้เลยดึงข้อมูลทั้งหมดมาใส่ ram แต่มันมากเกินไปใส่ไม่หมด วิธีแก้คือใช้ prepare หรืออย่างน้อยใช้ mysql_escape_string ครับ หรือไม่ก็เอา username ไป select อันเดียว แล้วตรวจ password ด้วย php ครับ

อันที่สอง ผมใช้ Dev Console ของ Chrome แก้ Cookie ตัว acc_member_id กับ acc_type ให้เป็นของ admin ได้ (จริงๆได้แค่ดูข้อมูลของ id=1 แต่คิดว่าถ้าลองดีๆ ก็น่าจะได้สิทธิไม่ยาก) ผมคาดว่าเค้าเอา ID จาก Cookie ไปค้นหาข้อมูลตรงๆใน Database เลยก็เป็นได้ ซึ่งไม่ควรทำ โดยทั่วไปเราไม่ควรเชื่อข้อมูลจากฝั่ง client หรือควรมีมาตรการยืนยันมากกว่านี้

อันที่สาม เป็น ผลมาจากครั้งที่ 2 ครับ ผมคาดว่าเค้าใช้ session ยืนยันตัวตนแทนในกรณีที่มี session ผมก็ใช้วิธีเดียวกับวิธีที่ 2 แต่คราวนี้ผมไม่แน่ใจว่าแก้พวก id กับ type ได้ไหม ใน mail ผมไม่ได้มีข้อมูลด้านนี้ แต่มีข้อมูลอีกแบบคือ ผมสามารถใส่ sql injection ลงไปในช่อง acc_member_id แล้วเกิด error memory limit ขึ้น ซึ่งผมเดาว่ามันคงไปดึงข้อมูลทั้งหมดใน DB ออกมาแน่ๆเลย ในตอนนั้นผมแนะนำให้เค้าใช้ mysqli (หรือ PDO) เพื่อให้ใช้ prepare statement เพื่อป้องกัน sql injection ครับ แต่จริงๆผมลืมแนะนำไปว่า ควรใส่ limit 1 ไว้ด้วย เพื่อประสิทธิภาพที่ดีครับ คือถ้าเจอ 1 ตัวแล้วมันจะหยุดหาไม่หาต่อ แล้วก็หากไม่มี session ควรยืนยันตัวตนผ่านวิธีอื่นไม่ใช่เอา id อย่างเดียวไปหาตรงๆ อย่างเช่นเอา pass มาก md5 หลายๆชั้นใส่ cookie แล้วเวลายืนยันก็ไป select id ออกมา แล้วเอา pass มาทำด้วยวิธีเดียวกันถ้าตรงกับ pass ที่เข้ารหัสใน cookie ค่อยเอาข้อมูลใส่ session [ref to mysqli.prepare]

อันสุดท้าย [สถานะวันที่ 22/11/13 คือยังไม่ได้รับการแก้ไข]  ดังนั้นผมยังไม่บอกไรมาก บอกได้แค่ XSS จ้า

mysql_connect กับ mysql_pconnect ต่างกันยังไง

mysql_connect กับ mysql_pconnect ต่างกันยังไง

  • mysql_connect จะปิด Connection หลังจากจบการทำงานของ script นั้นทันที(หรือมีถ่วงเวลาไว้แปปนึง) ทำให้ไม่มี process ค้างใน mysql เยอะนัก
  • mysql_pconnect จะเปิด Connection ค้างไว้ หากมีการเชื่อมต่อที่ Mysql Database ตัวนี้อีก จะใช้ Connection เดิมแทนที่จะต่อ Connection ใหม่

เปรียบเทียบข้อดี

  • mysql_connect จะมี Process ค้างใน mysql น้อยกว่า คือคืน memory ทันทีที่จบการทำงาน
  • mysql_pconnect จะเชื่อมต่อได้รวดเร็วกว่า แต่มี Process ต้างใน mysql อยู่ทำให้กิน memory มากกว่า

เปรียบเทียบข้อเสีย

  • mysql_connect จะใช้เวลาการเชื่อมต่อนานกว่าทำให้ช้ากว่า
  • mysql_pconnect มี Process ใน mysql มากกว่า ทำให้กิน ram มากกว่า

ฟันธงว่าควรใช้อะไรดี

อัน นี้ขึ้นอยู่กับงาน หากเป็นงานที่ต้องการ Connection เยอะมากๆแนะนำ mysql_pconnect (จริงๆ ถ้าไม่ได้ใช้ Share Host หรือมี Database แค่ตัวเดียว หรือจำนวนน้อยใน Server นั้นใช้ไปเลยก็ไม่เสียหายครับ) แต่ถ้าเป็น Shared Host หรือ มี Database จำนวนมากก็ใช้ mysql_connect ปกติไปก็ได้ครับ

ผลทดสอบความเร็ว

ของ mysql_connect

Code:

<?php
mysql_connect("127.0.0.1","root","");
mysql_select_db("test");

mysql_close();
?>

ผล:

mix5003@Shana:~$ ab -k -c 200 -t 10 http://127.0.0.1/benc/connect/connect.php
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 127.0.0.1 (be patient)
Completed 5000 requests
Completed 10000 requests
Completed 15000 requests
Completed 20000 requests
Completed 25000 requests
Finished 27160 requests


Server Software:        Apache/2.4.2
Server Hostname:        127.0.0.1
Server Port:            80

Document Path:          /benc/connect/connect.php
Document Length:        0 bytes

Concurrency Level:      200
Time taken for tests:   10.000 seconds
Complete requests:      27160
Failed requests:        0
Write errors:           0
Keep-Alive requests:    26939
Total transferred:      6345842 bytes
HTML transferred:       0 bytes
Requests per second:    2715.98 [#/sec] (mean)
Time per request:       73.638 [ms] (mean)
Time per request:       0.368 [ms] (mean, across all concurrent requests)
Transfer rate:          619.71 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0  10.5      0    1000
Processing:     1   69 539.0      9    9877
Waiting:        1   69 539.0      9    9876
Total:          1   69 540.1      9    9880

Percentage of the requests served within a certain time (ms)
  50%      9
  66%     19
  75%     27
  80%     32
  90%     44
  95%     57
  98%     98
  99%   2445
 100%   9880 (longest request)

ผลการทดสอบ mysql_connect จะเห็นได้ว่ากดราฟพุ่งสูงสุดที่ 8000 Connections เลยทีเดียว

จะเห็นว่ากราฟพุ่งสูงสุดที่ 8000 Connection (จริงๆน่าจะเป็น 4000 นะครับ แต่เวลานั้น เครื่องผมกาก refresh กราฟไม่ได้ 1 ทีครับ) และต่อได้ 2715 ครั้งต่อ 1 วินาทีเลยทีเดียว (Concurrent หรือจำนวนการเชื่อมต่อพร้อมกันคือ 200 นะครับ)

mysql_pconnect

Code:

<?php
mysql_pconnect("127.0.0.1","root","");
mysql_select_db("test");

mysql_close();
?>

ผล:

mix5003@Shana:~$ ab -k -c 200 -t 10 http://127.0.0.1/benc/connect/pconnect.php
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 127.0.0.1 (be patient)
Completed 5000 requests
Completed 10000 requests
Completed 15000 requests
Completed 20000 requests
Completed 25000 requests
Completed 30000 requests
Completed 35000 requests
Completed 40000 requests
Completed 45000 requests
Finished 46890 requests


Server Software:        Apache/2.4.2
Server Hostname:        127.0.0.1
Server Port:            80

Document Path:          /benc/connect/pconnect.php
Document Length:        0 bytes

Concurrency Level:      200
Time taken for tests:   10.000 seconds
Complete requests:      46890
Failed requests:        0
Write errors:           0
Keep-Alive requests:    46479
Total transferred:      10954242 bytes
HTML transferred:       0 bytes
Requests per second:    4688.95 [#/sec] (mean)
Time per request:       42.653 [ms] (mean)
Time per request:       0.213 [ms] (mean, across all concurrent requests)
Transfer rate:          1069.74 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   9.2      0     999
Processing:     1   41 362.9      5    8905
Waiting:        1   41 362.9      5    8905
Total:          1   41 363.8      5    8909

Percentage of the requests served within a certain time (ms)
  50%      5
  66%     10
  75%     14
  80%     17
  90%     26
  95%     35
  98%     79
  99%   1271
 100%   8909 (longest request)

ผลการทดสอบ mysql_pconnect จะเห็นได้ว่ากดราฟพุ่งสูงสุดที่ประมาณ 130 Connections เอง

จะเห็นว่ากราฟพุ่งสูงสุดที่ 130 Connection  และต่อได้ 4688 ครั้งต่อ 1 วินาทีเลยทีเดียว (Concurrent หรือจำนวนการเชื่อมต่อพร้อมกันคือ 200 นะครับ) จะเห็นได้ว่าเร็วกว่า mysql_connect เกือบ 2 เท่าเลยทีเดียว(ขี้เกียจคำนวน ประมาณเอาแล้วกัน 555)