วันอังคารที่ 25 กันยายน พ.ศ. 2555

~/.ssh/config

เทคนิคการใช้ไฟล์ config ของ ssh ซึ่งเป็นไฟล์ค่าปรับแต่งสำหรับ ssh client บน Linux ซึ่งปกติเราจะระบุค่าต่างๆ ด้วย option ท้ายคำสั่ง ซึ่งบางครั้ง โฮสต์ปลายทางคนละโฮสต์ก็ต้องใช้ option ต่างกัน โดยเฉพาะ port ซึ่งมีหลายๆ โฮสต์ที่อาจจะเปลี่ยนหมายเลข port เป็นตัวอื่น ไม่ใช่ 22

ให้สร้างไฟล์ ~/.ssh/config ขึ้นมา (ปกติไม่มีไฟล์นี้อยู่)

ใส่เนื้อหา
Host *
Protocol 2
ServerAliveInterval 300
ServerAliveCountMax 100

เป็นการตั้งค่าแบบทั่วไป คือเวลาติดต่อกับทุก host ให้ใช้ protocol version 2 เท่านั้น และกำหนดให้ส่งแพกเกจเพื่อกระตุ้นให้มีการเชื่อมต่อทุก 5 นาที (300 วินาที) และส่งเป็นจำนวนสูงสุด 100 ครั้ง แสดงว่า ssh ที่เชื่อมจะไม่หลุดเป็นเวลา 500 นาที (8 ชั่วโมง 20 นาที) ถ้าไม่ตั้งแบบนี้บ่อยครั้ง ssh จะหลุดเร็วเกินไป

Host terminus
HostName terminus.abcdef.com
Port 18222
User ubuntu

แบบนี้เป็นการตั้งค่าเฉพาะ host โดยจะกำหนดชื่อ host สำหรับใช้อ้างอิง เช่น terminus, แล้วระบุที่อยู่แบบเต็ม เช่น terminus.abcdef.com, กำหนด port เชื่อมต่อปลายทางเป็น 18222 ปกติคือ 22 ซึ่งไม่ระบุก็ได้เพราะเป็นค่าปริยาย แต่บางเซิร์ฟเวอร์อาจจะเลี่ยงมาใช้พอร์ตอื่นเพื่อลดการโจมตีแบบ portscan แล้ว attack ที่พอร์ต ssh, ระบุชื่อ user เป็น ubuntu ถ้าไม่ระบุมันจะใช้ชื่อ user เราที่เครื่อง local เอง หรือระบุที่ command line

ถ้ามีโฮสต์อื่นก็เพิ่มเข้าไปทำนองเดียวกัน

การตั้งค่าอื่นๆ มีอีกมาก ดูได้จากคำสั่ง
$ man ssh_config

ถ้าเป็นเครื่องที่ใช้ร่วมกันหลายๆ คนหรือมีบริการอื่นรันอยู่เพื่อให้บริการบุคคลภายนอกด้วย หรือไม่แน่ใจ ควรเปลี่ยนสิทธิของไฟล์นี้ให้อ่านเขียนได้เฉพาะ user เราเองเท่านั้น โดยสั่ง
$ chmod go-rw ~/.ssh/config
$ ls -l ~/.ssh/config
-rw------- 1 kamthorn kamthorn 381 Aug 15 12:31 /home/kamthorn/.ssh/config


การใช้งาน
เดิมเราต้องสั่งด้วย
$ ssh -p 18222 ubuntu@terminus.abcdef.com

พอเราตั้ง config ไว้แล้วก็แค่
$ ssh terminus

เวลาจะ scp หรือ rsync ผ่าน ssh ก็จะง่ายขึ้น ไม่ต้องระบุพอร์ตอีกต่อไป

วันอังคารที่ 26 มิถุนายน พ.ศ. 2555

10 ซอฟต์แวร์เพื่อบริการ web site สำหรับทุกวันนี้


Cores แกนหลักๆ ซึ่งก็คือ LAMP นั่นเอง

  1. Linux ซึ่งปกติใช้ Debian ตอนนี้รุ่น stable คือ squeeze (6.0) บนสถาปัตยกรรม amd64 คงไม่มีใครใช้ 32 bit นะ
  2. Apache httpd โดยใช้ MPM แบบ worker แม้ว่าจะมีคู่แข่งอย่าง lighttpd หรือ nginx แต่ apache httpd มีฟีเจอร์มากมายและตั้งค่าได้ง่าย ในเรื่องความเร็วในการให้บริการเว็บแบบ dynamic content ด้วย PHP ไม่ได้ต่างกันมาก
  3. MySQL จริงๆ คือ database server สักตัว แต่ส่วนใหญ่ developer ยังถนัดจะใช้ mysql กันอยู่ ก็ตามนั้นไปก่อน ทางเลือกอื่นๆ ก็มี percona ซึ่งเป็น mysql clone, postgreSQL หรือจะ NoSQL ก็ mongodb, couchdb เป็นต้น
  4. PHP ตอนนี้ใช้รุ่น 5.3 อยู่ แต่รันแบบ fpm (php5-fpm) ตัวนี้ยังไม่มีบน squeeze แต่สามารถลงได้จาก repo ของ dotdeb.org เป็นตัวที่ รัน php5 ในโหมด fastcgi ซึ่งมีระบบบริหารจัดการโพรเซสด้วย ต่างจาก php5-cgi ที่แม้ว่าจะรันเป็น fastcgi ได้แต่ชอบมีปัญหา process ค้าง

Extensions ส่วนขยายที่ทำให้ระบบทำงานได้ดีขึ้น

  1. varnish ตัวนี้เป็น reverse proxy สำหรับรับ connection ที่ port 80 ก่อนจะส่งต่อให้ apache จุดเด่นคือ ทำ caching ได้, มี apache เป็น backend หลายๆ เครื่องได้ คือทำ load balance ได้นั่นเอง, เขียน script เพื่อจัดการกับภาระกิจบางอย่างได้ เช่น backend เรียกไปแล้ว error 503  กลับมา ก็สั่งให้เรียกไปใหม่ หรือให้แสดงหน้ารอ แล้ว refresh ใหม่ เป็นต้น (503 มักเกิดจาก backend ไม่พร้อมให้บริการในขณะนั้น ส่วนใหญ่รอสักพักแล้ว refresh ก็ใช้ได้ตามปกติ) ปัญหาหนึ่งของการใช้ varnish + apache คือข้อมูลล็อกของ apache จะเก็บ client IP เป็น IP ของ varnish เอง ไม่ใช่ของ client ที่แท้จริง แก้โดยใช้ libapache2-mod-rpaf
  2. couchbase ใช้ทำหน้าที่เป็น memcache โดยใช้สำหรับ 1. เก็บ php session data 2. ใช้เป็น memcache สำหรับเก็บข้อมูล cache ชั่วคราว โดยไม่ต้อง query จาก database บ่อยๆ (ต้องเขียนแอปให้รองรับการใช้ memcache ด้วย) เดิมใช้ memcached แต่ couchbase ดีกว่าตรงที่ สามารถทำงานแบบ cluster คือมีหลายๆ เครื่องช่วยกันทำงานได้ สามารถเก็บลงดิสก์ได้ มีหน้าเว็บสำหรับ monitor และตั้งค่าต่างๆ ได้
  3. ufw การกำหนดค่า firewall ด้วย iptables (netfilter) นั้นสำหรับผมเองถือว่ายุ่งยากมาก โดยเฉพาะเวลาที่ต้องกลับมาแก้หรือปรับบางค่าใหม่ พอมาลอง ufw แล้วสะดวกสบายขึ้นมาก

Luxuries ไม่ได้ทำให้ระบบทำงานดีขึ้น แต่ช่วยสนับสนุนในการติดตามเฝ้าดูระบบ

  1. munin เอาไว้เก็บข้อมูลการทำงานของระบบไว้ตรวจสอบ โดยมันจะสร้างเป็นชุดของแผนภูมิให้ดูง่ายๆ และสวยงามเลยทีเดียว สามารถใช้เก็บข้อมูลจากหลายๆ เครื่องได้โดยติดตั้งเฉพาะแพกเกจชื่อ munin-node แล้วให้ munin ตัวหลักไปเรียกเอาข้อมูลมาประมวลเป็นแผนภูมิ
  2. monit เอาไว้คอย monitor ระบบและจัดการแจ้งเตือน หรือรันคำสั่งที่ตั้งไว้ เมื่อตรวจพบเหตุบางอย่าง เช่นเปิดหน้าเว็บที่กำหนดไว้ไม่ได้ ดิสก์เหลือน้อย พบแพทเทิร์นล็อกที่กำหนดไว้ เป็นต้น
  3. webalizer เอาไว้วิเคราะห์ล็อกของ web server แสดงเป็นรูปแผนภูมิสวยงาม ดูง่าย และน่าจะคุ้นเคยกันดี

วันพฤหัสบดีที่ 7 มิถุนายน พ.ศ. 2555

การเซ็ต IPv6 แบบ 6to4 ให้ server

รับกระแส World IPv6 Launch Day ที่เพิ่งมีไปเมื่อวาน (6/6/2012) ซึ่งแปลว่าต่อจากนี้จะเริ่มมีการให้บริการ IPv6 ขนานไปกับ IPv4 มากขึ้น ผู้ที่ต้องตื่นตัวก่อนคือผู้ให้บริการต่างๆ บน Internet นั่นเอง ที่ต้องเตรียมพร้อมรับมือกับมัน (อย่างน้อยก็แสดงให้เห็นว่าเราไม่ตกยุค) ส่วนรายละเอียดว่าทำไมเราต้องเปลี่ยนจาก IPv4 มาเป็น IPv6 ขอให้อ่านจาก http://www.blognone.com/topics/ipv6 นะครับ ในที่นี้ขอพูดถึงประเด็นเดียวคือ เมื่อเรามี server ที่ให้บริการบนเครือข่าย IPv4 อยู่ จะทำอย่างไรให้สามารถบริการ IPv6 ได้
ถ้าเป็นก่อนนี้จะวุ่นวายน่าดูเพราะต้องทำ tunnel ไปยัง IPv6 broker ต่างๆ แต่ตอนนี้มีเทคนิคที่เรียกว่า 6to4 โดยให้ IPv6 encapsulate ไปกับ IPv4 เลย โดยใช้ protocol 41 ซึ่งการตั้งค่าจะเป็นแบบอัตโนมัติ คือไม่ต้องไปหา tunnel ให้วุ่นวาย โดยเมื่อเรามี IPv4 แล้ว เราจะได้รับการจอง subnet ของ IPv6 ไว้ช่วงหนึ่งโดยอัตโนมัติ

สิ่งที่ต้องเตรียม

  • server ที่มี IP จริง (global IP) IP แบบ private ใช้ไม่ได้ อ่อ dynamic IP ก็ไม่ได้นะครับ
  • โปรแกรม ipv6calc (ติดตั้งบน debian ได้โดยสั่ง apt-get install ipv6calc)

วิธีการ

  • แปลงที่อยู่ IPv4 เป็น IPv6 ด้วย ipv6calc โดยสมมติว่า IPv4 คือ 202.183.164.17
    # ipv6calc --quiet --action conv6to4 202.183.164.17
    2002:cab7:a411::
  • แก้ไขไฟล์ /etc/network/interfaces โดยเพิ่มเนื้อหาตามนี้เข้าไป
    auto tun6to4
    iface tun6to4 inet6 v4tunnel
            address 2002:cab7:a411::1
            netmask 16
            gateway ::192.88.99.1
            endpoint any
            local 202.183.164.17
    ตรง 2002:cab7:a411::1 มาจาก ipv6calc โดยเพิ่มเลข 1 ต่อท้ายเข้าไป ส่วน local 202.183.164.17 ก็คือ IPv4 ปกติของ server เรา และ gateway ::192.88.99.1 ไม่ต้องแก้นะครับ จะเป็น anycast address สำหรับรับส่ง packet กับ 6to4 relay router ที่ใกล้ที่สุดซึ่งจะมีกระจายอยู่ทั่วโลก
  • เริ่มใช้งานโดยสั่ง
    # ifup tun6to4
  • ทดสอบโดยสั่ง
    # ping6 google.com
  • ถ้าใช้ไม่ได้อาจจะเกิดจาก firewall ในเครื่องเราเองที่ปกติจะไม่รับ protocol 41 ให้แก้โดยสั่ง
    # iptables -I INPUT -p 41 -j ACCEPT
    # iptables -I OUTPUT -p 41 -j ACCEPT
    แล้วลองทดสอบใหม่ ถ้าใช้ได้ก็เอาไปเพิ่มใน firewall script ของเรา เช่นผมใช้ ufw ก็เพิ่ม -I INPUT -p 41 -j ACCEPT และ -I OUTPUT -p 41 -j ACCEPT ไว้ที่ /etc/ufw/before.rules ก่อนหน้าบรรทัด COMMIT
  • แต่ถ้าใช้ไม่ได้เพราะ firewall ของ NOC ไม่ forward protocol 41 มาให้ ก็ต้องคุยกับผู้ให้บริการให้เปิดรับ protocol 41 ด้วย ปกติไม่เคยเจอปัญหานี้ที่ผู้ให้บริการทั่วไป แต่ถ้าภายในองค์กรอาจจะมีปัญหานี้ได้
  • ทดสอบการใช้บริการต่างๆ ผ่าน IPv6 ในที่นี้เราต้องใช้ IPv6 จากเครื่อง client ได้ก่อน
    เช่น เปิด browser ไปยัง url http://[2002:cab7:a411::1] หรือ ssh 2002:cab7:a411::1
  • ตรวจสอบบริการต่างๆ ให้รองรับ IPv6 ตรวจสอบการตั้งค่า firewall ให้รองรับ IPv6
  • ถ้ามั่นใจแล้วก็เพิ่ม record AAAA 2002:cab7:a411::1 ไปที่ DNS ของเราได้เลย เพื่อให้ผู้ใช้ที่ใช้ IPv6 ได้ สามารถติดต่อกับ site ของเราผ่าน IPv6 ได้โดยตรง
 ดูเพิ่มเติม : http://wiki.debian.org/DebianIPv6

วันอังคารที่ 13 กันยายน พ.ศ. 2554

MySQL Query Optimization 2 : เครื่องมือช่วย

เป็นแบบนี้ไหมครับ รู้ว่าปัญหาอยู่ที่ MySQL นี่แหละ แต่เป็นตรงไหนล่ะ เรามาดูเครื่องมือช่วยกันดีกว่า

mysqltuner
ตัวนี้สำหรับ admin โดยมันจะวิเคราะห์ข้อมูลต่างๆ ของ MySQL server ในขณะนั้นแล้วแนะนำว่าควรปรับค่าอะไร อย่างไร อันเนื่องมาจากว่าไม่มีสูตรตายตัวว่าควรปรับแต่งอย่างไร ซึ่งขึ้นอยู่กับข้อมูล และการใช้งานจริงด้วย

การติดตั้ง
# wget mysqltuner.pl -O mysqltuner.pl

เป็นการติดตั้งที่เท่มาก คือตัวนี้เป็น perl script ซึ่งปกติมันจะใช้ extension เป็น .pl เค้าเลยไปจดชื่อ domain เป็น mysqltuner.pl ไว้ คำสั่งนี้จึงหมายถึง ดาวน์โหลดหน้าเว็บหลักของ http://mysqltuner.pl/ ซึ่งมันจะให้ข้อมูลเป็น script รุ่นล่าสุดเสมอ เอามาบันทึกไว้ในชื่อ mysqltuner.pl

จากนั้นกำหนดให้ execute ได้
# chmod +x mysqltuner.pl

การใช้ สั่ง ./mysqltuner.pl แล้วใส่ชื่อแอดมินของ MySQL ปกติคือ root และรหัสผ่าน ก็จะได้ผลลัพธ์คล้ายๆ แบบนี้


# ./mysqltuner.pl


 >>  MySQLTuner 1.2.0 - Major Hayden <major@mhtx.net>
 >>  Bug reports, feature requests, and downloads at http://mysqltuner.com/
 >>  Run with '--help' for additional options and output filtering
Please enter your MySQL administrative login: root
Please enter your MySQL administrative password: 


-------- General Statistics --------------------------------------------------
[--] Skipped version check for MySQLTuner script
[OK] Currently running supported MySQL version 5.1.58-1~dotdeb.1-log
[OK] Operating on 64-bit architecture


-------- Storage Engine Statistics -------------------------------------------
[--] Status: +Archive -BDB -Federated -InnoDB -ISAM -NDBCluster 
[--] Data in MyISAM tables: 30G (Tables: 670)
[--] Data in MEMORY tables: 45M (Tables: 8)
[!!] Total fragmented tables: 130


-------- Security Recommendations  -------------------------------------------
[OK] All database users have passwords assigned


-------- Performance Metrics -------------------------------------------------
[--] Up for: 3d 7h 10m 37s (43M q [153.856 qps], 1M conn, TX: 20B, RX: 8B)
[--] Reads / Writes: 49% / 51%
[--] Total buffers: 1.6G global + 8.4M per thread (500 max threads)
[OK] Maximum possible memory usage: 5.7G (73% of installed RAM)
[OK] Slow queries: 3% (1M/43M)
[OK] Highest usage of available connections: 36% (180/500)
[OK] Key buffer size / total MyISAM indexes: 1000.0M/19.4G
[OK] Key buffer hit rate: 99.9% (2B cached / 4M reads)
[!!] Query cache efficiency: 19.2% (3M cached / 17M selects)
[OK] Query cache prunes per day: 0
[OK] Sorts requiring temporary tables: 0% (162 temp sorts / 1M sorts)
[OK] Temporary tables created on disk: 0% (52 on disk / 734K total)
[OK] Thread cache hit rate: 64% (686K created / 1M connections)
[!!] Table cache hit rate: 19% (1K open / 9K opened)
[OK] Open file limit used: 55% (16K/30K)
[OK] Table locks acquired immediately: 97% (224M immediate / 231M locks)


-------- Recommendations -----------------------------------------------------
General recommendations:
    Run OPTIMIZE TABLE to defragment tables for better performance
    Increase table_cache gradually to avoid file descriptor limits
Variables to adjust:
    query_cache_limit (> 16M, or use smaller result sets)
    table_cache (> 1800)

ผลลัพธ์ก็อธิบายในตัวเองค่อนข้างชัดเจน บรรทัดที่นำหน้าด้วย [OK] ก็คือดีแล้ว ไม่มีปัญหาอะไร นำหน้าด้วย [--] คือเป็นข้อมูลแจ้งให้ทราบ ส่วน [!!] อันนี้ไม่ดีละ มีบางอย่างควรต้องพิจารณา ซึ่งจะมีสรุปด้านล่างว่าควรทำอะไร หรือปรับค่าตัวแปรอะไร เพื่อให้ประสิทธิภาพดีขึ้น แต่ไม่ต้องเชื่อมันทุกอย่างก็ได้ ฟังไว้เป็นข้อมูลประกอบก็พอ

ดูเพิ่มเติม http://mysqltuner.com/

mysql_slow_log_parser
ตัวนี้เหมาะสำหรับ developer ไว้วิเคราะห์ว่า query ใดเป็นตัวปัญหา โดยปกติเราสามารถกำหนดให้ MySQL บันทึก log ของ slow query ที่เกิดขึ้น แต่ปัญหาคือมันเยอะมากจนไม่รู้จะเริ่มดูตรงไหนก่อน ตัวนี้จะช่วยเอา slow log ทั้งหมดมารวบรวม จัดหมวดหมู่คำสั่งที่คล้ายกัน นำจำนวน รวมเวลาที่ใช้ทั้งหมด เวลาน้อยสุด เวลามากสุด เวลาเฉลี่ย แล้วเรียงลำดับตามเวลา query ทั้งหมดที่ใช้ไปจากมากไปน้อย

การติดตั้ง
ก่อนอื่นให้ตั้งค่าของ MySQL server ให้เก็บ slow log query ก่อน โดยแก้ไฟล์ /etc/mysql/my.cnf แล้ว uncomment และแก้บรรทัด 2 บรรทัดนี้

log_slow_queries        = /var/log/mysql/mysql-slow.log
long_query_time = 0

สังเกตว่าเราจะกำหนด long_query_time เป็น 0 นั่นคือให้เก็บ log ทุก query ไว้หมด เพื่อนำมาวิเคราะห์ จากนั้น restart mysql ด้วยคำสั่ง
# /etc/init.d/mysql restart

ดาวน์โหลด script mysql_slow_log_parser
# chmod +x mysql_slow_log_parser

การใช้งาน
ถ้าเป็นล็อกที่เกิดในวันนี้ คือหลังจากที่ logrotate ทำงาน ให้สั่ง
# ./mysql_slow_log_parser /var/log/mysql/mysql-slow.log > mysql-slow-info.txt

ดูผลที่เก็บในไฟล์
# less mysql-slow-info.txt

ถ้าเป็นล็อกที่เกิดขึ้นในวันก่อนๆ ซึ่งถูก rotate และ compress ไปแล้ว
# zcat /var/log/mysql/mysql-slow.log.1.gz | ./mysql_slow_log_parser > mysql-slow-info.1.txt

ดูผลที่เก็บในไฟล์
# less mysql-slow-info.1.txt

ตัวอย่างไฟล์ mysql-slow.log
# User@Host: xxx[xxx] @ localhost [127.0.0.1]
# Query_time: 0.004879  Lock_time: 0.000031 Rows_sent: 1  Rows_examined: 2313
SET timestamp=1315178855;
SELECT COUNT(*) AS numRows FROM group_topics WHERE group_id=167 AND status='NORMAL';
# Time: 110905  6:27:37
# User@Host: xxx[xxx] @ localhost [127.0.0.1]
# Query_time: 0.182972  Lock_time: 0.000036 Rows_sent: 1  Rows_examined: 104030
SET timestamp=1315178857;
SELECT member_id  FROM member  WHERE LOWER(user_id)='xxxxx'    AND is_active='1'  LIMIT 1;

ตัวอย่างผลลัพธ์ หลังจากผ่าน parser
 Starting... 
### 53018 Queries 
### Total time: 204742.585691, Average time: 3.86175611473462
### Taking 0.000740  to 81.100860  seconds to complete
### Rows analyzed 100 - 1467
SET timestamp=XXX;
SELECT COUNT(*) AS total  FROM chat_msgs  WHERE owner_id=XXX AND is_unread='XXX';

SET timestamp=1315178847;
SELECT COUNT(*) AS total  FROM chat_msgs  WHERE owner_id=104170 AND is_unread='1';


### 29081 Queries 
### Total time: 90838.2220840001, Average time: 3.12362786988068
### Taking 0.000353  to 82.103217  seconds to complete
### Rows analyzed 100 - 4387
SET timestamp=XXX;
SELECT friend_id  FROM relation  WHERE member_id=XXX AND status='XXX';

SET timestamp=1315178835;
SELECT friend_id  FROM relation  WHERE member_id=235929 AND status='ACCEPT';

สังเกตว่ามันจะแปลงส่วน value ทั้ง string และตัวเลข ให้เป็น XXX ก่อน แล้วนำมาแยกนับ วิเคราะห์คำสั่งที่เหมือนกัน ก็จะทำให้ทราบว่าคำสั่งใดถูกใช้บ่อยแค่ไหน (กี่ query) ใช้เวลารวมทั้งหมดเท่าไหร่ (Total time:) ใช้เวลาเฉลี่ยต่อ query เท่าไหร่ (Average time:) ใช้เวลาน้อยสุดถึงมากสุดเท่าไหร่ (Taking ... to ... seconds to complete) จำนวน rows ของผลลัพธ์ โดยมันจะแสดง query ที่ใช้เวลารวมมากที่สุดก่อน ซึ่งถ้า optimize ได้ก็จะมีผลมากที่สุด

อ่อ ที่เห็นผลลัพธ์อันนี้ดูแย่ๆ คือ average สูง และช่วงเวลาที่ใช้ต่ำสุด - สูงสุดกว้างขนาดนี้ เพราะมีปัญหาเรื่อง capacity ของเครื่องไม่พอน่ะครับ จึงเร็วบ้าง ช้าบ้าง ไม่แน่ไม่นอน

วันอาทิตย์ที่ 4 กันยายน พ.ศ. 2554

MySQL Query Optimization 1


มีเหตุให้ต้องได้ช่วยฝ่ายอื่นแก้ปัญหาเรื่องประสิทธิภาพของเซิร์ฟเวอร์ลินุกซ์เลยขอบันทึกไว้หน่อยว่าเจออะไรบ้าง

เซิร์ฟเวอร์ให้บริการเว็บ ใช้ apache + php5 + mysql ตามปกติ อาการแรกที่เห็นคือเซิร์ฟเวอร์ไม่ตอบสนอง ถึงขนาด ssh เข้าไปยังต้องรอนานมาก และ timeout ไปหลายรอบกว่าจะเข้าได้ อาการนี้มักเกิดจากหน่วยความจำเต็ม จนต้อง swap หน่วยความจำบางส่วนลงดิสก์ ซึ่งบังเอิญหน่วยความจำที่มัน active อยู่ มันเกินหน่วยความจำจริง เลยเกิดการ swap ตลอดเวลา จน process ต่างๆ แทบจะทำงานอะไรไม่ได้ เพราะต้องรอหน่วยความจำจากดิสก์ก่อน

ทำไมหน่วยความจำจึงไม่พอ
เซิร์ฟเวอร์ตัวนี้ มีหน่วยความจำ 3GB ไม่ได้มากนัก แต่ก็ไม่น้อยเกินไป สาเหตุที่หน่วยความจำเต็มเกิดจาก apache httpd เกิดการแบ่งตัวเพื่อรับภาระงานจนเกินขนาดของหน่วยความจำ ซึ่งปกติเวลามี connection เข้ามา 1 อัน ต้องใช้ httpd 1 process ในการรับงาน แต่ละ process มีขนาดประมาณ 9-12MB กำหนดไว้สูงสุด 250 client ซึ่งเช็คแล้วพบว่าเต็ม 250 เลย จับคูณกันแล้วก็ราวๆ 3GB พอดี ยังไม่รวม mysql  และอื่นๆ
การเพิ่มหน่วยความจำไม่ใช่ทางออกที่ถูกต้อง จริงๆ หน่วยความจำเยอะๆ ดี แต่มันจะไม่ช่วยในกรณีนี้ เพราะเทียบได้กับการให้บริการอะไรสักอย่างของหน่วยงานหนึ่ง แล้วพบว่าที่นั่งรอไม่พอ เลยขยายห้องรอเป็นสองเท่า เพื่อให้คนเข้ามารอได้เป็นสองเท่า จริงๆ ต้องไปแก้ที่ต้นเหตุคือทำไมการให้บริการของหน่วยงานนั้นๆ ถึงได้ช้า

ทำไม apache httpd ถึงช้าจนเกิด connection ค้างมากถึง 250 อัน
ส่วนใหญ่ (รวมทั้งกรณีนี้ด้วย) คือเกิดสภาวะรอ mysql ตอบ query ที่เรียกไป

แล้วทำไม mysql มันช้านัก
ในหลายกรณี (รวมทั้งกรณีนี้ด้วย) คือการเรียกใช้ query ไม่ได้ถูก optimize ให้ทำงานได้ดีที่สุด

การตรวจสอบว่า query ใดทำให้ mysql ช้า
ให้เปิดใช้ฟีเจอร์เก็บล็อกของ query ที่ช้าโดยเพิ่ม
log_slow_queries = /var/log/mysql-slow.log
long_query_time = 5
เข้าไปใน section [mysqld] ในไฟล์ my.cnf ซึ่งน่าจะอยู่ใน /etc/mysql หรือ /etc
แล้ว restart mysql แล้วตามเฝ้าดูในไฟล์ /var/log/mysql-slow.log

หลักการ optimize query
อันแรกสุดคือลดการเกิด full table scan ให้มากที่สุด full table scan คือการที่จะหาคำตอบของ query จำเป็นต้อง scan อ่านทั้งตาราง ซึ่งจะช้ามาก และช้าขึ้นเรื่อยๆ เมื่อตารางใหญ่ขึ้น การลดอาการนี้อันแรกคือ ให้ทำ index ที่จำเป็นต้องใช้บ่อยๆ เสมอ โดยให้ดูที่ ORDER BY กับ WHERE ว่ามีการอ้างถึง column ใดบ้าง อีกอันที่เจอคือบางคนใช้ WHERE sss LIKE '%xxxx%' ในการเปรียบเทียบ string ทั้งๆ ที่ xxxx นั่นคือทั้งหมดที่อยู่ใน column นั้นๆ อยู่แล้ว การใช้ %xxxx% ทำให้เกิดการค้นหาตลอดตารางเพื่อหาทุกแถวที่เป็นไปได้ ที่จริงแล้วในกรณีนี้ใช้แค่ WHERE sss = 'xxxx' ก็ได้
อีกอันคือให้หลีกเลี่ยงการ JOIN ให้มากที่สุด อาจจะลองเลี่ยงไปใช้ sub query ก็ช่วยได้พอสมควร

ตัวอย่างการ optimize query

ของเดิม
SELECT topic.ID, topic.Title, topic.Description, topic.Publisher_Name, topic.Upload_Date
FROM topic
  LEFT JOIN topic_category ON ( topic.ID = topic_category.Topic_ID)
  LEFT JOIN topic_media ON (topic.ID = topic_media.Topic_ID)
WHERE
  topic.Status = 1
  AND topic_media.Status = 1
  AND topic.Approve_State = 1
  AND topic_media.Type like '%image%'
GROUP BY
  topic.ID
ORDER BY
  topic.ID DESC
LIMIT 65625, 25;
ใช้เวลา 13.20 วินาที

เปลี่ยน like '%...%' เป็น = '...'
SELECT topic.ID, topic.Title, topic.Description, topic.Publisher_Name, topic.Upload_Date
FROM topic
  LEFT JOIN topic_category ON ( topic.ID = topic_category.Topic_ID)
  LEFT JOIN topic_media ON (topic.ID = topic_media.Topic_ID)
WHERE
  topic.Status = 1
  AND topic_media.Status = 1
  AND topic.Approve_State = 1
  AND  topic_media.Type = 'image'
GROUP BY
  topic.ID
ORDER BY
  topic.ID DESC
LIMIT
  65625, 25;
ใช้เวลา 12.79 วินาที

สังเกตว่าใช้ GROUP BY แต่ไม่มีการใช้ aggregate function เช่น sum() หรือ count() แสดงว่าแค่ตั้งใจให้แสดงแถวแบบไม่ซ้ำ ลองเปลี่ยนเป็น DISTINCT
SELECT DISTINCT topic.ID, topic.Title, topic.Description, topic.Publisher_Name, topic.Upload_Date
FROM topic
  LEFT JOIN topic_category ON ( topic.ID = topic_category.Topic_ID)
  LEFT JOIN topic_media ON (topic.ID = topic_media.Topic_ID)
WHERE
  topic.Status = 1
  AND topic_media.Status = 1
  AND topic.Approve_State = 1
  AND  topic_media.Type = 'image'
ORDER BY
  topic.ID DESC
LIMIT
  65625, 25 ;
ใช้เวลา 10.65 วินาที

สังเกตพบว่ามีการ LEFT JOIN ตาราง topic_category แต่ไม่ได้เอามาใช้ทำอะไร งั้นตัดทิ้งไป
SELECT DISTINCT topic.ID, topic.Title, topic.Description, topic.Publisher_Name, topic.Upload_Date
FROM topic LEFT JOIN topic_media ON (topic.ID = topic_media.Topic_ID)
WHERE
  topic.Status = 1
  AND topic_media.Status = 1
  AND topic.Approve_State = 1
  AND  topic_media.Type = 'image'  
ORDER BY
  topic.ID DESC
LIMIT
   65625, 25 ;
ใช้เวลา 8.98 วินาที

เปลี่ยนจากการทำ LEFT JOIN เป็น sub query select
SELECT DISTINCT ID, Title, Description, Publisher_Name, Upload_Date
FROM topic
WHERE
  ID IN (SELECT Topic_ID FROM topic_media WHERE Status = 1 AND Type = 'image')
  AND Status = 1
  AND Approve_State = 1  
ORDER BY
  ID DESC
LIMIT
  65625, 25 ;
เหลือ 3.43 วินาที

ลองใช้ EXPLAIN SELECT เพื่อวิเคราะห์ว่ามีอะไรไม่เป็นไปตามที่ต้องการหรือไม่
EXPLAIN SELECT sql_no_cache DISTINCT ID, Title, Description, Publisher_Name, Upload_Date FROM topic WHERE ID IN (SELECT Topic_ID FROM topic_media WHERE Status = 1 AND Type = 'image') AND Status = 1 AND Approve_State = 1   ORDER BY ID DESC LIMIT 65625, 25  ;
+----+--------------------+-------------+----------------+----------------------+---------------+---------+-------+-------+-----------------------------+
| id | select_type        | table       | type           | possible_keys        | key           | key_len | ref   | rows  | Extra                       |
+----+--------------------+-------------+----------------+----------------------+---------------+---------+-------+-------+-----------------------------+
|  1 | PRIMARY            | topic       | ref            | Approve_State,Status | Approve_State | 1       | const | 75581 | Using where; Using filesort |
|  2 | DEPENDENT SUBQUERY | topic_media | index_subquery | Topic_ID,Type,Status | Topic_ID      | 8       | func  |     2 | Using where                 |
+----+--------------------+-------------+----------------+----------------------+---------------+---------+-------+-------+-----------------------------+
พบว่า มี Using filesort เพราะตอน query นั้น mysql ให้ความสำคัญกับการ look up (WHERE) ก่อน จึงใช้ index Approve_State หรือ Status ส่วน PRIMARY ที่จำเป็นต้องใช้ตอนเรียงลำดับ (ORDER BY) ไม่ได้ใช้ (ขณะ query สามารถเลือก index ได้อันเดียว) ทำให้ต้องเสียเวลาไปเรียงข้อมูลใหม่ก่อน ในกรณีนี้อาจจะลองบังคับให้ใช้ index ที่ต้องการ โดยเพิ่ม FORCE INDEX (keyname) หลังชื่อตาราง เช่น
EXPLAIN SELECT sql_no_cache DISTINCT ID, Title, Description, Publisher_Name, Upload_Date FROM topic FORCE INDEX (PRIMARY) WHERE ID IN (SELECT Topic_ID FROM topic_media WHERE Status = 1 AND Type = 'image') AND Status = 1 AND Approve_State = 1   ORDER BY ID DESC LIMIT 65625, 25  ;
+----+--------------------+-------------+----------------+----------------------+----------+---------+------+-------+-------------+
| id | select_type        | table       | type           | possible_keys        | key      | key_len | ref  | rows  | Extra       |
+----+--------------------+-------------+----------------+----------------------+----------+---------+------+-------+-------------+
|  1 | PRIMARY            | topic       | index          | NULL                 | PRIMARY  | 8       | NULL | 65650 | Using where |
|  2 | DEPENDENT SUBQUERY | topic_media | index_subquery | Topic_ID,Type,Status | Topic_ID | 8       | func |     2 | Using where |
+----+--------------------+-------------+----------------+----------------------+----------+---------+------+-------+-------------+
เมื่อเอา EXPLAIN ออก รันด้วยคำสั่งจริง 
SELECT sql_no_cache DISTINCT ID, Title, Description, Publisher_Name, Upload_Date 
FROM topic FORCE INDEX (PRIMARY) 
WHERE 
  ID IN (SELECT Topic_ID FROM topic_media WHERE Status = 1 AND Type = 'image') 
  AND Status = 1 
  AND Approve_State = 1   
ORDER BY 
  ID DESC 
LIMIT
 65625, 25  ;
ใช้เวลา 2.22 วินาที

ทั้งหมดนี้คือได้ผลลัพธ์เหมือนเดิมเป๊ะ

วันพฤหัสบดีที่ 1 กันยายน พ.ศ. 2554

MD RAID Migration จาก partition ธรรมดา เป็น RAID1,5,6,10

คำเตือน: บทความนี้มีขั้นตอนการสั่งงานระบบที่ค่อนข้างมีความเสี่ยงที่จะทำให้ข้อมูลสูญหายได้ ถ้าใช้งานจริง โปรดสำรองข้อมูลก่อนเสมอ ทุกคำสั่งควรทบทวนอย่างระมัดระวัง และต้องดัดแปลงให้ตรงกับสถานการณ์จริง และควรตรวจสอบผลลัพธ์ว่าเป็นไปตามที่ต้องการหรือไม่ ก่อนจะดำเนินการขั้นต่อไป

ปกติเรามักจะทำ RAID ก่อนที่จะติดตั้งระบบ โดยเฉพาะเมื่อต้องคอนฟิก hardware RAID นั้น เป็นไปไม่ได้เลยที่จะติดตั้งระบบบนดิสก์ตัวเดียวก่อน แล้วค่อยเปลี่ยนเป็น RAID ภายหลัง
แต่ความยืดหยุ่นของ Linux MD RAID ทำให้การ migrate จากพาร์ทิชันธรรมดา มาเป็น RAID ได้ ซึ่งทำได้เฉพาะกับ RAID ที่รองรับ redundant ดังนั้นจึงใช้กับ RAID0 ไม่ได้
ในขั้นแรก ขอยกตัวอย่างการ migrate จาก พาร์ทิชันธรรมดา -> RAID1
สถานการณ์ เดิมมีฮาร์ดดิสก์ 1 ตัว (sda) แบ่งเป็น

  1. /boot
  2. swap
  3. /
  4. /var
ต่อมาเพิ่มฮาร์ดดิสก์อีกตัวขนาดเท่ากัน (sdb) จะทำ RAID1 ทุกพาร์ทิชัน ยกเว้น swap

หลักการคือ เราจะทำให้ sdb ทำงานเป็น RAID1 ที่มีสถานะ degraded คือมีดิสก์ไม่ครบ แต่ยังทำงานได้ คือมีดิสก์เพียง 1 ตัว จากที่ต้องการจริงคือ 2 ตัว แล้วย้ายข้อมูลจาก sda แต่ละพาร์ทิชัน ไปยัง RAID1 แต่ละพาร์ทิชันที่สร้างขึ้น ปรับ boot parameter ต่างๆ ให้บูตและเมานท์จาก RAID ทั้งหมด แล้วรีบูต แล้วค่อยเพิ่ม sda แต่ละพาร์ทิชันเข้ามาใน RAID แต่ละชุด

ขั้นตอน
โคลนโครงสร้างพาร์ทิชัน
# sfdisk -d /dev/sda | sfdisk -L /dev/sdb

เปลี่ยนชนิดของพาร์ทิชันเป็น Linux raid autodetect (fd)
# fdisk /dev/sdb
t
1
fd
t
3
fd
t
4
fd
w

สร้าง swap
# mkswap /dev/sdb2

ติดตั้ง mdadm
# apt-get install mdadm

สร้าง RAID1 ในพาร์ทิชัน 1,3,4
# mdadm --create /dev/md0 --level=1 --raid-devices=2 --metadata=0.9 missing /dev/sdb1

# mdadm --create /dev/md1 --level=1 --raid-devices=2 missing /dev/sdb3
# mdadm --create /dev/md2 --level=1 --raid-devices=2 missing /dev/sdb4
สำหรับ raid ชุดแรกที่จะให้บูตได้ ควรกำหนดให้ใช้ metadata version 0.9 ส่วนตัวอื่นๆ ไม่ระบุ มันจะใช้ metadata version 1.2 และจะระบุ device เป็น missing เพื่อเว้นไว้เพิ่ม sda เข้ามาภายหลัง

จากนั้นสร้าง filesystem
# mkfs.ext2 -L boot -m 0 /dev/md0
# mkfs.ext4 -L root /dev/md1
# mkfs.ext4 -L var /dev/md2

เมานท์และคัดลอกไฟล์ จากพาร์ทิชันเดิม ในขั้นตอนนี้ ถ้ามีโปรแกรม หรือ service อะไรรันไว้ แ้ล้วอาจจะมีการเปิดไฟล์ค้างไว้ ควรหยุดโปรแกรมหรือ service นั้นก่อน เช่น MySQL มิฉะนั้นแล้วข้อมูลที่คัดลอกไปจะไม่สมบูรณ์ ถ้าไม่แน่ใจ ให้ reboot ระบบเป็น single user mode ก่อน
# mkdir /mnt/root
# mount /dev/md1 /mnt/root
# mkdir /mnt/old-root
# mount -o bind / /mnt/old-root
# (cd /mnt/old-root ; tar c . ) | (cd /mnt/root ; tar xpv )
# umount /mnt/old-root
# rmdir /mnt/old-root
# rmdir /mnt/root/mnt/old-root
# rmdir /mnt/root/mnt/root
# mount /dev/md0 /mnt/root/boot
# (cd /boot ; tar c . ) | (cd /mnt/root/boot ; tar xpv )
# mount /dev/md2 /mnt/root/var
# (cd /var ; tar c . ) | (cd /mnt/root/var ; tar xpv )

ต่อไปจะ chroot และติดตั้ง boot loader ใหม่ และแก้ไขค่าเกี่ยวกับการบูตทั้งหมด
# mount -o bind /dev /mnt/root/dev
# mount -t proc none /mnt/root/proc
# mount -t devpts none /mnt/root/dev/pts
# mount -t sysfs none /mnt/root/sys
# chroot /mnt/root /bin/bash --login
# vi /etc/fstab
   (หรือ nano /etc/fstab แล้วแต่ถนัด editor ใด)
   แล้้วแก้ไขพาร์ทิชัน หรือ UUID เดิมให้เป็น /dev/md0 สำหรับ /boot, /dev/md1 สำหรับ / และ /dev/md2 สำหรับ /var และเพิ่ม swap ให้กับ /dev/sdb2
mdadm --examine --scan >> /etc/mdadm/mdadm.conf
update-grub

# grub-install /dev/sda
# grub-install /dev/sdb
# update-initramfs -u

  ตรวจดูในไฟล์ /boot/grub/grub.cfg ว่า ในบรรทัดที่ขึ้นต้นด้วย linux ได้กำหนด root=/dev/md1 ถูกต้องหรือไม่ โดยปกติจะถูกกำหนดไว้ถูกต้องแล้วด้วยโปรแกรม update-grub
จากนั้นสั่ง reboot

# exit
# umount /mnt/root/sys
# umount /mnt/root/dev/pts
# umount /mnt/root/proc
# umount /mnt/root/dev
# umount /mnt/root
# reboot

เมื่อบูตแล้วระบบควรจะใช้ /dev/md1 เป็น root ลองตรวจสอบด้วยคำสั่ง mount
# mount
ควรจะพบบรรทัดนี้
/dev/md1 on / type ext4 (rw,noatime)

เมื่อสั่ง
# cat /proc/mdstat
จะพบระบบ RAID ยังเป็น degraded อยู่ ดังนี้
md0 : active raid1 sdb1[1]
      262140 blocks [2/1] [_U]
md1 : active raid1 sdb3[1]
      5242816 blocks [2/1] [_U]
md2 : active raid1 sdb4[1]
      41942480 blocks [2/1] [_U]

ตรวจสอบระบบและข้อมูลต่างๆ ว่ายังปกติดี เมื่อมั่นใจแล้วก็เอาพาร์ทิชันต่างๆ ใน sda มารวมเข้ากับระบบ RAID
# fdisk /dev/sda
t
1
fd
t
3
fd
t
4
fd
w
# mdadm /dev/md0 --add /dev/sda1
# mdadm /dev/md1 --add /dev/sda3
# mdadm /dev/md2 --add /dev/sda4
# cat /proc/mdstat
md0 : active raid1 sdb1[1]
      262140 blocks [2/1] [_U]
      [============>........]  recovery = 63.5% (166458/262140) finish=0.4min speed=3504K/sec
md1 : active raid1 sdb3[1]
      5242816 blocks [2/1] [_U]
       resync=DELAYED
md2 : active raid1 sdb4[1]
      41942480 blocks [2/1] [_U]
       resync=DELAYED

RAID จะ resync จาก sdb ไปยัง sda ซึ่งระยะเวลาขึ้นอยู่กับขนาดของพาร์ทิชันและความเร็วในการอ่านเขียนดิสก์ รวมถึงความ busy ของดิสก์ด้วย
ในระหว่างนี้ ถ้าเป็น server เราก็สามารถให้บริการได้ตามปกติ แต่ก็จะทำให้การ sync ข้อมูลของ RAID ช้าไปด้วย

ถ้าจะ migrate จากพาร์ทิชันธรรมดา ไปเป็น RAID5,6 หรือ RAID10 ก็ทำได้ในลักษณะเดียวกัน คือให้จัดการดิสก์ที่นำเข้ามาเพิ่มให้ทำงานเป็น RAID ที่ไม่สมบูรณ์ หรือ degraded ก่อน แล้วดัดแปลงขั้นตอนจากข้างบนได้เลย

มีข้อควรระวังคือพาร์ทิชันที่จะใช้ boot (ปกติคือ /boot หรือถ้าไม่ได้แยกไว้ ก็จะเป็น /) ต้องกำหนดเป็น RAID1 และใช้ metadata version 0.9 เสมอ ถ้าเป็นแบบอื่น อาจจะบูตไม่ได้ แต่ไม่แน่ใจว่า grub2 นี่รองรับ MD RAID ได้ขนาดไหนแล้ว

วันจันทร์ที่ 29 สิงหาคม พ.ศ. 2554

การ downgrade php เป็นรุ่น 5.2.x ใน Debian 6.0 (Squeeze)

เมื่อติดตั้ง Debian 6.0 จะได้ php5 เป็นรุ่น 5.3.x ซึ่งมีคุณสมบัติบางประการต่างไปจากรุ่นเดิมคือ 5.2.x (อ่านเพิ่มเติมที่ http://php.net/manual/en/migration53.php) ซึ่งในบางครั้งเรายังจำเป็นต้องใช้รุ่นเดิมอยู่ เช่นยังใช้ drupal 5.x ซึ่งยังใช้ไม่ได้กับ php 5.3 (ต้องเป็น drupal รุ่น 6.x ตัวหลังๆ หรือ drupal 7.x) สามารถเลือกติดตั้ง php5 จาก oldstable หรือ Debian 5.0 (lenny) โดยทำได้ดังนี้

เพิ่ม repository ของ lenny เช่น จากเดิมใน /etc/apt/sources.list มี


deb http://ftp.th.debian.org/debian squeeze main non-free contrib
deb http://ftp.th.debian.org/debian-security squeeze/updates main non-free contrib

ให้เพิ่ม ของ lenny เข้าไปด้วย เป็น

deb http://ftp.th.debian.org/debian squeeze main non-free contrib
deb http://ftp.th.debian.org/debian-security squeeze/updates main non-free contrib
deb http://ftp.th.debian.org/debian lenny main
deb http://ftp.th.debian.org/debian-security lenny/updates main

แก้ไข (หรือสร้างไฟล์ใหม่) /etc/apt/preferences ใส่คอนฟิกดังนี้ลงไป

Package: php-* php5 php5-* libapache2-mod-php5 php-pear
Pin: release a=oldstable
Pin-Priority: 999

จากนั้นสั่ง

# apt-get update
# apt-get -f install

ระบบจะ downgrade แพกเกจ PHP ที่ติดตั้งไปแล้ว ที่อยู่ในรายการที่เรากำหนด ให้เป็นรุ่นที่อยู่ใน oldstable ตามต้องการ ถ้ายังไม่ติดตั้ง ก็ติดตั้งตามปกติ

และเมื่อไหร่ที่พร้อมที่จะอัพเกรด php เป็นรุ่น 5.3 ก็แก้ไฟล์ /etc/apt/preferences เอาคอนฟิกที่เพิ่มเข้าไป 3 บรรทัดนั้นออก แล้วสั่ง upgrade ตามปกติได้เลย