LabHub
学习 学习路径 课程

Ansible 实战

先量清楚剧本为什么慢,再动手改

在 LabHub 中继续学习

한국어 원문으로 표시합니다.

목표

플레이북이 느린 이유를 콜백으로 먼저 재고, 팩트 수집·캐시·파이프라이닝·동시 실행·비동기라는 다섯 개의 손잡이를 차례로 돌립니다. 마지막에는 측정 결과에서 고칠 곳을 뽑아 주는 작은 도구를 직접 만듭니다.

왜 중요한가

"플레이북이 느리다" 는 신고는 늘 오지만 그 안에는 성격이 다른 원인이 섞여 있습니다. 대상 백 대에 팩트를 전부 모으느라 느린 것과, 태스크마다 SSH 를 세 번 왕복해서 느린 것과, 한 대가 십 분짜리 작업을 하는 동안 나머지 아흔아홉 대가 서서 기다려서 느린 것은 고치는 방법이 완전히 다릅니다. 그래서 순서가 중요합니다 — 먼저 재고, 그다음 고칩니다. 재지 않고 고치면 아는 데를 고치게 되고, 아는 데는 대개 병목이 아닙니다. 그리고 손잡이마다 대가가 있습니다. 캐시는 빠르지만 거짓 사실을 만들고, free 전략은 빠르지만 호스트 사이의 순서 약속을 깹니다. 이 실습은 각 손잡이를 돌려 보고 그 대가까지 함께 봅니다.

단계

  1. /root/ansperf/ansible.cfg 에 기본 인벤토리를 ./inventory/hosts.ini 로 두고 ansible.posix.profile_tasks 콜백을 켜세요. /root/ansperf/inventory/hosts.ini 에는 web 그룹의 web1(work_sec 1)·web2(2)와 db 그룹의 db1(3)을 적습니다 (셋 다 ansible_host=127.0.0.1 ansible_port=2222, [all:vars]ansible_userroot). /root/ansperf/slow.yml 은 팩트를 모두 모으고 3초·2초·1초를 각각 쉬는 태스크 셋과 /root/ansperf/out/baseline.marker 를 쓰는 태스크 하나로 만드세요. 실행 결과를 /root/ansperf/out/profile_baseline.txt 에 저장합니다.
  2. /root/ansperf/subsets.yml 을 만드세요 — 플레이 수준에서 gather_facts: false 로 자동 수집을 끄고, web1ansible.builtin.setup두 번 부릅니다. 한 번은 인자 없이 전부, 한 번은 gather_subset: [min] 으로 최소만 모읍니다. 각각 register 로 받아 /root/ansperf/out/facts_all.json/root/ansperf/out/facts_min.json 에 그 태스크가 돌려준 팩트를 JSON 으로 저장하고 실행하세요.
  3. /root/ansperf/ansible.cfggathering = smart, fact_caching = ansible.builtin.jsonfile, fact_caching_connection = ./factcache, fact_caching_timeout = 3600 을 더하세요. 그리고 대상이 스스로 알려 주는 지역 팩트를 만듭니다 — /etc/ansible/facts.d/lab.fact[app] 절과 release=1.0.0 을 적으세요. /root/ansperf/release.ymlweb1ansible_local.lab.app.release 를 변수 outfile 이 가리키는 파일에 쓰는 플레이북입니다. 캐시 디렉터리를 비운 뒤 이 플레이북을 돌려 /root/ansperf/out/rel1.txt 를 남기세요.
  4. 앞 단계가 끝난 지금 캐시에는 1.0.0 이 들어 있습니다. /etc/ansible/facts.d/lab.factrelease2.0.0 으로 올리세요. 플레이북을 그대로 다시 돌려 /root/ansperf/out/rel2_cached.txt 를 남기고, 이어서 캐시를 비우는 옵션을 붙여 다시 돌려 /root/ansperf/out/rel3_fresh.txt 를 남기세요. 세 파일의 값이 어떻게 갈리는지 직접 확인합니다.
  5. /root/ansperf/ansible.cfg[ssh_connection] 절을 더하고 pipelining = True 를 켜세요. 그다음 기본값과 다른 설정을 전부 담은 출력(ansible-config dump --only-changed -t all)을 /root/ansperf/out/config.txt 에 저장하고, 파이프라이닝을 켠 채로 web1 에 애드혹 ping 이 되는지 확인하세요.
  6. /root/ansperf/ansible.cfg[defaults]forks = 10 을 더하세요. /root/ansperf/free.yml 을 만듭니다 — strategy: free 이고 팩트는 모으지 않으며, 태스크 둘로 이루어집니다. 첫째는 그 호스트의 work_sec 만큼 쉬고, 둘째는 out/free-<호스트이름>.txt<호스트이름> <work_sec> 한 줄을 씁니다. 실행 출력은 /root/ansperf/out/free.txt 에 저장하세요.
  7. /root/ansperf/async.yml 을 만드세요 — web1 에서 8초짜리 작업을 async: 120 poll: 0 으로 던져 놓고 register 로 받습니다. 그다음 태스크에서 /root/ansperf/out/meanwhile.txt 를 쓰고(그 사이에 할 일이 있다는 뜻입니다), ansible.builtin.async_status 로 끝날 때까지 기다린 뒤 그 결과를 /root/ansperf/out/async.json 에 JSON 으로 저장하고 실행하세요.
  8. /root/ansperf/slowest.sh 를 만드세요 — 첫 인자로 받은 측정 출력 파일에서 콜백 요약을 읽어 오래 걸린 순서대로 <초>s <태스크이름> 을 한 줄씩 출력합니다. 몇 줄을 낼지는 둘째 인자로 받고 기본값은 3 입니다. 파일이 없으면 표준 오류에 알리고 0 이 아닌 값으로 끝냅니다. 이 도구를 1단계의 /root/ansperf/out/profile_baseline.txt 에 돌려 결과를 /root/ansperf/out/slowest.txt 에 저장하세요.

참고

고치기 전에 먼저 잰다

/root/ansperf/ansible.cfg 에 기본 인벤토리를 ./inventory/hosts.ini 로 두고 ansible.posix.profile_tasks 콜백을 켜세요. /root/ansperf/inventory/hosts.ini 에는 web 그룹의 web1(work_sec 1)·web2(2)와 db 그룹의 db1(3)을 적습니다 (셋 다 ansible_host=127.0.0.1 ansible_port=2222, [all:vars]ansible_userroot). /root/ansperf/slow.yml 은 팩트를 모두 모으고 3초·2초·1초를 각각 쉬는 태스크 셋과 /root/ansperf/out/baseline.marker 를 쓰는 태스크 하나로 만드세요. 실행 결과를 /root/ansperf/out/profile_baseline.txt 에 저장합니다.

콜백은 설치가 아니라 설정입니다 — callbacks_enabled 에 이름을 적으면 켜집니다. ansible.posix 컬렉션은 이 이미지에 이미 들어 있습니다. 이 콜백은 태스크마다 걸린 시간을 찍고, 실행 끝에 오래 걸린 순서로 요약을 한 번 더 찍습니다. 이 단계의 값어치는 최적화가 아니라 기준선입니다. 재지 않고 고치면 무엇이 좋아졌는지 아무도 말할 수 없습니다.

팩트 수집이 무엇을 들고 오는지 세어 본다

/root/ansperf/subsets.yml 을 만드세요 — 플레이 수준에서 gather_facts: false 로 자동 수집을 끄고, web1ansible.builtin.setup두 번 부릅니다. 한 번은 인자 없이 전부, 한 번은 gather_subset: [min] 으로 최소만 모읍니다. 각각 register 로 받아 /root/ansperf/out/facts_all.json/root/ansperf/out/facts_min.json 에 그 태스크가 돌려준 팩트를 JSON 으로 저장하고 실행하세요.

gather_facts: true 는 플레이 맨 앞에 setup 태스크를 몰래 하나 끼워 넣는 것과 같습니다. 끄고 필요한 자리에서 직접 부르면 언제 얼마나 모을지를 내가 정하게 됩니다. setup 태스크를 register 로 받으면 그 실행이 돌려준 팩트만 <이름>.ansible_facts 에 담깁니다 — 이미 호스트에 붙어 있는 팩트와 섞이지 않아 비교하기 좋습니다. JSON 으로 예쁘게 저장하는 필터는 to_nice_json 입니다. 무엇이 빠졌는지는 jq 'keys' 로 봅니다.

팩트를 파일에 캐시해 두 번째 실행을 건너뛴다

/root/ansperf/ansible.cfggathering = smart, fact_caching = ansible.builtin.jsonfile, fact_caching_connection = ./factcache, fact_caching_timeout = 3600 을 더하세요. 그리고 대상이 스스로 알려 주는 지역 팩트를 만듭니다 — /etc/ansible/facts.d/lab.fact[app] 절과 release=1.0.0 을 적으세요. /root/ansperf/release.ymlweb1ansible_local.lab.app.release 를 변수 outfile 이 가리키는 파일에 쓰는 플레이북입니다. 캐시 디렉터리를 비운 뒤 이 플레이북을 돌려 /root/ansperf/out/rel1.txt 를 남기세요.

gatheringsmart 면 '캐시에 있으면 다시 모으지 않는다' 는 뜻입니다. 캐시가 없는 implicit 과 캐시를 안 쓰는 explicit 사이의 자리입니다. jsonfile 캐시는 호스트마다 JSON 파일 하나를 남기므로 무엇이 들어 있는지 눈으로 열어 볼 수 있습니다. /etc/ansible/facts.d/*.fact 는 setup 모듈이 알아서 읽어 ansible_local.<파일이름> 아래에 담습니다 — INI 꼴이면 절 이름이 그대로 한 층이 됩니다.

캐시가 만드는 거짓 사실

앞 단계가 끝난 지금 캐시에는 1.0.0 이 들어 있습니다. /etc/ansible/facts.d/lab.factrelease2.0.0 으로 올리세요. 플레이북을 그대로 다시 돌려 /root/ansperf/out/rel2_cached.txt 를 남기고, 이어서 캐시를 비우는 옵션을 붙여 다시 돌려 /root/ansperf/out/rel3_fresh.txt 를 남기세요. 세 파일의 값이 어떻게 갈리는지 직접 확인합니다.

캐시는 '다시 물어보지 않는다' 는 뜻이고, 그래서 대상이 바뀌어도 앤서블은 모릅니다. 이것이 캐시를 켤 때 함께 사는 위험입니다. ansible-playbook 에는 그 자리에서 캐시를 버리고 다시 모으게 하는 옵션이 있습니다(--help 에서 cache 로 찾아보세요). 실무에서는 캐시 유효 시간을 배포 주기보다 짧게 잡거나, 배포 파이프라인의 첫 단계에서 캐시를 비웁니다.

SSH 왕복을 줄이고, 바뀐 설정을 파일로 남긴다

/root/ansperf/ansible.cfg[ssh_connection] 절을 더하고 pipelining = True 를 켜세요. 그다음 기본값과 다른 설정을 전부 담은 출력(ansible-config dump --only-changed -t all)을 /root/ansperf/out/config.txt 에 저장하고, 파이프라이닝을 켠 채로 web1 에 애드혹 ping 이 되는지 확인하세요.

파이프라이닝을 끄면 앤서블은 태스크마다 모듈 파일을 대상에 복사하고, 실행하고, 지웁니다 — SSH 왕복이 여러 번입니다. 켜면 모듈을 원격 파이썬의 표준 입력으로 흘려보내 왕복을 한 번으로 줄입니다. 대개 가장 값싸고 큰 이득입니다. 다만 대상의 sudoers 에 requiretty 가 켜져 있으면 켤 수 없습니다 — 그래서 기본값이 꺼짐입니다. 이 설정이 정말 먹었는지는 ansible-config dump --only-changed -t all 로 확인합니다. -t all 이 없으면 연결 플러그인의 설정은 보이지 않습니다.

동시에 몇 대를 붙잡을지, 서로를 기다리게 할지

/root/ansperf/ansible.cfg[defaults]forks = 10 을 더하세요. /root/ansperf/free.yml 을 만듭니다 — strategy: free 이고 팩트는 모으지 않으며, 태스크 둘로 이루어집니다. 첫째는 그 호스트의 work_sec 만큼 쉬고, 둘째는 out/free-<호스트이름>.txt<호스트이름> <work_sec> 한 줄을 씁니다. 실행 출력은 /root/ansperf/out/free.txt 에 저장하세요.

forks 는 앤서블이 동시에 붙잡는 대상 수입니다. 기본값 5 는 서버 수십 대짜리 인벤토리에서 바로 병목이 됩니다. strategy 는 다른 축입니다 — 기본값 linear 는 모든 호스트가 한 태스크를 끝내야 다음으로 넘어가고, free 는 각 호스트가 자기 속도로 끝까지 달립니다. free 가 늘 좋은 것은 아닙니다. 호스트 사이에 순서 약속이 있는 플레이북(먼저 빼고 나중에 넣기)에서는 그 약속이 깨집니다. 그래서 serial 이나 run_once 와 함께 쓸 때는 특히 조심해야 합니다.

긴 작업은 던져 놓고 나중에 거둔다

/root/ansperf/async.yml 을 만드세요 — web1 에서 8초짜리 작업을 async: 120 poll: 0 으로 던져 놓고 register 로 받습니다. 그다음 태스크에서 /root/ansperf/out/meanwhile.txt 를 쓰고(그 사이에 할 일이 있다는 뜻입니다), ansible.builtin.async_status 로 끝날 때까지 기다린 뒤 그 결과를 /root/ansperf/out/async.json 에 JSON 으로 저장하고 실행하세요.

poll 이 0 이 아니면 앤서블이 그 자리에서 기다립니다. 0 으로 두면 작업 번호만 받고 곧장 다음 태스크로 갑니다 — 이것이 '던져 놓기' 입니다. async 값은 그 작업에 허락한 최대 시간입니다. 짧게 잡으면 멀쩡한 작업이 중간에 끊깁니다. 거두어들이는 것은 async_status 이고, 받은 ansible_job_id 를 넘깁니다. 한 번에 끝나 있지 않을 수 있으니 until 로 몇 번 더 물어봅니다. 이 방식이 맞는 자리는 '오래 걸리지만 서로 독립인 작업' 입니다. 결과가 바로 필요한 작업에 쓰면 복잡해지기만 합니다.

측정 결과에서 고칠 곳을 뽑아 주는 도구

/root/ansperf/slowest.sh 를 만드세요 — 첫 인자로 받은 측정 출력 파일에서 콜백 요약을 읽어 오래 걸린 순서대로 <초>s <태스크이름> 을 한 줄씩 출력합니다. 몇 줄을 낼지는 둘째 인자로 받고 기본값은 3 입니다. 파일이 없으면 표준 오류에 알리고 0 이 아닌 값으로 끝냅니다. 이 도구를 1단계의 /root/ansperf/out/profile_baseline.txt 에 돌려 결과를 /root/ansperf/out/slowest.txt 에 저장하세요.

요약은 등호로 이루어진 구분선 다음부터 시작하고, 한 줄은 <이름> ----- <초>s 꼴입니다. 구분선 앞에는 태스크마다 찍힌 시각 줄이 섞여 있으니 구분선을 만난 뒤부터만 읽어야 합니다. 초 단위 값은 항상 소수점 둘째 자리까지 찍히므로 그대로 옮기면 됩니다. 정렬은 sort -rn 이고, 이름에 공백이 들어 있으니 초와 이름 사이를 탭으로 갈라 두면 뒤에서 다루기 쉽습니다. 이 도구가 하는 일은 작지만 자리가 중요합니다 — 사람들이 최적화에서 가장 자주 하는 실수는 재지 않고 아는 데를 고치는 것입니다.