Hadoop - เป็น Application (Framework) ที่ถูกเขียนขึ้นด้วยภาษา java ใช้สำหรับประมวลผลข้อมูลขนาดใหญ่ (ยักษ์) บน cluster (ใช้คอมพิวเตอร์หลายเครื่องในการประมวลผลร่วมกัน) มีความสามารถในการ scale out คือสามารถเพิ่มจำนวนเครื่องคอมพิวเตอร์ที่ใช้ในการประมวลผลร่วมกันได้หลายๆ เครื่อง (จากร้อยเครื่องไปเป็นพันๆ เครื่อง) มีความสามารถในการตรวจสอบและแก้ปัญหาเครื่องคอมพิวเตอร์ที่ทำงานผิดพลาดได้เองโดยอัตโนมัติ มีการจัดเก็บข้อมูลด้วย Hadoop Distribute File System ซึ่งเป็นการจัดเก็บข้อมูลแบบกระจาย โดยอาศัยแนวคิดของ Google File System ซึ่งถูกคิดค้นโดย Google มีความสามารถในการประมวลผลด้วย MapReduce Programming ซึ่งเป็น Programming Model หนึ่งที่ถูกคิดค้นขึ้นโดย Google อีกเช่นเดียวกัน
MapReduce - อธิบายแบบสั้นๆ เป็นกระบวนการที่ใช้สำหรับการแบ่ง input data ให้มีขนาดเล็กลง แล้วส่งไปประมวลผลยัง node อื่นๆ ที่อยู่ใน cluster เมื่อประมวลเสร็จแล้วจึงนำผลลัพธ์ที่ได้กลับมาลดขนาด ได้เป็น output data แล้วค่อยส่ง output data นั้นกลับออกไปครับ
อธิบายแบบยาวขึ้นมานิดนึง เป็นกระบวนการที่ใช้สำหรับการแบ่ง input data ให้มีขนาดเล็กลง แล้วส่งไปประมวลผลยัง node อื่นๆ ที่อยู่ใน cluster
- ซึ่งแต่ละ node จะทำการแปลง input data ที่ถูกแบ่งแล้วให้อยู่ในรูปของ key-value (Map) ตาม Mapper class ที่ได้เขียนเอาไว้ (แล้วแต่เราจะ map ยังไง)
- จากนั้นจึงทำการยุบรวม (ผสม) key value ใน Combiner class (แล้วแต่เราจะยุบรวมยังไง)
- เมื่อยุบรวม key value ที่ Combiner เสร็จแล้ว ก็จะทำการจัดเรียง หรือ sort key value ใหม่ (แล้วแต่เราจะ sort ยังไง)
- แล้วนำผลลัพธ์ที่ได้กลับมาลดขนาด (Reduce) ตามคำสั่งที่ถูกเขียนไว้ใน Reducer class (แล้วแต่เราจะ reduce ยังไง)
- จากนั้นจึงค่อยส่ง output data ที่ได้กลับออกไปครับ
สังเกตว่าผมจะเขียนว่า "แล้วแต่เราจะ ..." นั่นหมายความว่า การทำ MapReduce ขึ้นอยู่กับการเขียนโปรแกรมของเราครับ ว่าเราจะให้มัน Map อะไร ยังไง และ Reduce อะไร ยังไง
"การทำ MapReduce มันขึ้นอยู่กับว่าเราต้องการทำอะไรกับ input data นั้นๆ เราก็เขียนโปรแกรม MapReduce นั้นๆ ขึ้นมาครับ"
ซึ่ง hadoop แค่เป็นตัวที่คอยส่งต่องาน (job) ตาม MapReduce Flow ให้เราเท่านั้นครับ code ที่เป็น business logic เราต้องเขียนเอง
MapReduce Flow
Input data --> Mapper --> Combiner --> Shuffle and Sort --> Reducer --> Output data
การทำ MapReduce ไม่จำเป็นต้องครบ flow เพราะขึ้นอยู่กับโจทย์ที่เราจะเอาไปใช้ครับ
Video อธิบายการทำงานของ MapReduce http://www.youtube.com/watch?v=HFplUBeBhcM
หรือถ้าหากอ่าน code java ออก ลองดูตัวอย่างนี้ดูครับ WordCount.java จากหน้าเว็บ apache
http://hadoop.apache.org/docs/r1.2.1/mapred_tutorial.html
ถ้าอ่าน code WordCount ออก และเข้าใจว่ามันทำงานยังไง ก็แสดงว่าเราเข้าใจ MapReduce ในระดับนึงแล้วครับ
ลองสังเกตผลลัพธ์น่ะครับ ว่า Map แล้วได้อะไร Combine แล้วได้อะไร Reduce แล้วได้อะไร
ใช้ class ไหนในการ Map ใช้ class ไหนในการ Combine ใช้ class ไหนในการ Reduce (ดูจาก job configuration)
ตอน Map ส่งค่าเป็นอะไร ตอน Combine ส่งค่าเป็นอะไร ตอน Reduce ส่งค่าเป็นอะไร
ถ้าสังเกตดีๆ จะเห็นความแตกต่าง และทำให้เข้าใจการทำงานของ MapReduce ครับ
HDFS (Hadoop Distribute File System) - เป็น Distribute File System รูปแบบหนึ่งที่สร้างขึ้นโดยอาศัยแนวคิดของ Google File System (GFS) เขียนด้วยภาษา java และออกแบบมาเพื่อใช้งานกับ hadoop application โดยเฉพาะ ใช้สำหรับจัดเก็บข้อมูลขนาดใหญ่ ระดับ tera byte หรือ petra byte ขึ้นไป การจัดเก็บข้อมูลจะใช้ "รูปแบบของการกระจาย " (distribute storage) คือกระจายข้อมูลไปเก็บยัง node อื่นๆ ที่อยู่ใน cluster มีความสามารถในการทำ data replication (สำเนาข้อมูล) เองโดยอัตโนมัติ ทำให้มีความน่าเชื่อถือสูง เพราะสามารถป้องกันความผิดพลาดของข้อมูลที่อาจจะเกิดขึ้นได้ เมื่อ node ใด node หนึ่ง fail ไป สถาปัตยกรรมของ HDFS ประกอบไปด้วย 2 ส่วนหลักๆ คือ Name node และ Data node
Name node - HDFS จะประกอบไปด้วย 1 name node หรือ 1 master server ซึ่งมีหน้าที่
- คอยบริหารจัดการ File System Namespace ของ cluster
- คอยควบคุมการเข้าถึง file จากผู้ใช้หรือ client
- คอยบริหารจัดการ Data node ต่างๆ
- คอยจัดเก็บ meta data ต่างๆ ของ cluster ไว้ (เช่น การเก็บ block data id ว่า data นี้ถูกเก็บไว้ที่ Data node ตัวใด)
- ทำหน้าที่ในการ replicate data ไปยัง Data node ตัวอื่นๆใน cluster
Secondary Name node - เอาไว้สำหรับทำ check point ให้กับ Name node เป็นระยะๆ (ไม่ใช่ back up ของ Name node) เอาไว้แก้ปัญหาในกรณีที่ Name node ต้องไปค้นหาข้อมูลโดยตรงจาก edit log หรือ image file ซึ่งมีขนาดใหญ่ hadoop เลยแก้ปัญหาด้วยการทำ check point ไว้ที่ Secondary Name node เพื่อให้ Name node สามารถมาอ่านเอาข้อมูลล่าสุดไปได้ใช้ได้โดยตรง และ รวดเร็ว
reference : http://wiki.apache.org/hadoop/FAQ#What_is_the_purpose_of_the_secondary_name-node.3F
other : http://computegeeken.blogspot.com/2012/06/secondary-namenode-what-it-really-do.html
Data node - HDFS จะประกอบไปด้วยหลาย data node หรือหลาย slaves server มีหน้าที่ในการจัดเก็บข้อมูลของ cluster ซึ่งจะคอยอ่าน เขียนข้อมูล ตามที่ผู้ใช้หรือ client ต้องการ โดยรับคำสั่งผ่านทาง Name node
Job Tracker - คือ Service ที่ทำหน้าที่ในการสั่งงาน ควบคุมงาน หรือกระจายงานไปยัง task tracker หรือ node อื่นๆ ใน cluster เมื่อได้รับคำสั่งมาจาก (job) client ซึ่งจะมีอยู่เฉพาะที่ Name node หรือ master server เท่านั้น
reference : http://wiki.apache.org/hadoop/JobTracker
Task Tracker - คือ Service ที่ทำหน้าที่ run หรือประมวลผลงาน ตามคำสั่งที่ได้รับมาจาก Job Tracker การประมวลผลในที่นี้คือ การประมวลผล Map, Combine, Shuffle, Sort, Reduce ... ของแต่ละ node นั่นเอง (node ใคร node มัน) และยังทำหน้าที่ในการคอยติดตามงานของ node ด้วยว่างานที่ได้รับมานั้นทำสำเร็จหรือไม่ จากนั้นก็จะทำการแจ้งหรือส่งผลลัพธ์กลับไปยัง Job Tracker ครับ
คำศัพท์ที่ควรรู้ผ่านไปแล้ว เรามาเริ่มติดตั้ง hadoop กันครับ
แนวคิดของการติดตั้ง
- เราจะทำการติดตั้ง apache hadoop ใน VMware ด้วยกันทั้งหมด 4 ตัวหรือ 4 nodes ซึ่งประกอบไปด้วย
- 1 name node (1 master server)
- 3 data nodes (slaves server สำหรับเก็บ data)
- ทำการติดตั้ง hadoop ให้เสร็จ 1 node (1 VM) ซึ่งการติดตั้งจะประกอบไปด้วย
- ติดตั้ง java ซึ่งจะต้องมี version มากกว่าหรือเท่ากับ (>=) 6
- ติดตั้ง ssh แล้วทำการสร้าง ssh key
- ทำการ disabled ipv6 (เนื่องจาก hadoop ยังไม่รองรับ ipv6)
- ติดตั้ง hadoop
- config hadoop file *-site.xml และ hadoop-env.sh
- จากนั้นทำการ clone ไปยัง node ตัวอื่นๆ (VM ตัวอื่นๆ)
- ทำการ map ip --> hostname เพื่อให้ทุก node รู้จักกัน
- เลือก node นึงเป็น namenode (master) ที่เหลือเป็น datanode (slaves)
- ทำการ format hadoop file system (HDFS) ที่ name node
- สั่ง start hadoop ผ่าน name node ซึ่ง name node จะไป start data node ที่เหลือเองทั้งหมด
- สั่ง start map reduce
- ข้อ 7 กับ ข้อ 8 สามารถยุบรวมกันด้วยการ start-all ได้
สภาพแวดล้อมระบบ (System environment)
OS : ubuntu server 12.04 64-bit
java : oracle java 7 (1.7.0 update 45)
Ram : 1GB ต่อ 1 VM
User : hadoop
Group : hadoop
Hadoop : hadoop-1.2.1
Hadoop : hadoop-1.2.1
ก่อนการติดตั้ง apache hadoop
ก่อนการติดตั้ง hadoop เรามีสิ่งที่จะต้องเตรียมพร้อมก่อนดังนี้
เราจะเริ่มทำที่เครื่องแรก หรือว่า namenode กันก่อนน่ะครับ
โดยผมได้ตั้ง host name ของ node นี้ไว้แล้ว ชื่อ namenode
ซึ่งถ้ายังไม่ได้เปลี่ยน ก็ให้เข้าไปแก้ที่ file /etc/hostname เป็น namenode ก่อนครับ
1. ติดตั้ง java เพราะ apache hadoop เขียนด้วยภาษา java จึงต้องมี java environment ในการ run hadoop ครับ สามารถติดตั้งได้ตามนี้ install oracle java 7 on ubuntu 12.04
2. ทำการสร้าง ssh key สามารถทำได้ตามนี้ สร้าง ssh keys linux ubuntu
hadoop บังคับใช้ SSH Key based authentication เพื่อบริหารจัดการ data node (slave) ผ่านทาง name node
การสร้าง ssh key ในที่นี้ให้กำหนด passphress หรือ password เป็นค่าว่าง เพื่อที่เราจะได้ไม่ต้องมานั่งกรอก password ให้กับทุกๆ node
3. ทำการ disabled ipv6 สามารถทำได้ตามนี้ การ disabled ipv6 linux ubuntu
เนื่องจากตอนนี้ apache hadoop ยังไม่ support ipv6 ครับอ้างอิงจาก http://wiki.apache.org/hadoop/HadoopIPv6
เมื่อพร้อมแล้ว มาเริ่มติดตั้งจริงๆ กันครับ
1. download apache hadoop จาก page นี้ครับ http://www.apache.org/dyn/closer.cgi/hadoop/common/
โดยผมขอเลือกที่ http://mirror.reverse.net/pub/apache/hadoop/common/ อันบนสุด
จากนั้นเลือก hadoop 1.2.1
download : http://mirror.reverse.net/pub/apache/hadoop/common/hadoop-1.2.1/hadoop-1.2.1.tar.gz โดยใช้คำสั่ง wget ครับ
$ cd ~ $ mkdir Download $ cd Download $ wget http://mirror.reverse.net/pub/apache/hadoop/common/hadoop-1.2.1/hadoop-1.2.1.tar.gz
จะเห็นว่ามี file .tar.gz อยู่ใน folder Download ครับ
จากนั้นทำการแตก file .tar.gz ด้วยคำสั่ง tar ดังต่อไปนี้
$ ls -l $ chmod 777 hadoop-1.2.1.tar.gz $ tar xzf hadoop-1.2.1.tar.gz
จากนั้นทำการเปลี่ยนชื่อและย้าย hadoop ไปไว้ใน /opt/ ครับ
$ mv hadoop-1.2.1 hadoop $ sudo mv hadoop /opt/ $ cd /opt/
ทำการ update .bashrc ใน home directory ของ user ครับ
โดยการเพิ่ม HADOOP_HOME ลงไปในตัวแปร PATH ดังนี้
(เราติดตั้ง javaไปแล้ว สังเกตว่าจะมีตัวแปร JAVA_HOME อยู่แล้วครับ)
การต่อตัวแปร PATH ให้คั่นด้วยเครื่องหมาย : (colon)
export JAVA_HOME=/usr/lib/jvm/java-7-oracle export HADOOP=/opt/hadoop PATH=$PATH:$JAVA_HOME/bin:$HADOOP_HOME/bin
ทำการ config hadoop file
hadoop cluster เราจะต้องทำการ config ทั้งหมด 6 files ครับ ซึ่งประกอบไปด้วย
ครึ่งแรก 4 files (ก่อนทำ cluster)
- core-site.xml - เอาไว้ config พวก name node, file system ที่ใช้สำหรับเก็บข้อมูลของ hadoop
- mapred-site.xml - เอาไว้ config การทำ map reduce
- hdfs-site.xml - เอาไว้ config การจัดเก็บ data ของ HDFS และการทำ data replication
- hadoop-env.sh - เอาไว้ config environment ต่างๆ ของ hadoop เช่นพวกตัวแปรต่างๆ ที่สำคัญๆ
ครึ่งหลัง 2 files (ทำ cluster)
- masters - เอาไว้บอกว่าจะให้ node ใดเป็น Name node หรือ master server
- slaves - เอาไว้บอกว่าจะให้ node ใดเป็น Data node สำหรับเก็บข้อมูล หรือ slave server
ทั้งหมดถูกเก็บไว้ที่ HADOOP_PATH_INSTALL/conf/
ซึ่งในตัวอย่างนี้เก็บไว้ที่ /opt/hadoop/config ครับ
$ cd /opt/hadoop/conf $ ls -l $ vi core-site.xml
core-site.xml เพิ่ม config ดังต่อไปนี้ลงไป
hadoop.tmp.dir - ตัวแปร path (directory) ที่ใช้สำหรับเก็บข้อมูลการประมวลผล (temp) และในที่นี้เราจะใช้เป็น HDFS ของ node นั้นๆด้วย
fs.default.name - URL ของ Name node ซึ่งจะทำให้ Data node ตัวอื่นๆ สามารถติดต่อสื่อสารกับ Name node ได้ โดยผ่านทาง URL นี้ config ในที่นี้เป็น hdfs://namenode:54310
<configuration> <property> <name>hadoop.tmp.dir</name> <value>/app/hadoop/tmp</value> </property> <property> <name>fs.default.name</name> <value>hdfs://namenode:54310</value> </property> </configuration>
ทำการสร้าง temporary directory สำหรับเก็บข้อมูลตามที่ config ใน core-site.xml hadoop.tmp.dir
ซึ่งก็คือ /app/hadoop/tmp
กำหนดเจ้าของ directory เป็น hadoop ซึ่งมี group เป็น hadoop
กำหนดสิทธิ์การเข้าถึง เป็น 750 คือ
- 7 (111) เจ้าของสามารถอ่าน เขียน execute ได้
- 5 (101) คนที่อยู่ใน group สามารถอ่าน execute ได้
- 0 (000) ส่วนคนอื่น ไม่สิทธิ์เข้าถึง directory นี้
ตัวอย่างผลลัพธ์หลังจากที่เราติดตั้ง hadoop เสร็จแล้ว
แก้ mapred-site.xml (map reduce config)
mapred-site.xml เพิ่ม config ดังต่อไปนี้ลงไป
mared.job.tracker - URL Job tracker เพื่อให้ Task Tracker ของทุกๆ Data node สามารถสื่อสาร (ประสานงาน) กับ Job Tracker ของ Name node ได้ config ในที่นี้เป็น namenode:54311
<configuration> <property> <name>mared.job.tracker</name> <value>namenode:54311</value> </property> </configuration>
แก้ hdfs-site.xml
hdfs-site.xml เพิ่ม config ดังต่อไปนี้ลงไป
dfs.replication - กำหนด data replication ว่าจะให้ replicate ไว้กี่ที่ ซึ่งในที่นี้เราจะติดตั้ง hadoop กัน 4 nodes จึงทำหนดให้มีค่าเป็น 4 ครับ
<configuration> <property> <name>dfs.replication</name> <value>4</value> </property> </configuration>
ทำการ config hadoop environment (hadoop-env.sh)
โดยการกำหนด JAVA_HOME ครับ
แก้ JAVA_HOME ให้ reference ไปที่ java path ที่ได้ install ไว้ ซึ่งในที่นี้คือ /usr/lib/jvm/java-7-oracle
ทีนี้ ลอง start hadoop ดูครับ ด้วยคำสั่ง start-all.sh (start ทั้ง HDFS และ MapReduce)
$ bin/start-all.sh
ตอนนี้เราติดตั้ง hadoop เสร็จไปแล้ว 1 node ต่อไปเราจะทำการ clone node นี้ไปยัง node (VM) อื่นๆ กันครับ
แต่ก่อนอื่นให้ทำการ stop HDFS และ MapReduce ของ node ปัจจุบันไปก่อน ด้วยคำสั่ง
$ bin/stop-all.shจากนั้นปิดเครื่องครับ ด้วยคำสั่ง
$ sudo shutdown -h 0
ก่อนที่จะทำการ clone VM เรามาทำการ config Network Adapter ของ VM จาก NAT --> Bridge ก่อนครับ เพื่อให้ทุก node อยู่ในวง network เดียวกัน และสามารถสื่อสารกันได้ ด้วยการคลิกที่ Network Adapter
เลือก Bridge : Connected directly to the physical network --> OK
การ clone VM สามารถทำได้ตามนี้ครับ Cloning VMware
ซึ่งเมื่อ clone เสร็จแล้ว เราจะได้แบบนี้ (4 nodes หรือ 4 VM)
จากนั้นทำการ start VM ทั้งหมด (ทั้ง 4 nodes)
แล้วทำการแก้ไขชื่อ หรือ host name ของแต่ละ node ให้แตกต่างกัน เนื่องจาก ทุกๆ node (ทุก data node) ถูก clone มาจากที่เดียวกัน (namenode เดียวกัน) จึงมีชื่อเหมือนกันครับ
เปลี่ยนชื่อให้แต่ละ node ใหม่ โดยเข้าไปแก้ไขที่ file /etc/hostname
โดยกำหนดให้
VM namenode มีชื่อเป็น namenode
VM datanode1 มีชื่อเป็น datanode1
VM datanode2 มีชื่อเป็น datanode2
VM datanode3 มีชื่อเป็น datanode3
จากนั้นเราจะทำการ map จาก ip --> hostname ครับ
แต่ก่อนที่เราจะ map ip นั้น เราจะต้องรู้ก่อนว่าแค่ละเครื่อง มี ip เป็นอะไร
การดู ip แต่ละเครื่อง เราจะใช้คำสั่ง ifconfig ซึ่งได้ผลลัพธ์ดังต่อไปนี้
namenode --> 192.168.1.34
datanode1 --> 192.136.1.35
datanode2 --> 192.136.1.36
datanode3 --> 192.136.1.37
ดู ip ของ namenode ใช้คำสั่ง ifconfig
ดู ip ของ datanode1
ดู ip ของ datanode2
ดู ip ของ datanode3
จากนั้นทำการแก้ไข file /etc/hosts เพื่อทำการ map ip
พิมพ์ ip กับ hostname ที่ต้องการ map ลงไป ซึ่งในที่นี้เราจะ map ip ด้วยกันทั้งหมด 4 ip --> 4 nodes
ทำเหมือนกันทุกๆ nodes การ map ip ให้ยกเว้นอันที่ 2 (ip = 127.0.1.1) เป็นของใครของมันครับ
ทำการ restart (reboot) ทุกเครื่อง (ทุก nodes) เพื่อ reboot config
ทดสอบ SSH
ก่อนการติดตั้ง hadoop เราได้กำหนดว่าจะต้องทำการติดตั้ง SSH และกำหนด password หรือ passphress เป็น empty ให้เสร็จก่อน เรามาลองทดสอบกันดูครับว่าเราสามารถเชื่อมต่อด้วย SSH ได้แล้วหรือไม่ ด้วยคำสั่ง ssh แล้วตามด้วย hostname นั้นๆ
เชื่อมต่อไปที่ namenode
เชื่อมต่อไปที่ datanode1
เชื่อมต่อไปที่ datanode2
เชื่อมต่อไปที่ datanode3
จากการทดสอบพบว่า เราสามารถเชื่อมต่อได้ทุก node ครับ
กำหนด Name node และ Data node
กำหนด Name node โดยเพิ่ม namenode เข้าไปใน masters file
กำหนด Data node
โดยเพิ่ม namenode, datanode1, datanode2 และ datanode3 เข้าไปใน slaves file
ทำการ format namenode ที่ /HADOOP_PATH_INSTALL/bin (/opt/hadoop/bin) ด้วยคำสั่ง
$ hadoop namenode -format
ทำการ start HDFS (Name node และ Data node ยังไม่รวม MapReduce) ด้วยคำสั่ง
$ start-dfs.shหมายเหตุ : เราสามารถ start HDFS รวมทั้ง MapReduce ด้วยคำสั่ง start-all.sh
ใช้คำสั่ง jps (java Virtual Machine Process Status Tool) เพื่อดู process การทำงานของ java เนื่องจาก hadoop ถูกเขียนขึ้นด้วยภาษา java นั่นเอง
ที่ VM namenode ซึ่งทำหน้าที่เป็น Name node จะมี process Data node, Secondary Name node และ Name node ทำงานอยู่
VM datanode 1, 2, 3 จะมีเฉพาะ process DataNode เท่านั้นที่กำลังทำงานอยู่ เนื่องจาก datanode 1, 2, 3 ทำหน้าที่เป็น Data node
ทำการ start MapReduce ที่ Name node
สังเกตว่า hadoop จะทำการ start Job Tracker และ Task Tracker ของ ทุกๆ node
ดู java process
ทำการ stop map reduce (ทุก node) แล้วดู java process
ทำการ stop MapReduce ที่ Name node
สังเกตว่า hadoop จะทำการ stop Job Tracker และ Task Tracker ของ ทุกๆ node
ดู java process Name node จะเหลือแค่ Data node, Secondary Name node และ Name node เท่านั้น ที่กำลังทำงานอยู่
Data node 1 , 2, 3 เหลือแค่ Data node
เราสามารถใช้คำสั่ง start-all.sh และ stop-all.sh เพื่อ start / stop HDFS และ MapReduce ในทีเดียวได้ครับ
หวังว่าบทความนี้จะเป็นประโยชน์ต่อผู้อ่านทุกท่านน่ะครับ
หากมีความผิดพลาดประการใด ก็ต้องขออภัยมา ณ ที่นี้ด้วยครับ
ขอขอบคุณ (thank you)
http://www.michael-noll.com/tutorials/running-hadoop-on-ubuntu-linux-single-node-cluster/
http://hadoop.apache.org/docs/r1.2.1/index.html
http://computegeeken.blogspot.com/2012/06/secondary-namenode-what-it-really-do.html
เป็นบทความที่ดีมากครับ อ่านจบแล้วแต่ยังไม่มีเวลาลงมือทำตาม
ตอบลบน่าสนใจว่าเราจะเอามาประยุคใช้กับงานอะไรได้บ้าง
ขอบคุณครับ ถือว่าเขียนได้ดีเลยครับ ^^
ตอบลบขอบคุณครับ :)
ตอบลบเขียนได้ละเอียดดีมากครับ ขอบคุณสำหรับควมรู้ครับ
ตอบลบขอบคุณมากครับ
ตอบลบเขียนได้ละเอียดทำตามได้ทุกขั้นตอนเลยครับ ขอบคุณมากๆ ครับ
ตอบลบเขียนได้ดีมากเลยคับ ขอบคุณครับ
ตอบลบ