Google Cloud Run पर बिना 504 के कैप्चा कैसे हल करें

cloud run captcha - How to Solve CAPTCHAs on Google Cloud Run Without a 504

Cloud Run पर कैप्चा का solve तीन जगह दम तोड़ सकता है, और इनमें से दो में आपके कोड को कभी कोई error नहीं दिखता। Python buildpack आपके ऐप को gunicorn की डिफ़ॉल्ट सेटिंग्स के साथ शुरू करता है, और gunicorn उस worker को मार देता है जो 30 सेकंड तक व्यस्त रहे, और reCAPTCHA का solve अक्सर इतनी देर लेता है। Cloud Run का अपना request timeout डिफ़ॉल्ट रूप से 300 सेकंड है, ठीक उतना ही जितना SDK का reCAPTCHA polling timeout, इसलिए उस दौड़ में 504 हमेशा जीतता है। और कंटेनर के अंदर 127.0.0.1 खुद कंटेनर ही है। CapSkip आपकी अपनी Windows मशीन पर चलता है, और सर्विस सिर्फ़ क्लाइंट है। यह रहा वह डिप्लॉय जो तीनों अड़चनें पार कर लेता है।

आपको क्या चाहिए

  • आपके नियंत्रण वाली किसी Windows मशीन पर चलता हुआ CapSkip। यह एक डेस्कटॉप ऐप्लिकेशन है और Cloud Run के अंदर नहीं चलता। सर्विस इसे HTTP पर कॉल करती है, इससे ज़्यादा कुछ नहीं।
  • Server मोड चालू। Local मोड 127.0.0.1 पर सिर्फ़ उसी डिवाइस को जवाब देता है, जो Google के नेटवर्क में चल रहे किसी कंटेनर के किसी काम का नहीं। Server मोड आपके नेटवर्क पते या पब्लिक IP पर सुनता है ताकि सर्विस उसी API से उस तक पहुँच सके, और दोनों यहाँ मिलते हैं: कनेक्शन सेटिंग्स। स्टैटिक पब्लिक IP की सलाह दी जाती है, साथ में उस एक पते के लिए फ़ायरवॉल नियम जहाँ से Google आएगा।
  • source से डिप्लॉय की गई एक Python सर्विस, जिसकी requirements.txt में capskip, flask और gunicorn लिखे हों। CapSkip पैकेज को Python 3.10 या उससे नया वर्ज़न चाहिए।
  • gcloud CLI, और स्टेप 3 के लिए एक VPC नेटवर्क जिसमें सर्विस के region में एक subnet हो।

Cloud Run पर कैप्चा डिप्लॉय का नतीजा तीन timeout क्यों तय करते हैं

हर solve के ऊपर तीन घड़ियाँ चलती हैं, और डिफ़ॉल्ट डिप्लॉय पर गलत वाली पहले बजती है।

घड़ीडिफ़ॉल्टबजने पर क्या होता है
gunicorn worker timeout, buildpack के डिफ़ॉल्ट entrypoint से30 सेकंडsolve के बीच में ही worker मार दिया जाता है और फिर से शुरू होता है, और caller को server error मिलता है
Cloud Run का request timeout300 सेकंड, 3600 तक बढ़ाया जा सकता हैcaller को 504 मिलता है, जबकि कंटेनर request पर काम करता रहता है
CapSkip का reCAPTCHA polling timeout300 सेकंडक्लाइंट एक TimeoutException उठाता है जिसे आपका कोड सँभाल सकता है

शुरुआत gunicorn से करें। Python source डिप्लॉय के लिए buildpack का डिफ़ॉल्ट entrypoint port 8080 से बँधा gunicorn है, और बाकी कुछ सेट नहीं होता, यानी एक worker, एक thread, और gunicorn का 30 सेकंड वाला worker timeout। Gunicorn उस worker को मारकर फिर से शुरू कर देता है जो इस सीमा से ज़्यादा देर चुप रहे, और एक धीमे solve का इंतज़ार कर रहा sync worker चुप ही रहता है। इसलिए 40 सेकंड लेने वाला reCAPTCHA कभी लौटता ही नहीं।

फिर आती है बराबरी की बात। Cloud Run 300 सेकंड पर कनेक्शन बंद करके 504 लौटा देता है, और Google के docs बताते हैं कि instance बंद नहीं किया जाता, इसलिए आपका कोड एक ऐसी request पर काम करता रह सकता है जिसका कोई इंतज़ार नहीं कर रहा। SDK का timeout भी 300 सेकंड है, लेकिन उसकी घड़ी बाद में शुरू होती है, request आने और job सबमिट होने के बाद। Cloud Run की घड़ी हमेशा पहले बजती है, और जो TimeoutException आपको बता देता कि हुआ क्या, वह कभी caller तक पहुँचता ही नहीं। Google की request timeout गाइड कहती है कि सीमा को अपने अपेक्षित execution समय से ऊपर रखें, और अपने framework का अपना timeout भी जाँचें। Gunicorn ही वह framework timeout है।

स्टेप 1: सर्विस लिखें

एक route वाला छोटा Flask ऐप, main.py नाम से सेव किया हुआ। क्लाइंट को import के समय एक ही बार बनाएँ। उसमें सिर्फ़ उसकी सेटिंग्स होती हैं, इसलिए worker का हर thread उसे साझा कर सकता है।

# 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 away

host को डिफ़ॉल्ट के बजाय सख़्त lookup से पढ़ना जानबूझकर है। अगर वेरिएबल गायब है, तो import फेल होता है, gunicorn कोई worker बूट नहीं कर पाता, और नया revision कभी ट्रैफ़िक सर्व करना शुरू नहीं करता। यह उस revision से कहीं साफ़ नाकामी है जो आराम से डिप्लॉय हो जाए और फिर पहली असली request पर loopback के ख़िलाफ़ NetworkException फेंके।

स्टेप 2: सही entrypoint, timeout और concurrency के साथ डिप्लॉय करें

ऊपर की तालिका के हिसाब से Cloud Run कैप्चा सर्विस को जो भी सुधार चाहिए, वह सब डिप्लॉय कमांड में जाता है, इसलिए उसमें से कुछ भी कोड में नहीं रहता।

# 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 पर, हर लाइन के आख़िर वाले backslash को backtick से बदल दें। हर लाइन यह बदलती है:

  • entrypoint वाली लाइन gunicorn का इलाज है। 0 का timeout gunicorn के worker timeout को बंद कर देता है और समय का हिसाब Cloud Run पर छोड़ देता है, और आठ threads एक instance को एक साथ आठ solve चलाने देते हैं। Google के अपने buildpack docs उदाहरण के तौर पर यही सेटिंग्स इस्तेमाल करते हैं, और डिफ़ॉल्ट को override करने पर gunicorn को requirements.txt में लिखना ज़रूरी है। port को PORT वेरिएबल के बजाय 8080 लिखा गया है, क्योंकि आपका अपना shell उस वेरिएबल को gcloud के देखने से पहले ही expand करके खाली कर देगा। जब तक आप कुछ और न कहें, Cloud Run ट्रैफ़िक 8080 पर भेजता है।
  • 400 सेकंड का request timeout क्लाइंट के 300 से काफ़ी गुंजाइश के साथ ऊपर है। क्लाइंट की घड़ी तभी शुरू होती है जब job सबमिट हो जाता है, और Cloud NAT के पीछे बैठे नए instance पर वह पहला कनेक्शन एक मिनट ले सकता है।
  • concurrency वाली लाइन requests को वहाँ कतार में लगने से रोकती है जहाँ Cloud Run उन्हें देख नहीं सकता। gcloud से डिप्लॉय की गई सर्विस डिफ़ॉल्ट रूप से प्रति vCPU 80 तक concurrent requests स्वीकार करती है। आठ threads के साथ, नौवीं से अस्सीवीं तक की requests gunicorn के अंदर कतार में लगती हैं, और तब तक Cloud Run की घड़ी चल रही होती है, जबकि instance ऐसा दिखता है मानो उसके पास ख़ूब जगह बची है। concurrency को threads के बराबर रखने पर Cloud Run इसके बजाय एक और instance शुरू कर देता है।
  • एनवायरनमेंट वेरिएबल आपकी CapSkip मशीन का Server मोड पता और उसकी API key ले जाते हैं, जिन्हें main.py स्पष्ट रूप से पढ़ता है। यह चल जाए, तो key के लिए Secret Manager और set-secrets flag ज़्यादा सुथरी जगह हैं।
  • no-allow-unauthenticated वाली लाइन endpoint को प्राइवेट रखती है, ताकि सिर्फ़ वही callers इसके ज़रिए आपके सॉल्वर का समय खर्च कर सकें जिन्हें सर्विस invoke करने की अनुमति है।

स्टेप 3: सर्विस को एक स्टैटिक outbound IP दें

डिफ़ॉल्ट रूप से Cloud Run सर्विस Google के पतों के एक बदलते पूल से इंटरनेट तक पहुँचती है, इसलिए आपके फ़ायरवॉल के पास अनुमति देने को कोई एक IP नहीं होता। दस्तावेज़ में बताया गया इलाज यह है कि सर्विस का egress एक VPC नेटवर्क से होकर भेजा जाए, जिसमें एक Cloud NAT gateway हो जो एक आरक्षित स्टैटिक पता रखता है।

# 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 वही है जिसे लोग भूल जाते हैं। डिफ़ॉल्ट egress सेटिंग private-ranges-only है, जो सिर्फ़ प्राइवेट पतों वाले ट्रैफ़िक को VPC से होकर भेजती है। आपका सॉल्वर पब्लिक IP पर बैठा है, इसलिए all-traffic के बिना solve फिर भी बदलते पूल से ही निकलते हैं और NAT का पता आपके फ़ायरवॉल लॉग में कभी दिखता ही नहीं। Google पूरे सेटअप को क़दम-दर-क़दम यहाँ समझाता है: स्टैटिक outbound IP पर उसकी गाइड.

आरक्षित पता लग जाने के बाद, Windows मशीन के फ़ायरवॉल पर सॉल्वर के पोर्ट के लिए उसे अनुमति दें, और किसी और को नहीं। आपके अपने नेटवर्क तक Cloud VPN टनल यही काम पोर्ट को इंटरनेट के सामने खोले बिना कर देती है। दोनों ही सूरत में, Server मोड अब भी आपका अपना हार्डवेयर है और अब भी बिना मीटर वाला है। वह सिर्फ़ यह बदलता है कि सॉल्वर कहाँ सुनता है, ताकि उसी डेस्कटॉप के अलावा कोई और भी उसे कॉल कर सके।

response के बाद solve पूरा करना काम क्यों नहीं करता

धीमे solve के लिए लुभावना जुगाड़ यह है कि caller को तुरंत जवाब दे दें और काम background thread पर पूरा करें। Cloud Run की डिफ़ॉल्ट request-based billing में CPU सिर्फ़ तभी मिलता है जब instance requests पर काम कर रहा हो। response जाने के बाद भी polling कर रहे thread को CPU सिर्फ़ तब मिलता है जब उसी instance पर कोई दूसरी request चल रही हो, और खाली instance कभी भी बंद किया जा सकता है। काम अटक जाता है या गायब हो जाता है, और कुछ नहीं बताता कि दोनों में से क्या हुआ।

अगर caller सच में इंतज़ार नहीं कर सकता, तो काम को इसके बजाय Cloud Run job पर ले जाएँ। job पर कोई HTTP request इंतज़ार नहीं कर रही होती, हर task डिफ़ॉल्ट रूप से 10 मिनट और ज़्यादा से ज़्यादा 168 घंटे तक चल सकता है, और jobs भी सर्विस वाले ही network और egress flags लेते हैं, इसलिए वे flags दिया गया job उसी NAT पते से बाहर निकलता है।

पूरा चलने वाला उदाहरण

वही सर्विस, जो अब caller को बताती है कि कौन सी नाकामियाँ दोबारा आज़माने लायक हैं।

# 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"])

status codes इस तरह चुने गए हैं कि caller संदेश पढ़े बिना उन पर कार्रवाई कर सके। 503 का मतलब है कि job लेने के लिए सॉल्वर तक पहुँचा नहीं जा सका, और दोबारा कोशिश काम कर सकती है। आपके अपने कोड से आया 504, जो 300 सेकंड के ठीक बाद पहुँचता है, बताता है कि समय पर कोई जवाब नहीं लौटा, क्योंकि सॉल्वर धीमा था या job लेने के बाद ग़ायब हो गया। 422 का मतलब है कि CapSkip ने job मना कर दिया, आमतौर पर इसलिए कि sitekey, URL या API key गलत है, और सीधी दोबारा कोशिश से अक्सर वही जवाब मिलता है। अगर आप एक ही चीज़ catch करना चाहें, तो चारों exceptions CapSkipError से निकलते हैं।

token जिसे भी मिले, वह उसे तुरंत इस्तेमाल करे। reCAPTCHA token करीब दो मिनट चलता है, और उस विंडो का पूरा ब्योरा यहाँ है: reCAPTCHA v2 सॉल्वर गाइड। यही ढाँचा बाकी टाइप के लिए भी काम करता है, जो अलग आर्गुमेंट लेते हैं और अलग फ़ील्ड लौटाते हैं; Python पैकेज जो भी method देता है, वे सब यहाँ सूचीबद्ध हैं: Python कैप्चा सॉल्वर पेज.

आम errors और उनका मतलब

आप जो देखते हैंकारणफिक्स
लॉग में WORKER TIMEOUT, और solve शुरू होने के करीब 30 सेकंड बाद server errorbuildpack का डिफ़ॉल्ट entrypoint, जो gunicorn को उसके 30 सेकंड वाले worker timeout के साथ चलाता हैGOOGLE_ENTRYPOINT को 0 के timeout और कुछ threads के साथ सेट करें
300 सेकंड पर 504, और उसी solve की लॉग लाइनें उसके बाद भी आती रहती हैंCloud Run का request timeout क्लाइंट के polling timeout के बराबर है, और Cloud Run की घड़ी पहले शुरू हुई थी400 सेकंड के timeout के साथ डिप्लॉय करें
लोड में latency बढ़ती जाती है जबकि instances की गिनती वहीं की वहीं रहती हैCloud Run प्रति vCPU 80 तक requests ऐसे सर्वर को भेजता है जिसके पास कहीं कम threads हैंconcurrency को gunicorn के threads की संख्या के बराबर सेट करें
एक NetworkException जिसमें bad response: 404 लिखा होता हैक्लाइंट बिना host के बनाया गया था, इसलिए उसने port 8080 पर 127.0.0.1 को कॉल किया, जो कंटेनर के अंदर आपकी अपनी सर्विस है। वह CAPSKIP_HOST खुद नहीं पढ़ताhost को environment से पास करें, जैसा main.py करता है
डिप्लॉय के बाद नया revision कभी तैयार नहीं होताCAPSKIP_HOST सेट नहीं है, या requirements.txt में gunicorn नहीं हैसर्विस पर वेरिएबल सेट करें, और gunicorn को अपनी बाकी dependencies के साथ लिखें
ऐसे solve जो आपके लैपटॉप से चलते हैं और सर्विस से फेल होते हैंआपके घर के पते को फ़ायरवॉल से गुज़रने की अनुमति है और Google के पतों को नहींसर्विस को एक स्टैटिक outbound IP दें और उसी एक को अनुमति दें
VPC जोड़ने के बाद timeout तक अटके रहने वाले solveसारा ट्रैफ़िक ऐसे VPC से होकर जाता है जिसमें कोई Cloud NAT gateway नहीं है, इसलिए उसके पास बाहर निकलने का कोई रास्ता नहींसर्विस के subnet पर NAT gateway बनाएँ
आपके फ़ायरवॉल लॉग में ऐसा Google पता दिखता है जिसे आपने आरक्षित नहीं कियाegress सेटिंग अब भी private-ranges-only है, इसलिए पब्लिक ट्रैफ़िक NAT को छोड़कर निकल जाता हैसर्विस को all-traffic egress के साथ अपडेट करें

FAQ

क्या CapSkip खुद Cloud Run पर चल सकता है?

नहीं, और इसकी ज़रूरत भी नहीं। CapSkip एक Windows ऐप्लिकेशन है जो आपके अपने हार्डवेयर पर चलता है, और आपके कंटेनर में मौजूद Python पैकेज HTTP पर उसका एक पतला क्लाइंट भर है। Server मोड चालू करें, सर्विस को वह पता दें, और वह सॉल्वर को ठीक वैसे ही कॉल करती है जैसे उसी डेस्क पर रखी कोई script करती। हल करने का काम आपकी मशीन पर ही रहता है, और यही वजह है कि आप कितने कैप्चा हल करते हैं, इसका मीटर कोई नहीं रखता।

कैप्चा हल करने के लिए सर्विस बेहतर है या job?

सर्विस, जब कोई चीज़ token का इंतज़ार कर रही हो और उसे तुरंत इस्तेमाल करेगी, जैसे कोई स्क्रैपर जो crawl के बीच में आपके endpoint को कॉल करता है। job, जब काम एक ऐसा batch हो जिसका कोई इंतज़ार नहीं कर रहा, क्योंकि job पर कोई request timeout होता ही नहीं और उसका task timeout एक घंटे से कहीं आगे तक जाता है। दोनों ही सूरत में token को वही प्रोसेस इस्तेमाल करे जिसके पास वह है, और वह भी दो-एक मिनट के भीतर। जो job सौ कैप्चा हल करके tokens को बाद के लिए कहीं लिख देता है, उसने सौ solve बेकार किए। यही समझौता AWS पर भी सामने आता है, और AWS Lambda गाइड आगे एक queue रखकर इसे सुलझाती है।

मैं सिर्फ़ अपनी सर्विस को सॉल्वर तक कैसे पहुँचने दूँ?

सर्विस का सारा egress एक ऐसे VPC से होकर रूट करें जिसमें आरक्षित पता रखने वाला Cloud NAT gateway हो, फिर अपने फ़ायरवॉल पर सॉल्वर के पोर्ट के लिए उसी एक पते को अनुमति दें। Cloud VPN टनल आपके अपने नेटवर्क पर बैठे सॉल्वर तक पोर्ट को इंटरनेट के सामने खोले बिना पहुँच जाती है। पोर्ट को बाकी सबके लिए बंद रखें, और API key को इकलौता नहीं, दूसरा ताला मानें।

क्या solve का इंतज़ार करती request की लागत ज़्यादा होती है?

Cloud Run किसी instance का बिल तब बनाता है जब वह requests पर काम कर रहा हो, और नेटवर्क का इंतज़ार करती request भी इसमें गिनी जाती है। concurrency ही इसे वाजिब रखती है। एक instance पर साथ-साथ इंतज़ार करते आठ solve सिर्फ़ एक instance जितना बिल वाला समय लेते हैं, जबकि प्रति instance एक request होने पर आठ instances शुरू हो जाते। gunicorn को एक के बजाय आठ threads देने की दलील का यह दूसरा हिस्सा है। solve की खुद प्रति कैप्चा कोई कीमत नहीं, क्योंकि वह आपकी अपनी मशीन पर चलता है।

संक्षेप में

Cloud Run पर कैप्चा डिप्लॉय को चार सेटिंग्स चाहिए, और उनमें से तीन आपके कोड में नहीं, डिप्लॉय कमांड में रहती हैं। buildpack का डिफ़ॉल्ट entrypoint बदलें ताकि gunicorn 30 सेकंड पर workers को मारना बंद करे, और उसे threads दें। request timeout को क्लाइंट के 300 सेकंड से ऊपर रखें ताकि Cloud Run के 504 से पहले आपका अपना error पहुँचे। concurrency को उन threads के बराबर रखें ताकि अतिरिक्त लोड कतार में लगने के बजाय नए instances शुरू करे। और Server मोड का पता क्लाइंट को खुद पास करें, फिर सर्विस को Cloud NAT और all-traffic egress वाले VPC से होकर रूट करें, ताकि आपके फ़ायरवॉल के पास अनुमति देने को ठीक एक पता हो।

अर्थशास्त्र पर एक आख़िरी बात, क्योंकि यही ऊपर के retry codes पर बेझिझक कार्रवाई करने देती है। दोबारा भेजी गई request आपको Cloud Run के कुछ सेकंड की पड़ती है, इसके अलावा कुछ नहीं: कैप्चा सॉल्वर खुद उसी हार्डवेयर पर चलता है जिसके पैसे आप पहले ही दे चुके हैं, इसलिए नाकाम solve कभी किसी की तरफ़ से प्रति-कैप्चा शुल्क नहीं जोड़ता।