Google Cloud Run’da 504 Almadan CAPTCHA Nasıl Çözülür

Cloud Run’da bir captcha çözümü üç yerde ölebilir ve bunların ikisinde kodunuz hiçbir hata görmez. Python buildpack’i uygulamanızı gunicorn’un varsayılanlarıyla başlatır ve gunicorn 30 saniye boyunca meşgul kalan bir worker’ı öldürür; bir reCAPTCHA çözümü de çoğu zaman bu kadar sürer. Cloud Run’ın kendi istek zaman aşımı varsayılan olarak 300 saniyedir, bu da SDK’nın reCAPTCHA yoklama zaman aşımıyla tam olarak aynıdır; dolayısıyla bu yarışı her zaman 504 kazanır. Üstelik konteynerin içindeki 127.0.0.1 konteynerin kendisidir. CapSkip sizin sahip olduğunuz bir Windows makinesinde çalışır, servis ise yalnızca istemcidir. İşte bu üç sorunun hepsini aşan dağıtım.
Neye ihtiyacınız var
- Kontrol ettiğiniz bir Windows makinesinde çalışan CapSkip. CapSkip bir masaüstü uygulamasıdır ve Cloud Run’ın içinde çalışmaz. Servis onu yalnızca HTTP üzerinden çağırır, fazlası değil.
- Server modu açık olmalı. Local modu yalnızca o cihaz için 127.0.0.1 üzerinden yanıt verir ve bu, Google’ın ağındaki bir konteynerin hiçbir işine yaramaz. Server modu ise ağ adresinizi veya genel IP’nizi dinler, böylece servis ona aynı API üzerinden ulaşabilir. Her iki mod da bağlantı ayarları bölümünde yer alır. Statik bir genel IP önerilir ve Google’ın geleceği tek adres için bir güvenlik duvarı kuralı eklenmelidir.
- Kaynaktan dağıtılan bir Python servisi ve capskip, flask ve gunicorn paketlerini listeleyen bir requirements.txt dosyası. CapSkip paketi Python 3.10 veya daha yenisini gerektirir.
- gcloud CLI ve 3. adım için servisin bölgesinde bir subnet’i bulunan bir VPC ağı.
Bir Cloud Run captcha dağıtımına neden üç zaman aşımı karar veriyor
Her çözüm boyunca üç sayaç birden işler ve varsayılan bir dağıtımda önce yanlış olan tetiklenir.
| Saat | Varsayılan | Tetiklendiğinde ne olur |
|---|---|---|
| gunicorn worker zaman aşımı, buildpack’in varsayılan entrypoint’inden | 30 saniye | Worker çözümün ortasında öldürülüp yeniden başlatılır ve çağıran bir sunucu hatası alır |
| Cloud Run istek zaman aşımı | 300 saniye, 3600’e kadar yükseltilebilir | Çağıran bir 504 alır, konteyner ise isteği işlemeye devam eder |
| CapSkip reCAPTCHA yoklama zaman aşımı | 300 saniye | İstemci, kodunuzun ele alabileceği bir TimeoutException fırlatır |
Önce gunicorn’a bakalım. Kaynaktan yapılan bir Python dağıtımında buildpack’in varsayılan entrypoint’i, 8080 portuna bağlanmış ve başka hiçbir ayarı yapılmamış bir gunicorn’dur; bu da tek worker, tek thread ve gunicorn’un 30 saniyelik worker zaman aşımı demektir. Gunicorn bu sınırı aşacak kadar sessiz kalan bir worker’ı öldürüp yeniden başlatır ve yavaş bir çözümü bekleyen senkron (sync) bir worker sessizdir. Bu yüzden 40 saniye süren bir reCAPTCHA asla geri dönmez.
Ardından beraberlik meselesi. Cloud Run bağlantıyı 300. saniyede kapatır ve bir 504 döndürür; Google’ın dokümanları instance’ın sonlandırılmadığını belirtir, yani kodunuz kimsenin beklemediği bir isteği işlemeye devam edebilir. SDK’nın zaman aşımı da 300 saniyedir, ancak sayacı daha geç başlar: istek geldikten ve iş gönderildikten sonra. Her zaman önce Cloud Run’ın sayacı tetiklenir ve size ne olduğunu anlatacak olan TimeoutException çağırana hiçbir zaman ulaşmaz. Google’ın istek zaman aşımı rehberi şunu söylüyor: sınırı beklediğiniz yürütme süresinin üstünde belirleyin ve framework’ünüzün kendi zaman aşımını da kontrol edin. Buradaki framework zaman aşımı da gunicorn’unkidir.
1. Adım: servisi yazın
Tek bir route’u olan, main.py olarak kaydedilmiş küçük bir Flask uygulaması. İstemciyi import sırasında bir kez oluşturun. İstemci yalnızca kendi ayarlarını tutar, bu yüzden worker’daki her thread onu paylaşabilir.
# pip install capskip flask gunicorn
import os
from flask import Flask, jsonify, request
from capskip import CapSkip
app = Flask(__name__)
# The client does not read CAPSKIP_HOST by itself: pass it in.
solver = CapSkip(
host=os.environ["CAPSKIP_HOST"], # Server mode address
port=int(os.environ.get("CAPSKIP_PORT", "8080")),
apiKey=os.environ.get("CAPSKIP_API_KEY", "capskip"),
)
@app.post("/solve")
def solve():
job = request.get_json(force=True)
result = solver.recaptcha(sitekey=job["sitekey"], url=job["pageurl"])
return jsonify(token=result["code"]) # use it straight awayHost değerini varsayılana düşen bir okuma yerine katı bir anahtar okumasıyla almak bilinçli bir tercihtir. Değişken yoksa import başarısız olur, gunicorn bir worker başlatamaz ve yeni revizyon hiçbir zaman trafik almaya başlamaz. Bu, sorunsuzca dağıtılan ve ardından ilk gerçek istekte loopback’e karşı bir NetworkException fırlatan bir revizyondan çok daha net bir hatadır.
2. Adım: doğru entrypoint, zaman aşımı ve eşzamanlılıkla dağıtın
Bir Cloud Run captcha servisinin yukarıdaki tablodan ihtiyaç duyduğu her düzeltme dağıtım komutuna girer, dolayısıyla bunların hiçbiri kodda yer almaz.
# Run from the folder holding main.py and requirements.txt gcloud run deploy solve-captcha \ --source . \ --region us-central1 \ --no-allow-unauthenticated \ --set-build-env-vars GOOGLE_ENTRYPOINT="gunicorn --bind :8080 --workers 1 --threads 8 --timeout 0 main:app" \ --timeout 400 \ --concurrency 8 \ --set-env-vars CAPSKIP_HOST=203.0.113.10,CAPSKIP_PORT=8080,CAPSKIP_API_KEY=YOUR_API_KEY
Windows PowerShell’de satır sonundaki her ters eğik çizgiyi bir ters tırnakla (backtick) değiştirin. Her satırın neyi değiştirdiği şöyle:
- Entrypoint satırı gunicorn düzeltmesidir. 0 değerli bir zaman aşımı gunicorn’un worker zaman aşımını kapatır ve zamanlamayı Cloud Run’a bırakır; sekiz thread ise tek bir instance’ın aynı anda sekiz çözüm yürütmesini sağlar. Bunlar Google’ın kendi buildpack dokümanlarında örnek olarak kullanılan ayarlardır ve varsayılanı geçersiz kıldığınızda gunicorn’un requirements.txt içinde listelenmesi gerekir. Port, PORT değişkeni yerine 8080 olarak yazılmıştır, çünkü kendi kabuğunuz o değişkeni gcloud onu görmeden önce genişletir ve boş bir değere çevirirdi. Siz aksini söylemedikçe Cloud Run trafiği 8080’e gönderir.
- 400 saniyelik istek zaman aşımı, istemcinin 300 saniyesinin rahatça üstündedir. İstemcinin sayacı ancak iş gönderildiğinde başlar ve Cloud NAT arkasındaki yeni bir instance’ta o ilk bağlantı bir dakika sürebilir.
- Eşzamanlılık satırı, isteklerin Cloud Run’ın göremediği bir yerde kuyruğa girmesini önler. gcloud ile dağıtılan bir servis varsayılan olarak vCPU başına en fazla 80 eşzamanlı istek kabul eder. Sekiz thread varken dokuzuncu ile sekseninci arasındaki istekler, Cloud Run’ın sayacı çoktan işlemeye başlamışken gunicorn’un içinde kuyrukta bekler; instance ise hâlâ bol yeri varmış gibi görünür. Eşzamanlılığı thread sayısıyla eşleştirmek, Cloud Run’ın bunun yerine yeni bir instance başlatmasını sağlar.
- Ortam değişkenleri, CapSkip makinenizin Server modu adresini ve API anahtarını taşır; main.py bunları açıkça okur. Bu çalıştıktan sonra anahtar için Secret Manager ve set-secrets bayrağı daha derli toplu bir yer olur.
- no-allow-unauthenticated satırı endpoint’i özel tutar; böylece çözücünüzün zamanını bu servis üzerinden yalnızca servisi çağırma izni olanlar harcayabilir.
3. Adım: servise tek bir statik giden IP verin
Varsayılan olarak bir Cloud Run servisi internete Google adreslerinden oluşan dinamik bir havuzdan çıkar, dolayısıyla güvenlik duvarınızın izin verebileceği tek bir IP yoktur. Belgelenmiş çözüm, servisin çıkış trafiğini, rezerve edilmiş statik bir adresi tutan bir Cloud NAT gateway’e sahip bir VPC ağı üzerinden göndermektir.
# Reserve one address and put Cloud NAT in front of the subnet gcloud compute routers create capskip-router \ --network default --region us-central1 gcloud compute addresses create capskip-egress --region us-central1 gcloud compute routers nats create capskip-nat \ --router capskip-router --region us-central1 \ --nat-custom-subnet-ip-ranges default \ --nat-external-ip-pool capskip-egress # Send ALL of the service's outbound traffic through that VPC gcloud run services update solve-captcha --region us-central1 \ --network default --subnet default --vpc-egress all-traffic
İnsanların gözden kaçırdığı bayrak sonuncusudur.Varsayılan çıkış ayarı private-ranges-only değeridir ve VPC üzerinden yalnızca özel adreslere giden trafiği gönderir. Çözücünüz genel bir IP üzerinde durur, bu yüzden all-traffic olmadan çözümler yine dinamik havuzdan çıkar ve NAT adresi güvenlik duvarı logunuzda hiçbir zaman görünmez. Google kurulumun tamamını şurada adım adım anlatıyor: statik giden IP rehberi.
Rezerve adres yerine oturduğunda, Windows makinesinin güvenlik duvarında çözücünün portu için bu adrese izin verin ve başka hiçbir şeye izin vermeyin. Kendi ağınıza kurulan bir Cloud VPN tüneli, portu internete hiç açmadan aynı işi görür. Her iki durumda da Server modu yine sizin donanımınızdır ve çözüm başına ücretlendirme yine yoktur. Server modu yalnızca çözücünün nerede dinlediğini değiştirir, böylece aynı masaüstünden başka bir şey de onu çağırabilir.
Çözümü yanıttan sonra bitirmek neden işe yaramaz
Yavaş bir çözüm karşısında akla gelen cazip yol, çağırana hemen yanıt verip işi bir arka plan thread’inde bitirmektir. Cloud Run’ın varsayılan istek tabanlı faturalandırmasında CPU yalnızca instance istekleri işlerken ayrılır. Yanıt gittikten sonra hâlâ yoklama yapan bir thread, CPU’yu yalnızca o instance’ta başka bir istek işlenirken alır ve boştaki bir instance her an kapatılabilir. İş ya takılır ya da kaybolur ve hangisinin olduğunu size hiçbir şey söylemez.
Çağıran gerçekten bekleyemiyorsa işi bunun yerine bir Cloud Run job’ına taşıyın. Bir job’ı bekleyen bir HTTP isteği yoktur, her görev varsayılan olarak 10 dakika, en fazla da 168 saat çalışabilir; job’lar servislerle aynı ağ ve çıkış bayraklarını alır, dolayısıyla bu bayraklar verilen bir job aynı NAT adresinden çıkar.
Tam çalışan örnek
Aynı servis, bu kez hangi hataların yeniden denemeye değer olduğunu çağırana bildiriyor.
# pip install capskip flask gunicorn
import os
from flask import Flask, jsonify, request
from capskip import (CapSkip, ApiException, NetworkException,
TimeoutException, ValidationException)
app = Flask(__name__)
solver = CapSkip(
host=os.environ["CAPSKIP_HOST"],
port=int(os.environ.get("CAPSKIP_PORT", "8080")),
apiKey=os.environ.get("CAPSKIP_API_KEY", "capskip"),
recaptchaTimeout=300, # keep it below the Cloud Run --timeout
)
@app.post("/solve")
def solve():
job = request.get_json(force=True)
try:
result = solver.recaptcha(sitekey=job["sitekey"], url=job["pageurl"])
except NetworkException as exc:
# Worth retrying. Log the detail, but do not echo the
# solver's address back to the caller.
app.logger.warning("solver unreachable: %s", exc)
return jsonify(error="solver unreachable"), 503
except TimeoutException:
return jsonify(error="no answer inside 300 seconds"), 504
except (ApiException, ValidationException) as exc:
# Same input, same failure: do not retry.
return jsonify(error=str(exc)), 422
return jsonify(token=result["code"])Durum kodları, çağıranın mesajı okumadan onlara göre hareket edebileceği şekilde seçildi. 503 kodu, işi alması için çözücüye ulaşılamadığı anlamına gelir ve yeniden deneme işe yarayabilir. Kendi kodunuzdan 300 saniyenin hemen ardından gelen bir 504, cevabın zamanında dönmediği anlamına gelir; çünkü çözücü yavaş kaldı ya da işi aldıktan sonra bağlantısı koptu. 422 kodu, CapSkip’in işi reddettiği anlamına gelir; bunun nedeni genellikle sitekey’in, URL’nin veya API anahtarının yanlış olmasıdır ve doğrudan bir yeniden deneme büyük olasılıkla aynı cevabı alır. Tek bir şey yakalamayı tercih ederseniz, dört istisnanın dördü de CapSkipError’dan türer.
Token’ı kim alıyorsa onu hemen kullanmalıdır. Bir reCAPTCHA token’ı yaklaşık iki dakika geçerlidir; bu sürenin ayrıntıları şurada: reCAPTCHA v2 çözücü rehberi. Aynı yapı diğer türlerde de işe yarar; bu türler farklı argümanlar alır ve farklı alanlar döndürür. Python paketinin sunduğu her metot şurada listeleniyor: Python CAPTCHA çözücü sayfası.
Sık görülen hatalar ve anlamları
| Gördüğünüz | Neden | Düzeltme |
|---|---|---|
| Loglarda WORKER TIMEOUT ve bir çözümün yaklaşık 30. saniyesinde bir sunucu hatası | Buildpack’in varsayılan entrypoint’i gunicorn’u 30 saniyelik worker zaman aşımıyla çalıştırıyor | GOOGLE_ENTRYPOINT değişkenini 0 değerli bir zaman aşımı ve birkaç thread ile ayarlayın |
| 300. saniyede bir 504 ve ardından aynı çözümden gelmeye devam eden log satırları | Cloud Run’ın istek zaman aşımı istemcinin yoklama zaman aşımına eşit ve Cloud Run’ın sayacı önce başladı | 400 saniyelik bir zaman aşımıyla dağıtın |
| Instance sayısı sabit kalırken yük altında artan gecikme | Cloud Run, çok daha az thread’i olan bir sunucuya vCPU başına 80’e kadar istek gönderiyor | Eşzamanlılığı gunicorn thread sayısına eşitleyin |
| Mesajı bad response: 404 olan bir NetworkException | İstemci host verilmeden oluşturuldu, bu yüzden 8080 portunda 127.0.0.1’i çağırdı; bu da konteynerin içinde sizin kendi servisinizdir. İstemci CAPSKIP_HOST değişkenini kendiliğinden okumaz | Host değerini main.py dosyasında olduğu gibi ortamdan alıp istemciye verin |
| Dağıtımdan sonra yeni revizyon hiçbir zaman hazır hâle gelmiyor | CAPSKIP_HOST ayarlanmamış ya da gunicorn requirements.txt içinde yok | Değişkeni serviste ayarlayın ve gunicorn’u diğer bağımlılıklarınızla birlikte listeleyin |
| Dizüstünüzden çalışan ama servisten başarısız olan çözümler | Güvenlik duvarından ev adresinize izin veriliyor, Google’ın adreslerine verilmiyor | Servise statik bir giden IP verin ve yalnızca ona izin verin |
| VPC’yi bağladıktan sonra zaman aşımına kadar takılı kalan çözümler | Tüm trafik Cloud NAT gateway’i olmayan bir VPC üzerinden gidiyor, dolayısıyla dışarı çıkış yolu yok | NAT gateway’i servisin subnet’inde oluşturun |
| Güvenlik duvarı logunuzda rezerve etmediğiniz bir Google adresi görünüyor | Çıkış ayarı hâlâ private-ranges-only, dolayısıyla genel trafik NAT’ı atlıyor | Servisi all-traffic çıkış ayarıyla güncelleyin |
FAQ
CapSkip’in kendisi Cloud Run üzerinde çalışabilir mi?
Hayır ve buna gerek de yok. CapSkip, sizin sahip olduğunuz donanımda çalışan bir Windows uygulamasıdır; konteynerinizdeki Python paketi ise onun HTTP üzerinden çalışan ince bir istemcisidir. Server modunu açın, adresi servise verin; servis çözücüyü aynı masadaki bir script’in çağıracağı gibi çağırır. Çözüm işi sizin makinenizde kalır ve kaç CAPTCHA çözdüğünüzü kimsenin ölçmemesinin nedeni de budur.
CAPTCHA çözmek için servis mi yoksa job mı daha uygun?
Bir şey token’ı bekliyor ve onu hemen kullanacaksa servis; örneğin tarama sırasında endpoint’inizi çağıran bir scraper. İş, kimsenin beklemediği bir toplu işse job, çünkü bir job’ın hiç istek zaman aşımı yoktur ve görev zaman aşımı bir saatin çok ötesine gider. Her iki durumda da token’ı, onu elinde tutan süreç birkaç dakika içinde kullanmalıdır. Yüz CAPTCHA çözüp token’ları daha sonra kullanmak üzere bir yere yazan bir job, yüz çözümü boşuna yapmış olur. Aynı ödünleşim AWS’te de karşınıza çıkar ve AWS Lambda rehberi bunu önüne bir kuyruk koyarak ele alıyor.
Çözücüye yalnızca kendi servisimin ulaşmasını nasıl sağlarım?
Servisin tüm çıkış trafiğini, rezerve bir adres tutan bir Cloud NAT gateway’e sahip bir VPC üzerinden yönlendirin, ardından güvenlik duvarınızda çözücünün portu için yalnızca o adrese izin verin. Bir Cloud VPN tüneli, portu internete hiç açmadan kendi ağınızdaki bir çözücüye ulaşır. Portu diğer her şeye kapalı tutun ve API anahtarını tek kilit olarak değil, ikinci bir kilit olarak görün.
Bir çözümü bekleyen istek çok mu maliyetli?
Cloud Run bir instance’ı istekleri işlediği süre boyunca faturalandırır ve ağda bekleyen bir istek de buna dahildir. Bunu makul düzeyde tutan şey eşzamanlılıktır. Tek bir instance üzerinde yan yana bekleyen sekiz çözüm, tek bir instance’lık faturalanan süre kullanır; instance başına tek istek olsaydı sekiz instance başlardı. Gunicorn’a tek thread yerine sekiz thread vermenin gerekçesinin diğer yarısı da budur. Çözümün kendisi ise CAPTCHA başına hiçbir şeye mal olmaz, çünkü sizin kendi makinenizde çalışır.
Kısa özet
Bir Cloud Run captcha dağıtımı dört ayar gerektirir ve bunların üçü kodunuzda değil dağıtım komutunda yer alır. Gunicorn worker’ları 30. saniyede öldürmeyi bıraksın diye buildpack’in varsayılan entrypoint’ini değiştirin ve ona thread verin. Kendi hatanız Cloud Run’ın 504’ünden önce gelsin diye istek zaman aşımını istemcinin 300 saniyesinin üstüne ayarlayın. Ek yük kuyruk oluşturmak yerine yeni instance’lar başlatsın diye eşzamanlılığı bu thread’lerle eşleştirin. Son olarak Server modu adresini istemciye kendiniz verin, ardından servisi Cloud NAT ve all-traffic çıkış ayarıyla bir VPC üzerinden yönlendirin; böylece güvenlik duvarınızın izin vereceği tam olarak tek bir adres olur.
Ekonomi tarafında son bir nokta; yukarıdaki yeniden deneme kodlarına göre gönül rahatlığıyla hareket etmenizi sağlayan şey de bu. Yeniden denenen bir istek size birkaç saniyelik Cloud Run süresinden başka hiçbir şeye mal olmaz, çünkü captcha çözücü zaten parasını ödediğiniz donanımda çalışır; dolayısıyla başarısız bir çözüm yüzünden hiç kimse size CAPTCHA başına ücret kesmez.
