Table of Contents
- บทนำ (Introduction)
- สิ่งที่ต้องเตรียม (Prerequisites)
- การทำงานแบบ Multitasking ของ Ansible
- การใช้ Forks เพื่อกำหนดจำนวน Node ที่จะ Run พร้อมกัน
- การใช้ Serial เพื่อกำหนดรอบ (Batch) ในการ Run Tasks
- การใช้ Strategies เพื่อกำหนดวิธีการ Run Tasks
- การใช้ Asynchronous Tasks เพื่อ Run Tasks ได้โดยไม่ต้องรอผล
- บทสรุป (Conclusion)
บทนำ (Introduction)
Ansible คือ automation tool ที่ใช้งานง่ายและเป็นที่นิยมมากในการนำมา automate งานต่าง ๆ โดยเฉพาะงานที่เกี่ยวกับ traditional infrastructure เช่น การจัดการ servers หรือ network devices (ก่อนยุค containerization หรือ cloud native)
ด้วยความง่ายที่ใคร ๆ ก็ใช้งานได้แต่กลับมีประเด็นเล็ก ๆ ซ่อนอยู่ นั่นก็คือหลายคนไม่สามารถรีดประสิทธิภาพมันออกมาได้เต็มที่ หรือบางทีอาจไม่รู้ด้วยซ้ำว่ามันสามารถทำให้เร็วขึ้นกว่าเดิมได้ ผลก็คือ playbooks ที่เขียนออกมานั้น “ทำงานช้า” กว่าที่ควรจะเป็น ใช้เวลานานในการ run tasks และใช้ utilization ของ control node ได้ไม่เต็มที่
ในบทความนี้ผมจะอธิบายเทคนิคต่าง ๆ พร้อมยกตัวอย่างที่ทำให้ Ansible ของเราทำงานได้เร็วขึ้น ซึ่งทุกคนสามารถลอง hands-on ไปด้วยกันได้ครับ
สิ่งที่ต้องเตรียม (Prerequisites)
เฉพาะคนที่อยากจะลอง hands-on ไปด้วยกันเพื่อให้เข้าใจมากขึ้น เครื่องคุณจะต้อง…
- ติดตั้ง Python 3 แล้ว (ผมใช้ version 3.12.3)
- ติดตั้ง Ansible แล้ว (ผมใช้ version 2.16.5)
- ติดตั้ง sshpass แล้ว (ผมใช้ version 1.10)
- ติดตั้ง Docker แล้ว (ผมใช้ version 26.0.0, build 2ae903e)
- ติดตั้ง Git แล้ว (ผมใช้ version 2.44.0)
แต่ผมจะไม่สอนจระเข้ว่ายน้ำหรอกนะครับ ผมรู้ระดับคุณแล้วคุณหาวิธีติดตั้งเองได้หน่า 🤣
โอเค ถ้าติดตั้งทุกอย่างแล้วก็ clone ไฟล์ทั้งหมดจาก Git repository นี้ไปเลยครับ
git clone https://github.com/nopnithi/ansible-concurrency-and-parallelism.git
จะเห็นว่ามีไฟล์ Dockerfile
อยู่ ให้ build image ขึ้นมาก่อนครับ
docker build -t ansible-managed-node .
จากนั้น run containers ขึ้นมาจาก image นั้นครับ ผมสมมุติให้ containers พวกนี้แทน managed nodes ใน labs ของเรา
docker run -d --name node01-windows -p 2201:22 ansible-managed-node
docker run -d --name node02-debian -p 2202:22 ansible-managed-node
docker run -d --name node03-ubuntu -p 2203:22 ansible-managed-node
docker run -d --name node04-centos -p 2204:22 ansible-managed-node
docker run -d --name node05-windows -p 2205:22 ansible-managed-node
docker run -d --name node06-debian -p 2206:22 ansible-managed-node
docker run -d --name node07-ubuntu -p 2207:22 ansible-managed-node
docker run -d --name node08-centos -p 2208:22 ansible-managed-node
เสร็จแล้วลองใช้ Ansible ping
เพื่อเช็ค connectivity ดูก่อนครับ
ansible all -i hosts -m ping
การทำงานแบบ Multitasking ของ Ansible
ผมขอใช้คำว่า multitasking ซึ่งเป็นคำกลาง ๆ ที่หมายถึงการทำงานหลายอย่างพร้อมกัน ส่วนจะพร้อมจริง ๆ แบบ parallelism หรือใช้วิธีการสลับไปมาแบบ concurrency นั้นผมจะขอละไว้เพื่อไม่ให้สับสนครับ
ก่อนอื่นเรามาทำความเข้าใจพื้นฐานของการทำงานแบบ multitasking ใน Ansible ก่อน ซึ่งผมแบ่งเป็น 2 ส่วน
1) Multitasking ระดับ Node
คือการที่ Ansible run task ในหลาย nodes พร้อมกันในเวลาเดียว (พร้อมกันจริง ๆ ไม่ใช่สลับไปมาระหว่างรอ) เช่น สมมุติว่าผมมี task ที่สั่ง start service A และผมต้องการ start service นี้บน 5 servers สิ่งที่ได้คือทุก servers จะถูกสั่ง start service A พร้อมกันในเวลาเดียวกัน
พฤติกรรมปกติที่จะเกิดขึ้นเมื่อ Ansible ทำงานคือ…
- Run task 1 เพื่อ start service A บน server ที่ 1-5 พร้อมกันในเวลาเดียวกัน
- จากนั้นมันจะรอให้ task 1 เสร็จทั้งหมดในทุก nodes ก่อนแล้วจึงจบ
แต่คราวนี้ถ้าเรามีมากกว่า 1 task หละ? เช่น start service A (task 1) และ start service B (task 2) สิ่งที่เกิดขึ้นคือ…
- Run task 1 เพื่อ start service A บน server ที่ 1-5 พร้อมกันในเวลาเดียวกัน
- จากนั้นมันจะรอให้ task 1 เสร็จทั้งหมดในทุก nodes ก่อนแล้วจึงไปต่อ task 2
- Run task 2 เพื่อ start service B บน server ที่ 1-5 พร้อมกันในเวลาเดียวกัน
- จากนั้นมันจะรอให้ task 2 เสร็จทั้งหมดในทุก nodes ก่อนแล้วจึงจบ
สังเกตว่าการทำงานของ Ansible เป็น multitasking ระดับ node อยู่แล้ว แต่ยังไม่ใช่ระดับ task เพราะมันยังทำงานทีละ task อยู่และต้องรอให้ task ก่อนหน้าจบก่อนถึงจะทำ task ถัดไป
2) Multitasking ระดับ Task
คือการที่ Ansible run หลาย tasks พร้อมกันในหนึ่ง node ซึ่งแม้ Ansible จะทำงานเป็น multitasking ในระดับ node อยู่แล้ว แต่จะยังไม่เป็นระดับ task โดยเราจะต้องใช้เทคนิคบางอย่างเพิ่มเติมด้วย
แล้ว multitasking ระดับ task มันคือยังไงหละ?
สมมุติผมต้องการที่จะ start service A (task 1) และ start service B (task 2) สิ่งที่เกิดขึ้นมันจะต้องเป็นแบบนี้
- Run task 1 และ task 2 เพื่อ start service A และ B บน server ที่ 1-5 พร้อมกันในเวลาเดียวกัน
- จากนั้นมันจะรอให้ task 1 และ task 2 เสร็จทั้งหมดในทุก nodes
การใช้ Forks เพื่อกำหนดจำนวน Node ที่จะ Run พร้อมกัน
เทคนิคนี้เป็นวิธีควบคุม multitasking ระดับ node
Forks ใช้กำหนดว่าจะให้ Ansible run กี่ node ในเวลาเดียวกัน ซึ่งค่า default จะอยู่ที่ 5 แต่เราสามารถปรับเพิ่มหรือลดได้ใน configuration ครับ
สมมุติผมมี nodes ทั้งหมด 3 nodes และกำหนดค่า forks
เป็น 1 ใน ansible.cfg
แบบนี้
[defaults]
forks = 1
...
...
และเมื่อ playbook playbooks/01_forks.yaml
เป็นแบบนี้
---
- name: "Forks"
hosts: nodes[0:2] # 3 nodes
tasks:
- name: Task 1
command: sleep 5
- name: Task 2
command: sleep 5
สิ่งที่จะเกิดขึ้นเมื่อเรา run playbook นี้คือ…
- Run task 1 ใน node 1 (5 วินาที)
- Run task 1 ใน node 2 (5 วินาที)
- Run task 1 ใน node 3 (5 วินาที)
- Run task 2 ใน node 1 (5 วินาที)
- Run task 2 ใน node 2 (5 วินาที)
- Run task 2 ใน node 3 (5 วินาที)
เราจะใช้เวลา run ประมาณ 5 * 6 = 30 วินาที (ไม่รวม overhead)
Command:
time ansible-playbook playbooks/01_forks.yaml
Output:
PLAY [Forks] ******************************************************************************************************************
TASK [Task 1] *****************************************************************************************************************
changed: [node01-windows]
changed: [node02-debian]
changed: [node03-ubuntu]
TASK [Task 2] *****************************************************************************************************************
changed: [node01-windows]
changed: [node02-debian]
changed: [node03-ubuntu]
PLAY RECAP ********************************************************************************************************************
node01-windows : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node02-debian : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node03-ubuntu : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ansible-playbook playbooks/01_forks.yaml 0.77s user 0.64s system 4% cpu 32.313 total
ทีนี้เราลองเพิ่มค่า forks
ให้เท่ากับหรือมากกว่าจำนวน nodes ดู โดยผมจะใช้ 10 ไปเลยเผื่อหัวข้อต่อจากนี้ด้วย
[defaults]
forks = 10
...
...
จาก playbook เดียวกัน จะเหลือเวลาทำงานแค่ 10 วินาที (ไม่รวม overhead)
- Run task 1 ใน node 1, 2 และ 3 พร้อมกัน (5 วินาที)
- Run task 2 ใน node 1, 2 และ 3 พร้อมกัน (5 วินาที)
Command:
time ansible-playbook playbooks/01_forks.yaml
Output:
PLAY [Forks] ******************************************************************************************************************
TASK [Task 1] *****************************************************************************************************************
changed: [node01-windows]
changed: [node02-debian]
changed: [node03-ubuntu]
TASK [Task 2] *****************************************************************************************************************
changed: [node02-debian]
changed: [node03-ubuntu]
changed: [node01-windows]
PLAY RECAP ********************************************************************************************************************
node01-windows : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node02-debian : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node03-ubuntu : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ansible-playbook playbooks/01_forks.yaml 0.60s user 0.42s system 9% cpu 11.271 total
ดังนั้น ถ้าเรามี managed nodes จำนวนมากที่ต้องจัดการ การเพิ่มค่า forks
จะช่วยให้ Ansible ทำงานได้เร็วขึ้น เพราะทำงานได้หลาย nodes พร้อมกันมากขึ้น แต่ทั้งนี้ต้องระวังไม่ให้ค่า forks
มากเกินไปเพราะอาจส่งผลเสียได้ เช่น
- อาจทำให้ CPU และ memory ของ control node ไม่พอได้
- อาจมีการส่ง traffic มากเกินไปจนกระทบกับ network ทั้งหมด (เกิด conjestion)
การใช้ Serial เพื่อกำหนดรอบ (Batch) ในการ Run Tasks
เทคนิคนี้เป็นวิธีควบคุม multitasking ระดับ node
ในบางกรณีเราอาจต้องการควบคุมจำนวน nodes ที่จะ run ในแต่ละรอบให้น้อยกว่าค่า forks
ก็ได้ เช่น ต้องการ run ทีละ 2 nodes เพื่อลดความเสี่ยงในการ update ระบบ โดยเราสามารถใช้ serial
keyword เข้ามาช่วยได้
ยกตัวอย่างเช่น playbook นี้ playbooks/02_serial1.yaml
---
- name: "Serial #1"
hosts: nodes[0:7] # 8 nodes
serial: 2
tasks:
- name: Task
command: sleep 2
โดยจะ run บน 8 nodes แต่จะแบ่งเป็นรอบ (batch) ละ 2 nodes สิ่งที่จะเกิดขึ้นคือ…
- รอบที่ 1: Run task ใน node 1 และ 2 พร้อมกัน (2 วินาที)
- รอบที่ 2: Run task ใน node 3 และ 4 พร้อมกัน (2 วินาที)
- รอบที่ 3: Run task ใน node 5 และ 6 พร้อมกัน (2 วินาที)
- รอบที่ 4: Run task ใน node 7 และ 8 พร้อมกัน (2 วินาที)
ซึ่งจะใช้เวลาประมาณ 2 * 4 = 8 วินาที (ไม่รวม overhead)
Command:
time ansible-playbook playbooks/02_serial1.yaml
Output:
PLAY [Serial #1] **************************************************************************************************************
TASK [Task] *******************************************************************************************************************
changed: [node02-debian]
changed: [node01-windows]
PLAY [Serial #1] **************************************************************************************************************
TASK [Task] *******************************************************************************************************************
changed: [node03-ubuntu]
changed: [node04-centos]
PLAY [Serial #1] **************************************************************************************************************
TASK [Task] *******************************************************************************************************************
changed: [node06-debian]
changed: [node05-windows]
PLAY [Serial #1] **************************************************************************************************************
TASK [Task] *******************************************************************************************************************
changed: [node07-ubuntu]
changed: [node08-centos]
PLAY RECAP ********************************************************************************************************************
node01-windows : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node02-debian : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node03-ubuntu : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node04-centos : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node05-windows : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node06-debian : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node07-ubuntu : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node08-centos : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ansible-playbook playbooks/02_serial1.yaml 0.68s user 0.51s system 11% cpu 10.221 total
นอกจากนี้เรายังสามารถกำหนด serial
เป็น list โดยใช้ตัวเลขหรือเปอร์เซ็นต์ได้ โดยแต่ละ list item คือจำนวน nodes ในหนึ่งรอบ
ตัวอย่าง playbook นี้ playbooks/03_serial2.yaml
จะ run บน 8 nodes แต่จะแบ่งเป็น 3 รอบ
---
- name: "Serial #2"
hosts: nodes[0:7] # 8 nodes
serial:
- 1
- 2
- 3
tasks:
- name: Task
command: sleep 2
สิ่งที่จะเกิดขึ้นเมื่อเรา run playbook นี้คือ…
- รอบที่ 1: Run task ใน node 1 (2 วินาที)
- รอบที่ 2: Run task ใน node 2 และ 3 พร้อมกัน (2 วินาที)
- รอบที่ 3: Run task ใน node 4, 5 และ 6 พร้อมกัน (2 วินาที)
- รอบที่ 4 (สุดท้าย): Run task ใน node ที่เหลือทั้งหมดพร้อมกัน (2 วินาที)
ซึ่งจะใช้เวลาประมาณ 2 * 4 = 8 วินาที (ไม่รวม overhead)
อย่าลืมว่าการ run หลาย nodes พร้อมกันก็จะมีจำนวนได้ไม่เกินค่าใน
forks
ถ้าเกินก็ปัดขึ้น batch ใหม่
Command:
time ansible-playbook playbooks/03_serial2.yaml
Output:
PLAY [Serial #2] **************************************************************************************************************
TASK [Task] *******************************************************************************************************************
changed: [node01-windows]
PLAY [Serial #2] **************************************************************************************************************
TASK [Task] *******************************************************************************************************************
changed: [node02-debian]
changed: [node03-ubuntu]
PLAY [Serial #2] **************************************************************************************************************
TASK [Task] *******************************************************************************************************************
changed: [node04-centos]
changed: [node05-windows]
changed: [node06-debian]
PLAY [Serial #2] **************************************************************************************************************
TASK [Task] *******************************************************************************************************************
changed: [node08-centos]
changed: [node07-ubuntu]
PLAY RECAP ********************************************************************************************************************
node01-windows : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node02-debian : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node03-ubuntu : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node04-centos : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node05-windows : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node06-debian : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node07-ubuntu : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node08-centos : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ansible-playbook playbooks/03_serial2.yaml 0.67s user 0.56s system 11% cpu 10.786 total
การใช้ Strategies เพื่อกำหนดวิธีการ Run Tasks
เทคนิคนี้เป็นวิธีควบคุม multitasking ที่ก้ำกึ่งระหว่างระดับ node และ task นะ 😅
Ansible มี strategies ในการ run tasks อยู่ 2 แบบ คือ linear
และ free
ซึ่งกำหนดได้ผ่าน strategy
keyword ใน playbook หรือ configuration (แนะนำว่ากำหนดใน playbook ดีกว่า)
1) Linear Strategy (ค่า Default)
Linear strategy เป็นค่า default ที่ Ansible ใช้ในการ run playbook โดยมันจะ run task ทีละ task โดยจะรอให้แต่ละ task เสร็จบนทุก nodes ก่อนถึงจะ run task ถัดไป แม้ว่าบาง node จะไม่ต้องรัน task นั้นก็ยังคงต้องรออยู่ดี (พูดง่าย ๆ คือทำให้เสร็จเป็น task ไป)
ตัวอย่าง playbook playbooks/05_linear_strategy.yaml
ที่ใช้ linear strategy
---
- name: "Linear Strategy"
hosts: nodes # 8 nodes
tasks:
- name: Task 1 for Windows
command: sleep 5
when: simulated_os == "windows"
- name: Task 2 for Debian
command: sleep 5
when: simulated_os == "debian"
- name: Task 3 for Ubuntu
command: sleep 5
when: simulated_os == "ubuntu"
- name: Task 4 for CentOS
command: sleep 5
when: simulated_os == "centos"
- name: Task 5 for All
command: sleep 5
สิ่งที่จะเกิดขึ้นเมื่อเรา run playbook นี้คือ…
- Task 1 for Windows จะทำงานเฉพาะ node 1 และ 5 เท่านั้น แต่ nodes ที่เหลือก็ต้องรอให้ task นี้เสร็จก่อน (5 วินาที)
- Task 2 for Debian จะทำงานเฉพาะ node 2 และ 6 เท่านั้น แต่ nodes ที่เหลือก็ต้องรอให้ task นี้เสร็จก่อนอ (5 วินาที)
- Task 3 for Ubuntu จะทำงานเฉพาะ node 3 และ 7 เท่านั้น แต่ nodes ที่เหลือก็ต้องรอให้ task นี้เสร็จก่อน (5 วินาที)
- Task 4 for CentOS จะทำงานเฉพาะ node 4 และ 8 เท่านั้น แต่ nodes ที่เหลือก็ต้องรอให้ task นี้เสร็จก่อน (5 วินาที)
- Task 5 for All ทุก nodes จะต้อง run task นี้ (5 วินาที)
ดังนั้นมันจะใช้เวลาประมาณ 5 * 5 = 25 วินาที (ไม่รวม overhead)
Command:
time ansible-playbook playbooks/05_linear_strategy.yaml
Output:
PLAY [Linear Strategy] ********************************************************************************************************
TASK [Task 1 for Windows] *****************************************************************************************************
skipping: [node02-debian]
skipping: [node03-ubuntu]
skipping: [node04-centos]
skipping: [node06-debian]
skipping: [node07-ubuntu]
skipping: [node08-centos]
changed: [node05-windows]
changed: [node01-windows]
TASK [Task 2 for Debian] ******************************************************************************************************
skipping: [node01-windows]
skipping: [node03-ubuntu]
skipping: [node04-centos]
skipping: [node05-windows]
skipping: [node07-ubuntu]
skipping: [node08-centos]
changed: [node02-debian]
changed: [node06-debian]
TASK [Task 3 for Ubuntu] ******************************************************************************************************
skipping: [node01-windows]
skipping: [node02-debian]
skipping: [node04-centos]
skipping: [node05-windows]
skipping: [node06-debian]
skipping: [node08-centos]
changed: [node07-ubuntu]
changed: [node03-ubuntu]
TASK [Task 4 for CentOS] ******************************************************************************************************
skipping: [node01-windows]
skipping: [node02-debian]
skipping: [node03-ubuntu]
skipping: [node05-windows]
skipping: [node06-debian]
skipping: [node07-ubuntu]
changed: [node04-centos]
changed: [node08-centos]
TASK [Task 5 for All] *********************************************************************************************************
changed: [node01-windows]
changed: [node02-debian]
changed: [node04-centos]
changed: [node07-ubuntu]
changed: [node05-windows]
changed: [node06-debian]
changed: [node08-centos]
changed: [node03-ubuntu]
PLAY RECAP ********************************************************************************************************************
node01-windows : ok=2 changed=2 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
node02-debian : ok=2 changed=2 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
node03-ubuntu : ok=2 changed=2 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
node04-centos : ok=2 changed=2 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
node05-windows : ok=2 changed=2 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
node06-debian : ok=2 changed=2 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
node07-ubuntu : ok=2 changed=2 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
node08-centos : ok=2 changed=2 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
ansible-playbook playbooks/05_linear_strategy.yaml 1.14s user 1.19s system 8% cpu 27.028 total
2) Free Strategy
Free strategy แตกต่างจาก linear ตรงที่มันจะ run tasks แบบไม่ต้องรอกัน โดยแต่ละ node สามารถ run tasks ที่ตัวเองต้อง run ไปเลยโดยไม่ต้องรอ nodes อื่น ๆ ทำ task นั้น ๆ จบ ทำให้ playbook ที่มีเงื่อนไขทำงานเป็นบาง nodes เร็วขึ้นมาก
ตัวอย่าง playbook playbooks/06_free_strategy.yaml
ที่ใช้ free strategy
---
- name: "Free Strategy"
hosts: nodes # 8 nodes
strategy: free
tasks:
- name: Task 1 for Windows
command: sleep 5
when: simulated_os == "windows"
- name: Task 2 for Debian
command: sleep 5
when: simulated_os == "debian"
- name: Task 3 for Ubuntu
command: sleep 5
when: simulated_os == "ubuntu"
- name: Task 4 for CentOS
command: sleep 5
when: simulated_os == "centos"
- name: Task 5 for All
command: sleep 5
ผมแค่เพิ่ม strategy: free
เข้าไปที่ playbook เดิม สิ่งที่จะเกิดขึ้นคือ…
- Task 1 for Windows จะถูก run ใน node 1 และ 5 ในขณะเดียวกัน task 2, 3 และ 4 ก็จะถูก run ใน nodes อื่น ๆ ตามเงื่อนไขได้ทันทีโดยไม่ต้องรอให้เสร็จทีละ task (1, 2, 3, …) แบบ
linear
- เมื่อ nodes ใดก็ตามทำ task ที่ตัวเองต้องทำจบ ก็จะเริ่ม run task 5 ต่อไปได้เลย
โดยมันจะลดเวลาการทำงานจาก 25 วินาทีเหลือประมาณ 10 วินาทีเลยสำหรับ playbook เดียวกันนี้
ข้อดีของ free
strategy คือเร็วกว่า linear
มากกรณีที่เรามี tasks ที่มีเงื่อนไขให้ทำในบาง nodes เพราะไม่ต้องรอแต่ละ task เสร็จก่อน แต่ข้อเสียคือทำให้การ debug ยากขึ้นเพราะลำดับการ run จะไม่แน่นอน และจะไม่เหมาะกับ playbook ที่ tasks ต้องทำโดยเรียงลำดับก่อนหลัง
Command:
time ansible-playbook playbooks/06_free_strategy.yaml
Output:
PLAY [Free Strategy] **********************************************************************************************************
TASK [Task 1 for Windows] *****************************************************************************************************
skipping: [node02-debian]
skipping: [node03-ubuntu]
skipping: [node04-centos]
skipping: [node06-debian]
skipping: [node07-ubuntu]
skipping: [node08-centos]
TASK [Task 2 for Debian] ******************************************************************************************************
skipping: [node03-ubuntu]
skipping: [node04-centos]
skipping: [node07-ubuntu]
skipping: [node08-centos]
TASK [Task 3 for Ubuntu] ******************************************************************************************************
skipping: [node04-centos]
skipping: [node08-centos]
TASK [Task 2 for Debian] ******************************************************************************************************
changed: [node02-debian]
TASK [Task 3 for Ubuntu] ******************************************************************************************************
changed: [node03-ubuntu]
TASK [Task 1 for Windows] *****************************************************************************************************
changed: [node01-windows]
changed: [node05-windows]
TASK [Task 4 for CentOS] ******************************************************************************************************
changed: [node04-centos]
TASK [Task 3 for Ubuntu] ******************************************************************************************************
changed: [node07-ubuntu]
TASK [Task 4 for CentOS] ******************************************************************************************************
changed: [node08-centos]
TASK [Task 2 for Debian] ******************************************************************************************************
skipping: [node01-windows]
TASK [Task 3 for Ubuntu] ******************************************************************************************************
skipping: [node02-debian]
TASK [Task 4 for CentOS] ******************************************************************************************************
skipping: [node03-ubuntu]
TASK [Task 2 for Debian] ******************************************************************************************************
skipping: [node05-windows]
TASK [Task 4 for CentOS] ******************************************************************************************************
skipping: [node07-ubuntu]
TASK [Task 3 for Ubuntu] ******************************************************************************************************
skipping: [node01-windows]
TASK [Task 4 for CentOS] ******************************************************************************************************
skipping: [node02-debian]
TASK [Task 2 for Debian] ******************************************************************************************************
changed: [node06-debian]
TASK [Task 3 for Ubuntu] ******************************************************************************************************
skipping: [node05-windows]
TASK [Task 4 for CentOS] ******************************************************************************************************
skipping: [node01-windows]
TASK [Task 3 for Ubuntu] ******************************************************************************************************
skipping: [node06-debian]
TASK [Task 4 for CentOS] ******************************************************************************************************
skipping: [node05-windows]
skipping: [node06-debian]
TASK [Task 5 for All] *********************************************************************************************************
changed: [node04-centos]
changed: [node08-centos]
changed: [node07-ubuntu]
changed: [node03-ubuntu]
changed: [node02-debian]
changed: [node05-windows]
changed: [node01-windows]
changed: [node06-debian]
PLAY RECAP ********************************************************************************************************************
node01-windows : ok=2 changed=2 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
node02-debian : ok=2 changed=2 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
node03-ubuntu : ok=2 changed=2 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
node04-centos : ok=2 changed=2 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
node05-windows : ok=2 changed=2 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
node06-debian : ok=2 changed=2 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
node07-ubuntu : ok=2 changed=2 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
node08-centos : ok=2 changed=2 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
ansible-playbook playbooks/06_free_strategy.yaml 2.03s user 1.10s system 28% cpu 11.079 total
ก่อนจะไปหัวข้อสุดท้าย ถ้าอยากรู้ว่าตัวเองเข้าใจเรื่อง strategies จริงมั้ย? ให้ลองปรับ playbook โดยตัดเงื่อนไข when
ออกดูครับ สังเกตว่าเวลาที่ใช้ระหว่าง linear
กับ free
strategy จะไม่ต่างกันเลย ถ้าคุณรู้ว่าเพราะอะไรแสดงว่าเข้าใจเรื่องนี้แล้ว
การใช้ Asynchronous Tasks เพื่อ Run Tasks ได้โดยไม่ต้องรอผล
เทคนิคนี้เป็นวิธีควบคุม multitasking ในระดับ task
ปกติแล้ว Ansible จะ run tasks แบบ synchronous ซึ่งก็คือการเปิด connection ไว้ไม่ไปไหนรอจนกระทั่ง task นั้นทำงานเสร็จจึงจะทำ task ถัดไป (คิดภาพว่าเราเอารถไปล้างที่คาร์แคร์แล้วเราก็นั่งรอทั้ง ๆ ที่เราจะต้องไปทำธุระอื่นต่อ)
เราสามารถ run tasks แบบ asynchronous ได้โดยที่เราจะไม่รอให้ task นั้นเสร็จ แต่เราจะสลับไปทำ tasks อื่นในระหว่างนั้นแล้วค่อยมาเช็คผลลัพธ์ทีหลัง ซึ่งมักจะใช้กับงานที่มีลักษณะดังนี้
- การติดตั้งหรือ update applications
- การ update patches บน server
- การ backup ข้อมูล
- การ run script tests ต่าง ๆ เช่น ping test หรือ stress tests
- การ query ข้อมูลจาก databases หรือ APIs ที่อยู่ข้างนอก
- และอื่น ๆ อีกมากมาย
ข้อจำกัดของ async tasks คือมันต้องไม่มีความเกี่ยวข้องหรือมีลำดับก่อนหลังกับ tasks อื่น ๆ
เราใช้ async
keyword กำหนดให้ run task นั้นแบบ asynchronous โดยจะกำหนดเป็นวินาทีว่าจะให้ task นั้นทำงานนานสูงสุดเท่าไร ส่วน poll
เป็นการกำหนดให้ตรวจสอบสถานะของ task ทุก ๆ กี่วินาที ถ้าใส่เป็น 0 จะเป็นการสั่ง run แบบไม่รอผลลัพธ์เลย (เดี๋ยวมาเช็คเอาเองทีหลัง)
ตัวอย่าง playbook playbooks/07_synchronous.yaml
เป็นแบบ synchronous
---
- name: "Synchronous"
hosts: nodes[0:2] # 3 nodes
tasks:
- name: Task 1
command: sleep 30
- name: Task 2
command: sleep 5
- name: Task 3
command: sleep 5
- name: Task 4
command: sleep 5
- name: Task 5
command: sleep 5
สังเกตว่า task 1 เป็น task ที่ใช้เวลานาน สิ่งที่จะเกิดขึ้นเมื่อเรา run playbook นี้คือ…
- Ansible run task 1 ใน nodes ทั้งหมด (30 วินาที)
- จากนั้นจึง run task 2 ใน nodes ทั้งหมด (5 วินาที)
- ทำไปเรื่อย ๆ จนครบ 5 tasks (5 * 3 วินาที)
ซึ่งใช้เวลาประมาณ 30 + (5 * 4) = 50 วินาที (ไม่รวม overhead)
Command:
time ansible-playbook playbooks/07_synchronous.yaml
Output:
PLAY [Synchronous] *****************************************************************************************************
TASK [Task 1] *****************************************************************************************************************
changed: [node02-debian]
changed: [node01-windows]
changed: [node03-ubuntu]
TASK [Task 2] *****************************************************************************************************************
changed: [node02-debian]
changed: [node01-windows]
changed: [node03-ubuntu]
TASK [Task 3] *****************************************************************************************************************
changed: [node01-windows]
changed: [node02-debian]
changed: [node03-ubuntu]
TASK [Task 4] *****************************************************************************************************************
changed: [node03-ubuntu]
changed: [node01-windows]
changed: [node02-debian]
TASK [Task 5] *****************************************************************************************************************
changed: [node03-ubuntu]
changed: [node02-debian]
changed: [node01-windows]
PLAY RECAP ********************************************************************************************************************
node01-windows : ok=5 changed=5 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node02-debian : ok=5 changed=5 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node03-ubuntu : ok=5 changed=5 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ansible-playbook playbooks/07_synchronous.yaml 1.21s user 1.04s system 4% cpu 51.817 total
คราวนี้เราจะไม่รอแล้ว เราจะปรับ task 1 ให้เป็น asynchronous แบบ playbooks/08_asynchronous1.yaml
นี้
---
- name: "Asynchronous #1"
hosts: nodes[0:2] # 3 nodes
tasks:
- name: Task 1
command: sleep 30
async: 50
poll: 0
register: result1
- name: Task 2
command: sleep 5
- name: Task 3
command: sleep 5
- name: Task 4
command: sleep 5
- name: Task 5
command: sleep 5
- name: Check Task 1 Result
async_status:
jid: "{{ result1.ansible_job_id }}"
register: job_result1
until: job_result1.finished
retries: 30
สิ่งที่จะเกิดขึ้นเมื่อเรา run playbook นี้คือ…
- เริ่ม run task 1 แบบ async ในทุก nodes โดยกำหนด
poll: 0
เพื่อไม่ต้องรอผล - สลับไป run task 2 ใน nodes ทั้งหมดทันที (5 วินาที)
- จากนั้น run task 3 ใน nodes ทั้งหมดทันที (5 วินาที)
- จากนั้น run task 4 ใน nodes ทั้งหมดทันที (5 วินาที)
- จากนั้น run task 5 ใน nodes ทั้งหมดทันที (5 วินาที)
- กลับมาเช็คผลของ task 1 ไปเรื่อย ๆ จนกว่าจะเสร็จด้วย
async_status
และuntil
คราวนี้จะใช้เวลาประมาณ (5 * 4) + 10 ซึ่งเป็นเวลาที่เหลือจาก 30 วินาที = 30 วินาที (ไม่รวม overhead)
Command:
time ansible-playbook playbooks/08_asynchronous1.yaml
Output:
PLAY [Asynchronous #1] ********************************************************************************************************
TASK [Task 1] *****************************************************************************************************************
changed: [node03-ubuntu]
changed: [node02-debian]
changed: [node01-windows]
TASK [Task 2] *****************************************************************************************************************
changed: [node02-debian]
changed: [node03-ubuntu]
changed: [node01-windows]
TASK [Task 3] *****************************************************************************************************************
changed: [node02-debian]
changed: [node01-windows]
changed: [node03-ubuntu]
TASK [Task 4] *****************************************************************************************************************
changed: [node01-windows]
changed: [node02-debian]
changed: [node03-ubuntu]
TASK [Task 5] *****************************************************************************************************************
changed: [node01-windows]
changed: [node03-ubuntu]
changed: [node02-debian]
TASK [Check Task 1 Result] ****************************************************************************************************
FAILED - RETRYING: [node03-ubuntu]: Check Task 1 Result (30 retries left).
FAILED - RETRYING: [node02-debian]: Check Task 1 Result (30 retries left).
FAILED - RETRYING: [node01-windows]: Check Task 1 Result (30 retries left).
FAILED - RETRYING: [node03-ubuntu]: Check Task 1 Result (29 retries left).
FAILED - RETRYING: [node02-debian]: Check Task 1 Result (29 retries left).
FAILED - RETRYING: [node01-windows]: Check Task 1 Result (29 retries left).
changed: [node01-windows]
changed: [node03-ubuntu]
changed: [node02-debian]
PLAY RECAP ********************************************************************************************************************
node01-windows : ok=6 changed=6 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node02-debian : ok=6 changed=6 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node03-ubuntu : ok=6 changed=6 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ansible-playbook playbooks/08_asynchronous1.yaml 1.31s user 1.44s system 8% cpu 32.898 total
ปิดท้ายด้วย playbook playbooks/09_asynchronous2.yaml
ซึ่งเป็นการใช้ async
ในหลาย ๆ tasks
---
- name: "Asynchronous #2"
hosts: nodes[0:2] # 3 nodes
tasks:
- name: Define job_ids List
set_fact:
job_ids: []
- name: Task 1
command: sleep 10
async: 15
poll: 0
register: result1
- name: Task 2
command: sleep 10
async: 15
poll: 0
register: result2
- name: Task 3
command: sleep 10
async: 15
poll: 0
register: result3
- name: Task 4
command: sleep 10
async: 15
poll: 0
register: result4
- name: Task 5
command: sleep 10
async: 15
poll: 0
register: result5
- name: Get Job IDs
set_fact:
job_ids: >
{% if item.ansible_job_id is defined -%}
{{ job_ids + [item.ansible_job_id] }}
{% else -%}
{{ job_ids }}
{% endif %}
with_items: "{{ [ result1, result2, result3, result4, result5 ] }}"
- name: Show Job IDs
debug:
msg: "{{ job_ids }}"
- name: Wait for All Jobs to Finish
async_status:
jid: "{{ item }}"
register: job_result
until: job_result.finished
retries: 30
with_items: "{{ job_ids }}"
ขออธิบายรายละเอียดโดยเน้นที่วิธีการเขียนแทนลำดับการทำงานหรือเวลาที่ใช้ละกันนะครับ
- สร้าง variable
job_ids
เป็น list ไว้เก็บ job ทั้งหมด - Run task 1-5 แบบ async โดยไม่รอผล เก็บ output ไว้ใน variable
result1
ถึงresult5
- Loop เพื่อดึงค่า
ansible_job_id
จากresult1
ถึงresult5
ลงในjob_ids
- แสดงค่า
job_ids
เพื่อดูรายการ job ทั้งหมดที่ run อยู่ - ใช้
async_status
และuntil
ในการตรวจสอบสถานะของแต่ละ job ในjob_ids
จนกว่าทุกอันจะเสร็จ
ถ้าดูจาก tasks ทั้งหมดมันควรจะใช้เวลา (10 * 5) = 50 วินาที แต่การใช้ async
ช่วยก็จะเหลือแค่ 10 วินาที (ไม่รวม overhead)
Command:
time ansible-playbook playbooks/09_asynchronous2.yaml
Output:
PLAY [Asynchronous #2] ********************************************************************************************************
TASK [Define job_ids List] ****************************************************************************************************
ok: [node01-windows]
ok: [node02-debian]
ok: [node03-ubuntu]
TASK [Task 1] *****************************************************************************************************************
changed: [node02-debian]
changed: [node03-ubuntu]
changed: [node01-windows]
TASK [Task 2] *****************************************************************************************************************
changed: [node01-windows]
changed: [node02-debian]
changed: [node03-ubuntu]
TASK [Task 3] *****************************************************************************************************************
changed: [node01-windows]
changed: [node02-debian]
changed: [node03-ubuntu]
TASK [Task 4] *****************************************************************************************************************
changed: [node01-windows]
changed: [node03-ubuntu]
changed: [node02-debian]
TASK [Task 5] *****************************************************************************************************************
changed: [node01-windows]
changed: [node02-debian]
changed: [node03-ubuntu]
TASK [Get Job IDs] ************************************************************************************************************
ok: [node01-windows] => (item={'failed': 0, 'started': 1, 'finished': 0, 'ansible_job_id': 'j785886335879.1567', 'results_file': '/root/.ansible_async/j785886335879.1567', 'changed': True})
ok: [node02-debian] => (item={'failed': 0, 'started': 1, 'finished': 0, 'ansible_job_id': 'j73540722787.1554', 'results_file': '/root/.ansible_async/j73540722787.1554', 'changed': True})
ok: [node01-windows] => (item={'failed': 0, 'started': 1, 'finished': 0, 'ansible_job_id': 'j774700301569.1592', 'results_file': '/root/.ansible_async/j774700301569.1592', 'changed': True})
ok: [node02-debian] => (item={'failed': 0, 'started': 1, 'finished': 0, 'ansible_job_id': 'j650389322052.1579', 'results_file': '/root/.ansible_async/j650389322052.1579', 'changed': True})
ok: [node03-ubuntu] => (item={'failed': 0, 'started': 1, 'finished': 0, 'ansible_job_id': 'j922555708211.1576', 'results_file': '/root/.ansible_async/j922555708211.1576', 'changed': True})
ok: [node01-windows] => (item={'failed': 0, 'started': 1, 'finished': 0, 'ansible_job_id': 'j270988671300.1617', 'results_file': '/root/.ansible_async/j270988671300.1617', 'changed': True})
ok: [node02-debian] => (item={'failed': 0, 'started': 1, 'finished': 0, 'ansible_job_id': 'j327965088006.1604', 'results_file': '/root/.ansible_async/j327965088006.1604', 'changed': True})
ok: [node01-windows] => (item={'failed': 0, 'started': 1, 'finished': 0, 'ansible_job_id': 'j589267662819.1642', 'results_file': '/root/.ansible_async/j589267662819.1642', 'changed': True})
ok: [node03-ubuntu] => (item={'failed': 0, 'started': 1, 'finished': 0, 'ansible_job_id': 'j44228942109.1601', 'results_file': '/root/.ansible_async/j44228942109.1601', 'changed': True})
ok: [node02-debian] => (item={'failed': 0, 'started': 1, 'finished': 0, 'ansible_job_id': 'j312569620111.1629', 'results_file': '/root/.ansible_async/j312569620111.1629', 'changed': True})
ok: [node01-windows] => (item={'failed': 0, 'started': 1, 'finished': 0, 'ansible_job_id': 'j504586791473.1667', 'results_file': '/root/.ansible_async/j504586791473.1667', 'changed': True})
ok: [node03-ubuntu] => (item={'failed': 0, 'started': 1, 'finished': 0, 'ansible_job_id': 'j13662463163.1626', 'results_file': '/root/.ansible_async/j13662463163.1626', 'changed': True})
ok: [node02-debian] => (item={'failed': 0, 'started': 1, 'finished': 0, 'ansible_job_id': 'j553593370257.1654', 'results_file': '/root/.ansible_async/j553593370257.1654', 'changed': True})
ok: [node03-ubuntu] => (item={'failed': 0, 'started': 1, 'finished': 0, 'ansible_job_id': 'j430161236072.1651', 'results_file': '/root/.ansible_async/j430161236072.1651', 'changed': True})
ok: [node03-ubuntu] => (item={'failed': 0, 'started': 1, 'finished': 0, 'ansible_job_id': 'j334984357003.1676', 'results_file': '/root/.ansible_async/j334984357003.1676', 'changed': True})
TASK [Show Job IDs] ***********************************************************************************************************
ok: [node01-windows] => {
"msg": [
"j785886335879.1567",
"j774700301569.1592",
"j270988671300.1617",
"j589267662819.1642",
"j504586791473.1667"
]
}
ok: [node02-debian] => {
"msg": [
"j73540722787.1554",
"j650389322052.1579",
"j327965088006.1604",
"j312569620111.1629",
"j553593370257.1654"
]
}
ok: [node03-ubuntu] => {
"msg": [
"j922555708211.1576",
"j44228942109.1601",
"j13662463163.1626",
"j430161236072.1651",
"j334984357003.1676"
]
}
TASK [Wait for All Jobs to Finish] ********************************************************************************************
FAILED - RETRYING: [node03-ubuntu]: Wait for All Jobs to Finish (30 retries left).
FAILED - RETRYING: [node01-windows]: Wait for All Jobs to Finish (30 retries left).
FAILED - RETRYING: [node02-debian]: Wait for All Jobs to Finish (30 retries left).
FAILED - RETRYING: [node03-ubuntu]: Wait for All Jobs to Finish (29 retries left).
FAILED - RETRYING: [node02-debian]: Wait for All Jobs to Finish (29 retries left).
FAILED - RETRYING: [node01-windows]: Wait for All Jobs to Finish (29 retries left).
changed: [node03-ubuntu] => (item=j922555708211.1576)
changed: [node02-debian] => (item=j73540722787.1554)
changed: [node01-windows] => (item=j785886335879.1567)
changed: [node03-ubuntu] => (item=j44228942109.1601)
changed: [node02-debian] => (item=j650389322052.1579)
changed: [node01-windows] => (item=j774700301569.1592)
changed: [node02-debian] => (item=j327965088006.1604)
changed: [node03-ubuntu] => (item=j13662463163.1626)
changed: [node01-windows] => (item=j270988671300.1617)
changed: [node01-windows] => (item=j589267662819.1642)
changed: [node03-ubuntu] => (item=j430161236072.1651)
changed: [node02-debian] => (item=j312569620111.1629)
changed: [node03-ubuntu] => (item=j334984357003.1676)
changed: [node01-windows] => (item=j504586791473.1667)
changed: [node02-debian] => (item=j553593370257.1654)
PLAY RECAP ********************************************************************************************************************
node01-windows : ok=9 changed=6 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node02-debian : ok=9 changed=6 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node03-ubuntu : ok=9 changed=6 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ansible-playbook playbooks/09_asynchronous2.yaml 1.34s user 1.65s system 22% cpu 13.411 total
บทสรุป (Conclusion)
จากบทความนี้เราได้เรียนรู้เทคนิคต่าง ๆ ในการเพิ่มความเร็วให้กับ Ansible playbooks ทั้งในส่วนของการทำ multitasking ในระดับ node และ task ซึ่งสามารถสรุปได้ดังนี้
- การกำหนดค่า
forks
ให้เหมาะสมกับจำนวน nodes เพื่อ run หลาย nodes พร้อมกัน - การใช้
serial
แบ่งจำนวน nodes ออกเป็นรอบ (batch) เพื่อลดความเสี่ยงจากการ run พร้อมกันจำนวนมาก - การเลือกใช้ strategies ระหว่าง
linear
(default) กับfree
ซึ่งการใช้free
จะช่วยให้ทำงานเร็วขึ้นมากในกรณีที่มี tasks ที่ไม่จำเป็นต้อง run ใน nodes บางตัว - การใช้
async
กับ tasks ที่ใช้เวลานานเพื่อให้สามารถไป run tasks อื่น ๆ ได้ระหว่างรอ
ทั้งนี้ทุกเทคนิคล้วนมีข้อดีข้อเสีย เราต้องพิจารณาตามความเหมาะสมด้วย อย่างไรก็ตามผมเชื่อว่าเทคนิคเหล่านี้จะช่วยให้ playbooks ของคุณทำงานได้เร็วขึ้นอย่างเห็นได้ชัดแน่นอน แล้วเจอกันใหม่ในบทความหน้าครับ