BullMQ Worker’ında CAPTCHA Nasıl Çözülür (Node.js)

bullmq captcha - How to Solve CAPTCHAs in a BullMQ Worker (Node.js)

Bir BullMQ captcha worker’ı ilk denemenizde doğru çalışır, sonra iki sessiz noktada hayal kırıklığı yaratır. Aynı anda kaç işin çalışacağını belirleyen worker seçeneği varsayılan olarak 1’dir, bu yüzden biriken çözümler teker teker eritilir. Bir de processor event loop’u meşgul ederse BullMQ işin takılı kaldığına karar verir, işi başka bir worker’a devreder ve aynı CAPTCHA iki kez çözülür. Bunların hiçbiri bir hata değil. İkisi de varsayılan davranış ve ikisi de tek satırla değişiyor.

Neye ihtiyacınız var

  • Redis, ayrıca worker’larınızı çalıştıran projede kurulu BullMQ.
  • Bir Windows makinesinde çalışan CapSkip ve aynı projede Node istemcisi.
  • Sabit kodlanmak yerine iş verisiyle gelen sitekey ve sayfa URL’si; böylece tek kuyruk her formu karşılar.
  • Yatay olarak ölçeklenmeden önce kararlaştırılmış bir bağlantı modu; çünkü worker’lar genelde çözücüden daha fazla makineye dağılır.
# npm install capskip
npm install bullmq capskip

1. Adım: worker nerede çalışıyor ve bu hangi bağlantı modunu gerektiriyor

Önce bunu netleştirmekte fayda var, çünkü BullMQ’nun kendi tavsiyesi bir worker filosunu birçok farklı makinede çalıştırmaktır ve bunu yaptığınız anda loopback, dizüstünüzdeki anlamını kaybeder.

İki bağlantı modu vardır. Local, 127.0.0.1 adresine bağlanır ve yalnızca o cihaza yanıt verir; otomasyonunuz ile çözücü aynı makineyi paylaştığında doğru seçim budur. Server ise ağ adresinize veya genel IP adresinize bağlanır; böylece başka bir makine, bir VPS ya da barındırılan bir platform aynı Windows makinesine API üzerinden ulaşabilir. Server modu yalnızca çözücünün hangi adresi dinlediğini değiştirir. Donanım yine sizin donanımınızdır ve çözüm başına ücretlendirme yine yoktur. Her iki mod da şurada yer alır: bağlantı ayarları.

Worker süreci nerede çalışıyorHangi bağlantı modu
CapSkip makinesinde, tek süreçLocal modu. 127.0.0.1 gerçekten doğru
Kendi ağınızda bir worker filosuÇözücünün LAN adresiyle Server modu
Container içindeki ya da bir VPS üzerindeki worker’larUlaşılabilir bir adres ve bir güvenlik duvarı kuralıyla Server modu

VPS üzerindeki bir worker için sabit bir genel IP ayarlamakta fayda var; böylece adres, çalışan bir dağıtımın altından kaymaz. Adresi kaynak kodunda değil bir ortam değişkeninde tutun, çünkü dizüstünüz ile worker’larınız farklı değerler ister. İstemci hiçbir ortam değişkenini kendi başına okumaz; bu yüzden, aşağıdaki worker’ın yaptığı gibi, CAPSKIP_HOST değerini kendi kodunuzda okuyup istemciye iletin.

// npm install capskip
import { CapSkip } from "capskip";

// One client, shared by every job this worker handles.
// CAPSKIP_HOST is 127.0.0.1 locally and the solver's
// address on every other machine.
export const solver = new CapSkip({
  host: process.env.CAPSKIP_HOST ?? "127.0.0.1",
  port: 8080,
});

2. Adım: concurrency varsayılanı aynı anda tek iştir

Bir BullMQ worker’ı, aksini söylemediğiniz sürece aynı anda tek iş işler. Bu varsayılan, CPU işleri için makul; çözüm işleri için fena halde yanlıştır, çünkü bir çözüm neredeyse tamamen beklemekten ibarettir. BullMQ bunu doğrudan söylüyor: concurrency yalnızca worker’lar veritabanı çağrısı ya da harici bir HTTP servisine istek gibi asenkron işlemler yaptığında mümkündür. Bir çözücüyü HTTP üzerinden yoklamak tam olarak bu şekle uyar, dolayısıyla event loop baştan sona boşta kalır.

Hesabı bir kez yapmakta fayda var. Bir reCAPTCHA çözümü yirmi saniye sürüyorsa, varsayılandaki tek worker dakikada üç iş bitirir. Aynı worker, concurrency yirmi olduğunda aynı donanımda altmış iş bitirir; çünkü bu işlerin on dokuzu CPU için yarışmak yerine bir soket okumasında beklemektedir.

import { Worker } from "bullmq";
import { solver } from "./solver.js";

// A solve is an awaited HTTP call, so raising this costs
// almost no CPU. Size it for the solver machine and for
// what the target site will accept.
const worker = new Worker("captcha", async (job) => {
  const { sitekey, pageUrl } = job.data;
  const result = await solver.recaptcha(sitekey, pageUrl);
  return await submitForm(pageUrl, result.code);
}, { connection: { host: "127.0.0.1", port: 6379 }, concurrency: 20 });

Çözüm başına ücretlendirilen bir çözücüde bu sayı aslında bir harcama kontrolüdür; birçok örneğin onu çekingen bir değerde bırakmasının sebebi de budur. Burada ise soru, tek bir makinenin kapasitesi ve gönderim yaptığınız sitenin istekleri ne hızda kabul ettiğidir. İkincisi genelde daha dar olan sınırdır.

3. Adım: bir iş neden takılı kalır ve takılı kalmak neden iki kez çözüm demektir

İnsanların bir sabahını yiyen arıza budur, çünkü loglar kuyruk çalışıyormuş gibi görünür ama çözücünün kendi log kaydı tersini söyler.

Bir iş worker’a ulaştığında BullMQ onun üzerine bir kilit koyar, böylece başka hiçbir şey ona dokunamaz; worker da hâlâ çalıştığını kuyruğa söylemeye devam etmek zorundadır. Bu kilit lockDuration’dır, varsayılanı 30000 ms’dir ve worker onu bu sürenin yarısında bir yeniler. Ayrı bir tarama, stalledInterval, her 30000 ms’de bir çalışıp kimsenin yenilemediği kilitleri arar. Worker, zamanında yenileme yapamayacak kadar meşgulse iş stalled olarak işaretlenir, waiting durumuna geri alınır ve başka bir worker tarafından yeniden işlenir. İş, maxStalledCount’un izin verdiğinden daha fazla kez takılı kalırsa, ki bunun varsayılanı birdir, bu kez failed kümesine gider.

Yani takılı kalmış bir iş, başarısız olmuş bir iş değildir ve yeniden çalıştırma bir yeniden deneme değildir. Kuyruk, o işi tutan worker’ın öldüğünü haklı olarak varsaymaktadır. Çözücünüz tek bir CAPTCHA için iki gönderim görür ve yalnızca ikinci token kullanılır.

Worker ayarıVarsayılanNeyi belirler
"concurrency"1Bir worker’ın aynı anda kaç iş üstleneceğini
"lockDuration"30000 msKilidin yenilenmeden ne kadar yaşayacağını
"lockRenewTime"lockDuration’ın yarısıWorker’ın kilidi ne sıklıkla yenileyeceğini
"stalledInterval"30000 msYenilenmemiş kilitlerin ne sıklıkla toplanacağını
"maxStalledCount"1İş başarısız sayılmadan önce kaç kez yeniden çalıştırılacağını

Sebep her zaman aynıdır: processor CPU’yu tutmuştur. await edilen bir HTTP çağrısı bunu yapmaz, dolayısıyla istemcinin kendi yoklaması güvenlidir. Sonuç uç noktasında meşgul bekleyen, elle yazılmış bir döngü ise güvenli değildir; gönderimden önce büyük bir görüntüyü senkron biçimde çözmek de öyle. Yoklamayı istemciye bırakın: 250 ms’den başlar ve sabit bir aralıkta uyumak yerine geri çekilir. Gerçekten CPU’ya bağlı olan adımları ise processor’ın dışına ya da sandbox’lanmış bir processor içine taşıyın.

İlk hamle olarak kilit süresini uzatmak yanlıştır. Bu yalnızca belirtiyi tedavi eder; üstelik gerçekten ölmüş bir worker’daki uzun bir kilit, işi o pencere boyunca el değmemiş bırakır.

4. Adım: yeniden denemeler ve saklamamanız gereken token

BullMQ varsayılan olarak yeniden denemez. attempts seçeneği 1’dir; yani tek deneme, ardından failed kümesi. Bir çözüm için bu genelde doğrudur, çünkü buradaki hataların çoğu ya sonsuza dek aynı şekilde başarısız olacak bir parametre hatasıdır ya da artık değişmiş bir sitedir. Yeniden denemelerin hakkını verdiği yer, çözücüye kısa süreliğine ulaşamayan bir worker’dır; bu yüzden onlara bir backoff verin ve sayıyı küçük tutun.

await queue.add("signup", { sitekey, pageUrl }, {
  // Two tries, spaced out, for a solver that was
  // briefly unreachable. Not for a bad sitekey.
  attempts: 2,
  backoff: { type: "exponential", delay: 5000 },
  // Do not leave finished jobs sitting in Redis forever.
  removeOnComplete: { age: 3600, count: 1000 },
});

Bununla birlikte iki kural gelir. Bir tokeni denemeler arasında asla taşımayın: bir reCAPTCHA tokeni yaklaşık iki dakika geçerlidir, bu yüzden çözümü, tokeni gönderen denemenin içinde yapın. Bir tokeni önbelleğe almak aklınızdan geçiyorsa token sona erme rehberi sayfasını okuyun. Bir de tokeni işin dönüş değeri olarak döndürmeyin. BullMQ tamamlanan işleri varsayılan olarak saklar, yani o dönüş değeri Redis’e düşer ve orada kalır. Bunun yerine gönderimin sonucunu döndürün; sonradan bakmak isteyeceğiniz şey zaten odur.

Tam çalışan örnek

Tek üretici, tek worker ve çözümün, onu tüketen şeyle aynı processor içinde durması.

// npm install capskip
import { Queue, Worker } from "bullmq";
import { CapSkip, ValidationException } from "capskip";

const connection = { host: "127.0.0.1", port: 6379 };
const queue = new Queue("captcha", { connection });

const solver = new CapSkip({
  host: process.env.CAPSKIP_HOST ?? "127.0.0.1",
  port: 8080,
});

new Worker("captcha", async (job) => {
  const { sitekey, pageUrl, email } = job.data;
  try {
    // Solve and submit together. The token is short lived.
    const result = await solver.recaptcha(sitekey, pageUrl);
    const res = await postSignup(pageUrl, email, result.code);
    return { status: res.status };
  } catch (err) {
    // A bad sitekey fails the same way on every attempt.
    if (err instanceof ValidationException) {
      await job.discard();
    }
    throw err;
  }
}, { connection, concurrency: 20 });

Bu çağrı reCAPTCHA v2 içindir. Diğer türler de aynı biçimdedir: invisible veya enterprise değerini 1 yapın, ya da version değerini bir action ile birlikte v3 yapın, ya da bunun yerine turnstile veya geetest çağırın. Tüm yüzey için bakın: Node.js CAPTCHA çözücü sayfası.

Sık görülen hatalar ve anlamları

GördüğünüzNedenDüzeltme
Kuyruk, çözücünün gidebileceğinden çok daha yavaş boşalıyorWorker concurrency hâlâ varsayılan değeri olan birdeYükseltin. Çözüm, CPU değil await edilen I/O
Tek iş için saniyeler arayla iki çözüm loglanıyorKilit yenilenmedi, bu yüzden iş takılı kalmış sayıldıProcessor içinde event loop’u bloklamayı bırakın
Fırlatılan bir hata olmadan iş failed kümesine düşüyorİzin verilen en yüksek sayıdan fazla kez takılı kaldıAynı bloklayan iş. Sayacı değil, onu düzeltin
Dizüstünüzde çalışıyor, worker makinesinde NetworkExceptionO makinedeki 127.0.0.1 o makinenin kendisidirServer modu ve worker için CAPSKIP_HOST’u ayarlayın
Tokenler yalnızca ikinci denemede reddediliyorİlk denemeden kalan bir token taşındıÇözümü, gönderimi yapan denemenin içinde yapın
Bir ApiException içinde ERROR_WRONG_USER_KEYWorker’ın ortamında CAPSKIP_API_KEY tanımlı değilDeğişkeni, worker’ın çalıştığı yerde tanımlayın, sonra o worker’ı yeniden başlatın
Elle yazılmış bir yoklamadan CAPCHA_NOT_READYSonuç, tamamlanmadan önce okunduYoklamayı istemciye bırakın. Kendiliğinden geri çekilir

Son yanıt tam da göründüğü gibi yazılır; eksik harf bizim tarafımızdaki bir yazım hatası değildir, çünkü API gerçekten de onu bu şekilde döndürür. Tamamı şurada açıklanıyor: CAPCHA_NOT_READY rehberi.

FAQ

Worker’larım çözücüden farklı makinelerde çalışabilir mi?

Evet; tek süreci aştığınız anda normal kurulum zaten budur. CapSkip’i bağlantı ayarlarından Server moduna alın ki loopback yerine ağ adresinizi dinlesin, sonra her worker’ın ortamında CAPSKIP_HOST’u tanımlayın. Kendi ağınızda bu adres bir LAN adresidir ve hiçbir şeyin internete açılması gerekmez. Bir worker bir VPS üzerindeyse sabit bir genel IP kullanın ve yalnızca beklediğiniz adreslere izin veren bir güvenlik duvarı kuralı tanımlayın.

Concurrency değerini gerçekte kaça ayarlamalıyım?

Ondan başlayın ve iki şeyi izleyin: çözücü makinesini ve gönderim yaptığınız sitenin tepkisini. Bir çözüm await edilen bir I/O işlemi olduğu için sınır nadiren worker sürecinin kendisidir. Genelde sınır sitedir ve bunu size, Node’un yeri tükenmeden çok önce hız sınırı uygulayarak bildirir. Burada hiçbir şey bir bakiyenin arkasında sıraya girmez, yani bu sayı bir bütçe kararı değil bir kapasite kararıdır.

attempts değerini hiç ayarlamadığım hâlde aynı CAPTCHA neden iki kez çözüldü?

Çünkü o yeniden çalıştırma bir yeniden deneme değildi. Takılı kalmış bir iş, onu tutan worker’ın öldüğü varsayımıyla, attempts seçeneğinden bağımsız olarak yeniden kuyruğa alınır. Tetikleyen şey, zamanında yenilenmemiş bir kilittir; bu da processor await etmek yerine CPU’yu tuttuğunda olur. Processor içindeki senkron işi bulup taşıyın, mükerrer çözüm ortadan kalkar.

Çözüm, diğer işlerin çağırdığı ayrı bir kuyruk olmalı mı?

Genelde hayır. Ayırmak, tokenin bir kuyruk sınırını geçmesi ve üst iş devam ederken Redis’te beklemesi demektir; bu da süresi çoktan dolmuş bir tokeni harcamanın en hızlı yoludur. Çözümü ve onu tüketen şeyi aynı processor içinde tutun ve kimlik bilgisi yerine sonucu döndürün. Ayrı bir kuyruk yalnızca geri verdiği şey token değilse anlamlıdır.

Kısa özet

Worker concurrency değerini yükseltin, çünkü aynı anda tek iş varsayılanı, await edilen HTTP çağrılarından oluşan bir kuyruğu sebepsiz yere boğar. Processor’ı CPU’dan uzak tutun ki kilit yenilenmeye devam etsin; takılı kalmış bir iş başka bir worker tarafından yeniden çalıştırılır ve bunun bedelini boşa giden işlem hacmiyle ödersiniz. attempts değerini düşük bırakın ve bir tokeni asla denemeler arasında taşımayın. Bir worker başka bir yerde yaşamaya başladığı anda adresi CAPSKIP_HOST’a koyun ve çözücüyü Server moduna alın.

Bu concurrency sayısını seçmeden önce tartmaya değer bir nokta: CapSkip, zaten sahip olduğunuz donanımda çalışan bir sınırsız captcha çözücü olduğu ve zaten sahip olduğunuz donanımda çalıştığı için, bu sayıyı yükseltmek yalnızca tek bir makinenin kapasitesini harcar; çözüm başına hiçbir bedeli olmaz.