"Too many levels of symbolic links" with no symlink in sight: autofs, a bind mount and a cron container
PHP cron 작업이 실제 심볼릭 링크 없이 ELOOP 오류로 실패한 원인을 autofs와 컨테이너의 중첩된 마운트 구성에서 추적합니다. 이미 로컬에 bind mount된 코드를 cron이 autofs 경로로 다시 접근하고 있었으며, 직접 bind mount와 전용 automount map으로 의존성을 제거해 해결합니다.
AI 요약
PHP cron 작업이 어느 날 `Too many levels of symbolic links` 오류와 함께 실패하기 시작합니다. 오류명만 보면 `/srv/apps/app-a/task.php` 경로 어딘가에 심볼릭 링크(symbolic link)가 반복해서 연결되는 순환이 있다고 보는 것이 자연스럽습니다. 하지만 해당 경로에는 심볼릭 링크가 없었고, 한동안 원인을 설명할 만한 경로 구성도 발견되지 않았습니다. 이후 9월 10일 autofs 서비스를 재시작한 직후에는 같은 파일이 `/opt/code/app-a/task.php`와 `/srv/apps/app-a/task.php` 양쪽에서 읽혔습니다. 단순히 `ls`가 성공했다는 사실만으로는 두 번째 경로가 첫 번째 경로에 도달하기 위해 어떤 마운트와 자동 마운트 동작을 거쳤는지 알 수 없었습니다.
■ 문제가 발생한 마운트 구성
cron 컨테이너의 Compose 설정은 이미 호스트의 애플리케이션 코드를 컨테이너 내부로 bind mount하고 있었습니다. 호스트의 `/srv/code/app-a`는 컨테이너의 `/opt/code/app-a`에, `/srv/code/app-b`는 `/opt/code/app-b`에 연결되어 있었습니다. 그런데 애플리케이션이 실제로 기대하는 경로는 `/srv/apps/app-a`와 `/srv/apps/app-b`였습니다. 이미지에 포함된 autofs direct map은 이 두 경로를 다음과 같이 다시 bind mount하도록 구성되어 있었습니다.
`/- /etc/auto.apps --ghost,--timeout=30`
`/srv/apps/app-a -fstype=bind :/opt/code/app-a` `/srv/apps/app-b -fstype=bind :/opt/code/app-b`
따라서 `/srv/apps/app-a` 또는 `/srv/apps/app-b`에 접근할 때마다 automounter가 동작할 수 있었습니다. 코드 자체는 이미 호스트에서 컨테이너로 전달된 로컬 데이터였지만, 애플리케이션이 사용하는 경로가 autofs를 통과하도록 되어 있었습니다. 같은 autofs 데몬은 다른 항목에서 NFS 공유도 관리하고 있었기 때문에, 로컬 PHP 파일 하나를 여는 동작이 네트워크 파일시스템과 automounter의 상태에 간접적으로 의존하는 구조가 되어 있었습니다.
■ 심볼릭 링크가 없어도 ELOOP가 발생하는 이유
이 오류의 이름은 심볼릭 링크 순환을 가리키지만, Linux의 경로 탐색 내부에서 같은 제한값을 다른 동작도 공유합니다. Linux 6.1에서는 `follow_automount()`가 `nd->total_link_count`를 증가시킵니다. 이 카운터는 심볼릭 링크를 따라갈 때 사용되는 카운터이며, 경로 탐색 중 일정 한계를 넘으면 `-ELOOP`를 반환합니다. 한계값인 `MAXSYMLINKS`는 40입니다. 즉, 경로를 확인하는 동안 통과한 automount도 심볼릭 링크 추적 횟수와 같은 제한에 포함될 수 있으므로, 실제 심볼릭 링크가 하나도 없어도 `ELOOP`가 나타날 수 있습니다.
autofs 문서는 또 다른 가능성도 설명합니다. 하나의 autofs 파일시스템이 여러 위치에서 보이지만, 데몬이 만든 마운트가 그 모든 위치로 전파되지 않으면 다른 위치에서 접근할 때 `ELOOP`가 발생할 가능성이 있습니다. 호출자는 자신이 기다리는 실제 마운트를 보지 못한 채 automount 트리거를 계속 만나게 됩니다. 이번 장애가 이 메커니즘과 직접적으로 일치했는지, 아니면 automount 횟수가 누적되는 경로 탐색 제한에 도달했는지는 확인하지 못했습니다. 조사 과정에서 장애가 NFS 문제와 비정상 상태의 automounter에 연관되어 있다는 점은 확인했지만, 당시 커널 트레이스를 확보하지 못했기 때문입니다. 다만 구성 자체만으로도 코드 접근에 불필요한 automount 의존성이 존재한다는 점은 분명했습니다.
■ 해결 방법: cron이 사용하는 경로를 직접 bind mount하기
9월 10일에는 cron이 실제로 사용하는 경로에 Docker bind mount를 직접 추가했습니다. `/srv/code/app-a`를 컨테이너의 `/srv/apps/app-a`에, `/srv/code/app-b`를 `/srv/apps/app-b`에 연결했습니다. 이렇게 하면 cron이 애플리케이션 코드를 열 때 `/opt/code/...`를 거쳐 autofs가 설치한 경로를 다시 통과하지 않고, 해당 컨테이너 안에 직접 연결된 마운트를 사용할 수 있습니다.
다만 직접 bind mount만 추가해서는 충분하지 않았습니다. autofs map에 기존 두 항목이 남아 있으면 autofs가 직접 마운트 위에 자신의 트리거를 다시 설치할 수 있기 때문입니다. 그래서 cron 컨테이너에는 별도의 automount map 사본을 제공하고, `/srv/apps/app-a`와 `/srv/apps/app-b` 항목을 제거했습니다. 이 변경으로 cron 컨테이너에서는 코드 경로가 직접 bind mount되고, 해당 경로를 autofs가 다시 가로채지 않게 했습니다. 이미지 안에서 `/etc/auto.apps`가 실제 map 파일을 가리키는 심볼릭 링크였기 때문에, Compose override에서는 그 링크가 가리키는 대상 위에 cron 전용 map을 마운트했습니다. 웹 컨테이너는 기존 map을 계속 사용하도록 두었습니다.
■ 배포 후 확인 절차
각 마운트를 개별적으로 확인하기 위해 다음 명령을 사용합니다.
`findmnt -o TARGET,SOURCE,FSTYPE /srv/apps/app-a` `findmnt -o TARGET,SOURCE,FSTYPE /srv/apps/app-b` `grep -cE '^/srv/apps/app-(a|b)[[:space:]]' /etc/auto.apps`
두 경로는 호스트 코드에 연결된 bind mount로 확인되어야 하며, 그 아래에 autofs 트리거가 남아 있지 않아야 합니다. map에서 두 경로가 제거되었으므로 `grep` 결과는 0이어야 하고, 일치하는 줄이 없을 때 `grep`은 상태 코드 1로 종료합니다. 마운트가 여러 층으로 쌓인 경우에는 `/proc/self/mountinfo`도 확인해야 합니다. 위에 올라온 파일시스템이 아래쪽에 있는 마운트를 가릴 수 있으므로, 표면적인 마운트 정보만으로는 실제 구성을 모두 판단할 수 없습니다.
배포가 끝난 뒤에는 cron 작업이 실제로 실행되는지와 새 로그에 `ELOOP`가 없는지를 함께 확인해야 합니다. 작업이 시작되지 않아 로그가 조용한 것과 성공적으로 실행된 것은 다르므로, 오류가 보이지 않는다는 사실만으로 정상 동작을 증명할 수는 없습니다. 이번 배포는 9월 10일 정상 동작으로 확인되었습니다. 아직 수행하지 않은 추가 검증으로는 유지보수 시간에 NFS 의존 작업을 중지하고, cron 컨테이너 안에서 autofs를 멈춘 다음 PHP 진입점을 읽어 보고 autofs를 다시 시작해 활성 상태를 확인하는 절차가 있습니다. 직접 bind mount가 유지된다면 autofs가 없어도 파일을 읽을 수 있어야 하며, 이 테스트는 NFS 지연 자체가 아니라 코드 경로에서 automounter 의존성이 제거되었는지를 검증합니다.
이 변경으로 애플리케이션 코드나 PHP를 수정하지 않고 운영 구성을 조정해 문제를 해결했습니다. 코드와 애플리케이션 팀의 배포 경로는 그대로 유지했지만, cron이 로컬 코드를 여는 과정에서 automounter가 협조해야 했던 불필요한 의존성을 제거했습니다. 대신 cron용 map과 다른 컨테이너의 map을 따로 관리하게 되었으므로, 공유 NFS 항목을 변경할 때 두 구성에 모두 반영해야 하는 운영상의 관리 부담은 남았습니다.
원문: dev.to / 번역·요약: Trawling