Ansible 실전 · 느린 플레이북의 정체 · 이론
재지 않고 고치면 아는 데만 고친다
한 줄 요약
플레이북이 느린 이유는 대개 다섯 가지 중 하나이고 고치는 방법이 서로 다르다 — 그러니 profile_tasks 로 먼저 재고 그다음에 손잡이를 고른다. 그리고 모든 손잡이에는 대가가 있다.
왜 이게 필요했나
"배포가 40분 걸린다" 는 신고는 어느 팀에나 온다. 그런데 그 40분 안에는 성격이 전혀 다른 시간이 섞여 있다.
- 대상 200대에서 팩트를 전부 모으느라 걸린 5분
- 태스크 하나마다 SSH 를 서너 번 왕복해서 쌓인 시간
- 기본값 5인
forks때문에 200대를 40묶음으로 나눠 돌린 시간 - 한 대에서 도는 10분짜리 패키지 설치를 나머지 199대가 서서 기다린 시간
- 사실은 그냥 원격 서비스가 느린 것
재지 않고 고치기 시작하면 사람들은 아는 데를 고친다. 대개 forks 를 올리고 끝낸다. 그런데 병목이 팩트 수집이었다면 forks 를 올려도 거의 안 빨라지고, 오히려 컨트롤러의 메모리와 대상의 부하만 늘어난다. 측정은 성능 작업의 준비 운동이 아니라 작업 그 자체의 첫 단계다.
어떻게 동작하나
먼저 잰다 — profile_tasks
앤서블의 콜백은 실행 중에 일어나는 일을 받아 출력을 만드는 플러그인이다. ansible.posix.profile_tasks 를 켜면 태스크마다 걸린 시간이 찍히고, 실행 끝에 오래 걸린 순서로 요약이 한 번 더 나온다. 설치할 것은 없고 설정 한 줄이면 된다.
[defaults]callbacks_enabled = ansible.posix.profile_tasks요약은 태스크 단위다. 그래서 "어느 태스크" 까지는 알려 주지만 "그 태스크의 무엇" 까지는 알려 주지 않는다. 그다음은 아래 손잡이들을 하나씩 대 보는 일이다.
손잡이 하나 — 팩트를 얼마나 모을 것인가
gather_facts: true 는 플레이 맨 앞에 setup 태스크를 몰래 하나 끼워 넣는 것과 같다. 이 태스크는 대상의 CPU·메모리·디스크·네트워크 인터페이스·마운트 목록을 전부 긁어 온다. 백 개가 넘는 팩트가 돌아오는데, 실제로 쓰는 것은 대개 ansible_distribution 하나다.
고르는 법은 둘이다.
gather_facts: false로 끄고, 정말 필요한 플레이에서만ansible.builtin.setup을 직접 부른다.gather_subset으로 묶음을 좁힌다.min은 배포판과 호스트 이름 정도만,network는 인터페이스만 가져온다.!all,!min,network처럼 빼고 더하는 표기도 쓴다.
손잡이 둘 — 팩트 캐시, 그리고 거짓 사실
fact_caching 을 켜면 한 번 모은 팩트를 파일이나 Redis 에 두고 다음 실행에서 다시 쓴다. jsonfile 캐시는 호스트마다 JSON 파일 하나를 남기므로 눈으로 열어 볼 수 있어 배우기에도 좋다. gathering = smart 와 함께 두면 "캐시에 있으면 다시 묻지 않는다" 가 된다.
여기에 이 모듈에서 가장 중요한 대가가 있다. 다시 묻지 않는다는 것은, 대상이 바뀌어도 앤서블이 모른다는 뜻이다. 어제 캐시에 담긴 디스크 용량·커널 판·사내 지역 팩트를 오늘 그대로 믿고 판단한다. 조건문이 팩트를 보고 있다면 그 조건은 어제의 사실로 갈린다. 실무에서 쓰는 방어는 셋이다 — 캐시 유효 시간을 배포 주기보다 짧게 잡기, 배포 파이프라인의 첫 단계에서 --flush-cache 로 비우기, 그리고 캐시된 팩트로 위험한 판단을 하지 않기.
손잡이 셋 — SSH 왕복과 파이프라이닝
파이프라이닝을 끄면 앤서블은 태스크마다 모듈 파일을 대상에 복사하고, 실행하고, 지운다. SSH 왕복이 여러 번이다. 켜면 모듈을 원격 파이썬의 표준 입력으로 흘려보내 왕복을 한 번으로 줄인다. 대개 가장 값싸고 가장 큰 이득이다.
기본값이 꺼짐인 이유가 있다. 대상의 sudoers 에 requiretty 가 켜져 있으면 파이프라이닝이 동작하지 않는다. 그래서 앤서블은 "되는 곳에서 켜라" 로 두었다. 이 설정은 [ssh_connection] 절에 들어가고, 정말 먹었는지는 ansible-config dump --only-changed -t all 로 봐야 한다 — -t all 이 없으면 연결 플러그인의 설정은 보이지 않는다.
손잡이 넷 — forks 와 strategy
둘은 다른 축이다.
| | 무엇을 정하나 | 기본값 |
| --- | --- | --- |
| forks | 동시에 붙잡는 대상 수 | 5 |
| strategy | 호스트들이 태스크마다 서로를 기다리는가 | linear |
forks 가 5 인데 대상이 200대면 앤서블은 40묶음으로 나눠 돈다. 올리면 빨라지지만 컨트롤러의 CPU 와 메모리, 그리고 열리는 SSH 연결 수가 함께 올라간다.
strategy: linear 는 모든 호스트가 한 태스크를 끝내야 다음으로 넘어간다. free 는 각 호스트가 자기 속도로 끝까지 달린다. 느린 한 대가 전체를 붙잡는 상황에서는 free 가 크게 이긴다. 다만 free 는 호스트 사이의 순서 약속을 깬다. "모든 웹 서버에서 빼고 나서 배포한다" 같은 플레이북은 free 아래에서 뜻이 무너진다. serial 이나 run_once 와 섞어 쓸 때는 특히 조심해야 한다.
손잡이 다섯 — 비동기
async 와 poll 은 한 쌍이다. poll 이 0 이 아니면 앤서블이 그 자리에서 기다리고, 0 이면 작업 번호만 받고 곧장 다음 태스크로 간다. 나중에 async_status 로 거둔다.
- name: Start the long job ansible.builtin.command: /opt/bin/reindex async: 1800 poll: 0 register: jobasync 값은 그 작업에 허락한 최대 시간이다. 짧게 잡으면 멀쩡한 작업이 중간에 끊긴다. 이 방식이 맞는 자리는 '오래 걸리지만 서로 독립인 작업' 이고, 결과가 바로 필요한 작업에 쓰면 플레이북만 복잡해진다.
현장에서 만나는 모습
첫째, 가장 큰 이득은 대개 팩트에서 나온다. 대상이 많아질수록 팩트 수집은 선형으로 늘고, 그 대부분은 쓰이지 않는다. gather_facts: false 한 줄이 forks 를 두 배로 올리는 것보다 크게 이기는 일이 흔하다.
둘째, 설정이 먹었는지를 꼭 확인한다. ansible.cfg 는 여러 자리에 있을 수 있고 하나만 쓰인다. 고쳐 놓고 다른 디렉터리에서 실행해 아무것도 안 바뀐 채 "효과가 없네" 하는 일이 잦다. ansible-config dump --only-changed 의 CONFIG_FILE 줄이 그 자리를 알려 준다.
셋째, 시간을 재는 자리와 판정하는 자리를 구분한다. 에뮬레이션이나 공유 러너에서는 같은 플레이북의 실행 시간이 두 배까지 흔들린다. 그래서 "빨라졌다" 를 증명할 때는 한 번의 실행 시간이 아니라 구조가 바뀌었다는 증거를 남긴다 — 설정 덤프, 캐시 파일, 콜백이 남긴 측정 파일 같은 것이다.
넷째, 측정 결과를 읽는 습관을 도구로 만든다. 요약을 눈으로 훑는 것과 상위 몇 개를 뽑아 기록으로 남기는 것은 다르다. 기록이 쌓이면 "이번 주에 느려진 태스크" 를 말할 수 있게 되고, 그때부터 성능은 관리되는 것이 된다.
참고 문서
- 플레이북 전략과 forks: https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_strategies.html
- 비동기 작업과 폴링: https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_async.html
- setup 모듈과 gather_subset: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/setup_module.html
- 캐시 플러그인: https://docs.ansible.com/ansible/latest/plugins/cache.html
- ssh 연결 플러그인(파이프라이닝): https://docs.ansible.com/ansible/latest/collections/ansible/builtin/ssh_connection.html
다음 실습에서 할 것
profile_tasks 로 기준선을 먼저 남기고, 손잡이를 하나씩 돌린다. 팩트를 전부 모은 결과와 최소 묶음만 모은 결과를 나란히 저장해 무엇이 빠지는지 세어 보고, jsonfile 캐시를 켠 뒤 대상의 지역 팩트를 바꿔 캐시가 옛 값을 그대로 돌려주는 것을 직접 확인하고 --flush-cache 로 풀어 본다. 파이프라이닝을 켜고 설정 덤프로 정말 먹었는지 확인하고, forks 와 strategy: free 를 적용하고, 긴 작업을 poll: 0 으로 던져 놓았다가 async_status 로 거둔다. 마지막에는 측정 결과에서 가장 느린 태스크를 뽑아 주는 작은 도구를 직접 만든다.