구성한 로테이팅 프록시로 요청을 열 번 보냈는데, 매번 같은 종료 주소가 돌아온다. 가장 먼저 떠오르는 결론은 로테이션이 고장 났다는 것이다. 하지만 보통은 그렇지 않다. 대부분의 경우 게이트웨이는 요청한 대로 정확히 새 주소를 내주고 있으며, 코드와 게이트웨이 사이의 무언가가 그것을 막고 있을 뿐이다. 가장 흔한 원인은 속도를 높이기 위해 존재하는 HTTP 클라이언트의 한 기능이다.
확인할 가치가 있는 순서대로 원인과 각각의 해결책을 소개한다.
먼저, 제대로 확인할 것
진단에 앞서 테스트 자체가 타당한지 확인해야 한다. 호출당 하나의 커맨드라인 요청이 가장 깔끔한 확인 방법인데, 각 실행이 공유 상태 없는 별도의 프로세스이기 때문이다.
for i in 1 2 3 4 5; do
curl -s -x customer-USERNAME:PASSWORD@p.shifter.io:443 https://ipinfo.io/json \
| python3 -c "import sys,json; print(json.load(sys.stdin)['ip'])"
done
다섯 개의 서로 다른 주소가 나오면 로테이션은 작동하는 것이고 애플리케이션이 문제다. 다섯 개가 동일하면 설정이 문제다. 이 하나의 구분만으로도 디버깅 시간의 대부분을 절약할 수 있으니 가장 먼저 실행해 보라.
원인 하나: 사용자 이름에 들어간 세션 식별자
가장 단순한 설명이다. 사용자 이름에 sid 플래그가 포함되어 있다면, 명시적으로 고정 세션을 요청한 것이므로 주소를 유지하는 것은 결함이 아니라 올바른 동작이다.
customer-USERNAME-country-us-sid-abc123 # 고정: 설계상 동일한 IP
customer-USERNAME-country-us # 로테이팅: 요청마다 새로운 IP
이는 문서에서 예시를 그대로 복사했거나, 기본적으로 세션을 추가하는 헬퍼로 사용자 이름을 만든 경우에 해당한다. sid 플래그를 제거하고, TTL은 세션과 함께일 때만 의미가 있으므로 ttl도 함께 제거하라. 그 의미론은 스티키 대 로테이팅과 세션 문서에 나와 있다.
더 미묘한 경우도 있다. 세션 식별자가 의도와 달리 계속 고정되어 있는 경우다. 작업(job)당 하나씩 생성하지만 그 작업이 여러 요청을 실행한다면, 그 작업 내의 모든 요청은 같은 주소를 공유하게 되는데, 이는 올바른 동작이지만 의도한 바가 아닐 수 있다.
원인 둘: 커넥션 재사용, 모두가 걸려드는 문제
위의 curl 반복문은 회전하는데 코드는 회전하지 않는 경우, 대부분 이것이 답이다.
현대의 HTTP 클라이언트는 매 요청마다 새 TCP와 TLS 연결을 여는 것이 느리기 때문에 연결을 유지하고 재사용한다. 클라이언트가 프록시로 가는 기존 터널을 재사용하면, 요청은 이미 수립된 연결을 통해 이동하고, 그 연결에는 이미 종료 주소가 붙어 있다. 로테이션은 연결마다 이루어지는 것이지, 기존 연결로 전송되는 요청마다 이루어지는 것이 아니므로, 커넥션 풀을 유지하는 세션 객체는 모든 요청을 충실하게 같은 종료지로 보낸다.
해결책은 원하는 제어 수준에 따라 다르다. keep-alive를 비활성화하거나, 요청마다 새 연결을 강제하거나, 각 논리적 요청이 새 클라이언트를 사용하게 하면 된다.
import requests
PROXY = "http://customer-USERNAME:PASSWORD@p.shifter.io:443"
PROXIES = {"http": PROXY, "https": PROXY}
# 하나의 연결을 재사용: 모든 요청에서 동일한 종료 주소
s = requests.Session()
for _ in range(5):
print(s.get("https://ipinfo.io/json", proxies=PROXIES).json()["ip"])
# 요청마다 새 연결: 예상대로 로테이션됨
for _ in range(5):
r = requests.get("https://ipinfo.io/json", proxies=PROXIES,
headers={"Connection": "close"}, timeout=20)
print(r.json()["ip"])
같은 원리가 다른 곳에도 적용된다. Node의 keep-alive가 설정된 http.Agent, .NET이나 Java의 공유 HttpClient, Go의 커넥션 풀도 마찬가지다. 사용하는 언어에 커넥션을 풀링하는 기본 클라이언트가 있다면(대부분 그렇다), 애플리케이션 코드에서 가장 먼저 확인해야 할 것이 바로 이것이다.
분명히 짚어둘 점은 이것이 버그가 아니라 트레이드오프라는 것이다. 커넥션 재사용은 더 빠르고 비용이 적게 든다. 작업이 정말로 요청마다 새 주소를 원한다면 매번 새 연결의 비용을 치러야 하고, 그렇지 않다면 재사용이 괜찮으며 대체로 더 바람직하다.
원인 셋: 너무 빨리 확인하고 있거나, 해당 필터에 대해 풀이 생각보다 작다
관련된 두 가지 효과가 있다.
필터를 특정 도시나 특정 ASN처럼 좁게 좁혀놓았다면, 서비스할 수 있는 주소 집합이 전체 풀보다 훨씬 작아지므로, 같은 주소가 정당하게 더 자주 다시 나타난다. 이는 실패가 아니라 산술적인 문제다. 필터를 넓히면 반복이 사라진다.
또한 레지덴셜 풀은 실제로 접속했다가 끊기는 실제 연결들로 구성되어 있으므로, 많은 요청에 걸쳐 주소가 두 번 나타나는 것은 의심스러운 일이 아니라 예상되는 일이다. 로테이션은 다음 요청이 독립적으로 선택된다는 뜻이지, 어떤 주소가 절대 다시 나올 수 없다는 뜻이 아니다. 어떤 작업에 대해 반드시 서로 다른 주소가 필요하다면, 이는 여러분 자신의 코드에서 처리해야 할 설계상의 제약이다.
원인 넷: 상류(upstream)의 무언가가 캐싱하고 있다
브라우저, 확장 프로그램, 시스템 수준의 프록시 설정, 또는 회사 네트워크를 통해 테스트하고 있다면, 요청이 생각한 곳으로 가지 않을 수 있다. 특히 브라우저는 연결을 공격적으로 유지하고 탭 간에 재사용하므로, 브라우저는 로테이션을 확인하기에 가장 나쁜 환경이다. 먼저 커맨드라인에서 테스트한 다음 애플리케이션에서, 그 다음에야 브라우저에서 테스트하라.
마찬가지로, 코드가 명시적으로 설정을 전달하면서 동시에 프록시 환경 변수도 설정한다면, 둘 중 하나가 다른 하나를 덮어써서 여러분이 구성한 게이트웨이가 아닌 다른 곳으로 트래픽을 보낼 수 있다.
원인 다섯: 주소는 같은데 요청이 애초에 나가지 않았다
솔직히 확인해볼 만한 무딘 원인이다. 프록시가 실제로 사용되고 있지 않다면, 모든 요청은 같은 주소, 즉 여러분 자신의 주소를 보고한다. 보고 있는 주소가 여러분 서버 자신의 주소가 아닌지 확인하라. 만약 맞다면, 프록시 설정이 전혀 적용되지 않고 있는 것이며, 이는 다른 문제로, 보통 http 항목과 함께 있어야 할 https 항목이 누락되었거나, 사용 중인 스킴에 대해 프록시 설정을 무시하는 클라이언트 때문이다.
원인 여섯: 아직 만료되지 않은 스티키 세션
의도적으로 스티키 세션을 사용하면서 얼마 후 회전하기를 기대하고 있다면, TTL이 만료될 때까지 주소가 유지된다는 점을 기억하라. ttl을 명시적으로 설정하지 않으면 기본 수명은 120초다. 더 빨리 새 주소를 원한다면 기다리는 대신 세션 식별자를 바꾸면 되는데, 새 식별자는 곧 새 세션을 의미하기 때문이다.
또한 기반 연결이 끊어지면 스티키 주소는 TTL보다 일찍 떨어질 수 있다는 점도 유의하라. 이들은 전용 인프라가 아니라 실제 가정용 연결이기 때문이다. 스티키는 요청한 기간 동안의 최선의 노력이지, 보장이 아니다.
빠른 판단 경로
curl 반복문을 실행해 보라. 그것이 회전하는데 코드는 회전하지 않는다면, 커넥션 재사용 문제이며 이는 원인 둘에 해당한다. 둘 다 회전하지 않는다면, 사용자 이름에 sid 플래그가 있는지 확인한 다음, 여러분 자신의 주소를 보고 있는 것은 아닌지 확인하고, 그 다음 좁은 지리적 필터를 넓혀보라. 원하는 만큼 자주는 아니어도 회전은 하고 있다면, 결함이 아니라 해당 필터에 대한 풀 크기를 보고 있는 것이다.
그 밖의 경우에 대해서는, 주변 동작이 로테이션 작동 방식에 문서화되어 있고, 요청이 반복되는 것이 아니라 실패하고 있다면 요청 시간 초과 이유와 407 오류 해결하기가 가장 흔한 두 가지 실패 모드를 다룬다.
결론
로테이션 문제는 보통 로테이션 문제가 아니다. 먼저 별도의 프로세스로 테스트해서 게이트웨이가 애초에 회전하고 있는지 확인하고, 회전하고 있다면 HTTP 클라이언트를 살펴보라. 이미 열려 있는 터널로 전송된 요청은 그 터널이 이미 가지고 있는 종료 주소를 유지하기 때문에, 커넥션 재사용이 압도적으로 유력한 원인이다. 그 다음에는 엉뚱한 sid 플래그가 없는지 확인하고, 여러분 자신의 주소를 보고 있는 것은 아닌지 확인하고, 매우 좁은 지리적 필터는 훨씬 작은 집합에서 뽑히므로 반복이 정상이라는 점을 기억하라. 스티키 세션이 주소를 유지하는 것은 설계대로 작동하는 것이며, 식별자를 바꾸면 즉시 새 주소를 받을 수 있다.
로테이션 모델 자체와 그것을 제어하는 플래그들은 레지덴셜 프록시 네트워크에 있으며, 여기서 세션 동작은 요금제 설정이 아니라 요청당 매개변수이고, 얼마나 자주 회전하든 상관없이 GB당 과금된다.