Cara Memecahkan CAPTCHA di Google Cloud Run Tanpa 504

Pemecahan captcha di Cloud Run bisa mati di tiga tempat, dan di dua di antaranya kode Anda tidak pernah melihat error. Buildpack Python menjalankan aplikasi Anda dengan pengaturan default gunicorn, dan gunicorn mematikan worker yang tetap sibuk selama 30 detik, hal yang sering terjadi pada pemecahan reCAPTCHA. Timeout request milik Cloud Run sendiri secara default 300 detik, persis sama dengan timeout polling reCAPTCHA pada SDK, jadi 504 selalu memenangkan perlombaan itu. Selain itu, 127.0.0.1 di dalam container adalah container itu sendiri. CapSkip berjalan di mesin Windows milik Anda, dan layanan Cloud Run hanyalah kliennya. Berikut deploy yang mengatasi ketiganya.
Apa yang Anda butuhkan
- CapSkip yang berjalan di mesin Windows yang Anda kendalikan. Ia adalah aplikasi desktop dan tidak berjalan di dalam Cloud Run. Layanan Anda memanggilnya melalui HTTP, tidak lebih.
- Mode Server dinyalakan. Mode Local menjawab di 127.0.0.1 untuk perangkat itu saja, yang tidak berguna bagi container di jaringan Google. Mode Server mendengarkan di alamat jaringan atau IP publik Anda sehingga layanan itu dapat menjangkaunya melalui API yang sama, dan keduanya ada di pengaturan koneksi. IP publik statis dianjurkan, dengan aturan firewall untuk satu alamat yang akan dipakai Google untuk terhubung.
- Layanan Python yang di-deploy dari kode sumber, dengan requirements.txt yang mencantumkan capskip, flask dan gunicorn. Paket CapSkip membutuhkan Python 3.10 atau lebih baru.
- CLI gcloud, dan jaringan VPC dengan subnet di region layanan tersebut untuk Langkah 3.
Mengapa tiga timeout menentukan deploy captcha di Cloud Run
Tiga penghitung waktu berjalan selama setiap pemecahan, dan pada deploy default, penghitung yang salah yang terpicu lebih dulu.
| Penghitung waktu | Bawaan | Apa yang terjadi saat terpicu |
|---|---|---|
| Timeout worker gunicorn, dari entrypoint default buildpack | 30 detik | Worker dimatikan dan dijalankan ulang di tengah pemecahan, dan pemanggil mendapat error server |
| Timeout request Cloud Run | 300 detik, bisa dinaikkan hingga 3600 | Pemanggil mendapat 504, sementara container tetap melanjutkan request tersebut |
| Timeout polling reCAPTCHA CapSkip | 300 detik | Klien melempar TimeoutException yang bisa ditangani kode Anda |
Mulailah dengan gunicorn. Untuk deploy Python dari kode sumber, entrypoint default buildpack adalah gunicorn yang terikat ke port 8080 tanpa pengaturan lain, yang berarti satu worker, satu thread, dan timeout worker gunicorn sebesar 30 detik. Gunicorn mematikan lalu menjalankan ulang worker yang diam melewati batas itu, dan worker sync yang menunggu satu pemecahan lambat memang diam. Jadi reCAPTCHA yang memakan 40 detik tidak pernah kembali.
Lalu ada hasil seri. Cloud Run menutup koneksi pada detik ke-300 dan mengembalikan 504, dan dokumentasi Google menyebutkan bahwa instance tidak dihentikan, sehingga kode Anda mungkin terus memproses request yang tidak lagi ditunggu siapa pun. Timeout SDK juga 300 detik, tetapi penghitung waktunya mulai lebih lambat, setelah request tiba dan job dikirim. Penghitung waktu Cloud Run selalu terpicu lebih dulu, dan TimeoutException yang seharusnya memberi tahu Anda apa yang terjadi tidak pernah sampai ke pemanggil. Dalam panduan timeout request dari Google, Anda disarankan menyetel batas di atas waktu eksekusi yang Anda perkirakan, dan juga memeriksa timeout milik framework Anda sendiri. Timeout gunicorn itulah timeout framework yang dimaksud.
Langkah 1: tulis layanannya
Aplikasi Flask kecil dengan satu route, disimpan sebagai main.py. Buat klien sekali saja, saat import. Klien hanya menyimpan pengaturannya, jadi setiap thread di worker bisa memakainya bersama.
# 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 awayMembaca host dengan lookup ketat alih-alih memakai nilai default memang disengaja. Jika variabelnya tidak ada, import gagal, gunicorn tidak bisa menjalankan worker, dan revisi baru tidak pernah mulai melayani. Itu kegagalan yang jauh lebih jelas daripada revisi yang ter-deploy dengan mulus lalu melempar NetworkException ke loopback pada request sungguhan yang pertama.
Langkah 2: deploy dengan entrypoint, timeout dan concurrency yang tepat
Setiap perbaikan dari tabel di atas yang dibutuhkan layanan captcha di Cloud Run masuk ke perintah deploy, jadi tidak ada satu pun yang berada di dalam kode.
# 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
Di Windows PowerShell, ganti setiap backslash di akhir baris dengan backtick. Berikut yang diubah oleh setiap baris:
- Baris entrypoint adalah perbaikan untuk gunicorn. Timeout 0 mematikan timeout worker gunicorn dan menyerahkan urusan waktu ke Cloud Run, dan delapan thread membuat satu instance bisa menjalankan delapan pemecahan sekaligus. Ini adalah pengaturan yang dipakai dokumentasi buildpack milik Google sendiri sebagai contoh, dan gunicorn harus dicantumkan di requirements.txt ketika Anda mengganti default. Port ditulis sebagai 8080, bukan sebagai variabel PORT, karena shell Anda sendiri akan mengekspansi variabel itu menjadi kosong sebelum gcloud sempat melihatnya. Cloud Run mengirim lalu lintas ke 8080 kecuali Anda menentukan lain.
- Timeout request 400 detik berada di atas 300 detik milik klien dengan ruang yang cukup lega. Penghitung waktu klien baru mulai setelah job dikirim, dan pada instance baru di balik Cloud NAT, koneksi pertama itu bisa memakan waktu satu menit.
- Baris concurrency mencegah request mengantre di tempat yang tidak bisa dilihat Cloud Run. Layanan yang di-deploy dengan gcloud menerima hingga 80 request bersamaan per vCPU secara default. Dengan delapan thread, request kesembilan sampai kedelapan puluh mengantre di dalam gunicorn sementara penghitung waktu Cloud Run sudah berjalan, padahal instance tampak masih punya banyak ruang. Menyamakan concurrency dengan jumlah thread membuat Cloud Run memulai instance lain sebagai gantinya.
- Variabel lingkungan membawa alamat mode Server dari mesin CapSkip Anda beserta API key-nya, yang dibaca main.py secara eksplisit. Setelah semuanya berjalan, Secret Manager dan flag set-secrets menjadi tempat yang lebih rapi untuk key tersebut.
- Baris no-allow-unauthenticated menjaga endpoint tetap privat, sehingga hanya pemanggil yang punya izin untuk memanggil layanan itu yang bisa memakai waktu pemecah CAPTCHA Anda melaluinya.
Langkah 3: beri layanan satu IP keluar statis
Secara default, layanan Cloud Run menjangkau internet dari pool alamat Google yang dinamis, jadi firewall Anda tidak punya satu IP pun untuk diizinkan. Solusi yang terdokumentasi adalah mengirim egress layanan melalui jaringan VPC dengan gateway Cloud NAT yang memegang alamat statis yang sudah dicadangkan.
# 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
Flag terakhir inilah yang sering terlewat. Pengaturan egress default adalah private-ranges-only, yang hanya mengirim lalu lintas untuk alamat privat melalui VPC. Pemecah CAPTCHA Anda berada di IP publik, jadi tanpa all-traffic pemecahan tetap keluar dari pool dinamis dan alamat NAT tidak pernah muncul di log firewall Anda. Google menjelaskan seluruh penyiapannya di panduan IP keluar statis mereka.
Setelah alamat yang dicadangkan siap, izinkan alamat itu di firewall mesin Windows untuk port pemecah CAPTCHA, dan tidak ada yang lain. Tunnel Cloud VPN ke jaringan Anda sendiri melakukan tugas yang sama tanpa membuka port ke internet sama sekali. Bagaimanapun caranya, mode Server tetap perangkat keras Anda dan tetap tanpa kuota. Mode ini hanya mengubah tempat pemecah CAPTCHA mendengarkan, supaya sesuatu selain desktop yang sama bisa memanggilnya.
Mengapa menyelesaikan pemecahan setelah respons dikirim tidak berhasil
Solusi sementara yang menggoda untuk pemecahan yang lambat adalah langsung menjawab pemanggil lalu menyelesaikan pekerjaan di background thread. Pada penagihan berbasis request yang menjadi default Cloud Run, CPU hanya dialokasikan selama instance sedang memproses request. Thread yang masih melakukan polling setelah respons terkirim hanya mendapat CPU selama ada request lain yang sedang diproses di instance itu, dan instance yang menganggur bisa dimatikan kapan saja. Pekerjaannya macet atau hilang, dan tidak ada yang memberi tahu Anda mana yang terjadi.
Jika pemanggil memang tidak bisa menunggu, pindahkan pekerjaannya ke job Cloud Run. Job tidak memiliki request HTTP yang menunggunya, setiap task bisa berjalan 10 menit secara default dan hingga 168 jam, dan job menerima flag jaringan dan egress yang sama seperti layanan, sehingga job yang diberi flag itu keluar melalui alamat NAT yang sama.
Contoh lengkap yang berfungsi
Layanan yang sama, sekarang memberi tahu pemanggil kegagalan mana yang layak dicoba ulang.
# 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"])Kode status dipilih agar pemanggil bisa bertindak tanpa membaca pesannya. Kode 503 berarti pemecah CAPTCHA tidak bisa dijangkau untuk menerima job, dan percobaan ulang mungkin berhasil. Kode 504 dari kode Anda sendiri, yang tiba tepat setelah 300 detik, berarti tidak ada jawaban yang kembali tepat waktu, karena pemecah CAPTCHA lambat atau terputus setelah menerima job. Kode 422 berarti CapSkip menolak job tersebut, biasanya karena sitekey, URL atau API key-nya salah, dan percobaan ulang begitu saja cenderung mendapat jawaban yang sama. Keempat exception diturunkan dari CapSkipError jika Anda lebih suka menangkap satu hal saja.
Siapa pun yang menerima token harus langsung memakainya. Token reCAPTCHA berlaku sekitar dua menit, dan rincian jendela waktu itu ada di panduan pemecah reCAPTCHA v2. Pola yang sama berlaku untuk tipe lain, yang menerima argumen berbeda dan mengembalikan field berbeda; setiap metode yang disediakan paket Python tercantum di halaman pemecah CAPTCHA Python.
Kesalahan umum dan artinya
| Apa yang Anda lihat | Penyebab | Perbaiki |
|---|---|---|
| WORKER TIMEOUT di log dan error server sekitar 30 detik setelah pemecahan dimulai | Entrypoint default buildpack, yang menjalankan gunicorn dengan timeout worker 30 detik | Setel GOOGLE_ENTRYPOINT dengan timeout 0 dan beberapa thread |
| 504 pada detik ke-300, dan baris log dari pemecahan yang sama masih terus masuk setelahnya | Timeout request Cloud Run sama dengan timeout polling klien, dan penghitung waktu Cloud Run mulai lebih dulu | Deploy dengan timeout 400 detik |
| Latensi yang naik saat beban tinggi sementara jumlah instance tetap datar | Cloud Run mengirim hingga 80 request per vCPU ke server yang jumlah thread-nya jauh lebih sedikit | Setel concurrency agar sama dengan jumlah thread gunicorn |
| NetworkException yang berbunyi bad response: 404 | Klien dibuat tanpa host, jadi ia memanggil 127.0.0.1 di port 8080, yang di dalam container adalah layanan Anda sendiri. Klien tidak membaca CAPSKIP_HOST dengan sendirinya | Berikan host dari variabel lingkungan, seperti yang dilakukan main.py |
| Revisi baru tidak pernah siap setelah deploy | CAPSKIP_HOST tidak disetel, atau gunicorn tidak ada di requirements.txt | Setel variabel itu pada layanan, dan cantumkan gunicorn bersama dependensi Anda yang lain |
| Pemecahan berhasil dari laptop Anda tetapi gagal dari layanan | Alamat rumah Anda diizinkan lewat firewall sedangkan alamat Google tidak | Beri layanan IP keluar statis dan izinkan IP itu |
| Pemecahan hang sampai timeout setelah Anda menghubungkan VPC | Semua lalu lintas melewati VPC yang tidak punya gateway Cloud NAT, sehingga tidak ada jalan keluar | Buat NAT gateway di subnet layanan tersebut |
| Log firewall Anda menampilkan alamat Google yang tidak Anda cadangkan | Pengaturan egress masih private-ranges-only, jadi lalu lintas publik tidak melalui NAT | Perbarui layanan dengan egress all-traffic |
FAQ
Bisakah CapSkip sendiri berjalan di Cloud Run?
Tidak, dan memang tidak perlu. CapSkip adalah aplikasi Windows yang berjalan di perangkat keras milik Anda, dan paket Python di dalam container Anda hanyalah klien tipis untuknya melalui HTTP. Nyalakan mode Server, berikan alamatnya ke layanan, dan layanan itu memanggil pemecah CAPTCHA persis seperti skrip di meja yang sama. Pemecahannya tetap berlangsung di mesin Anda, dan itu juga sebabnya tidak ada yang menghitung berapa banyak CAPTCHA yang Anda pecahkan.
Mana yang lebih cocok untuk memecahkan CAPTCHA, layanan atau job?
Layanan, ketika ada sesuatu yang menunggu token dan akan langsung memakainya, seperti scraper yang memanggil endpoint Anda di tengah crawling. Job, ketika pekerjaannya berupa batch yang tidak ditunggu siapa pun, karena job sama sekali tidak punya timeout request dan timeout task-nya jauh melampaui satu jam. Bagaimanapun, token harus dipakai oleh proses yang memegangnya, dalam beberapa menit. Job yang memecahkan seratus CAPTCHA lalu menyimpan tokennya di suatu tempat untuk dipakai nanti telah melakukan seratus pemecahan dengan sia-sia. Pertimbangan yang sama muncul di AWS, dan panduan AWS Lambda membahasnya dengan queue di depannya.
Bagaimana caranya agar hanya layanan saya yang bisa menjangkau pemecah CAPTCHA?
Rutekan seluruh egress layanan melalui VPC dengan gateway Cloud NAT yang memegang alamat yang dicadangkan, lalu izinkan satu alamat itu di firewall Anda untuk port pemecah CAPTCHA. Tunnel Cloud VPN menjangkau pemecah CAPTCHA di jaringan Anda sendiri tanpa membuka port ke internet sama sekali. Jaga port tetap tertutup bagi yang lain, dan perlakukan API key sebagai kunci kedua, bukan sebagai satu-satunya kunci.
Apakah request yang menunggu pemecahan memakan biaya besar?
Cloud Run menagih instance selama instance itu memproses request, dan request yang sedang menunggu jaringan ikut dihitung. Concurrency-lah yang menjaga biaya itu tetap wajar. Delapan pemecahan yang menunggu berdampingan di satu instance hanya memakai waktu tagihan senilai satu instance, sedangkan satu request per instance akan menyalakan delapan instance. Itulah separuh lainnya dari alasan memberi gunicorn delapan thread alih-alih satu. Pemecahannya sendiri tidak memakan biaya per CAPTCHA, karena berjalan di mesin Anda sendiri.
Versi singkatnya
Deploy captcha di Cloud Run membutuhkan empat pengaturan, dan tiga di antaranya berada di perintah deploy, bukan di kode Anda. Ganti entrypoint default buildpack agar gunicorn berhenti mematikan worker pada detik ke-30, dan beri ia thread. Setel timeout request di atas 300 detik milik klien agar error Anda sendiri tiba sebelum 504 dari Cloud Run. Samakan concurrency dengan jumlah thread itu agar beban tambahan memulai instance baru, bukan mengantre. Terakhir, berikan sendiri alamat mode Server ke klien, lalu rutekan layanan melalui VPC dengan Cloud NAT dan egress all-traffic, sehingga firewall Anda hanya punya tepat satu alamat untuk diizinkan.
Satu poin terakhir tentang sisi ekonominya, karena itulah yang membuat kode percobaan ulang di atas nyaman untuk ditindaklanjuti. Request yang diulang hanya memakan beberapa detik Cloud Run dan tidak lebih: pemecah captcha itu sendiri berjalan di perangkat keras yang sudah Anda bayar, jadi pemecahan yang gagal tidak pernah menambah biaya per CAPTCHA dari siapa pun.
