วันอังคารที่ 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 ได้ขนาดไหนแล้ว