Oban은 예상치 못한 문제가 생겨도 큐가 계속 작동하도록 설계됐지만, 큐가 밀리거나 작업이 오래 걸리고 버그가 시스템을 막을 수 있다. 텔레메트리 이벤트와 주기적 작업, 로그를 활용하면 제3자 수집 서비스 없이도 문제를 일찍 감지하고 알림을 설정할 수 있다. 다만 제3자 서비스가 도움이 될 수도 있다.
모니터링할 문제 유형
Oban 시스템에서 감시할 문제는 심각도에 따라 세 가지로 나눌 수 있다.
- 완전히 멈춤: 실행 중인 큐가 없고 크론 작업도 예약되지 않는다.
- 적체: 큐가 작업 처리량을 따라가지 못해 대기 작업이 계속 쌓인다.
- 작업이 붙잡힘: 일부 작업이 끝없이 실행 상태에 머물러 큐가 막힌다.
각 문제는 서로 다른 방식으로 미리 감지하고 대응할 수 있다.
시스템이 완전히 멈췄을 때
완전히 멈춘 경우는 가장 심각하지만, 확인하기는 비교적 쉽다. 데이터베이스가 정상이고 애플리케이션의 나머지 부분도 실행 중인데 Oban 큐가 작업 처리를 중단했다면 보통 설정 문제다.
로그나 메트릭을 감시하고 일정 시간 동안 이벤트가 없으면 알림을 보내는 방법이 있다. 먼저 프로덕션 준비를 마쳐 로그가 기록되도록 해야 한다. 다만 활동이 적은 시간대에는 실제 문제가 없어도 알림이 울릴 수 있다. 이를 막으려면 전용 큐에서 주기적 작업을 실행해 활동이 이어지도록 한다.
다른 작업이 실행되지 않는 health 큐를 만들고, 매분 확인하는 크론 작업을 예약한다.
defmodule MyApp.Sentinel do
# Using the Pro worker for inline cron, you can also do it the old-fashioned way
use Oban.Pro.Worker, queue: :health, cron: "* * * * *"
@impl Oban.Pro.Worker
def process(_job) do
:my_app
|> Application.fetch_env!(:some_check_in_url)
|> Req.get!()
:ok
end
end
Honeybadger 같은 모니터링 서비스에서 체크인을 받으면 Oban이 정상 작동 중임을 확인할 수 있다. 예약된 작업이 실행되지 않아 체크인을 놓치면 알림을 받는다.
알림은 작업이 처리되지 않는다는 사실을 알려주지만 원인까지 설명하지는 않는다. 주된 원인은 설정 문제이며, 대표적으로 리더십이 잘못 지정되거나 알림 전달이 끊기는 경우가 있다.
- 리더십 문제: 작업을 처리하지 않는 노드가 리더가 됐거나, 종료되지 않은 좀비 노드가 리더십을 계속 유지한다.
- 알림 문제: 제대로 작동하는 분산 Erlang 클러스터 없이
PG기반 PubSub를 사용한다.
이 문제들은 Oban v2.23.1 이전 버전에서 발생할 수 있었다. 해당 버전부터는 분산 관련 문제가 있어도 처리가 계속되도록 대체 동작이 추가됐다. 이전 버전을 사용하거나 다른 이유로 문제 해결 방법을 알아보려면 다음을 확인한다.
리더 노드에서 플러그인을 실행할 수 없으면 크론 작업이 예약되지 않는다. 작업 정리와 고아 작업 복구도 중단돼 작업이 계속 쌓인다. 이는 작업을 처리하지 않는 웹 노드와 작업자 노드가 함께 있는 하이브리드 환경에서 흔히 발생한다. 이 상황을 방지하려면 plugins: false로 플러그인과 리더십을 끄거나 peer: false로 리더십을 끈다. plugins: [], queues: []는 사용하지 않는 편이 좋다. Elixir 설정은 깊은 병합을 사용하므로 빈 목록이 기존 옵션을 덮어쓰지 않고 합쳐지기 때문이다.
알림은 큐에 도달해 available 상태의 작업을 알려야 한다. 알림이 전달되지 않으면 큐가 작업을 가져오는 일을 완전히 멈출 수 있다. Oban은 알림 서비스의 이상을 감지해 연결이 끊기면 경고를 기록하려고 한다. 하지만 분산 클러스터에서는 각 노드가 자기 자신에게서 메시지를 받을 수 있어 연결 끊김을 정확히 감지하기 어렵다.
Oban.Notifiers.PG 알림 기능을 사용할 때는 클러스터가 제대로 작동하는지 확인한다. Oban.Notifiers.Postgres를 사용한다면 연결 풀러 뒤에 있지 않은지 확인한다.
작업이 쌓일 때
큐는 작업을 보관하도록 설계됐지만, 작업이 시스템의 처리 속도보다 빠르게 쌓이면 문제가 된다. 예를 들어 애플리케이션이 매일 아침 10만 건을 가져오고 다음 한 시간 동안 처리하는 일은 정상일 수 있다. 문제는 처리할 수 있는 속도보다 작업이 계속 더 빨리 들어오는 경우다.
앞서 설명한 상태 확인용 작업만으로는 큐 적체를 감지할 수 없다. 대기 작업 수와 전체 처리량을 살펴봐야 한다.
Oban Web을 사용한다면 필요한 건수와 메트릭을 수집하는 Oban.Met이 이미 작동한다. Met은 여러 노드의 값을 공유하므로 대시보드에 쓰이는 데이터를 그대로 활용할 수 있으며, 별도로 메트릭을 폴링할 필요가 없다. Oban Web은 무료다.
Python 애플리케이션은 대시보드에 데이터를 보내도록 메트릭을 활성화할 수 있지만, 자체 코드에서 조회할 Met은 Python에 없다. Python 작업자와 함께 Elixir 노드를 실행한다면 그 노드에서 전체 데이터를 조회할 수 있다. 그렇지 않으면 대시보드를 이용해 대기 작업과 처리량을 확인하거나, oban.job.stop에 텔레메트리 핸들러를 연결해 자체 수집기로 보낼 수 있다.
먼저 각 큐에서 대기 중인 작업 수를 추적한다.
Oban.Met.latest(Oban, :full_count,
filters: [state: "available"],
group: "queue",
lookback: 5
)
이 코드는 큐별로 가장 최근 건수를 보여줘 작업이 어디에 쌓이는지 확인할 수 있게 한다. 쌓인 작업을 얼마나 빠르게 처리하는지 보려면 지난 5분간 1분 단위로 exec_count를 확인한다.
Oban.Met.timeslice(Oban, :exec_count,
filters: [state: "completed"],
group: "queue",
lookback: 300,
by: 60
)
이 값은 대기 중인 작업량과 완료 속도를 보여준다. iex 세션 밖에서도 활용하려면 원하는 메트릭 수집기로 내보내면 된다.
다음은 Pro v1.8에서 추가된 크론 함수다. health 큐에서 매분 실행하며 최신 메트릭을 StatsD 유형의 수집기로 보낸다. 이후 큐별로 처리량이 밀릴 때 알림을 설정할 수 있다.
@job cron: "* * * * *", queue: :health
def report_queue_metrics do
queues = Oban.Met.labels("queue")
available =
Oban.Met.latest(Oban, :full_count,
filters: [state: "available"],
group: "queue",
lookback: 5
)
completed =
Oban
|> Oban.Met.timeslice(:exec_count,
filters: [state: "completed"],
group: "queue",
lookback: 60,
by: 60
)
|> Enum.reduce(%{}, fn {_bucket, count, queue}, acc ->
Map.update(acc, queue, count, &(&1 + count))
end)
Enum.each(queues, fn queue ->
:ok = Statsd.gauge("#{queue}.available", Map.get(available, queue, 0))
:ok = Statsd.gauge("#{queue}.completed", Map.get(completed, queue, 0))
end)
end
이 코드는 바깥 모듈에서 Oban.Pro.Decorator를 사용하고, Statsd가 설정된 StatsD 클라이언트라고 가정한다.
적절한 임계값은 애플리케이션에서 허용할 수 있는 지연 시간에 따라 달라진다. 야간 데이터 가져오기 작업은 제때 비밀번호 재설정 메일을 보내는 큐보다 더 많은 지연을 감당할 수 있다.
대기 작업이 늘어나는 것은 들어오는 작업량과 시스템 처리 능력이 맞지 않는다는 뜻이다. 문제가 있다는 사실만으로 원인을 알 수는 없으므로 추가 조사가 필요하다. 작업이 더 많이 들어오는지, 처리 시간이 길어졌는지, 큐의 동시성이 줄었는지 확인한다. 상황에 따라 동시성을 높이거나 사용 가능한 CPU를 늘리고, 노드를 (자동으로?) 확장하거나, 작업을 묶음으로 처리하거나, 전역 동시성을 조정할 수 있다. 다만 처리 속도가 느려진 원인을 파악하기 전에 설정을 바꾸지 않아야 한다.
끝나지 않는 작업이 있을 때
시간 제한이 없으면 Oban 작업은 애플리케이션이 재시작될 때까지 계속 실행될 수 있다. 무한 대기, 응답하지 않는 네트워크 호출, 제한 없는 쿼리는 작업을 아무 일도 하지 못한 채 붙잡아 둘 수 있다.
작업 하나가 붙잡히면 큐의 처리 능력이 줄고, 이런 작업이 충분히 많아지면 큐가 완전히 멈춘다. 이때 애플리케이션은 실행 중이고 상태 확인도 정상이라 모든 것이 살아 있는 것처럼 보일 수 있다.
먼저 각 작업자가 합리적으로 처리할 수 있는 시간을 정한다. 동영상을 변환하는 작업은 이메일을 보내는 작업보다 시간이 더 걸리는 것이 자연스럽다. 시스템에 맞는 시간 기준은 운영 환경에 따라 정해야 한다.
Met은 exec_time으로 실행 시간을 추적하며, 시간이 오래 걸리는 작업자를 찾도록 worker 레이블도 제공한다. 앞서와 마찬가지로 1분 단위로 지난 5분간 작업자별 최장 실행 시간을 확인할 수 있다.
Oban.Met.timeslice(Oban, :exec_time,
group: "worker",
lookback: 300,
by: 60,
operation: :max
)
이 값에는 성공하거나 실패했거나 취소된 작업의 실행 시간이 포함된다. 이전 절의 큐 메트릭과 함께 내보내고, 작업자가 예상 시간을 넘기면 여유 시간을 더한 기준으로 알림을 설정한다. Met은 기본적으로 네이티브 시간 단위로 기록하므로 내보내기 전에 밀리초처럼 다루기 쉬운 단위로 변환해야 한다.
주의할 점은 실행 시간이 시도가 끝난 뒤에만 기록된다는 것이다. 영원히 멈춘 작업은 실행 시간을 보고하지 않으므로 이상치가 보이지 않는다. 따라서 알림만으로는 충분하지 않다.
작업자에 timeout/1 콜백을 정의하면 실행 시간의 상한을 둘 수 있다. 다음은 2분으로 고정한 시간 제한의 예다.
@impl Oban.Worker
def timeout(_job), do: to_timeout(minute: 2)
변동에 대비할 여유를 두고 시간 제한을 정한 다음, 알림 임계값은 그보다 낮게 설정한다. 그러면 시간 제한 전에 끝나는 경우에도 예상보다 오래 걸리는 작업자를 발견할 수 있다.
시간 제한에 도달하면 Oban.TimeoutError 예외로 실행이 종료되며, 남은 시도 횟수가 있으면 다시 시도할 수 있다. 실패한 작업도 exec_time 측정에 포함되므로 실행 시간 보고서에서 확인할 수 있다.
작업 자체에 만료 시점이 있는 경우, 예를 들어 하루 늦게 도착하면 의미가 없는 알림의 경우, Pro의 작업 기한을 설정할 수 있다. 기한이 지난 뒤 다음 실행 시 작업을 취소하며, force: true를 설정하면 실행 중에도 취소할 수 있다.
use Oban.Pro.Worker, deadline: {1, :day}, force: true
실행 시간은 시간 제한으로, 작업이 남아 있는 기간은 기한으로 제한한다. 작업의 네트워크 호출과 데이터베이스 쿼리, 잠재적 교착 상태를 점검하고 가능한 곳에 내부 제한도 적용한다. 그러면 시간 제한이 더 의미 있게 작동하고, Oban이 처리하기 전에 작업이 실패를 다룰 기회도 생긴다.
감지는 문제 해결의 일부다
여기서 다룬 문제는 Oban 사용자에게 수년간 영향을 줬다. 가능한 경우 Oban과 Pro 자체를 개선해 특정 문제 유형이 발생하지 않도록 해왔다.
이 방법을 애플리케이션에 적용하면 고객이 알리기 전에 문제를 발견할 수 있다. 질문이나 의견은 Elixir Forum에서 나눌 수 있다.
댓글 (0)
로그인하면 이 기사에 내 생각을 남길 수 있어요