Skip to content

เขียน Ansible Playbook ยังไงให้ทำงานได้เร็วขึ้น?

Published:| Updated:

Table of Contents

บทนำ (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 ไปด้วยกันเพื่อให้เข้าใจมากขึ้น เครื่องคุณจะต้อง…

  1. ติดตั้ง Python 3 แล้ว (ผมใช้ version 3.12.3)
  2. ติดตั้ง Ansible แล้ว (ผมใช้ version 2.16.5)
  3. ติดตั้ง sshpass แล้ว (ผมใช้ version 1.10)
  4. ติดตั้ง Docker แล้ว (ผมใช้ version 26.0.0, build 2ae903e)
  5. ติดตั้ง 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 ทำงานคือ…

  1. Run task 1 เพื่อ start service A บน server ที่ 1-5 พร้อมกันในเวลาเดียวกัน
  2. จากนั้นมันจะรอให้ task 1 เสร็จทั้งหมดในทุก nodes ก่อนแล้วจึงจบ

แต่คราวนี้ถ้าเรามีมากกว่า 1 task หละ? เช่น start service A (task 1) และ start service B (task 2) สิ่งที่เกิดขึ้นคือ…

  1. Run task 1 เพื่อ start service A บน server ที่ 1-5 พร้อมกันในเวลาเดียวกัน
  2. จากนั้นมันจะรอให้ task 1 เสร็จทั้งหมดในทุก nodes ก่อนแล้วจึงไปต่อ task 2
  3. Run task 2 เพื่อ start service B บน server ที่ 1-5 พร้อมกันในเวลาเดียวกัน
  4. จากนั้นมันจะรอให้ 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) สิ่งที่เกิดขึ้นมันจะต้องเป็นแบบนี้

  1. Run task 1 และ task 2 เพื่อ start service A และ B บน server ที่ 1-5 พร้อมกันในเวลาเดียวกัน
  2. จากนั้นมันจะรอให้ 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 นี้คือ…

  1. Run task 1 ใน node 1 (5 วินาที)
  2. Run task 1 ใน node 2 (5 วินาที)
  3. Run task 1 ใน node 3 (5 วินาที)
  4. Run task 2 ใน node 1 (5 วินาที)
  5. Run task 2 ใน node 2 (5 วินาที)
  6. 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)

  1. Run task 1 ใน node 1, 2 และ 3 พร้อมกัน (5 วินาที)
  2. 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 มากเกินไปเพราะอาจส่งผลเสียได้ เช่น


การใช้ 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. รอบที่ 1: Run task ใน node 1 และ 2 พร้อมกัน (2 วินาที)
  2. รอบที่ 2: Run task ใน node 3 และ 4 พร้อมกัน (2 วินาที)
  3. รอบที่ 3: Run task ใน node 5 และ 6 พร้อมกัน (2 วินาที)
  4. รอบที่ 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. รอบที่ 1: Run task ใน node 1 (2 วินาที)
  2. รอบที่ 2: Run task ใน node 2 และ 3 พร้อมกัน (2 วินาที)
  3. รอบที่ 3: Run task ใน node 4, 5 และ 6 พร้อมกัน (2 วินาที)
  4. รอบที่ 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 นี้คือ…

  1. Task 1 for Windows จะทำงานเฉพาะ node 1 และ 5 เท่านั้น แต่ nodes ที่เหลือก็ต้องรอให้ task นี้เสร็จก่อน (5 วินาที)
  2. Task 2 for Debian จะทำงานเฉพาะ node 2 และ 6 เท่านั้น แต่ nodes ที่เหลือก็ต้องรอให้ task นี้เสร็จก่อนอ (5 วินาที)
  3. Task 3 for Ubuntu จะทำงานเฉพาะ node 3 และ 7 เท่านั้น แต่ nodes ที่เหลือก็ต้องรอให้ task นี้เสร็จก่อน (5 วินาที)
  4. Task 4 for CentOS จะทำงานเฉพาะ node 4 และ 8 เท่านั้น แต่ nodes ที่เหลือก็ต้องรอให้ task นี้เสร็จก่อน (5 วินาที)
  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 เดิม สิ่งที่จะเกิดขึ้นคือ…

  1. Task 1 for Windows จะถูก run ใน node 1 และ 5 ในขณะเดียวกัน task 2, 3 และ 4 ก็จะถูก run ใน nodes อื่น ๆ ตามเงื่อนไขได้ทันทีโดยไม่ต้องรอให้เสร็จทีละ task (1, 2, 3, …) แบบ linear
  2. เมื่อ 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 อื่นในระหว่างนั้นแล้วค่อยมาเช็คผลลัพธ์ทีหลัง ซึ่งมักจะใช้กับงานที่มีลักษณะดังนี้

ข้อจำกัดของ 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 นี้คือ…

  1. Ansible run task 1 ใน nodes ทั้งหมด (30 วินาที)
  2. จากนั้นจึง run task 2 ใน nodes ทั้งหมด (5 วินาที)
  3. ทำไปเรื่อย ๆ จนครบ 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 นี้คือ…

  1. เริ่ม run task 1 แบบ async ในทุก nodes โดยกำหนด poll: 0 เพื่อไม่ต้องรอผล
  2. สลับไป run task 2 ใน nodes ทั้งหมดทันที (5 วินาที)
  3. จากนั้น run task 3 ใน nodes ทั้งหมดทันที (5 วินาที)
  4. จากนั้น run task 4 ใน nodes ทั้งหมดทันที (5 วินาที)
  5. จากนั้น run task 5 ใน nodes ทั้งหมดทันที (5 วินาที)
  6. กลับมาเช็คผลของ 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 }}"

ขออธิบายรายละเอียดโดยเน้นที่วิธีการเขียนแทนลำดับการทำงานหรือเวลาที่ใช้ละกันนะครับ

  1. สร้าง variable job_ids เป็น list ไว้เก็บ job ทั้งหมด
  2. Run task 1-5 แบบ async โดยไม่รอผล เก็บ output ไว้ใน variable result1 ถึง result5
  3. Loop เพื่อดึงค่า ansible_job_id จาก result1 ถึง result5 ลงใน job_ids
  4. แสดงค่า job_ids เพื่อดูรายการ job ทั้งหมดที่ run อยู่
  5. ใช้ 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 ซึ่งสามารถสรุปได้ดังนี้

ทั้งนี้ทุกเทคนิคล้วนมีข้อดีข้อเสีย เราต้องพิจารณาตามความเหมาะสมด้วย อย่างไรก็ตามผมเชื่อว่าเทคนิคเหล่านี้จะช่วยให้ playbooks ของคุณทำงานได้เร็วขึ้นอย่างเห็นได้ชัดแน่นอน แล้วเจอกันใหม่ในบทความหน้าครับ


Does it help?

Don’t miss out on future updates - Follow or Subscribe me!

And don’t forget to share it with your friends. Your share means a lot.